17
19
# 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
34
39
log = logging.getLogger("loggerhead.controllers")
40
return '%d years' % (int(delta.days // 365.25),)
42
return '%d days' % delta.days
48
seg.append('%d days' % delta.days)
49
hrs = delta.seconds // 3600
50
mins = (delta.seconds % 3600) // 60
55
seg.append('%d hours' % hrs)
59
seg.append('1 minute')
61
seg.append('%d minutes' % mins)
63
seg.append('less than a minute')
68
now = datetime.datetime.now()
69
return timespan(now - timestamp) + ' ago'
72
41
def fix_year(year):
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))
80
128
class Container (object):
142
174
return '%s at %s' % (username, domains[-2])
143
175
return '%s at %s' % (username, domains[0])
146
def triple_factors(min_value=1):
152
yield n * factors[index]
154
if index >= len(factors):
159
def scan_range(pos, max, pagesize=1):
161
given a position in a maximum range, return a list of negative and positive
162
jump factors for an hgweb-style triple-factor geometric scan.
164
for example, with pos=20 and max=500, the range would be:
165
[ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
167
i admit this is a very strange way of jumping through revisions. i didn't
171
for n in triple_factors(pagesize + 1):
180
178
# only do this if unicode turns out to be a problem
181
179
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
181
# FIXME: get rid of this method; use fixed_width() and avoid XML().
183
182
def html_clean(s):
185
184
clean up a string for html display. expand any tabs, encode any html
187
186
in displaying monospace text.
189
188
s = cgi.escape(s.expandtabs())
190
# s = _BADCHARS_RE.sub(lambda x: '&#%d;' % (ord(x.group(0)),), s)
191
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)
195
235
def fake_permissions(kind, executable):
196
236
# fake up unix-style permissions given only a "kind" and executable bit
269
300
elif divisor == GIG:
274
def fill_in_navigation(history, navigation):
305
def fill_in_navigation(navigation):
276
307
given a navigation block (used by the template for the page header), fill
277
308
in useful calculated values.
279
navigation.position = history.get_revid_sequence(navigation.revid_list, navigation.revid)
280
if navigation.position is None:
310
if navigation.revid in navigation.revid_list: # XXX is this always true?
311
navigation.position = navigation.revid_list.index(navigation.revid)
281
313
navigation.position = 0
282
314
navigation.count = len(navigation.revid_list)
283
315
navigation.page_position = navigation.position // navigation.pagesize + 1
284
316
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
286
318
def get_offset(offset):
287
319
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
289
321
return navigation.revid_list[navigation.position + offset]
323
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
291
324
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
292
325
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
294
params = { 'file_id': navigation.file_id }
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 }
295
333
if getattr(navigation, 'query', None) is not None:
296
334
params['q'] = navigation.query
298
params['start_revid'] = navigation.start_revid
336
if getattr(navigation, 'start_revid', None) is not None:
337
params['start_revid'] = start_revno
300
339
if navigation.prev_page_revid:
301
navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
340
navigation.prev_page_url = navigation.branch.context_url(
341
[navigation.scan_url, prev_page_revno], **params)
302
342
if navigation.next_page_revid:
303
navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
306
def log_exception(log):
307
for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
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
311
412
def decorator(unbound):
341
def strip_whitespace(f):
342
443
def _f(*a, **kw):
345
out = re.sub(r'\n\s+', '\n', out)
346
out = re.sub(r'[ \t]+', ' ', out)
347
out = re.sub(r'\s+\n', '\n', out)
349
log.debug('Saved %sB (%d%%) by stripping whitespace.',
350
human_size(orig_len - new_len),
351
round(100.0 - float(new_len) * 100.0 / float(orig_len)))
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)
402
512
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
403
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