19
16
# 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
log = logging.getLogger("loggerhead.controllers")
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))
25
return '%d days' % delta.days
31
seg.append('%d days' % delta.days)
32
hrs = delta.seconds // 3600
33
mins = (delta.seconds % 3600) // 60
38
seg.append('%d hours' % hrs)
42
seg.append('1 minute')
44
seg.append('%d minutes' % mins)
46
seg.append('less than a minute')
132
50
class Container (object):
134
52
Convert a dict into an object with attributes.
137
54
def __init__(self, _dict=None, **kw):
138
55
if _dict is not None:
139
56
for key, value in _dict.iteritems():
181
91
return '%s at %s' % (username, domains[-2])
182
92
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))
193
# only do this if unicode turns out to be a problem
194
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
196
# FIXME: get rid of this method; use fixed_width() and avoid XML().
201
clean up a string for html display. expand any tabs, encode any html
202
entities, and replace spaces with ' '. this is primarily for use
203
in displaying monospace text.
205
s = cgi.escape(s.expandtabs())
206
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
def fake_permissions(kind, executable):
259
# fake up unix-style permissions given only a "kind" and executable bit
260
if kind == 'directory':
268
s = base64.encodestring(s).replace('\n', '')
269
while (len(s) > 0) and (s[-1] == '='):
276
turn a potentially long string into a unique smaller string.
280
uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
281
x = struct.pack('>I', next)
282
while (len(x) > 1) and (x[0] == '\x00'):
291
P95_MEG = int(0.9 * MEG)
292
P95_GIG = int(0.9 * GIG)
295
def human_size(size, min_divisor=0):
297
if (size == 0) and (min_divisor == 0):
299
if (size < 512) and (min_divisor == 0):
302
if (size >= P95_GIG) or (min_divisor >= GIG):
304
elif (size >= P95_MEG) or (min_divisor >= MEG):
311
dot = dot * 10 // divisor
318
if (base < 100) and (dot != 0):
329
def fill_in_navigation(navigation):
331
given a navigation block (used by the template for the page header), fill
332
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)
337
navigation.position = 0
338
navigation.count = len(navigation.revid_list)
339
navigation.page_position = navigation.position // navigation.pagesize + 1
340
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
341
- 1)) // navigation.pagesize
343
def get_offset(offset):
344
if (navigation.position + offset < 0) or (
345
navigation.position + offset > navigation.count - 1):
347
return navigation.revid_list[navigation.position + offset]
349
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
350
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
351
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}
359
if getattr(navigation, 'query', None) is not None:
360
params['q'] = navigation.query
362
if getattr(navigation, 'start_revid', None) is not None:
363
params['start_revid'] = start_revno
365
if navigation.prev_page_revid:
366
navigation.prev_page_url = navigation.branch.context_url(
367
[navigation.scan_url, prev_page_revno], **params)
368
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?
387
if view == 'directory':
395
# Create breadcrumb trail for the path leading up to the branch
397
'dir_name': "(root)",
402
dir_parts = path.strip('/').split('/')
403
for index, dir_name in enumerate(dir_parts):
405
'dir_name': dir_name,
406
'path': '/'.join(dir_parts[:index + 1]),
409
# If we are not in the directory view, the last crumb is a branch,
410
# so we need to specify a view
411
if view != 'directory':
412
breadcrumbs[-1]['suffix'] = '/' + view
416
def branch_breadcrumbs(path, inv, view):
418
Generate breadcrumb information from the branch path given
420
The path given should be a path that exists within a branch
423
path -- The path to convert into breadcrumbs
424
inv -- Inventory to get file information from
425
view -- The type of view we are showing (files, changes etc)
427
dir_parts = path.strip('/').split('/')
428
inner_breadcrumbs = []
429
for index, dir_name in enumerate(dir_parts):
430
inner_breadcrumbs.append({
431
'dir_name': dir_name,
432
'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
433
'suffix': '/' + view,
435
return inner_breadcrumbs
438
def decorator(unbound):
440
def new_decorator(f):
442
g.__name__ = f.__name__
443
g.__doc__ = f.__doc__
444
g.__dict__.update(f.__dict__)
446
new_decorator.__name__ = unbound.__name__
447
new_decorator.__doc__ = unbound.__doc__
448
new_decorator.__dict__.update(unbound.__dict__)
457
from loggerhead.lsprof import profile
460
ret, stats = profile(f, *a, **kw)
461
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
462
int((time.time() - z) * 1000)))
466
msec = int(now * 1000) % 1000
467
timestr = time.strftime('%Y%m%d%H%M%S',
468
time.localtime(now)) + ('%03d' % msec)
469
filename = f.__name__ + '-' + timestr + '.lsprof'
470
cPickle.dump(stats, open(filename, 'w'), 2)
475
# just thinking out loud here...
477
# so, when browsing around, there are 5 pieces of context, most optional:
479
# current location along the navigation path (while browsing)
480
# - starting revid (start_revid)
481
# the current beginning of navigation (navigation continues back to
482
# the original revision) -- this defines an 'alternate mainline'
483
# when the user navigates into a branch.
485
# the file being looked at
487
# if navigating the revisions that touched a file
489
# if navigating the revisions that matched a search query
491
# a previous revision to remember for future comparisons
493
# current revid is given on the url path. the rest are optional components
496
# other transient things can be set:
498
# to compare one revision to another, on /revision only
500
# for re-ordering an existing page by different sort
502
t_context = threading.local()
503
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
504
'compare_revid', 'sort')
507
def set_context(map):
508
t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
511
def get_context(**overrides):
513
Soon to be deprecated.
516
return a context map that may be overriden by specific values passed in,
517
but only contains keys from the list of valid context keys.
519
if 'clear' is set, only the 'remember' context value will be added, and
520
all other context will be omitted.
523
if overrides.get('clear', False):
524
map['remember'] = t_context.map.get('remember', None)
526
map.update(t_context.map)
527
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
528
map.update(overrides)
532
class Reloader(object):
534
This class wraps all paste.reloader logic. All methods are @classmethod.
537
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
540
def _turn_sigterm_into_systemexit(self):
542
Attempts to turn a SIGTERM exception into a SystemExit exception.
549
def handle_term(signo, frame):
551
signal.signal(signal.SIGTERM, handle_term)
554
def is_installed(self):
555
return os.environ.get(self._reloader_environ_key)
559
from paste import reloader
560
reloader.install(int(1))
563
def restart_with_reloader(self):
564
"""Based on restart_with_monitor from paste.script.serve."""
565
print 'Starting subprocess with file monitor'
567
args = [sys.executable] + sys.argv
568
new_environ = os.environ.copy()
569
new_environ[self._reloader_environ_key] = 'true'
573
self._turn_sigterm_into_systemexit()
574
proc = subprocess.Popen(args, env=new_environ)
575
exit_code = proc.wait()
577
except KeyboardInterrupt:
578
print '^C caught in monitor process'
582
and hasattr(os, 'kill')):
585
os.kill(proc.pid, signal.SIGTERM)
586
except (OSError, IOError):
589
# Reloader always exits with code 3; but if we are
590
# a monitor, any exit code will restart
593
print '-'*20, 'Restarting', '-'*20
100
yield n * factors[index]
102
if index >= len(factors):
107
def scan_range(pos, max):
109
given a position in a maximum range, return a list of negative and positive
110
jump factors for an hgweb-style triple-factor geometric scan.
112
for example, with pos=20 and max=500, the range would be:
113
[ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
115
i admit this is a very strange way of jumping through revisions. i didn't
119
for n in triple_factors():