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