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