##// END OF EJS Templates
helpers: flash messages can now carry additional information in json format
ergo -
r952:039d714b default
parent child Browse files
Show More
@@ -1,1992 +1,2001 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27
27
28 import random
28 import random
29 import hashlib
29 import hashlib
30 import StringIO
30 import StringIO
31 import urllib
31 import urllib
32 import math
32 import math
33 import logging
33 import logging
34 import re
34 import re
35 import urlparse
35 import urlparse
36 import time
36 import time
37 import string
37 import string
38 import hashlib
38 import hashlib
39 import pygments
39 import pygments
40
40
41 from datetime import datetime
41 from datetime import datetime
42 from functools import partial
42 from functools import partial
43 from pygments.formatters.html import HtmlFormatter
43 from pygments.formatters.html import HtmlFormatter
44 from pygments import highlight as code_highlight
44 from pygments import highlight as code_highlight
45 from pygments.lexers import (
45 from pygments.lexers import (
46 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
46 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
47 from pylons import url as pylons_url
47 from pylons import url as pylons_url
48 from pylons.i18n.translation import _, ungettext
48 from pylons.i18n.translation import _, ungettext
49 from pyramid.threadlocal import get_current_request
49 from pyramid.threadlocal import get_current_request
50
50
51 from webhelpers.html import literal, HTML, escape
51 from webhelpers.html import literal, HTML, escape
52 from webhelpers.html.tools import *
52 from webhelpers.html.tools import *
53 from webhelpers.html.builder import make_tag
53 from webhelpers.html.builder import make_tag
54 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
54 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
55 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
55 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
56 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
56 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
57 submit, text, password, textarea, title, ul, xml_declaration, radio
57 submit, text, password, textarea, title, ul, xml_declaration, radio
58 from webhelpers.html.tools import auto_link, button_to, highlight, \
58 from webhelpers.html.tools import auto_link, button_to, highlight, \
59 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
59 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
60 from webhelpers.pylonslib import Flash as _Flash
60 from webhelpers.pylonslib import Flash as _Flash
61 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
61 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
62 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
62 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
63 replace_whitespace, urlify, truncate, wrap_paragraphs
63 replace_whitespace, urlify, truncate, wrap_paragraphs
64 from webhelpers.date import time_ago_in_words
64 from webhelpers.date import time_ago_in_words
65 from webhelpers.paginate import Page as _Page
65 from webhelpers.paginate import Page as _Page
66 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
66 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
67 convert_boolean_attrs, NotGiven, _make_safe_id_component
67 convert_boolean_attrs, NotGiven, _make_safe_id_component
68 from webhelpers2.number import format_byte_size
68 from webhelpers2.number import format_byte_size
69
69
70 from rhodecode.lib.annotate import annotate_highlight
70 from rhodecode.lib.annotate import annotate_highlight
71 from rhodecode.lib.action_parser import action_parser
71 from rhodecode.lib.action_parser import action_parser
72 from rhodecode.lib.ext_json import json
72 from rhodecode.lib.ext_json import json
73 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
73 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
74 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
74 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
75 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
75 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
76 AttributeDict, safe_int, md5, md5_safe
76 AttributeDict, safe_int, md5, md5_safe
77 from rhodecode.lib.markup_renderer import MarkupRenderer
77 from rhodecode.lib.markup_renderer import MarkupRenderer
78 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
78 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
79 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
79 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
80 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
80 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
81 from rhodecode.model.changeset_status import ChangesetStatusModel
81 from rhodecode.model.changeset_status import ChangesetStatusModel
82 from rhodecode.model.db import Permission, User, Repository
82 from rhodecode.model.db import Permission, User, Repository
83 from rhodecode.model.repo_group import RepoGroupModel
83 from rhodecode.model.repo_group import RepoGroupModel
84 from rhodecode.model.settings import IssueTrackerSettingsModel
84 from rhodecode.model.settings import IssueTrackerSettingsModel
85
85
86 log = logging.getLogger(__name__)
86 log = logging.getLogger(__name__)
87
87
88
88
89 DEFAULT_USER = User.DEFAULT_USER
89 DEFAULT_USER = User.DEFAULT_USER
90 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
90 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
91
91
92
92
93 def url(*args, **kw):
93 def url(*args, **kw):
94 return pylons_url(*args, **kw)
94 return pylons_url(*args, **kw)
95
95
96
96
97 def pylons_url_current(*args, **kw):
97 def pylons_url_current(*args, **kw):
98 """
98 """
99 This function overrides pylons.url.current() which returns the current
99 This function overrides pylons.url.current() which returns the current
100 path so that it will also work from a pyramid only context. This
100 path so that it will also work from a pyramid only context. This
101 should be removed once port to pyramid is complete.
101 should be removed once port to pyramid is complete.
102 """
102 """
103 if not args and not kw:
103 if not args and not kw:
104 request = get_current_request()
104 request = get_current_request()
105 return request.path
105 return request.path
106 return pylons_url.current(*args, **kw)
106 return pylons_url.current(*args, **kw)
107
107
108 url.current = pylons_url_current
108 url.current = pylons_url_current
109
109
110
110
111 def asset(path, ver=None):
111 def asset(path, ver=None):
112 """
112 """
113 Helper to generate a static asset file path for rhodecode assets
113 Helper to generate a static asset file path for rhodecode assets
114
114
115 eg. h.asset('images/image.png', ver='3923')
115 eg. h.asset('images/image.png', ver='3923')
116
116
117 :param path: path of asset
117 :param path: path of asset
118 :param ver: optional version query param to append as ?ver=
118 :param ver: optional version query param to append as ?ver=
119 """
119 """
120 request = get_current_request()
120 request = get_current_request()
121 query = {}
121 query = {}
122 if ver:
122 if ver:
123 query = {'ver': ver}
123 query = {'ver': ver}
124 return request.static_path(
124 return request.static_path(
125 'rhodecode:public/{}'.format(path), _query=query)
125 'rhodecode:public/{}'.format(path), _query=query)
126
126
127
127
128 def html_escape(text, html_escape_table=None):
128 def html_escape(text, html_escape_table=None):
129 """Produce entities within text."""
129 """Produce entities within text."""
130 if not html_escape_table:
130 if not html_escape_table:
131 html_escape_table = {
131 html_escape_table = {
132 "&": "&amp;",
132 "&": "&amp;",
133 '"': "&quot;",
133 '"': "&quot;",
134 "'": "&apos;",
134 "'": "&apos;",
135 ">": "&gt;",
135 ">": "&gt;",
136 "<": "&lt;",
136 "<": "&lt;",
137 }
137 }
138 return "".join(html_escape_table.get(c, c) for c in text)
138 return "".join(html_escape_table.get(c, c) for c in text)
139
139
140
140
141 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
141 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
142 """
142 """
143 Truncate string ``s`` at the first occurrence of ``sub``.
143 Truncate string ``s`` at the first occurrence of ``sub``.
144
144
145 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
145 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
146 """
146 """
147 suffix_if_chopped = suffix_if_chopped or ''
147 suffix_if_chopped = suffix_if_chopped or ''
148 pos = s.find(sub)
148 pos = s.find(sub)
149 if pos == -1:
149 if pos == -1:
150 return s
150 return s
151
151
152 if inclusive:
152 if inclusive:
153 pos += len(sub)
153 pos += len(sub)
154
154
155 chopped = s[:pos]
155 chopped = s[:pos]
156 left = s[pos:].strip()
156 left = s[pos:].strip()
157
157
158 if left and suffix_if_chopped:
158 if left and suffix_if_chopped:
159 chopped += suffix_if_chopped
159 chopped += suffix_if_chopped
160
160
161 return chopped
161 return chopped
162
162
163
163
164 def shorter(text, size=20):
164 def shorter(text, size=20):
165 postfix = '...'
165 postfix = '...'
166 if len(text) > size:
166 if len(text) > size:
167 return text[:size - len(postfix)] + postfix
167 return text[:size - len(postfix)] + postfix
168 return text
168 return text
169
169
170
170
171 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
171 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
172 """
172 """
173 Reset button
173 Reset button
174 """
174 """
175 _set_input_attrs(attrs, type, name, value)
175 _set_input_attrs(attrs, type, name, value)
176 _set_id_attr(attrs, id, name)
176 _set_id_attr(attrs, id, name)
177 convert_boolean_attrs(attrs, ["disabled"])
177 convert_boolean_attrs(attrs, ["disabled"])
178 return HTML.input(**attrs)
178 return HTML.input(**attrs)
179
179
180 reset = _reset
180 reset = _reset
181 safeid = _make_safe_id_component
181 safeid = _make_safe_id_component
182
182
183
183
184 def branding(name, length=40):
184 def branding(name, length=40):
185 return truncate(name, length, indicator="")
185 return truncate(name, length, indicator="")
186
186
187
187
188 def FID(raw_id, path):
188 def FID(raw_id, path):
189 """
189 """
190 Creates a unique ID for filenode based on it's hash of path and commit
190 Creates a unique ID for filenode based on it's hash of path and commit
191 it's safe to use in urls
191 it's safe to use in urls
192
192
193 :param raw_id:
193 :param raw_id:
194 :param path:
194 :param path:
195 """
195 """
196
196
197 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
197 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
198
198
199
199
200 class _GetError(object):
200 class _GetError(object):
201 """Get error from form_errors, and represent it as span wrapped error
201 """Get error from form_errors, and represent it as span wrapped error
202 message
202 message
203
203
204 :param field_name: field to fetch errors for
204 :param field_name: field to fetch errors for
205 :param form_errors: form errors dict
205 :param form_errors: form errors dict
206 """
206 """
207
207
208 def __call__(self, field_name, form_errors):
208 def __call__(self, field_name, form_errors):
209 tmpl = """<span class="error_msg">%s</span>"""
209 tmpl = """<span class="error_msg">%s</span>"""
210 if form_errors and field_name in form_errors:
210 if form_errors and field_name in form_errors:
211 return literal(tmpl % form_errors.get(field_name))
211 return literal(tmpl % form_errors.get(field_name))
212
212
213 get_error = _GetError()
213 get_error = _GetError()
214
214
215
215
216 class _ToolTip(object):
216 class _ToolTip(object):
217
217
218 def __call__(self, tooltip_title, trim_at=50):
218 def __call__(self, tooltip_title, trim_at=50):
219 """
219 """
220 Special function just to wrap our text into nice formatted
220 Special function just to wrap our text into nice formatted
221 autowrapped text
221 autowrapped text
222
222
223 :param tooltip_title:
223 :param tooltip_title:
224 """
224 """
225 tooltip_title = escape(tooltip_title)
225 tooltip_title = escape(tooltip_title)
226 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
226 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
227 return tooltip_title
227 return tooltip_title
228 tooltip = _ToolTip()
228 tooltip = _ToolTip()
229
229
230
230
231 def files_breadcrumbs(repo_name, commit_id, file_path):
231 def files_breadcrumbs(repo_name, commit_id, file_path):
232 if isinstance(file_path, str):
232 if isinstance(file_path, str):
233 file_path = safe_unicode(file_path)
233 file_path = safe_unicode(file_path)
234
234
235 # TODO: johbo: Is this always a url like path, or is this operating
235 # TODO: johbo: Is this always a url like path, or is this operating
236 # system dependent?
236 # system dependent?
237 path_segments = file_path.split('/')
237 path_segments = file_path.split('/')
238
238
239 repo_name_html = escape(repo_name)
239 repo_name_html = escape(repo_name)
240 if len(path_segments) == 1 and path_segments[0] == '':
240 if len(path_segments) == 1 and path_segments[0] == '':
241 url_segments = [repo_name_html]
241 url_segments = [repo_name_html]
242 else:
242 else:
243 url_segments = [
243 url_segments = [
244 link_to(
244 link_to(
245 repo_name_html,
245 repo_name_html,
246 url('files_home',
246 url('files_home',
247 repo_name=repo_name,
247 repo_name=repo_name,
248 revision=commit_id,
248 revision=commit_id,
249 f_path=''),
249 f_path=''),
250 class_='pjax-link')]
250 class_='pjax-link')]
251
251
252 last_cnt = len(path_segments) - 1
252 last_cnt = len(path_segments) - 1
253 for cnt, segment in enumerate(path_segments):
253 for cnt, segment in enumerate(path_segments):
254 if not segment:
254 if not segment:
255 continue
255 continue
256 segment_html = escape(segment)
256 segment_html = escape(segment)
257
257
258 if cnt != last_cnt:
258 if cnt != last_cnt:
259 url_segments.append(
259 url_segments.append(
260 link_to(
260 link_to(
261 segment_html,
261 segment_html,
262 url('files_home',
262 url('files_home',
263 repo_name=repo_name,
263 repo_name=repo_name,
264 revision=commit_id,
264 revision=commit_id,
265 f_path='/'.join(path_segments[:cnt + 1])),
265 f_path='/'.join(path_segments[:cnt + 1])),
266 class_='pjax-link'))
266 class_='pjax-link'))
267 else:
267 else:
268 url_segments.append(segment_html)
268 url_segments.append(segment_html)
269
269
270 return literal('/'.join(url_segments))
270 return literal('/'.join(url_segments))
271
271
272
272
273 class CodeHtmlFormatter(HtmlFormatter):
273 class CodeHtmlFormatter(HtmlFormatter):
274 """
274 """
275 My code Html Formatter for source codes
275 My code Html Formatter for source codes
276 """
276 """
277
277
278 def wrap(self, source, outfile):
278 def wrap(self, source, outfile):
279 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
279 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
280
280
281 def _wrap_code(self, source):
281 def _wrap_code(self, source):
282 for cnt, it in enumerate(source):
282 for cnt, it in enumerate(source):
283 i, t = it
283 i, t = it
284 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
284 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
285 yield i, t
285 yield i, t
286
286
287 def _wrap_tablelinenos(self, inner):
287 def _wrap_tablelinenos(self, inner):
288 dummyoutfile = StringIO.StringIO()
288 dummyoutfile = StringIO.StringIO()
289 lncount = 0
289 lncount = 0
290 for t, line in inner:
290 for t, line in inner:
291 if t:
291 if t:
292 lncount += 1
292 lncount += 1
293 dummyoutfile.write(line)
293 dummyoutfile.write(line)
294
294
295 fl = self.linenostart
295 fl = self.linenostart
296 mw = len(str(lncount + fl - 1))
296 mw = len(str(lncount + fl - 1))
297 sp = self.linenospecial
297 sp = self.linenospecial
298 st = self.linenostep
298 st = self.linenostep
299 la = self.lineanchors
299 la = self.lineanchors
300 aln = self.anchorlinenos
300 aln = self.anchorlinenos
301 nocls = self.noclasses
301 nocls = self.noclasses
302 if sp:
302 if sp:
303 lines = []
303 lines = []
304
304
305 for i in range(fl, fl + lncount):
305 for i in range(fl, fl + lncount):
306 if i % st == 0:
306 if i % st == 0:
307 if i % sp == 0:
307 if i % sp == 0:
308 if aln:
308 if aln:
309 lines.append('<a href="#%s%d" class="special">%*d</a>' %
309 lines.append('<a href="#%s%d" class="special">%*d</a>' %
310 (la, i, mw, i))
310 (la, i, mw, i))
311 else:
311 else:
312 lines.append('<span class="special">%*d</span>' % (mw, i))
312 lines.append('<span class="special">%*d</span>' % (mw, i))
313 else:
313 else:
314 if aln:
314 if aln:
315 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
315 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
316 else:
316 else:
317 lines.append('%*d' % (mw, i))
317 lines.append('%*d' % (mw, i))
318 else:
318 else:
319 lines.append('')
319 lines.append('')
320 ls = '\n'.join(lines)
320 ls = '\n'.join(lines)
321 else:
321 else:
322 lines = []
322 lines = []
323 for i in range(fl, fl + lncount):
323 for i in range(fl, fl + lncount):
324 if i % st == 0:
324 if i % st == 0:
325 if aln:
325 if aln:
326 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
326 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
327 else:
327 else:
328 lines.append('%*d' % (mw, i))
328 lines.append('%*d' % (mw, i))
329 else:
329 else:
330 lines.append('')
330 lines.append('')
331 ls = '\n'.join(lines)
331 ls = '\n'.join(lines)
332
332
333 # in case you wonder about the seemingly redundant <div> here: since the
333 # in case you wonder about the seemingly redundant <div> here: since the
334 # content in the other cell also is wrapped in a div, some browsers in
334 # content in the other cell also is wrapped in a div, some browsers in
335 # some configurations seem to mess up the formatting...
335 # some configurations seem to mess up the formatting...
336 if nocls:
336 if nocls:
337 yield 0, ('<table class="%stable">' % self.cssclass +
337 yield 0, ('<table class="%stable">' % self.cssclass +
338 '<tr><td><div class="linenodiv" '
338 '<tr><td><div class="linenodiv" '
339 'style="background-color: #f0f0f0; padding-right: 10px">'
339 'style="background-color: #f0f0f0; padding-right: 10px">'
340 '<pre style="line-height: 125%">' +
340 '<pre style="line-height: 125%">' +
341 ls + '</pre></div></td><td id="hlcode" class="code">')
341 ls + '</pre></div></td><td id="hlcode" class="code">')
342 else:
342 else:
343 yield 0, ('<table class="%stable">' % self.cssclass +
343 yield 0, ('<table class="%stable">' % self.cssclass +
344 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
344 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
345 ls + '</pre></div></td><td id="hlcode" class="code">')
345 ls + '</pre></div></td><td id="hlcode" class="code">')
346 yield 0, dummyoutfile.getvalue()
346 yield 0, dummyoutfile.getvalue()
347 yield 0, '</td></tr></table>'
347 yield 0, '</td></tr></table>'
348
348
349
349
350 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
350 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
351 def __init__(self, **kw):
351 def __init__(self, **kw):
352 # only show these line numbers if set
352 # only show these line numbers if set
353 self.only_lines = kw.pop('only_line_numbers', [])
353 self.only_lines = kw.pop('only_line_numbers', [])
354 self.query_terms = kw.pop('query_terms', [])
354 self.query_terms = kw.pop('query_terms', [])
355 self.max_lines = kw.pop('max_lines', 5)
355 self.max_lines = kw.pop('max_lines', 5)
356 self.line_context = kw.pop('line_context', 3)
356 self.line_context = kw.pop('line_context', 3)
357 self.url = kw.pop('url', None)
357 self.url = kw.pop('url', None)
358
358
359 super(CodeHtmlFormatter, self).__init__(**kw)
359 super(CodeHtmlFormatter, self).__init__(**kw)
360
360
361 def _wrap_code(self, source):
361 def _wrap_code(self, source):
362 for cnt, it in enumerate(source):
362 for cnt, it in enumerate(source):
363 i, t = it
363 i, t = it
364 t = '<pre>%s</pre>' % t
364 t = '<pre>%s</pre>' % t
365 yield i, t
365 yield i, t
366
366
367 def _wrap_tablelinenos(self, inner):
367 def _wrap_tablelinenos(self, inner):
368 yield 0, '<table class="code-highlight %stable">' % self.cssclass
368 yield 0, '<table class="code-highlight %stable">' % self.cssclass
369
369
370 last_shown_line_number = 0
370 last_shown_line_number = 0
371 current_line_number = 1
371 current_line_number = 1
372
372
373 for t, line in inner:
373 for t, line in inner:
374 if not t:
374 if not t:
375 yield t, line
375 yield t, line
376 continue
376 continue
377
377
378 if current_line_number in self.only_lines:
378 if current_line_number in self.only_lines:
379 if last_shown_line_number + 1 != current_line_number:
379 if last_shown_line_number + 1 != current_line_number:
380 yield 0, '<tr>'
380 yield 0, '<tr>'
381 yield 0, '<td class="line">...</td>'
381 yield 0, '<td class="line">...</td>'
382 yield 0, '<td id="hlcode" class="code"></td>'
382 yield 0, '<td id="hlcode" class="code"></td>'
383 yield 0, '</tr>'
383 yield 0, '</tr>'
384
384
385 yield 0, '<tr>'
385 yield 0, '<tr>'
386 if self.url:
386 if self.url:
387 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
387 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
388 self.url, current_line_number, current_line_number)
388 self.url, current_line_number, current_line_number)
389 else:
389 else:
390 yield 0, '<td class="line"><a href="">%i</a></td>' % (
390 yield 0, '<td class="line"><a href="">%i</a></td>' % (
391 current_line_number)
391 current_line_number)
392 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
392 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
393 yield 0, '</tr>'
393 yield 0, '</tr>'
394
394
395 last_shown_line_number = current_line_number
395 last_shown_line_number = current_line_number
396
396
397 current_line_number += 1
397 current_line_number += 1
398
398
399
399
400 yield 0, '</table>'
400 yield 0, '</table>'
401
401
402
402
403 def extract_phrases(text_query):
403 def extract_phrases(text_query):
404 """
404 """
405 Extracts phrases from search term string making sure phrases
405 Extracts phrases from search term string making sure phrases
406 contained in double quotes are kept together - and discarding empty values
406 contained in double quotes are kept together - and discarding empty values
407 or fully whitespace values eg.
407 or fully whitespace values eg.
408
408
409 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
409 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
410
410
411 """
411 """
412
412
413 in_phrase = False
413 in_phrase = False
414 buf = ''
414 buf = ''
415 phrases = []
415 phrases = []
416 for char in text_query:
416 for char in text_query:
417 if in_phrase:
417 if in_phrase:
418 if char == '"': # end phrase
418 if char == '"': # end phrase
419 phrases.append(buf)
419 phrases.append(buf)
420 buf = ''
420 buf = ''
421 in_phrase = False
421 in_phrase = False
422 continue
422 continue
423 else:
423 else:
424 buf += char
424 buf += char
425 continue
425 continue
426 else:
426 else:
427 if char == '"': # start phrase
427 if char == '"': # start phrase
428 in_phrase = True
428 in_phrase = True
429 phrases.append(buf)
429 phrases.append(buf)
430 buf = ''
430 buf = ''
431 continue
431 continue
432 elif char == ' ':
432 elif char == ' ':
433 phrases.append(buf)
433 phrases.append(buf)
434 buf = ''
434 buf = ''
435 continue
435 continue
436 else:
436 else:
437 buf += char
437 buf += char
438
438
439 phrases.append(buf)
439 phrases.append(buf)
440 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
440 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
441 return phrases
441 return phrases
442
442
443
443
444 def get_matching_offsets(text, phrases):
444 def get_matching_offsets(text, phrases):
445 """
445 """
446 Returns a list of string offsets in `text` that the list of `terms` match
446 Returns a list of string offsets in `text` that the list of `terms` match
447
447
448 >>> get_matching_offsets('some text here', ['some', 'here'])
448 >>> get_matching_offsets('some text here', ['some', 'here'])
449 [(0, 4), (10, 14)]
449 [(0, 4), (10, 14)]
450
450
451 """
451 """
452 offsets = []
452 offsets = []
453 for phrase in phrases:
453 for phrase in phrases:
454 for match in re.finditer(phrase, text):
454 for match in re.finditer(phrase, text):
455 offsets.append((match.start(), match.end()))
455 offsets.append((match.start(), match.end()))
456
456
457 return offsets
457 return offsets
458
458
459
459
460 def normalize_text_for_matching(x):
460 def normalize_text_for_matching(x):
461 """
461 """
462 Replaces all non alnum characters to spaces and lower cases the string,
462 Replaces all non alnum characters to spaces and lower cases the string,
463 useful for comparing two text strings without punctuation
463 useful for comparing two text strings without punctuation
464 """
464 """
465 return re.sub(r'[^\w]', ' ', x.lower())
465 return re.sub(r'[^\w]', ' ', x.lower())
466
466
467
467
468 def get_matching_line_offsets(lines, terms):
468 def get_matching_line_offsets(lines, terms):
469 """ Return a set of `lines` indices (starting from 1) matching a
469 """ Return a set of `lines` indices (starting from 1) matching a
470 text search query, along with `context` lines above/below matching lines
470 text search query, along with `context` lines above/below matching lines
471
471
472 :param lines: list of strings representing lines
472 :param lines: list of strings representing lines
473 :param terms: search term string to match in lines eg. 'some text'
473 :param terms: search term string to match in lines eg. 'some text'
474 :param context: number of lines above/below a matching line to add to result
474 :param context: number of lines above/below a matching line to add to result
475 :param max_lines: cut off for lines of interest
475 :param max_lines: cut off for lines of interest
476 eg.
476 eg.
477
477
478 text = '''
478 text = '''
479 words words words
479 words words words
480 words words words
480 words words words
481 some text some
481 some text some
482 words words words
482 words words words
483 words words words
483 words words words
484 text here what
484 text here what
485 '''
485 '''
486 get_matching_line_offsets(text, 'text', context=1)
486 get_matching_line_offsets(text, 'text', context=1)
487 {3: [(5, 9)], 6: [(0, 4)]]
487 {3: [(5, 9)], 6: [(0, 4)]]
488
488
489 """
489 """
490 matching_lines = {}
490 matching_lines = {}
491 phrases = [normalize_text_for_matching(phrase)
491 phrases = [normalize_text_for_matching(phrase)
492 for phrase in extract_phrases(terms)]
492 for phrase in extract_phrases(terms)]
493
493
494 for line_index, line in enumerate(lines, start=1):
494 for line_index, line in enumerate(lines, start=1):
495 match_offsets = get_matching_offsets(
495 match_offsets = get_matching_offsets(
496 normalize_text_for_matching(line), phrases)
496 normalize_text_for_matching(line), phrases)
497 if match_offsets:
497 if match_offsets:
498 matching_lines[line_index] = match_offsets
498 matching_lines[line_index] = match_offsets
499
499
500 return matching_lines
500 return matching_lines
501
501
502
502
503 def get_lexer_safe(mimetype=None, filepath=None):
503 def get_lexer_safe(mimetype=None, filepath=None):
504 """
504 """
505 Tries to return a relevant pygments lexer using mimetype/filepath name,
505 Tries to return a relevant pygments lexer using mimetype/filepath name,
506 defaulting to plain text if none could be found
506 defaulting to plain text if none could be found
507 """
507 """
508 lexer = None
508 lexer = None
509 try:
509 try:
510 if mimetype:
510 if mimetype:
511 lexer = get_lexer_for_mimetype(mimetype)
511 lexer = get_lexer_for_mimetype(mimetype)
512 if not lexer:
512 if not lexer:
513 lexer = get_lexer_for_filename(filepath)
513 lexer = get_lexer_for_filename(filepath)
514 except pygments.util.ClassNotFound:
514 except pygments.util.ClassNotFound:
515 pass
515 pass
516
516
517 if not lexer:
517 if not lexer:
518 lexer = get_lexer_by_name('text')
518 lexer = get_lexer_by_name('text')
519
519
520 return lexer
520 return lexer
521
521
522
522
523 def get_lexer_for_filenode(filenode):
523 def get_lexer_for_filenode(filenode):
524 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
524 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
525 return lexer
525 return lexer
526
526
527
527
528 def pygmentize(filenode, **kwargs):
528 def pygmentize(filenode, **kwargs):
529 """
529 """
530 pygmentize function using pygments
530 pygmentize function using pygments
531
531
532 :param filenode:
532 :param filenode:
533 """
533 """
534 lexer = get_lexer_for_filenode(filenode)
534 lexer = get_lexer_for_filenode(filenode)
535 return literal(code_highlight(filenode.content, lexer,
535 return literal(code_highlight(filenode.content, lexer,
536 CodeHtmlFormatter(**kwargs)))
536 CodeHtmlFormatter(**kwargs)))
537
537
538
538
539 def pygmentize_annotation(repo_name, filenode, **kwargs):
539 def pygmentize_annotation(repo_name, filenode, **kwargs):
540 """
540 """
541 pygmentize function for annotation
541 pygmentize function for annotation
542
542
543 :param filenode:
543 :param filenode:
544 """
544 """
545
545
546 color_dict = {}
546 color_dict = {}
547
547
548 def gen_color(n=10000):
548 def gen_color(n=10000):
549 """generator for getting n of evenly distributed colors using
549 """generator for getting n of evenly distributed colors using
550 hsv color and golden ratio. It always return same order of colors
550 hsv color and golden ratio. It always return same order of colors
551
551
552 :returns: RGB tuple
552 :returns: RGB tuple
553 """
553 """
554
554
555 def hsv_to_rgb(h, s, v):
555 def hsv_to_rgb(h, s, v):
556 if s == 0.0:
556 if s == 0.0:
557 return v, v, v
557 return v, v, v
558 i = int(h * 6.0) # XXX assume int() truncates!
558 i = int(h * 6.0) # XXX assume int() truncates!
559 f = (h * 6.0) - i
559 f = (h * 6.0) - i
560 p = v * (1.0 - s)
560 p = v * (1.0 - s)
561 q = v * (1.0 - s * f)
561 q = v * (1.0 - s * f)
562 t = v * (1.0 - s * (1.0 - f))
562 t = v * (1.0 - s * (1.0 - f))
563 i = i % 6
563 i = i % 6
564 if i == 0:
564 if i == 0:
565 return v, t, p
565 return v, t, p
566 if i == 1:
566 if i == 1:
567 return q, v, p
567 return q, v, p
568 if i == 2:
568 if i == 2:
569 return p, v, t
569 return p, v, t
570 if i == 3:
570 if i == 3:
571 return p, q, v
571 return p, q, v
572 if i == 4:
572 if i == 4:
573 return t, p, v
573 return t, p, v
574 if i == 5:
574 if i == 5:
575 return v, p, q
575 return v, p, q
576
576
577 golden_ratio = 0.618033988749895
577 golden_ratio = 0.618033988749895
578 h = 0.22717784590367374
578 h = 0.22717784590367374
579
579
580 for _ in xrange(n):
580 for _ in xrange(n):
581 h += golden_ratio
581 h += golden_ratio
582 h %= 1
582 h %= 1
583 HSV_tuple = [h, 0.95, 0.95]
583 HSV_tuple = [h, 0.95, 0.95]
584 RGB_tuple = hsv_to_rgb(*HSV_tuple)
584 RGB_tuple = hsv_to_rgb(*HSV_tuple)
585 yield map(lambda x: str(int(x * 256)), RGB_tuple)
585 yield map(lambda x: str(int(x * 256)), RGB_tuple)
586
586
587 cgenerator = gen_color()
587 cgenerator = gen_color()
588
588
589 def get_color_string(commit_id):
589 def get_color_string(commit_id):
590 if commit_id in color_dict:
590 if commit_id in color_dict:
591 col = color_dict[commit_id]
591 col = color_dict[commit_id]
592 else:
592 else:
593 col = color_dict[commit_id] = cgenerator.next()
593 col = color_dict[commit_id] = cgenerator.next()
594 return "color: rgb(%s)! important;" % (', '.join(col))
594 return "color: rgb(%s)! important;" % (', '.join(col))
595
595
596 def url_func(repo_name):
596 def url_func(repo_name):
597
597
598 def _url_func(commit):
598 def _url_func(commit):
599 author = commit.author
599 author = commit.author
600 date = commit.date
600 date = commit.date
601 message = tooltip(commit.message)
601 message = tooltip(commit.message)
602
602
603 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
603 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
604 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
604 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
605 "</b> %s<br/></div>")
605 "</b> %s<br/></div>")
606
606
607 tooltip_html = tooltip_html % (author, date, message)
607 tooltip_html = tooltip_html % (author, date, message)
608 lnk_format = '%5s:%s' % ('r%s' % commit.idx, commit.short_id)
608 lnk_format = '%5s:%s' % ('r%s' % commit.idx, commit.short_id)
609 uri = link_to(
609 uri = link_to(
610 lnk_format,
610 lnk_format,
611 url('changeset_home', repo_name=repo_name,
611 url('changeset_home', repo_name=repo_name,
612 revision=commit.raw_id),
612 revision=commit.raw_id),
613 style=get_color_string(commit.raw_id),
613 style=get_color_string(commit.raw_id),
614 class_='tooltip',
614 class_='tooltip',
615 title=tooltip_html
615 title=tooltip_html
616 )
616 )
617
617
618 uri += '\n'
618 uri += '\n'
619 return uri
619 return uri
620 return _url_func
620 return _url_func
621
621
622 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
622 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
623
623
624
624
625 def is_following_repo(repo_name, user_id):
625 def is_following_repo(repo_name, user_id):
626 from rhodecode.model.scm import ScmModel
626 from rhodecode.model.scm import ScmModel
627 return ScmModel().is_following_repo(repo_name, user_id)
627 return ScmModel().is_following_repo(repo_name, user_id)
628
628
629
629
630 class _Message(object):
630 class _Message(object):
631 """A message returned by ``Flash.pop_messages()``.
631 """A message returned by ``Flash.pop_messages()``.
632
632
633 Converting the message to a string returns the message text. Instances
633 Converting the message to a string returns the message text. Instances
634 also have the following attributes:
634 also have the following attributes:
635
635
636 * ``message``: the message text.
636 * ``message``: the message text.
637 * ``category``: the category specified when the message was created.
637 * ``category``: the category specified when the message was created.
638 """
638 """
639
639
640 def __init__(self, category, message):
640 def __init__(self, category, message):
641 self.category = category
641 self.category = category
642 self.message = message
642 self.message = message
643
643
644 def __str__(self):
644 def __str__(self):
645 return self.message
645 return self.message
646
646
647 __unicode__ = __str__
647 __unicode__ = __str__
648
648
649 def __html__(self):
649 def __html__(self):
650 return escape(safe_unicode(self.message))
650 return escape(safe_unicode(self.message))
651
651
652
652
653 class Flash(_Flash):
653 class Flash(_Flash):
654
654
655 def pop_messages(self):
655 def pop_messages(self):
656 """Return all accumulated messages and delete them from the session.
656 """Return all accumulated messages and delete them from the session.
657
657
658 The return value is a list of ``Message`` objects.
658 The return value is a list of ``Message`` objects.
659 """
659 """
660 from pylons import session
660 from pylons import session
661
661
662 messages = []
662 messages = []
663
663
664 # Pop the 'old' pylons flash messages. They are tuples of the form
664 # Pop the 'old' pylons flash messages. They are tuples of the form
665 # (category, message)
665 # (category, message)
666 for cat, msg in session.pop(self.session_key, []):
666 for cat, msg in session.pop(self.session_key, []):
667 messages.append(_Message(cat, msg))
667 messages.append(_Message(cat, msg))
668
668
669 # Pop the 'new' pyramid flash messages for each category as list
669 # Pop the 'new' pyramid flash messages for each category as list
670 # of strings.
670 # of strings.
671 for cat in self.categories:
671 for cat in self.categories:
672 for msg in session.pop_flash(queue=cat):
672 for msg in session.pop_flash(queue=cat):
673 messages.append(_Message(cat, msg))
673 messages.append(_Message(cat, msg))
674 # Map messages from the default queue to the 'notice' category.
674 # Map messages from the default queue to the 'notice' category.
675 for msg in session.pop_flash():
675 for msg in session.pop_flash():
676 messages.append(_Message('notice', msg))
676 messages.append(_Message('notice', msg))
677
677
678 session.save()
678 session.save()
679 return messages
679 return messages
680
680
681 def json_alerts(self):
681 def json_alerts(self):
682 payloads = []
682 payloads = []
683 messages = flash.pop_messages()
683 messages = flash.pop_messages()
684 if messages:
684 if messages:
685 for message in messages:
685 for message in messages:
686 subdata = {}
687 if hasattr(message.message, 'rsplit'):
688 flash_data = message.message.rsplit('|DELIM|', 1)
689 org_message = flash_data[0]
690 if len(flash_data) > 1:
691 subdata = json.loads(flash_data[1])
692 else:
693 org_message = message.message
686 payloads.append({
694 payloads.append({
687 'message': {
695 'message': {
688 'message': u'{}'.format(message.message),
696 'message': u'{}'.format(org_message),
689 'level': message.category,
697 'level': message.category,
690 'force': 'true'
698 'force': True,
699 'subdata': subdata
691 }
700 }
692 })
701 })
693 return json.dumps(payloads)
702 return json.dumps(payloads)
694
703
695 flash = Flash()
704 flash = Flash()
696
705
697 #==============================================================================
706 #==============================================================================
698 # SCM FILTERS available via h.
707 # SCM FILTERS available via h.
699 #==============================================================================
708 #==============================================================================
700 from rhodecode.lib.vcs.utils import author_name, author_email
709 from rhodecode.lib.vcs.utils import author_name, author_email
701 from rhodecode.lib.utils2 import credentials_filter, age as _age
710 from rhodecode.lib.utils2 import credentials_filter, age as _age
702 from rhodecode.model.db import User, ChangesetStatus
711 from rhodecode.model.db import User, ChangesetStatus
703
712
704 age = _age
713 age = _age
705 capitalize = lambda x: x.capitalize()
714 capitalize = lambda x: x.capitalize()
706 email = author_email
715 email = author_email
707 short_id = lambda x: x[:12]
716 short_id = lambda x: x[:12]
708 hide_credentials = lambda x: ''.join(credentials_filter(x))
717 hide_credentials = lambda x: ''.join(credentials_filter(x))
709
718
710
719
711 def age_component(datetime_iso, value=None, time_is_local=False):
720 def age_component(datetime_iso, value=None, time_is_local=False):
712 title = value or format_date(datetime_iso)
721 title = value or format_date(datetime_iso)
713
722
714 # detect if we have a timezone info, otherwise, add it
723 # detect if we have a timezone info, otherwise, add it
715 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
724 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
716 tzinfo = '+00:00'
725 tzinfo = '+00:00'
717
726
718 if time_is_local:
727 if time_is_local:
719 tzinfo = time.strftime("+%H:%M",
728 tzinfo = time.strftime("+%H:%M",
720 time.gmtime(
729 time.gmtime(
721 (datetime.now() - datetime.utcnow()).seconds + 1
730 (datetime.now() - datetime.utcnow()).seconds + 1
722 )
731 )
723 )
732 )
724
733
725 return literal(
734 return literal(
726 '<time class="timeago tooltip" '
735 '<time class="timeago tooltip" '
727 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
736 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
728 datetime_iso, title, tzinfo))
737 datetime_iso, title, tzinfo))
729
738
730
739
731 def _shorten_commit_id(commit_id):
740 def _shorten_commit_id(commit_id):
732 from rhodecode import CONFIG
741 from rhodecode import CONFIG
733 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
742 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
734 return commit_id[:def_len]
743 return commit_id[:def_len]
735
744
736
745
737 def show_id(commit):
746 def show_id(commit):
738 """
747 """
739 Configurable function that shows ID
748 Configurable function that shows ID
740 by default it's r123:fffeeefffeee
749 by default it's r123:fffeeefffeee
741
750
742 :param commit: commit instance
751 :param commit: commit instance
743 """
752 """
744 from rhodecode import CONFIG
753 from rhodecode import CONFIG
745 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
754 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
746
755
747 raw_id = _shorten_commit_id(commit.raw_id)
756 raw_id = _shorten_commit_id(commit.raw_id)
748 if show_idx:
757 if show_idx:
749 return 'r%s:%s' % (commit.idx, raw_id)
758 return 'r%s:%s' % (commit.idx, raw_id)
750 else:
759 else:
751 return '%s' % (raw_id, )
760 return '%s' % (raw_id, )
752
761
753
762
754 def format_date(date):
763 def format_date(date):
755 """
764 """
756 use a standardized formatting for dates used in RhodeCode
765 use a standardized formatting for dates used in RhodeCode
757
766
758 :param date: date/datetime object
767 :param date: date/datetime object
759 :return: formatted date
768 :return: formatted date
760 """
769 """
761
770
762 if date:
771 if date:
763 _fmt = "%a, %d %b %Y %H:%M:%S"
772 _fmt = "%a, %d %b %Y %H:%M:%S"
764 return safe_unicode(date.strftime(_fmt))
773 return safe_unicode(date.strftime(_fmt))
765
774
766 return u""
775 return u""
767
776
768
777
769 class _RepoChecker(object):
778 class _RepoChecker(object):
770
779
771 def __init__(self, backend_alias):
780 def __init__(self, backend_alias):
772 self._backend_alias = backend_alias
781 self._backend_alias = backend_alias
773
782
774 def __call__(self, repository):
783 def __call__(self, repository):
775 if hasattr(repository, 'alias'):
784 if hasattr(repository, 'alias'):
776 _type = repository.alias
785 _type = repository.alias
777 elif hasattr(repository, 'repo_type'):
786 elif hasattr(repository, 'repo_type'):
778 _type = repository.repo_type
787 _type = repository.repo_type
779 else:
788 else:
780 _type = repository
789 _type = repository
781 return _type == self._backend_alias
790 return _type == self._backend_alias
782
791
783 is_git = _RepoChecker('git')
792 is_git = _RepoChecker('git')
784 is_hg = _RepoChecker('hg')
793 is_hg = _RepoChecker('hg')
785 is_svn = _RepoChecker('svn')
794 is_svn = _RepoChecker('svn')
786
795
787
796
788 def get_repo_type_by_name(repo_name):
797 def get_repo_type_by_name(repo_name):
789 repo = Repository.get_by_repo_name(repo_name)
798 repo = Repository.get_by_repo_name(repo_name)
790 return repo.repo_type
799 return repo.repo_type
791
800
792
801
793 def is_svn_without_proxy(repository):
802 def is_svn_without_proxy(repository):
794 if is_svn(repository):
803 if is_svn(repository):
795 from rhodecode.model.settings import VcsSettingsModel
804 from rhodecode.model.settings import VcsSettingsModel
796 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
805 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
797 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
806 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
798 return False
807 return False
799
808
800
809
801 def discover_user(author):
810 def discover_user(author):
802 """
811 """
803 Tries to discover RhodeCode User based on the autho string. Author string
812 Tries to discover RhodeCode User based on the autho string. Author string
804 is typically `FirstName LastName <email@address.com>`
813 is typically `FirstName LastName <email@address.com>`
805 """
814 """
806
815
807 # if author is already an instance use it for extraction
816 # if author is already an instance use it for extraction
808 if isinstance(author, User):
817 if isinstance(author, User):
809 return author
818 return author
810
819
811 # Valid email in the attribute passed, see if they're in the system
820 # Valid email in the attribute passed, see if they're in the system
812 _email = author_email(author)
821 _email = author_email(author)
813 if _email != '':
822 if _email != '':
814 user = User.get_by_email(_email, case_insensitive=True, cache=True)
823 user = User.get_by_email(_email, case_insensitive=True, cache=True)
815 if user is not None:
824 if user is not None:
816 return user
825 return user
817
826
818 # Maybe it's a username, we try to extract it and fetch by username ?
827 # Maybe it's a username, we try to extract it and fetch by username ?
819 _author = author_name(author)
828 _author = author_name(author)
820 user = User.get_by_username(_author, case_insensitive=True, cache=True)
829 user = User.get_by_username(_author, case_insensitive=True, cache=True)
821 if user is not None:
830 if user is not None:
822 return user
831 return user
823
832
824 return None
833 return None
825
834
826
835
827 def email_or_none(author):
836 def email_or_none(author):
828 # extract email from the commit string
837 # extract email from the commit string
829 _email = author_email(author)
838 _email = author_email(author)
830
839
831 # If we have an email, use it, otherwise
840 # If we have an email, use it, otherwise
832 # see if it contains a username we can get an email from
841 # see if it contains a username we can get an email from
833 if _email != '':
842 if _email != '':
834 return _email
843 return _email
835 else:
844 else:
836 user = User.get_by_username(
845 user = User.get_by_username(
837 author_name(author), case_insensitive=True, cache=True)
846 author_name(author), case_insensitive=True, cache=True)
838
847
839 if user is not None:
848 if user is not None:
840 return user.email
849 return user.email
841
850
842 # No valid email, not a valid user in the system, none!
851 # No valid email, not a valid user in the system, none!
843 return None
852 return None
844
853
845
854
846 def link_to_user(author, length=0, **kwargs):
855 def link_to_user(author, length=0, **kwargs):
847 user = discover_user(author)
856 user = discover_user(author)
848 # user can be None, but if we have it already it means we can re-use it
857 # user can be None, but if we have it already it means we can re-use it
849 # in the person() function, so we save 1 intensive-query
858 # in the person() function, so we save 1 intensive-query
850 if user:
859 if user:
851 author = user
860 author = user
852
861
853 display_person = person(author, 'username_or_name_or_email')
862 display_person = person(author, 'username_or_name_or_email')
854 if length:
863 if length:
855 display_person = shorter(display_person, length)
864 display_person = shorter(display_person, length)
856
865
857 if user:
866 if user:
858 return link_to(
867 return link_to(
859 escape(display_person),
868 escape(display_person),
860 url('user_profile', username=user.username),
869 url('user_profile', username=user.username),
861 **kwargs)
870 **kwargs)
862 else:
871 else:
863 return escape(display_person)
872 return escape(display_person)
864
873
865
874
866 def person(author, show_attr="username_and_name"):
875 def person(author, show_attr="username_and_name"):
867 user = discover_user(author)
876 user = discover_user(author)
868 if user:
877 if user:
869 return getattr(user, show_attr)
878 return getattr(user, show_attr)
870 else:
879 else:
871 _author = author_name(author)
880 _author = author_name(author)
872 _email = email(author)
881 _email = email(author)
873 return _author or _email
882 return _author or _email
874
883
875
884
876 def author_string(email):
885 def author_string(email):
877 if email:
886 if email:
878 user = User.get_by_email(email, case_insensitive=True, cache=True)
887 user = User.get_by_email(email, case_insensitive=True, cache=True)
879 if user:
888 if user:
880 if user.firstname or user.lastname:
889 if user.firstname or user.lastname:
881 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
890 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
882 else:
891 else:
883 return email
892 return email
884 else:
893 else:
885 return email
894 return email
886 else:
895 else:
887 return None
896 return None
888
897
889
898
890 def person_by_id(id_, show_attr="username_and_name"):
899 def person_by_id(id_, show_attr="username_and_name"):
891 # attr to return from fetched user
900 # attr to return from fetched user
892 person_getter = lambda usr: getattr(usr, show_attr)
901 person_getter = lambda usr: getattr(usr, show_attr)
893
902
894 #maybe it's an ID ?
903 #maybe it's an ID ?
895 if str(id_).isdigit() or isinstance(id_, int):
904 if str(id_).isdigit() or isinstance(id_, int):
896 id_ = int(id_)
905 id_ = int(id_)
897 user = User.get(id_)
906 user = User.get(id_)
898 if user is not None:
907 if user is not None:
899 return person_getter(user)
908 return person_getter(user)
900 return id_
909 return id_
901
910
902
911
903 def gravatar_with_user(author, show_disabled=False):
912 def gravatar_with_user(author, show_disabled=False):
904 from rhodecode.lib.utils import PartialRenderer
913 from rhodecode.lib.utils import PartialRenderer
905 _render = PartialRenderer('base/base.html')
914 _render = PartialRenderer('base/base.html')
906 return _render('gravatar_with_user', author, show_disabled=show_disabled)
915 return _render('gravatar_with_user', author, show_disabled=show_disabled)
907
916
908
917
909 def desc_stylize(value):
918 def desc_stylize(value):
910 """
919 """
911 converts tags from value into html equivalent
920 converts tags from value into html equivalent
912
921
913 :param value:
922 :param value:
914 """
923 """
915 if not value:
924 if not value:
916 return ''
925 return ''
917
926
918 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
927 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
919 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
928 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
920 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
929 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
921 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
930 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
922 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
931 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
923 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
932 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
924 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
933 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
925 '<div class="metatag" tag="lang">\\2</div>', value)
934 '<div class="metatag" tag="lang">\\2</div>', value)
926 value = re.sub(r'\[([a-z]+)\]',
935 value = re.sub(r'\[([a-z]+)\]',
927 '<div class="metatag" tag="\\1">\\1</div>', value)
936 '<div class="metatag" tag="\\1">\\1</div>', value)
928
937
929 return value
938 return value
930
939
931
940
932 def escaped_stylize(value):
941 def escaped_stylize(value):
933 """
942 """
934 converts tags from value into html equivalent, but escaping its value first
943 converts tags from value into html equivalent, but escaping its value first
935 """
944 """
936 if not value:
945 if not value:
937 return ''
946 return ''
938
947
939 # Using default webhelper escape method, but has to force it as a
948 # Using default webhelper escape method, but has to force it as a
940 # plain unicode instead of a markup tag to be used in regex expressions
949 # plain unicode instead of a markup tag to be used in regex expressions
941 value = unicode(escape(safe_unicode(value)))
950 value = unicode(escape(safe_unicode(value)))
942
951
943 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
952 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
944 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
953 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
945 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
954 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
946 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
955 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
947 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
956 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
948 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
957 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
949 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
958 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
950 '<div class="metatag" tag="lang">\\2</div>', value)
959 '<div class="metatag" tag="lang">\\2</div>', value)
951 value = re.sub(r'\[([a-z]+)\]',
960 value = re.sub(r'\[([a-z]+)\]',
952 '<div class="metatag" tag="\\1">\\1</div>', value)
961 '<div class="metatag" tag="\\1">\\1</div>', value)
953
962
954 return value
963 return value
955
964
956
965
957 def bool2icon(value):
966 def bool2icon(value):
958 """
967 """
959 Returns boolean value of a given value, represented as html element with
968 Returns boolean value of a given value, represented as html element with
960 classes that will represent icons
969 classes that will represent icons
961
970
962 :param value: given value to convert to html node
971 :param value: given value to convert to html node
963 """
972 """
964
973
965 if value: # does bool conversion
974 if value: # does bool conversion
966 return HTML.tag('i', class_="icon-true")
975 return HTML.tag('i', class_="icon-true")
967 else: # not true as bool
976 else: # not true as bool
968 return HTML.tag('i', class_="icon-false")
977 return HTML.tag('i', class_="icon-false")
969
978
970
979
971 #==============================================================================
980 #==============================================================================
972 # PERMS
981 # PERMS
973 #==============================================================================
982 #==============================================================================
974 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
983 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
975 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
984 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
976 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
985 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
977 csrf_token_key
986 csrf_token_key
978
987
979
988
980 #==============================================================================
989 #==============================================================================
981 # GRAVATAR URL
990 # GRAVATAR URL
982 #==============================================================================
991 #==============================================================================
983 class InitialsGravatar(object):
992 class InitialsGravatar(object):
984 def __init__(self, email_address, first_name, last_name, size=30,
993 def __init__(self, email_address, first_name, last_name, size=30,
985 background=None, text_color='#fff'):
994 background=None, text_color='#fff'):
986 self.size = size
995 self.size = size
987 self.first_name = first_name
996 self.first_name = first_name
988 self.last_name = last_name
997 self.last_name = last_name
989 self.email_address = email_address
998 self.email_address = email_address
990 self.background = background or self.str2color(email_address)
999 self.background = background or self.str2color(email_address)
991 self.text_color = text_color
1000 self.text_color = text_color
992
1001
993 def get_color_bank(self):
1002 def get_color_bank(self):
994 """
1003 """
995 returns a predefined list of colors that gravatars can use.
1004 returns a predefined list of colors that gravatars can use.
996 Those are randomized distinct colors that guarantee readability and
1005 Those are randomized distinct colors that guarantee readability and
997 uniqueness.
1006 uniqueness.
998
1007
999 generated with: http://phrogz.net/css/distinct-colors.html
1008 generated with: http://phrogz.net/css/distinct-colors.html
1000 """
1009 """
1001 return [
1010 return [
1002 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1011 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1003 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1012 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1004 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1013 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1005 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1014 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1006 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1015 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1007 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1016 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1008 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1017 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1009 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1018 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1010 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1019 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1011 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1020 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1012 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1021 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1013 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1022 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1014 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1023 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1015 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1024 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1016 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1025 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1017 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1026 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1018 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1027 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1019 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1028 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1020 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1029 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1021 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1030 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1022 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1031 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1023 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1032 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1024 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1033 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1025 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1034 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1026 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1035 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1027 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1036 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1028 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1037 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1029 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1038 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1030 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1039 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1031 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1040 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1032 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1041 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1033 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1042 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1034 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1043 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1035 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1044 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1036 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1045 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1037 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1046 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1038 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1047 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1039 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1048 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1040 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1049 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1041 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1050 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1042 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1051 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1043 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1052 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1044 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1053 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1045 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1054 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1046 '#4f8c46', '#368dd9', '#5c0073'
1055 '#4f8c46', '#368dd9', '#5c0073'
1047 ]
1056 ]
1048
1057
1049 def rgb_to_hex_color(self, rgb_tuple):
1058 def rgb_to_hex_color(self, rgb_tuple):
1050 """
1059 """
1051 Converts an rgb_tuple passed to an hex color.
1060 Converts an rgb_tuple passed to an hex color.
1052
1061
1053 :param rgb_tuple: tuple with 3 ints represents rgb color space
1062 :param rgb_tuple: tuple with 3 ints represents rgb color space
1054 """
1063 """
1055 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1064 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1056
1065
1057 def email_to_int_list(self, email_str):
1066 def email_to_int_list(self, email_str):
1058 """
1067 """
1059 Get every byte of the hex digest value of email and turn it to integer.
1068 Get every byte of the hex digest value of email and turn it to integer.
1060 It's going to be always between 0-255
1069 It's going to be always between 0-255
1061 """
1070 """
1062 digest = md5_safe(email_str.lower())
1071 digest = md5_safe(email_str.lower())
1063 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1072 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1064
1073
1065 def pick_color_bank_index(self, email_str, color_bank):
1074 def pick_color_bank_index(self, email_str, color_bank):
1066 return self.email_to_int_list(email_str)[0] % len(color_bank)
1075 return self.email_to_int_list(email_str)[0] % len(color_bank)
1067
1076
1068 def str2color(self, email_str):
1077 def str2color(self, email_str):
1069 """
1078 """
1070 Tries to map in a stable algorithm an email to color
1079 Tries to map in a stable algorithm an email to color
1071
1080
1072 :param email_str:
1081 :param email_str:
1073 """
1082 """
1074 color_bank = self.get_color_bank()
1083 color_bank = self.get_color_bank()
1075 # pick position (module it's length so we always find it in the
1084 # pick position (module it's length so we always find it in the
1076 # bank even if it's smaller than 256 values
1085 # bank even if it's smaller than 256 values
1077 pos = self.pick_color_bank_index(email_str, color_bank)
1086 pos = self.pick_color_bank_index(email_str, color_bank)
1078 return color_bank[pos]
1087 return color_bank[pos]
1079
1088
1080 def normalize_email(self, email_address):
1089 def normalize_email(self, email_address):
1081 import unicodedata
1090 import unicodedata
1082 # default host used to fill in the fake/missing email
1091 # default host used to fill in the fake/missing email
1083 default_host = u'localhost'
1092 default_host = u'localhost'
1084
1093
1085 if not email_address:
1094 if not email_address:
1086 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1095 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1087
1096
1088 email_address = safe_unicode(email_address)
1097 email_address = safe_unicode(email_address)
1089
1098
1090 if u'@' not in email_address:
1099 if u'@' not in email_address:
1091 email_address = u'%s@%s' % (email_address, default_host)
1100 email_address = u'%s@%s' % (email_address, default_host)
1092
1101
1093 if email_address.endswith(u'@'):
1102 if email_address.endswith(u'@'):
1094 email_address = u'%s%s' % (email_address, default_host)
1103 email_address = u'%s%s' % (email_address, default_host)
1095
1104
1096 email_address = unicodedata.normalize('NFKD', email_address)\
1105 email_address = unicodedata.normalize('NFKD', email_address)\
1097 .encode('ascii', 'ignore')
1106 .encode('ascii', 'ignore')
1098 return email_address
1107 return email_address
1099
1108
1100 def get_initials(self):
1109 def get_initials(self):
1101 """
1110 """
1102 Returns 2 letter initials calculated based on the input.
1111 Returns 2 letter initials calculated based on the input.
1103 The algorithm picks first given email address, and takes first letter
1112 The algorithm picks first given email address, and takes first letter
1104 of part before @, and then the first letter of server name. In case
1113 of part before @, and then the first letter of server name. In case
1105 the part before @ is in a format of `somestring.somestring2` it replaces
1114 the part before @ is in a format of `somestring.somestring2` it replaces
1106 the server letter with first letter of somestring2
1115 the server letter with first letter of somestring2
1107
1116
1108 In case function was initialized with both first and lastname, this
1117 In case function was initialized with both first and lastname, this
1109 overrides the extraction from email by first letter of the first and
1118 overrides the extraction from email by first letter of the first and
1110 last name. We add special logic to that functionality, In case Full name
1119 last name. We add special logic to that functionality, In case Full name
1111 is compound, like Guido Von Rossum, we use last part of the last name
1120 is compound, like Guido Von Rossum, we use last part of the last name
1112 (Von Rossum) picking `R`.
1121 (Von Rossum) picking `R`.
1113
1122
1114 Function also normalizes the non-ascii characters to they ascii
1123 Function also normalizes the non-ascii characters to they ascii
1115 representation, eg Δ„ => A
1124 representation, eg Δ„ => A
1116 """
1125 """
1117 import unicodedata
1126 import unicodedata
1118 # replace non-ascii to ascii
1127 # replace non-ascii to ascii
1119 first_name = unicodedata.normalize(
1128 first_name = unicodedata.normalize(
1120 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1129 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1121 last_name = unicodedata.normalize(
1130 last_name = unicodedata.normalize(
1122 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1131 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1123
1132
1124 # do NFKD encoding, and also make sure email has proper format
1133 # do NFKD encoding, and also make sure email has proper format
1125 email_address = self.normalize_email(self.email_address)
1134 email_address = self.normalize_email(self.email_address)
1126
1135
1127 # first push the email initials
1136 # first push the email initials
1128 prefix, server = email_address.split('@', 1)
1137 prefix, server = email_address.split('@', 1)
1129
1138
1130 # check if prefix is maybe a 'firstname.lastname' syntax
1139 # check if prefix is maybe a 'firstname.lastname' syntax
1131 _dot_split = prefix.rsplit('.', 1)
1140 _dot_split = prefix.rsplit('.', 1)
1132 if len(_dot_split) == 2:
1141 if len(_dot_split) == 2:
1133 initials = [_dot_split[0][0], _dot_split[1][0]]
1142 initials = [_dot_split[0][0], _dot_split[1][0]]
1134 else:
1143 else:
1135 initials = [prefix[0], server[0]]
1144 initials = [prefix[0], server[0]]
1136
1145
1137 # then try to replace either firtname or lastname
1146 # then try to replace either firtname or lastname
1138 fn_letter = (first_name or " ")[0].strip()
1147 fn_letter = (first_name or " ")[0].strip()
1139 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1148 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1140
1149
1141 if fn_letter:
1150 if fn_letter:
1142 initials[0] = fn_letter
1151 initials[0] = fn_letter
1143
1152
1144 if ln_letter:
1153 if ln_letter:
1145 initials[1] = ln_letter
1154 initials[1] = ln_letter
1146
1155
1147 return ''.join(initials).upper()
1156 return ''.join(initials).upper()
1148
1157
1149 def get_img_data_by_type(self, font_family, img_type):
1158 def get_img_data_by_type(self, font_family, img_type):
1150 default_user = """
1159 default_user = """
1151 <svg xmlns="http://www.w3.org/2000/svg"
1160 <svg xmlns="http://www.w3.org/2000/svg"
1152 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1161 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1153 viewBox="-15 -10 439.165 429.164"
1162 viewBox="-15 -10 439.165 429.164"
1154
1163
1155 xml:space="preserve"
1164 xml:space="preserve"
1156 style="background:{background};" >
1165 style="background:{background};" >
1157
1166
1158 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1167 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1159 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1168 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1160 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1169 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1161 168.596,153.916,216.671,
1170 168.596,153.916,216.671,
1162 204.583,216.671z" fill="{text_color}"/>
1171 204.583,216.671z" fill="{text_color}"/>
1163 <path d="M407.164,374.717L360.88,
1172 <path d="M407.164,374.717L360.88,
1164 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1173 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1165 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1174 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1166 15.366-44.203,23.488-69.076,23.488c-24.877,
1175 15.366-44.203,23.488-69.076,23.488c-24.877,
1167 0-48.762-8.122-69.078-23.488
1176 0-48.762-8.122-69.078-23.488
1168 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1177 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1169 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1178 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1170 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1179 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1171 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1180 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1172 19.402-10.527 C409.699,390.129,
1181 19.402-10.527 C409.699,390.129,
1173 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1182 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1174 </svg>""".format(
1183 </svg>""".format(
1175 size=self.size,
1184 size=self.size,
1176 background='#979797', # @grey4
1185 background='#979797', # @grey4
1177 text_color=self.text_color,
1186 text_color=self.text_color,
1178 font_family=font_family)
1187 font_family=font_family)
1179
1188
1180 return {
1189 return {
1181 "default_user": default_user
1190 "default_user": default_user
1182 }[img_type]
1191 }[img_type]
1183
1192
1184 def get_img_data(self, svg_type=None):
1193 def get_img_data(self, svg_type=None):
1185 """
1194 """
1186 generates the svg metadata for image
1195 generates the svg metadata for image
1187 """
1196 """
1188
1197
1189 font_family = ','.join([
1198 font_family = ','.join([
1190 'proximanovaregular',
1199 'proximanovaregular',
1191 'Proxima Nova Regular',
1200 'Proxima Nova Regular',
1192 'Proxima Nova',
1201 'Proxima Nova',
1193 'Arial',
1202 'Arial',
1194 'Lucida Grande',
1203 'Lucida Grande',
1195 'sans-serif'
1204 'sans-serif'
1196 ])
1205 ])
1197 if svg_type:
1206 if svg_type:
1198 return self.get_img_data_by_type(font_family, svg_type)
1207 return self.get_img_data_by_type(font_family, svg_type)
1199
1208
1200 initials = self.get_initials()
1209 initials = self.get_initials()
1201 img_data = """
1210 img_data = """
1202 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1211 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1203 width="{size}" height="{size}"
1212 width="{size}" height="{size}"
1204 style="width: 100%; height: 100%; background-color: {background}"
1213 style="width: 100%; height: 100%; background-color: {background}"
1205 viewBox="0 0 {size} {size}">
1214 viewBox="0 0 {size} {size}">
1206 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1215 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1207 pointer-events="auto" fill="{text_color}"
1216 pointer-events="auto" fill="{text_color}"
1208 font-family="{font_family}"
1217 font-family="{font_family}"
1209 style="font-weight: 400; font-size: {f_size}px;">{text}
1218 style="font-weight: 400; font-size: {f_size}px;">{text}
1210 </text>
1219 </text>
1211 </svg>""".format(
1220 </svg>""".format(
1212 size=self.size,
1221 size=self.size,
1213 f_size=self.size/1.85, # scale the text inside the box nicely
1222 f_size=self.size/1.85, # scale the text inside the box nicely
1214 background=self.background,
1223 background=self.background,
1215 text_color=self.text_color,
1224 text_color=self.text_color,
1216 text=initials.upper(),
1225 text=initials.upper(),
1217 font_family=font_family)
1226 font_family=font_family)
1218
1227
1219 return img_data
1228 return img_data
1220
1229
1221 def generate_svg(self, svg_type=None):
1230 def generate_svg(self, svg_type=None):
1222 img_data = self.get_img_data(svg_type)
1231 img_data = self.get_img_data(svg_type)
1223 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1232 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1224
1233
1225
1234
1226 def initials_gravatar(email_address, first_name, last_name, size=30):
1235 def initials_gravatar(email_address, first_name, last_name, size=30):
1227 svg_type = None
1236 svg_type = None
1228 if email_address == User.DEFAULT_USER_EMAIL:
1237 if email_address == User.DEFAULT_USER_EMAIL:
1229 svg_type = 'default_user'
1238 svg_type = 'default_user'
1230 klass = InitialsGravatar(email_address, first_name, last_name, size)
1239 klass = InitialsGravatar(email_address, first_name, last_name, size)
1231 return klass.generate_svg(svg_type=svg_type)
1240 return klass.generate_svg(svg_type=svg_type)
1232
1241
1233
1242
1234 def gravatar_url(email_address, size=30):
1243 def gravatar_url(email_address, size=30):
1235 # doh, we need to re-import those to mock it later
1244 # doh, we need to re-import those to mock it later
1236 from pylons import tmpl_context as c
1245 from pylons import tmpl_context as c
1237
1246
1238 _use_gravatar = c.visual.use_gravatar
1247 _use_gravatar = c.visual.use_gravatar
1239 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1248 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1240
1249
1241 email_address = email_address or User.DEFAULT_USER_EMAIL
1250 email_address = email_address or User.DEFAULT_USER_EMAIL
1242 if isinstance(email_address, unicode):
1251 if isinstance(email_address, unicode):
1243 # hashlib crashes on unicode items
1252 # hashlib crashes on unicode items
1244 email_address = safe_str(email_address)
1253 email_address = safe_str(email_address)
1245
1254
1246 # empty email or default user
1255 # empty email or default user
1247 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1256 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1248 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1257 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1249
1258
1250 if _use_gravatar:
1259 if _use_gravatar:
1251 # TODO: Disuse pyramid thread locals. Think about another solution to
1260 # TODO: Disuse pyramid thread locals. Think about another solution to
1252 # get the host and schema here.
1261 # get the host and schema here.
1253 request = get_current_request()
1262 request = get_current_request()
1254 tmpl = safe_str(_gravatar_url)
1263 tmpl = safe_str(_gravatar_url)
1255 tmpl = tmpl.replace('{email}', email_address)\
1264 tmpl = tmpl.replace('{email}', email_address)\
1256 .replace('{md5email}', md5_safe(email_address.lower())) \
1265 .replace('{md5email}', md5_safe(email_address.lower())) \
1257 .replace('{netloc}', request.host)\
1266 .replace('{netloc}', request.host)\
1258 .replace('{scheme}', request.scheme)\
1267 .replace('{scheme}', request.scheme)\
1259 .replace('{size}', safe_str(size))
1268 .replace('{size}', safe_str(size))
1260 return tmpl
1269 return tmpl
1261 else:
1270 else:
1262 return initials_gravatar(email_address, '', '', size=size)
1271 return initials_gravatar(email_address, '', '', size=size)
1263
1272
1264
1273
1265 class Page(_Page):
1274 class Page(_Page):
1266 """
1275 """
1267 Custom pager to match rendering style with paginator
1276 Custom pager to match rendering style with paginator
1268 """
1277 """
1269
1278
1270 def _get_pos(self, cur_page, max_page, items):
1279 def _get_pos(self, cur_page, max_page, items):
1271 edge = (items / 2) + 1
1280 edge = (items / 2) + 1
1272 if (cur_page <= edge):
1281 if (cur_page <= edge):
1273 radius = max(items / 2, items - cur_page)
1282 radius = max(items / 2, items - cur_page)
1274 elif (max_page - cur_page) < edge:
1283 elif (max_page - cur_page) < edge:
1275 radius = (items - 1) - (max_page - cur_page)
1284 radius = (items - 1) - (max_page - cur_page)
1276 else:
1285 else:
1277 radius = items / 2
1286 radius = items / 2
1278
1287
1279 left = max(1, (cur_page - (radius)))
1288 left = max(1, (cur_page - (radius)))
1280 right = min(max_page, cur_page + (radius))
1289 right = min(max_page, cur_page + (radius))
1281 return left, cur_page, right
1290 return left, cur_page, right
1282
1291
1283 def _range(self, regexp_match):
1292 def _range(self, regexp_match):
1284 """
1293 """
1285 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1294 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1286
1295
1287 Arguments:
1296 Arguments:
1288
1297
1289 regexp_match
1298 regexp_match
1290 A "re" (regular expressions) match object containing the
1299 A "re" (regular expressions) match object containing the
1291 radius of linked pages around the current page in
1300 radius of linked pages around the current page in
1292 regexp_match.group(1) as a string
1301 regexp_match.group(1) as a string
1293
1302
1294 This function is supposed to be called as a callable in
1303 This function is supposed to be called as a callable in
1295 re.sub.
1304 re.sub.
1296
1305
1297 """
1306 """
1298 radius = int(regexp_match.group(1))
1307 radius = int(regexp_match.group(1))
1299
1308
1300 # Compute the first and last page number within the radius
1309 # Compute the first and last page number within the radius
1301 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1310 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1302 # -> leftmost_page = 5
1311 # -> leftmost_page = 5
1303 # -> rightmost_page = 9
1312 # -> rightmost_page = 9
1304 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1313 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1305 self.last_page,
1314 self.last_page,
1306 (radius * 2) + 1)
1315 (radius * 2) + 1)
1307 nav_items = []
1316 nav_items = []
1308
1317
1309 # Create a link to the first page (unless we are on the first page
1318 # Create a link to the first page (unless we are on the first page
1310 # or there would be no need to insert '..' spacers)
1319 # or there would be no need to insert '..' spacers)
1311 if self.page != self.first_page and self.first_page < leftmost_page:
1320 if self.page != self.first_page and self.first_page < leftmost_page:
1312 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1321 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1313
1322
1314 # Insert dots if there are pages between the first page
1323 # Insert dots if there are pages between the first page
1315 # and the currently displayed page range
1324 # and the currently displayed page range
1316 if leftmost_page - self.first_page > 1:
1325 if leftmost_page - self.first_page > 1:
1317 # Wrap in a SPAN tag if nolink_attr is set
1326 # Wrap in a SPAN tag if nolink_attr is set
1318 text = '..'
1327 text = '..'
1319 if self.dotdot_attr:
1328 if self.dotdot_attr:
1320 text = HTML.span(c=text, **self.dotdot_attr)
1329 text = HTML.span(c=text, **self.dotdot_attr)
1321 nav_items.append(text)
1330 nav_items.append(text)
1322
1331
1323 for thispage in xrange(leftmost_page, rightmost_page + 1):
1332 for thispage in xrange(leftmost_page, rightmost_page + 1):
1324 # Hilight the current page number and do not use a link
1333 # Hilight the current page number and do not use a link
1325 if thispage == self.page:
1334 if thispage == self.page:
1326 text = '%s' % (thispage,)
1335 text = '%s' % (thispage,)
1327 # Wrap in a SPAN tag if nolink_attr is set
1336 # Wrap in a SPAN tag if nolink_attr is set
1328 if self.curpage_attr:
1337 if self.curpage_attr:
1329 text = HTML.span(c=text, **self.curpage_attr)
1338 text = HTML.span(c=text, **self.curpage_attr)
1330 nav_items.append(text)
1339 nav_items.append(text)
1331 # Otherwise create just a link to that page
1340 # Otherwise create just a link to that page
1332 else:
1341 else:
1333 text = '%s' % (thispage,)
1342 text = '%s' % (thispage,)
1334 nav_items.append(self._pagerlink(thispage, text))
1343 nav_items.append(self._pagerlink(thispage, text))
1335
1344
1336 # Insert dots if there are pages between the displayed
1345 # Insert dots if there are pages between the displayed
1337 # page numbers and the end of the page range
1346 # page numbers and the end of the page range
1338 if self.last_page - rightmost_page > 1:
1347 if self.last_page - rightmost_page > 1:
1339 text = '..'
1348 text = '..'
1340 # Wrap in a SPAN tag if nolink_attr is set
1349 # Wrap in a SPAN tag if nolink_attr is set
1341 if self.dotdot_attr:
1350 if self.dotdot_attr:
1342 text = HTML.span(c=text, **self.dotdot_attr)
1351 text = HTML.span(c=text, **self.dotdot_attr)
1343 nav_items.append(text)
1352 nav_items.append(text)
1344
1353
1345 # Create a link to the very last page (unless we are on the last
1354 # Create a link to the very last page (unless we are on the last
1346 # page or there would be no need to insert '..' spacers)
1355 # page or there would be no need to insert '..' spacers)
1347 if self.page != self.last_page and rightmost_page < self.last_page:
1356 if self.page != self.last_page and rightmost_page < self.last_page:
1348 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1357 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1349
1358
1350 ## prerender links
1359 ## prerender links
1351 #_page_link = url.current()
1360 #_page_link = url.current()
1352 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1361 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1353 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1362 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1354 return self.separator.join(nav_items)
1363 return self.separator.join(nav_items)
1355
1364
1356 def pager(self, format='~2~', page_param='page', partial_param='partial',
1365 def pager(self, format='~2~', page_param='page', partial_param='partial',
1357 show_if_single_page=False, separator=' ', onclick=None,
1366 show_if_single_page=False, separator=' ', onclick=None,
1358 symbol_first='<<', symbol_last='>>',
1367 symbol_first='<<', symbol_last='>>',
1359 symbol_previous='<', symbol_next='>',
1368 symbol_previous='<', symbol_next='>',
1360 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1369 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1361 curpage_attr={'class': 'pager_curpage'},
1370 curpage_attr={'class': 'pager_curpage'},
1362 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1371 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1363
1372
1364 self.curpage_attr = curpage_attr
1373 self.curpage_attr = curpage_attr
1365 self.separator = separator
1374 self.separator = separator
1366 self.pager_kwargs = kwargs
1375 self.pager_kwargs = kwargs
1367 self.page_param = page_param
1376 self.page_param = page_param
1368 self.partial_param = partial_param
1377 self.partial_param = partial_param
1369 self.onclick = onclick
1378 self.onclick = onclick
1370 self.link_attr = link_attr
1379 self.link_attr = link_attr
1371 self.dotdot_attr = dotdot_attr
1380 self.dotdot_attr = dotdot_attr
1372
1381
1373 # Don't show navigator if there is no more than one page
1382 # Don't show navigator if there is no more than one page
1374 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1383 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1375 return ''
1384 return ''
1376
1385
1377 from string import Template
1386 from string import Template
1378 # Replace ~...~ in token format by range of pages
1387 # Replace ~...~ in token format by range of pages
1379 result = re.sub(r'~(\d+)~', self._range, format)
1388 result = re.sub(r'~(\d+)~', self._range, format)
1380
1389
1381 # Interpolate '%' variables
1390 # Interpolate '%' variables
1382 result = Template(result).safe_substitute({
1391 result = Template(result).safe_substitute({
1383 'first_page': self.first_page,
1392 'first_page': self.first_page,
1384 'last_page': self.last_page,
1393 'last_page': self.last_page,
1385 'page': self.page,
1394 'page': self.page,
1386 'page_count': self.page_count,
1395 'page_count': self.page_count,
1387 'items_per_page': self.items_per_page,
1396 'items_per_page': self.items_per_page,
1388 'first_item': self.first_item,
1397 'first_item': self.first_item,
1389 'last_item': self.last_item,
1398 'last_item': self.last_item,
1390 'item_count': self.item_count,
1399 'item_count': self.item_count,
1391 'link_first': self.page > self.first_page and \
1400 'link_first': self.page > self.first_page and \
1392 self._pagerlink(self.first_page, symbol_first) or '',
1401 self._pagerlink(self.first_page, symbol_first) or '',
1393 'link_last': self.page < self.last_page and \
1402 'link_last': self.page < self.last_page and \
1394 self._pagerlink(self.last_page, symbol_last) or '',
1403 self._pagerlink(self.last_page, symbol_last) or '',
1395 'link_previous': self.previous_page and \
1404 'link_previous': self.previous_page and \
1396 self._pagerlink(self.previous_page, symbol_previous) \
1405 self._pagerlink(self.previous_page, symbol_previous) \
1397 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1406 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1398 'link_next': self.next_page and \
1407 'link_next': self.next_page and \
1399 self._pagerlink(self.next_page, symbol_next) \
1408 self._pagerlink(self.next_page, symbol_next) \
1400 or HTML.span(symbol_next, class_="pg-next disabled")
1409 or HTML.span(symbol_next, class_="pg-next disabled")
1401 })
1410 })
1402
1411
1403 return literal(result)
1412 return literal(result)
1404
1413
1405
1414
1406 #==============================================================================
1415 #==============================================================================
1407 # REPO PAGER, PAGER FOR REPOSITORY
1416 # REPO PAGER, PAGER FOR REPOSITORY
1408 #==============================================================================
1417 #==============================================================================
1409 class RepoPage(Page):
1418 class RepoPage(Page):
1410
1419
1411 def __init__(self, collection, page=1, items_per_page=20,
1420 def __init__(self, collection, page=1, items_per_page=20,
1412 item_count=None, url=None, **kwargs):
1421 item_count=None, url=None, **kwargs):
1413
1422
1414 """Create a "RepoPage" instance. special pager for paging
1423 """Create a "RepoPage" instance. special pager for paging
1415 repository
1424 repository
1416 """
1425 """
1417 self._url_generator = url
1426 self._url_generator = url
1418
1427
1419 # Safe the kwargs class-wide so they can be used in the pager() method
1428 # Safe the kwargs class-wide so they can be used in the pager() method
1420 self.kwargs = kwargs
1429 self.kwargs = kwargs
1421
1430
1422 # Save a reference to the collection
1431 # Save a reference to the collection
1423 self.original_collection = collection
1432 self.original_collection = collection
1424
1433
1425 self.collection = collection
1434 self.collection = collection
1426
1435
1427 # The self.page is the number of the current page.
1436 # The self.page is the number of the current page.
1428 # The first page has the number 1!
1437 # The first page has the number 1!
1429 try:
1438 try:
1430 self.page = int(page) # make it int() if we get it as a string
1439 self.page = int(page) # make it int() if we get it as a string
1431 except (ValueError, TypeError):
1440 except (ValueError, TypeError):
1432 self.page = 1
1441 self.page = 1
1433
1442
1434 self.items_per_page = items_per_page
1443 self.items_per_page = items_per_page
1435
1444
1436 # Unless the user tells us how many items the collections has
1445 # Unless the user tells us how many items the collections has
1437 # we calculate that ourselves.
1446 # we calculate that ourselves.
1438 if item_count is not None:
1447 if item_count is not None:
1439 self.item_count = item_count
1448 self.item_count = item_count
1440 else:
1449 else:
1441 self.item_count = len(self.collection)
1450 self.item_count = len(self.collection)
1442
1451
1443 # Compute the number of the first and last available page
1452 # Compute the number of the first and last available page
1444 if self.item_count > 0:
1453 if self.item_count > 0:
1445 self.first_page = 1
1454 self.first_page = 1
1446 self.page_count = int(math.ceil(float(self.item_count) /
1455 self.page_count = int(math.ceil(float(self.item_count) /
1447 self.items_per_page))
1456 self.items_per_page))
1448 self.last_page = self.first_page + self.page_count - 1
1457 self.last_page = self.first_page + self.page_count - 1
1449
1458
1450 # Make sure that the requested page number is the range of
1459 # Make sure that the requested page number is the range of
1451 # valid pages
1460 # valid pages
1452 if self.page > self.last_page:
1461 if self.page > self.last_page:
1453 self.page = self.last_page
1462 self.page = self.last_page
1454 elif self.page < self.first_page:
1463 elif self.page < self.first_page:
1455 self.page = self.first_page
1464 self.page = self.first_page
1456
1465
1457 # Note: the number of items on this page can be less than
1466 # Note: the number of items on this page can be less than
1458 # items_per_page if the last page is not full
1467 # items_per_page if the last page is not full
1459 self.first_item = max(0, (self.item_count) - (self.page *
1468 self.first_item = max(0, (self.item_count) - (self.page *
1460 items_per_page))
1469 items_per_page))
1461 self.last_item = ((self.item_count - 1) - items_per_page *
1470 self.last_item = ((self.item_count - 1) - items_per_page *
1462 (self.page - 1))
1471 (self.page - 1))
1463
1472
1464 self.items = list(self.collection[self.first_item:self.last_item + 1])
1473 self.items = list(self.collection[self.first_item:self.last_item + 1])
1465
1474
1466 # Links to previous and next page
1475 # Links to previous and next page
1467 if self.page > self.first_page:
1476 if self.page > self.first_page:
1468 self.previous_page = self.page - 1
1477 self.previous_page = self.page - 1
1469 else:
1478 else:
1470 self.previous_page = None
1479 self.previous_page = None
1471
1480
1472 if self.page < self.last_page:
1481 if self.page < self.last_page:
1473 self.next_page = self.page + 1
1482 self.next_page = self.page + 1
1474 else:
1483 else:
1475 self.next_page = None
1484 self.next_page = None
1476
1485
1477 # No items available
1486 # No items available
1478 else:
1487 else:
1479 self.first_page = None
1488 self.first_page = None
1480 self.page_count = 0
1489 self.page_count = 0
1481 self.last_page = None
1490 self.last_page = None
1482 self.first_item = None
1491 self.first_item = None
1483 self.last_item = None
1492 self.last_item = None
1484 self.previous_page = None
1493 self.previous_page = None
1485 self.next_page = None
1494 self.next_page = None
1486 self.items = []
1495 self.items = []
1487
1496
1488 # This is a subclass of the 'list' type. Initialise the list now.
1497 # This is a subclass of the 'list' type. Initialise the list now.
1489 list.__init__(self, reversed(self.items))
1498 list.__init__(self, reversed(self.items))
1490
1499
1491
1500
1492 def changed_tooltip(nodes):
1501 def changed_tooltip(nodes):
1493 """
1502 """
1494 Generates a html string for changed nodes in commit page.
1503 Generates a html string for changed nodes in commit page.
1495 It limits the output to 30 entries
1504 It limits the output to 30 entries
1496
1505
1497 :param nodes: LazyNodesGenerator
1506 :param nodes: LazyNodesGenerator
1498 """
1507 """
1499 if nodes:
1508 if nodes:
1500 pref = ': <br/> '
1509 pref = ': <br/> '
1501 suf = ''
1510 suf = ''
1502 if len(nodes) > 30:
1511 if len(nodes) > 30:
1503 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1512 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1504 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1513 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1505 for x in nodes[:30]]) + suf)
1514 for x in nodes[:30]]) + suf)
1506 else:
1515 else:
1507 return ': ' + _('No Files')
1516 return ': ' + _('No Files')
1508
1517
1509
1518
1510 def breadcrumb_repo_link(repo):
1519 def breadcrumb_repo_link(repo):
1511 """
1520 """
1512 Makes a breadcrumbs path link to repo
1521 Makes a breadcrumbs path link to repo
1513
1522
1514 ex::
1523 ex::
1515 group >> subgroup >> repo
1524 group >> subgroup >> repo
1516
1525
1517 :param repo: a Repository instance
1526 :param repo: a Repository instance
1518 """
1527 """
1519
1528
1520 path = [
1529 path = [
1521 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1530 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1522 for group in repo.groups_with_parents
1531 for group in repo.groups_with_parents
1523 ] + [
1532 ] + [
1524 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1533 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1525 ]
1534 ]
1526
1535
1527 return literal(' &raquo; '.join(path))
1536 return literal(' &raquo; '.join(path))
1528
1537
1529
1538
1530 def format_byte_size_binary(file_size):
1539 def format_byte_size_binary(file_size):
1531 """
1540 """
1532 Formats file/folder sizes to standard.
1541 Formats file/folder sizes to standard.
1533 """
1542 """
1534 formatted_size = format_byte_size(file_size, binary=True)
1543 formatted_size = format_byte_size(file_size, binary=True)
1535 return formatted_size
1544 return formatted_size
1536
1545
1537
1546
1538 def fancy_file_stats(stats):
1547 def fancy_file_stats(stats):
1539 """
1548 """
1540 Displays a fancy two colored bar for number of added/deleted
1549 Displays a fancy two colored bar for number of added/deleted
1541 lines of code on file
1550 lines of code on file
1542
1551
1543 :param stats: two element list of added/deleted lines of code
1552 :param stats: two element list of added/deleted lines of code
1544 """
1553 """
1545 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1554 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1546 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1555 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1547
1556
1548 def cgen(l_type, a_v, d_v):
1557 def cgen(l_type, a_v, d_v):
1549 mapping = {'tr': 'top-right-rounded-corner-mid',
1558 mapping = {'tr': 'top-right-rounded-corner-mid',
1550 'tl': 'top-left-rounded-corner-mid',
1559 'tl': 'top-left-rounded-corner-mid',
1551 'br': 'bottom-right-rounded-corner-mid',
1560 'br': 'bottom-right-rounded-corner-mid',
1552 'bl': 'bottom-left-rounded-corner-mid'}
1561 'bl': 'bottom-left-rounded-corner-mid'}
1553 map_getter = lambda x: mapping[x]
1562 map_getter = lambda x: mapping[x]
1554
1563
1555 if l_type == 'a' and d_v:
1564 if l_type == 'a' and d_v:
1556 #case when added and deleted are present
1565 #case when added and deleted are present
1557 return ' '.join(map(map_getter, ['tl', 'bl']))
1566 return ' '.join(map(map_getter, ['tl', 'bl']))
1558
1567
1559 if l_type == 'a' and not d_v:
1568 if l_type == 'a' and not d_v:
1560 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1569 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1561
1570
1562 if l_type == 'd' and a_v:
1571 if l_type == 'd' and a_v:
1563 return ' '.join(map(map_getter, ['tr', 'br']))
1572 return ' '.join(map(map_getter, ['tr', 'br']))
1564
1573
1565 if l_type == 'd' and not a_v:
1574 if l_type == 'd' and not a_v:
1566 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1575 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1567
1576
1568 a, d = stats['added'], stats['deleted']
1577 a, d = stats['added'], stats['deleted']
1569 width = 100
1578 width = 100
1570
1579
1571 if stats['binary']: # binary operations like chmod/rename etc
1580 if stats['binary']: # binary operations like chmod/rename etc
1572 lbl = []
1581 lbl = []
1573 bin_op = 0 # undefined
1582 bin_op = 0 # undefined
1574
1583
1575 # prefix with bin for binary files
1584 # prefix with bin for binary files
1576 if BIN_FILENODE in stats['ops']:
1585 if BIN_FILENODE in stats['ops']:
1577 lbl += ['bin']
1586 lbl += ['bin']
1578
1587
1579 if NEW_FILENODE in stats['ops']:
1588 if NEW_FILENODE in stats['ops']:
1580 lbl += [_('new file')]
1589 lbl += [_('new file')]
1581 bin_op = NEW_FILENODE
1590 bin_op = NEW_FILENODE
1582 elif MOD_FILENODE in stats['ops']:
1591 elif MOD_FILENODE in stats['ops']:
1583 lbl += [_('mod')]
1592 lbl += [_('mod')]
1584 bin_op = MOD_FILENODE
1593 bin_op = MOD_FILENODE
1585 elif DEL_FILENODE in stats['ops']:
1594 elif DEL_FILENODE in stats['ops']:
1586 lbl += [_('del')]
1595 lbl += [_('del')]
1587 bin_op = DEL_FILENODE
1596 bin_op = DEL_FILENODE
1588 elif RENAMED_FILENODE in stats['ops']:
1597 elif RENAMED_FILENODE in stats['ops']:
1589 lbl += [_('rename')]
1598 lbl += [_('rename')]
1590 bin_op = RENAMED_FILENODE
1599 bin_op = RENAMED_FILENODE
1591
1600
1592 # chmod can go with other operations, so we add a + to lbl if needed
1601 # chmod can go with other operations, so we add a + to lbl if needed
1593 if CHMOD_FILENODE in stats['ops']:
1602 if CHMOD_FILENODE in stats['ops']:
1594 lbl += [_('chmod')]
1603 lbl += [_('chmod')]
1595 if bin_op == 0:
1604 if bin_op == 0:
1596 bin_op = CHMOD_FILENODE
1605 bin_op = CHMOD_FILENODE
1597
1606
1598 lbl = '+'.join(lbl)
1607 lbl = '+'.join(lbl)
1599 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1608 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1600 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1609 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1601 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1610 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1602 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1611 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1603
1612
1604 t = stats['added'] + stats['deleted']
1613 t = stats['added'] + stats['deleted']
1605 unit = float(width) / (t or 1)
1614 unit = float(width) / (t or 1)
1606
1615
1607 # needs > 9% of width to be visible or 0 to be hidden
1616 # needs > 9% of width to be visible or 0 to be hidden
1608 a_p = max(9, unit * a) if a > 0 else 0
1617 a_p = max(9, unit * a) if a > 0 else 0
1609 d_p = max(9, unit * d) if d > 0 else 0
1618 d_p = max(9, unit * d) if d > 0 else 0
1610 p_sum = a_p + d_p
1619 p_sum = a_p + d_p
1611
1620
1612 if p_sum > width:
1621 if p_sum > width:
1613 #adjust the percentage to be == 100% since we adjusted to 9
1622 #adjust the percentage to be == 100% since we adjusted to 9
1614 if a_p > d_p:
1623 if a_p > d_p:
1615 a_p = a_p - (p_sum - width)
1624 a_p = a_p - (p_sum - width)
1616 else:
1625 else:
1617 d_p = d_p - (p_sum - width)
1626 d_p = d_p - (p_sum - width)
1618
1627
1619 a_v = a if a > 0 else ''
1628 a_v = a if a > 0 else ''
1620 d_v = d if d > 0 else ''
1629 d_v = d if d > 0 else ''
1621
1630
1622 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1631 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1623 cgen('a', a_v, d_v), a_p, a_v
1632 cgen('a', a_v, d_v), a_p, a_v
1624 )
1633 )
1625 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1634 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1626 cgen('d', a_v, d_v), d_p, d_v
1635 cgen('d', a_v, d_v), d_p, d_v
1627 )
1636 )
1628 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1637 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1629
1638
1630
1639
1631 def urlify_text(text_, safe=True):
1640 def urlify_text(text_, safe=True):
1632 """
1641 """
1633 Extrac urls from text and make html links out of them
1642 Extrac urls from text and make html links out of them
1634
1643
1635 :param text_:
1644 :param text_:
1636 """
1645 """
1637
1646
1638 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1647 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1639 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1648 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1640
1649
1641 def url_func(match_obj):
1650 def url_func(match_obj):
1642 url_full = match_obj.groups()[0]
1651 url_full = match_obj.groups()[0]
1643 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1652 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1644 _newtext = url_pat.sub(url_func, text_)
1653 _newtext = url_pat.sub(url_func, text_)
1645 if safe:
1654 if safe:
1646 return literal(_newtext)
1655 return literal(_newtext)
1647 return _newtext
1656 return _newtext
1648
1657
1649
1658
1650 def urlify_commits(text_, repository):
1659 def urlify_commits(text_, repository):
1651 """
1660 """
1652 Extract commit ids from text and make link from them
1661 Extract commit ids from text and make link from them
1653
1662
1654 :param text_:
1663 :param text_:
1655 :param repository: repo name to build the URL with
1664 :param repository: repo name to build the URL with
1656 """
1665 """
1657 from pylons import url # doh, we need to re-import url to mock it later
1666 from pylons import url # doh, we need to re-import url to mock it later
1658 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1667 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1659
1668
1660 def url_func(match_obj):
1669 def url_func(match_obj):
1661 commit_id = match_obj.groups()[1]
1670 commit_id = match_obj.groups()[1]
1662 pref = match_obj.groups()[0]
1671 pref = match_obj.groups()[0]
1663 suf = match_obj.groups()[2]
1672 suf = match_obj.groups()[2]
1664
1673
1665 tmpl = (
1674 tmpl = (
1666 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1675 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1667 '%(commit_id)s</a>%(suf)s'
1676 '%(commit_id)s</a>%(suf)s'
1668 )
1677 )
1669 return tmpl % {
1678 return tmpl % {
1670 'pref': pref,
1679 'pref': pref,
1671 'cls': 'revision-link',
1680 'cls': 'revision-link',
1672 'url': url('changeset_home', repo_name=repository,
1681 'url': url('changeset_home', repo_name=repository,
1673 revision=commit_id, qualified=True),
1682 revision=commit_id, qualified=True),
1674 'commit_id': commit_id,
1683 'commit_id': commit_id,
1675 'suf': suf
1684 'suf': suf
1676 }
1685 }
1677
1686
1678 newtext = URL_PAT.sub(url_func, text_)
1687 newtext = URL_PAT.sub(url_func, text_)
1679
1688
1680 return newtext
1689 return newtext
1681
1690
1682
1691
1683 def _process_url_func(match_obj, repo_name, uid, entry,
1692 def _process_url_func(match_obj, repo_name, uid, entry,
1684 return_raw_data=False):
1693 return_raw_data=False):
1685 pref = ''
1694 pref = ''
1686 if match_obj.group().startswith(' '):
1695 if match_obj.group().startswith(' '):
1687 pref = ' '
1696 pref = ' '
1688
1697
1689 issue_id = ''.join(match_obj.groups())
1698 issue_id = ''.join(match_obj.groups())
1690 tmpl = (
1699 tmpl = (
1691 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1700 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1692 '%(issue-prefix)s%(id-repr)s'
1701 '%(issue-prefix)s%(id-repr)s'
1693 '</a>')
1702 '</a>')
1694
1703
1695 (repo_name_cleaned,
1704 (repo_name_cleaned,
1696 parent_group_name) = RepoGroupModel().\
1705 parent_group_name) = RepoGroupModel().\
1697 _get_group_name_and_parent(repo_name)
1706 _get_group_name_and_parent(repo_name)
1698
1707
1699 # variables replacement
1708 # variables replacement
1700 named_vars = {
1709 named_vars = {
1701 'id': issue_id,
1710 'id': issue_id,
1702 'repo': repo_name,
1711 'repo': repo_name,
1703 'repo_name': repo_name_cleaned,
1712 'repo_name': repo_name_cleaned,
1704 'group_name': parent_group_name
1713 'group_name': parent_group_name
1705 }
1714 }
1706 # named regex variables
1715 # named regex variables
1707 named_vars.update(match_obj.groupdict())
1716 named_vars.update(match_obj.groupdict())
1708 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1717 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1709
1718
1710 data = {
1719 data = {
1711 'pref': pref,
1720 'pref': pref,
1712 'cls': 'issue-tracker-link',
1721 'cls': 'issue-tracker-link',
1713 'url': _url,
1722 'url': _url,
1714 'id-repr': issue_id,
1723 'id-repr': issue_id,
1715 'issue-prefix': entry['pref'],
1724 'issue-prefix': entry['pref'],
1716 'serv': entry['url'],
1725 'serv': entry['url'],
1717 }
1726 }
1718 if return_raw_data:
1727 if return_raw_data:
1719 return {
1728 return {
1720 'id': issue_id,
1729 'id': issue_id,
1721 'url': _url
1730 'url': _url
1722 }
1731 }
1723 return tmpl % data
1732 return tmpl % data
1724
1733
1725
1734
1726 def process_patterns(text_string, repo_name, config=None):
1735 def process_patterns(text_string, repo_name, config=None):
1727 repo = None
1736 repo = None
1728 if repo_name:
1737 if repo_name:
1729 # Retrieving repo_name to avoid invalid repo_name to explode on
1738 # Retrieving repo_name to avoid invalid repo_name to explode on
1730 # IssueTrackerSettingsModel but still passing invalid name further down
1739 # IssueTrackerSettingsModel but still passing invalid name further down
1731 repo = Repository.get_by_repo_name(repo_name, cache=True)
1740 repo = Repository.get_by_repo_name(repo_name, cache=True)
1732
1741
1733 settings_model = IssueTrackerSettingsModel(repo=repo)
1742 settings_model = IssueTrackerSettingsModel(repo=repo)
1734 active_entries = settings_model.get_settings(cache=True)
1743 active_entries = settings_model.get_settings(cache=True)
1735
1744
1736 issues_data = []
1745 issues_data = []
1737 newtext = text_string
1746 newtext = text_string
1738 for uid, entry in active_entries.items():
1747 for uid, entry in active_entries.items():
1739 log.debug('found issue tracker entry with uid %s' % (uid,))
1748 log.debug('found issue tracker entry with uid %s' % (uid,))
1740
1749
1741 if not (entry['pat'] and entry['url']):
1750 if not (entry['pat'] and entry['url']):
1742 log.debug('skipping due to missing data')
1751 log.debug('skipping due to missing data')
1743 continue
1752 continue
1744
1753
1745 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1754 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1746 % (uid, entry['pat'], entry['url'], entry['pref']))
1755 % (uid, entry['pat'], entry['url'], entry['pref']))
1747
1756
1748 try:
1757 try:
1749 pattern = re.compile(r'%s' % entry['pat'])
1758 pattern = re.compile(r'%s' % entry['pat'])
1750 except re.error:
1759 except re.error:
1751 log.exception(
1760 log.exception(
1752 'issue tracker pattern: `%s` failed to compile',
1761 'issue tracker pattern: `%s` failed to compile',
1753 entry['pat'])
1762 entry['pat'])
1754 continue
1763 continue
1755
1764
1756 data_func = partial(
1765 data_func = partial(
1757 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1766 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1758 return_raw_data=True)
1767 return_raw_data=True)
1759
1768
1760 for match_obj in pattern.finditer(text_string):
1769 for match_obj in pattern.finditer(text_string):
1761 issues_data.append(data_func(match_obj))
1770 issues_data.append(data_func(match_obj))
1762
1771
1763 url_func = partial(
1772 url_func = partial(
1764 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1773 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1765
1774
1766 newtext = pattern.sub(url_func, newtext)
1775 newtext = pattern.sub(url_func, newtext)
1767 log.debug('processed prefix:uid `%s`' % (uid,))
1776 log.debug('processed prefix:uid `%s`' % (uid,))
1768
1777
1769 return newtext, issues_data
1778 return newtext, issues_data
1770
1779
1771
1780
1772 def urlify_commit_message(commit_text, repository=None):
1781 def urlify_commit_message(commit_text, repository=None):
1773 """
1782 """
1774 Parses given text message and makes proper links.
1783 Parses given text message and makes proper links.
1775 issues are linked to given issue-server, and rest is a commit link
1784 issues are linked to given issue-server, and rest is a commit link
1776
1785
1777 :param commit_text:
1786 :param commit_text:
1778 :param repository:
1787 :param repository:
1779 """
1788 """
1780 from pylons import url # doh, we need to re-import url to mock it later
1789 from pylons import url # doh, we need to re-import url to mock it later
1781
1790
1782 def escaper(string):
1791 def escaper(string):
1783 return string.replace('<', '&lt;').replace('>', '&gt;')
1792 return string.replace('<', '&lt;').replace('>', '&gt;')
1784
1793
1785 newtext = escaper(commit_text)
1794 newtext = escaper(commit_text)
1786
1795
1787 # extract http/https links and make them real urls
1796 # extract http/https links and make them real urls
1788 newtext = urlify_text(newtext, safe=False)
1797 newtext = urlify_text(newtext, safe=False)
1789
1798
1790 # urlify commits - extract commit ids and make link out of them, if we have
1799 # urlify commits - extract commit ids and make link out of them, if we have
1791 # the scope of repository present.
1800 # the scope of repository present.
1792 if repository:
1801 if repository:
1793 newtext = urlify_commits(newtext, repository)
1802 newtext = urlify_commits(newtext, repository)
1794
1803
1795 # process issue tracker patterns
1804 # process issue tracker patterns
1796 newtext, issues = process_patterns(newtext, repository or '')
1805 newtext, issues = process_patterns(newtext, repository or '')
1797
1806
1798 return literal(newtext)
1807 return literal(newtext)
1799
1808
1800
1809
1801 def rst(source, mentions=False):
1810 def rst(source, mentions=False):
1802 return literal('<div class="rst-block">%s</div>' %
1811 return literal('<div class="rst-block">%s</div>' %
1803 MarkupRenderer.rst(source, mentions=mentions))
1812 MarkupRenderer.rst(source, mentions=mentions))
1804
1813
1805
1814
1806 def markdown(source, mentions=False):
1815 def markdown(source, mentions=False):
1807 return literal('<div class="markdown-block">%s</div>' %
1816 return literal('<div class="markdown-block">%s</div>' %
1808 MarkupRenderer.markdown(source, flavored=True,
1817 MarkupRenderer.markdown(source, flavored=True,
1809 mentions=mentions))
1818 mentions=mentions))
1810
1819
1811 def renderer_from_filename(filename, exclude=None):
1820 def renderer_from_filename(filename, exclude=None):
1812 return MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1821 return MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1813
1822
1814
1823
1815 def render(source, renderer='rst', mentions=False):
1824 def render(source, renderer='rst', mentions=False):
1816 if renderer == 'rst':
1825 if renderer == 'rst':
1817 return rst(source, mentions=mentions)
1826 return rst(source, mentions=mentions)
1818 if renderer == 'markdown':
1827 if renderer == 'markdown':
1819 return markdown(source, mentions=mentions)
1828 return markdown(source, mentions=mentions)
1820
1829
1821
1830
1822 def commit_status(repo, commit_id):
1831 def commit_status(repo, commit_id):
1823 return ChangesetStatusModel().get_status(repo, commit_id)
1832 return ChangesetStatusModel().get_status(repo, commit_id)
1824
1833
1825
1834
1826 def commit_status_lbl(commit_status):
1835 def commit_status_lbl(commit_status):
1827 return dict(ChangesetStatus.STATUSES).get(commit_status)
1836 return dict(ChangesetStatus.STATUSES).get(commit_status)
1828
1837
1829
1838
1830 def commit_time(repo_name, commit_id):
1839 def commit_time(repo_name, commit_id):
1831 repo = Repository.get_by_repo_name(repo_name)
1840 repo = Repository.get_by_repo_name(repo_name)
1832 commit = repo.get_commit(commit_id=commit_id)
1841 commit = repo.get_commit(commit_id=commit_id)
1833 return commit.date
1842 return commit.date
1834
1843
1835
1844
1836 def get_permission_name(key):
1845 def get_permission_name(key):
1837 return dict(Permission.PERMS).get(key)
1846 return dict(Permission.PERMS).get(key)
1838
1847
1839
1848
1840 def journal_filter_help():
1849 def journal_filter_help():
1841 return _(
1850 return _(
1842 'Example filter terms:\n' +
1851 'Example filter terms:\n' +
1843 ' repository:vcs\n' +
1852 ' repository:vcs\n' +
1844 ' username:marcin\n' +
1853 ' username:marcin\n' +
1845 ' action:*push*\n' +
1854 ' action:*push*\n' +
1846 ' ip:127.0.0.1\n' +
1855 ' ip:127.0.0.1\n' +
1847 ' date:20120101\n' +
1856 ' date:20120101\n' +
1848 ' date:[20120101100000 TO 20120102]\n' +
1857 ' date:[20120101100000 TO 20120102]\n' +
1849 '\n' +
1858 '\n' +
1850 'Generate wildcards using \'*\' character:\n' +
1859 'Generate wildcards using \'*\' character:\n' +
1851 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1860 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1852 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1861 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1853 '\n' +
1862 '\n' +
1854 'Optional AND / OR operators in queries\n' +
1863 'Optional AND / OR operators in queries\n' +
1855 ' "repository:vcs OR repository:test"\n' +
1864 ' "repository:vcs OR repository:test"\n' +
1856 ' "username:test AND repository:test*"\n'
1865 ' "username:test AND repository:test*"\n'
1857 )
1866 )
1858
1867
1859
1868
1860 def not_mapped_error(repo_name):
1869 def not_mapped_error(repo_name):
1861 flash(_('%s repository is not mapped to db perhaps'
1870 flash(_('%s repository is not mapped to db perhaps'
1862 ' it was created or renamed from the filesystem'
1871 ' it was created or renamed from the filesystem'
1863 ' please run the application again'
1872 ' please run the application again'
1864 ' in order to rescan repositories') % repo_name, category='error')
1873 ' in order to rescan repositories') % repo_name, category='error')
1865
1874
1866
1875
1867 def ip_range(ip_addr):
1876 def ip_range(ip_addr):
1868 from rhodecode.model.db import UserIpMap
1877 from rhodecode.model.db import UserIpMap
1869 s, e = UserIpMap._get_ip_range(ip_addr)
1878 s, e = UserIpMap._get_ip_range(ip_addr)
1870 return '%s - %s' % (s, e)
1879 return '%s - %s' % (s, e)
1871
1880
1872
1881
1873 def form(url, method='post', needs_csrf_token=True, **attrs):
1882 def form(url, method='post', needs_csrf_token=True, **attrs):
1874 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1883 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1875 if method.lower() != 'get' and needs_csrf_token:
1884 if method.lower() != 'get' and needs_csrf_token:
1876 raise Exception(
1885 raise Exception(
1877 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1886 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1878 'CSRF token. If the endpoint does not require such token you can ' +
1887 'CSRF token. If the endpoint does not require such token you can ' +
1879 'explicitly set the parameter needs_csrf_token to false.')
1888 'explicitly set the parameter needs_csrf_token to false.')
1880
1889
1881 return wh_form(url, method=method, **attrs)
1890 return wh_form(url, method=method, **attrs)
1882
1891
1883
1892
1884 def secure_form(url, method="POST", multipart=False, **attrs):
1893 def secure_form(url, method="POST", multipart=False, **attrs):
1885 """Start a form tag that points the action to an url. This
1894 """Start a form tag that points the action to an url. This
1886 form tag will also include the hidden field containing
1895 form tag will also include the hidden field containing
1887 the auth token.
1896 the auth token.
1888
1897
1889 The url options should be given either as a string, or as a
1898 The url options should be given either as a string, or as a
1890 ``url()`` function. The method for the form defaults to POST.
1899 ``url()`` function. The method for the form defaults to POST.
1891
1900
1892 Options:
1901 Options:
1893
1902
1894 ``multipart``
1903 ``multipart``
1895 If set to True, the enctype is set to "multipart/form-data".
1904 If set to True, the enctype is set to "multipart/form-data".
1896 ``method``
1905 ``method``
1897 The method to use when submitting the form, usually either
1906 The method to use when submitting the form, usually either
1898 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1907 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1899 hidden input with name _method is added to simulate the verb
1908 hidden input with name _method is added to simulate the verb
1900 over POST.
1909 over POST.
1901
1910
1902 """
1911 """
1903 from webhelpers.pylonslib.secure_form import insecure_form
1912 from webhelpers.pylonslib.secure_form import insecure_form
1904 form = insecure_form(url, method, multipart, **attrs)
1913 form = insecure_form(url, method, multipart, **attrs)
1905 token = csrf_input()
1914 token = csrf_input()
1906 return literal("%s\n%s" % (form, token))
1915 return literal("%s\n%s" % (form, token))
1907
1916
1908 def csrf_input():
1917 def csrf_input():
1909 return literal(
1918 return literal(
1910 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1919 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1911 csrf_token_key, csrf_token_key, get_csrf_token()))
1920 csrf_token_key, csrf_token_key, get_csrf_token()))
1912
1921
1913 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1922 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1914 select_html = select(name, selected, options, **attrs)
1923 select_html = select(name, selected, options, **attrs)
1915 select2 = """
1924 select2 = """
1916 <script>
1925 <script>
1917 $(document).ready(function() {
1926 $(document).ready(function() {
1918 $('#%s').select2({
1927 $('#%s').select2({
1919 containerCssClass: 'drop-menu',
1928 containerCssClass: 'drop-menu',
1920 dropdownCssClass: 'drop-menu-dropdown',
1929 dropdownCssClass: 'drop-menu-dropdown',
1921 dropdownAutoWidth: true%s
1930 dropdownAutoWidth: true%s
1922 });
1931 });
1923 });
1932 });
1924 </script>
1933 </script>
1925 """
1934 """
1926 filter_option = """,
1935 filter_option = """,
1927 minimumResultsForSearch: -1
1936 minimumResultsForSearch: -1
1928 """
1937 """
1929 input_id = attrs.get('id') or name
1938 input_id = attrs.get('id') or name
1930 filter_enabled = "" if enable_filter else filter_option
1939 filter_enabled = "" if enable_filter else filter_option
1931 select_script = literal(select2 % (input_id, filter_enabled))
1940 select_script = literal(select2 % (input_id, filter_enabled))
1932
1941
1933 return literal(select_html+select_script)
1942 return literal(select_html+select_script)
1934
1943
1935
1944
1936 def get_visual_attr(tmpl_context_var, attr_name):
1945 def get_visual_attr(tmpl_context_var, attr_name):
1937 """
1946 """
1938 A safe way to get a variable from visual variable of template context
1947 A safe way to get a variable from visual variable of template context
1939
1948
1940 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1949 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1941 :param attr_name: name of the attribute we fetch from the c.visual
1950 :param attr_name: name of the attribute we fetch from the c.visual
1942 """
1951 """
1943 visual = getattr(tmpl_context_var, 'visual', None)
1952 visual = getattr(tmpl_context_var, 'visual', None)
1944 if not visual:
1953 if not visual:
1945 return
1954 return
1946 else:
1955 else:
1947 return getattr(visual, attr_name, None)
1956 return getattr(visual, attr_name, None)
1948
1957
1949
1958
1950 def get_last_path_part(file_node):
1959 def get_last_path_part(file_node):
1951 if not file_node.path:
1960 if not file_node.path:
1952 return u''
1961 return u''
1953
1962
1954 path = safe_unicode(file_node.path.split('/')[-1])
1963 path = safe_unicode(file_node.path.split('/')[-1])
1955 return u'../' + path
1964 return u'../' + path
1956
1965
1957
1966
1958 def route_path(*args, **kwds):
1967 def route_path(*args, **kwds):
1959 """
1968 """
1960 Wrapper around pyramids `route_path` function. It is used to generate
1969 Wrapper around pyramids `route_path` function. It is used to generate
1961 URLs from within pylons views or templates. This will be removed when
1970 URLs from within pylons views or templates. This will be removed when
1962 pyramid migration if finished.
1971 pyramid migration if finished.
1963 """
1972 """
1964 req = get_current_request()
1973 req = get_current_request()
1965 return req.route_path(*args, **kwds)
1974 return req.route_path(*args, **kwds)
1966
1975
1967
1976
1968 def route_path_or_none(*args, **kwargs):
1977 def route_path_or_none(*args, **kwargs):
1969 try:
1978 try:
1970 return route_path(*args, **kwargs)
1979 return route_path(*args, **kwargs)
1971 except KeyError:
1980 except KeyError:
1972 return None
1981 return None
1973
1982
1974
1983
1975 def static_url(*args, **kwds):
1984 def static_url(*args, **kwds):
1976 """
1985 """
1977 Wrapper around pyramids `route_path` function. It is used to generate
1986 Wrapper around pyramids `route_path` function. It is used to generate
1978 URLs from within pylons views or templates. This will be removed when
1987 URLs from within pylons views or templates. This will be removed when
1979 pyramid migration if finished.
1988 pyramid migration if finished.
1980 """
1989 """
1981 req = get_current_request()
1990 req = get_current_request()
1982 return req.static_url(*args, **kwds)
1991 return req.static_url(*args, **kwds)
1983
1992
1984
1993
1985 def resource_path(*args, **kwds):
1994 def resource_path(*args, **kwds):
1986 """
1995 """
1987 Wrapper around pyramids `route_path` function. It is used to generate
1996 Wrapper around pyramids `route_path` function. It is used to generate
1988 URLs from within pylons views or templates. This will be removed when
1997 URLs from within pylons views or templates. This will be removed when
1989 pyramid migration if finished.
1998 pyramid migration if finished.
1990 """
1999 """
1991 req = get_current_request()
2000 req = get_current_request()
1992 return req.resource_path(*args, **kwds)
2001 return req.resource_path(*args, **kwds)
General Comments 0
You need to be logged in to leave comments. Login now