19
17
# 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
37
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'
39
72
def fix_year(year):
48
# date_day -- just the day
49
# date_time -- full date with time
51
# displaydate -- for use in sentences
52
# approximatedate -- for use in tables
54
# displaydate and approximatedate return an elementtree <span> Element
55
# with the full date in a tooltip.
58
return value.strftime('%Y-%m-%d')
62
return value.strftime('%Y-%m-%d %T')
65
def _displaydate(date):
66
delta = abs(datetime.datetime.now() - date)
67
if delta > datetime.timedelta(1, 0, 0):
68
# far in the past or future, display the date
69
return 'on ' + date_day(date)
70
return _approximatedate(date)
73
def _approximatedate(date):
74
delta = datetime.datetime.now() - date
75
if abs(delta) > datetime.timedelta(1, 0, 0):
76
# far in the past or future, display the date
78
future = delta < datetime.timedelta(0, 0, 0)
81
hours = delta.seconds / 3600
82
minutes = (delta.seconds - (3600*hours)) / 60
83
seconds = delta.seconds % 60
101
result += '%s %s' % (amount, unit)
107
def _wrap_with_date_time_title(date, formatted_date):
108
elem = ET.Element("span")
109
elem.text = formatted_date
110
elem.set("title", date_time(date))
114
def approximatedate(date):
115
#FIXME: Returns an object instead of a string
116
return _wrap_with_date_time_title(date, _approximatedate(date))
119
def displaydate(date):
120
return _wrap_with_date_time_title(date, _displaydate(date))
123
80
class Container (object):
169
142
return '%s at %s' % (username, domains[-2])
170
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):
173
180
# only do this if unicode turns out to be a problem
174
181
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
176
# FIXME: get rid of this method; use fixed_width() and avoid XML().
177
183
def html_clean(s):
179
185
clean up a string for html display. expand any tabs, encode any html
181
187
in displaying monospace text.
183
189
s = cgi.escape(s.expandtabs())
190
# s = _BADCHARS_RE.sub(lambda x: '&#%d;' % (ord(x.group(0)),), s)
184
191
s = s.replace(' ', ' ')
187
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
191
CSS is stupid. In some cases we need to replace an empty value with
192
a non breaking space ( ). There has to be a better way of doing this.
194
return: the same value recieved if not empty, and a ' ' if it is.
200
elif isinstance(s, int):
206
s = s.decode('utf-8')
207
except UnicodeDecodeError:
208
s = s.decode('iso-8859-15')
214
expand tabs and turn spaces into "non-breaking spaces", so browsers won't
217
if not isinstance(s, unicode):
218
# this kinda sucks. file contents are just binary data, and no
219
# encoding metadata is stored, so we need to guess. this is probably
220
# okay for most code, but for people using things like KOI-8, this
221
# will display gibberish. we have no way of detecting the correct
224
s = s.decode('utf-8')
225
except UnicodeDecodeError:
226
s = s.decode('iso-8859-15')
227
return s.expandtabs().replace(' ', NONBREAKING_SPACE)
230
195
def fake_permissions(kind, executable):
231
196
# fake up unix-style permissions given only a "kind" and executable bit
295
269
elif divisor == GIG:
300
def fill_in_navigation(navigation):
274
def fill_in_navigation(history, navigation):
302
276
given a navigation block (used by the template for the page header), fill
303
277
in useful calculated values.
305
if navigation.revid in navigation.revid_list: # XXX is this always true?
306
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:
308
281
navigation.position = 0
309
282
navigation.count = len(navigation.revid_list)
310
283
navigation.page_position = navigation.position // navigation.pagesize + 1
311
284
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
313
286
def get_offset(offset):
314
287
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
316
289
return navigation.revid_list[navigation.position + offset]
318
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
319
291
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
320
292
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
321
prev_page_revno = navigation.history.get_revno(
322
navigation.prev_page_revid)
323
next_page_revno = navigation.history.get_revno(
324
navigation.next_page_revid)
325
start_revno = navigation.history.get_revno(navigation.start_revid)
327
params = { 'filter_file_id': navigation.filter_file_id }
294
params = { 'file_id': navigation.file_id }
328
295
if getattr(navigation, 'query', None) is not None:
329
296
params['q'] = navigation.query
331
if getattr(navigation, 'start_revid', None) is not None:
332
params['start_revid'] = start_revno
298
params['start_revid'] = navigation.start_revid
334
300
if navigation.prev_page_revid:
335
navigation.prev_page_url = navigation.branch.context_url(
336
[navigation.scan_url, prev_page_revno], **params)
301
navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **params)
337
302
if navigation.next_page_revid:
338
navigation.next_page_url = navigation.branch.context_url(
339
[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'):
342
311
def decorator(unbound):
360
329
def _decorator(unbound):
361
330
def locked(self, *args, **kw):
331
#self.log.debug('-> %r lock %r', id(threading.currentThread()), debug_name)
362
332
getattr(self, lockname).acquire()
364
334
return unbound(self, *args, **kw)
366
336
getattr(self, lockname).release()
337
#self.log.debug('<- %r unlock %r', id(threading.currentThread()), debug_name)
368
339
return _decorator
374
from loggerhead.lsprof import profile
377
ret, stats = profile(f, *a, **kw)
378
log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
382
msec = int(now * 1000) % 1000
383
timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
384
filename = f.__name__ + '-' + timestr + '.lsprof'
385
cPickle.dump(stats, open(filename, 'w'), 2)
390
# just thinking out loud here...
392
# so, when browsing around, there are 5 pieces of context, most optional:
394
# current location along the navigation path (while browsing)
395
# - starting revid (start_revid)
396
# the current beginning of navigation (navigation continues back to
397
# the original revision) -- this defines an 'alternate mainline'
398
# when the user navigates into a branch.
400
# the file being looked at
402
# if navigating the revisions that touched a file
404
# if navigating the revisions that matched a search query
406
# a previous revision to remember for future comparisons
408
# current revid is given on the url path. the rest are optional components
411
# other transient things can be set:
413
# to compare one revision to another, on /revision only
415
# for re-ordering an existing page by different sort
417
t_context = threading.local()
418
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
419
'compare_revid', 'sort')
422
def set_context(map):
423
t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
426
def get_context(**overrides):
428
Soon to be deprecated.
431
return a context map that may be overriden by specific values passed in,
432
but only contains keys from the list of valid context keys.
434
if 'clear' is set, only the 'remember' context value will be added, and
435
all other context will be omitted.
438
if overrides.get('clear', False):
439
map['remember'] = t_context.map.get('remember', None)
441
map.update(t_context.map)
442
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
443
map.update(overrides)