##// END OF EJS Templates
forms: don't use secure forms with authentication token for GET requests...
Mads Kiilerich -
r5524:1346754f stable
parent child Browse files
Show More
@@ -1,198 +1,197 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.changelog
15 kallithea.controllers.changelog
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 changelog controller for Kallithea
18 changelog controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 21, 2010
22 :created_on: Apr 21, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30
30
31 from pylons import request, url, session, tmpl_context as c
31 from pylons import request, url, session, tmpl_context as c
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34 from webob.exc import HTTPNotFound, HTTPBadRequest
34 from webob.exc import HTTPNotFound, HTTPBadRequest
35
35
36 import kallithea.lib.helpers as h
36 import kallithea.lib.helpers as h
37 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 from kallithea.lib.base import BaseRepoController, render
38 from kallithea.lib.base import BaseRepoController, render
39 from kallithea.lib.helpers import RepoPage
39 from kallithea.lib.helpers import RepoPage
40 from kallithea.lib.compat import json
40 from kallithea.lib.compat import json
41 from kallithea.lib.graphmod import graph_data
41 from kallithea.lib.graphmod import graph_data
42 from kallithea.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError,\
42 from kallithea.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError,\
43 ChangesetError, NodeDoesNotExistError, EmptyRepositoryError
43 ChangesetError, NodeDoesNotExistError, EmptyRepositoryError
44 from kallithea.lib.utils2 import safe_int, safe_str
44 from kallithea.lib.utils2 import safe_int, safe_str
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 def _load_changelog_summary():
50 def _load_changelog_summary():
51 p = safe_int(request.GET.get('page'), 1)
51 p = safe_int(request.GET.get('page'), 1)
52 size = safe_int(request.GET.get('size'), 10)
52 size = safe_int(request.GET.get('size'), 10)
53
53
54 def url_generator(**kw):
54 def url_generator(**kw):
55 return url('changelog_summary_home',
55 return url('changelog_summary_home',
56 repo_name=c.db_repo.repo_name, size=size, **kw)
56 repo_name=c.db_repo.repo_name, size=size, **kw)
57
57
58 collection = c.db_repo_scm_instance
58 collection = c.db_repo_scm_instance
59
59
60 c.repo_changesets = RepoPage(collection, page=p,
60 c.repo_changesets = RepoPage(collection, page=p,
61 items_per_page=size,
61 items_per_page=size,
62 url=url_generator)
62 url=url_generator)
63 page_revisions = [x.raw_id for x in list(c.repo_changesets)]
63 page_revisions = [x.raw_id for x in list(c.repo_changesets)]
64 c.comments = c.db_repo.get_comments(page_revisions)
64 c.comments = c.db_repo.get_comments(page_revisions)
65 c.statuses = c.db_repo.statuses(page_revisions)
65 c.statuses = c.db_repo.statuses(page_revisions)
66
66
67
67
68 class ChangelogController(BaseRepoController):
68 class ChangelogController(BaseRepoController):
69
69
70 def __before__(self):
70 def __before__(self):
71 super(ChangelogController, self).__before__()
71 super(ChangelogController, self).__before__()
72 c.affected_files_cut_off = 60
72 c.affected_files_cut_off = 60
73
73
74 @staticmethod
74 @staticmethod
75 def __get_cs(rev, repo):
75 def __get_cs(rev, repo):
76 """
76 """
77 Safe way to get changeset. If error occur fail with error message.
77 Safe way to get changeset. If error occur fail with error message.
78
78
79 :param rev: revision to fetch
79 :param rev: revision to fetch
80 :param repo: repo instance
80 :param repo: repo instance
81 """
81 """
82
82
83 try:
83 try:
84 return c.db_repo_scm_instance.get_changeset(rev)
84 return c.db_repo_scm_instance.get_changeset(rev)
85 except EmptyRepositoryError as e:
85 except EmptyRepositoryError as e:
86 h.flash(h.literal(_('There are no changesets yet')),
86 h.flash(h.literal(_('There are no changesets yet')),
87 category='error')
87 category='error')
88 except RepositoryError as e:
88 except RepositoryError as e:
89 log.error(traceback.format_exc())
89 log.error(traceback.format_exc())
90 h.flash(safe_str(e), category='error')
90 h.flash(safe_str(e), category='error')
91 raise HTTPBadRequest()
91 raise HTTPBadRequest()
92
92
93 @LoginRequired()
93 @LoginRequired()
94 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
94 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
95 'repository.admin')
95 'repository.admin')
96 def index(self, repo_name, revision=None, f_path=None):
96 def index(self, repo_name, revision=None, f_path=None):
97 # Fix URL after page size form submission via GET
97 # Fix URL after page size form submission via GET
98 # TODO: Somehow just don't send this extra junk in the GET URL
98 # TODO: Somehow just don't send this extra junk in the GET URL
99 if request.GET.get('set'):
99 if request.GET.get('set'):
100 request.GET.pop('set', None)
100 request.GET.pop('set', None)
101 request.GET.pop('_authentication_token', None)
102 if revision is None:
101 if revision is None:
103 return redirect(url('changelog_home', repo_name=repo_name, **request.GET))
102 return redirect(url('changelog_home', repo_name=repo_name, **request.GET))
104 return redirect(url('changelog_file_home', repo_name=repo_name, revision=revision, f_path=f_path, **request.GET))
103 return redirect(url('changelog_file_home', repo_name=repo_name, revision=revision, f_path=f_path, **request.GET))
105
104
106 limit = 2000
105 limit = 2000
107 default = 100
106 default = 100
108 if request.GET.get('size'):
107 if request.GET.get('size'):
109 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
108 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
110 session['changelog_size'] = c.size
109 session['changelog_size'] = c.size
111 session.save()
110 session.save()
112 else:
111 else:
113 c.size = int(session.get('changelog_size', default))
112 c.size = int(session.get('changelog_size', default))
114 # min size must be 1
113 # min size must be 1
115 c.size = max(c.size, 1)
114 c.size = max(c.size, 1)
116 p = safe_int(request.GET.get('page', 1), 1)
115 p = safe_int(request.GET.get('page', 1), 1)
117 branch_name = request.GET.get('branch', None)
116 branch_name = request.GET.get('branch', None)
118 if (branch_name and
117 if (branch_name and
119 branch_name not in c.db_repo_scm_instance.branches and
118 branch_name not in c.db_repo_scm_instance.branches and
120 branch_name not in c.db_repo_scm_instance.closed_branches and
119 branch_name not in c.db_repo_scm_instance.closed_branches and
121 not revision):
120 not revision):
122 return redirect(url('changelog_file_home', repo_name=c.repo_name,
121 return redirect(url('changelog_file_home', repo_name=c.repo_name,
123 revision=branch_name, f_path=f_path or ''))
122 revision=branch_name, f_path=f_path or ''))
124
123
125 if revision == 'tip':
124 if revision == 'tip':
126 revision = None
125 revision = None
127
126
128 c.changelog_for_path = f_path
127 c.changelog_for_path = f_path
129 try:
128 try:
130
129
131 if f_path:
130 if f_path:
132 log.debug('generating changelog for path %s', f_path)
131 log.debug('generating changelog for path %s', f_path)
133 # get the history for the file !
132 # get the history for the file !
134 tip_cs = c.db_repo_scm_instance.get_changeset()
133 tip_cs = c.db_repo_scm_instance.get_changeset()
135 try:
134 try:
136 collection = tip_cs.get_file_history(f_path)
135 collection = tip_cs.get_file_history(f_path)
137 except (NodeDoesNotExistError, ChangesetError):
136 except (NodeDoesNotExistError, ChangesetError):
138 #this node is not present at tip !
137 #this node is not present at tip !
139 try:
138 try:
140 cs = self.__get_cs(revision, repo_name)
139 cs = self.__get_cs(revision, repo_name)
141 collection = cs.get_file_history(f_path)
140 collection = cs.get_file_history(f_path)
142 except RepositoryError as e:
141 except RepositoryError as e:
143 h.flash(safe_str(e), category='warning')
142 h.flash(safe_str(e), category='warning')
144 redirect(h.url('changelog_home', repo_name=repo_name))
143 redirect(h.url('changelog_home', repo_name=repo_name))
145 collection = list(reversed(collection))
144 collection = list(reversed(collection))
146 else:
145 else:
147 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
146 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
148 branch_name=branch_name)
147 branch_name=branch_name)
149 c.total_cs = len(collection)
148 c.total_cs = len(collection)
150
149
151 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
150 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
152 items_per_page=c.size, branch=branch_name,)
151 items_per_page=c.size, branch=branch_name,)
153
152
154 page_revisions = [x.raw_id for x in c.pagination]
153 page_revisions = [x.raw_id for x in c.pagination]
155 c.comments = c.db_repo.get_comments(page_revisions)
154 c.comments = c.db_repo.get_comments(page_revisions)
156 c.statuses = c.db_repo.statuses(page_revisions)
155 c.statuses = c.db_repo.statuses(page_revisions)
157 except EmptyRepositoryError as e:
156 except EmptyRepositoryError as e:
158 h.flash(safe_str(e), category='warning')
157 h.flash(safe_str(e), category='warning')
159 return redirect(url('summary_home', repo_name=c.repo_name))
158 return redirect(url('summary_home', repo_name=c.repo_name))
160 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
159 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
161 log.error(traceback.format_exc())
160 log.error(traceback.format_exc())
162 h.flash(safe_str(e), category='error')
161 h.flash(safe_str(e), category='error')
163 return redirect(url('changelog_home', repo_name=c.repo_name))
162 return redirect(url('changelog_home', repo_name=c.repo_name))
164
163
165 c.branch_name = branch_name
164 c.branch_name = branch_name
166 c.branch_filters = [('', _('None'))] + \
165 c.branch_filters = [('', _('None'))] + \
167 [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
166 [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
168 if c.db_repo_scm_instance.closed_branches:
167 if c.db_repo_scm_instance.closed_branches:
169 prefix = _('(closed)') + ' '
168 prefix = _('(closed)') + ' '
170 c.branch_filters += [('-', '-')] + \
169 c.branch_filters += [('-', '-')] + \
171 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
170 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
172 revs = []
171 revs = []
173 if not f_path:
172 if not f_path:
174 revs = [x.revision for x in c.pagination]
173 revs = [x.revision for x in c.pagination]
175 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
174 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
176
175
177 c.revision = revision # requested revision ref
176 c.revision = revision # requested revision ref
178 c.first_revision = c.pagination[0] # pagination is never empty here!
177 c.first_revision = c.pagination[0] # pagination is never empty here!
179 return render('changelog/changelog.html')
178 return render('changelog/changelog.html')
180
179
181 @LoginRequired()
180 @LoginRequired()
182 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
181 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
183 'repository.admin')
182 'repository.admin')
184 def changelog_details(self, cs):
183 def changelog_details(self, cs):
185 if request.environ.get('HTTP_X_PARTIAL_XHR'):
184 if request.environ.get('HTTP_X_PARTIAL_XHR'):
186 c.cs = c.db_repo_scm_instance.get_changeset(cs)
185 c.cs = c.db_repo_scm_instance.get_changeset(cs)
187 return render('changelog/changelog_details.html')
186 return render('changelog/changelog_details.html')
188 raise HTTPNotFound()
187 raise HTTPNotFound()
189
188
190 @LoginRequired()
189 @LoginRequired()
191 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
190 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
192 'repository.admin')
191 'repository.admin')
193 def changelog_summary(self, repo_name):
192 def changelog_summary(self, repo_name):
194 if request.environ.get('HTTP_X_PARTIAL_XHR'):
193 if request.environ.get('HTTP_X_PARTIAL_XHR'):
195 _load_changelog_summary()
194 _load_changelog_summary()
196
195
197 return render('changelog/changelog_summary_data.html')
196 return render('changelog/changelog_summary_data.html')
198 raise HTTPNotFound()
197 raise HTTPNotFound()
@@ -1,1453 +1,1464 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 Helper functions
15 Helper functions
16
16
17 Consists of functions to typically be used within templates, but also
17 Consists of functions to typically be used within templates, but also
18 available to Controllers. This module is available to both as 'h'.
18 available to Controllers. This module is available to both as 'h'.
19 """
19 """
20 import hashlib
20 import hashlib
21 import StringIO
21 import StringIO
22 import math
22 import math
23 import logging
23 import logging
24 import re
24 import re
25 import urlparse
25 import urlparse
26 import textwrap
26 import textwrap
27
27
28 from pygments.formatters.html import HtmlFormatter
28 from pygments.formatters.html import HtmlFormatter
29 from pygments import highlight as code_highlight
29 from pygments import highlight as code_highlight
30 from pylons import url
30 from pylons import url
31 from pylons.i18n.translation import _, ungettext
31 from pylons.i18n.translation import _, ungettext
32
32
33 from webhelpers.html import literal, HTML, escape
33 from webhelpers.html import literal, HTML, escape
34 from webhelpers.html.tools import *
34 from webhelpers.html.tools import *
35 from webhelpers.html.builder import make_tag
35 from webhelpers.html.builder import make_tag
36 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
36 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
37 end_form, file, hidden, image, javascript_link, link_to, \
37 end_form, file, hidden, image, javascript_link, link_to, \
38 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
38 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
39 submit, text, password, textarea, title, ul, xml_declaration, radio
39 submit, text, password, textarea, title, ul, xml_declaration, radio, \
40 form as insecure_form
40 from webhelpers.html.tools import auto_link, button_to, highlight, \
41 from webhelpers.html.tools import auto_link, button_to, highlight, \
41 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
42 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
42 from webhelpers.number import format_byte_size, format_bit_size
43 from webhelpers.number import format_byte_size, format_bit_size
43 from webhelpers.pylonslib import Flash as _Flash
44 from webhelpers.pylonslib import Flash as _Flash
44 from webhelpers.pylonslib.secure_form import secure_form as form, authentication_token
45 from webhelpers.pylonslib.secure_form import secure_form, authentication_token
45 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
46 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
46 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
47 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
47 replace_whitespace, urlify, truncate, wrap_paragraphs
48 replace_whitespace, urlify, truncate, wrap_paragraphs
48 from webhelpers.date import time_ago_in_words
49 from webhelpers.date import time_ago_in_words
49 from webhelpers.paginate import Page as _Page
50 from webhelpers.paginate import Page as _Page
50 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
51 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
51 convert_boolean_attrs, NotGiven, _make_safe_id_component
52 convert_boolean_attrs, NotGiven, _make_safe_id_component
52
53
53 from kallithea.lib.annotate import annotate_highlight
54 from kallithea.lib.annotate import annotate_highlight
54 from kallithea.lib.utils import repo_name_slug, get_custom_lexer
55 from kallithea.lib.utils import repo_name_slug, get_custom_lexer
55 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
56 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
56 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict,\
57 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict,\
57 safe_int
58 safe_int
58 from kallithea.lib.markup_renderer import MarkupRenderer, url_re
59 from kallithea.lib.markup_renderer import MarkupRenderer, url_re
59 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
60 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
60 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
61 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
61 from kallithea.config.conf import DATE_FORMAT, DATETIME_FORMAT
62 from kallithea.config.conf import DATE_FORMAT, DATETIME_FORMAT
62 from kallithea.model.changeset_status import ChangesetStatusModel
63 from kallithea.model.changeset_status import ChangesetStatusModel
63 from kallithea.model.db import URL_SEP, Permission
64 from kallithea.model.db import URL_SEP, Permission
64
65
65 log = logging.getLogger(__name__)
66 log = logging.getLogger(__name__)
66
67
67
68
68 def canonical_url(*args, **kargs):
69 def canonical_url(*args, **kargs):
69 '''Like url(x, qualified=True), but returns url that not only is qualified
70 '''Like url(x, qualified=True), but returns url that not only is qualified
70 but also canonical, as configured in canonical_url'''
71 but also canonical, as configured in canonical_url'''
71 from kallithea import CONFIG
72 from kallithea import CONFIG
72 try:
73 try:
73 parts = CONFIG.get('canonical_url', '').split('://', 1)
74 parts = CONFIG.get('canonical_url', '').split('://', 1)
74 kargs['host'] = parts[1].split('/', 1)[0]
75 kargs['host'] = parts[1].split('/', 1)[0]
75 kargs['protocol'] = parts[0]
76 kargs['protocol'] = parts[0]
76 except IndexError:
77 except IndexError:
77 kargs['qualified'] = True
78 kargs['qualified'] = True
78 return url(*args, **kargs)
79 return url(*args, **kargs)
79
80
80 def canonical_hostname():
81 def canonical_hostname():
81 '''Return canonical hostname of system'''
82 '''Return canonical hostname of system'''
82 from kallithea import CONFIG
83 from kallithea import CONFIG
83 try:
84 try:
84 parts = CONFIG.get('canonical_url', '').split('://', 1)
85 parts = CONFIG.get('canonical_url', '').split('://', 1)
85 return parts[1].split('/', 1)[0]
86 return parts[1].split('/', 1)[0]
86 except IndexError:
87 except IndexError:
87 parts = url('home', qualified=True).split('://', 1)
88 parts = url('home', qualified=True).split('://', 1)
88 return parts[1].split('/', 1)[0]
89 return parts[1].split('/', 1)[0]
89
90
90 def html_escape(s):
91 def html_escape(s):
91 """Return string with all html escaped.
92 """Return string with all html escaped.
92 This is also safe for javascript in html but not necessarily correct.
93 This is also safe for javascript in html but not necessarily correct.
93 """
94 """
94 return (s
95 return (s
95 .replace('&', '&amp;')
96 .replace('&', '&amp;')
96 .replace(">", "&gt;")
97 .replace(">", "&gt;")
97 .replace("<", "&lt;")
98 .replace("<", "&lt;")
98 .replace('"', "&quot;")
99 .replace('"', "&quot;")
99 .replace("'", "&apos;")
100 .replace("'", "&apos;")
100 )
101 )
101
102
102 def shorter(s, size=20):
103 def shorter(s, size=20):
103 postfix = '...'
104 postfix = '...'
104 if len(s) > size:
105 if len(s) > size:
105 return s[:size - len(postfix)] + postfix
106 return s[:size - len(postfix)] + postfix
106 return s
107 return s
107
108
108
109
109 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
110 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
110 """
111 """
111 Reset button
112 Reset button
112 """
113 """
113 _set_input_attrs(attrs, type, name, value)
114 _set_input_attrs(attrs, type, name, value)
114 _set_id_attr(attrs, id, name)
115 _set_id_attr(attrs, id, name)
115 convert_boolean_attrs(attrs, ["disabled"])
116 convert_boolean_attrs(attrs, ["disabled"])
116 return HTML.input(**attrs)
117 return HTML.input(**attrs)
117
118
118 reset = _reset
119 reset = _reset
119 safeid = _make_safe_id_component
120 safeid = _make_safe_id_component
120
121
121
122
122 def FID(raw_id, path):
123 def FID(raw_id, path):
123 """
124 """
124 Creates a unique ID for filenode based on it's hash of path and revision
125 Creates a unique ID for filenode based on it's hash of path and revision
125 it's safe to use in urls
126 it's safe to use in urls
126
127
127 :param raw_id:
128 :param raw_id:
128 :param path:
129 :param path:
129 """
130 """
130
131
131 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
132 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
132
133
133
134
134 class _GetError(object):
135 class _GetError(object):
135 """Get error from form_errors, and represent it as span wrapped error
136 """Get error from form_errors, and represent it as span wrapped error
136 message
137 message
137
138
138 :param field_name: field to fetch errors for
139 :param field_name: field to fetch errors for
139 :param form_errors: form errors dict
140 :param form_errors: form errors dict
140 """
141 """
141
142
142 def __call__(self, field_name, form_errors):
143 def __call__(self, field_name, form_errors):
143 tmpl = """<span class="error_msg">%s</span>"""
144 tmpl = """<span class="error_msg">%s</span>"""
144 if form_errors and field_name in form_errors:
145 if form_errors and field_name in form_errors:
145 return literal(tmpl % form_errors.get(field_name))
146 return literal(tmpl % form_errors.get(field_name))
146
147
147 get_error = _GetError()
148 get_error = _GetError()
148
149
149
150
150 class _FilesBreadCrumbs(object):
151 class _FilesBreadCrumbs(object):
151
152
152 def __call__(self, repo_name, rev, paths):
153 def __call__(self, repo_name, rev, paths):
153 if isinstance(paths, str):
154 if isinstance(paths, str):
154 paths = safe_unicode(paths)
155 paths = safe_unicode(paths)
155 url_l = [link_to(repo_name, url('files_home',
156 url_l = [link_to(repo_name, url('files_home',
156 repo_name=repo_name,
157 repo_name=repo_name,
157 revision=rev, f_path=''),
158 revision=rev, f_path=''),
158 class_='ypjax-link')]
159 class_='ypjax-link')]
159 paths_l = paths.split('/')
160 paths_l = paths.split('/')
160 for cnt, p in enumerate(paths_l):
161 for cnt, p in enumerate(paths_l):
161 if p != '':
162 if p != '':
162 url_l.append(link_to(p,
163 url_l.append(link_to(p,
163 url('files_home',
164 url('files_home',
164 repo_name=repo_name,
165 repo_name=repo_name,
165 revision=rev,
166 revision=rev,
166 f_path='/'.join(paths_l[:cnt + 1])
167 f_path='/'.join(paths_l[:cnt + 1])
167 ),
168 ),
168 class_='ypjax-link'
169 class_='ypjax-link'
169 )
170 )
170 )
171 )
171
172
172 return literal('/'.join(url_l))
173 return literal('/'.join(url_l))
173
174
174 files_breadcrumbs = _FilesBreadCrumbs()
175 files_breadcrumbs = _FilesBreadCrumbs()
175
176
176
177
177 class CodeHtmlFormatter(HtmlFormatter):
178 class CodeHtmlFormatter(HtmlFormatter):
178 """
179 """
179 My code Html Formatter for source codes
180 My code Html Formatter for source codes
180 """
181 """
181
182
182 def wrap(self, source, outfile):
183 def wrap(self, source, outfile):
183 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
184 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
184
185
185 def _wrap_code(self, source):
186 def _wrap_code(self, source):
186 for cnt, it in enumerate(source):
187 for cnt, it in enumerate(source):
187 i, t = it
188 i, t = it
188 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
189 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
189 yield i, t
190 yield i, t
190
191
191 def _wrap_tablelinenos(self, inner):
192 def _wrap_tablelinenos(self, inner):
192 dummyoutfile = StringIO.StringIO()
193 dummyoutfile = StringIO.StringIO()
193 lncount = 0
194 lncount = 0
194 for t, line in inner:
195 for t, line in inner:
195 if t:
196 if t:
196 lncount += 1
197 lncount += 1
197 dummyoutfile.write(line)
198 dummyoutfile.write(line)
198
199
199 fl = self.linenostart
200 fl = self.linenostart
200 mw = len(str(lncount + fl - 1))
201 mw = len(str(lncount + fl - 1))
201 sp = self.linenospecial
202 sp = self.linenospecial
202 st = self.linenostep
203 st = self.linenostep
203 la = self.lineanchors
204 la = self.lineanchors
204 aln = self.anchorlinenos
205 aln = self.anchorlinenos
205 nocls = self.noclasses
206 nocls = self.noclasses
206 if sp:
207 if sp:
207 lines = []
208 lines = []
208
209
209 for i in range(fl, fl + lncount):
210 for i in range(fl, fl + lncount):
210 if i % st == 0:
211 if i % st == 0:
211 if i % sp == 0:
212 if i % sp == 0:
212 if aln:
213 if aln:
213 lines.append('<a href="#%s%d" class="special">%*d</a>' %
214 lines.append('<a href="#%s%d" class="special">%*d</a>' %
214 (la, i, mw, i))
215 (la, i, mw, i))
215 else:
216 else:
216 lines.append('<span class="special">%*d</span>' % (mw, i))
217 lines.append('<span class="special">%*d</span>' % (mw, i))
217 else:
218 else:
218 if aln:
219 if aln:
219 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
220 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
220 else:
221 else:
221 lines.append('%*d' % (mw, i))
222 lines.append('%*d' % (mw, i))
222 else:
223 else:
223 lines.append('')
224 lines.append('')
224 ls = '\n'.join(lines)
225 ls = '\n'.join(lines)
225 else:
226 else:
226 lines = []
227 lines = []
227 for i in range(fl, fl + lncount):
228 for i in range(fl, fl + lncount):
228 if i % st == 0:
229 if i % st == 0:
229 if aln:
230 if aln:
230 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
231 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
231 else:
232 else:
232 lines.append('%*d' % (mw, i))
233 lines.append('%*d' % (mw, i))
233 else:
234 else:
234 lines.append('')
235 lines.append('')
235 ls = '\n'.join(lines)
236 ls = '\n'.join(lines)
236
237
237 # in case you wonder about the seemingly redundant <div> here: since the
238 # in case you wonder about the seemingly redundant <div> here: since the
238 # content in the other cell also is wrapped in a div, some browsers in
239 # content in the other cell also is wrapped in a div, some browsers in
239 # some configurations seem to mess up the formatting...
240 # some configurations seem to mess up the formatting...
240 if nocls:
241 if nocls:
241 yield 0, ('<table class="%stable">' % self.cssclass +
242 yield 0, ('<table class="%stable">' % self.cssclass +
242 '<tr><td><div class="linenodiv" '
243 '<tr><td><div class="linenodiv" '
243 'style="background-color: #f0f0f0; padding-right: 10px">'
244 'style="background-color: #f0f0f0; padding-right: 10px">'
244 '<pre style="line-height: 125%">' +
245 '<pre style="line-height: 125%">' +
245 ls + '</pre></div></td><td id="hlcode" class="code">')
246 ls + '</pre></div></td><td id="hlcode" class="code">')
246 else:
247 else:
247 yield 0, ('<table class="%stable">' % self.cssclass +
248 yield 0, ('<table class="%stable">' % self.cssclass +
248 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
249 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
249 ls + '</pre></div></td><td id="hlcode" class="code">')
250 ls + '</pre></div></td><td id="hlcode" class="code">')
250 yield 0, dummyoutfile.getvalue()
251 yield 0, dummyoutfile.getvalue()
251 yield 0, '</td></tr></table>'
252 yield 0, '</td></tr></table>'
252
253
253
254
254 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
255 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
255
256
256 def _markup_whitespace(m):
257 def _markup_whitespace(m):
257 groups = m.groups()
258 groups = m.groups()
258 if groups[0]:
259 if groups[0]:
259 return '<u>\t</u>'
260 return '<u>\t</u>'
260 if groups[1]:
261 if groups[1]:
261 return ' <i></i>'
262 return ' <i></i>'
262
263
263 def markup_whitespace(s):
264 def markup_whitespace(s):
264 return _whitespace_re.sub(_markup_whitespace, s)
265 return _whitespace_re.sub(_markup_whitespace, s)
265
266
266 def pygmentize(filenode, **kwargs):
267 def pygmentize(filenode, **kwargs):
267 """
268 """
268 pygmentize function using pygments
269 pygmentize function using pygments
269
270
270 :param filenode:
271 :param filenode:
271 """
272 """
272 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
273 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
273 return literal(markup_whitespace(
274 return literal(markup_whitespace(
274 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
275 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
275
276
276
277
277 def pygmentize_annotation(repo_name, filenode, **kwargs):
278 def pygmentize_annotation(repo_name, filenode, **kwargs):
278 """
279 """
279 pygmentize function for annotation
280 pygmentize function for annotation
280
281
281 :param filenode:
282 :param filenode:
282 """
283 """
283
284
284 color_dict = {}
285 color_dict = {}
285
286
286 def gen_color(n=10000):
287 def gen_color(n=10000):
287 """generator for getting n of evenly distributed colors using
288 """generator for getting n of evenly distributed colors using
288 hsv color and golden ratio. It always return same order of colors
289 hsv color and golden ratio. It always return same order of colors
289
290
290 :returns: RGB tuple
291 :returns: RGB tuple
291 """
292 """
292
293
293 def hsv_to_rgb(h, s, v):
294 def hsv_to_rgb(h, s, v):
294 if s == 0.0:
295 if s == 0.0:
295 return v, v, v
296 return v, v, v
296 i = int(h * 6.0) # XXX assume int() truncates!
297 i = int(h * 6.0) # XXX assume int() truncates!
297 f = (h * 6.0) - i
298 f = (h * 6.0) - i
298 p = v * (1.0 - s)
299 p = v * (1.0 - s)
299 q = v * (1.0 - s * f)
300 q = v * (1.0 - s * f)
300 t = v * (1.0 - s * (1.0 - f))
301 t = v * (1.0 - s * (1.0 - f))
301 i = i % 6
302 i = i % 6
302 if i == 0:
303 if i == 0:
303 return v, t, p
304 return v, t, p
304 if i == 1:
305 if i == 1:
305 return q, v, p
306 return q, v, p
306 if i == 2:
307 if i == 2:
307 return p, v, t
308 return p, v, t
308 if i == 3:
309 if i == 3:
309 return p, q, v
310 return p, q, v
310 if i == 4:
311 if i == 4:
311 return t, p, v
312 return t, p, v
312 if i == 5:
313 if i == 5:
313 return v, p, q
314 return v, p, q
314
315
315 golden_ratio = 0.618033988749895
316 golden_ratio = 0.618033988749895
316 h = 0.22717784590367374
317 h = 0.22717784590367374
317
318
318 for _unused in xrange(n):
319 for _unused in xrange(n):
319 h += golden_ratio
320 h += golden_ratio
320 h %= 1
321 h %= 1
321 HSV_tuple = [h, 0.95, 0.95]
322 HSV_tuple = [h, 0.95, 0.95]
322 RGB_tuple = hsv_to_rgb(*HSV_tuple)
323 RGB_tuple = hsv_to_rgb(*HSV_tuple)
323 yield map(lambda x: str(int(x * 256)), RGB_tuple)
324 yield map(lambda x: str(int(x * 256)), RGB_tuple)
324
325
325 cgenerator = gen_color()
326 cgenerator = gen_color()
326
327
327 def get_color_string(cs):
328 def get_color_string(cs):
328 if cs in color_dict:
329 if cs in color_dict:
329 col = color_dict[cs]
330 col = color_dict[cs]
330 else:
331 else:
331 col = color_dict[cs] = cgenerator.next()
332 col = color_dict[cs] = cgenerator.next()
332 return "color: rgb(%s)! important;" % (', '.join(col))
333 return "color: rgb(%s)! important;" % (', '.join(col))
333
334
334 def url_func(repo_name):
335 def url_func(repo_name):
335
336
336 def _url_func(changeset):
337 def _url_func(changeset):
337 author = escape(changeset.author)
338 author = escape(changeset.author)
338 date = changeset.date
339 date = changeset.date
339 message = escape(changeset.message)
340 message = escape(changeset.message)
340 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
341 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
341 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
342 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
342 "</b> %s<br/></div>") % (author, date, message)
343 "</b> %s<br/></div>") % (author, date, message)
343
344
344 lnk_format = show_id(changeset)
345 lnk_format = show_id(changeset)
345 uri = link_to(
346 uri = link_to(
346 lnk_format,
347 lnk_format,
347 url('changeset_home', repo_name=repo_name,
348 url('changeset_home', repo_name=repo_name,
348 revision=changeset.raw_id),
349 revision=changeset.raw_id),
349 style=get_color_string(changeset.raw_id),
350 style=get_color_string(changeset.raw_id),
350 class_='tooltip safe-html-title',
351 class_='tooltip safe-html-title',
351 title=tooltip_html
352 title=tooltip_html
352 )
353 )
353
354
354 uri += '\n'
355 uri += '\n'
355 return uri
356 return uri
356 return _url_func
357 return _url_func
357
358
358 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
359 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
359
360
360
361
361 def is_following_repo(repo_name, user_id):
362 def is_following_repo(repo_name, user_id):
362 from kallithea.model.scm import ScmModel
363 from kallithea.model.scm import ScmModel
363 return ScmModel().is_following_repo(repo_name, user_id)
364 return ScmModel().is_following_repo(repo_name, user_id)
364
365
365 class _Message(object):
366 class _Message(object):
366 """A message returned by ``Flash.pop_messages()``.
367 """A message returned by ``Flash.pop_messages()``.
367
368
368 Converting the message to a string returns the message text. Instances
369 Converting the message to a string returns the message text. Instances
369 also have the following attributes:
370 also have the following attributes:
370
371
371 * ``message``: the message text.
372 * ``message``: the message text.
372 * ``category``: the category specified when the message was created.
373 * ``category``: the category specified when the message was created.
373 """
374 """
374
375
375 def __init__(self, category, message):
376 def __init__(self, category, message):
376 self.category = category
377 self.category = category
377 self.message = message
378 self.message = message
378
379
379 def __str__(self):
380 def __str__(self):
380 return self.message
381 return self.message
381
382
382 __unicode__ = __str__
383 __unicode__ = __str__
383
384
384 def __html__(self):
385 def __html__(self):
385 return escape(safe_unicode(self.message))
386 return escape(safe_unicode(self.message))
386
387
387 class Flash(_Flash):
388 class Flash(_Flash):
388
389
389 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
390 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
390 """
391 """
391 Show a message to the user _and_ log it through the specified function
392 Show a message to the user _and_ log it through the specified function
392
393
393 category: notice (default), warning, error, success
394 category: notice (default), warning, error, success
394 logf: a custom log function - such as log.debug
395 logf: a custom log function - such as log.debug
395
396
396 logf defaults to log.info, unless category equals 'success', in which
397 logf defaults to log.info, unless category equals 'success', in which
397 case logf defaults to log.debug.
398 case logf defaults to log.debug.
398 """
399 """
399 if logf is None:
400 if logf is None:
400 logf = log.info
401 logf = log.info
401 if category == 'success':
402 if category == 'success':
402 logf = log.debug
403 logf = log.debug
403
404
404 logf('Flash %s: %s', category, message)
405 logf('Flash %s: %s', category, message)
405
406
406 super(Flash, self).__call__(message, category, ignore_duplicate)
407 super(Flash, self).__call__(message, category, ignore_duplicate)
407
408
408 def pop_messages(self):
409 def pop_messages(self):
409 """Return all accumulated messages and delete them from the session.
410 """Return all accumulated messages and delete them from the session.
410
411
411 The return value is a list of ``Message`` objects.
412 The return value is a list of ``Message`` objects.
412 """
413 """
413 from pylons import session
414 from pylons import session
414 messages = session.pop(self.session_key, [])
415 messages = session.pop(self.session_key, [])
415 session.save()
416 session.save()
416 return [_Message(*m) for m in messages]
417 return [_Message(*m) for m in messages]
417
418
418 flash = Flash()
419 flash = Flash()
419
420
420 #==============================================================================
421 #==============================================================================
421 # SCM FILTERS available via h.
422 # SCM FILTERS available via h.
422 #==============================================================================
423 #==============================================================================
423 from kallithea.lib.vcs.utils import author_name, author_email
424 from kallithea.lib.vcs.utils import author_name, author_email
424 from kallithea.lib.utils2 import credentials_filter, age as _age
425 from kallithea.lib.utils2 import credentials_filter, age as _age
425 from kallithea.model.db import User, ChangesetStatus, PullRequest
426 from kallithea.model.db import User, ChangesetStatus, PullRequest
426
427
427 age = lambda x, y=False: _age(x, y)
428 age = lambda x, y=False: _age(x, y)
428 capitalize = lambda x: x.capitalize()
429 capitalize = lambda x: x.capitalize()
429 email = author_email
430 email = author_email
430 short_id = lambda x: x[:12]
431 short_id = lambda x: x[:12]
431 hide_credentials = lambda x: ''.join(credentials_filter(x))
432 hide_credentials = lambda x: ''.join(credentials_filter(x))
432
433
433
434
434 def show_id(cs):
435 def show_id(cs):
435 """
436 """
436 Configurable function that shows ID
437 Configurable function that shows ID
437 by default it's r123:fffeeefffeee
438 by default it's r123:fffeeefffeee
438
439
439 :param cs: changeset instance
440 :param cs: changeset instance
440 """
441 """
441 from kallithea import CONFIG
442 from kallithea import CONFIG
442 def_len = safe_int(CONFIG.get('show_sha_length', 12))
443 def_len = safe_int(CONFIG.get('show_sha_length', 12))
443 show_rev = str2bool(CONFIG.get('show_revision_number', False))
444 show_rev = str2bool(CONFIG.get('show_revision_number', False))
444
445
445 raw_id = cs.raw_id[:def_len]
446 raw_id = cs.raw_id[:def_len]
446 if show_rev:
447 if show_rev:
447 return 'r%s:%s' % (cs.revision, raw_id)
448 return 'r%s:%s' % (cs.revision, raw_id)
448 else:
449 else:
449 return raw_id
450 return raw_id
450
451
451
452
452 def fmt_date(date):
453 def fmt_date(date):
453 if date:
454 if date:
454 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
455 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
455
456
456 return ""
457 return ""
457
458
458
459
459 def is_git(repository):
460 def is_git(repository):
460 if hasattr(repository, 'alias'):
461 if hasattr(repository, 'alias'):
461 _type = repository.alias
462 _type = repository.alias
462 elif hasattr(repository, 'repo_type'):
463 elif hasattr(repository, 'repo_type'):
463 _type = repository.repo_type
464 _type = repository.repo_type
464 else:
465 else:
465 _type = repository
466 _type = repository
466 return _type == 'git'
467 return _type == 'git'
467
468
468
469
469 def is_hg(repository):
470 def is_hg(repository):
470 if hasattr(repository, 'alias'):
471 if hasattr(repository, 'alias'):
471 _type = repository.alias
472 _type = repository.alias
472 elif hasattr(repository, 'repo_type'):
473 elif hasattr(repository, 'repo_type'):
473 _type = repository.repo_type
474 _type = repository.repo_type
474 else:
475 else:
475 _type = repository
476 _type = repository
476 return _type == 'hg'
477 return _type == 'hg'
477
478
478
479
479 def user_or_none(author):
480 def user_or_none(author):
480 email = author_email(author)
481 email = author_email(author)
481 if email:
482 if email:
482 user = User.get_by_email(email, case_insensitive=True, cache=True)
483 user = User.get_by_email(email, case_insensitive=True, cache=True)
483 if user is not None:
484 if user is not None:
484 return user
485 return user
485
486
486 user = User.get_by_username(author_name(author), case_insensitive=True, cache=True)
487 user = User.get_by_username(author_name(author), case_insensitive=True, cache=True)
487 if user is not None:
488 if user is not None:
488 return user
489 return user
489
490
490 return None
491 return None
491
492
492 def email_or_none(author):
493 def email_or_none(author):
493 if not author:
494 if not author:
494 return None
495 return None
495 user = user_or_none(author)
496 user = user_or_none(author)
496 if user is not None:
497 if user is not None:
497 return user.email # always use main email address - not necessarily the one used to find user
498 return user.email # always use main email address - not necessarily the one used to find user
498
499
499 # extract email from the commit string
500 # extract email from the commit string
500 email = author_email(author)
501 email = author_email(author)
501 if email:
502 if email:
502 return email
503 return email
503
504
504 # No valid email, not a valid user in the system, none!
505 # No valid email, not a valid user in the system, none!
505 return None
506 return None
506
507
507 def person(author, show_attr="username"):
508 def person(author, show_attr="username"):
508 """Find the user identified by 'author', return one of the users attributes,
509 """Find the user identified by 'author', return one of the users attributes,
509 default to the username attribute, None if there is no user"""
510 default to the username attribute, None if there is no user"""
510 # attr to return from fetched user
511 # attr to return from fetched user
511 person_getter = lambda usr: getattr(usr, show_attr)
512 person_getter = lambda usr: getattr(usr, show_attr)
512
513
513 # if author is already an instance use it for extraction
514 # if author is already an instance use it for extraction
514 if isinstance(author, User):
515 if isinstance(author, User):
515 return person_getter(author)
516 return person_getter(author)
516
517
517 user = user_or_none(author)
518 user = user_or_none(author)
518 if user is not None:
519 if user is not None:
519 return person_getter(user)
520 return person_getter(user)
520
521
521 # Still nothing? Just pass back the author name if any, else the email
522 # Still nothing? Just pass back the author name if any, else the email
522 return author_name(author) or email(author)
523 return author_name(author) or email(author)
523
524
524
525
525 def person_by_id(id_, show_attr="username"):
526 def person_by_id(id_, show_attr="username"):
526 # attr to return from fetched user
527 # attr to return from fetched user
527 person_getter = lambda usr: getattr(usr, show_attr)
528 person_getter = lambda usr: getattr(usr, show_attr)
528
529
529 #maybe it's an ID ?
530 #maybe it's an ID ?
530 if str(id_).isdigit() or isinstance(id_, int):
531 if str(id_).isdigit() or isinstance(id_, int):
531 id_ = int(id_)
532 id_ = int(id_)
532 user = User.get(id_)
533 user = User.get(id_)
533 if user is not None:
534 if user is not None:
534 return person_getter(user)
535 return person_getter(user)
535 return id_
536 return id_
536
537
537
538
538 def desc_stylize(value):
539 def desc_stylize(value):
539 """
540 """
540 converts tags from value into html equivalent
541 converts tags from value into html equivalent
541
542
542 :param value:
543 :param value:
543 """
544 """
544 if not value:
545 if not value:
545 return ''
546 return ''
546
547
547 value = re.sub(r'\[see\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
548 value = re.sub(r'\[see\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
548 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
549 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
549 value = re.sub(r'\[license\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
550 value = re.sub(r'\[license\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
550 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
551 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
551 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=&gt;\ *([a-zA-Z0-9\-\/]*)\]',
552 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=&gt;\ *([a-zA-Z0-9\-\/]*)\]',
552 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
553 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
553 value = re.sub(r'\[(lang|language)\ \=&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
554 value = re.sub(r'\[(lang|language)\ \=&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
554 '<div class="metatag" tag="lang">\\2</div>', value)
555 '<div class="metatag" tag="lang">\\2</div>', value)
555 value = re.sub(r'\[([a-z]+)\]',
556 value = re.sub(r'\[([a-z]+)\]',
556 '<div class="metatag" tag="\\1">\\1</div>', value)
557 '<div class="metatag" tag="\\1">\\1</div>', value)
557
558
558 return value
559 return value
559
560
560
561
561 def boolicon(value):
562 def boolicon(value):
562 """Returns boolean value of a value, represented as small html image of true/false
563 """Returns boolean value of a value, represented as small html image of true/false
563 icons
564 icons
564
565
565 :param value: value
566 :param value: value
566 """
567 """
567
568
568 if value:
569 if value:
569 return HTML.tag('i', class_="icon-ok")
570 return HTML.tag('i', class_="icon-ok")
570 else:
571 else:
571 return HTML.tag('i', class_="icon-minus-circled")
572 return HTML.tag('i', class_="icon-minus-circled")
572
573
573
574
574 def action_parser(user_log, feed=False, parse_cs=False):
575 def action_parser(user_log, feed=False, parse_cs=False):
575 """
576 """
576 This helper will action_map the specified string action into translated
577 This helper will action_map the specified string action into translated
577 fancy names with icons and links
578 fancy names with icons and links
578
579
579 :param user_log: user log instance
580 :param user_log: user log instance
580 :param feed: use output for feeds (no html and fancy icons)
581 :param feed: use output for feeds (no html and fancy icons)
581 :param parse_cs: parse Changesets into VCS instances
582 :param parse_cs: parse Changesets into VCS instances
582 """
583 """
583
584
584 action = user_log.action
585 action = user_log.action
585 action_params = ' '
586 action_params = ' '
586
587
587 x = action.split(':')
588 x = action.split(':')
588
589
589 if len(x) > 1:
590 if len(x) > 1:
590 action, action_params = x
591 action, action_params = x
591
592
592 def get_cs_links():
593 def get_cs_links():
593 revs_limit = 3 # display this amount always
594 revs_limit = 3 # display this amount always
594 revs_top_limit = 50 # show upto this amount of changesets hidden
595 revs_top_limit = 50 # show upto this amount of changesets hidden
595 revs_ids = action_params.split(',')
596 revs_ids = action_params.split(',')
596 deleted = user_log.repository is None
597 deleted = user_log.repository is None
597 if deleted:
598 if deleted:
598 return ','.join(revs_ids)
599 return ','.join(revs_ids)
599
600
600 repo_name = user_log.repository.repo_name
601 repo_name = user_log.repository.repo_name
601
602
602 def lnk(rev, repo_name):
603 def lnk(rev, repo_name):
603 lazy_cs = False
604 lazy_cs = False
604 title_ = None
605 title_ = None
605 url_ = '#'
606 url_ = '#'
606 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
607 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
607 if rev.op and rev.ref_name:
608 if rev.op and rev.ref_name:
608 if rev.op == 'delete_branch':
609 if rev.op == 'delete_branch':
609 lbl = _('Deleted branch: %s') % rev.ref_name
610 lbl = _('Deleted branch: %s') % rev.ref_name
610 elif rev.op == 'tag':
611 elif rev.op == 'tag':
611 lbl = _('Created tag: %s') % rev.ref_name
612 lbl = _('Created tag: %s') % rev.ref_name
612 else:
613 else:
613 lbl = 'Unknown operation %s' % rev.op
614 lbl = 'Unknown operation %s' % rev.op
614 else:
615 else:
615 lazy_cs = True
616 lazy_cs = True
616 lbl = rev.short_id[:8]
617 lbl = rev.short_id[:8]
617 url_ = url('changeset_home', repo_name=repo_name,
618 url_ = url('changeset_home', repo_name=repo_name,
618 revision=rev.raw_id)
619 revision=rev.raw_id)
619 else:
620 else:
620 # changeset cannot be found - it might have been stripped or removed
621 # changeset cannot be found - it might have been stripped or removed
621 lbl = rev[:12]
622 lbl = rev[:12]
622 title_ = _('Changeset not found')
623 title_ = _('Changeset not found')
623 if parse_cs:
624 if parse_cs:
624 return link_to(lbl, url_, title=title_, class_='tooltip')
625 return link_to(lbl, url_, title=title_, class_='tooltip')
625 return link_to(lbl, url_, raw_id=rev.raw_id, repo_name=repo_name,
626 return link_to(lbl, url_, raw_id=rev.raw_id, repo_name=repo_name,
626 class_='lazy-cs' if lazy_cs else '')
627 class_='lazy-cs' if lazy_cs else '')
627
628
628 def _get_op(rev_txt):
629 def _get_op(rev_txt):
629 _op = None
630 _op = None
630 _name = rev_txt
631 _name = rev_txt
631 if len(rev_txt.split('=>')) == 2:
632 if len(rev_txt.split('=>')) == 2:
632 _op, _name = rev_txt.split('=>')
633 _op, _name = rev_txt.split('=>')
633 return _op, _name
634 return _op, _name
634
635
635 revs = []
636 revs = []
636 if len(filter(lambda v: v != '', revs_ids)) > 0:
637 if len(filter(lambda v: v != '', revs_ids)) > 0:
637 repo = None
638 repo = None
638 for rev in revs_ids[:revs_top_limit]:
639 for rev in revs_ids[:revs_top_limit]:
639 _op, _name = _get_op(rev)
640 _op, _name = _get_op(rev)
640
641
641 # we want parsed changesets, or new log store format is bad
642 # we want parsed changesets, or new log store format is bad
642 if parse_cs:
643 if parse_cs:
643 try:
644 try:
644 if repo is None:
645 if repo is None:
645 repo = user_log.repository.scm_instance
646 repo = user_log.repository.scm_instance
646 _rev = repo.get_changeset(rev)
647 _rev = repo.get_changeset(rev)
647 revs.append(_rev)
648 revs.append(_rev)
648 except ChangesetDoesNotExistError:
649 except ChangesetDoesNotExistError:
649 log.error('cannot find revision %s in this repo', rev)
650 log.error('cannot find revision %s in this repo', rev)
650 revs.append(rev)
651 revs.append(rev)
651 else:
652 else:
652 _rev = AttributeDict({
653 _rev = AttributeDict({
653 'short_id': rev[:12],
654 'short_id': rev[:12],
654 'raw_id': rev,
655 'raw_id': rev,
655 'message': '',
656 'message': '',
656 'op': _op,
657 'op': _op,
657 'ref_name': _name
658 'ref_name': _name
658 })
659 })
659 revs.append(_rev)
660 revs.append(_rev)
660 cs_links = [" " + ', '.join(
661 cs_links = [" " + ', '.join(
661 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
662 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
662 )]
663 )]
663 _op1, _name1 = _get_op(revs_ids[0])
664 _op1, _name1 = _get_op(revs_ids[0])
664 _op2, _name2 = _get_op(revs_ids[-1])
665 _op2, _name2 = _get_op(revs_ids[-1])
665
666
666 _rev = '%s...%s' % (_name1, _name2)
667 _rev = '%s...%s' % (_name1, _name2)
667
668
668 compare_view = (
669 compare_view = (
669 ' <div class="compare_view tooltip" title="%s">'
670 ' <div class="compare_view tooltip" title="%s">'
670 '<a href="%s">%s</a> </div>' % (
671 '<a href="%s">%s</a> </div>' % (
671 _('Show all combined changesets %s->%s') % (
672 _('Show all combined changesets %s->%s') % (
672 revs_ids[0][:12], revs_ids[-1][:12]
673 revs_ids[0][:12], revs_ids[-1][:12]
673 ),
674 ),
674 url('changeset_home', repo_name=repo_name,
675 url('changeset_home', repo_name=repo_name,
675 revision=_rev
676 revision=_rev
676 ),
677 ),
677 _('Compare view')
678 _('Compare view')
678 )
679 )
679 )
680 )
680
681
681 # if we have exactly one more than normally displayed
682 # if we have exactly one more than normally displayed
682 # just display it, takes less space than displaying
683 # just display it, takes less space than displaying
683 # "and 1 more revisions"
684 # "and 1 more revisions"
684 if len(revs_ids) == revs_limit + 1:
685 if len(revs_ids) == revs_limit + 1:
685 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
686 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
686
687
687 # hidden-by-default ones
688 # hidden-by-default ones
688 if len(revs_ids) > revs_limit + 1:
689 if len(revs_ids) > revs_limit + 1:
689 uniq_id = revs_ids[0]
690 uniq_id = revs_ids[0]
690 html_tmpl = (
691 html_tmpl = (
691 '<span> %s <a class="show_more" id="_%s" '
692 '<span> %s <a class="show_more" id="_%s" '
692 'href="#more">%s</a> %s</span>'
693 'href="#more">%s</a> %s</span>'
693 )
694 )
694 if not feed:
695 if not feed:
695 cs_links.append(html_tmpl % (
696 cs_links.append(html_tmpl % (
696 _('and'),
697 _('and'),
697 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
698 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
698 _('revisions')
699 _('revisions')
699 )
700 )
700 )
701 )
701
702
702 if not feed:
703 if not feed:
703 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
704 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
704 else:
705 else:
705 html_tmpl = '<span id="%s"> %s </span>'
706 html_tmpl = '<span id="%s"> %s </span>'
706
707
707 morelinks = ', '.join(
708 morelinks = ', '.join(
708 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
709 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
709 )
710 )
710
711
711 if len(revs_ids) > revs_top_limit:
712 if len(revs_ids) > revs_top_limit:
712 morelinks += ', ...'
713 morelinks += ', ...'
713
714
714 cs_links.append(html_tmpl % (uniq_id, morelinks))
715 cs_links.append(html_tmpl % (uniq_id, morelinks))
715 if len(revs) > 1:
716 if len(revs) > 1:
716 cs_links.append(compare_view)
717 cs_links.append(compare_view)
717 return ''.join(cs_links)
718 return ''.join(cs_links)
718
719
719 def get_fork_name():
720 def get_fork_name():
720 repo_name = action_params
721 repo_name = action_params
721 url_ = url('summary_home', repo_name=repo_name)
722 url_ = url('summary_home', repo_name=repo_name)
722 return _('Fork name %s') % link_to(action_params, url_)
723 return _('Fork name %s') % link_to(action_params, url_)
723
724
724 def get_user_name():
725 def get_user_name():
725 user_name = action_params
726 user_name = action_params
726 return user_name
727 return user_name
727
728
728 def get_users_group():
729 def get_users_group():
729 group_name = action_params
730 group_name = action_params
730 return group_name
731 return group_name
731
732
732 def get_pull_request():
733 def get_pull_request():
733 pull_request_id = action_params
734 pull_request_id = action_params
734 nice_id = PullRequest.make_nice_id(pull_request_id)
735 nice_id = PullRequest.make_nice_id(pull_request_id)
735
736
736 deleted = user_log.repository is None
737 deleted = user_log.repository is None
737 if deleted:
738 if deleted:
738 repo_name = user_log.repository_name
739 repo_name = user_log.repository_name
739 else:
740 else:
740 repo_name = user_log.repository.repo_name
741 repo_name = user_log.repository.repo_name
741
742
742 return link_to(_('Pull request %s') % nice_id,
743 return link_to(_('Pull request %s') % nice_id,
743 url('pullrequest_show', repo_name=repo_name,
744 url('pullrequest_show', repo_name=repo_name,
744 pull_request_id=pull_request_id))
745 pull_request_id=pull_request_id))
745
746
746 def get_archive_name():
747 def get_archive_name():
747 archive_name = action_params
748 archive_name = action_params
748 return archive_name
749 return archive_name
749
750
750 # action : translated str, callback(extractor), icon
751 # action : translated str, callback(extractor), icon
751 action_map = {
752 action_map = {
752 'user_deleted_repo': (_('[deleted] repository'),
753 'user_deleted_repo': (_('[deleted] repository'),
753 None, 'icon-trashcan'),
754 None, 'icon-trashcan'),
754 'user_created_repo': (_('[created] repository'),
755 'user_created_repo': (_('[created] repository'),
755 None, 'icon-plus'),
756 None, 'icon-plus'),
756 'user_created_fork': (_('[created] repository as fork'),
757 'user_created_fork': (_('[created] repository as fork'),
757 None, 'icon-fork'),
758 None, 'icon-fork'),
758 'user_forked_repo': (_('[forked] repository'),
759 'user_forked_repo': (_('[forked] repository'),
759 get_fork_name, 'icon-fork'),
760 get_fork_name, 'icon-fork'),
760 'user_updated_repo': (_('[updated] repository'),
761 'user_updated_repo': (_('[updated] repository'),
761 None, 'icon-pencil'),
762 None, 'icon-pencil'),
762 'user_downloaded_archive': (_('[downloaded] archive from repository'),
763 'user_downloaded_archive': (_('[downloaded] archive from repository'),
763 get_archive_name, 'icon-download-cloud'),
764 get_archive_name, 'icon-download-cloud'),
764 'admin_deleted_repo': (_('[delete] repository'),
765 'admin_deleted_repo': (_('[delete] repository'),
765 None, 'icon-trashcan'),
766 None, 'icon-trashcan'),
766 'admin_created_repo': (_('[created] repository'),
767 'admin_created_repo': (_('[created] repository'),
767 None, 'icon-plus'),
768 None, 'icon-plus'),
768 'admin_forked_repo': (_('[forked] repository'),
769 'admin_forked_repo': (_('[forked] repository'),
769 None, 'icon-fork'),
770 None, 'icon-fork'),
770 'admin_updated_repo': (_('[updated] repository'),
771 'admin_updated_repo': (_('[updated] repository'),
771 None, 'icon-pencil'),
772 None, 'icon-pencil'),
772 'admin_created_user': (_('[created] user'),
773 'admin_created_user': (_('[created] user'),
773 get_user_name, 'icon-user'),
774 get_user_name, 'icon-user'),
774 'admin_updated_user': (_('[updated] user'),
775 'admin_updated_user': (_('[updated] user'),
775 get_user_name, 'icon-user'),
776 get_user_name, 'icon-user'),
776 'admin_created_users_group': (_('[created] user group'),
777 'admin_created_users_group': (_('[created] user group'),
777 get_users_group, 'icon-pencil'),
778 get_users_group, 'icon-pencil'),
778 'admin_updated_users_group': (_('[updated] user group'),
779 'admin_updated_users_group': (_('[updated] user group'),
779 get_users_group, 'icon-pencil'),
780 get_users_group, 'icon-pencil'),
780 'user_commented_revision': (_('[commented] on revision in repository'),
781 'user_commented_revision': (_('[commented] on revision in repository'),
781 get_cs_links, 'icon-comment'),
782 get_cs_links, 'icon-comment'),
782 'user_commented_pull_request': (_('[commented] on pull request for'),
783 'user_commented_pull_request': (_('[commented] on pull request for'),
783 get_pull_request, 'icon-comment'),
784 get_pull_request, 'icon-comment'),
784 'user_closed_pull_request': (_('[closed] pull request for'),
785 'user_closed_pull_request': (_('[closed] pull request for'),
785 get_pull_request, 'icon-ok'),
786 get_pull_request, 'icon-ok'),
786 'push': (_('[pushed] into'),
787 'push': (_('[pushed] into'),
787 get_cs_links, 'icon-move-up'),
788 get_cs_links, 'icon-move-up'),
788 'push_local': (_('[committed via Kallithea] into repository'),
789 'push_local': (_('[committed via Kallithea] into repository'),
789 get_cs_links, 'icon-pencil'),
790 get_cs_links, 'icon-pencil'),
790 'push_remote': (_('[pulled from remote] into repository'),
791 'push_remote': (_('[pulled from remote] into repository'),
791 get_cs_links, 'icon-move-up'),
792 get_cs_links, 'icon-move-up'),
792 'pull': (_('[pulled] from'),
793 'pull': (_('[pulled] from'),
793 None, 'icon-move-down'),
794 None, 'icon-move-down'),
794 'started_following_repo': (_('[started following] repository'),
795 'started_following_repo': (_('[started following] repository'),
795 None, 'icon-heart'),
796 None, 'icon-heart'),
796 'stopped_following_repo': (_('[stopped following] repository'),
797 'stopped_following_repo': (_('[stopped following] repository'),
797 None, 'icon-heart-empty'),
798 None, 'icon-heart-empty'),
798 }
799 }
799
800
800 action_str = action_map.get(action, action)
801 action_str = action_map.get(action, action)
801 if feed:
802 if feed:
802 action = action_str[0].replace('[', '').replace(']', '')
803 action = action_str[0].replace('[', '').replace(']', '')
803 else:
804 else:
804 action = action_str[0]\
805 action = action_str[0]\
805 .replace('[', '<span class="journal_highlight">')\
806 .replace('[', '<span class="journal_highlight">')\
806 .replace(']', '</span>')
807 .replace(']', '</span>')
807
808
808 action_params_func = lambda: ""
809 action_params_func = lambda: ""
809
810
810 if callable(action_str[1]):
811 if callable(action_str[1]):
811 action_params_func = action_str[1]
812 action_params_func = action_str[1]
812
813
813 def action_parser_icon():
814 def action_parser_icon():
814 action = user_log.action
815 action = user_log.action
815 action_params = None
816 action_params = None
816 x = action.split(':')
817 x = action.split(':')
817
818
818 if len(x) > 1:
819 if len(x) > 1:
819 action, action_params = x
820 action, action_params = x
820
821
821 tmpl = """<i class="%s" alt="%s"></i>"""
822 tmpl = """<i class="%s" alt="%s"></i>"""
822 ico = action_map.get(action, ['', '', ''])[2]
823 ico = action_map.get(action, ['', '', ''])[2]
823 return literal(tmpl % (ico, action))
824 return literal(tmpl % (ico, action))
824
825
825 # returned callbacks we need to call to get
826 # returned callbacks we need to call to get
826 return [lambda: literal(action), action_params_func, action_parser_icon]
827 return [lambda: literal(action), action_params_func, action_parser_icon]
827
828
828
829
829
830
830 #==============================================================================
831 #==============================================================================
831 # PERMS
832 # PERMS
832 #==============================================================================
833 #==============================================================================
833 from kallithea.lib.auth import HasPermissionAny, HasPermissionAll, \
834 from kallithea.lib.auth import HasPermissionAny, HasPermissionAll, \
834 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
835 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
835 HasRepoGroupPermissionAny
836 HasRepoGroupPermissionAny
836
837
837
838
838 #==============================================================================
839 #==============================================================================
839 # GRAVATAR URL
840 # GRAVATAR URL
840 #==============================================================================
841 #==============================================================================
841 def gravatar(email_address, cls='', size=30, ssl_enabled=True):
842 def gravatar(email_address, cls='', size=30, ssl_enabled=True):
842 """return html element of the gravatar
843 """return html element of the gravatar
843
844
844 This method will return an <img> with the resolution double the size (for
845 This method will return an <img> with the resolution double the size (for
845 retina screens) of the image. If the url returned from gravatar_url is
846 retina screens) of the image. If the url returned from gravatar_url is
846 empty then we fallback to using an icon.
847 empty then we fallback to using an icon.
847
848
848 """
849 """
849 src = gravatar_url(email_address, size*2, ssl_enabled)
850 src = gravatar_url(email_address, size*2, ssl_enabled)
850
851
851 # here it makes sense to use style="width: ..." (instead of, say, a
852 # here it makes sense to use style="width: ..." (instead of, say, a
852 # stylesheet) because we using this to generate a high-res (retina) size
853 # stylesheet) because we using this to generate a high-res (retina) size
853 tmpl = '<img alt="" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>'
854 tmpl = '<img alt="" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>'
854
855
855 # if src is empty then there was no gravatar, so we use a font icon
856 # if src is empty then there was no gravatar, so we use a font icon
856 if not src:
857 if not src:
857 tmpl = """<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
858 tmpl = """<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
858
859
859 tmpl = tmpl.format(cls=cls, size=size, src=src)
860 tmpl = tmpl.format(cls=cls, size=size, src=src)
860 return literal(tmpl)
861 return literal(tmpl)
861
862
862 def gravatar_url(email_address, size=30, ssl_enabled=True):
863 def gravatar_url(email_address, size=30, ssl_enabled=True):
863 # doh, we need to re-import those to mock it later
864 # doh, we need to re-import those to mock it later
864 from pylons import url
865 from pylons import url
865 from pylons import tmpl_context as c
866 from pylons import tmpl_context as c
866
867
867 _def = 'anonymous@kallithea-scm.org' # default gravatar
868 _def = 'anonymous@kallithea-scm.org' # default gravatar
868 _use_gravatar = c.visual.use_gravatar
869 _use_gravatar = c.visual.use_gravatar
869 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
870 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
870
871
871 email_address = email_address or _def
872 email_address = email_address or _def
872
873
873 if not _use_gravatar or not email_address or email_address == _def:
874 if not _use_gravatar or not email_address or email_address == _def:
874 return ""
875 return ""
875
876
876 if _use_gravatar:
877 if _use_gravatar:
877 _md5 = lambda s: hashlib.md5(s).hexdigest()
878 _md5 = lambda s: hashlib.md5(s).hexdigest()
878
879
879 tmpl = _gravatar_url
880 tmpl = _gravatar_url
880 parsed_url = urlparse.urlparse(url.current(qualified=True))
881 parsed_url = urlparse.urlparse(url.current(qualified=True))
881 tmpl = tmpl.replace('{email}', email_address)\
882 tmpl = tmpl.replace('{email}', email_address)\
882 .replace('{md5email}', _md5(safe_str(email_address).lower())) \
883 .replace('{md5email}', _md5(safe_str(email_address).lower())) \
883 .replace('{netloc}', parsed_url.netloc)\
884 .replace('{netloc}', parsed_url.netloc)\
884 .replace('{scheme}', parsed_url.scheme)\
885 .replace('{scheme}', parsed_url.scheme)\
885 .replace('{size}', safe_str(size))
886 .replace('{size}', safe_str(size))
886 return tmpl
887 return tmpl
887
888
888 class Page(_Page):
889 class Page(_Page):
889 """
890 """
890 Custom pager to match rendering style with YUI paginator
891 Custom pager to match rendering style with YUI paginator
891 """
892 """
892
893
893 def _get_pos(self, cur_page, max_page, items):
894 def _get_pos(self, cur_page, max_page, items):
894 edge = (items / 2) + 1
895 edge = (items / 2) + 1
895 if (cur_page <= edge):
896 if (cur_page <= edge):
896 radius = max(items / 2, items - cur_page)
897 radius = max(items / 2, items - cur_page)
897 elif (max_page - cur_page) < edge:
898 elif (max_page - cur_page) < edge:
898 radius = (items - 1) - (max_page - cur_page)
899 radius = (items - 1) - (max_page - cur_page)
899 else:
900 else:
900 radius = items / 2
901 radius = items / 2
901
902
902 left = max(1, (cur_page - (radius)))
903 left = max(1, (cur_page - (radius)))
903 right = min(max_page, cur_page + (radius))
904 right = min(max_page, cur_page + (radius))
904 return left, cur_page, right
905 return left, cur_page, right
905
906
906 def _range(self, regexp_match):
907 def _range(self, regexp_match):
907 """
908 """
908 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
909 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
909
910
910 Arguments:
911 Arguments:
911
912
912 regexp_match
913 regexp_match
913 A "re" (regular expressions) match object containing the
914 A "re" (regular expressions) match object containing the
914 radius of linked pages around the current page in
915 radius of linked pages around the current page in
915 regexp_match.group(1) as a string
916 regexp_match.group(1) as a string
916
917
917 This function is supposed to be called as a callable in
918 This function is supposed to be called as a callable in
918 re.sub.
919 re.sub.
919
920
920 """
921 """
921 radius = int(regexp_match.group(1))
922 radius = int(regexp_match.group(1))
922
923
923 # Compute the first and last page number within the radius
924 # Compute the first and last page number within the radius
924 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
925 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
925 # -> leftmost_page = 5
926 # -> leftmost_page = 5
926 # -> rightmost_page = 9
927 # -> rightmost_page = 9
927 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
928 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
928 self.last_page,
929 self.last_page,
929 (radius * 2) + 1)
930 (radius * 2) + 1)
930 nav_items = []
931 nav_items = []
931
932
932 # Create a link to the first page (unless we are on the first page
933 # Create a link to the first page (unless we are on the first page
933 # or there would be no need to insert '..' spacers)
934 # or there would be no need to insert '..' spacers)
934 if self.page != self.first_page and self.first_page < leftmost_page:
935 if self.page != self.first_page and self.first_page < leftmost_page:
935 nav_items.append(self._pagerlink(self.first_page, self.first_page))
936 nav_items.append(self._pagerlink(self.first_page, self.first_page))
936
937
937 # Insert dots if there are pages between the first page
938 # Insert dots if there are pages between the first page
938 # and the currently displayed page range
939 # and the currently displayed page range
939 if leftmost_page - self.first_page > 1:
940 if leftmost_page - self.first_page > 1:
940 # Wrap in a SPAN tag if nolink_attr is set
941 # Wrap in a SPAN tag if nolink_attr is set
941 text_ = '..'
942 text_ = '..'
942 if self.dotdot_attr:
943 if self.dotdot_attr:
943 text_ = HTML.span(c=text_, **self.dotdot_attr)
944 text_ = HTML.span(c=text_, **self.dotdot_attr)
944 nav_items.append(text_)
945 nav_items.append(text_)
945
946
946 for thispage in xrange(leftmost_page, rightmost_page + 1):
947 for thispage in xrange(leftmost_page, rightmost_page + 1):
947 # Highlight the current page number and do not use a link
948 # Highlight the current page number and do not use a link
948 text_ = str(thispage)
949 text_ = str(thispage)
949 if thispage == self.page:
950 if thispage == self.page:
950 # Wrap in a SPAN tag if nolink_attr is set
951 # Wrap in a SPAN tag if nolink_attr is set
951 if self.curpage_attr:
952 if self.curpage_attr:
952 text_ = HTML.span(c=text_, **self.curpage_attr)
953 text_ = HTML.span(c=text_, **self.curpage_attr)
953 nav_items.append(text_)
954 nav_items.append(text_)
954 # Otherwise create just a link to that page
955 # Otherwise create just a link to that page
955 else:
956 else:
956 nav_items.append(self._pagerlink(thispage, text_))
957 nav_items.append(self._pagerlink(thispage, text_))
957
958
958 # Insert dots if there are pages between the displayed
959 # Insert dots if there are pages between the displayed
959 # page numbers and the end of the page range
960 # page numbers and the end of the page range
960 if self.last_page - rightmost_page > 1:
961 if self.last_page - rightmost_page > 1:
961 text_ = '..'
962 text_ = '..'
962 # Wrap in a SPAN tag if nolink_attr is set
963 # Wrap in a SPAN tag if nolink_attr is set
963 if self.dotdot_attr:
964 if self.dotdot_attr:
964 text_ = HTML.span(c=text_, **self.dotdot_attr)
965 text_ = HTML.span(c=text_, **self.dotdot_attr)
965 nav_items.append(text_)
966 nav_items.append(text_)
966
967
967 # Create a link to the very last page (unless we are on the last
968 # Create a link to the very last page (unless we are on the last
968 # page or there would be no need to insert '..' spacers)
969 # page or there would be no need to insert '..' spacers)
969 if self.page != self.last_page and rightmost_page < self.last_page:
970 if self.page != self.last_page and rightmost_page < self.last_page:
970 nav_items.append(self._pagerlink(self.last_page, self.last_page))
971 nav_items.append(self._pagerlink(self.last_page, self.last_page))
971
972
972 #_page_link = url.current()
973 #_page_link = url.current()
973 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
974 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
974 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
975 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
975 return self.separator.join(nav_items)
976 return self.separator.join(nav_items)
976
977
977 def pager(self, format='~2~', page_param='page', partial_param='partial',
978 def pager(self, format='~2~', page_param='page', partial_param='partial',
978 show_if_single_page=False, separator=' ', onclick=None,
979 show_if_single_page=False, separator=' ', onclick=None,
979 symbol_first='<<', symbol_last='>>',
980 symbol_first='<<', symbol_last='>>',
980 symbol_previous='<', symbol_next='>',
981 symbol_previous='<', symbol_next='>',
981 link_attr={'class': 'pager_link', 'rel': 'prerender'},
982 link_attr={'class': 'pager_link', 'rel': 'prerender'},
982 curpage_attr={'class': 'pager_curpage'},
983 curpage_attr={'class': 'pager_curpage'},
983 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
984 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
984
985
985 self.curpage_attr = curpage_attr
986 self.curpage_attr = curpage_attr
986 self.separator = separator
987 self.separator = separator
987 self.pager_kwargs = kwargs
988 self.pager_kwargs = kwargs
988 self.page_param = page_param
989 self.page_param = page_param
989 self.partial_param = partial_param
990 self.partial_param = partial_param
990 self.onclick = onclick
991 self.onclick = onclick
991 self.link_attr = link_attr
992 self.link_attr = link_attr
992 self.dotdot_attr = dotdot_attr
993 self.dotdot_attr = dotdot_attr
993
994
994 # Don't show navigator if there is no more than one page
995 # Don't show navigator if there is no more than one page
995 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
996 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
996 return ''
997 return ''
997
998
998 from string import Template
999 from string import Template
999 # Replace ~...~ in token format by range of pages
1000 # Replace ~...~ in token format by range of pages
1000 result = re.sub(r'~(\d+)~', self._range, format)
1001 result = re.sub(r'~(\d+)~', self._range, format)
1001
1002
1002 # Interpolate '%' variables
1003 # Interpolate '%' variables
1003 result = Template(result).safe_substitute({
1004 result = Template(result).safe_substitute({
1004 'first_page': self.first_page,
1005 'first_page': self.first_page,
1005 'last_page': self.last_page,
1006 'last_page': self.last_page,
1006 'page': self.page,
1007 'page': self.page,
1007 'page_count': self.page_count,
1008 'page_count': self.page_count,
1008 'items_per_page': self.items_per_page,
1009 'items_per_page': self.items_per_page,
1009 'first_item': self.first_item,
1010 'first_item': self.first_item,
1010 'last_item': self.last_item,
1011 'last_item': self.last_item,
1011 'item_count': self.item_count,
1012 'item_count': self.item_count,
1012 'link_first': self.page > self.first_page and \
1013 'link_first': self.page > self.first_page and \
1013 self._pagerlink(self.first_page, symbol_first) or '',
1014 self._pagerlink(self.first_page, symbol_first) or '',
1014 'link_last': self.page < self.last_page and \
1015 'link_last': self.page < self.last_page and \
1015 self._pagerlink(self.last_page, symbol_last) or '',
1016 self._pagerlink(self.last_page, symbol_last) or '',
1016 'link_previous': self.previous_page and \
1017 'link_previous': self.previous_page and \
1017 self._pagerlink(self.previous_page, symbol_previous) \
1018 self._pagerlink(self.previous_page, symbol_previous) \
1018 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1019 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1019 'link_next': self.next_page and \
1020 'link_next': self.next_page and \
1020 self._pagerlink(self.next_page, symbol_next) \
1021 self._pagerlink(self.next_page, symbol_next) \
1021 or HTML.span(symbol_next, class_="yui-pg-next")
1022 or HTML.span(symbol_next, class_="yui-pg-next")
1022 })
1023 })
1023
1024
1024 return literal(result)
1025 return literal(result)
1025
1026
1026
1027
1027 #==============================================================================
1028 #==============================================================================
1028 # REPO PAGER, PAGER FOR REPOSITORY
1029 # REPO PAGER, PAGER FOR REPOSITORY
1029 #==============================================================================
1030 #==============================================================================
1030 class RepoPage(Page):
1031 class RepoPage(Page):
1031
1032
1032 def __init__(self, collection, page=1, items_per_page=20,
1033 def __init__(self, collection, page=1, items_per_page=20,
1033 item_count=None, url=None, **kwargs):
1034 item_count=None, url=None, **kwargs):
1034
1035
1035 """Create a "RepoPage" instance. special pager for paging
1036 """Create a "RepoPage" instance. special pager for paging
1036 repository
1037 repository
1037 """
1038 """
1038 self._url_generator = url
1039 self._url_generator = url
1039
1040
1040 # Safe the kwargs class-wide so they can be used in the pager() method
1041 # Safe the kwargs class-wide so they can be used in the pager() method
1041 self.kwargs = kwargs
1042 self.kwargs = kwargs
1042
1043
1043 # Save a reference to the collection
1044 # Save a reference to the collection
1044 self.original_collection = collection
1045 self.original_collection = collection
1045
1046
1046 self.collection = collection
1047 self.collection = collection
1047
1048
1048 # The self.page is the number of the current page.
1049 # The self.page is the number of the current page.
1049 # The first page has the number 1!
1050 # The first page has the number 1!
1050 try:
1051 try:
1051 self.page = int(page) # make it int() if we get it as a string
1052 self.page = int(page) # make it int() if we get it as a string
1052 except (ValueError, TypeError):
1053 except (ValueError, TypeError):
1053 self.page = 1
1054 self.page = 1
1054
1055
1055 self.items_per_page = items_per_page
1056 self.items_per_page = items_per_page
1056
1057
1057 # Unless the user tells us how many items the collections has
1058 # Unless the user tells us how many items the collections has
1058 # we calculate that ourselves.
1059 # we calculate that ourselves.
1059 if item_count is not None:
1060 if item_count is not None:
1060 self.item_count = item_count
1061 self.item_count = item_count
1061 else:
1062 else:
1062 self.item_count = len(self.collection)
1063 self.item_count = len(self.collection)
1063
1064
1064 # Compute the number of the first and last available page
1065 # Compute the number of the first and last available page
1065 if self.item_count > 0:
1066 if self.item_count > 0:
1066 self.first_page = 1
1067 self.first_page = 1
1067 self.page_count = int(math.ceil(float(self.item_count) /
1068 self.page_count = int(math.ceil(float(self.item_count) /
1068 self.items_per_page))
1069 self.items_per_page))
1069 self.last_page = self.first_page + self.page_count - 1
1070 self.last_page = self.first_page + self.page_count - 1
1070
1071
1071 # Make sure that the requested page number is the range of
1072 # Make sure that the requested page number is the range of
1072 # valid pages
1073 # valid pages
1073 if self.page > self.last_page:
1074 if self.page > self.last_page:
1074 self.page = self.last_page
1075 self.page = self.last_page
1075 elif self.page < self.first_page:
1076 elif self.page < self.first_page:
1076 self.page = self.first_page
1077 self.page = self.first_page
1077
1078
1078 # Note: the number of items on this page can be less than
1079 # Note: the number of items on this page can be less than
1079 # items_per_page if the last page is not full
1080 # items_per_page if the last page is not full
1080 self.first_item = max(0, (self.item_count) - (self.page *
1081 self.first_item = max(0, (self.item_count) - (self.page *
1081 items_per_page))
1082 items_per_page))
1082 self.last_item = ((self.item_count - 1) - items_per_page *
1083 self.last_item = ((self.item_count - 1) - items_per_page *
1083 (self.page - 1))
1084 (self.page - 1))
1084
1085
1085 self.items = list(self.collection[self.first_item:self.last_item + 1])
1086 self.items = list(self.collection[self.first_item:self.last_item + 1])
1086
1087
1087 # Links to previous and next page
1088 # Links to previous and next page
1088 if self.page > self.first_page:
1089 if self.page > self.first_page:
1089 self.previous_page = self.page - 1
1090 self.previous_page = self.page - 1
1090 else:
1091 else:
1091 self.previous_page = None
1092 self.previous_page = None
1092
1093
1093 if self.page < self.last_page:
1094 if self.page < self.last_page:
1094 self.next_page = self.page + 1
1095 self.next_page = self.page + 1
1095 else:
1096 else:
1096 self.next_page = None
1097 self.next_page = None
1097
1098
1098 # No items available
1099 # No items available
1099 else:
1100 else:
1100 self.first_page = None
1101 self.first_page = None
1101 self.page_count = 0
1102 self.page_count = 0
1102 self.last_page = None
1103 self.last_page = None
1103 self.first_item = None
1104 self.first_item = None
1104 self.last_item = None
1105 self.last_item = None
1105 self.previous_page = None
1106 self.previous_page = None
1106 self.next_page = None
1107 self.next_page = None
1107 self.items = []
1108 self.items = []
1108
1109
1109 # This is a subclass of the 'list' type. Initialise the list now.
1110 # This is a subclass of the 'list' type. Initialise the list now.
1110 list.__init__(self, reversed(self.items))
1111 list.__init__(self, reversed(self.items))
1111
1112
1112
1113
1113 def changed_tooltip(nodes):
1114 def changed_tooltip(nodes):
1114 """
1115 """
1115 Generates a html string for changed nodes in changeset page.
1116 Generates a html string for changed nodes in changeset page.
1116 It limits the output to 30 entries
1117 It limits the output to 30 entries
1117
1118
1118 :param nodes: LazyNodesGenerator
1119 :param nodes: LazyNodesGenerator
1119 """
1120 """
1120 if nodes:
1121 if nodes:
1121 pref = ': <br/> '
1122 pref = ': <br/> '
1122 suf = ''
1123 suf = ''
1123 if len(nodes) > 30:
1124 if len(nodes) > 30:
1124 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1125 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1125 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1126 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1126 for x in nodes[:30]]) + suf)
1127 for x in nodes[:30]]) + suf)
1127 else:
1128 else:
1128 return ': ' + _('No files')
1129 return ': ' + _('No files')
1129
1130
1130
1131
1131 def repo_link(groups_and_repos):
1132 def repo_link(groups_and_repos):
1132 """
1133 """
1133 Makes a breadcrumbs link to repo within a group
1134 Makes a breadcrumbs link to repo within a group
1134 joins &raquo; on each group to create a fancy link
1135 joins &raquo; on each group to create a fancy link
1135
1136
1136 ex::
1137 ex::
1137 group >> subgroup >> repo
1138 group >> subgroup >> repo
1138
1139
1139 :param groups_and_repos:
1140 :param groups_and_repos:
1140 :param last_url:
1141 :param last_url:
1141 """
1142 """
1142 groups, just_name, repo_name = groups_and_repos
1143 groups, just_name, repo_name = groups_and_repos
1143 last_url = url('summary_home', repo_name=repo_name)
1144 last_url = url('summary_home', repo_name=repo_name)
1144 last_link = link_to(just_name, last_url)
1145 last_link = link_to(just_name, last_url)
1145
1146
1146 def make_link(group):
1147 def make_link(group):
1147 return link_to(group.name,
1148 return link_to(group.name,
1148 url('repos_group_home', group_name=group.group_name))
1149 url('repos_group_home', group_name=group.group_name))
1149 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1150 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1150
1151
1151
1152
1152 def fancy_file_stats(stats):
1153 def fancy_file_stats(stats):
1153 """
1154 """
1154 Displays a fancy two colored bar for number of added/deleted
1155 Displays a fancy two colored bar for number of added/deleted
1155 lines of code on file
1156 lines of code on file
1156
1157
1157 :param stats: two element list of added/deleted lines of code
1158 :param stats: two element list of added/deleted lines of code
1158 """
1159 """
1159 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1160 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1160 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1161 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1161
1162
1162 def cgen(l_type, a_v, d_v):
1163 def cgen(l_type, a_v, d_v):
1163 mapping = {'tr': 'top-right-rounded-corner-mid',
1164 mapping = {'tr': 'top-right-rounded-corner-mid',
1164 'tl': 'top-left-rounded-corner-mid',
1165 'tl': 'top-left-rounded-corner-mid',
1165 'br': 'bottom-right-rounded-corner-mid',
1166 'br': 'bottom-right-rounded-corner-mid',
1166 'bl': 'bottom-left-rounded-corner-mid'}
1167 'bl': 'bottom-left-rounded-corner-mid'}
1167 map_getter = lambda x: mapping[x]
1168 map_getter = lambda x: mapping[x]
1168
1169
1169 if l_type == 'a' and d_v:
1170 if l_type == 'a' and d_v:
1170 #case when added and deleted are present
1171 #case when added and deleted are present
1171 return ' '.join(map(map_getter, ['tl', 'bl']))
1172 return ' '.join(map(map_getter, ['tl', 'bl']))
1172
1173
1173 if l_type == 'a' and not d_v:
1174 if l_type == 'a' and not d_v:
1174 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1175 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1175
1176
1176 if l_type == 'd' and a_v:
1177 if l_type == 'd' and a_v:
1177 return ' '.join(map(map_getter, ['tr', 'br']))
1178 return ' '.join(map(map_getter, ['tr', 'br']))
1178
1179
1179 if l_type == 'd' and not a_v:
1180 if l_type == 'd' and not a_v:
1180 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1181 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1181
1182
1182 a, d = stats['added'], stats['deleted']
1183 a, d = stats['added'], stats['deleted']
1183 width = 100
1184 width = 100
1184
1185
1185 if stats['binary']:
1186 if stats['binary']:
1186 #binary mode
1187 #binary mode
1187 lbl = ''
1188 lbl = ''
1188 bin_op = 1
1189 bin_op = 1
1189
1190
1190 if BIN_FILENODE in stats['ops']:
1191 if BIN_FILENODE in stats['ops']:
1191 lbl = 'bin+'
1192 lbl = 'bin+'
1192
1193
1193 if NEW_FILENODE in stats['ops']:
1194 if NEW_FILENODE in stats['ops']:
1194 lbl += _('new file')
1195 lbl += _('new file')
1195 bin_op = NEW_FILENODE
1196 bin_op = NEW_FILENODE
1196 elif MOD_FILENODE in stats['ops']:
1197 elif MOD_FILENODE in stats['ops']:
1197 lbl += _('mod')
1198 lbl += _('mod')
1198 bin_op = MOD_FILENODE
1199 bin_op = MOD_FILENODE
1199 elif DEL_FILENODE in stats['ops']:
1200 elif DEL_FILENODE in stats['ops']:
1200 lbl += _('del')
1201 lbl += _('del')
1201 bin_op = DEL_FILENODE
1202 bin_op = DEL_FILENODE
1202 elif RENAMED_FILENODE in stats['ops']:
1203 elif RENAMED_FILENODE in stats['ops']:
1203 lbl += _('rename')
1204 lbl += _('rename')
1204 bin_op = RENAMED_FILENODE
1205 bin_op = RENAMED_FILENODE
1205
1206
1206 #chmod can go with other operations
1207 #chmod can go with other operations
1207 if CHMOD_FILENODE in stats['ops']:
1208 if CHMOD_FILENODE in stats['ops']:
1208 _org_lbl = _('chmod')
1209 _org_lbl = _('chmod')
1209 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1210 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1210
1211
1211 #import ipdb;ipdb.set_trace()
1212 #import ipdb;ipdb.set_trace()
1212 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1213 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1213 b_a = '<div class="bin bin1" style="width:0%"></div>'
1214 b_a = '<div class="bin bin1" style="width:0%"></div>'
1214 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1215 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1215
1216
1216 t = stats['added'] + stats['deleted']
1217 t = stats['added'] + stats['deleted']
1217 unit = float(width) / (t or 1)
1218 unit = float(width) / (t or 1)
1218
1219
1219 # needs > 9% of width to be visible or 0 to be hidden
1220 # needs > 9% of width to be visible or 0 to be hidden
1220 a_p = max(9, unit * a) if a > 0 else 0
1221 a_p = max(9, unit * a) if a > 0 else 0
1221 d_p = max(9, unit * d) if d > 0 else 0
1222 d_p = max(9, unit * d) if d > 0 else 0
1222 p_sum = a_p + d_p
1223 p_sum = a_p + d_p
1223
1224
1224 if p_sum > width:
1225 if p_sum > width:
1225 #adjust the percentage to be == 100% since we adjusted to 9
1226 #adjust the percentage to be == 100% since we adjusted to 9
1226 if a_p > d_p:
1227 if a_p > d_p:
1227 a_p = a_p - (p_sum - width)
1228 a_p = a_p - (p_sum - width)
1228 else:
1229 else:
1229 d_p = d_p - (p_sum - width)
1230 d_p = d_p - (p_sum - width)
1230
1231
1231 a_v = a if a > 0 else ''
1232 a_v = a if a > 0 else ''
1232 d_v = d if d > 0 else ''
1233 d_v = d if d > 0 else ''
1233
1234
1234 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1235 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1235 cgen('a', a_v, d_v), a_p, a_v
1236 cgen('a', a_v, d_v), a_p, a_v
1236 )
1237 )
1237 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1238 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1238 cgen('d', a_v, d_v), d_p, d_v
1239 cgen('d', a_v, d_v), d_p, d_v
1239 )
1240 )
1240 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1241 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1241
1242
1242
1243
1243 def _urlify_text(s):
1244 def _urlify_text(s):
1244 """
1245 """
1245 Extract urls from text and make html links out of them
1246 Extract urls from text and make html links out of them
1246 """
1247 """
1247 def url_func(match_obj):
1248 def url_func(match_obj):
1248 url_full = match_obj.group(1)
1249 url_full = match_obj.group(1)
1249 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1250 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1250 return url_re.sub(url_func, s)
1251 return url_re.sub(url_func, s)
1251
1252
1252 def urlify_text(s, truncate=None, stylize=False, truncatef=truncate):
1253 def urlify_text(s, truncate=None, stylize=False, truncatef=truncate):
1253 """
1254 """
1254 Extract urls from text and make literal html links out of them
1255 Extract urls from text and make literal html links out of them
1255 """
1256 """
1256 if truncate is not None:
1257 if truncate is not None:
1257 s = truncatef(s, truncate)
1258 s = truncatef(s, truncate)
1258 s = html_escape(s)
1259 s = html_escape(s)
1259 if stylize:
1260 if stylize:
1260 s = desc_stylize(s)
1261 s = desc_stylize(s)
1261 s = _urlify_text(s)
1262 s = _urlify_text(s)
1262 return literal(s)
1263 return literal(s)
1263
1264
1264 def urlify_changesets(text_, repository):
1265 def urlify_changesets(text_, repository):
1265 """
1266 """
1266 Extract revision ids from changeset and make link from them
1267 Extract revision ids from changeset and make link from them
1267
1268
1268 :param text_:
1269 :param text_:
1269 :param repository: repo name to build the URL with
1270 :param repository: repo name to build the URL with
1270 """
1271 """
1271 from pylons import url # doh, we need to re-import url to mock it later
1272 from pylons import url # doh, we need to re-import url to mock it later
1272
1273
1273 def url_func(match_obj):
1274 def url_func(match_obj):
1274 rev = match_obj.group(0)
1275 rev = match_obj.group(0)
1275 return '<a class="revision-link" href="%(url)s">%(rev)s</a>' % {
1276 return '<a class="revision-link" href="%(url)s">%(rev)s</a>' % {
1276 'url': url('changeset_home', repo_name=repository, revision=rev),
1277 'url': url('changeset_home', repo_name=repository, revision=rev),
1277 'rev': rev,
1278 'rev': rev,
1278 }
1279 }
1279
1280
1280 return re.sub(r'(?:^|(?<=[\s(),]))([0-9a-fA-F]{12,40})(?=$|\s|[.,:()])', url_func, text_)
1281 return re.sub(r'(?:^|(?<=[\s(),]))([0-9a-fA-F]{12,40})(?=$|\s|[.,:()])', url_func, text_)
1281
1282
1282 def linkify_others(t, l):
1283 def linkify_others(t, l):
1283 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1284 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1284 links = []
1285 links = []
1285 for e in urls.split(t):
1286 for e in urls.split(t):
1286 if not urls.match(e):
1287 if not urls.match(e):
1287 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1288 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1288 else:
1289 else:
1289 links.append(e)
1290 links.append(e)
1290
1291
1291 return ''.join(links)
1292 return ''.join(links)
1292
1293
1293 def urlify_commit(text_, repository, link_=None):
1294 def urlify_commit(text_, repository, link_=None):
1294 """
1295 """
1295 Parses given text message and makes proper links.
1296 Parses given text message and makes proper links.
1296 issues are linked to given issue-server, and rest is a changeset link
1297 issues are linked to given issue-server, and rest is a changeset link
1297 if link_ is given, in other case it's a plain text
1298 if link_ is given, in other case it's a plain text
1298
1299
1299 :param text_:
1300 :param text_:
1300 :param repository:
1301 :param repository:
1301 :param link_: changeset link
1302 :param link_: changeset link
1302 """
1303 """
1303 newtext = html_escape(text_)
1304 newtext = html_escape(text_)
1304
1305
1305 # urlify changesets - extract revisions and make link out of them
1306 # urlify changesets - extract revisions and make link out of them
1306 newtext = urlify_changesets(newtext, repository)
1307 newtext = urlify_changesets(newtext, repository)
1307
1308
1308 # extract http/https links and make them real urls
1309 # extract http/https links and make them real urls
1309 newtext = _urlify_text(newtext)
1310 newtext = _urlify_text(newtext)
1310
1311
1311 newtext = urlify_issues(newtext, repository, link_)
1312 newtext = urlify_issues(newtext, repository, link_)
1312
1313
1313 return literal(newtext)
1314 return literal(newtext)
1314
1315
1315 def urlify_issues(newtext, repository, link_=None):
1316 def urlify_issues(newtext, repository, link_=None):
1316 from kallithea import CONFIG as conf
1317 from kallithea import CONFIG as conf
1317
1318
1318 # allow multiple issue servers to be used
1319 # allow multiple issue servers to be used
1319 valid_indices = [
1320 valid_indices = [
1320 x.group(1)
1321 x.group(1)
1321 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1322 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1322 if x and 'issue_server_link%s' % x.group(1) in conf
1323 if x and 'issue_server_link%s' % x.group(1) in conf
1323 and 'issue_prefix%s' % x.group(1) in conf
1324 and 'issue_prefix%s' % x.group(1) in conf
1324 ]
1325 ]
1325
1326
1326 if valid_indices:
1327 if valid_indices:
1327 log.debug('found issue server suffixes `%s` during valuation of: %s',
1328 log.debug('found issue server suffixes `%s` during valuation of: %s',
1328 ','.join(valid_indices), newtext)
1329 ','.join(valid_indices), newtext)
1329
1330
1330 for pattern_index in valid_indices:
1331 for pattern_index in valid_indices:
1331 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1332 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1332 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1333 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1333 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1334 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1334
1335
1335 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s',
1336 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s',
1336 pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1337 pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1337 ISSUE_PREFIX)
1338 ISSUE_PREFIX)
1338
1339
1339 URL_PAT = re.compile(ISSUE_PATTERN)
1340 URL_PAT = re.compile(ISSUE_PATTERN)
1340
1341
1341 def url_func(match_obj):
1342 def url_func(match_obj):
1342 pref = ''
1343 pref = ''
1343 if match_obj.group().startswith(' '):
1344 if match_obj.group().startswith(' '):
1344 pref = ' '
1345 pref = ' '
1345
1346
1346 issue_id = ''.join(match_obj.groups())
1347 issue_id = ''.join(match_obj.groups())
1347 issue_url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1348 issue_url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1348 if repository:
1349 if repository:
1349 issue_url = issue_url.replace('{repo}', repository)
1350 issue_url = issue_url.replace('{repo}', repository)
1350 repo_name = repository.split(URL_SEP)[-1]
1351 repo_name = repository.split(URL_SEP)[-1]
1351 issue_url = issue_url.replace('{repo_name}', repo_name)
1352 issue_url = issue_url.replace('{repo_name}', repo_name)
1352
1353
1353 return (
1354 return (
1354 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1355 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1355 '%(issue-prefix)s%(id-repr)s'
1356 '%(issue-prefix)s%(id-repr)s'
1356 '</a>'
1357 '</a>'
1357 ) % {
1358 ) % {
1358 'pref': pref,
1359 'pref': pref,
1359 'cls': 'issue-tracker-link',
1360 'cls': 'issue-tracker-link',
1360 'url': issue_url,
1361 'url': issue_url,
1361 'id-repr': issue_id,
1362 'id-repr': issue_id,
1362 'issue-prefix': ISSUE_PREFIX,
1363 'issue-prefix': ISSUE_PREFIX,
1363 'serv': ISSUE_SERVER_LNK,
1364 'serv': ISSUE_SERVER_LNK,
1364 }
1365 }
1365 newtext = URL_PAT.sub(url_func, newtext)
1366 newtext = URL_PAT.sub(url_func, newtext)
1366 log.debug('processed prefix:`%s` => %s', pattern_index, newtext)
1367 log.debug('processed prefix:`%s` => %s', pattern_index, newtext)
1367
1368
1368 # if we actually did something above
1369 # if we actually did something above
1369 if link_:
1370 if link_:
1370 # wrap not links into final link => link_
1371 # wrap not links into final link => link_
1371 newtext = linkify_others(newtext, link_)
1372 newtext = linkify_others(newtext, link_)
1372 return newtext
1373 return newtext
1373
1374
1374
1375
1375 def rst(source):
1376 def rst(source):
1376 return literal('<div class="rst-block">%s</div>' %
1377 return literal('<div class="rst-block">%s</div>' %
1377 MarkupRenderer.rst(source))
1378 MarkupRenderer.rst(source))
1378
1379
1379
1380
1380 def rst_w_mentions(source):
1381 def rst_w_mentions(source):
1381 """
1382 """
1382 Wrapped rst renderer with @mention highlighting
1383 Wrapped rst renderer with @mention highlighting
1383
1384
1384 :param source:
1385 :param source:
1385 """
1386 """
1386 return literal('<div class="rst-block">%s</div>' %
1387 return literal('<div class="rst-block">%s</div>' %
1387 MarkupRenderer.rst_with_mentions(source))
1388 MarkupRenderer.rst_with_mentions(source))
1388
1389
1389 def short_ref(ref_type, ref_name):
1390 def short_ref(ref_type, ref_name):
1390 if ref_type == 'rev':
1391 if ref_type == 'rev':
1391 return short_id(ref_name)
1392 return short_id(ref_name)
1392 return ref_name
1393 return ref_name
1393
1394
1394 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1395 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1395 """
1396 """
1396 Return full markup for a href to changeset_home for a changeset.
1397 Return full markup for a href to changeset_home for a changeset.
1397 If ref_type is branch it will link to changelog.
1398 If ref_type is branch it will link to changelog.
1398 ref_name is shortened if ref_type is 'rev'.
1399 ref_name is shortened if ref_type is 'rev'.
1399 if rev is specified show it too, explicitly linking to that revision.
1400 if rev is specified show it too, explicitly linking to that revision.
1400 """
1401 """
1401 txt = short_ref(ref_type, ref_name)
1402 txt = short_ref(ref_type, ref_name)
1402 if ref_type == 'branch':
1403 if ref_type == 'branch':
1403 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1404 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1404 else:
1405 else:
1405 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1406 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1406 l = link_to(repo_name + '#' + txt, u)
1407 l = link_to(repo_name + '#' + txt, u)
1407 if rev and ref_type != 'rev':
1408 if rev and ref_type != 'rev':
1408 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1409 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1409 return l
1410 return l
1410
1411
1411 def changeset_status(repo, revision):
1412 def changeset_status(repo, revision):
1412 return ChangesetStatusModel().get_status(repo, revision)
1413 return ChangesetStatusModel().get_status(repo, revision)
1413
1414
1414
1415
1415 def changeset_status_lbl(changeset_status):
1416 def changeset_status_lbl(changeset_status):
1416 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1417 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1417
1418
1418
1419
1419 def get_permission_name(key):
1420 def get_permission_name(key):
1420 return dict(Permission.PERMS).get(key)
1421 return dict(Permission.PERMS).get(key)
1421
1422
1422
1423
1423 def journal_filter_help():
1424 def journal_filter_help():
1424 return _(textwrap.dedent('''
1425 return _(textwrap.dedent('''
1425 Example filter terms:
1426 Example filter terms:
1426 repository:vcs
1427 repository:vcs
1427 username:developer
1428 username:developer
1428 action:*push*
1429 action:*push*
1429 ip:127.0.0.1
1430 ip:127.0.0.1
1430 date:20120101
1431 date:20120101
1431 date:[20120101100000 TO 20120102]
1432 date:[20120101100000 TO 20120102]
1432
1433
1433 Generate wildcards using '*' character:
1434 Generate wildcards using '*' character:
1434 "repository:vcs*" - search everything starting with 'vcs'
1435 "repository:vcs*" - search everything starting with 'vcs'
1435 "repository:*vcs*" - search for repository containing 'vcs'
1436 "repository:*vcs*" - search for repository containing 'vcs'
1436
1437
1437 Optional AND / OR operators in queries
1438 Optional AND / OR operators in queries
1438 "repository:vcs OR repository:test"
1439 "repository:vcs OR repository:test"
1439 "username:test AND repository:test*"
1440 "username:test AND repository:test*"
1440 '''))
1441 '''))
1441
1442
1442
1443
1443 def not_mapped_error(repo_name):
1444 def not_mapped_error(repo_name):
1444 flash(_('%s repository is not mapped to db perhaps'
1445 flash(_('%s repository is not mapped to db perhaps'
1445 ' it was created or renamed from the filesystem'
1446 ' it was created or renamed from the filesystem'
1446 ' please run the application again'
1447 ' please run the application again'
1447 ' in order to rescan repositories') % repo_name, category='error')
1448 ' in order to rescan repositories') % repo_name, category='error')
1448
1449
1449
1450
1450 def ip_range(ip_addr):
1451 def ip_range(ip_addr):
1451 from kallithea.model.db import UserIpMap
1452 from kallithea.model.db import UserIpMap
1452 s, e = UserIpMap._get_ip_range(ip_addr)
1453 s, e = UserIpMap._get_ip_range(ip_addr)
1453 return '%s - %s' % (s, e)
1454 return '%s - %s' % (s, e)
1455
1456
1457 def form(url, method="post", **attrs):
1458 """Like webhelpers.html.tags.form but automatically using secure_form with
1459 authentication_token for POST. authentication_token is thus never leaked
1460 in the URL."""
1461 if method.lower() == 'get':
1462 return insecure_form(url, method=method, **attrs)
1463 # webhelpers will turn everything but GET into POST
1464 return secure_form(url, method=method, **attrs)
General Comments 0
You need to be logged in to leave comments. Login now