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")
51
# date_day -- just the day
52
# date_time -- full date with time
54
# displaydate -- for use in sentences
55
# approximatedate -- for use in tables
57
# displaydate and approximatedate return an elementtree <span> Element
58
# with the full date in a tooltip.
62
return value.strftime('%Y-%m-%d')
67
return value.strftime('%Y-%m-%d %T')
72
def _displaydate(date):
73
delta = abs(datetime.datetime.now() - date)
74
if delta > datetime.timedelta(1, 0, 0):
75
# far in the past or future, display the date
76
return 'on ' + date_day(date)
77
return _approximatedate(date)
80
def _approximatedate(date):
81
delta = datetime.datetime.now() - date
82
if abs(delta) > datetime.timedelta(1, 0, 0):
83
# far in the past or future, display the date
85
future = delta < datetime.timedelta(0, 0, 0)
88
hours = delta.seconds / 3600
89
minutes = (delta.seconds - (3600*hours)) / 60
90
seconds = delta.seconds % 60
108
result += '%s %s' % (amount, unit)
114
def _wrap_with_date_time_title(date, formatted_date):
115
elem = ET.Element("span")
116
elem.text = formatted_date
117
elem.set("title", date_time(date))
121
def approximatedate(date):
122
#FIXME: Returns an object instead of a string
123
return _wrap_with_date_time_title(date, _approximatedate(date))
126
def displaydate(date):
127
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')
130
50
class Container (object):
132
52
Convert a dict into an object with attributes.
135
54
def __init__(self, _dict=None, **kw):
136
55
if _dict is not None:
137
56
for key, value in _dict.iteritems():
179
91
return '%s at %s' % (username, domains[-2])
180
92
return '%s at %s' % (username, domains[0])
183
# only do this if unicode turns out to be a problem
184
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
186
# FIXME: get rid of this method; use fixed_width() and avoid XML().
191
clean up a string for html display. expand any tabs, encode any html
192
entities, and replace spaces with ' '. this is primarily for use
193
in displaying monospace text.
195
s = cgi.escape(s.expandtabs())
196
s = s.replace(' ', ' ')
200
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
205
CSS is stupid. In some cases we need to replace an empty value with
206
a non breaking space ( ). There has to be a better way of doing this.
208
return: the same value recieved if not empty, and a ' ' if it is.
214
elif isinstance(s, int):
220
s = s.decode('utf-8')
221
except UnicodeDecodeError:
222
s = s.decode('iso-8859-15')
228
expand tabs and turn spaces into "non-breaking spaces", so browsers won't
231
if not isinstance(s, unicode):
232
# this kinda sucks. file contents are just binary data, and no
233
# encoding metadata is stored, so we need to guess. this is probably
234
# okay for most code, but for people using things like KOI-8, this
235
# will display gibberish. we have no way of detecting the correct
238
s = s.decode('utf-8')
239
except UnicodeDecodeError:
240
s = s.decode('iso-8859-15')
241
return s.expandtabs().replace(' ', NONBREAKING_SPACE)
244
def fake_permissions(kind, executable):
245
# fake up unix-style permissions given only a "kind" and executable bit
246
if kind == 'directory':
254
s = base64.encodestring(s).replace('\n', '')
255
while (len(s) > 0) and (s[-1] == '='):
262
turn a potentially long string into a unique smaller string.
266
uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
267
x = struct.pack('>I', next)
268
while (len(x) > 1) and (x[0] == '\x00'):
277
P95_MEG = int(0.9 * MEG)
278
P95_GIG = int(0.9 * GIG)
281
def human_size(size, min_divisor=0):
283
if (size == 0) and (min_divisor == 0):
285
if (size < 512) and (min_divisor == 0):
288
if (size >= P95_GIG) or (min_divisor >= GIG):
290
elif (size >= P95_MEG) or (min_divisor >= MEG):
297
dot = dot * 10 // divisor
304
if (base < 100) and (dot != 0):
315
def fill_in_navigation(navigation):
317
given a navigation block (used by the template for the page header), fill
318
in useful calculated values.
320
if navigation.revid in navigation.revid_list: # XXX is this always true?
321
navigation.position = navigation.revid_list.index(navigation.revid)
323
navigation.position = 0
324
navigation.count = len(navigation.revid_list)
325
navigation.page_position = navigation.position // navigation.pagesize + 1
326
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
327
- 1)) // navigation.pagesize
329
def get_offset(offset):
330
if (navigation.position + offset < 0) or (
331
navigation.position + offset > navigation.count - 1):
333
return navigation.revid_list[navigation.position + offset]
335
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
336
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
337
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
338
prev_page_revno = navigation.history.get_revno(
339
navigation.prev_page_revid)
340
next_page_revno = navigation.history.get_revno(
341
navigation.next_page_revid)
342
start_revno = navigation.history.get_revno(navigation.start_revid)
344
params = {'filter_file_id': navigation.filter_file_id}
345
if getattr(navigation, 'query', None) is not None:
346
params['q'] = navigation.query
348
if getattr(navigation, 'start_revid', None) is not None:
349
params['start_revid'] = start_revno
351
if navigation.prev_page_revid:
352
navigation.prev_page_url = navigation.branch.context_url(
353
[navigation.scan_url, prev_page_revno], **params)
354
if navigation.next_page_revid:
355
navigation.next_page_url = navigation.branch.context_url(
356
[navigation.scan_url, next_page_revno], **params)
359
def directory_breadcrumbs(path, is_root, view):
361
Generate breadcrumb information from the directory path given
363
The path given should be a path up to any branch that is currently being
367
path -- The path to convert into breadcrumbs
368
is_root -- Whether or not loggerhead is serving a branch at its root
369
view -- The type of view we are showing (files, changes etc)
371
# Is our root directory itself a branch?
373
if view == 'directory':
381
# Create breadcrumb trail for the path leading up to the branch
383
'dir_name': "(root)",
388
dir_parts = path.strip('/').split('/')
389
for index, dir_name in enumerate(dir_parts):
391
'dir_name': dir_name,
392
'path': '/'.join(dir_parts[:index + 1]),
395
# If we are not in the directory view, the last crumb is a branch,
396
# so we need to specify a view
397
if view != 'directory':
398
breadcrumbs[-1]['suffix'] = '/' + view
402
def branch_breadcrumbs(path, inv, view):
404
Generate breadcrumb information from the branch path given
406
The path given should be a path that exists within a branch
409
path -- The path to convert into breadcrumbs
410
inv -- Inventory to get file information from
411
view -- The type of view we are showing (files, changes etc)
413
dir_parts = path.strip('/').split('/')
414
inner_breadcrumbs = []
415
for index, dir_name in enumerate(dir_parts):
416
inner_breadcrumbs.append({
417
'dir_name': dir_name,
418
'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
419
'suffix': '/' + view,
421
return inner_breadcrumbs
424
def decorator(unbound):
426
def new_decorator(f):
428
g.__name__ = f.__name__
429
g.__doc__ = f.__doc__
430
g.__dict__.update(f.__dict__)
432
new_decorator.__name__ = unbound.__name__
433
new_decorator.__doc__ = unbound.__doc__
434
new_decorator.__dict__.update(unbound.__dict__)
438
# common threading-lock decorator
441
def with_lock(lockname, debug_name=None):
442
if debug_name is None:
443
debug_name = lockname
446
def _decorator(unbound):
448
def locked(self, *args, **kw):
449
getattr(self, lockname).acquire()
451
return unbound(self, *args, **kw)
453
getattr(self, lockname).release()
462
from loggerhead.lsprof import profile
465
ret, stats = profile(f, *a, **kw)
466
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
467
int((time.time() - z) * 1000)))
471
msec = int(now * 1000) % 1000
472
timestr = time.strftime('%Y%m%d%H%M%S',
473
time.localtime(now)) + ('%03d' % msec)
474
filename = f.__name__ + '-' + timestr + '.lsprof'
475
cPickle.dump(stats, open(filename, 'w'), 2)
480
# just thinking out loud here...
482
# so, when browsing around, there are 5 pieces of context, most optional:
484
# current location along the navigation path (while browsing)
485
# - starting revid (start_revid)
486
# the current beginning of navigation (navigation continues back to
487
# the original revision) -- this defines an 'alternate mainline'
488
# when the user navigates into a branch.
490
# the file being looked at
492
# if navigating the revisions that touched a file
494
# if navigating the revisions that matched a search query
496
# a previous revision to remember for future comparisons
498
# current revid is given on the url path. the rest are optional components
501
# other transient things can be set:
503
# to compare one revision to another, on /revision only
505
# for re-ordering an existing page by different sort
507
t_context = threading.local()
508
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
509
'compare_revid', 'sort')
512
def set_context(map):
513
t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
516
def get_context(**overrides):
518
Soon to be deprecated.
521
return a context map that may be overriden by specific values passed in,
522
but only contains keys from the list of valid context keys.
524
if 'clear' is set, only the 'remember' context value will be added, and
525
all other context will be omitted.
528
if overrides.get('clear', False):
529
map['remember'] = t_context.map.get('remember', None)
531
map.update(t_context.map)
532
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
533
map.update(overrides)
537
class Reloader(object):
539
This class wraps all paste.reloader logic. All methods are @classmethod.
542
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
545
def _turn_sigterm_into_systemexit(self):
547
Attempts to turn a SIGTERM exception into a SystemExit exception.
554
def handle_term(signo, frame):
556
signal.signal(signal.SIGTERM, handle_term)
559
def is_installed(self):
560
return os.environ.get(self._reloader_environ_key)
564
from paste import reloader
565
reloader.install(int(1))
568
def restart_with_reloader(self):
569
"""Based on restart_with_monitor from paste.script.serve."""
570
print 'Starting subprocess with file monitor'
572
args = [sys.executable] + sys.argv
573
new_environ = os.environ.copy()
574
new_environ[self._reloader_environ_key] = 'true'
578
self._turn_sigterm_into_systemexit()
579
proc = subprocess.Popen(args, env=new_environ)
580
exit_code = proc.wait()
582
except KeyboardInterrupt:
583
print '^C caught in monitor process'
587
and hasattr(os, 'kill')):
590
os.kill(proc.pid, signal.SIGTERM)
591
except (OSError, IOError):
594
# Reloader always exits with code 3; but if we are
595
# a monitor, any exit code will restart
598
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():