19
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23
from xml.etree import ElementTree as ET
25
from elementtree import ElementTree as ET
39
log = logging.getLogger("loggerhead.controllers")
50
# date_day -- just the day
51
# date_time -- full date with time
53
# displaydate -- for use in sentences
54
# approximatedate -- for use in tables
56
# displaydate and approximatedate return an elementtree <span> Element
57
# with the full date in a tooltip.
60
return value.strftime('%Y-%m-%d')
65
return value.strftime('%Y-%m-%d %T')
70
def _displaydate(date):
71
delta = abs(datetime.datetime.now() - date)
72
if delta > datetime.timedelta(1, 0, 0):
73
# far in the past or future, display the date
74
return 'on ' + date_day(date)
75
return _approximatedate(date)
78
def _approximatedate(date):
79
delta = datetime.datetime.now() - date
80
if abs(delta) > datetime.timedelta(1, 0, 0):
81
# far in the past or future, display the date
83
future = delta < datetime.timedelta(0, 0, 0)
86
hours = delta.seconds / 3600
87
minutes = (delta.seconds - (3600*hours)) / 60
88
seconds = delta.seconds % 60
106
result += '%s %s' % (amount, unit)
112
def _wrap_with_date_time_title(date, formatted_date):
113
elem = ET.Element("span")
114
elem.text = formatted_date
115
elem.set("title", date_time(date))
119
def approximatedate(date):
120
#FIXME: Returns an object instead of a string
121
return _wrap_with_date_time_title(date, _approximatedate(date))
124
def displaydate(date):
125
return _wrap_with_date_time_title(date, _displaydate(date))
25
return '%d days' % delta.days
31
seg.append('%d days' % delta.days)
32
hrs = delta.seconds // 3600
33
mins = (delta.seconds % 3600) // 60
38
seg.append('%d hours' % hrs)
42
seg.append('1 minute')
44
seg.append('%d minutes' % mins)
46
seg.append('less than a minute')
128
50
class Container (object):
174
91
return '%s at %s' % (username, domains[-2])
175
92
return '%s at %s' % (username, domains[0])
178
# only do this if unicode turns out to be a problem
179
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
181
# FIXME: get rid of this method; use fixed_width() and avoid XML().
184
clean up a string for html display. expand any tabs, encode any html
185
entities, and replace spaces with ' '. this is primarily for use
186
in displaying monospace text.
188
s = cgi.escape(s.expandtabs())
189
s = s.replace(' ', ' ')
192
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
196
CSS is stupid. In some cases we need to replace an empty value with
197
a non breaking space ( ). There has to be a better way of doing this.
199
return: the same value recieved if not empty, and a ' ' if it is.
205
elif isinstance(s, int):
211
s = s.decode('utf-8')
212
except UnicodeDecodeError:
213
s = s.decode('iso-8859-15')
219
expand tabs and turn spaces into "non-breaking spaces", so browsers won't
222
if not isinstance(s, unicode):
223
# this kinda sucks. file contents are just binary data, and no
224
# encoding metadata is stored, so we need to guess. this is probably
225
# okay for most code, but for people using things like KOI-8, this
226
# will display gibberish. we have no way of detecting the correct
229
s = s.decode('utf-8')
230
except UnicodeDecodeError:
231
s = s.decode('iso-8859-15')
232
return s.expandtabs().replace(' ', NONBREAKING_SPACE)
235
def fake_permissions(kind, executable):
236
# fake up unix-style permissions given only a "kind" and executable bit
237
if kind == 'directory':
245
s = base64.encodestring(s).replace('\n', '')
246
while (len(s) > 0) and (s[-1] == '='):
253
turn a potentially long string into a unique smaller string.
257
uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
258
x = struct.pack('>I', next)
259
while (len(x) > 1) and (x[0] == '\x00'):
268
P95_MEG = int(0.9 * MEG)
269
P95_GIG = int(0.9 * GIG)
271
def human_size(size, min_divisor=0):
273
if (size == 0) and (min_divisor == 0):
275
if (size < 512) and (min_divisor == 0):
278
if (size >= P95_GIG) or (min_divisor >= GIG):
280
elif (size >= P95_MEG) or (min_divisor >= MEG):
287
dot = dot * 10 // divisor
294
if (base < 100) and (dot != 0):
295
out += '.%d' % (dot,)
305
def fill_in_navigation(navigation):
307
given a navigation block (used by the template for the page header), fill
308
in useful calculated values.
310
if navigation.revid in navigation.revid_list: # XXX is this always true?
311
navigation.position = navigation.revid_list.index(navigation.revid)
313
navigation.position = 0
314
navigation.count = len(navigation.revid_list)
315
navigation.page_position = navigation.position // navigation.pagesize + 1
316
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
318
def get_offset(offset):
319
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
321
return navigation.revid_list[navigation.position + offset]
323
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
324
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
325
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
326
prev_page_revno = navigation.history.get_revno(
327
navigation.prev_page_revid)
328
next_page_revno = navigation.history.get_revno(
329
navigation.next_page_revid)
330
start_revno = navigation.history.get_revno(navigation.start_revid)
332
params = { 'filter_file_id': navigation.filter_file_id }
333
if getattr(navigation, 'query', None) is not None:
334
params['q'] = navigation.query
336
if getattr(navigation, 'start_revid', None) is not None:
337
params['start_revid'] = start_revno
339
if navigation.prev_page_revid:
340
navigation.prev_page_url = navigation.branch.context_url(
341
[navigation.scan_url, prev_page_revno], **params)
342
if navigation.next_page_revid:
343
navigation.next_page_url = navigation.branch.context_url(
344
[navigation.scan_url, next_page_revno], **params)
347
def directory_breadcrumbs(path, is_root, view):
349
Generate breadcrumb information from the directory path given
351
The path given should be a path up to any branch that is currently being
355
path -- The path to convert into breadcrumbs
356
is_root -- Whether or not loggerhead is serving a branch at its root
357
view -- The type of view we are showing (files, changes etc)
359
# Is our root directory itself a branch?
361
if view == 'directory':
369
# Create breadcrumb trail for the path leading up to the branch
371
'dir_name': "(root)",
376
dir_parts = path.strip('/').split('/')
377
for index, dir_name in enumerate(dir_parts):
379
'dir_name': dir_name,
380
'path': '/'.join(dir_parts[:index + 1]),
383
# If we are not in the directory view, the last crumb is a branch,
384
# so we need to specify a view
385
if view != 'directory':
386
breadcrumbs[-1]['suffix'] = '/' + view
390
def branch_breadcrumbs(path, inv, view):
392
Generate breadcrumb information from the branch path given
394
The path given should be a path that exists within a branch
397
path -- The path to convert into breadcrumbs
398
inv -- Inventory to get file information from
399
view -- The type of view we are showing (files, changes etc)
401
dir_parts = path.strip('/').split('/')
402
inner_breadcrumbs = []
403
for index, dir_name in enumerate(dir_parts):
404
inner_breadcrumbs.append({
405
'dir_name': dir_name,
406
'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
407
'suffix': '/' + view ,
409
return inner_breadcrumbs
412
def decorator(unbound):
413
def new_decorator(f):
415
g.__name__ = f.__name__
416
g.__doc__ = f.__doc__
417
g.__dict__.update(f.__dict__)
419
new_decorator.__name__ = unbound.__name__
420
new_decorator.__doc__ = unbound.__doc__
421
new_decorator.__dict__.update(unbound.__dict__)
425
# common threading-lock decorator
426
def with_lock(lockname, debug_name=None):
427
if debug_name is None:
428
debug_name = lockname
430
def _decorator(unbound):
431
def locked(self, *args, **kw):
432
getattr(self, lockname).acquire()
434
return unbound(self, *args, **kw)
436
getattr(self, lockname).release()
444
from loggerhead.lsprof import profile
447
ret, stats = profile(f, *a, **kw)
448
log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
452
msec = int(now * 1000) % 1000
453
timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
454
filename = f.__name__ + '-' + timestr + '.lsprof'
455
cPickle.dump(stats, open(filename, 'w'), 2)
460
# just thinking out loud here...
462
# so, when browsing around, there are 5 pieces of context, most optional:
464
# current location along the navigation path (while browsing)
465
# - starting revid (start_revid)
466
# the current beginning of navigation (navigation continues back to
467
# the original revision) -- this defines an 'alternate mainline'
468
# when the user navigates into a branch.
470
# the file being looked at
472
# if navigating the revisions that touched a file
474
# if navigating the revisions that matched a search query
476
# a previous revision to remember for future comparisons
478
# current revid is given on the url path. the rest are optional components
481
# other transient things can be set:
483
# to compare one revision to another, on /revision only
485
# for re-ordering an existing page by different sort
487
t_context = threading.local()
488
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
489
'compare_revid', 'sort')
492
def set_context(map):
493
t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
496
def get_context(**overrides):
498
Soon to be deprecated.
501
return a context map that may be overriden by specific values passed in,
502
but only contains keys from the list of valid context keys.
504
if 'clear' is set, only the 'remember' context value will be added, and
505
all other context will be omitted.
508
if overrides.get('clear', False):
509
map['remember'] = t_context.map.get('remember', None)
511
map.update(t_context.map)
512
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
513
map.update(overrides)
517
class Reloader(object):
519
This class wraps all paste.reloader logic. All methods are @classmethod.
522
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
525
def _turn_sigterm_into_systemexit(self):
527
Attempts to turn a SIGTERM exception into a SystemExit exception.
533
def handle_term(signo, frame):
535
signal.signal(signal.SIGTERM, handle_term)
538
def is_installed(self):
539
return os.environ.get(self._reloader_environ_key)
543
from paste import reloader
544
reloader.install(int(1))
547
def restart_with_reloader(self):
548
"""Based on restart_with_monitor from paste.script.serve."""
549
print 'Starting subprocess with file monitor'
551
args = [sys.executable] + sys.argv
552
new_environ = os.environ.copy()
553
new_environ[self._reloader_environ_key] = 'true'
557
self._turn_sigterm_into_systemexit()
558
proc = subprocess.Popen(args, env=new_environ)
559
exit_code = proc.wait()
561
except KeyboardInterrupt:
562
print '^C caught in monitor process'
566
and hasattr(os, 'kill')):
569
os.kill(proc.pid, signal.SIGTERM)
570
except (OSError, IOError):
573
# Reloader always exits with code 3; but if we are
574
# a monitor, any exit code will restart
577
print '-'*20, 'Restarting', '-'*20
100
yield n * factors[index]
102
if index >= len(factors):
107
def scan_range(pos, max):
109
given a position in a maximum range, return a list of negative and positive
110
jump factors for an hgweb-style triple-factor geometric scan.
112
for example, with pos=20 and max=500, the range would be:
113
[ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
115
i admit this is a very strange way of jumping through revisions. i didn't
119
for n in triple_factors():