~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Martin Albisetti
  • Date: 2008-07-29 20:48:16 UTC
  • mto: This revision was merged to the branch mainline in revision 188.
  • Revision ID: argentina@gmail.com-20080729204816-svbbqks9csquw9j0
Move version to __init__

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#
 
2
# Copyright (C) 2008  Canonical Ltd.
 
3
#                     (Authored by Martin Albisetti <argentina@gmail.com)
2
4
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
3
5
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
4
6
#
17
19
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
20
#
19
21
 
 
22
try:
 
23
    from xml.etree import ElementTree as ET
 
24
except ImportError:
 
25
    from elementtree import ElementTree as ET
 
26
 
20
27
import base64
21
28
import cgi
22
29
import datetime
23
30
import logging
24
31
import re
25
 
import sha
26
32
import struct
27
 
import sys
28
33
import threading
29
34
import time
30
 
import traceback
31
 
 
32
 
import turbogears
33
 
 
 
35
import types
34
36
 
35
37
log = logging.getLogger("loggerhead.controllers")
36
38
 
37
 
 
38
 
def timespan(delta):
39
 
    if delta.days > 730:
40
 
        # good grief!
41
 
        return '%d years' % (int(delta.days // 365.25),)
42
 
    if delta.days >= 3:
43
 
        return '%d days' % delta.days
44
 
    seg = []
45
 
    if delta.days > 0:
46
 
        if delta.days == 1:
47
 
            seg.append('1 day')
48
 
        else:
49
 
            seg.append('%d days' % delta.days)
50
 
    hrs = delta.seconds // 3600
51
 
    mins = (delta.seconds % 3600) // 60
52
 
    if hrs > 0:
53
 
        if hrs == 1:
54
 
            seg.append('1 hour')
55
 
        else:
56
 
            seg.append('%d hours' % hrs)
57
 
    if delta.days == 0:
58
 
        if mins > 0:
59
 
            if mins == 1:
60
 
                seg.append('1 minute')
61
 
            else:
62
 
                seg.append('%d minutes' % mins)
63
 
        elif hrs == 0:
64
 
            seg.append('less than a minute')
65
 
    return ', '.join(seg)
66
 
 
67
 
 
68
 
def ago(timestamp):
69
 
    now = datetime.datetime.now()
70
 
    return timespan(now - timestamp) + ' ago'
71
 
 
72
 
 
73
39
def fix_year(year):
74
40
    if year < 70:
75
41
        year += 2000
77
43
        year += 1900
78
44
    return year
79
45
 
 
46
# Display of times.
 
47
 
 
48
# date_day -- just the day
 
49
# date_time -- full date with time
 
50
#
 
51
# displaydate -- for use in sentences
 
52
# approximatedate -- for use in tables
 
53
#
 
54
# displaydate and approximatedate return an elementtree <span> Element
 
55
# with the full date in a tooltip.
 
56
 
 
57
def date_day(value):
 
58
    return value.strftime('%Y-%m-%d')
 
59
 
 
60
 
 
61
def date_time(value):
 
62
    return value.strftime('%Y-%m-%d %T')
 
63
 
 
64
 
 
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)
 
71
 
 
72
 
 
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
 
77
        return date_day(date)
 
78
    future = delta < datetime.timedelta(0, 0, 0)
 
79
    delta = abs(delta)
 
80
    days = delta.days
 
81
    hours = delta.seconds / 3600
 
82
    minutes = (delta.seconds - (3600*hours)) / 60
 
83
    seconds = delta.seconds % 60
 
84
    result = ''
 
85
    if future:
 
86
        result += 'in '
 
87
    if days != 0:
 
88
        amount = days
 
89
        unit = 'day'
 
90
    elif hours != 0:
 
91
        amount = hours
 
92
        unit = 'hour'
 
93
    elif minutes != 0:
 
94
        amount = minutes
 
95
        unit = 'minute'
 
96
    else:
 
97
        amount = seconds
 
98
        unit = 'second'
 
99
    if amount != 1:
 
100
        unit += 's'
 
101
    result += '%s %s' % (amount, unit)
 
102
    if not future:
 
103
        result += ' ago'
 
104
        return result
 
105
 
 
106
 
 
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))
 
