17
17
# 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
35
34
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'
37
72
def fix_year(year):
46
# date_day -- just the day
47
# date_time -- full date with time
49
# displaydate -- for use in sentences
50
# approximatedate -- for use in tables
52
# displaydate and approximatedate return an elementtree <span> Element
53
# with the full date in a tooltip.
56
return value.strftime('%Y-%m-%d')
60
return value.strftime('%Y-%m-%d %T')
63
def _displaydate(date):
64
delta = abs(datetime.datetime.now() - date)
65
if delta > datetime.timedelta(1, 0, 0):
66
# far in the past or future, display the date
67
return 'on ' + date_day(date)
68
return _approximatedate(date)
71
def _approximatedate(date):
72
delta = datetime.datetime.now() - date
73
if abs(delta) > datetime.timedelta(1, 0, 0):
74
# far in the past or future, display the date
76
future = delta < datetime.timedelta(0, 0, 0)
79
hours = delta.seconds / 3600
80
minutes = (delta.seconds - (3600*hours)) / 60
81
seconds = delta.seconds % 60
99
result += '%s %s' % (amount, unit)
105
def _wrap_with_date_time_title(date, formatted_date):
106
elem = ET.Element("span")
107
elem.text = formatted_date
108
elem.set("title", date_time(date))
112
def approximatedate(date):
113
#FIXME: Returns an object instead of a string
114
return _wrap_with_date_time_title(date, _approximatedate(date))
117
def displaydate(date):
118
return _wrap_with_date_time_title(date, _displaydate(date))
121
80
class Container (object):
167
142
return '%s at %s' % (username, domains[-2])
168
143
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):
171
180
# only do this if unicode turns out to be a problem
172
181
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
174
# FIXME: get rid of this method; use fixed_width() and avoid XML().
175
183
def html_clean(s):
177
185
clean up a string for html display. expand any tabs, encode any html
179
187
in displaying monospace text.
181
189
s = cgi.escape(s.expandtabs())
190
# s = _BADCHARS_RE.sub(lambda x: '&#%d;' % (ord(x.group(0)),), s)
182
191
s = s.replace(' ', ' ')
187
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
191
expand tabs and turn spaces into "non-breaking spaces", so browsers won't
194
if not isinstance(s, unicode):
195
# this kinda sucks. file contents are just binary data, and no
196
# encoding metadata is stored, so we need to guess. this is probably
197
# okay for most code, but for people using things like KOI-8, this
198
# will display gibberish. we have no way of detecting the correct
201
s = s.decode('utf-8')
202
except UnicodeDecodeError:
203
s = s.decode('iso-8859-15')
204
return s.expandtabs().replace(' ', NONBREAKING_SPACE)
207
195
def fake_permissions(kind, executable):
208
196
# fake up unix-style permissions given only a "kind" and executable bit
209
197
if kind == 'directory':
272
269
elif divisor == GIG:
277
def fill_in_navigation(navigation):
274
def fill_in_navigation(history, navigation):
279
276
given a navigation block (used by the template for the page header), fill
280
277
in useful calculated values.
282
if navigation.revid in navigation.revid_list: # XXX is this always true?
283
navigation.position = navigation.revid_list.index(navigation.revid)
279
navigation.position = history.get_revid_sequence(navigation.revid_list, navigation.revid)
280
if navigation.position is None:
285
281
navigation.position = 0
286
282
navigation.count = len(navigation.revid_list)
287
283
navigation.page_position = navigation.position // navigation.pagesize + 1
288
284
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
290
286
def get_offset(offset):
291
287
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
293
289
return navigation.revid_list[navigation.position + offset]
295
291
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
296
292
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
297
prev_page_revno = navigation.history.get_revno(
298
navigation.prev_page_revid)
299
next_page_revno = navigation.history.get_revno(
300
navigation.next_page_revid)
301
start_revno = navigation.history.get_revno(navigation.start_revid)
303
params = { 'filter_file_id': navigation.filter_file_id }
294
params = { 'file_id': navigation.file_id }
304
295
if getattr(navigation, 'query', None) is not None:
305
296
params['q'] = navigation.query
307
if getattr(navigation, 'start_revid', None) is not None:
308
params['start_revid'] = start_revno
298
params['start_revid'] = navigation.start_revid
310
300
if navigation.prev_page_revid:
311
navigation.prev_page_url = navigation.branch.context_url(
312
[navigation.scan_url, prev_page_revno], **params)
301
navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **params)
313
302
if navigation.next_page_revid:
314
navigation.next_page_url = navigation.branch.context_url(
315
[navigation.scan_url, next_page_revno], **params)
303
navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **params)
306
def log_exception(log):
307
for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
318
311
def decorator(unbound):
336
329
def _decorator(unbound):
337
330
def locked(self, *args, **kw):
331
#self.log.debug('-> %r lock %r', id(threading.currentThread()), debug_name)
338
332
getattr(self, lockname).acquire()
340
334
return unbound(self, *args, **kw)
342
336
getattr(self, lockname).release()
337
#self.log.debug('<- %r unlock %r', id(threading.currentThread()), debug_name)
344
339
return _decorator
350
from loggerhead.lsprof import profile
353
ret, stats = profile(f, *a, **kw)
354
log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
358
msec = int(now * 1000) % 1000
359
timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
360
filename = f.__name__ + '-' + timestr + '.lsprof'
361
cPickle.dump(stats, open(filename, 'w'), 2)
366
# just thinking out loud here...
368
# so, when browsing around, there are 5 pieces of context, most optional:
370
# current location along the navigation path (while browsing)
371
# - starting revid (start_revid)
372
# the current beginning of navigation (navigation continues back to
373
# the original revision) -- this defines an 'alternate mainline'
374
# when the user navigates into a branch.
376
# the file being looked at
378
# if navigating the revisions that touched a file
380
# if navigating the revisions that matched a search query
382
# a previous revision to remember for future comparisons
384
# current revid is given on the url path. the rest are optional components
387
# other transient things can be set:
389
# to compare one revision to another, on /revision only
391
# for re-ordering an existing page by different sort
393
t_context = threading.local()
394
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
395
'compare_revid', 'sort')
398
def set_context(map):
399
t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
402
def get_context(**overrides):
404
Soon to be deprecated.
407
return a context map that may be overriden by specific values passed in,
408
but only contains keys from the list of valid context keys.
410
if 'clear' is set, only the 'remember' context value will be added, and
411
all other context will be omitted.
414
if overrides.get('clear', False):
415
map['remember'] = t_context.map.get('remember', None)
417
map.update(t_context.map)
418
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
419
map.update(overrides)