34
from xml.etree import ElementTree as ET
36
from elementtree import ElementTree as ET
38
from bzrlib import urlutils
40
from simpletal.simpleTALUtils import HTMLStructureCleaner
42
33
log = logging.getLogger("loggerhead.controllers")
39
return '%d years' % (int(delta.days // 365.25),)
41
return '%d days' % delta.days
47
seg.append('%d days' % delta.days)
48
hrs = delta.seconds // 3600
49
mins = (delta.seconds % 3600) // 60
54
seg.append('%d hours' % hrs)
58
seg.append('1 minute')
60
seg.append('%d minutes' % mins)
62
seg.append('less than a minute')
67
now = datetime.datetime.now()
68
return timespan(now - timestamp) + ' ago'
45
71
def fix_year(year):
54
# date_day -- just the day
55
# date_time -- full date with time
57
# displaydate -- for use in sentences
58
# approximatedate -- for use in tables
60
# displaydate and approximatedate return an elementtree <span> Element
61
# with the full date in a tooltip.
65
return value.strftime('%Y-%m-%d')
70
return value.strftime('%Y-%m-%d %H:%M:%S')
75
def _displaydate(date):
76
delta = abs(datetime.datetime.now() - date)
77
if delta > datetime.timedelta(1, 0, 0):
78
# far in the past or future, display the date
79
return 'on ' + date_day(date)
80
return _approximatedate(date)
83
def _approximatedate(date):
79
_g_format = '%Y-%m-%d @ %H:%M'
81
def format_date(date):
82
if _g_format == 'fancy':
83
return fancy_format_date(date)
84
return date.strftime(_g_format)
86
def fancy_format_date(date):
84
87
delta = datetime.datetime.now() - date
85
if abs(delta) > datetime.timedelta(1, 0, 0):
86
# far in the past or future, display the date
88
future = delta < datetime.timedelta(0, 0, 0)
91
hours = delta.seconds / 3600
92
minutes = (delta.seconds - (3600*hours)) / 60
93
seconds = delta.seconds % 60
89
return date.strftime('%d %b %Y')
111
result += '%s %s' % (amount, unit)
117
def _wrap_with_date_time_title(date, formatted_date):
118
elem = ET.Element("span")
119
elem.text = formatted_date
120
elem.set("title", date_time(date))
124
def approximatedate(date):
125
#FIXME: Returns an object instead of a string
126
return _wrap_with_date_time_title(date, _approximatedate(date))
129
def displaydate(date):
130
return _wrap_with_date_time_title(date, _displaydate(date))
133
class Container(object):
91
return date.strftime('%d %b %H:%M')
93
def set_date_format(format):
98
class Container (object):
135
100
Convert a dict into an object with attributes.
138
102
def __init__(self, _dict=None, **kw):
139
self._properties = {}
140
103
if _dict is not None:
141
104
for key, value in _dict.iteritems():
142
105
setattr(self, key, value)
146
109
def __repr__(self):
148
111
for key, value in self.__dict__.iteritems():
149
if key.startswith('_') or (getattr(self.__dict__[key],
150
'__call__', None) is not None):
112
if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
152
114
out += '%r => %r, ' % (key, value)
156
def __getattr__(self, attr):
157
"""Used for handling things that aren't already available."""
158
if attr.startswith('_') or attr not in self._properties:
159
raise AttributeError('No attribute: %s' % (attr,))
160
val = self._properties[attr](self, attr)
161
setattr(self, attr, val)
164
def _set_property(self, attr, prop_func):
165
"""Set a function that will be called when an attribute is desired.
167
We will cache the return value, so the function call should be
168
idempotent. We will pass 'self' and the 'attr' name when triggered.
170
if attr.startswith('_'):
171
raise ValueError("Cannot create properties that start with _")
172
self._properties[attr] = prop_func
119
def clean_revid(revid):
120
if revid == 'missing':
122
return sha.new(revid).hexdigest()
126
return ''.join([ '&#%d;' % ord(c) for c in text ])
175
129
def trunc(text, limit=10):
201
160
return '%s at %s' % (username, domains[-2])
202
161
return '%s at %s' % (username, domains[0])
204
def hide_emails(emails):
206
try to obscure any email address in a list of bazaar committers' names.
210
result.append(hide_email(email))
164
def triple_factors(min_value=1):
170
yield n * factors[index]
172
if index >= len(factors):
177
def scan_range(pos, max, pagesize=1):
179
given a position in a maximum range, return a list of negative and positive
180
jump factors for an hgweb-style triple-factor geometric scan.
182
for example, with pos=20 and max=500, the range would be:
183
[ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
185
i admit this is a very strange way of jumping through revisions. i didn't
189
for n in triple_factors(pagesize + 1):
213
198
# only do this if unicode turns out to be a problem
214
199
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
216
# Can't be a dict; & needs to be done first.
220
("'", "'"), # ' is defined in XML, but not HTML.
227
"""Transform dangerous (X)HTML characters into entities.
229
Like cgi.escape, except also escaping " and '. This makes it safe to use
230
in both attribute and element content.
232
If you want to safely fill a format string with escaped values, use
235
for char, repl in html_entity_subs:
236
s = s.replace(char, repl)
240
def html_format(template, *args):
241
"""Safely format an HTML template string, escaping the arguments.
243
The template string must not be user-controlled; it will not be escaped.
245
return template % tuple(html_escape(arg) for arg in args)
248
201
# FIXME: get rid of this method; use fixed_width() and avoid XML().
250
202
def html_clean(s):
252
204
clean up a string for html display. expand any tabs, encode any html
253
205
entities, and replace spaces with ' '. this is primarily for use
254
206
in displaying monospace text.
256
s = html_escape(s.expandtabs())
208
s = cgi.escape(s.expandtabs())
257
209
s = s.replace(' ', ' ')
261
214
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
266
CSS is stupid. In some cases we need to replace an empty value with
267
a non breaking space ( ). There has to be a better way of doing this.
269
return: the same value recieved if not empty, and a ' ' if it is.
273
elif isinstance(s, int):
279
s = s.decode('utf-8')
280
except UnicodeDecodeError:
281
s = s.decode('iso-8859-15')
284
HSC = HTMLStructureCleaner()
286
216
def fixed_width(s):
288
218
expand tabs and turn spaces into "non-breaking spaces", so browsers won't
394
321
navigation.position = 0
395
322
navigation.count = len(navigation.revid_list)
396
323
navigation.page_position = navigation.position // navigation.pagesize + 1
397
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
398
- 1)) // navigation.pagesize
324
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
400
326
def get_offset(offset):
401
if (navigation.position + offset < 0) or (
402
navigation.position + offset > navigation.count - 1):
327
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
404
329
return navigation.revid_list[navigation.position + offset]
406
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
407
331
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
408
332
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
409
prev_page_revno = navigation.history.get_revno(
410
navigation.prev_page_revid)
411
next_page_revno = navigation.history.get_revno(
412
navigation.next_page_revid)
413
start_revno = navigation.history.get_revno(navigation.start_revid)
415
params = {'filter_file_id': navigation.filter_file_id}
334
params = { 'file_id': navigation.file_id }
416
335
if getattr(navigation, 'query', None) is not None:
417
336
params['q'] = navigation.query
419
if getattr(navigation, 'start_revid', None) is not None:
420
params['start_revid'] = start_revno
338
params['start_revid'] = navigation.start_revid
422
340
if navigation.prev_page_revid:
423
navigation.prev_page_url = navigation.branch.context_url(
424
[navigation.scan_url, prev_page_revno], **params)
341
navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
425
342
if navigation.next_page_revid:
426
navigation.next_page_url = navigation.branch.context_url(
427
[navigation.scan_url, next_page_revno], **params)
430
def directory_breadcrumbs(path, is_root, view):
432
Generate breadcrumb information from the directory path given
434
The path given should be a path up to any branch that is currently being
438
path -- The path to convert into breadcrumbs
439
is_root -- Whether or not loggerhead is serving a branch at its root
440
view -- The type of view we are showing (files, changes etc)
442
# Is our root directory itself a branch?
450
# Create breadcrumb trail for the path leading up to the branch
452
'dir_name': "(root)",
457
dir_parts = path.strip('/').split('/')
458
for index, dir_name in enumerate(dir_parts):
460
'dir_name': dir_name,
461
'path': '/'.join(dir_parts[:index + 1]),
464
# If we are not in the directory view, the last crumb is a branch,
465
# so we need to specify a view
466
if view != 'directory':
467
breadcrumbs[-1]['suffix'] = '/' + view
471
def branch_breadcrumbs(path, inv, view):
473
Generate breadcrumb information from the branch path given
475
The path given should be a path that exists within a branch
478
path -- The path to convert into breadcrumbs
479
inv -- Inventory to get file information from
480
view -- The type of view we are showing (files, changes etc)
482
dir_parts = path.strip('/').split('/')
483
inner_breadcrumbs = []
484
for index, dir_name in enumerate(dir_parts):
485
inner_breadcrumbs.append({
486
'dir_name': dir_name,
487
'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
488
'suffix': '/' + view,
490
return inner_breadcrumbs
343
navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
346
def log_exception(log):
347
for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
493
351
def decorator(unbound):
495
352
def new_decorator(f):
497
354
g.__name__ = f.__name__
504
361
return new_decorator
364
# common threading-lock decorator
365
def with_lock(lockname, debug_name=None):
366
if debug_name is None:
367
debug_name = lockname
369
def _decorator(unbound):
370
def locked(self, *args, **kw):
371
getattr(self, lockname).acquire()
373
return unbound(self, *args, **kw)
375
getattr(self, lockname).release()
381
def strip_whitespace(f):
385
out = re.sub(r'\n\s+', '\n', out)
386
out = re.sub(r'[ \t]+', ' ', out)
387
out = re.sub(r'\s+\n', '\n', out)
389
log.debug('Saved %sB (%d%%) by stripping whitespace.',
390
human_size(orig_len - new_len),
391
round(100.0 - float(new_len) * 100.0 / float(orig_len)))
511
398
def _f(*a, **kw):
512
399
from loggerhead.lsprof import profile
515
402
ret, stats = profile(f, *a, **kw)
516
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
517
int((time.time() - z) * 1000)))
403
log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
520
406
now = time.time()
521
407
msec = int(now * 1000) % 1000
522
timestr = time.strftime('%Y%m%d%H%M%S',
523
time.localtime(now)) + ('%03d' % (msec,))
408
timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
524
409
filename = f.__name__ + '-' + timestr + '.lsprof'
525
410
cPickle.dump(stats, open(filename, 'w'), 2)
582
461
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
583
462
map.update(overrides)
587
class Reloader(object):
589
This class wraps all paste.reloader logic. All methods are @classmethod.
592
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
595
def _turn_sigterm_into_systemexit(cls):
597
Attempts to turn a SIGTERM exception into a SystemExit exception.
604
def handle_term(signo, frame):
606
signal.signal(signal.SIGTERM, handle_term)
609
def is_installed(cls):
610
return os.environ.get(cls._reloader_environ_key)
614
from paste import reloader
615
reloader.install(int(1))
618
def restart_with_reloader(cls):
619
"""Based on restart_with_monitor from paste.script.serve."""
620
print 'Starting subprocess with file monitor'
622
args = [sys.executable] + sys.argv
623
new_environ = os.environ.copy()
624
new_environ[cls._reloader_environ_key] = 'true'
628
cls._turn_sigterm_into_systemexit()
629
proc = subprocess.Popen(args, env=new_environ)
630
exit_code = proc.wait()
632
except KeyboardInterrupt:
633
print '^C caught in monitor process'
637
and getattr(os, 'kill', None) is not None):
640
os.kill(proc.pid, signal.SIGTERM)
641
except (OSError, IOError):
644
# Reloader always exits with code 3; but if we are
645
# a monitor, any exit code will restart
648
print '-'*20, 'Restarting', '-'*20
651
def convert_file_errors(application):
652
"""WSGI wrapper to convert some file errors to Paste exceptions"""
653
def new_application(environ, start_response):
655
return application(environ, start_response)
656
except (IOError, OSError), e:
658
from paste import httpexceptions
659
if e.errno == errno.ENOENT:
660
raise httpexceptions.HTTPNotFound()
661
elif e.errno == errno.EACCES:
662
raise httpexceptions.HTTPForbidden()
665
return new_application