diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -7,11 +7,12 @@ import random import hashlib import StringIO import urllib +import math from datetime import datetime from pygments.formatters import HtmlFormatter from pygments import highlight as code_highlight -from pylons import url, app_globals as g +from pylons import url, request, config from pylons.i18n.translation import _, ungettext from webhelpers.html import literal, HTML, escape @@ -36,7 +37,7 @@ from webhelpers.html.tags import _set_in from vcs.utils.annotate import annotate_highlight from rhodecode.lib.utils import repo_name_slug -from rhodecode.lib import str2bool, safe_unicode +from rhodecode.lib import str2bool, safe_unicode, safe_str,get_changeset_safe def _reset(name, value=None, id=NotGiven, type="reset", **attrs): """ @@ -89,45 +90,7 @@ class _ToolTip(object): :param tooltip_title: """ - - return wrap_paragraphs(escape(tooltip_title), trim_at)\ - .replace('\n', '
') - - def activate(self): - """Adds tooltip mechanism to the given Html all tooltips have to have - set class `tooltip` and set attribute `tooltip_title`. - Then a tooltip will be generated based on that. All with yui js tooltip - """ - - js = ''' - YAHOO.util.Event.onDOMReady(function(){ - function toolTipsId(){ - var ids = []; - var tts = YAHOO.util.Dom.getElementsByClassName('tooltip'); - - for (var i = 0; i < tts.length; i++) { - //if element doesn't not have and id autogenerate one for tooltip - - if (!tts[i].id){ - tts[i].id='tt'+i*100; - } - ids.push(tts[i].id); - } - return ids - }; - var myToolTips = new YAHOO.widget.Tooltip("tooltip", { - context: [[toolTipsId()],"tl","bl",null,[0,5]], - monitorresize:false, - xyoffset :[0,0], - autodismissdelay:300000, - hidedelay:5, - showdelay:20, - }); - - }); - ''' - return literal(js) - + return escape(tooltip_title) tooltip = _ToolTip() class _FilesBreadCrumbs(object): @@ -160,19 +123,84 @@ class CodeHtmlFormatter(HtmlFormatter): def _wrap_code(self, source): for cnt, it in enumerate(source): i, t = it - t = '
%s
' % (cnt + 1, t) + t = '
%s
' % (cnt + 1, t) yield i, t + + 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('%*d' % + (la, i, mw, i)) + else: + lines.append('%*d' % (mw, i)) + else: + if aln: + lines.append('%*d' % (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('%*d' % (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
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, ('' % self.cssclass + + '
' + '
' +
+                      ls + '
') + else: + yield 0, ('' % self.cssclass + + '
' +
+                      ls + '
') + yield 0, dummyoutfile.getvalue() + yield 0, '
' + + def pygmentize(filenode, **kwargs): - """ - pygmentize function using pygments + """pygmentize function using pygments + :param filenode: """ + return literal(code_highlight(filenode.content, filenode.lexer, CodeHtmlFormatter(**kwargs))) -def pygmentize_annotation(filenode, **kwargs): - """ - pygmentize function for annotation +def pygmentize_annotation(repo_name, filenode, **kwargs): + """pygmentize function for annotation + :param filenode: """ @@ -183,15 +211,30 @@ def pygmentize_annotation(filenode, **kw :returns: RGB tuple """ - import colorsys + + def hsv_to_rgb(h, s, v): + if s == 0.0: return v, v, v + i = int(h * 6.0) # XXX assume int() truncates! + f = (h * 6.0) - i + p = v * (1.0 - s) + q = v * (1.0 - s * f) + t = v * (1.0 - s * (1.0 - f)) + i = i % 6 + if i == 0: return v, t, p + if i == 1: return q, v, p + if i == 2: return p, v, t + if i == 3: return p, q, v + if i == 4: return t, p, v + if i == 5: return v, p, q + golden_ratio = 0.618033988749895 h = 0.22717784590367374 - for c in xrange(n): + for _ in xrange(n): h += golden_ratio h %= 1 HSV_tuple = [h, 0.95, 0.95] - RGB_tuple = colorsys.hsv_to_rgb(*HSV_tuple) + RGB_tuple = hsv_to_rgb(*HSV_tuple) yield map(lambda x:str(int(x * 256)), RGB_tuple) cgenerator = gen_color() @@ -203,83 +246,54 @@ def pygmentize_annotation(filenode, **kw col = color_dict[cs] = cgenerator.next() return "color: rgb(%s)! important;" % (', '.join(col)) - def url_func(changeset): - tooltip_html = "
Author:" + \ - " %s
Date: %s
Message: %s
" + def url_func(repo_name): - tooltip_html = tooltip_html % (changeset.author, - changeset.date, - tooltip(changeset.message)) - lnk_format = '%5s:%s' % ('r%s' % changeset.revision, - short_id(changeset.raw_id)) - uri = link_to( - lnk_format, - url('changeset_home', repo_name=changeset.repository.name, - revision=changeset.raw_id), - style=get_color_string(changeset.raw_id), - class_='tooltip', - title=tooltip_html - ) + def _url_func(changeset): + author = changeset.author + date = changeset.date + message = tooltip(changeset.message) + + tooltip_html = ("
Author:" + " %s
Date: %s
Message:" + " %s
") - uri += '\n' - return uri - return literal(annotate_highlight(filenode, url_func, **kwargs)) + tooltip_html = tooltip_html % (author, date, message) + 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 + ) -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)) + uri += '\n' + return uri + return _url_func - try: - cs = repo.get_changeset(rev) - except RepositoryError: - from rhodecode.lib.utils import EmptyChangeset - cs = EmptyChangeset() - return cs + return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs)) +def is_following_repo(repo_name, user_id): + from rhodecode.model.scm import ScmModel + return ScmModel().is_following_repo(repo_name, user_id) flash = _Flash() - #============================================================================== -# MERCURIAL FILTERS available via h. +# SCM FILTERS available via h. #============================================================================== -from mercurial import util -from mercurial.templatefilters import person as _person - -def _age(curdate): - """turns a datetime into an age string.""" - - if not curdate: - return '' - - agescales = [("year", 3600 * 24 * 365), - ("month", 3600 * 24 * 30), - ("day", 3600 * 24), - ("hour", 3600), - ("minute", 60), - ("second", 1), ] - - age = datetime.now() - curdate - age_seconds = (age.days * agescales[2][1]) + age.seconds - pos = 1 - for scale in agescales: - if scale[1] <= age_seconds: - if pos == 6:pos = 5 - return time_ago_in_words(curdate, agescales[pos][0]) + ' ' + _('ago') - pos += 1 - - return _('just now') +from vcs.utils import author_name, author_email +from rhodecode.lib import credentials_filter, age as _age age = lambda x:_age(x) capitalize = lambda x: x.capitalize() -email = util.email -email_or_none = lambda x: util.email(x) if util.email(x) != x else None -person = lambda x: _person(x) +email = author_email +email_or_none = lambda x: email(x) if email(x) != x else None +person = lambda x: author_name(x) short_id = lambda x: x[:12] - +hide_credentials = lambda x: ''.join(credentials_filter(x)) def bool2icon(value): """Returns True/False values represented as small html image of true/false @@ -299,13 +313,14 @@ def bool2icon(value): return value -def action_parser(user_log): - """ - This helper will map the specified string action into translated +def action_parser(user_log, feed=False): + """This helper will action_map the specified string action into translated fancy names with icons and links - - @param action: + + :param user_log: user log instance + :param feed: use output for feeds (no html and fancy icons) """ + action = user_log.action action_params = ' ' @@ -315,56 +330,90 @@ def action_parser(user_log): action, action_params = x def get_cs_links(): - revs_limit = 5 + revs_limit = 3 #display this amount always + revs_top_limit = 50 #show upto this amount of changesets hidden revs = action_params.split(',') - cs_links = " " + ', '.join ([link_to(rev, url('changeset_home', - repo_name=user_log.repository.repo_name, - revision=rev)) for rev in revs[:revs_limit]]) + repo_name = user_log.repository.repo_name + + from rhodecode.model.scm import ScmModel + repo = user_log.repository.scm_instance + + message = lambda rev: get_changeset_safe(repo, rev).message + cs_links = [] + cs_links.append(" " + ', '.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] ])) + + compare_view = ('
' + '%s ' + '
' % (_('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')) + ) + if len(revs) > revs_limit: uniq_id = revs[0] html_tmpl = (' %s ' - '%s ' + '%s ' '%s') - cs_links += html_tmpl % (_('and'), uniq_id, _('%s more') \ + if not feed: + cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \ % (len(revs) - revs_limit), - _('revisions')) + _('revisions'))) - html_tmpl = '' - cs_links += html_tmpl % (uniq_id, ', '.join([link_to(rev, + if not feed: + html_tmpl = '' + else: + html_tmpl = ' %s ' + + cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev, url('changeset_home', - repo_name=user_log.repository.repo_name, - revision=rev)) for rev in revs[revs_limit:] ])) - - return cs_links + repo_name=repo_name, revision=rev), + title=message(rev), class_='tooltip') + for rev in revs[revs_limit:revs_top_limit]]))) + if len(revs) > 1: + cs_links.append(compare_view) + return ''.join(cs_links) def get_fork_name(): repo_name = action_params - return str(link_to(action_params, url('summary_home', + return _('fork name ') + str(link_to(action_params, url('summary_home', repo_name=repo_name,))) - map = {'user_deleted_repo':(_('[deleted] repository'), None), + action_map = {'user_deleted_repo':(_('[deleted] repository'), None), 'user_created_repo':(_('[created] repository'), None), - 'user_forked_repo':(_('[forked] repository as'), get_fork_name), + 'user_forked_repo':(_('[forked] repository'), get_fork_name), '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), - 'push':(_('[pushed] '), get_cs_links), - 'pull':(_('[pulled] '), None), + 'push':(_('[pushed] into'), get_cs_links), + 'push_local':(_('[committed via RhodeCode] into'), get_cs_links), + 'push_remote':(_('[pulled from remote] into'), get_cs_links), + 'pull':(_('[pulled] from'), None), 'started_following_repo':(_('[started following] repository'), None), 'stopped_following_repo':(_('[stopped following] repository'), None), } - action_str = map.get(action, action) - action = action_str[0].replace('[', '')\ + action_str = action_map.get(action, action) + if feed: + action = action_str[0].replace('[', '').replace(']', '') + else: + action = action_str[0].replace('[', '')\ .replace(']', '') + action_params_func = lambda :"" - if action_str[1] is not None: + if callable(action_str[1]): action_params_func = action_str[1] - return literal(action + " " + action_params_func()) + return [literal(action), action_params_func] def action_parser_icon(user_log): action = user_log.action @@ -384,6 +433,7 @@ def action_parser_icon(user_log): 'admin_forked_repo':'arrow_divide.png', 'admin_updated_repo':'database_edit.png', 'push':'script_add.png', + 'push_local':'script_edit.png', 'push_remote':'connect.png', 'pull':'down_16.png', 'started_following_repo':'heart_add.png', @@ -402,9 +452,12 @@ HasRepoPermissionAny, HasRepoPermissionA #============================================================================== # GRAVATAR URL #============================================================================== -from pylons import request def gravatar_url(email_address, size=30): + if not str2bool(config['app_conf'].get('use_gravatar')) or \ + email_address == 'anonymous@rhodecode.org': + return url("/images/user%s.png" % size) + ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme') default = 'identicon' baseurl_nossl = "http://www.gravatar.com/avatar/" @@ -413,19 +466,208 @@ def gravatar_url(email_address, size=30) if isinstance(email_address, unicode): #hashlib crashes on unicode items - email_address = email_address.encode('utf8', 'replace') + email_address = safe_str(email_address) # construct the url gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?" gravatar_url += urllib.urlencode({'d':default, 's':str(size)}) return gravatar_url + +#============================================================================== +# REPO PAGER, PAGER FOR REPOSITORY +#============================================================================== +class RepoPage(Page): + + def __init__(self, collection, page=1, items_per_page=20, + item_count=None, url=None, branch_name=None, **kwargs): + + """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 = int(math.ceil(float(self.item_count) / + self.items_per_page)) + 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)) + self.last_item = ((self.item_count - 1) - items_per_page * + (self.page - 1)) + + iterator = self.collection.get_changesets(start=self.first_item, + end=self.last_item, + reverse=True, + branch_name=branch_name) + 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) + + def changed_tooltip(nodes): + """ + Generates a html string for changed nodes in changeset page. + It limits the output to 30 entries + + :param nodes: LazyNodesGenerator + """ if nodes: pref = ':
' suf = '' if len(nodes) > 30: suf = '
' + _(' and %s more') % (len(nodes) - 30) - return literal(pref + '
'.join([safe_unicode(x.path) for x in nodes[:30]]) + suf) + return literal(pref + '
'.join([safe_unicode(x.path) + for x in nodes[:30]]) + suf) else: return ': ' + _('No Files') + + + +def repo_link(groups_and_repos): + """ + 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: + """ + groups, repo_name = groups_and_repos + + if not groups: + return repo_name + else: + def make_link(group): + return link_to(group.group_name, url('repos_group', + id=group.group_id)) + return literal(' » '.join(map(make_link, groups)) + \ + " » " + repo_name) + + +def fancy_file_stats(stats): + """ + 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 + """ + + a, d, t = stats[0], stats[1], stats[0] + stats[1] + width = 100 + unit = float(width) / (t or 1) + + # 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 + 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 = '
%s
' % (cgen('a'), + a_p, a_v) + d_d = '
%s
' % (cgen('d'), + d_p, d_v) + return literal('
%s%s
' % (width, d_a, d_d)) + + +def urlify_text(text): + import re + + url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)') + + def url_func(match_obj): + url_full = match_obj.groups()[0] + return '%(url)s' % ({'url':url_full}) + + return literal(url_pat.sub(url_func, text))