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
_g_format = '%Y-%m-%d @ %H:%M'
83
def format_date(date):
84
if _g_format == 'fancy':
85
return fancy_format_date(date)
86
return date.strftime(_g_format)
88
def fancy_format_date(date):
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):
89
84
delta = datetime.datetime.now() - date
91
return date.strftime('%d %b %Y')
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
93
return date.strftime('%d %b %H:%M')
95
def set_date_format(format):
100
class Container (object):
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):
102
135
Convert a dict into an object with attributes.
104
138
def __init__(self, _dict=None, **kw):
139
self._properties = {}
105
140
if _dict is not None:
106
141
for key, value in _dict.iteritems():
107
142
setattr(self, key, value)
108
143
for key, value in kw.iteritems():
109
144
setattr(self, key, value)
111
146
def __repr__(self):
113
148
for key, value in self.__dict__.iteritems():
114
if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
149
if key.startswith('_') or (getattr(self.__dict__[key],
150
'__call__', None) is not None):
116
152
out += '%r => %r, ' % (key, value)
121
def clean_revid(revid):
122
if revid == 'missing':
124
return sha.new(revid).hexdigest()
128
return ''.join([ '&#%d;' % ord(c) for c in text ])
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
131
175
def trunc(text, limit=10):
162
201
return '%s at %s' % (username, domains[-2])
163
202
return '%s at %s' % (username, domains[0])
166
def triple_factors(min_value=1):
172
yield n * factors[index]
174
if index >= len(factors):
179
def scan_range(pos, max, pagesize=1):
181
given a position in a maximum range, return a list of negative and positive
182
jump factors for an hgweb-style triple-factor geometric scan.
184
for example, with pos=20 and max=500, the range would be:
185
[ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
187
i admit this is a very strange way of jumping through revisions. i didn't
191
for n in triple_factors(pagesize + 1):
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))
200
213
# only do this if unicode turns out to be a problem
201
214
#_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)
203
248
# FIXME: get rid of this method; use fixed_width() and avoid XML().
204
250
def html_clean(s):
206
252
clean up a string for html display. expand any tabs, encode any html
207
253
entities, and replace spaces with ' '. this is primarily for use
208
254
in displaying monospace text.
210
s = cgi.escape(s.expandtabs())
256
s = html_escape(s.expandtabs())
211
257
s = s.replace(' ', ' ')
216
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()
218
286
def fixed_width(s):
220
288
expand tabs and turn spaces into "non-breaking spaces", so browsers won't
323
394
navigation.position = 0
324
395
navigation.count = len(navigation.revid_list)
325
396
navigation.page_position = navigation.position // navigation.pagesize + 1
326
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
397
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
398
- 1)) // navigation.pagesize
328
400
def get_offset(offset):
329
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
401
if (navigation.position + offset < 0) or (
402
navigation.position + offset > navigation.count - 1):
331
404
return navigation.revid_list[navigation.position + offset]
406
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
333
407
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
334
408
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
336
params = { 'file_id': navigation.file_id }
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}
337
416
if getattr(navigation, 'query', None) is not None:
338
417
params['q'] = navigation.query
340
params['start_revid'] = navigation.start_revid
419
if getattr(navigation, 'start_revid', None) is not None:
420
params['start_revid'] = start_revno
342
422
if navigation.prev_page_revid:
343
navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
423
navigation.prev_page_url = navigation.branch.context_url(
424
[navigation.scan_url, prev_page_revno], **params)
344
425
if navigation.next_page_revid:
345
navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
348
def log_exception(log):
349
for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
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
353
493
def decorator(unbound):
354
495
def new_decorator(f):
356
497
g.__name__ = f.__name__
363
504
return new_decorator
366
# common threading-lock decorator
367
def with_lock(lockname, debug_name=None):
368
if debug_name is None:
369
debug_name = lockname
371
def _decorator(unbound):
372
def locked(self, *args, **kw):
373
getattr(self, lockname).acquire()
375
return unbound(self, *args, **kw)
377
getattr(self, lockname).release()
383
def strip_whitespace(f):
387
out = re.sub(r'\n\s+', '\n', out)
388
out = re.sub(r'[ \t]+', ' ', out)
389
out = re.sub(r'\s+\n', '\n', out)
391
log.debug('Saved %sB (%d%%) by stripping whitespace.',
392
human_size(orig_len - new_len),
393
round(100.0 - float(new_len) * 100.0 / float(orig_len)))
400
511
def _f(*a, **kw):
401
512
from loggerhead.lsprof import profile
404
515
ret, stats = profile(f, *a, **kw)
405
log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
516
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
517
int((time.time() - z) * 1000)))
408
520
now = time.time()
409
521
msec = int(now * 1000) % 1000
410
timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
522
timestr = time.strftime('%Y%m%d%H%M%S',
523
time.localtime(now)) + ('%03d' % (msec,))
411
524
filename = f.__name__ + '-' + timestr + '.lsprof'
412
525
cPickle.dump(stats, open(filename, 'w'), 2)
463
582
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
464
583
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")