19
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
34
from xml.etree import ElementTree as ET
36
from elementtree import ElementTree as ET
38
from bzrlib import urlutils
40
from simpletal.simpleTALUtils import HTMLStructureCleaner
42
37
log = logging.getLogger("loggerhead.controllers")
45
39
def fix_year(year):
52
46
# Display of times.
54
48
# date_day -- just the day
55
# date_time -- full date with time (UTC)
49
# date_time -- full date with time
51
# displaydate -- for use in sentences
57
52
# approximatedate -- for use in tables
59
# approximatedate return an elementtree <span> Element
60
# with the full date (UTC) in a tooltip.
54
# displaydate and approximatedate return an elementtree <span> Element
55
# with the full date in a tooltip.
63
57
def date_day(value):
64
58
return value.strftime('%Y-%m-%d')
67
61
def date_time(value):
69
# Note: this assumes that the value is UTC in some fashion.
70
return value.strftime('%Y-%m-%d %H:%M:%S UTC')
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)
75
73
def _approximatedate(date):
118
116
return _wrap_with_date_time_title(date, _approximatedate(date))
121
class Container(object):
119
def displaydate(date):
120
return _wrap_with_date_time_title(date, _displaydate(date))
123
class Container (object):
123
125
Convert a dict into an object with attributes.
126
127
def __init__(self, _dict=None, **kw):
127
self._properties = {}
128
128
if _dict is not None:
129
129
for key, value in _dict.iteritems():
130
130
setattr(self, key, value)
134
134
def __repr__(self):
136
136
for key, value in self.__dict__.iteritems():
137
if key.startswith('_') or (getattr(self.__dict__[key],
138
'__call__', None) is not None):
137
if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
140
139
out += '%r => %r, ' % (key, value)
144
def __getattr__(self, attr):
145
"""Used for handling things that aren't already available."""
146
if attr.startswith('_') or attr not in self._properties:
147
raise AttributeError('No attribute: %s' % (attr,))
148
val = self._properties[attr](self, attr)
149
setattr(self, attr, val)
152
def _set_property(self, attr, prop_func):
153
"""Set a function that will be called when an attribute is desired.
155
We will cache the return value, so the function call should be
156
idempotent. We will pass 'self' and the 'attr' name when triggered.
158
if attr.startswith('_'):
159
raise ValueError("Cannot create properties that start with _")
160
self._properties[attr] = prop_func
163
144
def trunc(text, limit=10):
164
145
if len(text) <= limit:
189
169
return '%s at %s' % (username, domains[-2])
190
170
return '%s at %s' % (username, domains[0])
192
def hide_emails(emails):
194
try to obscure any email address in a list of bazaar committers' names.
198
result.append(hide_email(email))
201
173
# only do this if unicode turns out to be a problem
202
174
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
204
# Can't be a dict; & needs to be done first.
208
("'", "'"), # ' is defined in XML, but not HTML.
215
"""Transform dangerous (X)HTML characters into entities.
217
Like cgi.escape, except also escaping \" and '. This makes it safe to use
218
in both attribute and element content.
220
If you want to safely fill a format string with escaped values, use
223
for char, repl in html_entity_subs:
224
s = s.replace(char, repl)
228
def html_format(template, *args):
229
"""Safely format an HTML template string, escaping the arguments.
231
The template string must not be user-controlled; it will not be escaped.
233
return template % tuple(html_escape(arg) for arg in args)
236
176
# FIXME: get rid of this method; use fixed_width() and avoid XML().
238
177
def html_clean(s):
240
179
clean up a string for html display. expand any tabs, encode any html
241
180
entities, and replace spaces with ' '. this is primarily for use
242
181
in displaying monospace text.
244
s = html_escape(s.expandtabs())
183
s = cgi.escape(s.expandtabs())
245
184
s = s.replace(' ', ' ')
249
187
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
254
191
CSS is stupid. In some cases we need to replace an empty value with
286
224
s = s.decode('utf-8')
287
225
except UnicodeDecodeError:
288
226
s = s.decode('iso-8859-15')
290
s = html_escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
292
return HSC.clean(s).replace('\n', '<br/>')
227
return s.expandtabs().replace(' ', NONBREAKING_SPACE)
295
230
def fake_permissions(kind, executable):
366
def local_path_from_url(url):
367
"""Convert Bazaar URL to local path, ignoring readonly+ prefix"""
368
readonly_prefix = 'readonly+'
369
if url.startswith(readonly_prefix):
370
url = url[len(readonly_prefix):]
371
return urlutils.local_path_from_url(url)
374
300
def fill_in_navigation(navigation):
376
302
given a navigation block (used by the template for the page header), fill
382
308
navigation.position = 0
383
309
navigation.count = len(navigation.revid_list)
384
310
navigation.page_position = navigation.position // navigation.pagesize + 1
385
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
386
- 1)) // navigation.pagesize
311
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
388
313
def get_offset(offset):
389
if (navigation.position + offset < 0) or (
390
navigation.position + offset > navigation.count - 1):
314
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
392
316
return navigation.revid_list[navigation.position + offset]
400
324
navigation.next_page_revid)
401
325
start_revno = navigation.history.get_revno(navigation.start_revid)
403
params = {'filter_file_id': navigation.filter_file_id}
327
params = { 'filter_file_id': navigation.filter_file_id }
404
328
if getattr(navigation, 'query', None) is not None:
405
329
params['q'] = navigation.query
415
339
[navigation.scan_url, next_page_revno], **params)
418
def directory_breadcrumbs(path, is_root, view):
420
Generate breadcrumb information from the directory path given
422
The path given should be a path up to any branch that is currently being
426
path -- The path to convert into breadcrumbs
427
is_root -- Whether or not loggerhead is serving a branch at its root
428
view -- The type of view we are showing (files, changes etc)
430
# Is our root directory itself a branch?
438
# Create breadcrumb trail for the path leading up to the branch
440
'dir_name': "(root)",
445
dir_parts = path.strip('/').split('/')
446
for index, dir_name in enumerate(dir_parts):
448
'dir_name': dir_name,
449
'path': '/'.join(dir_parts[:index + 1]),
452
# If we are not in the directory view, the last crumb is a branch,
453
# so we need to specify a view
454
if view != 'directory':
455
breadcrumbs[-1]['suffix'] = '/' + view
459
def branch_breadcrumbs(path, inv, view):
461
Generate breadcrumb information from the branch path given
463
The path given should be a path that exists within a branch
466
path -- The path to convert into breadcrumbs
467
inv -- Inventory to get file information from
468
view -- The type of view we are showing (files, changes etc)
470
dir_parts = path.strip('/').split('/')
471
inner_breadcrumbs = []
472
for index, dir_name in enumerate(dir_parts):
473
inner_breadcrumbs.append({
474
'dir_name': dir_name,
475
'path': '/'.join(dir_parts[:index + 1]),
476
'suffix': '/' + view,
478
return inner_breadcrumbs
481
342
def decorator(unbound):
483
343
def new_decorator(f):
485
345
g.__name__ = f.__name__
492
352
return new_decorator
355
# common threading-lock decorator
356
def with_lock(lockname, debug_name=None):
357
if debug_name is None:
358
debug_name = lockname
360
def _decorator(unbound):
361
def locked(self, *args, **kw):
362
getattr(self, lockname).acquire()
364
return unbound(self, *args, **kw)
366
getattr(self, lockname).release()
499
373
def _f(*a, **kw):
500
374
from loggerhead.lsprof import profile
503
377
ret, stats = profile(f, *a, **kw)
504
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
505
int((time.time() - z) * 1000)))
378
log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
508
381
now = time.time()
509
382
msec = int(now * 1000) % 1000
510
timestr = time.strftime('%Y%m%d%H%M%S',
511
time.localtime(now)) + ('%03d' % (msec,))
383
timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
512
384
filename = f.__name__ + '-' + timestr + '.lsprof'
513
385
cPickle.dump(stats, open(filename, 'w'), 2)
543
415
# for re-ordering an existing page by different sort
545
417
t_context = threading.local()
547
'start_revid', 'filter_file_id', 'q', 'remember', 'compare_revid', 'sort')
418
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
419
'compare_revid', 'sort')
550
422
def set_context(map):
570
442
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
571
443
map.update(overrides)
575
class Reloader(object):
577
This class wraps all paste.reloader logic. All methods are @classmethod.
580
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
583
def _turn_sigterm_into_systemexit(cls):
585
Attempts to turn a SIGTERM exception into a SystemExit exception.
592
def handle_term(signo, frame):
594
signal.signal(signal.SIGTERM, handle_term)
597
def is_installed(cls):
598
return os.environ.get(cls._reloader_environ_key)
602
from paste import reloader
603
reloader.install(int(1))
606
def restart_with_reloader(cls):
607
"""Based on restart_with_monitor from paste.script.serve."""
608
print 'Starting subprocess with file monitor'
610
args = [sys.executable] + sys.argv
611
new_environ = os.environ.copy()
612
new_environ[cls._reloader_environ_key] = 'true'
616
cls._turn_sigterm_into_systemexit()
617
proc = subprocess.Popen(args, env=new_environ)
618
exit_code = proc.wait()
620
except KeyboardInterrupt:
621
print '^C caught in monitor process'
625
and getattr(os, 'kill', None) is not None):
628
os.kill(proc.pid, signal.SIGTERM)
629
except (OSError, IOError):
632
# Reloader always exits with code 3; but if we are
633
# a monitor, any exit code will restart
636
print '-'*20, 'Restarting', '-'*20
639
def convert_file_errors(application):
640
"""WSGI wrapper to convert some file errors to Paste exceptions"""
641
def new_application(environ, start_response):
643
return application(environ, start_response)
644
except (IOError, OSError), e:
646
from paste import httpexceptions
647
if e.errno == errno.ENOENT:
648
raise httpexceptions.HTTPNotFound()
649
elif e.errno == errno.EACCES:
650
raise httpexceptions.HTTPForbidden()
653
return new_application
656
def convert_to_json_ready(obj):
657
if isinstance(obj, Container):
658
d = obj.__dict__.copy()
661
elif isinstance(obj, datetime.datetime):
662
return tuple(obj.utctimetuple())
663
raise TypeError(repr(obj) + " is not JSON serializable")