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