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
35
log = logging.getLogger("loggerhead.controllers")
41
return '%d years' % (int(delta.days // 365.25),)
43
return '%d days' % delta.days
49
seg.append('%d days' % delta.days)
50
hrs = delta.seconds // 3600
51
mins = (delta.seconds % 3600) // 60
56
seg.append('%d hours' % hrs)
60
seg.append('1 minute')
62
seg.append('%d minutes' % mins)
64
seg.append('less than a minute')
69
now = datetime.datetime.now()
70
return timespan(now - timestamp) + ' ago'
45
73
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):
84
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
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):
81
class Container (object):
135
83
Convert a dict into an object with attributes.
138
85
def __init__(self, _dict=None, **kw):
139
self._properties = {}
140
86
if _dict is not None:
141
87
for key, value in _dict.iteritems():
142
88
setattr(self, key, value)
143
89
for key, value in kw.iteritems():
144
90
setattr(self, key, value)
146
92
def __repr__(self):
148
94
for key, value in self.__dict__.iteritems():
149
if key.startswith('_') or (getattr(self.__dict__[key],
150
'__call__', None) is not None):
95
if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
152
97
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
102
def clean_revid(revid):
103
if revid == 'missing':
105
return sha.new(revid).hexdigest()
109
return ''.join([ '&#%d;' % ord(c) for c in text ])
175
112
def trunc(text, limit=10):
201
143
return '%s at %s' % (username, domains[-2])
202
144
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))
147
def triple_factors(min_value=1):
153
yield n * factors[index]
155
if index >= len(factors):
160
def scan_range(pos, max, pagesize=1):
162
given a position in a maximum range, return a list of negative and positive
163
jump factors for an hgweb-style triple-factor geometric scan.
165
for example, with pos=20 and max=500, the range would be:
166
[ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
168
i admit this is a very strange way of jumping through revisions. i didn't
172
for n in triple_factors(pagesize + 1):
213
181
# only do this if unicode turns out to be a problem
214
182
#_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
# FIXME: get rid of this method; use fixed_width() and avoid XML().
250
184
def html_clean(s):
252
186
clean up a string for html display. expand any tabs, encode any html
253
187
entities, and replace spaces with ' '. this is primarily for use
254
188
in displaying monospace text.
256
s = html_escape(s.expandtabs())
190
s = cgi.escape(s.expandtabs())
191
# s = _BADCHARS_RE.sub(lambda x: '&#%d;' % (ord(x.group(0)),), s)
257
192
s = s.replace(' ', ' ')
261
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()
288
expand tabs and turn spaces into "non-breaking spaces", so browsers won't
291
if not isinstance(s, unicode):
292
# this kinda sucks. file contents are just binary data, and no
293
# encoding metadata is stored, so we need to guess. this is probably
294
# okay for most code, but for people using things like KOI-8, this
295
# will display gibberish. we have no way of detecting the correct
298
s = s.decode('utf-8')
299
except UnicodeDecodeError:
300
s = s.decode('iso-8859-15')
302
s = html_escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
304
return HSC.clean(s).replace('\n', '<br/>')
307
196
def fake_permissions(kind, executable):
308
197
# fake up unix-style permissions given only a "kind" and executable bit
309
198
if kind == 'directory':
373
270
elif divisor == GIG:
378
def local_path_from_url(url):
379
"""Convert Bazaar URL to local path, ignoring readonly+ prefix"""
380
readonly_prefix = 'readonly+'
381
if url.startswith(readonly_prefix):
382
url = url[len(readonly_prefix):]
383
return urlutils.local_path_from_url(url)
386
def fill_in_navigation(navigation):
275
def fill_in_navigation(history, navigation):
388
277
given a navigation block (used by the template for the page header), fill
389
278
in useful calculated values.
391
if navigation.revid in navigation.revid_list: # XXX is this always true?
392
navigation.position = navigation.revid_list.index(navigation.revid)
280
navigation.position = history.get_revid_sequence(navigation.revid_list, navigation.revid)
281
if navigation.position is None:
394
282
navigation.position = 0
395
283
navigation.count = len(navigation.revid_list)
396
284
navigation.page_position = navigation.position // navigation.pagesize + 1
397
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
398
- 1)) // navigation.pagesize
285
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
400
287
def get_offset(offset):
401
if (navigation.position + offset < 0) or (
402
navigation.position + offset > navigation.count - 1):
288
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
404
290
return navigation.revid_list[navigation.position + offset]
406
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
407
292
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
408
293
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}
295
params = { 'file_id': navigation.file_id }
416
296
if getattr(navigation, 'query', None) is not None:
417
297
params['q'] = navigation.query
419
if getattr(navigation, 'start_revid', None) is not None:
420
params['start_revid'] = start_revno
299
params['start_revid'] = navigation.start_revid
422
301
if navigation.prev_page_revid:
423
navigation.prev_page_url = navigation.branch.context_url(
424
[navigation.scan_url, prev_page_revno], **params)
302
navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
425
303
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
'path': '/'.join(dir_parts[:index + 1]),
488
'suffix': '/' + view,
490
return inner_breadcrumbs
304
navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
307
def log_exception(log):
308
for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
493
312
def decorator(unbound):
495
313
def new_decorator(f):
497
315
g.__name__ = f.__name__
504
322
return new_decorator
325
# common threading-lock decorator
326
def with_lock(lockname, debug_name=None):
327
if debug_name is None:
328
debug_name = lockname
330
def _decorator(unbound):
331
def locked(self, *args, **kw):
332
getattr(self, lockname).acquire()
334
return unbound(self, *args, **kw)
336
getattr(self, lockname).release()
342
def strip_whitespace(f):
346
out = re.sub(r'\n\s+', '\n', out)
347
out = re.sub(r'[ \t]+', ' ', out)
348
out = re.sub(r'\s+\n', '\n', out)
350
log.debug('Saved %sB (%d%%) by stripping whitespace.',
351
human_size(orig_len - new_len),
352
round(100.0 - float(new_len) * 100.0 / float(orig_len)))
511
359
def _f(*a, **kw):
512
360
from loggerhead.lsprof import profile
515
363
ret, stats = profile(f, *a, **kw)
516
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
517
int((time.time() - z) * 1000)))
364
log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
520
367
now = time.time()
521
368
msec = int(now * 1000) % 1000
522
timestr = time.strftime('%Y%m%d%H%M%S',
523
time.localtime(now)) + ('%03d' % (msec,))
369
timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
524
370
filename = f.__name__ + '-' + timestr + '.lsprof'
525
371
cPickle.dump(stats, open(filename, 'w'), 2)
582
422
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
583
423
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
668
def convert_to_json_ready(obj):
669
if isinstance(obj, Container):
670
d = obj.__dict__.copy()
673
elif isinstance(obj, datetime.datetime):
674
return tuple(obj.utctimetuple())
675
raise TypeError(repr(obj) + " is not JSON serializable")