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