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
35
37
log = logging.getLogger("loggerhead.controllers")
41
return '%d years' % (int(delta.days // 365.25),)
43
return '%d days' % delta.days
49
seg.append('%d days' % delta.days)
50
hrs = delta.seconds // 3600
51
mins = (delta.seconds % 3600) // 60
56
seg.append('%d hours' % hrs)
60
seg.append('1 minute')
62
seg.append('%d minutes' % mins)
64
seg.append('less than a minute')
69
now = datetime.datetime.now()
70
return timespan(now - timestamp) + ' ago'
73
39
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))
81
123
class Container (object):
88
130
setattr(self, key, value)
89
131
for key, value in kw.iteritems():
90
132
setattr(self, key, value)
92
134
def __repr__(self):
94
136
for key, value in self.__dict__.iteritems():
102
def clean_revid(revid):
103
if revid == 'missing':
105
return sha.new(revid).hexdigest()
109
return ''.join([ '&#%d;' % ord(c) for c in text ])
112
144
def trunc(text, limit=10):
113
145
if len(text) <= limit:
115
147
return text[:limit] + '...'
119
if isinstance(s, unicode):
120
return s.encode('utf-8')
124
150
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
125
151
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
143
169
return '%s at %s' % (username, domains[-2])
144
170
return '%s at %s' % (username, domains[0])
147
def triple_factors(min_value=1):
153
yield n * factors[index]
155
if index >= len(factors):
160
def scan_range(pos, max, pagesize=1):
162
given a position in a maximum range, return a list of negative and positive
163
jump factors for an hgweb-style triple-factor geometric scan.
165
for example, with pos=20 and max=500, the range would be:
166
[ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
168
i admit this is a very strange way of jumping through revisions. i didn't
172
for n in triple_factors(pagesize + 1):
181
173
# only do this if unicode turns out to be a problem
182
174
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
176
# FIXME: get rid of this method; use fixed_width() and avoid XML().
184
177
def html_clean(s):
186
179
clean up a string for html display. expand any tabs, encode any html
188
181
in displaying monospace text.
190
183
s = cgi.escape(s.expandtabs())
191
# s = _BADCHARS_RE.sub(lambda x: '&#%d;' % (ord(x.group(0)),), s)
192
184
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)
196
230
def fake_permissions(kind, executable):
197
231
# fake up unix-style permissions given only a "kind" and executable bit
270
295
elif divisor == GIG:
275
def fill_in_navigation(history, navigation):
300
def fill_in_navigation(navigation):
277
302
given a navigation block (used by the template for the page header), fill
278
303
in useful calculated values.
280
navigation.position = history.get_revid_sequence(navigation.revid_list, navigation.revid)
281
if navigation.position is None:
305
if navigation.revid in navigation.revid_list: # XXX is this always true?
306
navigation.position = navigation.revid_list.index(navigation.revid)
282
308
navigation.position = 0
283
309
navigation.count = len(navigation.revid_list)
284
310
navigation.page_position = navigation.position // navigation.pagesize + 1
285
311
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
287
313
def get_offset(offset):
288
314
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
290
316
return navigation.revid_list[navigation.position + offset]
318
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
292
319
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
293
320
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
295
params = { 'file_id': navigation.file_id }
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 }
296
328
if getattr(navigation, 'query', None) is not None:
297
329
params['q'] = navigation.query
299
params['start_revid'] = navigation.start_revid
331
if getattr(navigation, 'start_revid', None) is not None:
332
params['start_revid'] = start_revno
301
334
if navigation.prev_page_revid:
302
navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
335
navigation.prev_page_url = navigation.branch.context_url(
336
[navigation.scan_url, prev_page_revno], **params)
303
337
if navigation.next_page_revid:
304
navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
307
def log_exception(log):
308
for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
338
navigation.next_page_url = navigation.branch.context_url(
339
[navigation.scan_url, next_page_revno], **params)
312
342
def decorator(unbound):
342
def strip_whitespace(f):
346
out = re.sub(r'\n\s+', '\n', out)
347
out = re.sub(r'[ \t]+', ' ', out)
348
out = re.sub(r'\s+\n', '\n', out)
350
log.debug('Saved %sB (%d%%) by stripping whitespace.',
351
human_size(orig_len - new_len),
352
round(100.0 - float(new_len) * 100.0 / float(orig_len)))
359
373
def _f(*a, **kw):
360
374
from loggerhead.lsprof import profile
380
394
# current location along the navigation path (while browsing)
381
395
# - starting revid (start_revid)
382
396
# the current beginning of navigation (navigation continues back to
383
# the original revision) -- this may not be along the primary revision
384
# path since the user may have navigated into a branch
397
# the original revision) -- this defines an 'alternate mainline'
398
# when the user navigates into a branch.
400
# the file being looked at
386
402
# if navigating the revisions that touched a file
388
404
# if navigating the revisions that matched a search query
399
415
# for re-ordering an existing page by different sort
401
417
t_context = threading.local()
402
_valid = ('start_revid', 'file_id', 'q', 'remember', 'compare_revid', 'sort')
418
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
419
'compare_revid', 'sort')
405
422
def set_context(map):
409
426
def get_context(**overrides):
428
Soon to be deprecated.
411
431
return a context map that may be overriden by specific values passed in,
412
432
but only contains keys from the list of valid context keys.
414
434
if 'clear' is set, only the 'remember' context value will be added, and
415
435
all other context will be omitted.