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
35
42
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'
73
45
def fix_year(year):
81
class Container (object):
54
# date_day -- just the day
55
# date_time -- full date with time (UTC)
57
# approximatedate -- for use in tables
59
# approximatedate return an elementtree <span> Element
60
# with the full date (UTC) in a tooltip.
64
return value.strftime('%Y-%m-%d')
69
# Note: this assumes that the value is UTC in some fashion.
70
return value.strftime('%Y-%m-%d %H:%M:%S UTC')
75
def _approximatedate(date):
76
delta = datetime.datetime.now() - date
77
if abs(delta) > datetime.timedelta(1, 0, 0):
78
# far in the past or future, display the date
80
future = delta < datetime.timedelta(0, 0, 0)
83
hours = delta.seconds / 3600
84
minutes = (delta.seconds - (3600*hours)) / 60
85
seconds = delta.seconds % 60
103
result += '%s %s' % (amount, unit)
109
def _wrap_with_date_time_title(date, formatted_date):
110
elem = ET.Element("span")
111
elem.text = formatted_date
112
elem.set("title", date_time(date))
116
def approximatedate(date):
117
#FIXME: Returns an object instead of a string
118
return _wrap_with_date_time_title(date, _approximatedate(date))
121
class Container(object):
83
123
Convert a dict into an object with attributes.
85
126
def __init__(self, _dict=None, **kw):
127
self._properties = {}
86
128
if _dict is not None:
87
129
for key, value in _dict.iteritems():
88
130
setattr(self, key, value)
89
131
for key, value in kw.iteritems():
90
132
setattr(self, key, value)
92
134
def __repr__(self):
94
136
for key, value in self.__dict__.iteritems():
95
if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
137
if key.startswith('_') or (getattr(self.__dict__[key],
138
'__call__', None) is not None):
97
140
out += '%r => %r, ' % (key, value)
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 ])
144
def __getattr__(self, attr):
145
"""Used for handling things that aren't already available."""
146
if attr.startswith('_') or attr not in self._properties:
147
raise AttributeError('No attribute: %s' % (attr,))
148
val = self._properties[attr](self, attr)
149
setattr(self, attr, val)
152
def _set_property(self, attr, prop_func):
153
"""Set a function that will be called when an attribute is desired.
155
We will cache the return value, so the function call should be
156
idempotent. We will pass 'self' and the 'attr' name when triggered.
158
if attr.startswith('_'):
159
raise ValueError("Cannot create properties that start with _")
160
self._properties[attr] = prop_func
112
163
def trunc(text, limit=10):
143
189
return '%s at %s' % (username, domains[-2])
144
190
return '%s at %s' % (username, domains[0])
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):
192
def hide_emails(emails):
194
try to obscure any email address in a list of bazaar committers' names.
198
result.append(hide_email(email))
181
201
# only do this if unicode turns out to be a problem
182
202
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
204
# Can't be a dict; & needs to be done first.
208
("'", "'"), # ' is defined in XML, but not HTML.
215
"""Transform dangerous (X)HTML characters into entities.
217
Like cgi.escape, except also escaping \" and '. This makes it safe to use
218
in both attribute and element content.
220
If you want to safely fill a format string with escaped values, use
223
for char, repl in html_entity_subs:
224
s = s.replace(char, repl)
228
def html_format(template, *args):
229
"""Safely format an HTML template string, escaping the arguments.
231
The template string must not be user-controlled; it will not be escaped.
233
return template % tuple(html_escape(arg) for arg in args)
236
# FIXME: get rid of this method; use fixed_width() and avoid XML().
184
238
def html_clean(s):
186
240
clean up a string for html display. expand any tabs, encode any html
187
241
entities, and replace spaces with ' '. this is primarily for use
188
242
in displaying monospace text.
190
s = cgi.escape(s.expandtabs())
191
# s = _BADCHARS_RE.sub(lambda x: '&#%d;' % (ord(x.group(0)),), s)
244
s = html_escape(s.expandtabs())
192
245
s = s.replace(' ', ' ')
249
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
254
CSS is stupid. In some cases we need to replace an empty value with
255
a non breaking space ( ). There has to be a better way of doing this.
257
return: the same value recieved if not empty, and a ' ' if it is.
261
elif isinstance(s, int):
267
s = s.decode('utf-8')
268
except UnicodeDecodeError:
269
s = s.decode('iso-8859-15')
272
HSC = HTMLStructureCleaner()
276
expand tabs and turn spaces into "non-breaking spaces", so browsers won't
279
if not isinstance(s, unicode):
280
# this kinda sucks. file contents are just binary data, and no
281
# encoding metadata is stored, so we need to guess. this is probably
282
# okay for most code, but for people using things like KOI-8, this
283
# will display gibberish. we have no way of detecting the correct
286
s = s.decode('utf-8')
287
except UnicodeDecodeError:
288
s = s.decode('iso-8859-15')
290
s = html_escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
292
return HSC.clean(s).replace('\n', '<br/>')
196
295
def fake_permissions(kind, executable):
197
296
# fake up unix-style permissions given only a "kind" and executable bit
198
297
if kind == 'directory':
270
361
elif divisor == GIG:
275
def fill_in_navigation(history, navigation):
366
def local_path_from_url(url):
367
"""Convert Bazaar URL to local path, ignoring readonly+ prefix"""
368
readonly_prefix = 'readonly+'
369
if url.startswith(readonly_prefix):
370
url = url[len(readonly_prefix):]
371
return urlutils.local_path_from_url(url)
374
def fill_in_navigation(navigation):
277
376
given a navigation block (used by the template for the page header), fill
278
377
in useful calculated values.
280
navigation.position = history.get_revid_sequence(navigation.revid_list, navigation.revid)
281
if navigation.position is None:
379
if navigation.revid in navigation.revid_list: # XXX is this always true?
380
navigation.position = navigation.revid_list.index(navigation.revid)
282
382
navigation.position = 0
283
383
navigation.count = len(navigation.revid_list)
284
384
navigation.page_position = navigation.position // navigation.pagesize + 1
285
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
385
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
386
- 1)) // navigation.pagesize
287
388
def get_offset(offset):
288
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
389
if (navigation.position + offset < 0) or (
390
navigation.position + offset > navigation.count - 1):
290
392
return navigation.revid_list[navigation.position + offset]
394
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
292
395
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
293
396
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
295
params = { 'file_id': navigation.file_id }
397
prev_page_revno = navigation.history.get_revno(
398
navigation.prev_page_revid)
399
next_page_revno = navigation.history.get_revno(
400
navigation.next_page_revid)
401
start_revno = navigation.history.get_revno(navigation.start_revid)
403
params = {'filter_file_id': navigation.filter_file_id}
296
404
if getattr(navigation, 'query', None) is not None:
297
405
params['q'] = navigation.query
299
params['start_revid'] = navigation.start_revid
407
if getattr(navigation, 'start_revid', None) is not None:
408
params['start_revid'] = start_revno
301
410
if navigation.prev_page_revid:
302
navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
411
navigation.prev_page_url = navigation.branch.context_url(
412
[navigation.scan_url, prev_page_revno], **params)
303
413
if navigation.next_page_revid:
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'):
414
navigation.next_page_url = navigation.branch.context_url(
415
[navigation.scan_url, next_page_revno], **params)
418
def directory_breadcrumbs(path, is_root, view):
420
Generate breadcrumb information from the directory path given
422
The path given should be a path up to any branch that is currently being
426
path -- The path to convert into breadcrumbs
427
is_root -- Whether or not loggerhead is serving a branch at its root
428
view -- The type of view we are showing (files, changes etc)
430
# Is our root directory itself a branch?
438
# Create breadcrumb trail for the path leading up to the branch
440
'dir_name': "(root)",
445
dir_parts = path.strip('/').split('/')
446
for index, dir_name in enumerate(dir_parts):
448
'dir_name': dir_name,
449
'path': '/'.join(dir_parts[:index + 1]),
452
# If we are not in the directory view, the last crumb is a branch,
453
# so we need to specify a view
454
if view != 'directory':
455
breadcrumbs[-1]['suffix'] = '/' + view
459
def branch_breadcrumbs(path, inv, view):
461
Generate breadcrumb information from the branch path given
463
The path given should be a path that exists within a branch
466
path -- The path to convert into breadcrumbs
467
inv -- Inventory to get file information from
468
view -- The type of view we are showing (files, changes etc)
470
dir_parts = path.strip('/').split('/')
471
inner_breadcrumbs = []
472
for index, dir_name in enumerate(dir_parts):
473
inner_breadcrumbs.append({
474
'dir_name': dir_name,
475
'path': '/'.join(dir_parts[:index + 1]),
476
'suffix': '/' + view,
478
return inner_breadcrumbs
312
481
def decorator(unbound):
313
483
def new_decorator(f):
315
485
g.__name__ = f.__name__
322
492
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)))
359
499
def _f(*a, **kw):
360
500
from loggerhead.lsprof import profile
363
503
ret, stats = profile(f, *a, **kw)
364
log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
504
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
505
int((time.time() - z) * 1000)))
367
508
now = time.time()
368
509
msec = int(now * 1000) % 1000
369
timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
510
timestr = time.strftime('%Y%m%d%H%M%S',
511
time.localtime(now)) + ('%03d' % (msec,))
370
512
filename = f.__name__ + '-' + timestr + '.lsprof'
371
513
cPickle.dump(stats, open(filename, 'w'), 2)
422
570
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
423
571
map.update(overrides)
575
class Reloader(object):
577
This class wraps all paste.reloader logic. All methods are @classmethod.
580
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
583
def _turn_sigterm_into_systemexit(cls):
585
Attempts to turn a SIGTERM exception into a SystemExit exception.
592
def handle_term(signo, frame):
594
signal.signal(signal.SIGTERM, handle_term)
597
def is_installed(cls):
598
return os.environ.get(cls._reloader_environ_key)
602
from paste import reloader
603
reloader.install(int(1))
606
def restart_with_reloader(cls):
607
"""Based on restart_with_monitor from paste.script.serve."""
608
print 'Starting subprocess with file monitor'
610
args = [sys.executable] + sys.argv
611
new_environ = os.environ.copy()
612
new_environ[cls._reloader_environ_key] = 'true'
616
cls._turn_sigterm_into_systemexit()
617
proc = subprocess.Popen(args, env=new_environ)
618
exit_code = proc.wait()
620
except KeyboardInterrupt:
621
print '^C caught in monitor process'
625
and getattr(os, 'kill', None) is not None):
628
os.kill(proc.pid, signal.SIGTERM)
629
except (OSError, IOError):
632
# Reloader always exits with code 3; but if we are
633
# a monitor, any exit code will restart
636
print '-'*20, 'Restarting', '-'*20
639
def convert_file_errors(application):
640
"""WSGI wrapper to convert some file errors to Paste exceptions"""
641
def new_application(environ, start_response):
643
return application(environ, start_response)
644
except (IOError, OSError), e:
646
from paste import httpexceptions
647
if e.errno == errno.ENOENT:
648
raise httpexceptions.HTTPNotFound()
649
elif e.errno == errno.EACCES:
650
raise httpexceptions.HTTPForbidden()
653
return new_application
656
def convert_to_json_ready(obj):
657
if isinstance(obj, Container):
658
d = obj.__dict__.copy()
661
elif isinstance(obj, datetime.datetime):
662
return tuple(obj.utctimetuple())
663
raise TypeError(repr(obj) + " is not JSON serializable")