111
    return elem
 
112
 
 
113
 
 
114
def approximatedate(date):
 
115
    #FIXME: Returns an object instead of a string
 
116
    return _wrap_with_date_time_title(date, _approximatedate(date))
 
117
 
 
118
 
 
119
def displaydate(date):
 
120
    return _wrap_with_date_time_title(date, _displaydate(date))
 
121
 
80
122
 
81
123
class Container (object):
82
124
    """
88
130
                setattr(self, key, value)
89
131
        for key, value in kw.iteritems():
90
132
            setattr(self, key, value)
91
 
    
 
133
 
92
134
    def __repr__(self):
93
135
        out = '{ '
94
136
        for key, value in self.__dict__.iteritems():
99
141
        return out
100
142
 
101
143
 
102
 
def clean_revid(revid):
103
 
    if revid == 'missing':
104
 
        return revid
105
 
    return sha.new(revid).hexdigest()
106
 
 
107
 
 
108
 
def obfuscate(text):
109
 
    return ''.join([ '&#%d;' % ord(c) for c in text ])
110
 
 
111
 
 
112
144
def trunc(text, limit=10):
113
145
    if len(text) <= limit:
114
146
        return text
115
147
    return text[:limit] + '...'
116
148
 
117
149
 
118
 
def to_utf8(s):
119
 
    if isinstance(s, unicode):
120
 
        return s.encode('utf-8')
121
 
    return s
122
 
 
123
 
 
124
150
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
125
151
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
126
152
 
143
169
        return '%s at %s' % (username, domains[-2])
144
170
    return '%s at %s' % (username, domains[0])
145
171
 
146
 
    
147
 
def triple_factors(min_value=1):
148
 
    factors = (1, 3)
149
 
    index = 0
150
 
    n = 1
151
 
    while True:
152
 
        if n >= min_value:
153
 
            yield n * factors[index]
154
 
        index += 1
155
 
        if index >= len(factors):
156
 
            index = 0
157
 
            n *= 10
158
 
 
159
 
 
160
 
def scan_range(pos, max, pagesize=1):
161
 
    """
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.
164
 
    
165
 
    for example, with pos=20 and max=500, the range would be:
166
 
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
167
 
    
168
 
    i admit this is a very strange way of jumping through revisions.  i didn't
169
 
    invent it. :)
170
 
    """
171
 
    out = []
172
 
    for n in triple_factors(pagesize + 1):
173
 
        if n > max:
174
 
            return out
175
 
        if pos + n < max:
176
 
            out.append(n)
177
 
        if pos - n >= 0:
178
 
            out.insert(0, -n)
179
 
 
180
172
 
181
173
# only do this if unicode turns out to be a problem
182
174
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
183
175
 
 
176
# FIXME: get rid of this method; use fixed_width() and avoid XML().
184
177
def html_clean(s):
185
178
    """
186
179
    clean up a string for html display.  expand any tabs, encode any html
188
181
    in displaying monospace text.
189
182
    """
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(' ', '&nbsp;')
193
185
    return s
194
186
 
 
187
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
 
188
 
 
189
def fill_div(s):
 
190
    """
 
191
    CSS is stupid. In some cases we need to replace an empty value with
 
192
    a non breaking space (&nbsp;). There has to be a better way of doing this.
 
193
 
 
194
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
 
195
    """
 
196
    
 
197
 
 
198
    if s is None:
 
199
        return '&nbsp;'
 
200
    elif isinstance(s, int):
 
201
        return s
 
202
    elif not s.strip():
 
203
        return '&nbsp;'
 
204
    else:
 
205
        try:
 
206
            s = s.decode('utf-8')
 
207
        except UnicodeDecodeError:
 
208
            s = s.decode('iso-8859-15')
 
209
        return s
 
210
 
 
211
 
 
212
def fixed_width(s):
 
213
    """
 
