##// END OF EJS Templates
helpers: remove bad warning calls introduced in commit c8bec9b70fb9
marcink -
r416:502b7879 default
parent child Browse files
Show More
@@ -1,1933 +1,1931 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))
793
791
794 # If we have an email, use it, otherwise
792 # If we have an email, use it, otherwise
795 # see if it contains a username we can get an email from
793 # see if it contains a username we can get an email from
796 if _email != '':
794 if _email != '':
797 return _email
795 return _email
798 else:
796 else:
799 user = User.get_by_username(author_name(author), case_insensitive=True,
797 user = User.get_by_username(
800 cache=True)
798 author_name(author), case_insensitive=True, cache=True)
801
799
802 if user is not None:
800 if user is not None:
803 return user.email
801 return user.email
804
802
805 # No valid email, not a valid user in the system, none!
803 # No valid email, not a valid user in the system, none!
806 return None
804 return None
807
805
808
806
809 def link_to_user(author, length=0, **kwargs):
807 def link_to_user(author, length=0, **kwargs):
810 user = discover_user(author)
808 user = discover_user(author)
811 # user can be None, but if we have it already it means we can re-use it
809 # user can be None, but if we have it already it means we can re-use it
812 # in the person() function, so we save 1 intensive-query
810 # in the person() function, so we save 1 intensive-query
813 if user:
811 if user:
814 author = user
812 author = user
815
813
816 display_person = person(author, 'username_or_name_or_email')
814 display_person = person(author, 'username_or_name_or_email')
817 if length:
815 if length:
818 display_person = shorter(display_person, length)
816 display_person = shorter(display_person, length)
819
817
820 if user:
818 if user:
821 return link_to(
819 return link_to(
822 escape(display_person),
820 escape(display_person),
823 url('user_profile', username=user.username),
821 url('user_profile', username=user.username),
824 **kwargs)
822 **kwargs)
825 else:
823 else:
826 return escape(display_person)
824 return escape(display_person)
827
825
828
826
829 def person(author, show_attr="username_and_name"):
827 def person(author, show_attr="username_and_name"):
830 user = discover_user(author)
828 user = discover_user(author)
831 if user:
829 if user:
832 return getattr(user, show_attr)
830 return getattr(user, show_attr)
833 else:
831 else:
834 _author = author_name(author)
832 _author = author_name(author)
835 _email = email(author)
833 _email = email(author)
836 return _author or _email
834 return _author or _email
837
835
838
836
839 def author_string(email):
837 def author_string(email):
840 if email:
838 if email:
841 user = User.get_by_email(email, case_insensitive=True, cache=True)
839 user = User.get_by_email(email, case_insensitive=True, cache=True)
842 if user:
840 if user:
843 if user.firstname or user.lastname:
841 if user.firstname or user.lastname:
844 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
842 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
845 else:
843 else:
846 return email
844 return email
847 else:
845 else:
848 return email
846 return email
849 else:
847 else:
850 return None
848 return None
851
849
852
850
853 def person_by_id(id_, show_attr="username_and_name"):
851 def person_by_id(id_, show_attr="username_and_name"):
854 # attr to return from fetched user
852 # attr to return from fetched user
855 person_getter = lambda usr: getattr(usr, show_attr)
853 person_getter = lambda usr: getattr(usr, show_attr)
856
854
857 #maybe it's an ID ?
855 #maybe it's an ID ?
858 if str(id_).isdigit() or isinstance(id_, int):
856 if str(id_).isdigit() or isinstance(id_, int):
859 id_ = int(id_)
857 id_ = int(id_)
860 user = User.get(id_)
858 user = User.get(id_)
861 if user is not None:
859 if user is not None:
862 return person_getter(user)
860 return person_getter(user)
863 return id_
861 return id_
864
862
865
863
866 def gravatar_with_user(author, show_disabled=False):
864 def gravatar_with_user(author, show_disabled=False):
867 from rhodecode.lib.utils import PartialRenderer
865 from rhodecode.lib.utils import PartialRenderer
868 _render = PartialRenderer('base/base.html')
866 _render = PartialRenderer('base/base.html')
869 return _render('gravatar_with_user', author, show_disabled=show_disabled)
867 return _render('gravatar_with_user', author, show_disabled=show_disabled)
870
868
871
869
872 def desc_stylize(value):
870 def desc_stylize(value):
873 """
871 """
874 converts tags from value into html equivalent
872 converts tags from value into html equivalent
875
873
876 :param value:
874 :param value:
877 """
875 """
878 if not value:
876 if not value:
879 return ''
877 return ''
880
878
881 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
879 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
882 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
880 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
883 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
881 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
884 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
882 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
885 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
883 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
886 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
884 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
887 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
885 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
888 '<div class="metatag" tag="lang">\\2</div>', value)
886 '<div class="metatag" tag="lang">\\2</div>', value)
889 value = re.sub(r'\[([a-z]+)\]',
887 value = re.sub(r'\[([a-z]+)\]',
890 '<div class="metatag" tag="\\1">\\1</div>', value)
888 '<div class="metatag" tag="\\1">\\1</div>', value)
891
889
892 return value
890 return value
893
891
894
892
895 def escaped_stylize(value):
893 def escaped_stylize(value):
896 """
894 """
897 converts tags from value into html equivalent, but escaping its value first
895 converts tags from value into html equivalent, but escaping its value first
898 """
896 """
899 if not value:
897 if not value:
900 return ''
898 return ''
901
899
902 # Using default webhelper escape method, but has to force it as a
900 # Using default webhelper escape method, but has to force it as a
903 # plain unicode instead of a markup tag to be used in regex expressions
901 # plain unicode instead of a markup tag to be used in regex expressions
904 value = unicode(escape(safe_unicode(value)))
902 value = unicode(escape(safe_unicode(value)))
905
903
906 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
904 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
907 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
905 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
908 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
906 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
909 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
907 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
910 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
908 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
911 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
909 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
912 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
910 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
913 '<div class="metatag" tag="lang">\\2</div>', value)
911 '<div class="metatag" tag="lang">\\2</div>', value)
914 value = re.sub(r'\[([a-z]+)\]',
912 value = re.sub(r'\[([a-z]+)\]',
915 '<div class="metatag" tag="\\1">\\1</div>', value)
913 '<div class="metatag" tag="\\1">\\1</div>', value)
916
914
917 return value
915 return value
918
916
919
917
920 def bool2icon(value):
918 def bool2icon(value):
921 """
919 """
922 Returns boolean value of a given value, represented as html element with
920 Returns boolean value of a given value, represented as html element with
923 classes that will represent icons
921 classes that will represent icons
924
922
925 :param value: given value to convert to html node
923 :param value: given value to convert to html node
926 """
924 """
927
925
928 if value: # does bool conversion
926 if value: # does bool conversion
929 return HTML.tag('i', class_="icon-true")
927 return HTML.tag('i', class_="icon-true")
930 else: # not true as bool
928 else: # not true as bool
931 return HTML.tag('i', class_="icon-false")
929 return HTML.tag('i', class_="icon-false")
932
930
933
931
934 #==============================================================================
932 #==============================================================================
935 # PERMS
933 # PERMS
936 #==============================================================================
934 #==============================================================================
937 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
935 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
938 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
936 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
939 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token
937 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token
940
938
941
939
942 #==============================================================================
940 #==============================================================================
943 # GRAVATAR URL
941 # GRAVATAR URL
944 #==============================================================================
942 #==============================================================================
945 class InitialsGravatar(object):
943 class InitialsGravatar(object):
946 def __init__(self, email_address, first_name, last_name, size=30,
944 def __init__(self, email_address, first_name, last_name, size=30,
947 background=None, text_color='#fff'):
945 background=None, text_color='#fff'):
948 self.size = size
946 self.size = size
949 self.first_name = first_name
947 self.first_name = first_name
950 self.last_name = last_name
948 self.last_name = last_name
951 self.email_address = email_address
949 self.email_address = email_address
952 self.background = background or self.str2color(email_address)
950 self.background = background or self.str2color(email_address)
953 self.text_color = text_color
951 self.text_color = text_color
954
952
955 def get_color_bank(self):
953 def get_color_bank(self):
956 """
954 """
957 returns a predefined list of colors that gravatars can use.
955 returns a predefined list of colors that gravatars can use.
958 Those are randomized distinct colors that guarantee readability and
956 Those are randomized distinct colors that guarantee readability and
959 uniqueness.
957 uniqueness.
960
958
961 generated with: http://phrogz.net/css/distinct-colors.html
959 generated with: http://phrogz.net/css/distinct-colors.html
962 """
960 """
963 return [
961 return [
964 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
962 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
965 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
963 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
966 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
964 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
967 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
965 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
968 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
966 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
969 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
967 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
970 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
968 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
971 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
969 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
972 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
970 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
973 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
971 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
974 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
972 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
975 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
973 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
976 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
974 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
977 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
975 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
978 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
976 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
979 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
977 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
980 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
978 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
981 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
979 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
982 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
980 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
983 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
981 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
984 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
982 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
985 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
983 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
986 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
984 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
987 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
985 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
988 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
986 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
989 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
987 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
990 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
988 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
991 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
989 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
992 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
990 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
993 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
991 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
994 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
992 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
995 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
993 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
996 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
994 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
997 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
995 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
998 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
996 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
999 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
997 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1000 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
998 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1001 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
999 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1002 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1000 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1003 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1001 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1004 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1002 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1005 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1003 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1006 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1004 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1007 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1005 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1008 '#4f8c46', '#368dd9', '#5c0073'
1006 '#4f8c46', '#368dd9', '#5c0073'
1009 ]
1007 ]
1010
1008
1011 def rgb_to_hex_color(self, rgb_tuple):
1009 def rgb_to_hex_color(self, rgb_tuple):
1012 """
1010 """
1013 Converts an rgb_tuple passed to an hex color.
1011 Converts an rgb_tuple passed to an hex color.
1014
1012
1015 :param rgb_tuple: tuple with 3 ints represents rgb color space
1013 :param rgb_tuple: tuple with 3 ints represents rgb color space
1016 """
1014 """
1017 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1015 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1018
1016
1019 def email_to_int_list(self, email_str):
1017 def email_to_int_list(self, email_str):
1020 """
1018 """
1021 Get every byte of the hex digest value of email and turn it to integer.
1019 Get every byte of the hex digest value of email and turn it to integer.
1022 It's going to be always between 0-255
1020 It's going to be always between 0-255
1023 """
1021 """
1024 digest = md5_safe(email_str.lower())
1022 digest = md5_safe(email_str.lower())
1025 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1023 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1026
1024
1027 def pick_color_bank_index(self, email_str, color_bank):
1025 def pick_color_bank_index(self, email_str, color_bank):
1028 return self.email_to_int_list(email_str)[0] % len(color_bank)
1026 return self.email_to_int_list(email_str)[0] % len(color_bank)
1029
1027
1030 def str2color(self, email_str):
1028 def str2color(self, email_str):
1031 """
1029 """
1032 Tries to map in a stable algorithm an email to color
1030 Tries to map in a stable algorithm an email to color
1033
1031
1034 :param email_str:
1032 :param email_str:
1035 """
1033 """
1036 color_bank = self.get_color_bank()
1034 color_bank = self.get_color_bank()
1037 # pick position (module it's length so we always find it in the
1035 # pick position (module it's length so we always find it in the
1038 # bank even if it's smaller than 256 values
1036 # bank even if it's smaller than 256 values
1039 pos = self.pick_color_bank_index(email_str, color_bank)
1037 pos = self.pick_color_bank_index(email_str, color_bank)
1040 return color_bank[pos]
1038 return color_bank[pos]
1041
1039
1042 def normalize_email(self, email_address):
1040 def normalize_email(self, email_address):
1043 import unicodedata
1041 import unicodedata
1044 # default host used to fill in the fake/missing email
1042 # default host used to fill in the fake/missing email
1045 default_host = u'localhost'
1043 default_host = u'localhost'
1046
1044
1047 if not email_address:
1045 if not email_address:
1048 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1046 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1049
1047
1050 email_address = safe_unicode(email_address)
1048 email_address = safe_unicode(email_address)
1051
1049
1052 if u'@' not in email_address:
1050 if u'@' not in email_address:
1053 email_address = u'%s@%s' % (email_address, default_host)
1051 email_address = u'%s@%s' % (email_address, default_host)
1054
1052
1055 if email_address.endswith(u'@'):
1053 if email_address.endswith(u'@'):
1056 email_address = u'%s%s' % (email_address, default_host)
1054 email_address = u'%s%s' % (email_address, default_host)
1057
1055
1058 email_address = unicodedata.normalize('NFKD', email_address)\
1056 email_address = unicodedata.normalize('NFKD', email_address)\
1059 .encode('ascii', 'ignore')
1057 .encode('ascii', 'ignore')
1060 return email_address
1058 return email_address
1061
1059
1062 def get_initials(self):
1060 def get_initials(self):
1063 """
1061 """
1064 Returns 2 letter initials calculated based on the input.
1062 Returns 2 letter initials calculated based on the input.
1065 The algorithm picks first given email address, and takes first letter
1063 The algorithm picks first given email address, and takes first letter
1066 of part before @, and then the first letter of server name. In case
1064 of part before @, and then the first letter of server name. In case
1067 the part before @ is in a format of `somestring.somestring2` it replaces
1065 the part before @ is in a format of `somestring.somestring2` it replaces
1068 the server letter with first letter of somestring2
1066 the server letter with first letter of somestring2
1069
1067
1070 In case function was initialized with both first and lastname, this
1068 In case function was initialized with both first and lastname, this
1071 overrides the extraction from email by first letter of the first and
1069 overrides the extraction from email by first letter of the first and
1072 last name. We add special logic to that functionality, In case Full name
1070 last name. We add special logic to that functionality, In case Full name
1073 is compound, like Guido Von Rossum, we use last part of the last name
1071 is compound, like Guido Von Rossum, we use last part of the last name
1074 (Von Rossum) picking `R`.
1072 (Von Rossum) picking `R`.
1075
1073
1076 Function also normalizes the non-ascii characters to they ascii
1074 Function also normalizes the non-ascii characters to they ascii
1077 representation, eg Δ„ => A
1075 representation, eg Δ„ => A
1078 """
1076 """
1079 import unicodedata
1077 import unicodedata
1080 # replace non-ascii to ascii
1078 # replace non-ascii to ascii
1081 first_name = unicodedata.normalize(
1079 first_name = unicodedata.normalize(
1082 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1080 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1083 last_name = unicodedata.normalize(
1081 last_name = unicodedata.normalize(
1084 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1082 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1085
1083
1086 # do NFKD encoding, and also make sure email has proper format
1084 # do NFKD encoding, and also make sure email has proper format
1087 email_address = self.normalize_email(self.email_address)
1085 email_address = self.normalize_email(self.email_address)
1088
1086
1089 # first push the email initials
1087 # first push the email initials
1090 prefix, server = email_address.split('@', 1)
1088 prefix, server = email_address.split('@', 1)
1091
1089
1092 # check if prefix is maybe a 'firstname.lastname' syntax
1090 # check if prefix is maybe a 'firstname.lastname' syntax
1093 _dot_split = prefix.rsplit('.', 1)
1091 _dot_split = prefix.rsplit('.', 1)
1094 if len(_dot_split) == 2:
1092 if len(_dot_split) == 2:
1095 initials = [_dot_split[0][0], _dot_split[1][0]]
1093 initials = [_dot_split[0][0], _dot_split[1][0]]
1096 else:
1094 else:
1097 initials = [prefix[0], server[0]]
1095 initials = [prefix[0], server[0]]
1098
1096
1099 # then try to replace either firtname or lastname
1097 # then try to replace either firtname or lastname
1100 fn_letter = (first_name or " ")[0].strip()
1098 fn_letter = (first_name or " ")[0].strip()
1101 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1099 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1102
1100
1103 if fn_letter:
1101 if fn_letter:
1104 initials[0] = fn_letter
1102 initials[0] = fn_letter
1105
1103
1106 if ln_letter:
1104 if ln_letter:
1107 initials[1] = ln_letter
1105 initials[1] = ln_letter
1108
1106
1109 return ''.join(initials).upper()
1107 return ''.join(initials).upper()
1110
1108
1111 def get_img_data_by_type(self, font_family, img_type):
1109 def get_img_data_by_type(self, font_family, img_type):
1112 default_user = """
1110 default_user = """
1113 <svg xmlns="http://www.w3.org/2000/svg"
1111 <svg xmlns="http://www.w3.org/2000/svg"
1114 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1112 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1115 viewBox="-15 -10 439.165 429.164"
1113 viewBox="-15 -10 439.165 429.164"
1116
1114
1117 xml:space="preserve"
1115 xml:space="preserve"
1118 style="background:{background};" >
1116 style="background:{background};" >
1119
1117
1120 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1118 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1121 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1119 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1122 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1120 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1123 168.596,153.916,216.671,
1121 168.596,153.916,216.671,
1124 204.583,216.671z" fill="{text_color}"/>
1122 204.583,216.671z" fill="{text_color}"/>
1125 <path d="M407.164,374.717L360.88,
1123 <path d="M407.164,374.717L360.88,
1126 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1124 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1127 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1125 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1128 15.366-44.203,23.488-69.076,23.488c-24.877,
1126 15.366-44.203,23.488-69.076,23.488c-24.877,
1129 0-48.762-8.122-69.078-23.488
1127 0-48.762-8.122-69.078-23.488
1130 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1128 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1131 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1129 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1132 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1130 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1133 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1131 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1134 19.402-10.527 C409.699,390.129,
1132 19.402-10.527 C409.699,390.129,
1135 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1133 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1136 </svg>""".format(
1134 </svg>""".format(
1137 size=self.size,
1135 size=self.size,
1138 background='#979797', # @grey4
1136 background='#979797', # @grey4
1139 text_color=self.text_color,
1137 text_color=self.text_color,
1140 font_family=font_family)
1138 font_family=font_family)
1141
1139
1142 return {
1140 return {
1143 "default_user": default_user
1141 "default_user": default_user
1144 }[img_type]
1142 }[img_type]
1145
1143
1146 def get_img_data(self, svg_type=None):
1144 def get_img_data(self, svg_type=None):
1147 """
1145 """
1148 generates the svg metadata for image
1146 generates the svg metadata for image
1149 """
1147 """
1150
1148
1151 font_family = ','.join([
1149 font_family = ','.join([
1152 'proximanovaregular',
1150 'proximanovaregular',
1153 'Proxima Nova Regular',
1151 'Proxima Nova Regular',
1154 'Proxima Nova',
1152 'Proxima Nova',
1155 'Arial',
1153 'Arial',
1156 'Lucida Grande',
1154 'Lucida Grande',
1157 'sans-serif'
1155 'sans-serif'
1158 ])
1156 ])
1159 if svg_type:
1157 if svg_type:
1160 return self.get_img_data_by_type(font_family, svg_type)
1158 return self.get_img_data_by_type(font_family, svg_type)
1161
1159
1162 initials = self.get_initials()
1160 initials = self.get_initials()
1163 img_data = """
1161 img_data = """
1164 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1162 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1165 width="{size}" height="{size}"
1163 width="{size}" height="{size}"
1166 style="width: 100%; height: 100%; background-color: {background}"
1164 style="width: 100%; height: 100%; background-color: {background}"
1167 viewBox="0 0 {size} {size}">
1165 viewBox="0 0 {size} {size}">
1168 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1166 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1169 pointer-events="auto" fill="{text_color}"
1167 pointer-events="auto" fill="{text_color}"
1170 font-family="{font_family}"
1168 font-family="{font_family}"
1171 style="font-weight: 400; font-size: {f_size}px;">{text}
1169 style="font-weight: 400; font-size: {f_size}px;">{text}
1172 </text>
1170 </text>
1173 </svg>""".format(
1171 </svg>""".format(
1174 size=self.size,
1172 size=self.size,
1175 f_size=self.size/1.85, # scale the text inside the box nicely
1173 f_size=self.size/1.85, # scale the text inside the box nicely
1176 background=self.background,
1174 background=self.background,
1177 text_color=self.text_color,
1175 text_color=self.text_color,
1178 text=initials.upper(),
1176 text=initials.upper(),
1179 font_family=font_family)
1177 font_family=font_family)
1180
1178
1181 return img_data
1179 return img_data
1182
1180
1183 def generate_svg(self, svg_type=None):
1181 def generate_svg(self, svg_type=None):
1184 img_data = self.get_img_data(svg_type)
1182 img_data = self.get_img_data(svg_type)
1185 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1183 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1186
1184
1187
1185
1188 def initials_gravatar(email_address, first_name, last_name, size=30):
1186 def initials_gravatar(email_address, first_name, last_name, size=30):
1189 svg_type = None
1187 svg_type = None
1190 if email_address == User.DEFAULT_USER_EMAIL:
1188 if email_address == User.DEFAULT_USER_EMAIL:
1191 svg_type = 'default_user'
1189 svg_type = 'default_user'
1192 klass = InitialsGravatar(email_address, first_name, last_name, size)
1190 klass = InitialsGravatar(email_address, first_name, last_name, size)
1193 return klass.generate_svg(svg_type=svg_type)
1191 return klass.generate_svg(svg_type=svg_type)
1194
1192
1195
1193
1196 def gravatar_url(email_address, size=30):
1194 def gravatar_url(email_address, size=30):
1197 # doh, we need to re-import those to mock it later
1195 # doh, we need to re-import those to mock it later
1198 from pylons import tmpl_context as c
1196 from pylons import tmpl_context as c
1199
1197
1200 _use_gravatar = c.visual.use_gravatar
1198 _use_gravatar = c.visual.use_gravatar
1201 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1199 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1202
1200
1203 email_address = email_address or User.DEFAULT_USER_EMAIL
1201 email_address = email_address or User.DEFAULT_USER_EMAIL
1204 if isinstance(email_address, unicode):
1202 if isinstance(email_address, unicode):
1205 # hashlib crashes on unicode items
1203 # hashlib crashes on unicode items
1206 email_address = safe_str(email_address)
1204 email_address = safe_str(email_address)
1207
1205
1208 # empty email or default user
1206 # empty email or default user
1209 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1207 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1210 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1208 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1211
1209
1212 if _use_gravatar:
1210 if _use_gravatar:
1213 # TODO: Disuse pyramid thread locals. Think about another solution to
1211 # TODO: Disuse pyramid thread locals. Think about another solution to
1214 # get the host and schema here.
1212 # get the host and schema here.
1215 request = get_current_request()
1213 request = get_current_request()
1216 tmpl = safe_str(_gravatar_url)
1214 tmpl = safe_str(_gravatar_url)
1217 tmpl = tmpl.replace('{email}', email_address)\
1215 tmpl = tmpl.replace('{email}', email_address)\
1218 .replace('{md5email}', md5_safe(email_address.lower())) \
1216 .replace('{md5email}', md5_safe(email_address.lower())) \
1219 .replace('{netloc}', request.host)\
1217 .replace('{netloc}', request.host)\
1220 .replace('{scheme}', request.scheme)\
1218 .replace('{scheme}', request.scheme)\
1221 .replace('{size}', safe_str(size))
1219 .replace('{size}', safe_str(size))
1222 return tmpl
1220 return tmpl
1223 else:
1221 else:
1224 return initials_gravatar(email_address, '', '', size=size)
1222 return initials_gravatar(email_address, '', '', size=size)
1225
1223
1226
1224
1227 class Page(_Page):
1225 class Page(_Page):
1228 """
1226 """
1229 Custom pager to match rendering style with paginator
1227 Custom pager to match rendering style with paginator
1230 """
1228 """
1231
1229
1232 def _get_pos(self, cur_page, max_page, items):
1230 def _get_pos(self, cur_page, max_page, items):
1233 edge = (items / 2) + 1
1231 edge = (items / 2) + 1
1234 if (cur_page <= edge):
1232 if (cur_page <= edge):
1235 radius = max(items / 2, items - cur_page)
1233 radius = max(items / 2, items - cur_page)
1236 elif (max_page - cur_page) < edge:
1234 elif (max_page - cur_page) < edge:
1237 radius = (items - 1) - (max_page - cur_page)
1235 radius = (items - 1) - (max_page - cur_page)
1238 else:
1236 else:
1239 radius = items / 2
1237 radius = items / 2
1240
1238
1241 left = max(1, (cur_page - (radius)))
1239 left = max(1, (cur_page - (radius)))
1242 right = min(max_page, cur_page + (radius))
1240 right = min(max_page, cur_page + (radius))
1243 return left, cur_page, right
1241 return left, cur_page, right
1244
1242
1245 def _range(self, regexp_match):
1243 def _range(self, regexp_match):
1246 """
1244 """
1247 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1245 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1248
1246
1249 Arguments:
1247 Arguments:
1250
1248
1251 regexp_match
1249 regexp_match
1252 A "re" (regular expressions) match object containing the
1250 A "re" (regular expressions) match object containing the
1253 radius of linked pages around the current page in
1251 radius of linked pages around the current page in
1254 regexp_match.group(1) as a string
1252 regexp_match.group(1) as a string
1255
1253
1256 This function is supposed to be called as a callable in
1254 This function is supposed to be called as a callable in
1257 re.sub.
1255 re.sub.
1258
1256
1259 """
1257 """
1260 radius = int(regexp_match.group(1))
1258 radius = int(regexp_match.group(1))
1261
1259
1262 # Compute the first and last page number within the radius
1260 # Compute the first and last page number within the radius
1263 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1261 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1264 # -> leftmost_page = 5
1262 # -> leftmost_page = 5
1265 # -> rightmost_page = 9
1263 # -> rightmost_page = 9
1266 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1264 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1267 self.last_page,
1265 self.last_page,
1268 (radius * 2) + 1)
1266 (radius * 2) + 1)
1269 nav_items = []
1267 nav_items = []
1270
1268
1271 # Create a link to the first page (unless we are on the first page
1269 # Create a link to the first page (unless we are on the first page
1272 # or there would be no need to insert '..' spacers)
1270 # or there would be no need to insert '..' spacers)
1273 if self.page != self.first_page and self.first_page < leftmost_page:
1271 if self.page != self.first_page and self.first_page < leftmost_page:
1274 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1272 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1275
1273
1276 # Insert dots if there are pages between the first page
1274 # Insert dots if there are pages between the first page
1277 # and the currently displayed page range
1275 # and the currently displayed page range
1278 if leftmost_page - self.first_page > 1:
1276 if leftmost_page - self.first_page > 1:
1279 # Wrap in a SPAN tag if nolink_attr is set
1277 # Wrap in a SPAN tag if nolink_attr is set
1280 text = '..'
1278 text = '..'
1281 if self.dotdot_attr:
1279 if self.dotdot_attr:
1282 text = HTML.span(c=text, **self.dotdot_attr)
1280 text = HTML.span(c=text, **self.dotdot_attr)
1283 nav_items.append(text)
1281 nav_items.append(text)
1284
1282
1285 for thispage in xrange(leftmost_page, rightmost_page + 1):
1283 for thispage in xrange(leftmost_page, rightmost_page + 1):
1286 # Hilight the current page number and do not use a link
1284 # Hilight the current page number and do not use a link
1287 if thispage == self.page:
1285 if thispage == self.page:
1288 text = '%s' % (thispage,)
1286 text = '%s' % (thispage,)
1289 # Wrap in a SPAN tag if nolink_attr is set
1287 # Wrap in a SPAN tag if nolink_attr is set
1290 if self.curpage_attr:
1288 if self.curpage_attr:
1291 text = HTML.span(c=text, **self.curpage_attr)
1289 text = HTML.span(c=text, **self.curpage_attr)
1292 nav_items.append(text)
1290 nav_items.append(text)
1293 # Otherwise create just a link to that page
1291 # Otherwise create just a link to that page
1294 else:
1292 else:
1295 text = '%s' % (thispage,)
1293 text = '%s' % (thispage,)
1296 nav_items.append(self._pagerlink(thispage, text))
1294 nav_items.append(self._pagerlink(thispage, text))
1297
1295
1298 # Insert dots if there are pages between the displayed
1296 # Insert dots if there are pages between the displayed
1299 # page numbers and the end of the page range
1297 # page numbers and the end of the page range
1300 if self.last_page - rightmost_page > 1:
1298 if self.last_page - rightmost_page > 1:
1301 text = '..'
1299 text = '..'
1302 # Wrap in a SPAN tag if nolink_attr is set
1300 # Wrap in a SPAN tag if nolink_attr is set
1303 if self.dotdot_attr:
1301 if self.dotdot_attr:
1304 text = HTML.span(c=text, **self.dotdot_attr)
1302 text = HTML.span(c=text, **self.dotdot_attr)
1305 nav_items.append(text)
1303 nav_items.append(text)
1306
1304
1307 # Create a link to the very last page (unless we are on the last
1305 # Create a link to the very last page (unless we are on the last
1308 # page or there would be no need to insert '..' spacers)
1306 # page or there would be no need to insert '..' spacers)
1309 if self.page != self.last_page and rightmost_page < self.last_page:
1307 if self.page != self.last_page and rightmost_page < self.last_page:
1310 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1308 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1311
1309
1312 ## prerender links
1310 ## prerender links
1313 #_page_link = url.current()
1311 #_page_link = url.current()
1314 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1312 #nav_items.append(literal('<link rel="prerender" 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))))
1313 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1316 return self.separator.join(nav_items)
1314 return self.separator.join(nav_items)
1317
1315
1318 def pager(self, format='~2~', page_param='page', partial_param='partial',
1316 def pager(self, format='~2~', page_param='page', partial_param='partial',
1319 show_if_single_page=False, separator=' ', onclick=None,
1317 show_if_single_page=False, separator=' ', onclick=None,
1320 symbol_first='<<', symbol_last='>>',
1318 symbol_first='<<', symbol_last='>>',
1321 symbol_previous='<', symbol_next='>',
1319 symbol_previous='<', symbol_next='>',
1322 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1320 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1323 curpage_attr={'class': 'pager_curpage'},
1321 curpage_attr={'class': 'pager_curpage'},
1324 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1322 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1325
1323
1326 self.curpage_attr = curpage_attr
1324 self.curpage_attr = curpage_attr
1327 self.separator = separator
1325 self.separator = separator
1328 self.pager_kwargs = kwargs
1326 self.pager_kwargs = kwargs
1329 self.page_param = page_param
1327 self.page_param = page_param
1330 self.partial_param = partial_param
1328 self.partial_param = partial_param
1331 self.onclick = onclick
1329 self.onclick = onclick
1332 self.link_attr = link_attr
1330 self.link_attr = link_attr
1333 self.dotdot_attr = dotdot_attr
1331 self.dotdot_attr = dotdot_attr
1334
1332
1335 # Don't show navigator if there is no more than one page
1333 # Don't show navigator if there is no more than one page
1336 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1334 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1337 return ''
1335 return ''
1338
1336
1339 from string import Template
1337 from string import Template
1340 # Replace ~...~ in token format by range of pages
1338 # Replace ~...~ in token format by range of pages
1341 result = re.sub(r'~(\d+)~', self._range, format)
1339 result = re.sub(r'~(\d+)~', self._range, format)
1342
1340
1343 # Interpolate '%' variables
1341 # Interpolate '%' variables
1344 result = Template(result).safe_substitute({
1342 result = Template(result).safe_substitute({
1345 'first_page': self.first_page,
1343 'first_page': self.first_page,
1346 'last_page': self.last_page,
1344 'last_page': self.last_page,
1347 'page': self.page,
1345 'page': self.page,
1348 'page_count': self.page_count,
1346 'page_count': self.page_count,
1349 'items_per_page': self.items_per_page,
1347 'items_per_page': self.items_per_page,
1350 'first_item': self.first_item,
1348 'first_item': self.first_item,
1351 'last_item': self.last_item,
1349 'last_item': self.last_item,
1352 'item_count': self.item_count,
1350 'item_count': self.item_count,
1353 'link_first': self.page > self.first_page and \
1351 'link_first': self.page > self.first_page and \
1354 self._pagerlink(self.first_page, symbol_first) or '',
1352 self._pagerlink(self.first_page, symbol_first) or '',
1355 'link_last': self.page < self.last_page and \
1353 'link_last': self.page < self.last_page and \
1356 self._pagerlink(self.last_page, symbol_last) or '',
1354 self._pagerlink(self.last_page, symbol_last) or '',
1357 'link_previous': self.previous_page and \
1355 'link_previous': self.previous_page and \
1358 self._pagerlink(self.previous_page, symbol_previous) \
1356 self._pagerlink(self.previous_page, symbol_previous) \
1359 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1357 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1360 'link_next': self.next_page and \
1358 'link_next': self.next_page and \
1361 self._pagerlink(self.next_page, symbol_next) \
1359 self._pagerlink(self.next_page, symbol_next) \
1362 or HTML.span(symbol_next, class_="pg-next disabled")
1360 or HTML.span(symbol_next, class_="pg-next disabled")
1363 })
1361 })
1364
1362
1365 return literal(result)
1363 return literal(result)
1366
1364
1367
1365
1368 #==============================================================================
1366 #==============================================================================
1369 # REPO PAGER, PAGER FOR REPOSITORY
1367 # REPO PAGER, PAGER FOR REPOSITORY
1370 #==============================================================================
1368 #==============================================================================
1371 class RepoPage(Page):
1369 class RepoPage(Page):
1372
1370
1373 def __init__(self, collection, page=1, items_per_page=20,
1371 def __init__(self, collection, page=1, items_per_page=20,
1374 item_count=None, url=None, **kwargs):
1372 item_count=None, url=None, **kwargs):
1375
1373
1376 """Create a "RepoPage" instance. special pager for paging
1374 """Create a "RepoPage" instance. special pager for paging
1377 repository
1375 repository
1378 """
1376 """
1379 self._url_generator = url
1377 self._url_generator = url
1380
1378
1381 # Safe the kwargs class-wide so they can be used in the pager() method
1379 # Safe the kwargs class-wide so they can be used in the pager() method
1382 self.kwargs = kwargs
1380 self.kwargs = kwargs
1383
1381
1384 # Save a reference to the collection
1382 # Save a reference to the collection
1385 self.original_collection = collection
1383 self.original_collection = collection
1386
1384
1387 self.collection = collection
1385 self.collection = collection
1388
1386
1389 # The self.page is the number of the current page.
1387 # The self.page is the number of the current page.
1390 # The first page has the number 1!
1388 # The first page has the number 1!
1391 try:
1389 try:
1392 self.page = int(page) # make it int() if we get it as a string
1390 self.page = int(page) # make it int() if we get it as a string
1393 except (ValueError, TypeError):
1391 except (ValueError, TypeError):
1394 self.page = 1
1392 self.page = 1
1395
1393
1396 self.items_per_page = items_per_page
1394 self.items_per_page = items_per_page
1397
1395
1398 # Unless the user tells us how many items the collections has
1396 # Unless the user tells us how many items the collections has
1399 # we calculate that ourselves.
1397 # we calculate that ourselves.
1400 if item_count is not None:
1398 if item_count is not None:
1401 self.item_count = item_count
1399 self.item_count = item_count
1402 else:
1400 else:
1403 self.item_count = len(self.collection)
1401 self.item_count = len(self.collection)
1404
1402
1405 # Compute the number of the first and last available page
1403 # Compute the number of the first and last available page
1406 if self.item_count > 0:
1404 if self.item_count > 0:
1407 self.first_page = 1
1405 self.first_page = 1
1408 self.page_count = int(math.ceil(float(self.item_count) /
1406 self.page_count = int(math.ceil(float(self.item_count) /
1409 self.items_per_page))
1407 self.items_per_page))
1410 self.last_page = self.first_page + self.page_count - 1
1408 self.last_page = self.first_page + self.page_count - 1
1411
1409
1412 # Make sure that the requested page number is the range of
1410 # Make sure that the requested page number is the range of
1413 # valid pages
1411 # valid pages
1414 if self.page > self.last_page:
1412 if self.page > self.last_page:
1415 self.page = self.last_page
1413 self.page = self.last_page
1416 elif self.page < self.first_page:
1414 elif self.page < self.first_page:
1417 self.page = self.first_page
1415 self.page = self.first_page
1418
1416
1419 # Note: the number of items on this page can be less than
1417 # Note: the number of items on this page can be less than
1420 # items_per_page if the last page is not full
1418 # items_per_page if the last page is not full
1421 self.first_item = max(0, (self.item_count) - (self.page *
1419 self.first_item = max(0, (self.item_count) - (self.page *
1422 items_per_page))
1420 items_per_page))
1423 self.last_item = ((self.item_count - 1) - items_per_page *
1421 self.last_item = ((self.item_count - 1) - items_per_page *
1424 (self.page - 1))
1422 (self.page - 1))
1425
1423
1426 self.items = list(self.collection[self.first_item:self.last_item + 1])
1424 self.items = list(self.collection[self.first_item:self.last_item + 1])
1427
1425
1428 # Links to previous and next page
1426 # Links to previous and next page
1429 if self.page > self.first_page:
1427 if self.page > self.first_page:
1430 self.previous_page = self.page - 1
1428 self.previous_page = self.page - 1
1431 else:
1429 else:
1432 self.previous_page = None
1430 self.previous_page = None
1433
1431
1434 if self.page < self.last_page:
1432 if self.page < self.last_page:
1435 self.next_page = self.page + 1
1433 self.next_page = self.page + 1
1436 else:
1434 else:
1437 self.next_page = None
1435 self.next_page = None
1438
1436
1439 # No items available
1437 # No items available
1440 else:
1438 else:
1441 self.first_page = None
1439 self.first_page = None
1442 self.page_count = 0
1440 self.page_count = 0
1443 self.last_page = None
1441 self.last_page = None
1444 self.first_item = None
1442 self.first_item = None
1445 self.last_item = None
1443 self.last_item = None
1446 self.previous_page = None
1444 self.previous_page = None
1447 self.next_page = None
1445 self.next_page = None
1448 self.items = []
1446 self.items = []
1449
1447
1450 # This is a subclass of the 'list' type. Initialise the list now.
1448 # This is a subclass of the 'list' type. Initialise the list now.
1451 list.__init__(self, reversed(self.items))
1449 list.__init__(self, reversed(self.items))
1452
1450
1453
1451
1454 def changed_tooltip(nodes):
1452 def changed_tooltip(nodes):
1455 """
1453 """
1456 Generates a html string for changed nodes in commit page.
1454 Generates a html string for changed nodes in commit page.
1457 It limits the output to 30 entries
1455 It limits the output to 30 entries
1458
1456
1459 :param nodes: LazyNodesGenerator
1457 :param nodes: LazyNodesGenerator
1460 """
1458 """
1461 if nodes:
1459 if nodes:
1462 pref = ': <br/> '
1460 pref = ': <br/> '
1463 suf = ''
1461 suf = ''
1464 if len(nodes) > 30:
1462 if len(nodes) > 30:
1465 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1463 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1466 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1464 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1467 for x in nodes[:30]]) + suf)
1465 for x in nodes[:30]]) + suf)
1468 else:
1466 else:
1469 return ': ' + _('No Files')
1467 return ': ' + _('No Files')
1470
1468
1471
1469
1472 def breadcrumb_repo_link(repo):
1470 def breadcrumb_repo_link(repo):
1473 """
1471 """
1474 Makes a breadcrumbs path link to repo
1472 Makes a breadcrumbs path link to repo
1475
1473
1476 ex::
1474 ex::
1477 group >> subgroup >> repo
1475 group >> subgroup >> repo
1478
1476
1479 :param repo: a Repository instance
1477 :param repo: a Repository instance
1480 """
1478 """
1481
1479
1482 path = [
1480 path = [
1483 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1481 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1484 for group in repo.groups_with_parents
1482 for group in repo.groups_with_parents
1485 ] + [
1483 ] + [
1486 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1484 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1487 ]
1485 ]
1488
1486
1489 return literal(' &raquo; '.join(path))
1487 return literal(' &raquo; '.join(path))
1490
1488
1491
1489
1492 def format_byte_size_binary(file_size):
1490 def format_byte_size_binary(file_size):
1493 """
1491 """
1494 Formats file/folder sizes to standard.
1492 Formats file/folder sizes to standard.
1495 """
1493 """
1496 formatted_size = format_byte_size(file_size, binary=True)
1494 formatted_size = format_byte_size(file_size, binary=True)
1497 return formatted_size
1495 return formatted_size
1498
1496
1499
1497
1500 def fancy_file_stats(stats):
1498 def fancy_file_stats(stats):
1501 """
1499 """
1502 Displays a fancy two colored bar for number of added/deleted
1500 Displays a fancy two colored bar for number of added/deleted
1503 lines of code on file
1501 lines of code on file
1504
1502
1505 :param stats: two element list of added/deleted lines of code
1503 :param stats: two element list of added/deleted lines of code
1506 """
1504 """
1507 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1505 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1508 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1506 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1509
1507
1510 def cgen(l_type, a_v, d_v):
1508 def cgen(l_type, a_v, d_v):
1511 mapping = {'tr': 'top-right-rounded-corner-mid',
1509 mapping = {'tr': 'top-right-rounded-corner-mid',
1512 'tl': 'top-left-rounded-corner-mid',
1510 'tl': 'top-left-rounded-corner-mid',
1513 'br': 'bottom-right-rounded-corner-mid',
1511 'br': 'bottom-right-rounded-corner-mid',
1514 'bl': 'bottom-left-rounded-corner-mid'}
1512 'bl': 'bottom-left-rounded-corner-mid'}
1515 map_getter = lambda x: mapping[x]
1513 map_getter = lambda x: mapping[x]
1516
1514
1517 if l_type == 'a' and d_v:
1515 if l_type == 'a' and d_v:
1518 #case when added and deleted are present
1516 #case when added and deleted are present
1519 return ' '.join(map(map_getter, ['tl', 'bl']))
1517 return ' '.join(map(map_getter, ['tl', 'bl']))
1520
1518
1521 if l_type == 'a' and not d_v:
1519 if l_type == 'a' and not d_v:
1522 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1520 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1523
1521
1524 if l_type == 'd' and a_v:
1522 if l_type == 'd' and a_v:
1525 return ' '.join(map(map_getter, ['tr', 'br']))
1523 return ' '.join(map(map_getter, ['tr', 'br']))
1526
1524
1527 if l_type == 'd' and not a_v:
1525 if l_type == 'd' and not a_v:
1528 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1526 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1529
1527
1530 a, d = stats['added'], stats['deleted']
1528 a, d = stats['added'], stats['deleted']
1531 width = 100
1529 width = 100
1532
1530
1533 if stats['binary']: # binary operations like chmod/rename etc
1531 if stats['binary']: # binary operations like chmod/rename etc
1534 lbl = []
1532 lbl = []
1535 bin_op = 0 # undefined
1533 bin_op = 0 # undefined
1536
1534
1537 # prefix with bin for binary files
1535 # prefix with bin for binary files
1538 if BIN_FILENODE in stats['ops']:
1536 if BIN_FILENODE in stats['ops']:
1539 lbl += ['bin']
1537 lbl += ['bin']
1540
1538
1541 if NEW_FILENODE in stats['ops']:
1539 if NEW_FILENODE in stats['ops']:
1542 lbl += [_('new file')]
1540 lbl += [_('new file')]
1543 bin_op = NEW_FILENODE
1541 bin_op = NEW_FILENODE
1544 elif MOD_FILENODE in stats['ops']:
1542 elif MOD_FILENODE in stats['ops']:
1545 lbl += [_('mod')]
1543 lbl += [_('mod')]
1546 bin_op = MOD_FILENODE
1544 bin_op = MOD_FILENODE
1547 elif DEL_FILENODE in stats['ops']:
1545 elif DEL_FILENODE in stats['ops']:
1548 lbl += [_('del')]
1546 lbl += [_('del')]
1549 bin_op = DEL_FILENODE
1547 bin_op = DEL_FILENODE
1550 elif RENAMED_FILENODE in stats['ops']:
1548 elif RENAMED_FILENODE in stats['ops']:
1551 lbl += [_('rename')]
1549 lbl += [_('rename')]
1552 bin_op = RENAMED_FILENODE
1550 bin_op = RENAMED_FILENODE
1553
1551
1554 # chmod can go with other operations, so we add a + to lbl if needed
1552 # chmod can go with other operations, so we add a + to lbl if needed
1555 if CHMOD_FILENODE in stats['ops']:
1553 if CHMOD_FILENODE in stats['ops']:
1556 lbl += [_('chmod')]
1554 lbl += [_('chmod')]
1557 if bin_op == 0:
1555 if bin_op == 0:
1558 bin_op = CHMOD_FILENODE
1556 bin_op = CHMOD_FILENODE
1559
1557
1560 lbl = '+'.join(lbl)
1558 lbl = '+'.join(lbl)
1561 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1559 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1562 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1560 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1563 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1561 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1564 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1562 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1565
1563
1566 t = stats['added'] + stats['deleted']
1564 t = stats['added'] + stats['deleted']
1567 unit = float(width) / (t or 1)
1565 unit = float(width) / (t or 1)
1568
1566
1569 # needs > 9% of width to be visible or 0 to be hidden
1567 # needs > 9% of width to be visible or 0 to be hidden
1570 a_p = max(9, unit * a) if a > 0 else 0
1568 a_p = max(9, unit * a) if a > 0 else 0
1571 d_p = max(9, unit * d) if d > 0 else 0
1569 d_p = max(9, unit * d) if d > 0 else 0
1572 p_sum = a_p + d_p
1570 p_sum = a_p + d_p
1573
1571
1574 if p_sum > width:
1572 if p_sum > width:
1575 #adjust the percentage to be == 100% since we adjusted to 9
1573 #adjust the percentage to be == 100% since we adjusted to 9
1576 if a_p > d_p:
1574 if a_p > d_p:
1577 a_p = a_p - (p_sum - width)
1575 a_p = a_p - (p_sum - width)
1578 else:
1576 else:
1579 d_p = d_p - (p_sum - width)
1577 d_p = d_p - (p_sum - width)
1580
1578
1581 a_v = a if a > 0 else ''
1579 a_v = a if a > 0 else ''
1582 d_v = d if d > 0 else ''
1580 d_v = d if d > 0 else ''
1583
1581
1584 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1582 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1585 cgen('a', a_v, d_v), a_p, a_v
1583 cgen('a', a_v, d_v), a_p, a_v
1586 )
1584 )
1587 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1585 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1588 cgen('d', a_v, d_v), d_p, d_v
1586 cgen('d', a_v, d_v), d_p, d_v
1589 )
1587 )
1590 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1588 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1591
1589
1592
1590
1593 def urlify_text(text_, safe=True):
1591 def urlify_text(text_, safe=True):
1594 """
1592 """
1595 Extrac urls from text and make html links out of them
1593 Extrac urls from text and make html links out of them
1596
1594
1597 :param text_:
1595 :param text_:
1598 """
1596 """
1599
1597
1600 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1598 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1601 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1599 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1602
1600
1603 def url_func(match_obj):
1601 def url_func(match_obj):
1604 url_full = match_obj.groups()[0]
1602 url_full = match_obj.groups()[0]
1605 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1603 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1606 _newtext = url_pat.sub(url_func, text_)
1604 _newtext = url_pat.sub(url_func, text_)
1607 if safe:
1605 if safe:
1608 return literal(_newtext)
1606 return literal(_newtext)
1609 return _newtext
1607 return _newtext
1610
1608
1611
1609
1612 def urlify_commits(text_, repository):
1610 def urlify_commits(text_, repository):
1613 """
1611 """
1614 Extract commit ids from text and make link from them
1612 Extract commit ids from text and make link from them
1615
1613
1616 :param text_:
1614 :param text_:
1617 :param repository: repo name to build the URL with
1615 :param repository: repo name to build the URL with
1618 """
1616 """
1619 from pylons import url # doh, we need to re-import url to mock it later
1617 from pylons import url # doh, we need to re-import url to mock it later
1620 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1618 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1621
1619
1622 def url_func(match_obj):
1620 def url_func(match_obj):
1623 commit_id = match_obj.groups()[1]
1621 commit_id = match_obj.groups()[1]
1624 pref = match_obj.groups()[0]
1622 pref = match_obj.groups()[0]
1625 suf = match_obj.groups()[2]
1623 suf = match_obj.groups()[2]
1626
1624
1627 tmpl = (
1625 tmpl = (
1628 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1626 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1629 '%(commit_id)s</a>%(suf)s'
1627 '%(commit_id)s</a>%(suf)s'
1630 )
1628 )
1631 return tmpl % {
1629 return tmpl % {
1632 'pref': pref,
1630 'pref': pref,
1633 'cls': 'revision-link',
1631 'cls': 'revision-link',
1634 'url': url('changeset_home', repo_name=repository,
1632 'url': url('changeset_home', repo_name=repository,
1635 revision=commit_id, qualified=True),
1633 revision=commit_id, qualified=True),
1636 'commit_id': commit_id,
1634 'commit_id': commit_id,
1637 'suf': suf
1635 'suf': suf
1638 }
1636 }
1639
1637
1640 newtext = URL_PAT.sub(url_func, text_)
1638 newtext = URL_PAT.sub(url_func, text_)
1641
1639
1642 return newtext
1640 return newtext
1643
1641
1644
1642
1645 def _process_url_func(match_obj, repo_name, uid, entry,
1643 def _process_url_func(match_obj, repo_name, uid, entry,
1646 return_raw_data=False):
1644 return_raw_data=False):
1647 pref = ''
1645 pref = ''
1648 if match_obj.group().startswith(' '):
1646 if match_obj.group().startswith(' '):
1649 pref = ' '
1647 pref = ' '
1650
1648
1651 issue_id = ''.join(match_obj.groups())
1649 issue_id = ''.join(match_obj.groups())
1652 tmpl = (
1650 tmpl = (
1653 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1651 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1654 '%(issue-prefix)s%(id-repr)s'
1652 '%(issue-prefix)s%(id-repr)s'
1655 '</a>')
1653 '</a>')
1656
1654
1657 (repo_name_cleaned,
1655 (repo_name_cleaned,
1658 parent_group_name) = RepoGroupModel().\
1656 parent_group_name) = RepoGroupModel().\
1659 _get_group_name_and_parent(repo_name)
1657 _get_group_name_and_parent(repo_name)
1660
1658
1661 # variables replacement
1659 # variables replacement
1662 named_vars = {
1660 named_vars = {
1663 'id': issue_id,
1661 'id': issue_id,
1664 'repo': repo_name,
1662 'repo': repo_name,
1665 'repo_name': repo_name_cleaned,
1663 'repo_name': repo_name_cleaned,
1666 'group_name': parent_group_name
1664 'group_name': parent_group_name
1667 }
1665 }
1668 # named regex variables
1666 # named regex variables
1669 named_vars.update(match_obj.groupdict())
1667 named_vars.update(match_obj.groupdict())
1670 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1668 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1671
1669
1672 data = {
1670 data = {
1673 'pref': pref,
1671 'pref': pref,
1674 'cls': 'issue-tracker-link',
1672 'cls': 'issue-tracker-link',
1675 'url': _url,
1673 'url': _url,
1676 'id-repr': issue_id,
1674 'id-repr': issue_id,
1677 'issue-prefix': entry['pref'],
1675 'issue-prefix': entry['pref'],
1678 'serv': entry['url'],
1676 'serv': entry['url'],
1679 }
1677 }
1680 if return_raw_data:
1678 if return_raw_data:
1681 return {
1679 return {
1682 'id': issue_id,
1680 'id': issue_id,
1683 'url': _url
1681 'url': _url
1684 }
1682 }
1685 return tmpl % data
1683 return tmpl % data
1686
1684
1687
1685
1688 def process_patterns(text_string, repo_name, config=None):
1686 def process_patterns(text_string, repo_name, config=None):
1689 repo = None
1687 repo = None
1690 if repo_name:
1688 if repo_name:
1691 # Retrieving repo_name to avoid invalid repo_name to explode on
1689 # Retrieving repo_name to avoid invalid repo_name to explode on
1692 # IssueTrackerSettingsModel but still passing invalid name further down
1690 # IssueTrackerSettingsModel but still passing invalid name further down
1693 repo = Repository.get_by_repo_name(repo_name, cache=True)
1691 repo = Repository.get_by_repo_name(repo_name, cache=True)
1694
1692
1695 settings_model = IssueTrackerSettingsModel(repo=repo)
1693 settings_model = IssueTrackerSettingsModel(repo=repo)
1696 active_entries = settings_model.get_settings(cache=True)
1694 active_entries = settings_model.get_settings(cache=True)
1697
1695
1698 issues_data = []
1696 issues_data = []
1699 newtext = text_string
1697 newtext = text_string
1700 for uid, entry in active_entries.items():
1698 for uid, entry in active_entries.items():
1701 log.debug('found issue tracker entry with uid %s' % (uid,))
1699 log.debug('found issue tracker entry with uid %s' % (uid,))
1702
1700
1703 if not (entry['pat'] and entry['url']):
1701 if not (entry['pat'] and entry['url']):
1704 log.debug('skipping due to missing data')
1702 log.debug('skipping due to missing data')
1705 continue
1703 continue
1706
1704
1707 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1705 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1708 % (uid, entry['pat'], entry['url'], entry['pref']))
1706 % (uid, entry['pat'], entry['url'], entry['pref']))
1709
1707
1710 try:
1708 try:
1711 pattern = re.compile(r'%s' % entry['pat'])
1709 pattern = re.compile(r'%s' % entry['pat'])
1712 except re.error:
1710 except re.error:
1713 log.exception(
1711 log.exception(
1714 'issue tracker pattern: `%s` failed to compile',
1712 'issue tracker pattern: `%s` failed to compile',
1715 entry['pat'])
1713 entry['pat'])
1716 continue
1714 continue
1717
1715
1718 data_func = partial(
1716 data_func = partial(
1719 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1717 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1720 return_raw_data=True)
1718 return_raw_data=True)
1721
1719
1722 for match_obj in pattern.finditer(text_string):
1720 for match_obj in pattern.finditer(text_string):
1723 issues_data.append(data_func(match_obj))
1721 issues_data.append(data_func(match_obj))
1724
1722
1725 url_func = partial(
1723 url_func = partial(
1726 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1724 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1727
1725
1728 newtext = pattern.sub(url_func, newtext)
1726 newtext = pattern.sub(url_func, newtext)
1729 log.debug('processed prefix:uid `%s`' % (uid,))
1727 log.debug('processed prefix:uid `%s`' % (uid,))
1730
1728
1731 return newtext, issues_data
1729 return newtext, issues_data
1732
1730
1733
1731
1734 def urlify_commit_message(commit_text, repository=None):
1732 def urlify_commit_message(commit_text, repository=None):
1735 """
1733 """
1736 Parses given text message and makes proper links.
1734 Parses given text message and makes proper links.
1737 issues are linked to given issue-server, and rest is a commit link
1735 issues are linked to given issue-server, and rest is a commit link
1738
1736
1739 :param commit_text:
1737 :param commit_text:
1740 :param repository:
1738 :param repository:
1741 """
1739 """
1742 from pylons import url # doh, we need to re-import url to mock it later
1740 from pylons import url # doh, we need to re-import url to mock it later
1743
1741
1744 def escaper(string):
1742 def escaper(string):
1745 return string.replace('<', '&lt;').replace('>', '&gt;')
1743 return string.replace('<', '&lt;').replace('>', '&gt;')
1746
1744
1747 newtext = escaper(commit_text)
1745 newtext = escaper(commit_text)
1748
1746
1749 # extract http/https links and make them real urls
1747 # extract http/https links and make them real urls
1750 newtext = urlify_text(newtext, safe=False)
1748 newtext = urlify_text(newtext, safe=False)
1751
1749
1752 # urlify commits - extract commit ids and make link out of them, if we have
1750 # urlify commits - extract commit ids and make link out of them, if we have
1753 # the scope of repository present.
1751 # the scope of repository present.
1754 if repository:
1752 if repository:
1755 newtext = urlify_commits(newtext, repository)
1753 newtext = urlify_commits(newtext, repository)
1756
1754
1757 # process issue tracker patterns
1755 # process issue tracker patterns
1758 newtext, issues = process_patterns(newtext, repository or '')
1756 newtext, issues = process_patterns(newtext, repository or '')
1759
1757
1760 return literal(newtext)
1758 return literal(newtext)
1761
1759
1762
1760
1763 def rst(source, mentions=False):
1761 def rst(source, mentions=False):
1764 return literal('<div class="rst-block">%s</div>' %
1762 return literal('<div class="rst-block">%s</div>' %
1765 MarkupRenderer.rst(source, mentions=mentions))
1763 MarkupRenderer.rst(source, mentions=mentions))
1766
1764
1767
1765
1768 def markdown(source, mentions=False):
1766 def markdown(source, mentions=False):
1769 return literal('<div class="markdown-block">%s</div>' %
1767 return literal('<div class="markdown-block">%s</div>' %
1770 MarkupRenderer.markdown(source, flavored=True,
1768 MarkupRenderer.markdown(source, flavored=True,
1771 mentions=mentions))
1769 mentions=mentions))
1772
1770
1773 def renderer_from_filename(filename, exclude=None):
1771 def renderer_from_filename(filename, exclude=None):
1774 return MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1772 return MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1775
1773
1776
1774
1777 def render(source, renderer='rst', mentions=False):
1775 def render(source, renderer='rst', mentions=False):
1778 if renderer == 'rst':
1776 if renderer == 'rst':
1779 return rst(source, mentions=mentions)
1777 return rst(source, mentions=mentions)
1780 if renderer == 'markdown':
1778 if renderer == 'markdown':
1781 return markdown(source, mentions=mentions)
1779 return markdown(source, mentions=mentions)
1782
1780
1783
1781
1784 def commit_status(repo, commit_id):
1782 def commit_status(repo, commit_id):
1785 return ChangesetStatusModel().get_status(repo, commit_id)
1783 return ChangesetStatusModel().get_status(repo, commit_id)
1786
1784
1787
1785
1788 def commit_status_lbl(commit_status):
1786 def commit_status_lbl(commit_status):
1789 return dict(ChangesetStatus.STATUSES).get(commit_status)
1787 return dict(ChangesetStatus.STATUSES).get(commit_status)
1790
1788
1791
1789
1792 def commit_time(repo_name, commit_id):
1790 def commit_time(repo_name, commit_id):
1793 repo = Repository.get_by_repo_name(repo_name)
1791 repo = Repository.get_by_repo_name(repo_name)
1794 commit = repo.get_commit(commit_id=commit_id)
1792 commit = repo.get_commit(commit_id=commit_id)
1795 return commit.date
1793 return commit.date
1796
1794
1797
1795
1798 def get_permission_name(key):
1796 def get_permission_name(key):
1799 return dict(Permission.PERMS).get(key)
1797 return dict(Permission.PERMS).get(key)
1800
1798
1801
1799
1802 def journal_filter_help():
1800 def journal_filter_help():
1803 return _(
1801 return _(
1804 'Example filter terms:\n' +
1802 'Example filter terms:\n' +
1805 ' repository:vcs\n' +
1803 ' repository:vcs\n' +
1806 ' username:marcin\n' +
1804 ' username:marcin\n' +
1807 ' action:*push*\n' +
1805 ' action:*push*\n' +
1808 ' ip:127.0.0.1\n' +
1806 ' ip:127.0.0.1\n' +
1809 ' date:20120101\n' +
1807 ' date:20120101\n' +
1810 ' date:[20120101100000 TO 20120102]\n' +
1808 ' date:[20120101100000 TO 20120102]\n' +
1811 '\n' +
1809 '\n' +
1812 'Generate wildcards using \'*\' character:\n' +
1810 'Generate wildcards using \'*\' character:\n' +
1813 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1811 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1814 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1812 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1815 '\n' +
1813 '\n' +
1816 'Optional AND / OR operators in queries\n' +
1814 'Optional AND / OR operators in queries\n' +
1817 ' "repository:vcs OR repository:test"\n' +
1815 ' "repository:vcs OR repository:test"\n' +
1818 ' "username:test AND repository:test*"\n'
1816 ' "username:test AND repository:test*"\n'
1819 )
1817 )
1820
1818
1821
1819
1822 def not_mapped_error(repo_name):
1820 def not_mapped_error(repo_name):
1823 flash(_('%s repository is not mapped to db perhaps'
1821 flash(_('%s repository is not mapped to db perhaps'
1824 ' it was created or renamed from the filesystem'
1822 ' it was created or renamed from the filesystem'
1825 ' please run the application again'
1823 ' please run the application again'
1826 ' in order to rescan repositories') % repo_name, category='error')
1824 ' in order to rescan repositories') % repo_name, category='error')
1827
1825
1828
1826
1829 def ip_range(ip_addr):
1827 def ip_range(ip_addr):
1830 from rhodecode.model.db import UserIpMap
1828 from rhodecode.model.db import UserIpMap
1831 s, e = UserIpMap._get_ip_range(ip_addr)
1829 s, e = UserIpMap._get_ip_range(ip_addr)
1832 return '%s - %s' % (s, e)
1830 return '%s - %s' % (s, e)
1833
1831
1834
1832
1835 def form(url, method='post', needs_csrf_token=True, **attrs):
1833 def form(url, method='post', needs_csrf_token=True, **attrs):
1836 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1834 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1837 if method.lower() != 'get' and needs_csrf_token:
1835 if method.lower() != 'get' and needs_csrf_token:
1838 raise Exception(
1836 raise Exception(
1839 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1837 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1840 'CSRF token. If the endpoint does not require such token you can ' +
1838 'CSRF token. If the endpoint does not require such token you can ' +
1841 'explicitly set the parameter needs_csrf_token to false.')
1839 'explicitly set the parameter needs_csrf_token to false.')
1842
1840
1843 return wh_form(url, method=method, **attrs)
1841 return wh_form(url, method=method, **attrs)
1844
1842
1845
1843
1846 def secure_form(url, method="POST", multipart=False, **attrs):
1844 def secure_form(url, method="POST", multipart=False, **attrs):
1847 """Start a form tag that points the action to an url. This
1845 """Start a form tag that points the action to an url. This
1848 form tag will also include the hidden field containing
1846 form tag will also include the hidden field containing
1849 the auth token.
1847 the auth token.
1850
1848
1851 The url options should be given either as a string, or as a
1849 The url options should be given either as a string, or as a
1852 ``url()`` function. The method for the form defaults to POST.
1850 ``url()`` function. The method for the form defaults to POST.
1853
1851
1854 Options:
1852 Options:
1855
1853
1856 ``multipart``
1854 ``multipart``
1857 If set to True, the enctype is set to "multipart/form-data".
1855 If set to True, the enctype is set to "multipart/form-data".
1858 ``method``
1856 ``method``
1859 The method to use when submitting the form, usually either
1857 The method to use when submitting the form, usually either
1860 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1858 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1861 hidden input with name _method is added to simulate the verb
1859 hidden input with name _method is added to simulate the verb
1862 over POST.
1860 over POST.
1863
1861
1864 """
1862 """
1865 from webhelpers.pylonslib.secure_form import insecure_form
1863 from webhelpers.pylonslib.secure_form import insecure_form
1866 from rhodecode.lib.auth import get_csrf_token, csrf_token_key
1864 from rhodecode.lib.auth import get_csrf_token, csrf_token_key
1867 form = insecure_form(url, method, multipart, **attrs)
1865 form = insecure_form(url, method, multipart, **attrs)
1868 token = HTML.div(hidden(csrf_token_key, get_csrf_token()), style="display: none;")
1866 token = HTML.div(hidden(csrf_token_key, get_csrf_token()), style="display: none;")
1869 return literal("%s\n%s" % (form, token))
1867 return literal("%s\n%s" % (form, token))
1870
1868
1871 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1869 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1872 select_html = select(name, selected, options, **attrs)
1870 select_html = select(name, selected, options, **attrs)
1873 select2 = """
1871 select2 = """
1874 <script>
1872 <script>
1875 $(document).ready(function() {
1873 $(document).ready(function() {
1876 $('#%s').select2({
1874 $('#%s').select2({
1877 containerCssClass: 'drop-menu',
1875 containerCssClass: 'drop-menu',
1878 dropdownCssClass: 'drop-menu-dropdown',
1876 dropdownCssClass: 'drop-menu-dropdown',
1879 dropdownAutoWidth: true%s
1877 dropdownAutoWidth: true%s
1880 });
1878 });
1881 });
1879 });
1882 </script>
1880 </script>
1883 """
1881 """
1884 filter_option = """,
1882 filter_option = """,
1885 minimumResultsForSearch: -1
1883 minimumResultsForSearch: -1
1886 """
1884 """
1887 input_id = attrs.get('id') or name
1885 input_id = attrs.get('id') or name
1888 filter_enabled = "" if enable_filter else filter_option
1886 filter_enabled = "" if enable_filter else filter_option
1889 select_script = literal(select2 % (input_id, filter_enabled))
1887 select_script = literal(select2 % (input_id, filter_enabled))
1890
1888
1891 return literal(select_html+select_script)
1889 return literal(select_html+select_script)
1892
1890
1893
1891
1894 def get_visual_attr(tmpl_context_var, attr_name):
1892 def get_visual_attr(tmpl_context_var, attr_name):
1895 """
1893 """
1896 A safe way to get a variable from visual variable of template context
1894 A safe way to get a variable from visual variable of template context
1897
1895
1898 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1896 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1899 :param attr_name: name of the attribute we fetch from the c.visual
1897 :param attr_name: name of the attribute we fetch from the c.visual
1900 """
1898 """
1901 visual = getattr(tmpl_context_var, 'visual', None)
1899 visual = getattr(tmpl_context_var, 'visual', None)
1902 if not visual:
1900 if not visual:
1903 return
1901 return
1904 else:
1902 else:
1905 return getattr(visual, attr_name, None)
1903 return getattr(visual, attr_name, None)
1906
1904
1907
1905
1908 def get_last_path_part(file_node):
1906 def get_last_path_part(file_node):
1909 if not file_node.path:
1907 if not file_node.path:
1910 return u''
1908 return u''
1911
1909
1912 path = safe_unicode(file_node.path.split('/')[-1])
1910 path = safe_unicode(file_node.path.split('/')[-1])
1913 return u'../' + path
1911 return u'../' + path
1914
1912
1915
1913
1916 def route_path(*args, **kwds):
1914 def route_path(*args, **kwds):
1917 """
1915 """
1918 Wrapper around pyramids `route_path` function. It is used to generate
1916 Wrapper around pyramids `route_path` function. It is used to generate
1919 URLs from within pylons views or templates. This will be removed when
1917 URLs from within pylons views or templates. This will be removed when
1920 pyramid migration if finished.
1918 pyramid migration if finished.
1921 """
1919 """
1922 req = get_current_request()
1920 req = get_current_request()
1923 return req.route_path(*args, **kwds)
1921 return req.route_path(*args, **kwds)
1924
1922
1925
1923
1926 def resource_path(*args, **kwds):
1924 def resource_path(*args, **kwds):
1927 """
1925 """
1928 Wrapper around pyramids `route_path` function. It is used to generate
1926 Wrapper around pyramids `route_path` function. It is used to generate
1929 URLs from within pylons views or templates. This will be removed when
1927 URLs from within pylons views or templates. This will be removed when
1930 pyramid migration if finished.
1928 pyramid migration if finished.
1931 """
1929 """
1932 req = get_current_request()
1930 req = get_current_request()
1933 return req.resource_path(*args, **kwds)
1931 return req.resource_path(*args, **kwds)
General Comments 0
You need to be logged in to leave comments. Login now