19
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
35
from xml.etree import ElementTree as ET
37
from elementtree import ElementTree as ET
39
from bzrlib import urlutils
41
from simpletal.simpleTALUtils import HTMLStructureCleaner
43
29
log = logging.getLogger("loggerhead.controllers")
55
# date_day -- just the day
56
# date_time -- full date with time
58
# displaydate -- for use in sentences
59
# approximatedate -- for use in tables
61
# displaydate and approximatedate return an elementtree <span> Element
62
# with the full date in a tooltip.
66
return value.strftime('%Y-%m-%d')
71
return value.strftime('%Y-%m-%d %H:%M:%S')
76
def _displaydate(date):
77
delta = abs(datetime.datetime.now() - date)
78
if delta > datetime.timedelta(1, 0, 0):
79
# far in the past or future, display the date
80
return 'on ' + date_day(date)
81
return _approximatedate(date)
84
def _approximatedate(date):
85
delta = datetime.datetime.now() - date
86
if abs(delta) > datetime.timedelta(1, 0, 0):
87
# far in the past or future, display the date
89
future = delta < datetime.timedelta(0, 0, 0)
92
hours = delta.seconds / 3600
93
minutes = (delta.seconds - (3600*hours)) / 60
94
seconds = delta.seconds % 60
112
result += '%s %s' % (amount, unit)
118
def _wrap_with_date_time_title(date, formatted_date):
119
elem = ET.Element("span")
120
elem.text = formatted_date
121
elem.set("title", date_time(date))
125
def approximatedate(date):
126
#FIXME: Returns an object instead of a string
127
return _wrap_with_date_time_title(date, _approximatedate(date))
130
def displaydate(date):
131
return _wrap_with_date_time_title(date, _displaydate(date))
134
class Container(object):
35
return '%d years' % (int(delta.days // 365.25),)
37
return '%d days' % delta.days
43
seg.append('%d days' % delta.days)
44
hrs = delta.seconds // 3600
45
mins = (delta.seconds % 3600) // 60
50
seg.append('%d hours' % hrs)
54
seg.append('1 minute')
56
seg.append('%d minutes' % mins)
58
seg.append('less than a minute')
63
now = datetime.datetime.now()
64
return timespan(now - timestamp) + ' ago'
67
class Container (object):
136
69
Convert a dict into an object with attributes.
139
71
def __init__(self, _dict=None, **kw):
140
self._properties = {}
141
72
if _dict is not None:
142
73
for key, value in _dict.iteritems():
143
74
setattr(self, key, value)
144
75
for key, value in kw.iteritems():
145
76
setattr(self, key, value)
147
78
def __repr__(self):
149
80
for key, value in self.__dict__.iteritems():
150
if key.startswith('_') or (getattr(self.__dict__[key],
151
'__call__', None) is not None):
81
if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
153
83
out += '%r => %r, ' % (key, value)
157
def __getattr__(self, attr):
158
"""Used for handling things that aren't already available."""
159
if attr.startswith('_') or attr not in self._properties:
160
raise AttributeError('No attribute: %s' % (attr,))
161
val = self._properties[attr](self, attr)
162
setattr(self, attr, val)
165
def _set_property(self, attr, prop_func):
166
"""Set a function that will be called when an attribute is desired.
168
We will cache the return value, so the function call should be
169
idempotent. We will pass 'self' and the 'attr' name when triggered.
171
if attr.startswith('_'):
172
raise ValueError("Cannot create properties that start with _")
173
self._properties[attr] = prop_func
176
def trunc(text, limit=10):
177
if len(text) <= limit:
179
return text[:limit] + '...'
88
def clean_revid(revid):
89
if revid == 'missing':
91
return sha.new(revid).hexdigest()
95
return ''.join([ '&#%d;' % ord(c) for c in text ])
182
98
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
183
99
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
186
101
def hide_email(email):
188
103
try to obsure any email address in a bazaar committer's name.
283
171
return '-rw-r--r--'
287
s = base64.encodestring(s).replace('\n', '')
288
while (len(s) > 0) and (s[-1] == '='):
295
turn a potentially long string into a unique smaller string.
299
uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
300
x = struct.pack('>I', next)
301
while (len(x) > 1) and (x[0] == '\x00'):
310
P95_MEG = int(0.9 * MEG)
311
P95_GIG = int(0.9 * GIG)
314
def human_size(size, min_divisor=0):
316
if (size == 0) and (min_divisor == 0):
318
if (size < 512) and (min_divisor == 0):
321
if (size >= P95_GIG) or (min_divisor >= GIG):
323
elif (size >= P95_MEG) or (min_divisor >= MEG):
330
dot = dot * 10 // divisor
337
if (base < 100) and (dot != 0):
338
out += '.%d' % (dot,)
348
def local_path_from_url(url):
349
"""Convert Bazaar URL to local path, ignoring readonly+ prefix"""
350
readonly_prefix = 'readonly+'
351
if url.startswith(readonly_prefix):
352
url = url[len(readonly_prefix):]
353
return urlutils.local_path_from_url(url)
356
def fill_in_navigation(navigation):
358
given a navigation block (used by the template for the page header), fill
359
in useful calculated values.
361
if navigation.revid in navigation.revid_list: # XXX is this always true?
362
navigation.position = navigation.revid_list.index(navigation.revid)
364
navigation.position = 0
365
navigation.count = len(navigation.revid_list)
366
navigation.page_position = navigation.position // navigation.pagesize + 1
367
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
368
- 1)) // navigation.pagesize
370
def get_offset(offset):
371
if (navigation.position + offset < 0) or (
372
navigation.position + offset > navigation.count - 1):
374
return navigation.revid_list[navigation.position + offset]
376
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
377
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
378
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
379
prev_page_revno = navigation.history.get_revno(
380
navigation.prev_page_revid)
381
next_page_revno = navigation.history.get_revno(
382
navigation.next_page_revid)
383
start_revno = navigation.history.get_revno(navigation.start_revid)
385
params = {'filter_file_id': navigation.filter_file_id}
386
if getattr(navigation, 'query', None) is not None:
387
params['q'] = navigation.query
389
if getattr(navigation, 'start_revid', None) is not None:
390
params['start_revid'] = start_revno
392
if navigation.prev_page_revid:
393
navigation.prev_page_url = navigation.branch.context_url(
394
[navigation.scan_url, prev_page_revno], **params)
395
if navigation.next_page_revid:
396
navigation.next_page_url = navigation.branch.context_url(
397
[navigation.scan_url, next_page_revno], **params)
400
def directory_breadcrumbs(path, is_root, view):
402
Generate breadcrumb information from the directory path given
404
The path given should be a path up to any branch that is currently being
408
path -- The path to convert into breadcrumbs
409
is_root -- Whether or not loggerhead is serving a branch at its root
410
view -- The type of view we are showing (files, changes etc)
412
# Is our root directory itself a branch?
420
# Create breadcrumb trail for the path leading up to the branch
422
'dir_name': "(root)",
427
dir_parts = path.strip('/').split('/')
428
for index, dir_name in enumerate(dir_parts):
430
'dir_name': dir_name,
431
'path': '/'.join(dir_parts[:index + 1]),
434
# If we are not in the directory view, the last crumb is a branch,
435
# so we need to specify a view
436
if view != 'directory':
437
breadcrumbs[-1]['suffix'] = '/' + view
441
def branch_breadcrumbs(path, inv, view):
443
Generate breadcrumb information from the branch path given
445
The path given should be a path that exists within a branch
448
path -- The path to convert into breadcrumbs
449
inv -- Inventory to get file information from
450
view -- The type of view we are showing (files, changes etc)
452
dir_parts = path.strip('/').split('/')
453
inner_breadcrumbs = []
454
for index, dir_name in enumerate(dir_parts):
455
inner_breadcrumbs.append({
456
'dir_name': dir_name,
457
'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
458
'suffix': '/' + view,
460
return inner_breadcrumbs
463
def decorator(unbound):
465
def new_decorator(f):
467
g.__name__ = f.__name__
468
g.__doc__ = f.__doc__
469
g.__dict__.update(f.__dict__)
471
new_decorator.__name__ = unbound.__name__
472
new_decorator.__doc__ = unbound.__doc__
473
new_decorator.__dict__.update(unbound.__dict__)
482
from loggerhead.lsprof import profile
485
ret, stats = profile(f, *a, **kw)
486
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
487
int((time.time() - z) * 1000)))
491
msec = int(now * 1000) % 1000
492
timestr = time.strftime('%Y%m%d%H%M%S',
493
time.localtime(now)) + ('%03d' % (msec,))
494
filename = f.__name__ + '-' + timestr + '.lsprof'
495
cPickle.dump(stats, open(filename, 'w'), 2)
500
# just thinking out loud here...
502
# so, when browsing around, there are 5 pieces of context, most optional:
504
# current location along the navigation path (while browsing)
505
# - starting revid (start_revid)
506
# the current beginning of navigation (navigation continues back to
507
# the original revision) -- this defines an 'alternate mainline'
508
# when the user navigates into a branch.
510
# the file being looked at
512
# if navigating the revisions that touched a file
514
# if navigating the revisions that matched a search query
516
# a previous revision to remember for future comparisons
518
# current revid is given on the url path. the rest are optional components
521
# other transient things can be set:
523
# to compare one revision to another, on /revision only
525
# for re-ordering an existing page by different sort
527
t_context = threading.local()
528
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
529
'compare_revid', 'sort')
532
def set_context(map):
533
t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
536
def get_context(**overrides):
538
Soon to be deprecated.
541
return a context map that may be overriden by specific values passed in,
542
but only contains keys from the list of valid context keys.
544
if 'clear' is set, only the 'remember' context value will be added, and
545
all other context will be omitted.
548
if overrides.get('clear', False):
549
map['remember'] = t_context.map.get('remember', None)
551
map.update(t_context.map)
552
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
553
map.update(overrides)
557
class Reloader(object):
559
This class wraps all paste.reloader logic. All methods are @classmethod.
562
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
565
def _turn_sigterm_into_systemexit(cls):
567
Attempts to turn a SIGTERM exception into a SystemExit exception.
574
def handle_term(signo, frame):
576
signal.signal(signal.SIGTERM, handle_term)
579
def is_installed(cls):
580
return os.environ.get(cls._reloader_environ_key)
584
from paste import reloader
585
reloader.install(int(1))
588
def restart_with_reloader(cls):
589
"""Based on restart_with_monitor from paste.script.serve."""
590
print 'Starting subprocess with file monitor'
592
args = [sys.executable] + sys.argv
593
new_environ = os.environ.copy()
594
new_environ[cls._reloader_environ_key] = 'true'
598
cls._turn_sigterm_into_systemexit()
599
proc = subprocess.Popen(args, env=new_environ)
600
exit_code = proc.wait()
602
except KeyboardInterrupt:
603
print '^C caught in monitor process'
607
and getattr(os, 'kill', None) is not None):
610
os.kill(proc.pid, signal.SIGTERM)
611
except (OSError, IOError):
614
# Reloader always exits with code 3; but if we are
615
# a monitor, any exit code will restart
618
print '-'*20, 'Restarting', '-'*20
621
def convert_file_errors(application):
622
"""WSGI wrapper to convert some file errors to Paste exceptions"""
623
def new_application(environ, start_response):
625
return application(environ, start_response)
626
except (IOError, OSError), e:
628
from paste import httpexceptions
629
if e.errno == errno.ENOENT:
630
raise httpexceptions.HTTPNotFound()
631
elif e.errno == errno.EACCES:
632
raise httpexceptions.HTTPForbidden()
635
return new_application
174
def if_present(format, value):
176
format a value using a format string, if the value exists and is not None.
180
return format % value
183
# global branch history & cache
186
_history_lock = threading.Lock()
190
from loggerhead.history import History
192
_history_lock.acquire()
194
if (_history is None) or _history.out_of_date():
195
log.debug('Reload branch history...')
196
if _history is not None:
197
_history.dont_use_cache()
198
_history = History.from_folder(turbogears.config.get('loggerhead.folder'))
199
_history.use_cache(turbogears.config.get('loggerhead.cachepath'))
202
_history_lock.release()