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