19
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
from xml.etree import ElementTree as ET
23
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
38
log = logging.getLogger("loggerhead.controllers")
45
40
def fix_year(year):
130
121
return _wrap_with_date_time_title(date, _displaydate(date))
133
class Container(object):
124
class Container (object):
135
126
Convert a dict into an object with attributes.
138
128
def __init__(self, _dict=None, **kw):
139
self._properties = {}
140
129
if _dict is not None:
141
130
for key, value in _dict.iteritems():
142
131
setattr(self, key, value)
146
135
def __repr__(self):
148
137
for key, value in self.__dict__.iteritems():
149
if key.startswith('_') or (getattr(self.__dict__[key],
150
'__call__', None) is not None):
138
if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
152
140
out += '%r => %r, ' % (key, value)
156
def __getattr__(self, attr):
157
"""Used for handling things that aren't already available."""
158
if attr.startswith('_') or attr not in self._properties:
159
raise AttributeError('No attribute: %s' % (attr,))
160
val = self._properties[attr](self, attr)
161
setattr(self, attr, val)
164
def _set_property(self, attr, prop_func):
165
"""Set a function that will be called when an attribute is desired.
167
We will cache the return value, so the function call should be
168
idempotent. We will pass 'self' and the 'attr' name when triggered.
170
if attr.startswith('_'):
171
raise ValueError("Cannot create properties that start with _")
172
self._properties[attr] = prop_func
145
def clean_revid(revid):
146
if revid == 'missing':
148
return sha.new(revid).hexdigest()
152
return ''.join([ '&#%d;' % ord(c) for c in text ])
175
155
def trunc(text, limit=10):
201
186
return '%s at %s' % (username, domains[-2])
202
187
return '%s at %s' % (username, domains[0])
204
def hide_emails(emails):
206
try to obscure any email address in a list of bazaar committers' names.
210
result.append(hide_email(email))
190
def triple_factors(min_value=1):
196
yield n * factors[index]
198
if index >= len(factors):
203
def scan_range(pos, max, pagesize=1):
205
given a position in a maximum range, return a list of negative and positive
206
jump factors for an hgweb-style triple-factor geometric scan.
208
for example, with pos=20 and max=500, the range would be:
209
[ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
211
i admit this is a very strange way of jumping through revisions. i didn't
215
for n in triple_factors(pagesize + 1):
213
224
# only do this if unicode turns out to be a problem
214
225
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
216
# Can't be a dict; & needs to be done first.
220
("'", "'"), # ' is defined in XML, but not HTML.
227
"""Transform dangerous (X)HTML characters into entities.
229
Like cgi.escape, except also escaping " and '. This makes it safe to use
230
in both attribute and element content.
232
If you want to safely fill a format string with escaped values, use
235
for char, repl in html_entity_subs:
236
s = s.replace(char, repl)
240
def html_format(template, *args):
241
"""Safely format an HTML template string, escaping the arguments.
243
The template string must not be user-controlled; it will not be escaped.
245
return template % tuple(html_escape(arg) for arg in args)
248
227
# FIXME: get rid of this method; use fixed_width() and avoid XML().
250
228
def html_clean(s):
252
230
clean up a string for html display. expand any tabs, encode any html
253
231
entities, and replace spaces with ' '. this is primarily for use
254
232
in displaying monospace text.
256
s = html_escape(s.expandtabs())
234
s = cgi.escape(s.expandtabs())
257
235
s = s.replace(' ', ' ')
261
238
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
266
242
CSS is stupid. In some cases we need to replace an empty value with
267
243
a non breaking space ( ). There has to be a better way of doing this.
269
return: the same value recieved if not empty, and a ' ' if it is.
245
return: the same value recieved if not empty, and a NONBREAKING_SPACE
273
elif isinstance(s, int):
248
if type(s) is int and s is None:
249
return NONBREAKING_SPACE
250
elif type(s) is int and s is not None:
252
elif type(s) is types.NoneType:
253
return NONBREAKING_SPACE
255
return NONBREAKING_SPACE
279
s = s.decode('utf-8')
280
except UnicodeDecodeError:
281
s = s.decode('iso-8859-15')
284
HSC = HTMLStructureCleaner()
286
261
def fixed_width(s):
378
def local_path_from_url(url):
379
"""Convert Bazaar URL to local path, ignoring readonly+ prefix"""
380
readonly_prefix = 'readonly+'
381
if url.startswith(readonly_prefix):
382
url = url[len(readonly_prefix):]
383
return urlutils.local_path_from_url(url)
386
358
def fill_in_navigation(navigation):
388
360
given a navigation block (used by the template for the page header), fill
394
366
navigation.position = 0
395
367
navigation.count = len(navigation.revid_list)
396
368
navigation.page_position = navigation.position // navigation.pagesize + 1
397
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
398
- 1)) // navigation.pagesize
369
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
400
371
def get_offset(offset):
401
if (navigation.position + offset < 0) or (
402
navigation.position + offset > navigation.count - 1):
372
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
404
374
return navigation.revid_list[navigation.position + offset]
406
376
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
407
377
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
408
378
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
409
prev_page_revno = navigation.history.get_revno(
410
navigation.prev_page_revid)
411
next_page_revno = navigation.history.get_revno(
412
navigation.next_page_revid)
413
start_revno = navigation.history.get_revno(navigation.start_revid)
415
params = {'filter_file_id': navigation.filter_file_id}
379
prev_page_revno = navigation.branch.history.get_revno(
380
navigation.prev_page_revid)
381
next_page_revno = navigation.branch.history.get_revno(
382
navigation.next_page_revid)
383
start_revno = navigation.branch._history.get_revno(navigation.start_revid)
385
prev_page_revno = navigation.branch._history.get_revno(
386
navigation.prev_page_revid)
387
next_page_revno = navigation.branch._history.get_revno(
388
navigation.next_page_revid)
390
params = { 'filter_file_id': navigation.filter_file_id }
416
391
if getattr(navigation, 'query', None) is not None:
417
392
params['q'] = navigation.query
427
402
[navigation.scan_url, next_page_revno], **params)
430
def directory_breadcrumbs(path, is_root, view):
432
Generate breadcrumb information from the directory path given
434
The path given should be a path up to any branch that is currently being
438
path -- The path to convert into breadcrumbs
439
is_root -- Whether or not loggerhead is serving a branch at its root
440
view -- The type of view we are showing (files, changes etc)
442
# Is our root directory itself a branch?
450
# Create breadcrumb trail for the path leading up to the branch
452
'dir_name': "(root)",
457
dir_parts = path.strip('/').split('/')
458
for index, dir_name in enumerate(dir_parts):
460
'dir_name': dir_name,
461
'path': '/'.join(dir_parts[:index + 1]),
464
# If we are not in the directory view, the last crumb is a branch,
465
# so we need to specify a view
466
if view != 'directory':
467
breadcrumbs[-1]['suffix'] = '/' + view
471
def branch_breadcrumbs(path, inv, view):
473
Generate breadcrumb information from the branch path given
475
The path given should be a path that exists within a branch
478
path -- The path to convert into breadcrumbs
479
inv -- Inventory to get file information from
480
view -- The type of view we are showing (files, changes etc)
482
dir_parts = path.strip('/').split('/')
483
inner_breadcrumbs = []
484
for index, dir_name in enumerate(dir_parts):
485
inner_breadcrumbs.append({
486
'dir_name': dir_name,
487
'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
488
'suffix': '/' + view,
490
return inner_breadcrumbs
405
def log_exception(log):
406
for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
493
410
def decorator(unbound):
495
411
def new_decorator(f):
497
413
g.__name__ = f.__name__
504
420
return new_decorator
423
# common threading-lock decorator
424
def with_lock(lockname, debug_name=None):
425
if debug_name is None:
426
debug_name = lockname
428
def _decorator(unbound):
429
def locked(self, *args, **kw):
430
getattr(self, lockname).acquire()
432
return unbound(self, *args, **kw)
434
getattr(self, lockname).release()
440
def strip_whitespace(f):
444
out = re.sub(r'\n\s+', '\n', out)
445
out = re.sub(r'[ \t]+', ' ', out)
446
out = re.sub(r'\s+\n', '\n', out)
448
log.debug('Saved %sB (%d%%) by stripping whitespace.',
449
human_size(orig_len - new_len),
450
round(100.0 - float(new_len) * 100.0 / float(orig_len)))
511
457
def _f(*a, **kw):
512
458
from loggerhead.lsprof import profile
515
461
ret, stats = profile(f, *a, **kw)
516
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
517
int((time.time() - z) * 1000)))
462
log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
520
465
now = time.time()
521
466
msec = int(now * 1000) % 1000
522
timestr = time.strftime('%Y%m%d%H%M%S',
523
time.localtime(now)) + ('%03d' % (msec,))
467
timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
524
468
filename = f.__name__ + '-' + timestr + '.lsprof'
525
469
cPickle.dump(stats, open(filename, 'w'), 2)
582
526
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
583
527
map.update(overrides)
587
class Reloader(object):
589
This class wraps all paste.reloader logic. All methods are @classmethod.
592
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
595
def _turn_sigterm_into_systemexit(cls):
597
Attempts to turn a SIGTERM exception into a SystemExit exception.
604
def handle_term(signo, frame):
606
signal.signal(signal.SIGTERM, handle_term)
609
def is_installed(cls):
610
return os.environ.get(cls._reloader_environ_key)
614
from paste import reloader
615
reloader.install(int(1))
618
def restart_with_reloader(cls):
619
"""Based on restart_with_monitor from paste.script.serve."""
620
print 'Starting subprocess with file monitor'
622
args = [sys.executable] + sys.argv
623
new_environ = os.environ.copy()
624
new_environ[cls._reloader_environ_key] = 'true'
628
cls._turn_sigterm_into_systemexit()
629
proc = subprocess.Popen(args, env=new_environ)
630
exit_code = proc.wait()
632
except KeyboardInterrupt:
633
print '^C caught in monitor process'
637
and getattr(os, 'kill', None) is not None):
640
os.kill(proc.pid, signal.SIGTERM)
641
except (OSError, IOError):
644
# Reloader always exits with code 3; but if we are
645
# a monitor, any exit code will restart
648
print '-'*20, 'Restarting', '-'*20
651
def convert_file_errors(application):
652
"""WSGI wrapper to convert some file errors to Paste exceptions"""
653
def new_application(environ, start_response):
655
return application(environ, start_response)
656
except (IOError, OSError), e:
658
from paste import httpexceptions
659
if e.errno == errno.ENOENT:
660
raise httpexceptions.HTTPNotFound()
661
elif e.errno == errno.EACCES:
662
raise httpexceptions.HTTPForbidden()
665
return new_application