19
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23
from xml.etree import ElementTree as ET
25
from elementtree import ElementTree as ET
27
from simpletal.simpleTALUtils import HTMLStructureCleaner
41
34
log = logging.getLogger("loggerhead.controllers")
40
return '%d years' % (int(delta.days // 365.25),)
42
return '%d days' % delta.days
48
seg.append('%d days' % delta.days)
49
hrs = delta.seconds // 3600
50
mins = (delta.seconds % 3600) // 60
55
seg.append('%d hours' % hrs)
59
seg.append('1 minute')
61
seg.append('%d minutes' % mins)
63
seg.append('less than a minute')
68
now = datetime.datetime.now()
69
return timespan(now - timestamp) + ' ago'
44
72
def fix_year(year):
53
# date_day -- just the day
54
# date_time -- full date with time
56
# displaydate -- for use in sentences
57
# approximatedate -- for use in tables
59
# displaydate and approximatedate return an elementtree <span> Element
60
# with the full date in a tooltip.
64
return value.strftime('%Y-%m-%d')
69
return value.strftime('%Y-%m-%d %T')
74
def _displaydate(date):
75
delta = abs(datetime.datetime.now() - date)
76
if delta > datetime.timedelta(1, 0, 0):
77
# far in the past or future, display the date
78
return 'on ' + date_day(date)
79
return _approximatedate(date)
82
def _approximatedate(date):
83
delta = datetime.datetime.now() - date
84
if abs(delta) > datetime.timedelta(1, 0, 0):
85
# far in the past or future, display the date
87
future = delta < datetime.timedelta(0, 0, 0)
90
hours = delta.seconds / 3600
91
minutes = (delta.seconds - (3600*hours)) / 60
92
seconds = delta.seconds % 60
110
result += '%s %s' % (amount, unit)
116
def _wrap_with_date_time_title(date, formatted_date):
117
elem = ET.Element("span")
118
elem.text = formatted_date
119
elem.set("title", date_time(date))
123
def approximatedate(date):
124
#FIXME: Returns an object instead of a string
125
return _wrap_with_date_time_title(date, _approximatedate(date))
128
def displaydate(date):
129
return _wrap_with_date_time_title(date, _displaydate(date))
132
80
class Container (object):
134
82
Convert a dict into an object with attributes.
137
84
def __init__(self, _dict=None, **kw):
138
85
if _dict is not None:
139
86
for key, value in _dict.iteritems():
140
87
setattr(self, key, value)
141
88
for key, value in kw.iteritems():
142
89
setattr(self, key, value)
144
91
def __repr__(self):
146
93
for key, value in self.__dict__.iteritems():
147
if key.startswith('_') or (getattr(self.__dict__[key],
148
'__call__', None) is not None):
94
if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
150
96
out += '%r => %r, ' % (key, value)
101
def clean_revid(revid):
102
if revid == 'missing':
104
return sha.new(revid).hexdigest()
108
return ''.join([ '&#%d;' % ord(c) for c in text ])
155
111
def trunc(text, limit=10):
156
112
if len(text) <= limit:
158
114
return text[:limit] + '...'
118
if isinstance(s, unicode):
119
return s.encode('utf-8')
161
123
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
162
124
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
165
126
def hide_email(email):
167
128
try to obsure any email address in a bazaar committer's name.
181
142
return '%s at %s' % (username, domains[-2])
182
143
return '%s at %s' % (username, domains[0])
184
def hide_emails(emails):
186
try to obscure any email address in a list of bazaar committers' names.
190
result.append(hide_email(email))
146
def triple_factors(min_value=1):
152
yield n * factors[index]
154
if index >= len(factors):
159
def scan_range(pos, max, pagesize=1):
161
given a position in a maximum range, return a list of negative and positive
162
jump factors for an hgweb-style triple-factor geometric scan.
164
for example, with pos=20 and max=500, the range would be:
165
[ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
167
i admit this is a very strange way of jumping through revisions. i didn't
171
for n in triple_factors(pagesize + 1):
193
180
# only do this if unicode turns out to be a problem
194
181
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
196
# FIXME: get rid of this method; use fixed_width() and avoid XML().
199
183
def html_clean(s):
201
185
clean up a string for html display. expand any tabs, encode any html
203
187
in displaying monospace text.
205
189
s = cgi.escape(s.expandtabs())
190
# s = _BADCHARS_RE.sub(lambda x: '&#%d;' % (ord(x.group(0)),), s)
206
191
s = s.replace(' ', ' ')
210
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
215
CSS is stupid. In some cases we need to replace an empty value with
216
a non breaking space ( ). There has to be a better way of doing this.
218
return: the same value recieved if not empty, and a ' ' if it is.
224
elif isinstance(s, int):
230
s = s.decode('utf-8')
231
except UnicodeDecodeError:
232
s = s.decode('iso-8859-15')
235
HSC = HTMLStructureCleaner()
239
expand tabs and turn spaces into "non-breaking spaces", so browsers won't
242
if not isinstance(s, unicode):
243
# this kinda sucks. file contents are just binary data, and no
244
# encoding metadata is stored, so we need to guess. this is probably
245
# okay for most code, but for people using things like KOI-8, this
246
# will display gibberish. we have no way of detecting the correct
249
s = s.decode('utf-8')
250
except UnicodeDecodeError:
251
s = s.decode('iso-8859-15')
253
s = cgi.escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
255
return HSC.clean(s).replace('\n', '<br/>')
258
195
def fake_permissions(kind, executable):
259
196
# fake up unix-style permissions given only a "kind" and executable bit
260
197
if kind == 'directory':
324
269
elif divisor == GIG:
329
def fill_in_navigation(navigation):
274
def fill_in_navigation(history, navigation):
331
276
given a navigation block (used by the template for the page header), fill
332
277
in useful calculated values.
334
if navigation.revid in navigation.revid_list: # XXX is this always true?
335
navigation.position = navigation.revid_list.index(navigation.revid)
279
navigation.position = history.get_revid_sequence(navigation.revid_list, navigation.revid)
280
if navigation.position is None:
337
281
navigation.position = 0
338
282
navigation.count = len(navigation.revid_list)
339
283
navigation.page_position = navigation.position // navigation.pagesize + 1
340
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
341
- 1)) // navigation.pagesize
284
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
343
286
def get_offset(offset):
344
if (navigation.position + offset < 0) or (
345
navigation.position + offset > navigation.count - 1):
287
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
347
289
return navigation.revid_list[navigation.position + offset]
349
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
350
291
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
351
292
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
352
prev_page_revno = navigation.history.get_revno(
353
navigation.prev_page_revid)
354
next_page_revno = navigation.history.get_revno(
355
navigation.next_page_revid)
356
start_revno = navigation.history.get_revno(navigation.start_revid)
358
params = {'filter_file_id': navigation.filter_file_id}
294
params = { 'file_id': navigation.file_id }
359
295
if getattr(navigation, 'query', None) is not None:
360
296
params['q'] = navigation.query
362
if getattr(navigation, 'start_revid', None) is not None:
363
params['start_revid'] = start_revno
298
params['start_revid'] = navigation.start_revid
365
300
if navigation.prev_page_revid:
366
navigation.prev_page_url = navigation.branch.context_url(
367
[navigation.scan_url, prev_page_revno], **params)
301
navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **params)
368
302
if navigation.next_page_revid:
369
navigation.next_page_url = navigation.branch.context_url(
370
[navigation.scan_url, next_page_revno], **params)
373
def directory_breadcrumbs(path, is_root, view):
375
Generate breadcrumb information from the directory path given
377
The path given should be a path up to any branch that is currently being
381
path -- The path to convert into breadcrumbs
382
is_root -- Whether or not loggerhead is serving a branch at its root
383
view -- The type of view we are showing (files, changes etc)
385
# Is our root directory itself a branch?
393
# Create breadcrumb trail for the path leading up to the branch
395
'dir_name': "(root)",
400
dir_parts = path.strip('/').split('/')
401
for index, dir_name in enumerate(dir_parts):
403
'dir_name': dir_name,
404
'path': '/'.join(dir_parts[:index + 1]),
407
# If we are not in the directory view, the last crumb is a branch,
408
# so we need to specify a view
409
if view != 'directory':
410
breadcrumbs[-1]['suffix'] = '/' + view
414
def branch_breadcrumbs(path, inv, view):
416
Generate breadcrumb information from the branch path given
418
The path given should be a path that exists within a branch
421
path -- The path to convert into breadcrumbs
422
inv -- Inventory to get file information from
423
view -- The type of view we are showing (files, changes etc)
425
dir_parts = path.strip('/').split('/')
426
inner_breadcrumbs = []
427
for index, dir_name in enumerate(dir_parts):
428
inner_breadcrumbs.append({
429
'dir_name': dir_name,
430
'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
431
'suffix': '/' + view,
433
return inner_breadcrumbs
303
navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **params)
306
def log_exception(log):
307
for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
436
311
def decorator(unbound):
438
312
def new_decorator(f):
440
314
g.__name__ = f.__name__
447
321
return new_decorator
455
from loggerhead.lsprof import profile
458
ret, stats = profile(f, *a, **kw)
459
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
460
int((time.time() - z) * 1000)))
464
msec = int(now * 1000) % 1000
465
timestr = time.strftime('%Y%m%d%H%M%S',
466
time.localtime(now)) + ('%03d' % msec)
467
filename = f.__name__ + '-' + timestr + '.lsprof'
468
cPickle.dump(stats, open(filename, 'w'), 2)
473
# just thinking out loud here...
475
# so, when browsing around, there are 5 pieces of context, most optional:
477
# current location along the navigation path (while browsing)
478
# - starting revid (start_revid)
479
# the current beginning of navigation (navigation continues back to
480
# the original revision) -- this defines an 'alternate mainline'
481
# when the user navigates into a branch.
483
# the file being looked at
485
# if navigating the revisions that touched a file
487
# if navigating the revisions that matched a search query
489
# a previous revision to remember for future comparisons
491
# current revid is given on the url path. the rest are optional components
494
# other transient things can be set:
496
# to compare one revision to another, on /revision only
498
# for re-ordering an existing page by different sort
500
t_context = threading.local()
501
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
502
'compare_revid', 'sort')
505
def set_context(map):
506
t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
509
def get_context(**overrides):
511
Soon to be deprecated.
514
return a context map that may be overriden by specific values passed in,
515
but only contains keys from the list of valid context keys.
517
if 'clear' is set, only the 'remember' context value will be added, and
518
all other context will be omitted.
521
if overrides.get('clear', False):
522
map['remember'] = t_context.map.get('remember', None)
524
map.update(t_context.map)
525
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
526
map.update(overrides)
530
class Reloader(object):
532
This class wraps all paste.reloader logic. All methods are @classmethod.
535
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
538
def _turn_sigterm_into_systemexit(self):
540
Attempts to turn a SIGTERM exception into a SystemExit exception.
547
def handle_term(signo, frame):
549
signal.signal(signal.SIGTERM, handle_term)
552
def is_installed(self):
553
return os.environ.get(self._reloader_environ_key)
557
from paste import reloader
558
reloader.install(int(1))
561
def restart_with_reloader(self):
562
"""Based on restart_with_monitor from paste.script.serve."""
563
print 'Starting subprocess with file monitor'
565
args = [sys.executable] + sys.argv
566
new_environ = os.environ.copy()
567
new_environ[self._reloader_environ_key] = 'true'
324
# common threading-lock decorator
325
def with_lock(lockname, debug_name=None):
326
if debug_name is None:
327
debug_name = lockname
329
def _decorator(unbound):
330
def locked(self, *args, **kw):
331
#self.log.debug('-> %r lock %r', id(threading.currentThread()), debug_name)
332
getattr(self, lockname).acquire()
571
self._turn_sigterm_into_systemexit()
572
proc = subprocess.Popen(args, env=new_environ)
573
exit_code = proc.wait()
575
except KeyboardInterrupt:
576
print '^C caught in monitor process'
334
return unbound(self, *args, **kw)
580
and hasattr(os, 'kill')):
583
os.kill(proc.pid, signal.SIGTERM)
584
except (OSError, IOError):
336
getattr(self, lockname).release()
337
#self.log.debug('<- %r unlock %r', id(threading.currentThread()), debug_name)
587
# Reloader always exits with code 3; but if we are
588
# a monitor, any exit code will restart
591
print '-'*20, 'Restarting', '-'*20