helpers.py
695 lines
| 24.3 KiB
| text/x-python
|
PythonLexer
r547 | """Helper functions | |||
Consists of functions to typically be used within templates, but also | ||||
available to Controllers. This module is available to both as 'h'. | ||||
""" | ||||
r734 | import random | |||
import hashlib | ||||
r966 | import StringIO | |||
r1101 | import urllib | |||
r1154 | from datetime import datetime | |||
r547 | from pygments.formatters import HtmlFormatter | |||
from pygments import highlight as code_highlight | ||||
r1110 | from pylons import url, request, config | |||
r547 | from pylons.i18n.translation import _, ungettext | |||
r1022 | ||||
r547 | from webhelpers.html import literal, HTML, escape | |||
from webhelpers.html.tools import * | ||||
from webhelpers.html.builder import make_tag | ||||
from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \ | ||||
end_form, file, form, hidden, image, javascript_link, link_to, link_to_if, \ | ||||
link_to_unless, ol, required_legend, select, stylesheet_link, submit, text, \ | ||||
password, textarea, title, ul, xml_declaration, radio | ||||
from webhelpers.html.tools import auto_link, button_to, highlight, js_obfuscate, \ | ||||
mail_to, strip_links, strip_tags, tag_re | ||||
from webhelpers.number import format_byte_size, format_bit_size | ||||
from webhelpers.pylonslib import Flash as _Flash | ||||
from webhelpers.pylonslib.secure_form import secure_form | ||||
from webhelpers.text import chop_at, collapse, convert_accented_entities, \ | ||||
convert_misc_entities, lchop, plural, rchop, remove_formatting, \ | ||||
replace_whitespace, urlify, truncate, wrap_paragraphs | ||||
r635 | from webhelpers.date import time_ago_in_words | |||
r1098 | from webhelpers.paginate import Page | |||
r698 | from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \ | |||
convert_boolean_attrs, NotGiven | ||||
r1101 | from vcs.utils.annotate import annotate_highlight | |||
from rhodecode.lib.utils import repo_name_slug | ||||
r1176 | from rhodecode.lib import str2bool, safe_unicode | |||
r1101 | ||||
r698 | def _reset(name, value=None, id=NotGiven, type="reset", **attrs): | |||
r1154 | """ | |||
Reset button | ||||
r899 | """ | |||
r698 | _set_input_attrs(attrs, type, name, value) | |||
_set_id_attr(attrs, id, name) | ||||
convert_boolean_attrs(attrs, ["disabled"]) | ||||
return HTML.input(**attrs) | ||||
reset = _reset | ||||
r734 | ||||
def get_token(): | ||||
"""Return the current authentication token, creating one if one doesn't | ||||
already exist. | ||||
""" | ||||
token_key = "_authentication_token" | ||||
from pylons import session | ||||
if not token_key in session: | ||||
try: | ||||
token = hashlib.sha1(str(random.getrandbits(128))).hexdigest() | ||||
except AttributeError: # Python < 2.4 | ||||
token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest() | ||||
session[token_key] = token | ||||
if hasattr(session, 'save'): | ||||
session.save() | ||||
return session[token_key] | ||||
r547 | class _GetError(object): | |||
r899 | """Get error from form_errors, and represent it as span wrapped error | |||
message | ||||
r1203 | ||||
r899 | :param field_name: field to fetch errors for | |||
:param form_errors: form errors dict | ||||
""" | ||||
r547 | ||||
def __call__(self, field_name, form_errors): | ||||
tmpl = """<span class="error_msg">%s</span>""" | ||||
if form_errors and form_errors.has_key(field_name): | ||||
return literal(tmpl % form_errors.get(field_name)) | ||||
get_error = _GetError() | ||||
class _ToolTip(object): | ||||
r631 | ||||
r547 | def __call__(self, tooltip_title, trim_at=50): | |||
r1203 | """Special function just to wrap our text into nice formatted | |||
r905 | autowrapped text | |||
r1203 | ||||
r604 | :param tooltip_title: | |||
r547 | """ | |||
r631 | ||||
r1352 | return escape(tooltip_title) | |||
r631 | ||||
r547 | def activate(self): | |||
r1203 | """Adds tooltip mechanism to the given Html all tooltips have to have | |||
r904 | set class `tooltip` and set attribute `tooltip_title`. | |||
Then a tooltip will be generated based on that. All with yui js tooltip | ||||
r547 | """ | |||
r631 | ||||
r547 | js = ''' | |||
YAHOO.util.Event.onDOMReady(function(){ | ||||
function toolTipsId(){ | ||||
var ids = []; | ||||
var tts = YAHOO.util.Dom.getElementsByClassName('tooltip'); | ||||
r1203 | ||||
r547 | for (var i = 0; i < tts.length; i++) { | |||
r904 | //if element doesn't not have and id autogenerate one for tooltip | |||
r1203 | ||||
r547 | if (!tts[i].id){ | |||
tts[i].id='tt'+i*100; | ||||
} | ||||
ids.push(tts[i].id); | ||||
} | ||||
r1203 | return ids | |||
r547 | }; | |||
r1203 | var myToolTips = new YAHOO.widget.Tooltip("tooltip", { | |||
r1187 | context: [[toolTipsId()],"tl","bl",null,[0,5]], | |||
r547 | monitorresize:false, | |||
xyoffset :[0,0], | ||||
autodismissdelay:300000, | ||||
hidedelay:5, | ||||
showdelay:20, | ||||
}); | ||||
r1203 | ||||
r547 | }); | |||
r631 | ''' | |||
r547 | return literal(js) | |||
tooltip = _ToolTip() | ||||
class _FilesBreadCrumbs(object): | ||||
r631 | ||||
r547 | def __call__(self, repo_name, rev, paths): | |||
r955 | if isinstance(paths, str): | |||
r1176 | paths = safe_unicode(paths) | |||
r547 | url_l = [link_to(repo_name, url('files_home', | |||
repo_name=repo_name, | ||||
revision=rev, f_path=''))] | ||||
paths_l = paths.split('/') | ||||
r740 | for cnt, p in enumerate(paths_l): | |||
r547 | if p != '': | |||
url_l.append(link_to(p, url('files_home', | ||||
repo_name=repo_name, | ||||
revision=rev, | ||||
r740 | f_path='/'.join(paths_l[:cnt + 1])))) | |||
r547 | ||||
return literal('/'.join(url_l)) | ||||
files_breadcrumbs = _FilesBreadCrumbs() | ||||
r899 | ||||
r547 | class CodeHtmlFormatter(HtmlFormatter): | |||
r966 | """My code Html Formatter for source codes | |||
""" | ||||
r547 | ||||
def wrap(self, source, outfile): | ||||
return self._wrap_div(self._wrap_pre(self._wrap_code(source))) | ||||
def _wrap_code(self, source): | ||||
r740 | for cnt, it in enumerate(source): | |||
r547 | i, t = it | |||
r966 | t = '<div id="L%s">%s</div>' % (cnt + 1, t) | |||
r547 | yield i, t | |||
r966 | ||||
def _wrap_tablelinenos(self, inner): | ||||
dummyoutfile = StringIO.StringIO() | ||||
lncount = 0 | ||||
for t, line in inner: | ||||
if t: | ||||
lncount += 1 | ||||
dummyoutfile.write(line) | ||||
fl = self.linenostart | ||||
mw = len(str(lncount + fl - 1)) | ||||
sp = self.linenospecial | ||||
st = self.linenostep | ||||
la = self.lineanchors | ||||
aln = self.anchorlinenos | ||||
nocls = self.noclasses | ||||
if sp: | ||||
lines = [] | ||||
for i in range(fl, fl + lncount): | ||||
if i % st == 0: | ||||
if i % sp == 0: | ||||
if aln: | ||||
lines.append('<a href="#%s%d" class="special">%*d</a>' % | ||||
(la, i, mw, i)) | ||||
else: | ||||
lines.append('<span class="special">%*d</span>' % (mw, i)) | ||||
else: | ||||
if aln: | ||||
lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i)) | ||||
else: | ||||
lines.append('%*d' % (mw, i)) | ||||
else: | ||||
lines.append('') | ||||
ls = '\n'.join(lines) | ||||
else: | ||||
lines = [] | ||||
for i in range(fl, fl + lncount): | ||||
if i % st == 0: | ||||
if aln: | ||||
lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i)) | ||||
else: | ||||
lines.append('%*d' % (mw, i)) | ||||
else: | ||||
lines.append('') | ||||
ls = '\n'.join(lines) | ||||
# in case you wonder about the seemingly redundant <div> here: since the | ||||
# content in the other cell also is wrapped in a div, some browsers in | ||||
# some configurations seem to mess up the formatting... | ||||
if nocls: | ||||
yield 0, ('<table class="%stable">' % self.cssclass + | ||||
'<tr><td><div class="linenodiv" ' | ||||
'style="background-color: #f0f0f0; padding-right: 10px">' | ||||
'<pre style="line-height: 125%">' + | ||||
r1320 | ls + '</pre></div></td><td id="hlcode" class="code">') | |||
r966 | else: | |||
yield 0, ('<table class="%stable">' % self.cssclass + | ||||
'<tr><td class="linenos"><div class="linenodiv"><pre>' + | ||||
r1320 | ls + '</pre></div></td><td id="hlcode" class="code">') | |||
r966 | yield 0, dummyoutfile.getvalue() | |||
yield 0, '</td></tr></table>' | ||||
r547 | def pygmentize(filenode, **kwargs): | |||
r899 | """pygmentize function using pygments | |||
r1203 | ||||
r604 | :param filenode: | |||
r547 | """ | |||
r899 | ||||
r547 | return literal(code_highlight(filenode.content, | |||
filenode.lexer, CodeHtmlFormatter(**kwargs))) | ||||
r1171 | def pygmentize_annotation(repo_name, filenode, **kwargs): | |||
r899 | """pygmentize function for annotation | |||
r1203 | ||||
r604 | :param filenode: | |||
r547 | """ | |||
r631 | ||||
r547 | color_dict = {} | |||
r947 | def gen_color(n=10000): | |||
r1203 | """generator for getting n of evenly distributed colors using | |||
r947 | hsv color and golden ratio. It always return same order of colors | |||
r1203 | ||||
r947 | :returns: RGB tuple | |||
r631 | """ | |||
r547 | import colorsys | |||
golden_ratio = 0.618033988749895 | ||||
h = 0.22717784590367374 | ||||
r947 | ||||
r1320 | for _ in xrange(n): | |||
r547 | h += golden_ratio | |||
h %= 1 | ||||
HSV_tuple = [h, 0.95, 0.95] | ||||
RGB_tuple = colorsys.hsv_to_rgb(*HSV_tuple) | ||||
r631 | yield map(lambda x:str(int(x * 256)), RGB_tuple) | |||
r547 | ||||
cgenerator = gen_color() | ||||
r631 | ||||
r547 | def get_color_string(cs): | |||
if color_dict.has_key(cs): | ||||
col = color_dict[cs] | ||||
else: | ||||
col = color_dict[cs] = cgenerator.next() | ||||
return "color: rgb(%s)! important;" % (', '.join(col)) | ||||
r631 | ||||
r1171 | def url_func(repo_name): | |||
r1352 | ||||
r1171 | def _url_func(changeset): | |||
r1352 | author = changeset.author | |||
date = changeset.date | ||||
message = tooltip(changeset.message) | ||||
r631 | ||||
r1352 | tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>" | |||
" %s<br/><b>Date:</b> %s</b><br/><b>Message:" | ||||
"</b> %s<br/></div>") | ||||
tooltip_html = tooltip_html % (author, date, message) | ||||
r1171 | lnk_format = '%5s:%s' % ('r%s' % changeset.revision, | |||
short_id(changeset.raw_id)) | ||||
uri = link_to( | ||||
lnk_format, | ||||
url('changeset_home', repo_name=repo_name, | ||||
revision=changeset.raw_id), | ||||
style=get_color_string(changeset.raw_id), | ||||
class_='tooltip', | ||||
title=tooltip_html | ||||
) | ||||
r631 | ||||
r1171 | uri += '\n' | |||
return uri | ||||
return _url_func | ||||
return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs)) | ||||
r631 | ||||
r547 | def get_changeset_safe(repo, rev): | |||
from vcs.backends.base import BaseRepository | ||||
from vcs.exceptions import RepositoryError | ||||
if not isinstance(repo, BaseRepository): | ||||
raise Exception('You must pass an Repository ' | ||||
'object as first argument got %s', type(repo)) | ||||
r631 | ||||
r547 | try: | |||
cs = repo.get_changeset(rev) | ||||
except RepositoryError: | ||||
from rhodecode.lib.utils import EmptyChangeset | ||||
cs = EmptyChangeset() | ||||
return cs | ||||
r999 | def is_following_repo(repo_name, user_id): | |||
from rhodecode.model.scm import ScmModel | ||||
return ScmModel().is_following_repo(repo_name, user_id) | ||||
r547 | flash = _Flash() | |||
r635 | #============================================================================== | |||
r1356 | # SCM FILTERS available via h. | |||
r635 | #============================================================================== | |||
r1356 | from vcs.utils import author_name, author_email | |||
r1342 | from rhodecode.lib import credentials_hidder, age as _age | |||
r547 | ||||
r635 | age = lambda x:_age(x) | |||
r547 | capitalize = lambda x: x.capitalize() | |||
r1356 | email = author_email | |||
email_or_none = lambda x: email(x) if email(x) != x else None | ||||
person = lambda x: author_name(x) | ||||
r636 | short_id = lambda x: x[:12] | |||
r1342 | hide_credentials = lambda x: ''.join(credentials_hidder(x)) | |||
r660 | ||||
r712 | def bool2icon(value): | |||
r899 | """Returns True/False values represented as small html image of true/false | |||
r712 | icons | |||
r1203 | ||||
r712 | :param value: bool value | |||
""" | ||||
if value is True: | ||||
r1050 | return HTML.tag('img', src=url("/images/icons/accept.png"), | |||
alt=_('True')) | ||||
r712 | ||||
if value is False: | ||||
r1050 | return HTML.tag('img', src=url("/images/icons/cancel.png"), | |||
alt=_('False')) | ||||
r712 | ||||
return value | ||||
r1087 | def action_parser(user_log, feed=False): | |||
"""This helper will action_map the specified string action into translated | ||||
r660 | fancy names with icons and links | |||
r1203 | ||||
r899 | :param user_log: user log instance | |||
r1087 | :param feed: use output for feeds (no html and fancy icons) | |||
r660 | """ | |||
r899 | ||||
r660 | action = user_log.action | |||
r840 | action_params = ' ' | |||
r660 | ||||
x = action.split(':') | ||||
if len(x) > 1: | ||||
action, action_params = x | ||||
r718 | def get_cs_links(): | |||
r953 | revs_limit = 5 #display this amount always | |||
revs_top_limit = 50 #show upto this amount of changesets hidden | ||||
revs = action_params.split(',') | ||||
repo_name = user_log.repository.repo_name | ||||
r1045 | ||||
r953 | from rhodecode.model.scm import ScmModel | |||
r1045 | repo, dbrepo = ScmModel().get(repo_name, retval='repo', | |||
invalidation_list=[]) | ||||
r1040 | message = lambda rev: get_changeset_safe(repo, rev).message | |||
r899 | ||||
r953 | cs_links = " " + ', '.join ([link_to(rev, | |||
url('changeset_home', | ||||
repo_name=repo_name, | ||||
revision=rev), title=tooltip(message(rev)), | ||||
class_='tooltip') for rev in revs[:revs_limit] ]) | ||||
r1009 | ||||
compare_view = (' <div class="compare_view tooltip" title="%s">' | ||||
'<a href="%s">%s</a> ' | ||||
'</div>' % (_('Show all combined changesets %s->%s' \ | ||||
% (revs[0], revs[-1])), | ||||
url('changeset_home', repo_name=repo_name, | ||||
revision='%s...%s' % (revs[0], revs[-1]) | ||||
), | ||||
_('compare view')) | ||||
) | ||||
r953 | if len(revs) > revs_limit: | |||
uniq_id = revs[0] | ||||
html_tmpl = ('<span> %s ' | ||||
r995 | '<a class="show_more" id="_%s" href="#more">%s</a> ' | |||
r953 | '%s</span>') | |||
r1087 | if not feed: | |||
cs_links += html_tmpl % (_('and'), uniq_id, _('%s more') \ | ||||
r953 | % (len(revs) - revs_limit), | |||
_('revisions')) | ||||
r808 | ||||
r1087 | if not feed: | |||
html_tmpl = '<span id="%s" style="display:none"> %s </span>' | ||||
else: | ||||
html_tmpl = '<span id="%s"> %s </span>' | ||||
r953 | cs_links += html_tmpl % (uniq_id, ', '.join([link_to(rev, | |||
url('changeset_home', | ||||
repo_name=repo_name, revision=rev), | ||||
title=message(rev), class_='tooltip') | ||||
for rev in revs[revs_limit:revs_top_limit]])) | ||||
r1024 | if len(revs) > 1: | |||
cs_links += compare_view | ||||
r953 | return cs_links | |||
r734 | ||||
r718 | def get_fork_name(): | |||
r953 | repo_name = action_params | |||
r1055 | return _('fork name ') + str(link_to(action_params, url('summary_home', | |||
r1045 | repo_name=repo_name,))) | |||
r953 | ||||
r1087 | action_map = {'user_deleted_repo':(_('[deleted] repository'), None), | |||
r1041 | 'user_created_repo':(_('[created] repository'), None), | |||
r1055 | 'user_forked_repo':(_('[forked] repository'), get_fork_name), | |||
r1041 | 'user_updated_repo':(_('[updated] repository'), None), | |||
'admin_deleted_repo':(_('[delete] repository'), None), | ||||
'admin_created_repo':(_('[created] repository'), None), | ||||
'admin_forked_repo':(_('[forked] repository'), None), | ||||
'admin_updated_repo':(_('[updated] repository'), None), | ||||
r1052 | 'push':(_('[pushed] into'), get_cs_links), | |||
r1312 | 'push_local':(_('[committed via RhodeCode] into'), get_cs_links), | |||
r1114 | 'push_remote':(_('[pulled from remote] into'), get_cs_links), | |||
r1053 | 'pull':(_('[pulled] from'), None), | |||
r1041 | 'started_following_repo':(_('[started following] repository'), None), | |||
'stopped_following_repo':(_('[stopped following] repository'), None), | ||||
r735 | } | |||
r660 | ||||
r1087 | action_str = action_map.get(action, action) | |||
if feed: | ||||
action = action_str[0].replace('[', '').replace(']', '') | ||||
else: | ||||
action = action_str[0].replace('[', '<span class="journal_highlight">')\ | ||||
r953 | .replace(']', '</span>') | |||
r1114 | ||||
r1052 | action_params_func = lambda :"" | |||
r1114 | if callable(action_str[1]): | |||
r1052 | action_params_func = action_str[1] | |||
r953 | ||||
r1052 | return [literal(action), action_params_func] | |||
r808 | ||||
def action_parser_icon(user_log): | ||||
action = user_log.action | ||||
action_params = None | ||||
x = action.split(':') | ||||
if len(x) > 1: | ||||
action, action_params = x | ||||
r1114 | tmpl = """<img src="%s%s" alt="%s"/>""" | |||
r808 | map = {'user_deleted_repo':'database_delete.png', | |||
'user_created_repo':'database_add.png', | ||||
'user_forked_repo':'arrow_divide.png', | ||||
'user_updated_repo':'database_edit.png', | ||||
'admin_deleted_repo':'database_delete.png', | ||||
r899 | 'admin_created_repo':'database_add.png', | |||
r808 | 'admin_forked_repo':'arrow_divide.png', | |||
'admin_updated_repo':'database_edit.png', | ||||
'push':'script_add.png', | ||||
r1312 | 'push_local':'script_edit.png', | |||
r1114 | 'push_remote':'connect.png', | |||
r808 | 'pull':'down_16.png', | |||
'started_following_repo':'heart_add.png', | ||||
'stopped_following_repo':'heart_delete.png', | ||||
} | ||||
r1050 | return literal(tmpl % ((url('/images/icons/')), | |||
map.get(action, action), action)) | ||||
r660 | ||||
r635 | #============================================================================== | |||
r547 | # PERMS | |||
r635 | #============================================================================== | |||
r547 | from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \ | |||
HasRepoPermissionAny, HasRepoPermissionAll | ||||
r635 | #============================================================================== | |||
r547 | # GRAVATAR URL | |||
r635 | #============================================================================== | |||
r547 | ||||
def gravatar_url(email_address, size=30): | ||||
r1330 | if not str2bool(config['app_conf'].get('use_gravatar')) or \ | |||
email_address == 'anonymous@rhodecode.org': | ||||
r1110 | return "/images/user%s.png" % size | |||
r946 | ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme') | |||
r547 | default = 'identicon' | |||
baseurl_nossl = "http://www.gravatar.com/avatar/" | ||||
baseurl_ssl = "https://secure.gravatar.com/avatar/" | ||||
baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl | ||||
r631 | ||||
r1101 | if isinstance(email_address, unicode): | |||
#hashlib crashes on unicode items | ||||
email_address = email_address.encode('utf8', 'replace') | ||||
r547 | # construct the url | |||
gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?" | ||||
gravatar_url += urllib.urlencode({'d':default, 's':str(size)}) | ||||
return gravatar_url | ||||
r1098 | ||||
#============================================================================== | ||||
r1342 | # REPO PAGER, PAGER FOR REPOSITORY | |||
r1098 | #============================================================================== | |||
class RepoPage(Page): | ||||
def __init__(self, collection, page=1, items_per_page=20, | ||||
r1105 | item_count=None, url=None, branch_name=None, **kwargs): | |||
r1098 | ||||
"""Create a "RepoPage" instance. special pager for paging | ||||
repository | ||||
""" | ||||
self._url_generator = url | ||||
# Safe the kwargs class-wide so they can be used in the pager() method | ||||
self.kwargs = kwargs | ||||
# Save a reference to the collection | ||||
self.original_collection = collection | ||||
self.collection = collection | ||||
# The self.page is the number of the current page. | ||||
# The first page has the number 1! | ||||
try: | ||||
self.page = int(page) # make it int() if we get it as a string | ||||
except (ValueError, TypeError): | ||||
self.page = 1 | ||||
self.items_per_page = items_per_page | ||||
# Unless the user tells us how many items the collections has | ||||
# we calculate that ourselves. | ||||
if item_count is not None: | ||||
self.item_count = item_count | ||||
else: | ||||
self.item_count = len(self.collection) | ||||
# Compute the number of the first and last available page | ||||
if self.item_count > 0: | ||||
self.first_page = 1 | ||||
self.page_count = ((self.item_count - 1) / self.items_per_page) + 1 | ||||
self.last_page = self.first_page + self.page_count - 1 | ||||
# Make sure that the requested page number is the range of valid pages | ||||
if self.page > self.last_page: | ||||
self.page = self.last_page | ||||
elif self.page < self.first_page: | ||||
self.page = self.first_page | ||||
# Note: the number of items on this page can be less than | ||||
# items_per_page if the last page is not full | ||||
self.first_item = max(0, (self.item_count) - (self.page * items_per_page)) | ||||
r1105 | self.last_item = ((self.item_count - 1) - items_per_page * (self.page - 1)) | |||
r1098 | ||||
iterator = self.collection.get_changesets(start=self.first_item, | ||||
end=self.last_item, | ||||
r1105 | reverse=True, | |||
branch_name=branch_name) | ||||
r1098 | self.items = list(iterator) | |||
# Links to previous and next page | ||||
if self.page > self.first_page: | ||||
self.previous_page = self.page - 1 | ||||
else: | ||||
self.previous_page = None | ||||
if self.page < self.last_page: | ||||
self.next_page = self.page + 1 | ||||
else: | ||||
self.next_page = None | ||||
# No items available | ||||
else: | ||||
self.first_page = None | ||||
self.page_count = 0 | ||||
self.last_page = None | ||||
self.first_item = None | ||||
self.last_item = None | ||||
self.previous_page = None | ||||
self.next_page = None | ||||
self.items = [] | ||||
# This is a subclass of the 'list' type. Initialise the list now. | ||||
list.__init__(self, self.items) | ||||
r990 | def changed_tooltip(nodes): | |||
r1342 | """ | |||
Generates a html string for changed nodes in changeset page. | ||||
It limits the output to 30 entries | ||||
:param nodes: LazyNodesGenerator | ||||
""" | ||||
r990 | if nodes: | |||
pref = ': <br/> ' | ||||
suf = '' | ||||
if len(nodes) > 30: | ||||
suf = '<br/>' + _(' and %s more') % (len(nodes) - 30) | ||||
r1257 | return literal(pref + '<br/> '.join([safe_unicode(x.path) | |||
for x in nodes[:30]]) + suf) | ||||
r990 | else: | |||
return ': ' + _('No Files') | ||||
r1159 | ||||
def repo_link(groups_and_repos): | ||||
r1342 | """ | |||
Makes a breadcrumbs link to repo within a group | ||||
joins » on each group to create a fancy link | ||||
ex:: | ||||
group >> subgroup >> repo | ||||
:param groups_and_repos: | ||||
""" | ||||
r1159 | groups, repo_name = groups_and_repos | |||
if not groups: | ||||
return repo_name | ||||
else: | ||||
def make_link(group): | ||||
r1257 | return link_to(group.group_name, url('repos_group', | |||
id=group.group_id)) | ||||
r1159 | return literal(' » '.join(map(make_link, groups)) + \ | |||
" » " + repo_name) | ||||
r1257 | ||||
def fancy_file_stats(stats): | ||||
r1342 | """ | |||
Displays a fancy two colored bar for number of added/deleted | ||||
lines of code on file | ||||
:param stats: two element list of added/deleted lines of code | ||||
""" | ||||
r1257 | a, d, t = stats[0], stats[1], stats[0] + stats[1] | |||
width = 100 | ||||
r1258 | unit = float(width) / (t or 1) | |||
r1257 | ||||
r1342 | # needs > 9% of width to be visible or 0 to be hidden | |||
a_p = max(9, unit * a) if a > 0 else 0 | ||||
d_p = max(9, unit * d) if d > 0 else 0 | ||||
r1257 | p_sum = a_p + d_p | |||
if p_sum > width: | ||||
#adjust the percentage to be == 100% since we adjusted to 9 | ||||
if a_p > d_p: | ||||
a_p = a_p - (p_sum - width) | ||||
else: | ||||
d_p = d_p - (p_sum - width) | ||||
a_v = a if a > 0 else '' | ||||
d_v = d if d > 0 else '' | ||||
def cgen(l_type): | ||||
mapping = {'tr':'top-right-rounded-corner', | ||||
'tl':'top-left-rounded-corner', | ||||
'br':'bottom-right-rounded-corner', | ||||
'bl':'bottom-left-rounded-corner'} | ||||
map_getter = lambda x:mapping[x] | ||||
if l_type == 'a' and d_v: | ||||
#case when added and deleted are present | ||||
return ' '.join(map(map_getter, ['tl', 'bl'])) | ||||
if l_type == 'a' and not d_v: | ||||
return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl'])) | ||||
if l_type == 'd' and a_v: | ||||
return ' '.join(map(map_getter, ['tr', 'br'])) | ||||
if l_type == 'd' and not a_v: | ||||
return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl'])) | ||||
d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'), | ||||
a_p, a_v) | ||||
d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'), | ||||
d_p, d_v) | ||||
return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d)) | ||||