17
19
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
from xml.etree import ElementTree as ET
23
from elementtree import ElementTree as ET
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
38
42
log = logging.getLogger("loggerhead.controllers")
40
45
def fix_year(year):
47
52
# Display of times.
49
54
# date_day -- just the day
50
# date_time -- full date with time
55
# date_time -- full date with time (UTC)
52
# displaydate -- for use in sentences
53
57
# approximatedate -- for use in tables
55
# displaydate and approximatedate return an elementtree <span> Element
56
# with the full date in a tooltip.
59
# approximatedate return an elementtree <span> Element
60
# with the full date (UTC) in a tooltip.
58
63
def date_day(value):
59
64
return value.strftime('%Y-%m-%d')
62
67
def date_time(value):
63
return value.strftime('%Y-%m-%d %T')
66
def _displaydate(date):
67
delta = abs(datetime.datetime.now() - date)
68
if delta > datetime.timedelta(1, 0, 0):
69
# far in the past or future, display the date
70
return 'on ' + date_day(date)
71
return _approximatedate(date)
69
# Note: this assumes that the value is UTC in some fashion.
70
return value.strftime('%Y-%m-%d %H:%M:%S UTC')
74
75
def _approximatedate(date):
117
118
return _wrap_with_date_time_title(date, _approximatedate(date))
120
def displaydate(date):
121
return _wrap_with_date_time_title(date, _displaydate(date))
124
class Container (object):
121
class Container(object):
126
123
Convert a dict into an object with attributes.
128
126
def __init__(self, _dict=None, **kw):
127
self._properties = {}
129
128
if _dict is not None:
130
129
for key, value in _dict.iteritems():
131
130
setattr(self, key, value)
135
134
def __repr__(self):
137
136
for key, value in self.__dict__.iteritems():
138
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):
140
140
out += '%r => %r, ' % (key, value)
145
def clean_revid(revid):
146
if revid == 'missing':
148
return sha.new(revid).hexdigest()
152
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
155
163
def trunc(text, limit=10):
186
189
return '%s at %s' % (username, domains[-2])
187
190
return '%s at %s' % (username, domains[0])
190
def triple_factors(min_value=1):
196
yield n * factors[index]
198
if index >= len(factors):
203
def scan_range(pos, max, pagesize=1):
205
given a position in a maximum range, return a list of negative and positive
206
jump factors for an hgweb-style triple-factor geometric scan.
208
for example, with pos=20 and max=500, the range would be:
209
[ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
211
i admit this is a very strange way of jumping through revisions. i didn't
215
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))
224
201
# only do this if unicode turns out to be a problem
225
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)
227
236
# FIXME: get rid of this method; use fixed_width() and avoid XML().
228
238
def html_clean(s):
230
240
clean up a string for html display. expand any tabs, encode any html
231
241
entities, and replace spaces with ' '. this is primarily for use
232
242
in displaying monospace text.
234
s = cgi.escape(s.expandtabs())
244
s = html_escape(s.expandtabs())
235
245
s = s.replace(' ', ' ')
240
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()
242
274
def fixed_width(s):
244
276
expand tabs and turn spaces into "non-breaking spaces", so browsers won't
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)
339
374
def fill_in_navigation(navigation):
341
376
given a navigation block (used by the template for the page header), fill
347
382
navigation.position = 0
348
383
navigation.count = len(navigation.revid_list)
349
384
navigation.page_position = navigation.position // navigation.pagesize + 1
350
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
352
388
def get_offset(offset):
353
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):
355
392
return navigation.revid_list[navigation.position + offset]
394
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
357
395
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
358
396
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
359
prev_page_revno = navigation.branch.history.get_revno(
397
prev_page_revno = navigation.history.get_revno(
360
398
navigation.prev_page_revid)
361
next_page_revno = navigation.branch.history.get_revno(
399
next_page_revno = navigation.history.get_revno(
362
400
navigation.next_page_revid)
363
start_revno = navigation.branch._history.get_revno(navigation.start_revid)
401
start_revno = navigation.history.get_revno(navigation.start_revid)
365
params = { 'filter_file_id': navigation.filter_file_id }
403
params = {'filter_file_id': navigation.filter_file_id}
366
404
if getattr(navigation, 'query', None) is not None:
367
405
params['q'] = navigation.query
377
415
[navigation.scan_url, next_page_revno], **params)
380
def log_exception(log):
381
for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
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
385
481
def decorator(unbound):
386
483
def new_decorator(f):
388
485
g.__name__ = f.__name__
395
492
return new_decorator
398
# common threading-lock decorator
399
def with_lock(lockname, debug_name=None):
400
if debug_name is None:
401
debug_name = lockname
403
def _decorator(unbound):
404
def locked(self, *args, **kw):
405
getattr(self, lockname).acquire()
407
return unbound(self, *args, **kw)
409
getattr(self, lockname).release()
415
def strip_whitespace(f):
419
out = re.sub(r'\n\s+', '\n', out)
420
out = re.sub(r'[ \t]+', ' ', out)
421
out = re.sub(r'\s+\n', '\n', out)
423
log.debug('Saved %sB (%d%%) by stripping whitespace.',
424
human_size(orig_len - new_len),
425
round(100.0 - float(new_len) * 100.0 / float(orig_len)))
432
499
def _f(*a, **kw):
433
500
from loggerhead.lsprof import profile
436
503
ret, stats = profile(f, *a, **kw)
437
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)))
440
508
now = time.time()
441
509
msec = int(now * 1000) % 1000
442
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,))
443
512
filename = f.__name__ + '-' + timestr + '.lsprof'
444
513
cPickle.dump(stats, open(filename, 'w'), 2)
474
543
# for re-ordering an existing page by different sort
476
545
t_context = threading.local()
477
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
478
'compare_revid', 'sort')
547
'start_revid', 'filter_file_id', 'q', 'remember', 'compare_revid', 'sort')
481
550
def set_context(map):
501
570
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
502
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")