214
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
 
215
    chop up the string.
 
216
    """
 
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
 
222
        # encoding to use.
 
223
        try:
 
224
            s = s.decode('utf-8')
 
225
        except UnicodeDecodeError:
 
226
            s = s.decode('iso-8859-15')
 
227
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
 
228
 
195
229
 
196
230
def fake_permissions(kind, executable):
197
231
    # fake up unix-style permissions given only a "kind" and executable bit
202
236
    return '-rw-r--r--'
203
237
 
204
238
 
205
 
def if_present(format, value):
206
 
    """
207
 
    format a value using a format string, if the value exists and is not None.
208
 
    """
209
 
    if value is None:
210
 
        return ''
211
 
    return format % value
212
 
 
213
 
 
214
239
def b64(s):
215
240
    s = base64.encodestring(s).replace('\n', '')
216
241
    while (len(s) > 0) and (s[-1] == '='):
251
276
        divisor = MEG
252
277
    else:
253
278
        divisor = KILO
254
 
    
 
279
 
255
280
    dot = size % divisor
256
281
    base = size - dot
257
282
    dot = dot * 10 // divisor
259
284
    if dot >= 10:
260
285
        base += 1
261
286
        dot -= 10
262
 
    
 
287
 
263
288
    out = str(base)
264
289
    if (base < 100) and (dot != 0):
265
290
        out += '.%d' % (dot,)
270
295
    elif divisor == GIG:
271
296
        out += 'G'
272
297
    return out
273
 
    
274
 
 
275
 
def fill_in_navigation(history, navigation):
 
298
 
 
299
 
 
300
def fill_in_navigation(navigation):
276
301
    """
277
302
    given a navigation block (used by the template for the page header), fill
278
303
    in useful calculated values.
279
304
    """
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)
 
307
    else:
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
286
 
    
 
312
 
287
313
    def get_offset(offset):
288
314
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
289
315
            return None
290
316
        return navigation.revid_list[navigation.position + offset]
291
 
    
 
317
 
 
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)
294
 
    
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)
 
326
 
 
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
298
 
    else:
299
 
        params['start_revid'] = navigation.start_revid
300
 
        
 
330
 
 
331
    if getattr(navigation, 'start_revid', None) is not None:
 
332
        params['start_revid'] = start_revno
 
333
 
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))
305
 
 
306
 
 
307
 
def log_exception(log):
308
 
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
309
 
        log.debug(line)
 
338
        navigation.next_page_url = navigation.branch.context_url(
 
339
            [navigation.scan_url, next_page_revno], **params)
310
340
 
311
341
 
312
342
def decorator(unbound):
339
369
 
340
370
 
341
371
@decorator
342
 
def strip_whitespace(f):
343
 
    def _f(*a, **kw):
344
 
        out = f(*a, **kw)
345
 
        orig_len = len(out)
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)
349
 
        new_len = len(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)))
353
 
        return out
354
 
    return _f
355
 
 
356
 
 
357
 
@decorator
358
372
def lsprof(f):
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.
385
399
#     - file_id
 
400
#         the file being looked at
 
401
#     - filter_file_id
386
402
#         if navigating the revisions that touched a file
387
403
#     - q (query)
388
404
#         if navigating the revisions that matched a search query
399
415
#         for re-ordering an existing page by different sort
400
416
 
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')
403
420
 
404
421
 
405
422
def set_context(map):
408
425
 
409
426
def get_context(**overrides):
410
427
    """
 
428
    Soon to be deprecated.
 
429
 
 
430
 
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.
413
 
    
 
433
 
414
434
    if 'clear' is set, only the 'remember' context value will be added, and
415
435
    all other context will be omitted.
416
436
    """