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