##// END OF EJS Templates
pull-requests: fixed multiple repetitions of referenced tickets in pull requests summary sidebar.
milka -
r4650:5d2569e3 default
parent child Browse files
Show More
@@ -1,2117 +1,2148 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import base64
27 import base64
28 import collections
28
29
29 import os
30 import os
30 import random
31 import random
31 import hashlib
32 import hashlib
32 import StringIO
33 import StringIO
33 import textwrap
34 import textwrap
34 import urllib
35 import urllib
35 import math
36 import math
36 import logging
37 import logging
37 import re
38 import re
38 import time
39 import time
39 import string
40 import string
40 import hashlib
41 import hashlib
41 import regex
42 import regex
42 from collections import OrderedDict
43 from collections import OrderedDict
43
44
44 import pygments
45 import pygments
45 import itertools
46 import itertools
46 import fnmatch
47 import fnmatch
47 import bleach
48 import bleach
48
49
49 from pyramid import compat
50 from pyramid import compat
50 from datetime import datetime
51 from datetime import datetime
51 from functools import partial
52 from functools import partial
52 from pygments.formatters.html import HtmlFormatter
53 from pygments.formatters.html import HtmlFormatter
53 from pygments.lexers import (
54 from pygments.lexers import (
54 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
55 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
55
56
56 from pyramid.threadlocal import get_current_request
57 from pyramid.threadlocal import get_current_request
57 from tempita import looper
58 from tempita import looper
58 from webhelpers2.html import literal, HTML, escape
59 from webhelpers2.html import literal, HTML, escape
59 from webhelpers2.html._autolink import _auto_link_urls
60 from webhelpers2.html._autolink import _auto_link_urls
60 from webhelpers2.html.tools import (
61 from webhelpers2.html.tools import (
61 button_to, highlight, js_obfuscate, strip_links, strip_tags)
62 button_to, highlight, js_obfuscate, strip_links, strip_tags)
62
63
63 from webhelpers2.text import (
64 from webhelpers2.text import (
64 chop_at, collapse, convert_accented_entities,
65 chop_at, collapse, convert_accented_entities,
65 convert_misc_entities, lchop, plural, rchop, remove_formatting,
66 convert_misc_entities, lchop, plural, rchop, remove_formatting,
66 replace_whitespace, urlify, truncate, wrap_paragraphs)
67 replace_whitespace, urlify, truncate, wrap_paragraphs)
67 from webhelpers2.date import time_ago_in_words
68 from webhelpers2.date import time_ago_in_words
68
69
69 from webhelpers2.html.tags import (
70 from webhelpers2.html.tags import (
70 _input, NotGiven, _make_safe_id_component as safeid,
71 _input, NotGiven, _make_safe_id_component as safeid,
71 form as insecure_form,
72 form as insecure_form,
72 auto_discovery_link, checkbox, end_form, file,
73 auto_discovery_link, checkbox, end_form, file,
73 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
74 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
74 select as raw_select, stylesheet_link, submit, text, password, textarea,
75 select as raw_select, stylesheet_link, submit, text, password, textarea,
75 ul, radio, Options)
76 ul, radio, Options)
76
77
77 from webhelpers2.number import format_byte_size
78 from webhelpers2.number import format_byte_size
78
79
79 from rhodecode.lib.action_parser import action_parser
80 from rhodecode.lib.action_parser import action_parser
80 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
81 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
81 from rhodecode.lib.ext_json import json
82 from rhodecode.lib.ext_json import json
82 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
83 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
83 from rhodecode.lib.utils2 import (
84 from rhodecode.lib.utils2 import (
84 str2bool, safe_unicode, safe_str,
85 str2bool, safe_unicode, safe_str,
85 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
86 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
86 AttributeDict, safe_int, md5, md5_safe, get_host_info)
87 AttributeDict, safe_int, md5, md5_safe, get_host_info)
87 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
88 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
88 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
89 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
89 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
90 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
90 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
91 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
91 from rhodecode.lib.index.search_utils import get_matching_line_offsets
92 from rhodecode.lib.index.search_utils import get_matching_line_offsets
92 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
93 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
93 from rhodecode.model.changeset_status import ChangesetStatusModel
94 from rhodecode.model.changeset_status import ChangesetStatusModel
94 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
95 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
95 from rhodecode.model.repo_group import RepoGroupModel
96 from rhodecode.model.repo_group import RepoGroupModel
96 from rhodecode.model.settings import IssueTrackerSettingsModel
97 from rhodecode.model.settings import IssueTrackerSettingsModel
97
98
98
99
99 log = logging.getLogger(__name__)
100 log = logging.getLogger(__name__)
100
101
101
102
102 DEFAULT_USER = User.DEFAULT_USER
103 DEFAULT_USER = User.DEFAULT_USER
103 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
104 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
104
105
105
106
106 def asset(path, ver=None, **kwargs):
107 def asset(path, ver=None, **kwargs):
107 """
108 """
108 Helper to generate a static asset file path for rhodecode assets
109 Helper to generate a static asset file path for rhodecode assets
109
110
110 eg. h.asset('images/image.png', ver='3923')
111 eg. h.asset('images/image.png', ver='3923')
111
112
112 :param path: path of asset
113 :param path: path of asset
113 :param ver: optional version query param to append as ?ver=
114 :param ver: optional version query param to append as ?ver=
114 """
115 """
115 request = get_current_request()
116 request = get_current_request()
116 query = {}
117 query = {}
117 query.update(kwargs)
118 query.update(kwargs)
118 if ver:
119 if ver:
119 query = {'ver': ver}
120 query = {'ver': ver}
120 return request.static_path(
121 return request.static_path(
121 'rhodecode:public/{}'.format(path), _query=query)
122 'rhodecode:public/{}'.format(path), _query=query)
122
123
123
124
124 default_html_escape_table = {
125 default_html_escape_table = {
125 ord('&'): u'&amp;',
126 ord('&'): u'&amp;',
126 ord('<'): u'&lt;',
127 ord('<'): u'&lt;',
127 ord('>'): u'&gt;',
128 ord('>'): u'&gt;',
128 ord('"'): u'&quot;',
129 ord('"'): u'&quot;',
129 ord("'"): u'&#39;',
130 ord("'"): u'&#39;',
130 }
131 }
131
132
132
133
133 def html_escape(text, html_escape_table=default_html_escape_table):
134 def html_escape(text, html_escape_table=default_html_escape_table):
134 """Produce entities within text."""
135 """Produce entities within text."""
135 return text.translate(html_escape_table)
136 return text.translate(html_escape_table)
136
137
137
138
138 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
139 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
139 """
140 """
140 Truncate string ``s`` at the first occurrence of ``sub``.
141 Truncate string ``s`` at the first occurrence of ``sub``.
141
142
142 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
143 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
143 """
144 """
144 suffix_if_chopped = suffix_if_chopped or ''
145 suffix_if_chopped = suffix_if_chopped or ''
145 pos = s.find(sub)
146 pos = s.find(sub)
146 if pos == -1:
147 if pos == -1:
147 return s
148 return s
148
149
149 if inclusive:
150 if inclusive:
150 pos += len(sub)
151 pos += len(sub)
151
152
152 chopped = s[:pos]
153 chopped = s[:pos]
153 left = s[pos:].strip()
154 left = s[pos:].strip()
154
155
155 if left and suffix_if_chopped:
156 if left and suffix_if_chopped:
156 chopped += suffix_if_chopped
157 chopped += suffix_if_chopped
157
158
158 return chopped
159 return chopped
159
160
160
161
161 def shorter(text, size=20, prefix=False):
162 def shorter(text, size=20, prefix=False):
162 postfix = '...'
163 postfix = '...'
163 if len(text) > size:
164 if len(text) > size:
164 if prefix:
165 if prefix:
165 # shorten in front
166 # shorten in front
166 return postfix + text[-(size - len(postfix)):]
167 return postfix + text[-(size - len(postfix)):]
167 else:
168 else:
168 return text[:size - len(postfix)] + postfix
169 return text[:size - len(postfix)] + postfix
169 return text
170 return text
170
171
171
172
172 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
173 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
173 """
174 """
174 Reset button
175 Reset button
175 """
176 """
176 return _input(type, name, value, id, attrs)
177 return _input(type, name, value, id, attrs)
177
178
178
179
179 def select(name, selected_values, options, id=NotGiven, **attrs):
180 def select(name, selected_values, options, id=NotGiven, **attrs):
180
181
181 if isinstance(options, (list, tuple)):
182 if isinstance(options, (list, tuple)):
182 options_iter = options
183 options_iter = options
183 # Handle old value,label lists ... where value also can be value,label lists
184 # Handle old value,label lists ... where value also can be value,label lists
184 options = Options()
185 options = Options()
185 for opt in options_iter:
186 for opt in options_iter:
186 if isinstance(opt, tuple) and len(opt) == 2:
187 if isinstance(opt, tuple) and len(opt) == 2:
187 value, label = opt
188 value, label = opt
188 elif isinstance(opt, basestring):
189 elif isinstance(opt, basestring):
189 value = label = opt
190 value = label = opt
190 else:
191 else:
191 raise ValueError('invalid select option type %r' % type(opt))
192 raise ValueError('invalid select option type %r' % type(opt))
192
193
193 if isinstance(value, (list, tuple)):
194 if isinstance(value, (list, tuple)):
194 option_group = options.add_optgroup(label)
195 option_group = options.add_optgroup(label)
195 for opt2 in value:
196 for opt2 in value:
196 if isinstance(opt2, tuple) and len(opt2) == 2:
197 if isinstance(opt2, tuple) and len(opt2) == 2:
197 group_value, group_label = opt2
198 group_value, group_label = opt2
198 elif isinstance(opt2, basestring):
199 elif isinstance(opt2, basestring):
199 group_value = group_label = opt2
200 group_value = group_label = opt2
200 else:
201 else:
201 raise ValueError('invalid select option type %r' % type(opt2))
202 raise ValueError('invalid select option type %r' % type(opt2))
202
203
203 option_group.add_option(group_label, group_value)
204 option_group.add_option(group_label, group_value)
204 else:
205 else:
205 options.add_option(label, value)
206 options.add_option(label, value)
206
207
207 return raw_select(name, selected_values, options, id=id, **attrs)
208 return raw_select(name, selected_values, options, id=id, **attrs)
208
209
209
210
210 def branding(name, length=40):
211 def branding(name, length=40):
211 return truncate(name, length, indicator="")
212 return truncate(name, length, indicator="")
212
213
213
214
214 def FID(raw_id, path):
215 def FID(raw_id, path):
215 """
216 """
216 Creates a unique ID for filenode based on it's hash of path and commit
217 Creates a unique ID for filenode based on it's hash of path and commit
217 it's safe to use in urls
218 it's safe to use in urls
218
219
219 :param raw_id:
220 :param raw_id:
220 :param path:
221 :param path:
221 """
222 """
222
223
223 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
224 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
224
225
225
226
226 class _GetError(object):
227 class _GetError(object):
227 """Get error from form_errors, and represent it as span wrapped error
228 """Get error from form_errors, and represent it as span wrapped error
228 message
229 message
229
230
230 :param field_name: field to fetch errors for
231 :param field_name: field to fetch errors for
231 :param form_errors: form errors dict
232 :param form_errors: form errors dict
232 """
233 """
233
234
234 def __call__(self, field_name, form_errors):
235 def __call__(self, field_name, form_errors):
235 tmpl = """<span class="error_msg">%s</span>"""
236 tmpl = """<span class="error_msg">%s</span>"""
236 if form_errors and field_name in form_errors:
237 if form_errors and field_name in form_errors:
237 return literal(tmpl % form_errors.get(field_name))
238 return literal(tmpl % form_errors.get(field_name))
238
239
239
240
240 get_error = _GetError()
241 get_error = _GetError()
241
242
242
243
243 class _ToolTip(object):
244 class _ToolTip(object):
244
245
245 def __call__(self, tooltip_title, trim_at=50):
246 def __call__(self, tooltip_title, trim_at=50):
246 """
247 """
247 Special function just to wrap our text into nice formatted
248 Special function just to wrap our text into nice formatted
248 autowrapped text
249 autowrapped text
249
250
250 :param tooltip_title:
251 :param tooltip_title:
251 """
252 """
252 tooltip_title = escape(tooltip_title)
253 tooltip_title = escape(tooltip_title)
253 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
254 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
254 return tooltip_title
255 return tooltip_title
255
256
256
257
257 tooltip = _ToolTip()
258 tooltip = _ToolTip()
258
259
259 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
260 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
260
261
261
262
262 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
263 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
263 limit_items=False, linkify_last_item=False, hide_last_item=False,
264 limit_items=False, linkify_last_item=False, hide_last_item=False,
264 copy_path_icon=True):
265 copy_path_icon=True):
265 if isinstance(file_path, str):
266 if isinstance(file_path, str):
266 file_path = safe_unicode(file_path)
267 file_path = safe_unicode(file_path)
267
268
268 if at_ref:
269 if at_ref:
269 route_qry = {'at': at_ref}
270 route_qry = {'at': at_ref}
270 default_landing_ref = at_ref or landing_ref_name or commit_id
271 default_landing_ref = at_ref or landing_ref_name or commit_id
271 else:
272 else:
272 route_qry = None
273 route_qry = None
273 default_landing_ref = commit_id
274 default_landing_ref = commit_id
274
275
275 # first segment is a `HOME` link to repo files root location
276 # first segment is a `HOME` link to repo files root location
276 root_name = literal(u'<i class="icon-home"></i>')
277 root_name = literal(u'<i class="icon-home"></i>')
277
278
278 url_segments = [
279 url_segments = [
279 link_to(
280 link_to(
280 root_name,
281 root_name,
281 repo_files_by_ref_url(
282 repo_files_by_ref_url(
282 repo_name,
283 repo_name,
283 repo_type,
284 repo_type,
284 f_path=None, # None here is a special case for SVN repos,
285 f_path=None, # None here is a special case for SVN repos,
285 # that won't prefix with a ref
286 # that won't prefix with a ref
286 ref_name=default_landing_ref,
287 ref_name=default_landing_ref,
287 commit_id=commit_id,
288 commit_id=commit_id,
288 query=route_qry
289 query=route_qry
289 )
290 )
290 )]
291 )]
291
292
292 path_segments = file_path.split('/')
293 path_segments = file_path.split('/')
293 last_cnt = len(path_segments) - 1
294 last_cnt = len(path_segments) - 1
294 for cnt, segment in enumerate(path_segments):
295 for cnt, segment in enumerate(path_segments):
295 if not segment:
296 if not segment:
296 continue
297 continue
297 segment_html = escape(segment)
298 segment_html = escape(segment)
298
299
299 last_item = cnt == last_cnt
300 last_item = cnt == last_cnt
300
301
301 if last_item and hide_last_item:
302 if last_item and hide_last_item:
302 # iterate over and hide last element
303 # iterate over and hide last element
303 continue
304 continue
304
305
305 if last_item and linkify_last_item is False:
306 if last_item and linkify_last_item is False:
306 # plain version
307 # plain version
307 url_segments.append(segment_html)
308 url_segments.append(segment_html)
308 else:
309 else:
309 url_segments.append(
310 url_segments.append(
310 link_to(
311 link_to(
311 segment_html,
312 segment_html,
312 repo_files_by_ref_url(
313 repo_files_by_ref_url(
313 repo_name,
314 repo_name,
314 repo_type,
315 repo_type,
315 f_path='/'.join(path_segments[:cnt + 1]),
316 f_path='/'.join(path_segments[:cnt + 1]),
316 ref_name=default_landing_ref,
317 ref_name=default_landing_ref,
317 commit_id=commit_id,
318 commit_id=commit_id,
318 query=route_qry
319 query=route_qry
319 ),
320 ),
320 ))
321 ))
321
322
322 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
323 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
323 if limit_items and len(limited_url_segments) < len(url_segments):
324 if limit_items and len(limited_url_segments) < len(url_segments):
324 url_segments = limited_url_segments
325 url_segments = limited_url_segments
325
326
326 full_path = file_path
327 full_path = file_path
327 if copy_path_icon:
328 if copy_path_icon:
328 icon = files_icon.format(escape(full_path))
329 icon = files_icon.format(escape(full_path))
329 else:
330 else:
330 icon = ''
331 icon = ''
331
332
332 if file_path == '':
333 if file_path == '':
333 return root_name
334 return root_name
334 else:
335 else:
335 return literal(' / '.join(url_segments) + icon)
336 return literal(' / '.join(url_segments) + icon)
336
337
337
338
338 def files_url_data(request):
339 def files_url_data(request):
339 matchdict = request.matchdict
340 matchdict = request.matchdict
340
341
341 if 'f_path' not in matchdict:
342 if 'f_path' not in matchdict:
342 matchdict['f_path'] = ''
343 matchdict['f_path'] = ''
343
344
344 if 'commit_id' not in matchdict:
345 if 'commit_id' not in matchdict:
345 matchdict['commit_id'] = 'tip'
346 matchdict['commit_id'] = 'tip'
346
347
347 return json.dumps(matchdict)
348 return json.dumps(matchdict)
348
349
349
350
350 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
351 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
351 _is_svn = is_svn(db_repo_type)
352 _is_svn = is_svn(db_repo_type)
352 final_f_path = f_path
353 final_f_path = f_path
353
354
354 if _is_svn:
355 if _is_svn:
355 """
356 """
356 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
357 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
357 actually commit_id followed by the ref_name. This should be done only in case
358 actually commit_id followed by the ref_name. This should be done only in case
358 This is a initial landing url, without additional paths.
359 This is a initial landing url, without additional paths.
359
360
360 like: /1000/tags/1.0.0/?at=tags/1.0.0
361 like: /1000/tags/1.0.0/?at=tags/1.0.0
361 """
362 """
362
363
363 if ref_name and ref_name != 'tip':
364 if ref_name and ref_name != 'tip':
364 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
365 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
365 # for SVN we only do this magic prefix if it's root, .eg landing revision
366 # for SVN we only do this magic prefix if it's root, .eg landing revision
366 # of files link. If we are in the tree we don't need this since we traverse the url
367 # of files link. If we are in the tree we don't need this since we traverse the url
367 # that has everything stored
368 # that has everything stored
368 if f_path in ['', '/']:
369 if f_path in ['', '/']:
369 final_f_path = '/'.join([ref_name, f_path])
370 final_f_path = '/'.join([ref_name, f_path])
370
371
371 # SVN always needs a commit_id explicitly, without a named REF
372 # SVN always needs a commit_id explicitly, without a named REF
372 default_commit_id = commit_id
373 default_commit_id = commit_id
373 else:
374 else:
374 """
375 """
375 For git and mercurial we construct a new URL using the names instead of commit_id
376 For git and mercurial we construct a new URL using the names instead of commit_id
376 like: /master/some_path?at=master
377 like: /master/some_path?at=master
377 """
378 """
378 # We currently do not support branches with slashes
379 # We currently do not support branches with slashes
379 if '/' in ref_name:
380 if '/' in ref_name:
380 default_commit_id = commit_id
381 default_commit_id = commit_id
381 else:
382 else:
382 default_commit_id = ref_name
383 default_commit_id = ref_name
383
384
384 # sometimes we pass f_path as None, to indicate explicit no prefix,
385 # sometimes we pass f_path as None, to indicate explicit no prefix,
385 # we translate it to string to not have None
386 # we translate it to string to not have None
386 final_f_path = final_f_path or ''
387 final_f_path = final_f_path or ''
387
388
388 files_url = route_path(
389 files_url = route_path(
389 'repo_files',
390 'repo_files',
390 repo_name=db_repo_name,
391 repo_name=db_repo_name,
391 commit_id=default_commit_id,
392 commit_id=default_commit_id,
392 f_path=final_f_path,
393 f_path=final_f_path,
393 _query=query
394 _query=query
394 )
395 )
395 return files_url
396 return files_url
396
397
397
398
398 def code_highlight(code, lexer, formatter, use_hl_filter=False):
399 def code_highlight(code, lexer, formatter, use_hl_filter=False):
399 """
400 """
400 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
401 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
401
402
402 If ``outfile`` is given and a valid file object (an object
403 If ``outfile`` is given and a valid file object (an object
403 with a ``write`` method), the result will be written to it, otherwise
404 with a ``write`` method), the result will be written to it, otherwise
404 it is returned as a string.
405 it is returned as a string.
405 """
406 """
406 if use_hl_filter:
407 if use_hl_filter:
407 # add HL filter
408 # add HL filter
408 from rhodecode.lib.index import search_utils
409 from rhodecode.lib.index import search_utils
409 lexer.add_filter(search_utils.ElasticSearchHLFilter())
410 lexer.add_filter(search_utils.ElasticSearchHLFilter())
410 return pygments.format(pygments.lex(code, lexer), formatter)
411 return pygments.format(pygments.lex(code, lexer), formatter)
411
412
412
413
413 class CodeHtmlFormatter(HtmlFormatter):
414 class CodeHtmlFormatter(HtmlFormatter):
414 """
415 """
415 My code Html Formatter for source codes
416 My code Html Formatter for source codes
416 """
417 """
417
418
418 def wrap(self, source, outfile):
419 def wrap(self, source, outfile):
419 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
420 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
420
421
421 def _wrap_code(self, source):
422 def _wrap_code(self, source):
422 for cnt, it in enumerate(source):
423 for cnt, it in enumerate(source):
423 i, t = it
424 i, t = it
424 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
425 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
425 yield i, t
426 yield i, t
426
427
427 def _wrap_tablelinenos(self, inner):
428 def _wrap_tablelinenos(self, inner):
428 dummyoutfile = StringIO.StringIO()
429 dummyoutfile = StringIO.StringIO()
429 lncount = 0
430 lncount = 0
430 for t, line in inner:
431 for t, line in inner:
431 if t:
432 if t:
432 lncount += 1
433 lncount += 1
433 dummyoutfile.write(line)
434 dummyoutfile.write(line)
434
435
435 fl = self.linenostart
436 fl = self.linenostart
436 mw = len(str(lncount + fl - 1))
437 mw = len(str(lncount + fl - 1))
437 sp = self.linenospecial
438 sp = self.linenospecial
438 st = self.linenostep
439 st = self.linenostep
439 la = self.lineanchors
440 la = self.lineanchors
440 aln = self.anchorlinenos
441 aln = self.anchorlinenos
441 nocls = self.noclasses
442 nocls = self.noclasses
442 if sp:
443 if sp:
443 lines = []
444 lines = []
444
445
445 for i in range(fl, fl + lncount):
446 for i in range(fl, fl + lncount):
446 if i % st == 0:
447 if i % st == 0:
447 if i % sp == 0:
448 if i % sp == 0:
448 if aln:
449 if aln:
449 lines.append('<a href="#%s%d" class="special">%*d</a>' %
450 lines.append('<a href="#%s%d" class="special">%*d</a>' %
450 (la, i, mw, i))
451 (la, i, mw, i))
451 else:
452 else:
452 lines.append('<span class="special">%*d</span>' % (mw, i))
453 lines.append('<span class="special">%*d</span>' % (mw, i))
453 else:
454 else:
454 if aln:
455 if aln:
455 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
456 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
456 else:
457 else:
457 lines.append('%*d' % (mw, i))
458 lines.append('%*d' % (mw, i))
458 else:
459 else:
459 lines.append('')
460 lines.append('')
460 ls = '\n'.join(lines)
461 ls = '\n'.join(lines)
461 else:
462 else:
462 lines = []
463 lines = []
463 for i in range(fl, fl + lncount):
464 for i in range(fl, fl + lncount):
464 if i % st == 0:
465 if i % st == 0:
465 if aln:
466 if aln:
466 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
467 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
467 else:
468 else:
468 lines.append('%*d' % (mw, i))
469 lines.append('%*d' % (mw, i))
469 else:
470 else:
470 lines.append('')
471 lines.append('')
471 ls = '\n'.join(lines)
472 ls = '\n'.join(lines)
472
473
473 # in case you wonder about the seemingly redundant <div> here: since the
474 # in case you wonder about the seemingly redundant <div> here: since the
474 # content in the other cell also is wrapped in a div, some browsers in
475 # content in the other cell also is wrapped in a div, some browsers in
475 # some configurations seem to mess up the formatting...
476 # some configurations seem to mess up the formatting...
476 if nocls:
477 if nocls:
477 yield 0, ('<table class="%stable">' % self.cssclass +
478 yield 0, ('<table class="%stable">' % self.cssclass +
478 '<tr><td><div class="linenodiv" '
479 '<tr><td><div class="linenodiv" '
479 'style="background-color: #f0f0f0; padding-right: 10px">'
480 'style="background-color: #f0f0f0; padding-right: 10px">'
480 '<pre style="line-height: 125%">' +
481 '<pre style="line-height: 125%">' +
481 ls + '</pre></div></td><td id="hlcode" class="code">')
482 ls + '</pre></div></td><td id="hlcode" class="code">')
482 else:
483 else:
483 yield 0, ('<table class="%stable">' % self.cssclass +
484 yield 0, ('<table class="%stable">' % self.cssclass +
484 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
485 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
485 ls + '</pre></div></td><td id="hlcode" class="code">')
486 ls + '</pre></div></td><td id="hlcode" class="code">')
486 yield 0, dummyoutfile.getvalue()
487 yield 0, dummyoutfile.getvalue()
487 yield 0, '</td></tr></table>'
488 yield 0, '</td></tr></table>'
488
489
489
490
490 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
491 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
491 def __init__(self, **kw):
492 def __init__(self, **kw):
492 # only show these line numbers if set
493 # only show these line numbers if set
493 self.only_lines = kw.pop('only_line_numbers', [])
494 self.only_lines = kw.pop('only_line_numbers', [])
494 self.query_terms = kw.pop('query_terms', [])
495 self.query_terms = kw.pop('query_terms', [])
495 self.max_lines = kw.pop('max_lines', 5)
496 self.max_lines = kw.pop('max_lines', 5)
496 self.line_context = kw.pop('line_context', 3)
497 self.line_context = kw.pop('line_context', 3)
497 self.url = kw.pop('url', None)
498 self.url = kw.pop('url', None)
498
499
499 super(CodeHtmlFormatter, self).__init__(**kw)
500 super(CodeHtmlFormatter, self).__init__(**kw)
500
501
501 def _wrap_code(self, source):
502 def _wrap_code(self, source):
502 for cnt, it in enumerate(source):
503 for cnt, it in enumerate(source):
503 i, t = it
504 i, t = it
504 t = '<pre>%s</pre>' % t
505 t = '<pre>%s</pre>' % t
505 yield i, t
506 yield i, t
506
507
507 def _wrap_tablelinenos(self, inner):
508 def _wrap_tablelinenos(self, inner):
508 yield 0, '<table class="code-highlight %stable">' % self.cssclass
509 yield 0, '<table class="code-highlight %stable">' % self.cssclass
509
510
510 last_shown_line_number = 0
511 last_shown_line_number = 0
511 current_line_number = 1
512 current_line_number = 1
512
513
513 for t, line in inner:
514 for t, line in inner:
514 if not t:
515 if not t:
515 yield t, line
516 yield t, line
516 continue
517 continue
517
518
518 if current_line_number in self.only_lines:
519 if current_line_number in self.only_lines:
519 if last_shown_line_number + 1 != current_line_number:
520 if last_shown_line_number + 1 != current_line_number:
520 yield 0, '<tr>'
521 yield 0, '<tr>'
521 yield 0, '<td class="line">...</td>'
522 yield 0, '<td class="line">...</td>'
522 yield 0, '<td id="hlcode" class="code"></td>'
523 yield 0, '<td id="hlcode" class="code"></td>'
523 yield 0, '</tr>'
524 yield 0, '</tr>'
524
525
525 yield 0, '<tr>'
526 yield 0, '<tr>'
526 if self.url:
527 if self.url:
527 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
528 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
528 self.url, current_line_number, current_line_number)
529 self.url, current_line_number, current_line_number)
529 else:
530 else:
530 yield 0, '<td class="line"><a href="">%i</a></td>' % (
531 yield 0, '<td class="line"><a href="">%i</a></td>' % (
531 current_line_number)
532 current_line_number)
532 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
533 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
533 yield 0, '</tr>'
534 yield 0, '</tr>'
534
535
535 last_shown_line_number = current_line_number
536 last_shown_line_number = current_line_number
536
537
537 current_line_number += 1
538 current_line_number += 1
538
539
539 yield 0, '</table>'
540 yield 0, '</table>'
540
541
541
542
542 def hsv_to_rgb(h, s, v):
543 def hsv_to_rgb(h, s, v):
543 """ Convert hsv color values to rgb """
544 """ Convert hsv color values to rgb """
544
545
545 if s == 0.0:
546 if s == 0.0:
546 return v, v, v
547 return v, v, v
547 i = int(h * 6.0) # XXX assume int() truncates!
548 i = int(h * 6.0) # XXX assume int() truncates!
548 f = (h * 6.0) - i
549 f = (h * 6.0) - i
549 p = v * (1.0 - s)
550 p = v * (1.0 - s)
550 q = v * (1.0 - s * f)
551 q = v * (1.0 - s * f)
551 t = v * (1.0 - s * (1.0 - f))
552 t = v * (1.0 - s * (1.0 - f))
552 i = i % 6
553 i = i % 6
553 if i == 0:
554 if i == 0:
554 return v, t, p
555 return v, t, p
555 if i == 1:
556 if i == 1:
556 return q, v, p
557 return q, v, p
557 if i == 2:
558 if i == 2:
558 return p, v, t
559 return p, v, t
559 if i == 3:
560 if i == 3:
560 return p, q, v
561 return p, q, v
561 if i == 4:
562 if i == 4:
562 return t, p, v
563 return t, p, v
563 if i == 5:
564 if i == 5:
564 return v, p, q
565 return v, p, q
565
566
566
567
567 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
568 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
568 """
569 """
569 Generator for getting n of evenly distributed colors using
570 Generator for getting n of evenly distributed colors using
570 hsv color and golden ratio. It always return same order of colors
571 hsv color and golden ratio. It always return same order of colors
571
572
572 :param n: number of colors to generate
573 :param n: number of colors to generate
573 :param saturation: saturation of returned colors
574 :param saturation: saturation of returned colors
574 :param lightness: lightness of returned colors
575 :param lightness: lightness of returned colors
575 :returns: RGB tuple
576 :returns: RGB tuple
576 """
577 """
577
578
578 golden_ratio = 0.618033988749895
579 golden_ratio = 0.618033988749895
579 h = 0.22717784590367374
580 h = 0.22717784590367374
580
581
581 for _ in xrange(n):
582 for _ in xrange(n):
582 h += golden_ratio
583 h += golden_ratio
583 h %= 1
584 h %= 1
584 HSV_tuple = [h, saturation, lightness]
585 HSV_tuple = [h, saturation, lightness]
585 RGB_tuple = hsv_to_rgb(*HSV_tuple)
586 RGB_tuple = hsv_to_rgb(*HSV_tuple)
586 yield map(lambda x: str(int(x * 256)), RGB_tuple)
587 yield map(lambda x: str(int(x * 256)), RGB_tuple)
587
588
588
589
589 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
590 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
590 """
591 """
591 Returns a function which when called with an argument returns a unique
592 Returns a function which when called with an argument returns a unique
592 color for that argument, eg.
593 color for that argument, eg.
593
594
594 :param n: number of colors to generate
595 :param n: number of colors to generate
595 :param saturation: saturation of returned colors
596 :param saturation: saturation of returned colors
596 :param lightness: lightness of returned colors
597 :param lightness: lightness of returned colors
597 :returns: css RGB string
598 :returns: css RGB string
598
599
599 >>> color_hash = color_hasher()
600 >>> color_hash = color_hasher()
600 >>> color_hash('hello')
601 >>> color_hash('hello')
601 'rgb(34, 12, 59)'
602 'rgb(34, 12, 59)'
602 >>> color_hash('hello')
603 >>> color_hash('hello')
603 'rgb(34, 12, 59)'
604 'rgb(34, 12, 59)'
604 >>> color_hash('other')
605 >>> color_hash('other')
605 'rgb(90, 224, 159)'
606 'rgb(90, 224, 159)'
606 """
607 """
607
608
608 color_dict = {}
609 color_dict = {}
609 cgenerator = unique_color_generator(
610 cgenerator = unique_color_generator(
610 saturation=saturation, lightness=lightness)
611 saturation=saturation, lightness=lightness)
611
612
612 def get_color_string(thing):
613 def get_color_string(thing):
613 if thing in color_dict:
614 if thing in color_dict:
614 col = color_dict[thing]
615 col = color_dict[thing]
615 else:
616 else:
616 col = color_dict[thing] = cgenerator.next()
617 col = color_dict[thing] = cgenerator.next()
617 return "rgb(%s)" % (', '.join(col))
618 return "rgb(%s)" % (', '.join(col))
618
619
619 return get_color_string
620 return get_color_string
620
621
621
622
622 def get_lexer_safe(mimetype=None, filepath=None):
623 def get_lexer_safe(mimetype=None, filepath=None):
623 """
624 """
624 Tries to return a relevant pygments lexer using mimetype/filepath name,
625 Tries to return a relevant pygments lexer using mimetype/filepath name,
625 defaulting to plain text if none could be found
626 defaulting to plain text if none could be found
626 """
627 """
627 lexer = None
628 lexer = None
628 try:
629 try:
629 if mimetype:
630 if mimetype:
630 lexer = get_lexer_for_mimetype(mimetype)
631 lexer = get_lexer_for_mimetype(mimetype)
631 if not lexer:
632 if not lexer:
632 lexer = get_lexer_for_filename(filepath)
633 lexer = get_lexer_for_filename(filepath)
633 except pygments.util.ClassNotFound:
634 except pygments.util.ClassNotFound:
634 pass
635 pass
635
636
636 if not lexer:
637 if not lexer:
637 lexer = get_lexer_by_name('text')
638 lexer = get_lexer_by_name('text')
638
639
639 return lexer
640 return lexer
640
641
641
642
642 def get_lexer_for_filenode(filenode):
643 def get_lexer_for_filenode(filenode):
643 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
644 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
644 return lexer
645 return lexer
645
646
646
647
647 def pygmentize(filenode, **kwargs):
648 def pygmentize(filenode, **kwargs):
648 """
649 """
649 pygmentize function using pygments
650 pygmentize function using pygments
650
651
651 :param filenode:
652 :param filenode:
652 """
653 """
653 lexer = get_lexer_for_filenode(filenode)
654 lexer = get_lexer_for_filenode(filenode)
654 return literal(code_highlight(filenode.content, lexer,
655 return literal(code_highlight(filenode.content, lexer,
655 CodeHtmlFormatter(**kwargs)))
656 CodeHtmlFormatter(**kwargs)))
656
657
657
658
658 def is_following_repo(repo_name, user_id):
659 def is_following_repo(repo_name, user_id):
659 from rhodecode.model.scm import ScmModel
660 from rhodecode.model.scm import ScmModel
660 return ScmModel().is_following_repo(repo_name, user_id)
661 return ScmModel().is_following_repo(repo_name, user_id)
661
662
662
663
663 class _Message(object):
664 class _Message(object):
664 """A message returned by ``Flash.pop_messages()``.
665 """A message returned by ``Flash.pop_messages()``.
665
666
666 Converting the message to a string returns the message text. Instances
667 Converting the message to a string returns the message text. Instances
667 also have the following attributes:
668 also have the following attributes:
668
669
669 * ``message``: the message text.
670 * ``message``: the message text.
670 * ``category``: the category specified when the message was created.
671 * ``category``: the category specified when the message was created.
671 """
672 """
672
673
673 def __init__(self, category, message, sub_data=None):
674 def __init__(self, category, message, sub_data=None):
674 self.category = category
675 self.category = category
675 self.message = message
676 self.message = message
676 self.sub_data = sub_data or {}
677 self.sub_data = sub_data or {}
677
678
678 def __str__(self):
679 def __str__(self):
679 return self.message
680 return self.message
680
681
681 __unicode__ = __str__
682 __unicode__ = __str__
682
683
683 def __html__(self):
684 def __html__(self):
684 return escape(safe_unicode(self.message))
685 return escape(safe_unicode(self.message))
685
686
686
687
687 class Flash(object):
688 class Flash(object):
688 # List of allowed categories. If None, allow any category.
689 # List of allowed categories. If None, allow any category.
689 categories = ["warning", "notice", "error", "success"]
690 categories = ["warning", "notice", "error", "success"]
690
691
691 # Default category if none is specified.
692 # Default category if none is specified.
692 default_category = "notice"
693 default_category = "notice"
693
694
694 def __init__(self, session_key="flash", categories=None,
695 def __init__(self, session_key="flash", categories=None,
695 default_category=None):
696 default_category=None):
696 """
697 """
697 Instantiate a ``Flash`` object.
698 Instantiate a ``Flash`` object.
698
699
699 ``session_key`` is the key to save the messages under in the user's
700 ``session_key`` is the key to save the messages under in the user's
700 session.
701 session.
701
702
702 ``categories`` is an optional list which overrides the default list
703 ``categories`` is an optional list which overrides the default list
703 of categories.
704 of categories.
704
705
705 ``default_category`` overrides the default category used for messages
706 ``default_category`` overrides the default category used for messages
706 when none is specified.
707 when none is specified.
707 """
708 """
708 self.session_key = session_key
709 self.session_key = session_key
709 if categories is not None:
710 if categories is not None:
710 self.categories = categories
711 self.categories = categories
711 if default_category is not None:
712 if default_category is not None:
712 self.default_category = default_category
713 self.default_category = default_category
713 if self.categories and self.default_category not in self.categories:
714 if self.categories and self.default_category not in self.categories:
714 raise ValueError(
715 raise ValueError(
715 "unrecognized default category %r" % (self.default_category,))
716 "unrecognized default category %r" % (self.default_category,))
716
717
717 def pop_messages(self, session=None, request=None):
718 def pop_messages(self, session=None, request=None):
718 """
719 """
719 Return all accumulated messages and delete them from the session.
720 Return all accumulated messages and delete them from the session.
720
721
721 The return value is a list of ``Message`` objects.
722 The return value is a list of ``Message`` objects.
722 """
723 """
723 messages = []
724 messages = []
724
725
725 if not session:
726 if not session:
726 if not request:
727 if not request:
727 request = get_current_request()
728 request = get_current_request()
728 session = request.session
729 session = request.session
729
730
730 # Pop the 'old' pylons flash messages. They are tuples of the form
731 # Pop the 'old' pylons flash messages. They are tuples of the form
731 # (category, message)
732 # (category, message)
732 for cat, msg in session.pop(self.session_key, []):
733 for cat, msg in session.pop(self.session_key, []):
733 messages.append(_Message(cat, msg))
734 messages.append(_Message(cat, msg))
734
735
735 # Pop the 'new' pyramid flash messages for each category as list
736 # Pop the 'new' pyramid flash messages for each category as list
736 # of strings.
737 # of strings.
737 for cat in self.categories:
738 for cat in self.categories:
738 for msg in session.pop_flash(queue=cat):
739 for msg in session.pop_flash(queue=cat):
739 sub_data = {}
740 sub_data = {}
740 if hasattr(msg, 'rsplit'):
741 if hasattr(msg, 'rsplit'):
741 flash_data = msg.rsplit('|DELIM|', 1)
742 flash_data = msg.rsplit('|DELIM|', 1)
742 org_message = flash_data[0]
743 org_message = flash_data[0]
743 if len(flash_data) > 1:
744 if len(flash_data) > 1:
744 sub_data = json.loads(flash_data[1])
745 sub_data = json.loads(flash_data[1])
745 else:
746 else:
746 org_message = msg
747 org_message = msg
747
748
748 messages.append(_Message(cat, org_message, sub_data=sub_data))
749 messages.append(_Message(cat, org_message, sub_data=sub_data))
749
750
750 # Map messages from the default queue to the 'notice' category.
751 # Map messages from the default queue to the 'notice' category.
751 for msg in session.pop_flash():
752 for msg in session.pop_flash():
752 messages.append(_Message('notice', msg))
753 messages.append(_Message('notice', msg))
753
754
754 session.save()
755 session.save()
755 return messages
756 return messages
756
757
757 def json_alerts(self, session=None, request=None):
758 def json_alerts(self, session=None, request=None):
758 payloads = []
759 payloads = []
759 messages = flash.pop_messages(session=session, request=request) or []
760 messages = flash.pop_messages(session=session, request=request) or []
760 for message in messages:
761 for message in messages:
761 payloads.append({
762 payloads.append({
762 'message': {
763 'message': {
763 'message': u'{}'.format(message.message),
764 'message': u'{}'.format(message.message),
764 'level': message.category,
765 'level': message.category,
765 'force': True,
766 'force': True,
766 'subdata': message.sub_data
767 'subdata': message.sub_data
767 }
768 }
768 })
769 })
769 return json.dumps(payloads)
770 return json.dumps(payloads)
770
771
771 def __call__(self, message, category=None, ignore_duplicate=True,
772 def __call__(self, message, category=None, ignore_duplicate=True,
772 session=None, request=None):
773 session=None, request=None):
773
774
774 if not session:
775 if not session:
775 if not request:
776 if not request:
776 request = get_current_request()
777 request = get_current_request()
777 session = request.session
778 session = request.session
778
779
779 session.flash(
780 session.flash(
780 message, queue=category, allow_duplicate=not ignore_duplicate)
781 message, queue=category, allow_duplicate=not ignore_duplicate)
781
782
782
783
783 flash = Flash()
784 flash = Flash()
784
785
785 #==============================================================================
786 #==============================================================================
786 # SCM FILTERS available via h.
787 # SCM FILTERS available via h.
787 #==============================================================================
788 #==============================================================================
788 from rhodecode.lib.vcs.utils import author_name, author_email
789 from rhodecode.lib.vcs.utils import author_name, author_email
789 from rhodecode.lib.utils2 import age, age_from_seconds
790 from rhodecode.lib.utils2 import age, age_from_seconds
790 from rhodecode.model.db import User, ChangesetStatus
791 from rhodecode.model.db import User, ChangesetStatus
791
792
792
793
793 email = author_email
794 email = author_email
794
795
795
796
796 def capitalize(raw_text):
797 def capitalize(raw_text):
797 return raw_text.capitalize()
798 return raw_text.capitalize()
798
799
799
800
800 def short_id(long_id):
801 def short_id(long_id):
801 return long_id[:12]
802 return long_id[:12]
802
803
803
804
804 def hide_credentials(url):
805 def hide_credentials(url):
805 from rhodecode.lib.utils2 import credentials_filter
806 from rhodecode.lib.utils2 import credentials_filter
806 return credentials_filter(url)
807 return credentials_filter(url)
807
808
808
809
809 import pytz
810 import pytz
810 import tzlocal
811 import tzlocal
811 local_timezone = tzlocal.get_localzone()
812 local_timezone = tzlocal.get_localzone()
812
813
813
814
814 def get_timezone(datetime_iso, time_is_local=False):
815 def get_timezone(datetime_iso, time_is_local=False):
815 tzinfo = '+00:00'
816 tzinfo = '+00:00'
816
817
817 # detect if we have a timezone info, otherwise, add it
818 # detect if we have a timezone info, otherwise, add it
818 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
819 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
819 force_timezone = os.environ.get('RC_TIMEZONE', '')
820 force_timezone = os.environ.get('RC_TIMEZONE', '')
820 if force_timezone:
821 if force_timezone:
821 force_timezone = pytz.timezone(force_timezone)
822 force_timezone = pytz.timezone(force_timezone)
822 timezone = force_timezone or local_timezone
823 timezone = force_timezone or local_timezone
823 offset = timezone.localize(datetime_iso).strftime('%z')
824 offset = timezone.localize(datetime_iso).strftime('%z')
824 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
825 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
825 return tzinfo
826 return tzinfo
826
827
827
828
828 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
829 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
829 title = value or format_date(datetime_iso)
830 title = value or format_date(datetime_iso)
830 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
831 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
831
832
832 return literal(
833 return literal(
833 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
834 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
834 cls='tooltip' if tooltip else '',
835 cls='tooltip' if tooltip else '',
835 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
836 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
836 title=title, dt=datetime_iso, tzinfo=tzinfo
837 title=title, dt=datetime_iso, tzinfo=tzinfo
837 ))
838 ))
838
839
839
840
840 def _shorten_commit_id(commit_id, commit_len=None):
841 def _shorten_commit_id(commit_id, commit_len=None):
841 if commit_len is None:
842 if commit_len is None:
842 request = get_current_request()
843 request = get_current_request()
843 commit_len = request.call_context.visual.show_sha_length
844 commit_len = request.call_context.visual.show_sha_length
844 return commit_id[:commit_len]
845 return commit_id[:commit_len]
845
846
846
847
847 def show_id(commit, show_idx=None, commit_len=None):
848 def show_id(commit, show_idx=None, commit_len=None):
848 """
849 """
849 Configurable function that shows ID
850 Configurable function that shows ID
850 by default it's r123:fffeeefffeee
851 by default it's r123:fffeeefffeee
851
852
852 :param commit: commit instance
853 :param commit: commit instance
853 """
854 """
854 if show_idx is None:
855 if show_idx is None:
855 request = get_current_request()
856 request = get_current_request()
856 show_idx = request.call_context.visual.show_revision_number
857 show_idx = request.call_context.visual.show_revision_number
857
858
858 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
859 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
859 if show_idx:
860 if show_idx:
860 return 'r%s:%s' % (commit.idx, raw_id)
861 return 'r%s:%s' % (commit.idx, raw_id)
861 else:
862 else:
862 return '%s' % (raw_id, )
863 return '%s' % (raw_id, )
863
864
864
865
865 def format_date(date):
866 def format_date(date):
866 """
867 """
867 use a standardized formatting for dates used in RhodeCode
868 use a standardized formatting for dates used in RhodeCode
868
869
869 :param date: date/datetime object
870 :param date: date/datetime object
870 :return: formatted date
871 :return: formatted date
871 """
872 """
872
873
873 if date:
874 if date:
874 _fmt = "%a, %d %b %Y %H:%M:%S"
875 _fmt = "%a, %d %b %Y %H:%M:%S"
875 return safe_unicode(date.strftime(_fmt))
876 return safe_unicode(date.strftime(_fmt))
876
877
877 return u""
878 return u""
878
879
879
880
880 class _RepoChecker(object):
881 class _RepoChecker(object):
881
882
882 def __init__(self, backend_alias):
883 def __init__(self, backend_alias):
883 self._backend_alias = backend_alias
884 self._backend_alias = backend_alias
884
885
885 def __call__(self, repository):
886 def __call__(self, repository):
886 if hasattr(repository, 'alias'):
887 if hasattr(repository, 'alias'):
887 _type = repository.alias
888 _type = repository.alias
888 elif hasattr(repository, 'repo_type'):
889 elif hasattr(repository, 'repo_type'):
889 _type = repository.repo_type
890 _type = repository.repo_type
890 else:
891 else:
891 _type = repository
892 _type = repository
892 return _type == self._backend_alias
893 return _type == self._backend_alias
893
894
894
895
895 is_git = _RepoChecker('git')
896 is_git = _RepoChecker('git')
896 is_hg = _RepoChecker('hg')
897 is_hg = _RepoChecker('hg')
897 is_svn = _RepoChecker('svn')
898 is_svn = _RepoChecker('svn')
898
899
899
900
900 def get_repo_type_by_name(repo_name):
901 def get_repo_type_by_name(repo_name):
901 repo = Repository.get_by_repo_name(repo_name)
902 repo = Repository.get_by_repo_name(repo_name)
902 if repo:
903 if repo:
903 return repo.repo_type
904 return repo.repo_type
904
905
905
906
906 def is_svn_without_proxy(repository):
907 def is_svn_without_proxy(repository):
907 if is_svn(repository):
908 if is_svn(repository):
908 from rhodecode.model.settings import VcsSettingsModel
909 from rhodecode.model.settings import VcsSettingsModel
909 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
910 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
910 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
911 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
911 return False
912 return False
912
913
913
914
914 def discover_user(author):
915 def discover_user(author):
915 """
916 """
916 Tries to discover RhodeCode User based on the author string. Author string
917 Tries to discover RhodeCode User based on the author string. Author string
917 is typically `FirstName LastName <email@address.com>`
918 is typically `FirstName LastName <email@address.com>`
918 """
919 """
919
920
920 # if author is already an instance use it for extraction
921 # if author is already an instance use it for extraction
921 if isinstance(author, User):
922 if isinstance(author, User):
922 return author
923 return author
923
924
924 # Valid email in the attribute passed, see if they're in the system
925 # Valid email in the attribute passed, see if they're in the system
925 _email = author_email(author)
926 _email = author_email(author)
926 if _email != '':
927 if _email != '':
927 user = User.get_by_email(_email, case_insensitive=True, cache=True)
928 user = User.get_by_email(_email, case_insensitive=True, cache=True)
928 if user is not None:
929 if user is not None:
929 return user
930 return user
930
931
931 # Maybe it's a username, we try to extract it and fetch by username ?
932 # Maybe it's a username, we try to extract it and fetch by username ?
932 _author = author_name(author)
933 _author = author_name(author)
933 user = User.get_by_username(_author, case_insensitive=True, cache=True)
934 user = User.get_by_username(_author, case_insensitive=True, cache=True)
934 if user is not None:
935 if user is not None:
935 return user
936 return user
936
937
937 return None
938 return None
938
939
939
940
940 def email_or_none(author):
941 def email_or_none(author):
941 # extract email from the commit string
942 # extract email from the commit string
942 _email = author_email(author)
943 _email = author_email(author)
943
944
944 # If we have an email, use it, otherwise
945 # If we have an email, use it, otherwise
945 # see if it contains a username we can get an email from
946 # see if it contains a username we can get an email from
946 if _email != '':
947 if _email != '':
947 return _email
948 return _email
948 else:
949 else:
949 user = User.get_by_username(
950 user = User.get_by_username(
950 author_name(author), case_insensitive=True, cache=True)
951 author_name(author), case_insensitive=True, cache=True)
951
952
952 if user is not None:
953 if user is not None:
953 return user.email
954 return user.email
954
955
955 # No valid email, not a valid user in the system, none!
956 # No valid email, not a valid user in the system, none!
956 return None
957 return None
957
958
958
959
959 def link_to_user(author, length=0, **kwargs):
960 def link_to_user(author, length=0, **kwargs):
960 user = discover_user(author)
961 user = discover_user(author)
961 # user can be None, but if we have it already it means we can re-use it
962 # user can be None, but if we have it already it means we can re-use it
962 # in the person() function, so we save 1 intensive-query
963 # in the person() function, so we save 1 intensive-query
963 if user:
964 if user:
964 author = user
965 author = user
965
966
966 display_person = person(author, 'username_or_name_or_email')
967 display_person = person(author, 'username_or_name_or_email')
967 if length:
968 if length:
968 display_person = shorter(display_person, length)
969 display_person = shorter(display_person, length)
969
970
970 if user and user.username != user.DEFAULT_USER:
971 if user and user.username != user.DEFAULT_USER:
971 return link_to(
972 return link_to(
972 escape(display_person),
973 escape(display_person),
973 route_path('user_profile', username=user.username),
974 route_path('user_profile', username=user.username),
974 **kwargs)
975 **kwargs)
975 else:
976 else:
976 return escape(display_person)
977 return escape(display_person)
977
978
978
979
979 def link_to_group(users_group_name, **kwargs):
980 def link_to_group(users_group_name, **kwargs):
980 return link_to(
981 return link_to(
981 escape(users_group_name),
982 escape(users_group_name),
982 route_path('user_group_profile', user_group_name=users_group_name),
983 route_path('user_group_profile', user_group_name=users_group_name),
983 **kwargs)
984 **kwargs)
984
985
985
986
986 def person(author, show_attr="username_and_name"):
987 def person(author, show_attr="username_and_name"):
987 user = discover_user(author)
988 user = discover_user(author)
988 if user:
989 if user:
989 return getattr(user, show_attr)
990 return getattr(user, show_attr)
990 else:
991 else:
991 _author = author_name(author)
992 _author = author_name(author)
992 _email = email(author)
993 _email = email(author)
993 return _author or _email
994 return _author or _email
994
995
995
996
996 def author_string(email):
997 def author_string(email):
997 if email:
998 if email:
998 user = User.get_by_email(email, case_insensitive=True, cache=True)
999 user = User.get_by_email(email, case_insensitive=True, cache=True)
999 if user:
1000 if user:
1000 if user.first_name or user.last_name:
1001 if user.first_name or user.last_name:
1001 return '%s %s &lt;%s&gt;' % (
1002 return '%s %s &lt;%s&gt;' % (
1002 user.first_name, user.last_name, email)
1003 user.first_name, user.last_name, email)
1003 else:
1004 else:
1004 return email
1005 return email
1005 else:
1006 else:
1006 return email
1007 return email
1007 else:
1008 else:
1008 return None
1009 return None
1009
1010
1010
1011
1011 def person_by_id(id_, show_attr="username_and_name"):
1012 def person_by_id(id_, show_attr="username_and_name"):
1012 # attr to return from fetched user
1013 # attr to return from fetched user
1013 person_getter = lambda usr: getattr(usr, show_attr)
1014 person_getter = lambda usr: getattr(usr, show_attr)
1014
1015
1015 #maybe it's an ID ?
1016 #maybe it's an ID ?
1016 if str(id_).isdigit() or isinstance(id_, int):
1017 if str(id_).isdigit() or isinstance(id_, int):
1017 id_ = int(id_)
1018 id_ = int(id_)
1018 user = User.get(id_)
1019 user = User.get(id_)
1019 if user is not None:
1020 if user is not None:
1020 return person_getter(user)
1021 return person_getter(user)
1021 return id_
1022 return id_
1022
1023
1023
1024
1024 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1025 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1025 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1026 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1026 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1027 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1027
1028
1028
1029
1029 tags_paterns = OrderedDict((
1030 tags_paterns = OrderedDict((
1030 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1031 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1031 '<div class="metatag" tag="lang">\\2</div>')),
1032 '<div class="metatag" tag="lang">\\2</div>')),
1032
1033
1033 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1034 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1034 '<div class="metatag" tag="see">see: \\1 </div>')),
1035 '<div class="metatag" tag="see">see: \\1 </div>')),
1035
1036
1036 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1037 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1037 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1038 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1038
1039
1039 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1040 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1040 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1041 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1041
1042
1042 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1043 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1043 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1044 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1044
1045
1045 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1046 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1046 '<div class="metatag" tag="state \\1">\\1</div>')),
1047 '<div class="metatag" tag="state \\1">\\1</div>')),
1047
1048
1048 # label in grey
1049 # label in grey
1049 ('label', (re.compile(r'\[([a-z]+)\]'),
1050 ('label', (re.compile(r'\[([a-z]+)\]'),
1050 '<div class="metatag" tag="label">\\1</div>')),
1051 '<div class="metatag" tag="label">\\1</div>')),
1051
1052
1052 # generic catch all in grey
1053 # generic catch all in grey
1053 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1054 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1054 '<div class="metatag" tag="generic">\\1</div>')),
1055 '<div class="metatag" tag="generic">\\1</div>')),
1055 ))
1056 ))
1056
1057
1057
1058
1058 def extract_metatags(value):
1059 def extract_metatags(value):
1059 """
1060 """
1060 Extract supported meta-tags from given text value
1061 Extract supported meta-tags from given text value
1061 """
1062 """
1062 tags = []
1063 tags = []
1063 if not value:
1064 if not value:
1064 return tags, ''
1065 return tags, ''
1065
1066
1066 for key, val in tags_paterns.items():
1067 for key, val in tags_paterns.items():
1067 pat, replace_html = val
1068 pat, replace_html = val
1068 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1069 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1069 value = pat.sub('', value)
1070 value = pat.sub('', value)
1070
1071
1071 return tags, value
1072 return tags, value
1072
1073
1073
1074
1074 def style_metatag(tag_type, value):
1075 def style_metatag(tag_type, value):
1075 """
1076 """
1076 converts tags from value into html equivalent
1077 converts tags from value into html equivalent
1077 """
1078 """
1078 if not value:
1079 if not value:
1079 return ''
1080 return ''
1080
1081
1081 html_value = value
1082 html_value = value
1082 tag_data = tags_paterns.get(tag_type)
1083 tag_data = tags_paterns.get(tag_type)
1083 if tag_data:
1084 if tag_data:
1084 pat, replace_html = tag_data
1085 pat, replace_html = tag_data
1085 # convert to plain `unicode` instead of a markup tag to be used in
1086 # convert to plain `unicode` instead of a markup tag to be used in
1086 # regex expressions. safe_unicode doesn't work here
1087 # regex expressions. safe_unicode doesn't work here
1087 html_value = pat.sub(replace_html, unicode(value))
1088 html_value = pat.sub(replace_html, unicode(value))
1088
1089
1089 return html_value
1090 return html_value
1090
1091
1091
1092
1092 def bool2icon(value, show_at_false=True):
1093 def bool2icon(value, show_at_false=True):
1093 """
1094 """
1094 Returns boolean value of a given value, represented as html element with
1095 Returns boolean value of a given value, represented as html element with
1095 classes that will represent icons
1096 classes that will represent icons
1096
1097
1097 :param value: given value to convert to html node
1098 :param value: given value to convert to html node
1098 """
1099 """
1099
1100
1100 if value: # does bool conversion
1101 if value: # does bool conversion
1101 return HTML.tag('i', class_="icon-true", title='True')
1102 return HTML.tag('i', class_="icon-true", title='True')
1102 else: # not true as bool
1103 else: # not true as bool
1103 if show_at_false:
1104 if show_at_false:
1104 return HTML.tag('i', class_="icon-false", title='False')
1105 return HTML.tag('i', class_="icon-false", title='False')
1105 return HTML.tag('i')
1106 return HTML.tag('i')
1106
1107
1107
1108
1108 def b64(inp):
1109 def b64(inp):
1109 return base64.b64encode(inp)
1110 return base64.b64encode(inp)
1110
1111
1111 #==============================================================================
1112 #==============================================================================
1112 # PERMS
1113 # PERMS
1113 #==============================================================================
1114 #==============================================================================
1114 from rhodecode.lib.auth import (
1115 from rhodecode.lib.auth import (
1115 HasPermissionAny, HasPermissionAll,
1116 HasPermissionAny, HasPermissionAll,
1116 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1117 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1117 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1118 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1118 csrf_token_key, AuthUser)
1119 csrf_token_key, AuthUser)
1119
1120
1120
1121
1121 #==============================================================================
1122 #==============================================================================
1122 # GRAVATAR URL
1123 # GRAVATAR URL
1123 #==============================================================================
1124 #==============================================================================
1124 class InitialsGravatar(object):
1125 class InitialsGravatar(object):
1125 def __init__(self, email_address, first_name, last_name, size=30,
1126 def __init__(self, email_address, first_name, last_name, size=30,
1126 background=None, text_color='#fff'):
1127 background=None, text_color='#fff'):
1127 self.size = size
1128 self.size = size
1128 self.first_name = first_name
1129 self.first_name = first_name
1129 self.last_name = last_name
1130 self.last_name = last_name
1130 self.email_address = email_address
1131 self.email_address = email_address
1131 self.background = background or self.str2color(email_address)
1132 self.background = background or self.str2color(email_address)
1132 self.text_color = text_color
1133 self.text_color = text_color
1133
1134
1134 def get_color_bank(self):
1135 def get_color_bank(self):
1135 """
1136 """
1136 returns a predefined list of colors that gravatars can use.
1137 returns a predefined list of colors that gravatars can use.
1137 Those are randomized distinct colors that guarantee readability and
1138 Those are randomized distinct colors that guarantee readability and
1138 uniqueness.
1139 uniqueness.
1139
1140
1140 generated with: http://phrogz.net/css/distinct-colors.html
1141 generated with: http://phrogz.net/css/distinct-colors.html
1141 """
1142 """
1142 return [
1143 return [
1143 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1144 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1144 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1145 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1145 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1146 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1146 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1147 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1147 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1148 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1148 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1149 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1149 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1150 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1150 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1151 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1151 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1152 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1152 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1153 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1153 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1154 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1154 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1155 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1155 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1156 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1156 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1157 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1157 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1158 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1158 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1159 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1159 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1160 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1160 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1161 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1161 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1162 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1162 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1163 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1163 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1164 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1164 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1165 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1165 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1166 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1166 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1167 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1167 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1168 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1168 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1169 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1169 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1170 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1170 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1171 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1171 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1172 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1172 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1173 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1173 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1174 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1174 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1175 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1175 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1176 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1176 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1177 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1177 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1178 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1178 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1179 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1179 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1180 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1180 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1181 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1181 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1182 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1182 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1183 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1183 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1184 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1184 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1185 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1185 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1186 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1186 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1187 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1187 '#4f8c46', '#368dd9', '#5c0073'
1188 '#4f8c46', '#368dd9', '#5c0073'
1188 ]
1189 ]
1189
1190
1190 def rgb_to_hex_color(self, rgb_tuple):
1191 def rgb_to_hex_color(self, rgb_tuple):
1191 """
1192 """
1192 Converts an rgb_tuple passed to an hex color.
1193 Converts an rgb_tuple passed to an hex color.
1193
1194
1194 :param rgb_tuple: tuple with 3 ints represents rgb color space
1195 :param rgb_tuple: tuple with 3 ints represents rgb color space
1195 """
1196 """
1196 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1197 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1197
1198
1198 def email_to_int_list(self, email_str):
1199 def email_to_int_list(self, email_str):
1199 """
1200 """
1200 Get every byte of the hex digest value of email and turn it to integer.
1201 Get every byte of the hex digest value of email and turn it to integer.
1201 It's going to be always between 0-255
1202 It's going to be always between 0-255
1202 """
1203 """
1203 digest = md5_safe(email_str.lower())
1204 digest = md5_safe(email_str.lower())
1204 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1205 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1205
1206
1206 def pick_color_bank_index(self, email_str, color_bank):
1207 def pick_color_bank_index(self, email_str, color_bank):
1207 return self.email_to_int_list(email_str)[0] % len(color_bank)
1208 return self.email_to_int_list(email_str)[0] % len(color_bank)
1208
1209
1209 def str2color(self, email_str):
1210 def str2color(self, email_str):
1210 """
1211 """
1211 Tries to map in a stable algorithm an email to color
1212 Tries to map in a stable algorithm an email to color
1212
1213
1213 :param email_str:
1214 :param email_str:
1214 """
1215 """
1215 color_bank = self.get_color_bank()
1216 color_bank = self.get_color_bank()
1216 # pick position (module it's length so we always find it in the
1217 # pick position (module it's length so we always find it in the
1217 # bank even if it's smaller than 256 values
1218 # bank even if it's smaller than 256 values
1218 pos = self.pick_color_bank_index(email_str, color_bank)
1219 pos = self.pick_color_bank_index(email_str, color_bank)
1219 return color_bank[pos]
1220 return color_bank[pos]
1220
1221
1221 def normalize_email(self, email_address):
1222 def normalize_email(self, email_address):
1222 import unicodedata
1223 import unicodedata
1223 # default host used to fill in the fake/missing email
1224 # default host used to fill in the fake/missing email
1224 default_host = u'localhost'
1225 default_host = u'localhost'
1225
1226
1226 if not email_address:
1227 if not email_address:
1227 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1228 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1228
1229
1229 email_address = safe_unicode(email_address)
1230 email_address = safe_unicode(email_address)
1230
1231
1231 if u'@' not in email_address:
1232 if u'@' not in email_address:
1232 email_address = u'%s@%s' % (email_address, default_host)
1233 email_address = u'%s@%s' % (email_address, default_host)
1233
1234
1234 if email_address.endswith(u'@'):
1235 if email_address.endswith(u'@'):
1235 email_address = u'%s%s' % (email_address, default_host)
1236 email_address = u'%s%s' % (email_address, default_host)
1236
1237
1237 email_address = unicodedata.normalize('NFKD', email_address)\
1238 email_address = unicodedata.normalize('NFKD', email_address)\
1238 .encode('ascii', 'ignore')
1239 .encode('ascii', 'ignore')
1239 return email_address
1240 return email_address
1240
1241
1241 def get_initials(self):
1242 def get_initials(self):
1242 """
1243 """
1243 Returns 2 letter initials calculated based on the input.
1244 Returns 2 letter initials calculated based on the input.
1244 The algorithm picks first given email address, and takes first letter
1245 The algorithm picks first given email address, and takes first letter
1245 of part before @, and then the first letter of server name. In case
1246 of part before @, and then the first letter of server name. In case
1246 the part before @ is in a format of `somestring.somestring2` it replaces
1247 the part before @ is in a format of `somestring.somestring2` it replaces
1247 the server letter with first letter of somestring2
1248 the server letter with first letter of somestring2
1248
1249
1249 In case function was initialized with both first and lastname, this
1250 In case function was initialized with both first and lastname, this
1250 overrides the extraction from email by first letter of the first and
1251 overrides the extraction from email by first letter of the first and
1251 last name. We add special logic to that functionality, In case Full name
1252 last name. We add special logic to that functionality, In case Full name
1252 is compound, like Guido Von Rossum, we use last part of the last name
1253 is compound, like Guido Von Rossum, we use last part of the last name
1253 (Von Rossum) picking `R`.
1254 (Von Rossum) picking `R`.
1254
1255
1255 Function also normalizes the non-ascii characters to they ascii
1256 Function also normalizes the non-ascii characters to they ascii
1256 representation, eg Δ„ => A
1257 representation, eg Δ„ => A
1257 """
1258 """
1258 import unicodedata
1259 import unicodedata
1259 # replace non-ascii to ascii
1260 # replace non-ascii to ascii
1260 first_name = unicodedata.normalize(
1261 first_name = unicodedata.normalize(
1261 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1262 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1262 last_name = unicodedata.normalize(
1263 last_name = unicodedata.normalize(
1263 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1264 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1264
1265
1265 # do NFKD encoding, and also make sure email has proper format
1266 # do NFKD encoding, and also make sure email has proper format
1266 email_address = self.normalize_email(self.email_address)
1267 email_address = self.normalize_email(self.email_address)
1267
1268
1268 # first push the email initials
1269 # first push the email initials
1269 prefix, server = email_address.split('@', 1)
1270 prefix, server = email_address.split('@', 1)
1270
1271
1271 # check if prefix is maybe a 'first_name.last_name' syntax
1272 # check if prefix is maybe a 'first_name.last_name' syntax
1272 _dot_split = prefix.rsplit('.', 1)
1273 _dot_split = prefix.rsplit('.', 1)
1273 if len(_dot_split) == 2 and _dot_split[1]:
1274 if len(_dot_split) == 2 and _dot_split[1]:
1274 initials = [_dot_split[0][0], _dot_split[1][0]]
1275 initials = [_dot_split[0][0], _dot_split[1][0]]
1275 else:
1276 else:
1276 initials = [prefix[0], server[0]]
1277 initials = [prefix[0], server[0]]
1277
1278
1278 # then try to replace either first_name or last_name
1279 # then try to replace either first_name or last_name
1279 fn_letter = (first_name or " ")[0].strip()
1280 fn_letter = (first_name or " ")[0].strip()
1280 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1281 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1281
1282
1282 if fn_letter:
1283 if fn_letter:
1283 initials[0] = fn_letter
1284 initials[0] = fn_letter
1284
1285
1285 if ln_letter:
1286 if ln_letter:
1286 initials[1] = ln_letter
1287 initials[1] = ln_letter
1287
1288
1288 return ''.join(initials).upper()
1289 return ''.join(initials).upper()
1289
1290
1290 def get_img_data_by_type(self, font_family, img_type):
1291 def get_img_data_by_type(self, font_family, img_type):
1291 default_user = """
1292 default_user = """
1292 <svg xmlns="http://www.w3.org/2000/svg"
1293 <svg xmlns="http://www.w3.org/2000/svg"
1293 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1294 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1294 viewBox="-15 -10 439.165 429.164"
1295 viewBox="-15 -10 439.165 429.164"
1295
1296
1296 xml:space="preserve"
1297 xml:space="preserve"
1297 style="background:{background};" >
1298 style="background:{background};" >
1298
1299
1299 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1300 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1300 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1301 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1301 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1302 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1302 168.596,153.916,216.671,
1303 168.596,153.916,216.671,
1303 204.583,216.671z" fill="{text_color}"/>
1304 204.583,216.671z" fill="{text_color}"/>
1304 <path d="M407.164,374.717L360.88,
1305 <path d="M407.164,374.717L360.88,
1305 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1306 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1306 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1307 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1307 15.366-44.203,23.488-69.076,23.488c-24.877,
1308 15.366-44.203,23.488-69.076,23.488c-24.877,
1308 0-48.762-8.122-69.078-23.488
1309 0-48.762-8.122-69.078-23.488
1309 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1310 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1310 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1311 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1311 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1312 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1312 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1313 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1313 19.402-10.527 C409.699,390.129,
1314 19.402-10.527 C409.699,390.129,
1314 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1315 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1315 </svg>""".format(
1316 </svg>""".format(
1316 size=self.size,
1317 size=self.size,
1317 background='#979797', # @grey4
1318 background='#979797', # @grey4
1318 text_color=self.text_color,
1319 text_color=self.text_color,
1319 font_family=font_family)
1320 font_family=font_family)
1320
1321
1321 return {
1322 return {
1322 "default_user": default_user
1323 "default_user": default_user
1323 }[img_type]
1324 }[img_type]
1324
1325
1325 def get_img_data(self, svg_type=None):
1326 def get_img_data(self, svg_type=None):
1326 """
1327 """
1327 generates the svg metadata for image
1328 generates the svg metadata for image
1328 """
1329 """
1329 fonts = [
1330 fonts = [
1330 '-apple-system',
1331 '-apple-system',
1331 'BlinkMacSystemFont',
1332 'BlinkMacSystemFont',
1332 'Segoe UI',
1333 'Segoe UI',
1333 'Roboto',
1334 'Roboto',
1334 'Oxygen-Sans',
1335 'Oxygen-Sans',
1335 'Ubuntu',
1336 'Ubuntu',
1336 'Cantarell',
1337 'Cantarell',
1337 'Helvetica Neue',
1338 'Helvetica Neue',
1338 'sans-serif'
1339 'sans-serif'
1339 ]
1340 ]
1340 font_family = ','.join(fonts)
1341 font_family = ','.join(fonts)
1341 if svg_type:
1342 if svg_type:
1342 return self.get_img_data_by_type(font_family, svg_type)
1343 return self.get_img_data_by_type(font_family, svg_type)
1343
1344
1344 initials = self.get_initials()
1345 initials = self.get_initials()
1345 img_data = """
1346 img_data = """
1346 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1347 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1347 width="{size}" height="{size}"
1348 width="{size}" height="{size}"
1348 style="width: 100%; height: 100%; background-color: {background}"
1349 style="width: 100%; height: 100%; background-color: {background}"
1349 viewBox="0 0 {size} {size}">
1350 viewBox="0 0 {size} {size}">
1350 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1351 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1351 pointer-events="auto" fill="{text_color}"
1352 pointer-events="auto" fill="{text_color}"
1352 font-family="{font_family}"
1353 font-family="{font_family}"
1353 style="font-weight: 400; font-size: {f_size}px;">{text}
1354 style="font-weight: 400; font-size: {f_size}px;">{text}
1354 </text>
1355 </text>
1355 </svg>""".format(
1356 </svg>""".format(
1356 size=self.size,
1357 size=self.size,
1357 f_size=self.size/2.05, # scale the text inside the box nicely
1358 f_size=self.size/2.05, # scale the text inside the box nicely
1358 background=self.background,
1359 background=self.background,
1359 text_color=self.text_color,
1360 text_color=self.text_color,
1360 text=initials.upper(),
1361 text=initials.upper(),
1361 font_family=font_family)
1362 font_family=font_family)
1362
1363
1363 return img_data
1364 return img_data
1364
1365
1365 def generate_svg(self, svg_type=None):
1366 def generate_svg(self, svg_type=None):
1366 img_data = self.get_img_data(svg_type)
1367 img_data = self.get_img_data(svg_type)
1367 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1368 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1368
1369
1369
1370
1370 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1371 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1371
1372
1372 svg_type = None
1373 svg_type = None
1373 if email_address == User.DEFAULT_USER_EMAIL:
1374 if email_address == User.DEFAULT_USER_EMAIL:
1374 svg_type = 'default_user'
1375 svg_type = 'default_user'
1375
1376
1376 klass = InitialsGravatar(email_address, first_name, last_name, size)
1377 klass = InitialsGravatar(email_address, first_name, last_name, size)
1377
1378
1378 if store_on_disk:
1379 if store_on_disk:
1379 from rhodecode.apps.file_store import utils as store_utils
1380 from rhodecode.apps.file_store import utils as store_utils
1380 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1381 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1381 FileOverSizeException
1382 FileOverSizeException
1382 from rhodecode.model.db import Session
1383 from rhodecode.model.db import Session
1383
1384
1384 image_key = md5_safe(email_address.lower()
1385 image_key = md5_safe(email_address.lower()
1385 + first_name.lower() + last_name.lower())
1386 + first_name.lower() + last_name.lower())
1386
1387
1387 storage = store_utils.get_file_storage(request.registry.settings)
1388 storage = store_utils.get_file_storage(request.registry.settings)
1388 filename = '{}.svg'.format(image_key)
1389 filename = '{}.svg'.format(image_key)
1389 subdir = 'gravatars'
1390 subdir = 'gravatars'
1390 # since final name has a counter, we apply the 0
1391 # since final name has a counter, we apply the 0
1391 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1392 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1392 store_uid = os.path.join(subdir, uid)
1393 store_uid = os.path.join(subdir, uid)
1393
1394
1394 db_entry = FileStore.get_by_store_uid(store_uid)
1395 db_entry = FileStore.get_by_store_uid(store_uid)
1395 if db_entry:
1396 if db_entry:
1396 return request.route_path('download_file', fid=store_uid)
1397 return request.route_path('download_file', fid=store_uid)
1397
1398
1398 img_data = klass.get_img_data(svg_type=svg_type)
1399 img_data = klass.get_img_data(svg_type=svg_type)
1399 img_file = store_utils.bytes_to_file_obj(img_data)
1400 img_file = store_utils.bytes_to_file_obj(img_data)
1400
1401
1401 try:
1402 try:
1402 store_uid, metadata = storage.save_file(
1403 store_uid, metadata = storage.save_file(
1403 img_file, filename, directory=subdir,
1404 img_file, filename, directory=subdir,
1404 extensions=['.svg'], randomized_name=False)
1405 extensions=['.svg'], randomized_name=False)
1405 except (FileNotAllowedException, FileOverSizeException):
1406 except (FileNotAllowedException, FileOverSizeException):
1406 raise
1407 raise
1407
1408
1408 try:
1409 try:
1409 entry = FileStore.create(
1410 entry = FileStore.create(
1410 file_uid=store_uid, filename=metadata["filename"],
1411 file_uid=store_uid, filename=metadata["filename"],
1411 file_hash=metadata["sha256"], file_size=metadata["size"],
1412 file_hash=metadata["sha256"], file_size=metadata["size"],
1412 file_display_name=filename,
1413 file_display_name=filename,
1413 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1414 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1414 hidden=True, check_acl=False, user_id=1
1415 hidden=True, check_acl=False, user_id=1
1415 )
1416 )
1416 Session().add(entry)
1417 Session().add(entry)
1417 Session().commit()
1418 Session().commit()
1418 log.debug('Stored upload in DB as %s', entry)
1419 log.debug('Stored upload in DB as %s', entry)
1419 except Exception:
1420 except Exception:
1420 raise
1421 raise
1421
1422
1422 return request.route_path('download_file', fid=store_uid)
1423 return request.route_path('download_file', fid=store_uid)
1423
1424
1424 else:
1425 else:
1425 return klass.generate_svg(svg_type=svg_type)
1426 return klass.generate_svg(svg_type=svg_type)
1426
1427
1427
1428
1428 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1429 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1429 return safe_str(gravatar_url_tmpl)\
1430 return safe_str(gravatar_url_tmpl)\
1430 .replace('{email}', email_address) \
1431 .replace('{email}', email_address) \
1431 .replace('{md5email}', md5_safe(email_address.lower())) \
1432 .replace('{md5email}', md5_safe(email_address.lower())) \
1432 .replace('{netloc}', request.host) \
1433 .replace('{netloc}', request.host) \
1433 .replace('{scheme}', request.scheme) \
1434 .replace('{scheme}', request.scheme) \
1434 .replace('{size}', safe_str(size))
1435 .replace('{size}', safe_str(size))
1435
1436
1436
1437
1437 def gravatar_url(email_address, size=30, request=None):
1438 def gravatar_url(email_address, size=30, request=None):
1438 request = request or get_current_request()
1439 request = request or get_current_request()
1439 _use_gravatar = request.call_context.visual.use_gravatar
1440 _use_gravatar = request.call_context.visual.use_gravatar
1440
1441
1441 email_address = email_address or User.DEFAULT_USER_EMAIL
1442 email_address = email_address or User.DEFAULT_USER_EMAIL
1442 if isinstance(email_address, unicode):
1443 if isinstance(email_address, unicode):
1443 # hashlib crashes on unicode items
1444 # hashlib crashes on unicode items
1444 email_address = safe_str(email_address)
1445 email_address = safe_str(email_address)
1445
1446
1446 # empty email or default user
1447 # empty email or default user
1447 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1448 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1448 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1449 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1449
1450
1450 if _use_gravatar:
1451 if _use_gravatar:
1451 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1452 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1452 or User.DEFAULT_GRAVATAR_URL
1453 or User.DEFAULT_GRAVATAR_URL
1453 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1454 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1454
1455
1455 else:
1456 else:
1456 return initials_gravatar(request, email_address, '', '', size=size)
1457 return initials_gravatar(request, email_address, '', '', size=size)
1457
1458
1458
1459
1459 def breadcrumb_repo_link(repo):
1460 def breadcrumb_repo_link(repo):
1460 """
1461 """
1461 Makes a breadcrumbs path link to repo
1462 Makes a breadcrumbs path link to repo
1462
1463
1463 ex::
1464 ex::
1464 group >> subgroup >> repo
1465 group >> subgroup >> repo
1465
1466
1466 :param repo: a Repository instance
1467 :param repo: a Repository instance
1467 """
1468 """
1468
1469
1469 path = [
1470 path = [
1470 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1471 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1471 title='last change:{}'.format(format_date(group.last_commit_change)))
1472 title='last change:{}'.format(format_date(group.last_commit_change)))
1472 for group in repo.groups_with_parents
1473 for group in repo.groups_with_parents
1473 ] + [
1474 ] + [
1474 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1475 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1475 title='last change:{}'.format(format_date(repo.last_commit_change)))
1476 title='last change:{}'.format(format_date(repo.last_commit_change)))
1476 ]
1477 ]
1477
1478
1478 return literal(' &raquo; '.join(path))
1479 return literal(' &raquo; '.join(path))
1479
1480
1480
1481
1481 def breadcrumb_repo_group_link(repo_group):
1482 def breadcrumb_repo_group_link(repo_group):
1482 """
1483 """
1483 Makes a breadcrumbs path link to repo
1484 Makes a breadcrumbs path link to repo
1484
1485
1485 ex::
1486 ex::
1486 group >> subgroup
1487 group >> subgroup
1487
1488
1488 :param repo_group: a Repository Group instance
1489 :param repo_group: a Repository Group instance
1489 """
1490 """
1490
1491
1491 path = [
1492 path = [
1492 link_to(group.name,
1493 link_to(group.name,
1493 route_path('repo_group_home', repo_group_name=group.group_name),
1494 route_path('repo_group_home', repo_group_name=group.group_name),
1494 title='last change:{}'.format(format_date(group.last_commit_change)))
1495 title='last change:{}'.format(format_date(group.last_commit_change)))
1495 for group in repo_group.parents
1496 for group in repo_group.parents
1496 ] + [
1497 ] + [
1497 link_to(repo_group.name,
1498 link_to(repo_group.name,
1498 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1499 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1499 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1500 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1500 ]
1501 ]
1501
1502
1502 return literal(' &raquo; '.join(path))
1503 return literal(' &raquo; '.join(path))
1503
1504
1504
1505
1505 def format_byte_size_binary(file_size):
1506 def format_byte_size_binary(file_size):
1506 """
1507 """
1507 Formats file/folder sizes to standard.
1508 Formats file/folder sizes to standard.
1508 """
1509 """
1509 if file_size is None:
1510 if file_size is None:
1510 file_size = 0
1511 file_size = 0
1511
1512
1512 formatted_size = format_byte_size(file_size, binary=True)
1513 formatted_size = format_byte_size(file_size, binary=True)
1513 return formatted_size
1514 return formatted_size
1514
1515
1515
1516
1516 def urlify_text(text_, safe=True, **href_attrs):
1517 def urlify_text(text_, safe=True, **href_attrs):
1517 """
1518 """
1518 Extract urls from text and make html links out of them
1519 Extract urls from text and make html links out of them
1519 """
1520 """
1520
1521
1521 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1522 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1522 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1523 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1523
1524
1524 def url_func(match_obj):
1525 def url_func(match_obj):
1525 url_full = match_obj.groups()[0]
1526 url_full = match_obj.groups()[0]
1526 a_options = dict(href_attrs)
1527 a_options = dict(href_attrs)
1527 a_options['href'] = url_full
1528 a_options['href'] = url_full
1528 a_text = url_full
1529 a_text = url_full
1529 return HTML.tag("a", a_text, **a_options)
1530 return HTML.tag("a", a_text, **a_options)
1530
1531
1531 _new_text = url_pat.sub(url_func, text_)
1532 _new_text = url_pat.sub(url_func, text_)
1532
1533
1533 if safe:
1534 if safe:
1534 return literal(_new_text)
1535 return literal(_new_text)
1535 return _new_text
1536 return _new_text
1536
1537
1537
1538
1538 def urlify_commits(text_, repo_name):
1539 def urlify_commits(text_, repo_name):
1539 """
1540 """
1540 Extract commit ids from text and make link from them
1541 Extract commit ids from text and make link from them
1541
1542
1542 :param text_:
1543 :param text_:
1543 :param repo_name: repo name to build the URL with
1544 :param repo_name: repo name to build the URL with
1544 """
1545 """
1545
1546
1546 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1547 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1547
1548
1548 def url_func(match_obj):
1549 def url_func(match_obj):
1549 commit_id = match_obj.groups()[1]
1550 commit_id = match_obj.groups()[1]
1550 pref = match_obj.groups()[0]
1551 pref = match_obj.groups()[0]
1551 suf = match_obj.groups()[2]
1552 suf = match_obj.groups()[2]
1552
1553
1553 tmpl = (
1554 tmpl = (
1554 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1555 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1555 '%(commit_id)s</a>%(suf)s'
1556 '%(commit_id)s</a>%(suf)s'
1556 )
1557 )
1557 return tmpl % {
1558 return tmpl % {
1558 'pref': pref,
1559 'pref': pref,
1559 'cls': 'revision-link',
1560 'cls': 'revision-link',
1560 'url': route_url(
1561 'url': route_url(
1561 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1562 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1562 'commit_id': commit_id,
1563 'commit_id': commit_id,
1563 'suf': suf,
1564 'suf': suf,
1564 'hovercard_alt': 'Commit: {}'.format(commit_id),
1565 'hovercard_alt': 'Commit: {}'.format(commit_id),
1565 'hovercard_url': route_url(
1566 'hovercard_url': route_url(
1566 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1567 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1567 }
1568 }
1568
1569
1569 new_text = url_pat.sub(url_func, text_)
1570 new_text = url_pat.sub(url_func, text_)
1570
1571
1571 return new_text
1572 return new_text
1572
1573
1573
1574
1574 def _process_url_func(match_obj, repo_name, uid, entry,
1575 def _process_url_func(match_obj, repo_name, uid, entry,
1575 return_raw_data=False, link_format='html'):
1576 return_raw_data=False, link_format='html'):
1576 pref = ''
1577 pref = ''
1577 if match_obj.group().startswith(' '):
1578 if match_obj.group().startswith(' '):
1578 pref = ' '
1579 pref = ' '
1579
1580
1580 issue_id = ''.join(match_obj.groups())
1581 issue_id = ''.join(match_obj.groups())
1581
1582
1582 if link_format == 'html':
1583 if link_format == 'html':
1583 tmpl = (
1584 tmpl = (
1584 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1585 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1585 '%(issue-prefix)s%(id-repr)s'
1586 '%(issue-prefix)s%(id-repr)s'
1586 '</a>')
1587 '</a>')
1587 elif link_format == 'html+hovercard':
1588 elif link_format == 'html+hovercard':
1588 tmpl = (
1589 tmpl = (
1589 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1590 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1590 '%(issue-prefix)s%(id-repr)s'
1591 '%(issue-prefix)s%(id-repr)s'
1591 '</a>')
1592 '</a>')
1592 elif link_format in ['rst', 'rst+hovercard']:
1593 elif link_format in ['rst', 'rst+hovercard']:
1593 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1594 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1594 elif link_format in ['markdown', 'markdown+hovercard']:
1595 elif link_format in ['markdown', 'markdown+hovercard']:
1595 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1596 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1596 else:
1597 else:
1597 raise ValueError('Bad link_format:{}'.format(link_format))
1598 raise ValueError('Bad link_format:{}'.format(link_format))
1598
1599
1599 (repo_name_cleaned,
1600 (repo_name_cleaned,
1600 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1601 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1601
1602
1602 # variables replacement
1603 # variables replacement
1603 named_vars = {
1604 named_vars = {
1604 'id': issue_id,
1605 'id': issue_id,
1605 'repo': repo_name,
1606 'repo': repo_name,
1606 'repo_name': repo_name_cleaned,
1607 'repo_name': repo_name_cleaned,
1607 'group_name': parent_group_name,
1608 'group_name': parent_group_name,
1608 # set dummy keys so we always have them
1609 # set dummy keys so we always have them
1609 'hostname': '',
1610 'hostname': '',
1610 'netloc': '',
1611 'netloc': '',
1611 'scheme': ''
1612 'scheme': ''
1612 }
1613 }
1613
1614
1614 request = get_current_request()
1615 request = get_current_request()
1615 if request:
1616 if request:
1616 # exposes, hostname, netloc, scheme
1617 # exposes, hostname, netloc, scheme
1617 host_data = get_host_info(request)
1618 host_data = get_host_info(request)
1618 named_vars.update(host_data)
1619 named_vars.update(host_data)
1619
1620
1620 # named regex variables
1621 # named regex variables
1621 named_vars.update(match_obj.groupdict())
1622 named_vars.update(match_obj.groupdict())
1622 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1623 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1623 desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
1624 desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
1624 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1625 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1625
1626
1626 def quote_cleaner(input_str):
1627 def quote_cleaner(input_str):
1627 """Remove quotes as it's HTML"""
1628 """Remove quotes as it's HTML"""
1628 return input_str.replace('"', '')
1629 return input_str.replace('"', '')
1629
1630
1630 data = {
1631 data = {
1631 'pref': pref,
1632 'pref': pref,
1632 'cls': quote_cleaner('issue-tracker-link'),
1633 'cls': quote_cleaner('issue-tracker-link'),
1633 'url': quote_cleaner(_url),
1634 'url': quote_cleaner(_url),
1634 'id-repr': issue_id,
1635 'id-repr': issue_id,
1635 'issue-prefix': entry['pref'],
1636 'issue-prefix': entry['pref'],
1636 'serv': entry['url'],
1637 'serv': entry['url'],
1637 'title': bleach.clean(desc, strip=True),
1638 'title': bleach.clean(desc, strip=True),
1638 'hovercard_url': hovercard_url
1639 'hovercard_url': hovercard_url
1639 }
1640 }
1640
1641
1641 if return_raw_data:
1642 if return_raw_data:
1642 return {
1643 return {
1643 'id': issue_id,
1644 'id': issue_id,
1644 'url': _url
1645 'url': _url
1645 }
1646 }
1646 return tmpl % data
1647 return tmpl % data
1647
1648
1648
1649
1649 def get_active_pattern_entries(repo_name):
1650 def get_active_pattern_entries(repo_name):
1650 repo = None
1651 repo = None
1651 if repo_name:
1652 if repo_name:
1652 # Retrieving repo_name to avoid invalid repo_name to explode on
1653 # Retrieving repo_name to avoid invalid repo_name to explode on
1653 # IssueTrackerSettingsModel but still passing invalid name further down
1654 # IssueTrackerSettingsModel but still passing invalid name further down
1654 repo = Repository.get_by_repo_name(repo_name, cache=True)
1655 repo = Repository.get_by_repo_name(repo_name, cache=True)
1655
1656
1656 settings_model = IssueTrackerSettingsModel(repo=repo)
1657 settings_model = IssueTrackerSettingsModel(repo=repo)
1657 active_entries = settings_model.get_settings(cache=True)
1658 active_entries = settings_model.get_settings(cache=True)
1658 return active_entries
1659 return active_entries
1659
1660
1660
1661
1661 pr_pattern_re = regex.compile(r'(?:(?:^!)|(?: !))(\d+)')
1662 pr_pattern_re = regex.compile(r'(?:(?:^!)|(?: !))(\d+)')
1662
1663
1663 allowed_link_formats = [
1664 allowed_link_formats = [
1664 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1665 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1665
1666
1666
1667
1667 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1668 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1668
1669
1669 if link_format not in allowed_link_formats:
1670 if link_format not in allowed_link_formats:
1670 raise ValueError('Link format can be only one of:{} got {}'.format(
1671 raise ValueError('Link format can be only one of:{} got {}'.format(
1671 allowed_link_formats, link_format))
1672 allowed_link_formats, link_format))
1672
1673
1673 if active_entries is None:
1674 if active_entries is None:
1674 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1675 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1675 active_entries = get_active_pattern_entries(repo_name)
1676 active_entries = get_active_pattern_entries(repo_name)
1676
1677
1677 issues_data = []
1678 issues_data = []
1678 errors = []
1679 errors = []
1679 new_text = text_string
1680 new_text = text_string
1680
1681
1681 log.debug('Got %s entries to process', len(active_entries))
1682 log.debug('Got %s entries to process', len(active_entries))
1682 for uid, entry in active_entries.items():
1683 for uid, entry in active_entries.items():
1683 log.debug('found issue tracker entry with uid %s', uid)
1684 log.debug('found issue tracker entry with uid %s', uid)
1684
1685
1685 if not (entry['pat'] and entry['url']):
1686 if not (entry['pat'] and entry['url']):
1686 log.debug('skipping due to missing data')
1687 log.debug('skipping due to missing data')
1687 continue
1688 continue
1688
1689
1689 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1690 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1690 uid, entry['pat'], entry['url'], entry['pref'])
1691 uid, entry['pat'], entry['url'], entry['pref'])
1691
1692
1692 if entry.get('pat_compiled'):
1693 if entry.get('pat_compiled'):
1693 pattern = entry['pat_compiled']
1694 pattern = entry['pat_compiled']
1694 else:
1695 else:
1695 try:
1696 try:
1696 pattern = regex.compile(r'%s' % entry['pat'])
1697 pattern = regex.compile(r'%s' % entry['pat'])
1697 except regex.error as e:
1698 except regex.error as e:
1698 regex_err = ValueError('{}:{}'.format(entry['pat'], e))
1699 regex_err = ValueError('{}:{}'.format(entry['pat'], e))
1699 log.exception('issue tracker pattern: `%s` failed to compile', regex_err)
1700 log.exception('issue tracker pattern: `%s` failed to compile', regex_err)
1700 errors.append(regex_err)
1701 errors.append(regex_err)
1701 continue
1702 continue
1702
1703
1703 data_func = partial(
1704 data_func = partial(
1704 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1705 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1705 return_raw_data=True)
1706 return_raw_data=True)
1706
1707
1707 for match_obj in pattern.finditer(text_string):
1708 for match_obj in pattern.finditer(text_string):
1708 issues_data.append(data_func(match_obj))
1709 issues_data.append(data_func(match_obj))
1709
1710
1710 url_func = partial(
1711 url_func = partial(
1711 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1712 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1712 link_format=link_format)
1713 link_format=link_format)
1713
1714
1714 new_text = pattern.sub(url_func, new_text)
1715 new_text = pattern.sub(url_func, new_text)
1715 log.debug('processed prefix:uid `%s`', uid)
1716 log.debug('processed prefix:uid `%s`', uid)
1716
1717
1717 # finally use global replace, eg !123 -> pr-link, those will not catch
1718 # finally use global replace, eg !123 -> pr-link, those will not catch
1718 # if already similar pattern exists
1719 # if already similar pattern exists
1719 server_url = '${scheme}://${netloc}'
1720 server_url = '${scheme}://${netloc}'
1720 pr_entry = {
1721 pr_entry = {
1721 'pref': '!',
1722 'pref': '!',
1722 'url': server_url + '/_admin/pull-requests/${id}',
1723 'url': server_url + '/_admin/pull-requests/${id}',
1723 'desc': 'Pull Request !${id}',
1724 'desc': 'Pull Request !${id}',
1724 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1725 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1725 }
1726 }
1726 pr_url_func = partial(
1727 pr_url_func = partial(
1727 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1728 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1728 link_format=link_format+'+hovercard')
1729 link_format=link_format+'+hovercard')
1729 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1730 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1730 log.debug('processed !pr pattern')
1731 log.debug('processed !pr pattern')
1731
1732
1732 return new_text, issues_data, errors
1733 return new_text, issues_data, errors
1733
1734
1734
1735
1735 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1736 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1736 issues_container=None, error_container=None):
1737 issues_container_callback=None, error_container=None):
1737 """
1738 """
1738 Parses given text message and makes proper links.
1739 Parses given text message and makes proper links.
1739 issues are linked to given issue-server, and rest is a commit link
1740 issues are linked to given issue-server, and rest is a commit link
1740 """
1741 """
1741
1742
1742 def escaper(_text):
1743 def escaper(_text):
1743 return _text.replace('<', '&lt;').replace('>', '&gt;')
1744 return _text.replace('<', '&lt;').replace('>', '&gt;')
1744
1745
1745 new_text = escaper(commit_text)
1746 new_text = escaper(commit_text)
1746
1747
1747 # extract http/https links and make them real urls
1748 # extract http/https links and make them real urls
1748 new_text = urlify_text(new_text, safe=False)
1749 new_text = urlify_text(new_text, safe=False)
1749
1750
1750 # urlify commits - extract commit ids and make link out of them, if we have
1751 # urlify commits - extract commit ids and make link out of them, if we have
1751 # the scope of repository present.
1752 # the scope of repository present.
1752 if repository:
1753 if repository:
1753 new_text = urlify_commits(new_text, repository)
1754 new_text = urlify_commits(new_text, repository)
1754
1755
1755 # process issue tracker patterns
1756 # process issue tracker patterns
1756 new_text, issues, errors = process_patterns(
1757 new_text, issues, errors = process_patterns(
1757 new_text, repository or '', active_entries=active_pattern_entries)
1758 new_text, repository or '', active_entries=active_pattern_entries)
1758
1759
1759 if issues_container is not None:
1760 if issues_container_callback is not None:
1760 issues_container.extend(issues)
1761 for issue in issues:
1762 issues_container_callback(issue)
1761
1763
1762 if error_container is not None:
1764 if error_container is not None:
1763 error_container.extend(errors)
1765 error_container.extend(errors)
1764
1766
1765 return literal(new_text)
1767 return literal(new_text)
1766
1768
1767
1769
1768 def render_binary(repo_name, file_obj):
1770 def render_binary(repo_name, file_obj):
1769 """
1771 """
1770 Choose how to render a binary file
1772 Choose how to render a binary file
1771 """
1773 """
1772
1774
1773 # unicode
1775 # unicode
1774 filename = file_obj.name
1776 filename = file_obj.name
1775
1777
1776 # images
1778 # images
1777 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1779 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1778 if fnmatch.fnmatch(filename, pat=ext):
1780 if fnmatch.fnmatch(filename, pat=ext):
1779 src = route_path(
1781 src = route_path(
1780 'repo_file_raw', repo_name=repo_name,
1782 'repo_file_raw', repo_name=repo_name,
1781 commit_id=file_obj.commit.raw_id,
1783 commit_id=file_obj.commit.raw_id,
1782 f_path=file_obj.path)
1784 f_path=file_obj.path)
1783
1785
1784 return literal(
1786 return literal(
1785 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1787 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1786
1788
1787
1789
1788 def renderer_from_filename(filename, exclude=None):
1790 def renderer_from_filename(filename, exclude=None):
1789 """
1791 """
1790 choose a renderer based on filename, this works only for text based files
1792 choose a renderer based on filename, this works only for text based files
1791 """
1793 """
1792
1794
1793 # ipython
1795 # ipython
1794 for ext in ['*.ipynb']:
1796 for ext in ['*.ipynb']:
1795 if fnmatch.fnmatch(filename, pat=ext):
1797 if fnmatch.fnmatch(filename, pat=ext):
1796 return 'jupyter'
1798 return 'jupyter'
1797
1799
1798 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1800 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1799 if is_markup:
1801 if is_markup:
1800 return is_markup
1802 return is_markup
1801 return None
1803 return None
1802
1804
1803
1805
1804 def render(source, renderer='rst', mentions=False, relative_urls=None,
1806 def render(source, renderer='rst', mentions=False, relative_urls=None,
1805 repo_name=None, active_pattern_entries=None, issues_container=None):
1807 repo_name=None, active_pattern_entries=None, issues_container_callback=None):
1806
1808
1807 def maybe_convert_relative_links(html_source):
1809 def maybe_convert_relative_links(html_source):
1808 if relative_urls:
1810 if relative_urls:
1809 return relative_links(html_source, relative_urls)
1811 return relative_links(html_source, relative_urls)
1810 return html_source
1812 return html_source
1811
1813
1812 if renderer == 'plain':
1814 if renderer == 'plain':
1813 return literal(
1815 return literal(
1814 MarkupRenderer.plain(source, leading_newline=False))
1816 MarkupRenderer.plain(source, leading_newline=False))
1815
1817
1816 elif renderer == 'rst':
1818 elif renderer == 'rst':
1817 if repo_name:
1819 if repo_name:
1818 # process patterns on comments if we pass in repo name
1820 # process patterns on comments if we pass in repo name
1819 source, issues, errors = process_patterns(
1821 source, issues, errors = process_patterns(
1820 source, repo_name, link_format='rst',
1822 source, repo_name, link_format='rst',
1821 active_entries=active_pattern_entries)
1823 active_entries=active_pattern_entries)
1822 if issues_container is not None:
1824 if issues_container_callback is not None:
1823 issues_container.extend(issues)
1825 for issue in issues:
1826 issues_container_callback(issue)
1824
1827
1825 return literal(
1828 return literal(
1826 '<div class="rst-block">%s</div>' %
1829 '<div class="rst-block">%s</div>' %
1827 maybe_convert_relative_links(
1830 maybe_convert_relative_links(
1828 MarkupRenderer.rst(source, mentions=mentions)))
1831 MarkupRenderer.rst(source, mentions=mentions)))
1829
1832
1830 elif renderer == 'markdown':
1833 elif renderer == 'markdown':
1831 if repo_name:
1834 if repo_name:
1832 # process patterns on comments if we pass in repo name
1835 # process patterns on comments if we pass in repo name
1833 source, issues, errors = process_patterns(
1836 source, issues, errors = process_patterns(
1834 source, repo_name, link_format='markdown',
1837 source, repo_name, link_format='markdown',
1835 active_entries=active_pattern_entries)
1838 active_entries=active_pattern_entries)
1836 if issues_container is not None:
1839 if issues_container_callback is not None:
1837 issues_container.extend(issues)
1840 for issue in issues:
1841 issues_container_callback(issue)
1842
1838
1843
1839 return literal(
1844 return literal(
1840 '<div class="markdown-block">%s</div>' %
1845 '<div class="markdown-block">%s</div>' %
1841 maybe_convert_relative_links(
1846 maybe_convert_relative_links(
1842 MarkupRenderer.markdown(source, flavored=True,
1847 MarkupRenderer.markdown(source, flavored=True,
1843 mentions=mentions)))
1848 mentions=mentions)))
1844
1849
1845 elif renderer == 'jupyter':
1850 elif renderer == 'jupyter':
1846 return literal(
1851 return literal(
1847 '<div class="ipynb">%s</div>' %
1852 '<div class="ipynb">%s</div>' %
1848 maybe_convert_relative_links(
1853 maybe_convert_relative_links(
1849 MarkupRenderer.jupyter(source)))
1854 MarkupRenderer.jupyter(source)))
1850
1855
1851 # None means just show the file-source
1856 # None means just show the file-source
1852 return None
1857 return None
1853
1858
1854
1859
1855 def commit_status(repo, commit_id):
1860 def commit_status(repo, commit_id):
1856 return ChangesetStatusModel().get_status(repo, commit_id)
1861 return ChangesetStatusModel().get_status(repo, commit_id)
1857
1862
1858
1863
1859 def commit_status_lbl(commit_status):
1864 def commit_status_lbl(commit_status):
1860 return dict(ChangesetStatus.STATUSES).get(commit_status)
1865 return dict(ChangesetStatus.STATUSES).get(commit_status)
1861
1866
1862
1867
1863 def commit_time(repo_name, commit_id):
1868 def commit_time(repo_name, commit_id):
1864 repo = Repository.get_by_repo_name(repo_name)
1869 repo = Repository.get_by_repo_name(repo_name)
1865 commit = repo.get_commit(commit_id=commit_id)
1870 commit = repo.get_commit(commit_id=commit_id)
1866 return commit.date
1871 return commit.date
1867
1872
1868
1873
1869 def get_permission_name(key):
1874 def get_permission_name(key):
1870 return dict(Permission.PERMS).get(key)
1875 return dict(Permission.PERMS).get(key)
1871
1876
1872
1877
1873 def journal_filter_help(request):
1878 def journal_filter_help(request):
1874 _ = request.translate
1879 _ = request.translate
1875 from rhodecode.lib.audit_logger import ACTIONS
1880 from rhodecode.lib.audit_logger import ACTIONS
1876 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1881 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1877
1882
1878 return _(
1883 return _(
1879 'Example filter terms:\n' +
1884 'Example filter terms:\n' +
1880 ' repository:vcs\n' +
1885 ' repository:vcs\n' +
1881 ' username:marcin\n' +
1886 ' username:marcin\n' +
1882 ' username:(NOT marcin)\n' +
1887 ' username:(NOT marcin)\n' +
1883 ' action:*push*\n' +
1888 ' action:*push*\n' +
1884 ' ip:127.0.0.1\n' +
1889 ' ip:127.0.0.1\n' +
1885 ' date:20120101\n' +
1890 ' date:20120101\n' +
1886 ' date:[20120101100000 TO 20120102]\n' +
1891 ' date:[20120101100000 TO 20120102]\n' +
1887 '\n' +
1892 '\n' +
1888 'Actions: {actions}\n' +
1893 'Actions: {actions}\n' +
1889 '\n' +
1894 '\n' +
1890 'Generate wildcards using \'*\' character:\n' +
1895 'Generate wildcards using \'*\' character:\n' +
1891 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1896 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1892 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1897 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1893 '\n' +
1898 '\n' +
1894 'Optional AND / OR operators in queries\n' +
1899 'Optional AND / OR operators in queries\n' +
1895 ' "repository:vcs OR repository:test"\n' +
1900 ' "repository:vcs OR repository:test"\n' +
1896 ' "username:test AND repository:test*"\n'
1901 ' "username:test AND repository:test*"\n'
1897 ).format(actions=actions)
1902 ).format(actions=actions)
1898
1903
1899
1904
1900 def not_mapped_error(repo_name):
1905 def not_mapped_error(repo_name):
1901 from rhodecode.translation import _
1906 from rhodecode.translation import _
1902 flash(_('%s repository is not mapped to db perhaps'
1907 flash(_('%s repository is not mapped to db perhaps'
1903 ' it was created or renamed from the filesystem'
1908 ' it was created or renamed from the filesystem'
1904 ' please run the application again'
1909 ' please run the application again'
1905 ' in order to rescan repositories') % repo_name, category='error')
1910 ' in order to rescan repositories') % repo_name, category='error')
1906
1911
1907
1912
1908 def ip_range(ip_addr):
1913 def ip_range(ip_addr):
1909 from rhodecode.model.db import UserIpMap
1914 from rhodecode.model.db import UserIpMap
1910 s, e = UserIpMap._get_ip_range(ip_addr)
1915 s, e = UserIpMap._get_ip_range(ip_addr)
1911 return '%s - %s' % (s, e)
1916 return '%s - %s' % (s, e)
1912
1917
1913
1918
1914 def form(url, method='post', needs_csrf_token=True, **attrs):
1919 def form(url, method='post', needs_csrf_token=True, **attrs):
1915 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1920 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1916 if method.lower() != 'get' and needs_csrf_token:
1921 if method.lower() != 'get' and needs_csrf_token:
1917 raise Exception(
1922 raise Exception(
1918 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1923 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1919 'CSRF token. If the endpoint does not require such token you can ' +
1924 'CSRF token. If the endpoint does not require such token you can ' +
1920 'explicitly set the parameter needs_csrf_token to false.')
1925 'explicitly set the parameter needs_csrf_token to false.')
1921
1926
1922 return insecure_form(url, method=method, **attrs)
1927 return insecure_form(url, method=method, **attrs)
1923
1928
1924
1929
1925 def secure_form(form_url, method="POST", multipart=False, **attrs):
1930 def secure_form(form_url, method="POST", multipart=False, **attrs):
1926 """Start a form tag that points the action to an url. This
1931 """Start a form tag that points the action to an url. This
1927 form tag will also include the hidden field containing
1932 form tag will also include the hidden field containing
1928 the auth token.
1933 the auth token.
1929
1934
1930 The url options should be given either as a string, or as a
1935 The url options should be given either as a string, or as a
1931 ``url()`` function. The method for the form defaults to POST.
1936 ``url()`` function. The method for the form defaults to POST.
1932
1937
1933 Options:
1938 Options:
1934
1939
1935 ``multipart``
1940 ``multipart``
1936 If set to True, the enctype is set to "multipart/form-data".
1941 If set to True, the enctype is set to "multipart/form-data".
1937 ``method``
1942 ``method``
1938 The method to use when submitting the form, usually either
1943 The method to use when submitting the form, usually either
1939 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1944 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1940 hidden input with name _method is added to simulate the verb
1945 hidden input with name _method is added to simulate the verb
1941 over POST.
1946 over POST.
1942
1947
1943 """
1948 """
1944
1949
1945 if 'request' in attrs:
1950 if 'request' in attrs:
1946 session = attrs['request'].session
1951 session = attrs['request'].session
1947 del attrs['request']
1952 del attrs['request']
1948 else:
1953 else:
1949 raise ValueError(
1954 raise ValueError(
1950 'Calling this form requires request= to be passed as argument')
1955 'Calling this form requires request= to be passed as argument')
1951
1956
1952 _form = insecure_form(form_url, method, multipart, **attrs)
1957 _form = insecure_form(form_url, method, multipart, **attrs)
1953 token = literal(
1958 token = literal(
1954 '<input type="hidden" name="{}" value="{}">'.format(
1959 '<input type="hidden" name="{}" value="{}">'.format(
1955 csrf_token_key, get_csrf_token(session)))
1960 csrf_token_key, get_csrf_token(session)))
1956
1961
1957 return literal("%s\n%s" % (_form, token))
1962 return literal("%s\n%s" % (_form, token))
1958
1963
1959
1964
1960 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1965 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1961 select_html = select(name, selected, options, **attrs)
1966 select_html = select(name, selected, options, **attrs)
1962
1967
1963 select2 = """
1968 select2 = """
1964 <script>
1969 <script>
1965 $(document).ready(function() {
1970 $(document).ready(function() {
1966 $('#%s').select2({
1971 $('#%s').select2({
1967 containerCssClass: 'drop-menu %s',
1972 containerCssClass: 'drop-menu %s',
1968 dropdownCssClass: 'drop-menu-dropdown',
1973 dropdownCssClass: 'drop-menu-dropdown',
1969 dropdownAutoWidth: true%s
1974 dropdownAutoWidth: true%s
1970 });
1975 });
1971 });
1976 });
1972 </script>
1977 </script>
1973 """
1978 """
1974
1979
1975 filter_option = """,
1980 filter_option = """,
1976 minimumResultsForSearch: -1
1981 minimumResultsForSearch: -1
1977 """
1982 """
1978 input_id = attrs.get('id') or name
1983 input_id = attrs.get('id') or name
1979 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1984 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1980 filter_enabled = "" if enable_filter else filter_option
1985 filter_enabled = "" if enable_filter else filter_option
1981 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1986 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1982
1987
1983 return literal(select_html+select_script)
1988 return literal(select_html+select_script)
1984
1989
1985
1990
1986 def get_visual_attr(tmpl_context_var, attr_name):
1991 def get_visual_attr(tmpl_context_var, attr_name):
1987 """
1992 """
1988 A safe way to get a variable from visual variable of template context
1993 A safe way to get a variable from visual variable of template context
1989
1994
1990 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1995 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1991 :param attr_name: name of the attribute we fetch from the c.visual
1996 :param attr_name: name of the attribute we fetch from the c.visual
1992 """
1997 """
1993 visual = getattr(tmpl_context_var, 'visual', None)
1998 visual = getattr(tmpl_context_var, 'visual', None)
1994 if not visual:
1999 if not visual:
1995 return
2000 return
1996 else:
2001 else:
1997 return getattr(visual, attr_name, None)
2002 return getattr(visual, attr_name, None)
1998
2003
1999
2004
2000 def get_last_path_part(file_node):
2005 def get_last_path_part(file_node):
2001 if not file_node.path:
2006 if not file_node.path:
2002 return u'/'
2007 return u'/'
2003
2008
2004 path = safe_unicode(file_node.path.split('/')[-1])
2009 path = safe_unicode(file_node.path.split('/')[-1])
2005 return u'../' + path
2010 return u'../' + path
2006
2011
2007
2012
2008 def route_url(*args, **kwargs):
2013 def route_url(*args, **kwargs):
2009 """
2014 """
2010 Wrapper around pyramids `route_url` (fully qualified url) function.
2015 Wrapper around pyramids `route_url` (fully qualified url) function.
2011 """
2016 """
2012 req = get_current_request()
2017 req = get_current_request()
2013 return req.route_url(*args, **kwargs)
2018 return req.route_url(*args, **kwargs)
2014
2019
2015
2020
2016 def route_path(*args, **kwargs):
2021 def route_path(*args, **kwargs):
2017 """
2022 """
2018 Wrapper around pyramids `route_path` function.
2023 Wrapper around pyramids `route_path` function.
2019 """
2024 """
2020 req = get_current_request()
2025 req = get_current_request()
2021 return req.route_path(*args, **kwargs)
2026 return req.route_path(*args, **kwargs)
2022
2027
2023
2028
2024 def route_path_or_none(*args, **kwargs):
2029 def route_path_or_none(*args, **kwargs):
2025 try:
2030 try:
2026 return route_path(*args, **kwargs)
2031 return route_path(*args, **kwargs)
2027 except KeyError:
2032 except KeyError:
2028 return None
2033 return None
2029
2034
2030
2035
2031 def current_route_path(request, **kw):
2036 def current_route_path(request, **kw):
2032 new_args = request.GET.mixed()
2037 new_args = request.GET.mixed()
2033 new_args.update(kw)
2038 new_args.update(kw)
2034 return request.current_route_path(_query=new_args)
2039 return request.current_route_path(_query=new_args)
2035
2040
2036
2041
2037 def curl_api_example(method, args):
2042 def curl_api_example(method, args):
2038 args_json = json.dumps(OrderedDict([
2043 args_json = json.dumps(OrderedDict([
2039 ('id', 1),
2044 ('id', 1),
2040 ('auth_token', 'SECRET'),
2045 ('auth_token', 'SECRET'),
2041 ('method', method),
2046 ('method', method),
2042 ('args', args)
2047 ('args', args)
2043 ]))
2048 ]))
2044
2049
2045 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2050 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2046 api_url=route_url('apiv2'),
2051 api_url=route_url('apiv2'),
2047 args_json=args_json
2052 args_json=args_json
2048 )
2053 )
2049
2054
2050
2055
2051 def api_call_example(method, args):
2056 def api_call_example(method, args):
2052 """
2057 """
2053 Generates an API call example via CURL
2058 Generates an API call example via CURL
2054 """
2059 """
2055 curl_call = curl_api_example(method, args)
2060 curl_call = curl_api_example(method, args)
2056
2061
2057 return literal(
2062 return literal(
2058 curl_call +
2063 curl_call +
2059 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2064 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2060 "and needs to be of `api calls` role."
2065 "and needs to be of `api calls` role."
2061 .format(token_url=route_url('my_account_auth_tokens')))
2066 .format(token_url=route_url('my_account_auth_tokens')))
2062
2067
2063
2068
2064 def notification_description(notification, request):
2069 def notification_description(notification, request):
2065 """
2070 """
2066 Generate notification human readable description based on notification type
2071 Generate notification human readable description based on notification type
2067 """
2072 """
2068 from rhodecode.model.notification import NotificationModel
2073 from rhodecode.model.notification import NotificationModel
2069 return NotificationModel().make_description(
2074 return NotificationModel().make_description(
2070 notification, translate=request.translate)
2075 notification, translate=request.translate)
2071
2076
2072
2077
2073 def go_import_header(request, db_repo=None):
2078 def go_import_header(request, db_repo=None):
2074 """
2079 """
2075 Creates a header for go-import functionality in Go Lang
2080 Creates a header for go-import functionality in Go Lang
2076 """
2081 """
2077
2082
2078 if not db_repo:
2083 if not db_repo:
2079 return
2084 return
2080 if 'go-get' not in request.GET:
2085 if 'go-get' not in request.GET:
2081 return
2086 return
2082
2087
2083 clone_url = db_repo.clone_url()
2088 clone_url = db_repo.clone_url()
2084 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2089 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2085 # we have a repo and go-get flag,
2090 # we have a repo and go-get flag,
2086 return literal('<meta name="go-import" content="{} {} {}">'.format(
2091 return literal('<meta name="go-import" content="{} {} {}">'.format(
2087 prefix, db_repo.repo_type, clone_url))
2092 prefix, db_repo.repo_type, clone_url))
2088
2093
2089
2094
2090 def reviewer_as_json(*args, **kwargs):
2095 def reviewer_as_json(*args, **kwargs):
2091 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2096 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2092 return _reviewer_as_json(*args, **kwargs)
2097 return _reviewer_as_json(*args, **kwargs)
2093
2098
2094
2099
2095 def get_repo_view_type(request):
2100 def get_repo_view_type(request):
2096 route_name = request.matched_route.name
2101 route_name = request.matched_route.name
2097 route_to_view_type = {
2102 route_to_view_type = {
2098 'repo_changelog': 'commits',
2103 'repo_changelog': 'commits',
2099 'repo_commits': 'commits',
2104 'repo_commits': 'commits',
2100 'repo_files': 'files',
2105 'repo_files': 'files',
2101 'repo_summary': 'summary',
2106 'repo_summary': 'summary',
2102 'repo_commit': 'commit'
2107 'repo_commit': 'commit'
2103 }
2108 }
2104
2109
2105 return route_to_view_type.get(route_name)
2110 return route_to_view_type.get(route_name)
2106
2111
2107
2112
2108 def is_active(menu_entry, selected):
2113 def is_active(menu_entry, selected):
2109 """
2114 """
2110 Returns active class for selecting menus in templates
2115 Returns active class for selecting menus in templates
2111 <li class=${h.is_active('settings', current_active)}></li>
2116 <li class=${h.is_active('settings', current_active)}></li>
2112 """
2117 """
2113 if not isinstance(menu_entry, list):
2118 if not isinstance(menu_entry, list):
2114 menu_entry = [menu_entry]
2119 menu_entry = [menu_entry]
2115
2120
2116 if selected in menu_entry:
2121 if selected in menu_entry:
2117 return "active"
2122 return "active"
2123
2124
2125 class IssuesRegistry(object):
2126 """
2127 issue_registry = IssuesRegistry()
2128 some_func(issues_callback=issues_registry(...))
2129 """
2130
2131 def __init__(self):
2132 self.issues = []
2133 self.unique_issues = collections.defaultdict(lambda: [])
2134
2135 def __call__(self, commit_dict=None):
2136 def callback(issue):
2137 if commit_dict and issue:
2138 issue['commit'] = commit_dict
2139 self.issues.append(issue)
2140 self.unique_issues[issue['id']].append(issue)
2141 return callback
2142
2143 def get_issues(self):
2144 return self.issues
2145
2146 @property
2147 def issues_unique_count(self):
2148 return len(set(i['id'] for i in self.issues))
@@ -1,88 +1,88 b''
1 ## Changesets table !
1 ## Changesets table !
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4 %if c.ancestor:
4 %if c.ancestor:
5 <div class="ancestor">${_('Compare was calculated based on this common ancestor commit')}:
5 <div class="ancestor">${_('Compare was calculated based on this common ancestor commit')}:
6 <a href="${h.route_path('repo_commit', repo_name=c.repo_name, commit_id=c.ancestor)}">${h.short_id(c.ancestor)}</a>
6 <a href="${h.route_path('repo_commit', repo_name=c.repo_name, commit_id=c.ancestor)}">${h.short_id(c.ancestor)}</a>
7 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
7 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
8 </div>
8 </div>
9 %endif
9 %endif
10
10
11 <div class="container">
11 <div class="container">
12 <input type="hidden" name="__start__" value="revisions:sequence">
12 <input type="hidden" name="__start__" value="revisions:sequence">
13 <table class="rctable compare_view_commits">
13 <table class="rctable compare_view_commits">
14 <tr>
14 <tr>
15 % if hasattr(c, 'commit_versions'):
15 % if hasattr(c, 'commit_versions'):
16 <th>ver</th>
16 <th>ver</th>
17 % endif
17 % endif
18 <th>${_('Time')}</th>
18 <th>${_('Time')}</th>
19 <th>${_('Author')}</th>
19 <th>${_('Author')}</th>
20 <th>${_('Commit')}</th>
20 <th>${_('Commit')}</th>
21 <th></th>
21 <th></th>
22 <th>${_('Description')}</th>
22 <th>${_('Description')}</th>
23 </tr>
23 </tr>
24 ## to speed up lookups cache some functions before the loop
24 ## to speed up lookups cache some functions before the loop
25 <%
25 <%
26 active_patterns = h.get_active_pattern_entries(c.repo_name)
26 active_patterns = h.get_active_pattern_entries(c.repo_name)
27 urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns, issues_container=getattr(c, 'referenced_commit_issues', None))
27 urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns)
28 %>
28 %>
29
29
30 %for commit in c.commit_ranges:
30 %for commit in c.commit_ranges:
31 <tr id="row-${commit.raw_id}"
31 <tr id="row-${commit.raw_id}"
32 commit_id="${commit.raw_id}"
32 commit_id="${commit.raw_id}"
33 class="compare_select"
33 class="compare_select"
34 style="${'display: none' if c.collapse_all_commits else ''}"
34 style="${'display: none' if c.collapse_all_commits else ''}"
35 >
35 >
36 % if hasattr(c, 'commit_versions'):
36 % if hasattr(c, 'commit_versions'):
37 <td class="tooltip" title="${_('Pull request version this commit was introduced')}">
37 <td class="tooltip" title="${_('Pull request version this commit was introduced')}">
38 <code>${('v{}'.format(c.commit_versions[commit.raw_id][0]) if c.commit_versions[commit.raw_id] else 'latest')}</code>
38 <code>${('v{}'.format(c.commit_versions[commit.raw_id][0]) if c.commit_versions[commit.raw_id] else 'latest')}</code>
39 </td>
39 </td>
40 % endif
40 % endif
41 <td class="td-time">
41 <td class="td-time">
42 ${h.age_component(commit.date)}
42 ${h.age_component(commit.date)}
43 </td>
43 </td>
44 <td class="td-user">
44 <td class="td-user">
45 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
45 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
46 </td>
46 </td>
47 <td class="td-hash">
47 <td class="td-hash">
48 <code>
48 <code>
49 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
49 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
50 r${commit.idx}:${h.short_id(commit.raw_id)}
50 r${commit.idx}:${h.short_id(commit.raw_id)}
51 </a>
51 </a>
52 ${h.hidden('revisions',commit.raw_id)}
52 ${h.hidden('revisions',commit.raw_id)}
53 </code>
53 </code>
54 </td>
54 </td>
55 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
55 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
56 <i class="icon-expand-linked"></i>
56 <i class="icon-expand-linked"></i>
57 </td>
57 </td>
58 <td class="mid td-description">
58 <td class="mid td-description">
59 <div class="log-container truncate-wrap">
59 <div class="log-container truncate-wrap">
60 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${urlify_commit_message(commit.message, c.repo_name)}</div>
60 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${urlify_commit_message(commit.message, c.repo_name, issues_container_callback=getattr(c, 'referenced_commit_issues', lambda p:p)(commit.serialize()))}</div>
61 </div>
61 </div>
62 </td>
62 </td>
63 </tr>
63 </tr>
64 %endfor
64 %endfor
65 <tr class="compare_select_hidden" style="${('' if c.collapse_all_commits else 'display: none')}">
65 <tr class="compare_select_hidden" style="${('' if c.collapse_all_commits else 'display: none')}">
66 <td colspan="5">
66 <td colspan="5">
67 ${_ungettext('{} commit hidden, click expand to show them.', '{} commits hidden, click expand to show them.', len(c.commit_ranges)).format(len(c.commit_ranges))}
67 ${_ungettext('{} commit hidden, click expand to show them.', '{} commits hidden, click expand to show them.', len(c.commit_ranges)).format(len(c.commit_ranges))}
68 </td>
68 </td>
69 </tr>
69 </tr>
70 % if not c.commit_ranges:
70 % if not c.commit_ranges:
71 <tr class="compare_select">
71 <tr class="compare_select">
72 <td colspan="5">
72 <td colspan="5">
73 ${_('No commits in this compare')}
73 ${_('No commits in this compare')}
74 </td>
74 </td>
75 </tr>
75 </tr>
76 % endif
76 % endif
77 </table>
77 </table>
78 <input type="hidden" name="__end__" value="revisions:sequence">
78 <input type="hidden" name="__end__" value="revisions:sequence">
79
79
80 </div>
80 </div>
81
81
82 <script>
82 <script>
83 commitsController = new CommitsController();
83 commitsController = new CommitsController();
84 $('.compare_select').on('click',function(e){
84 $('.compare_select').on('click',function(e){
85 var cid = $(this).attr('commit_id');
85 var cid = $(this).attr('commit_id');
86 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
86 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
87 });
87 });
88 </script>
88 </script>
@@ -1,1052 +1,1055 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 <%namespace name="sidebar" file="/base/sidebar.mako"/>
4 <%namespace name="sidebar" file="/base/sidebar.mako"/>
5
5
6
6
7 <%def name="title()">
7 <%def name="title()">
8 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
8 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
9 %if c.rhodecode_name:
9 %if c.rhodecode_name:
10 &middot; ${h.branding(c.rhodecode_name)}
10 &middot; ${h.branding(c.rhodecode_name)}
11 %endif
11 %endif
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()">
14 <%def name="breadcrumbs_links()">
15
15
16 </%def>
16 </%def>
17
17
18 <%def name="menu_bar_nav()">
18 <%def name="menu_bar_nav()">
19 ${self.menu_items(active='repositories')}
19 ${self.menu_items(active='repositories')}
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_subnav()">
22 <%def name="menu_bar_subnav()">
23 ${self.repo_menu(active='showpullrequest')}
23 ${self.repo_menu(active='showpullrequest')}
24 </%def>
24 </%def>
25
25
26
26
27 <%def name="main()">
27 <%def name="main()">
28 ## Container to gather extracted Tickets
28 ## Container to gather extracted Tickets
29 <%
29 <%
30 c.referenced_commit_issues = []
30 c.referenced_commit_issues = h.IssuesRegistry()
31 c.referenced_desc_issues = []
31 c.referenced_desc_issues = h.IssuesRegistry()
32 %>
32 %>
33
33
34 <script type="text/javascript">
34 <script type="text/javascript">
35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
36 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
37 </script>
37 </script>
38
38
39 <div class="box">
39 <div class="box">
40
40
41 <div class="box pr-summary">
41 <div class="box pr-summary">
42
42
43 <div class="summary-details block-left">
43 <div class="summary-details block-left">
44 <div id="pr-title">
44 <div id="pr-title">
45 % if c.pull_request.is_closed():
45 % if c.pull_request.is_closed():
46 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
46 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
47 % endif
47 % endif
48 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
48 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
49 </div>
49 </div>
50 <div id="pr-title-edit" class="input" style="display: none;">
50 <div id="pr-title-edit" class="input" style="display: none;">
51 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
51 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
52 </div>
52 </div>
53
53
54 <% summary = lambda n:{False:'summary-short'}.get(n) %>
54 <% summary = lambda n:{False:'summary-short'}.get(n) %>
55 <div class="pr-details-title">
55 <div class="pr-details-title">
56 <div class="pull-left">
56 <div class="pull-left">
57 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a>
57 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a>
58 ${_('Created on')}
58 ${_('Created on')}
59 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
59 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
60 <span class="pr-details-title-author-pref">${_('by')}</span>
60 <span class="pr-details-title-author-pref">${_('by')}</span>
61 </div>
61 </div>
62
62
63 <div class="pull-left">
63 <div class="pull-left">
64 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
64 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
65 </div>
65 </div>
66
66
67 %if c.allowed_to_update:
67 %if c.allowed_to_update:
68 <div class="pull-right">
68 <div class="pull-right">
69 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
69 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
70 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
70 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
71 % if c.allowed_to_delete:
71 % if c.allowed_to_delete:
72 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
72 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
73 <input class="btn btn-link btn-danger no-margin" id="remove_${c.pull_request.pull_request_id}" name="remove_${c.pull_request.pull_request_id}"
73 <input class="btn btn-link btn-danger no-margin" id="remove_${c.pull_request.pull_request_id}" name="remove_${c.pull_request.pull_request_id}"
74 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
74 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
75 type="submit" value="${_('Delete pull request')}">
75 type="submit" value="${_('Delete pull request')}">
76 ${h.end_form()}
76 ${h.end_form()}
77 % else:
77 % else:
78 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
78 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
79 % endif
79 % endif
80 </div>
80 </div>
81 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
81 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
82 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
82 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
83 </div>
83 </div>
84
84
85 %endif
85 %endif
86 </div>
86 </div>
87
87
88 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
88 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
89 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container=c.referenced_desc_issues)}
89 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container_callback=c.referenced_desc_issues())}
90 </div>
90 </div>
91
91
92 <div id="pr-desc-edit" class="input textarea" style="display: none;">
92 <div id="pr-desc-edit" class="input textarea" style="display: none;">
93 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
93 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
94 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
94 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
95 </div>
95 </div>
96
96
97 <div id="summary" class="fields pr-details-content">
97 <div id="summary" class="fields pr-details-content">
98
98
99 ## source
99 ## source
100 <div class="field">
100 <div class="field">
101 <div class="label-pr-detail">
101 <div class="label-pr-detail">
102 <label>${_('Commit flow')}:</label>
102 <label>${_('Commit flow')}:</label>
103 </div>
103 </div>
104 <div class="input">
104 <div class="input">
105 <div class="pr-commit-flow">
105 <div class="pr-commit-flow">
106 ## Source
106 ## Source
107 %if c.pull_request.source_ref_parts.type == 'branch':
107 %if c.pull_request.source_ref_parts.type == 'branch':
108 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a>
108 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a>
109 %else:
109 %else:
110 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
110 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
111 %endif
111 %endif
112 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a>
112 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a>
113 &rarr;
113 &rarr;
114 ## Target
114 ## Target
115 %if c.pull_request.target_ref_parts.type == 'branch':
115 %if c.pull_request.target_ref_parts.type == 'branch':
116 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}"><code class="pr-target-info">${c.pull_request.target_ref_parts.type}:${c.pull_request.target_ref_parts.name}</code></a>
116 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}"><code class="pr-target-info">${c.pull_request.target_ref_parts.type}:${c.pull_request.target_ref_parts.name}</code></a>
117 %else:
117 %else:
118 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
118 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
119 %endif
119 %endif
120
120
121 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
121 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
122
122
123 <a class="source-details-action" href="#expand-source-details" onclick="return toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
123 <a class="source-details-action" href="#expand-source-details" onclick="return toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
124 <i class="icon-angle-down">more details</i>
124 <i class="icon-angle-down">more details</i>
125 </a>
125 </a>
126
126
127 </div>
127 </div>
128
128
129 <div class="source-details" style="display: none">
129 <div class="source-details" style="display: none">
130
130
131 <ul>
131 <ul>
132
132
133 ## common ancestor
133 ## common ancestor
134 <li>
134 <li>
135 ${_('Common ancestor')}:
135 ${_('Common ancestor')}:
136 % if c.ancestor_commit:
136 % if c.ancestor_commit:
137 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a>
137 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a>
138 % else:
138 % else:
139 ${_('not available')}
139 ${_('not available')}
140 % endif
140 % endif
141 </li>
141 </li>
142
142
143 ## pull url
143 ## pull url
144 <li>
144 <li>
145 %if h.is_hg(c.pull_request.source_repo):
145 %if h.is_hg(c.pull_request.source_repo):
146 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
146 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
147 %elif h.is_git(c.pull_request.source_repo):
147 %elif h.is_git(c.pull_request.source_repo):
148 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
148 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
149 %endif
149 %endif
150
150
151 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
151 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
152 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
152 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
153 </li>
153 </li>
154
154
155 ## Shadow repo
155 ## Shadow repo
156 <li>
156 <li>
157 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
157 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
158 %if h.is_hg(c.pull_request.target_repo):
158 %if h.is_hg(c.pull_request.target_repo):
159 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
159 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
160 %elif h.is_git(c.pull_request.target_repo):
160 %elif h.is_git(c.pull_request.target_repo):
161 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
161 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
162 %endif
162 %endif
163
163
164 <span class="tooltip" title="${_('Clone repository in its merged state using shadow repository')}">${_('Clone from shadow repository')}</span>: <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
164 <span class="tooltip" title="${_('Clone repository in its merged state using shadow repository')}">${_('Clone from shadow repository')}</span>: <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
165 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
165 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
166
166
167 % else:
167 % else:
168 <div class="">
168 <div class="">
169 ${_('Shadow repository data not available')}.
169 ${_('Shadow repository data not available')}.
170 </div>
170 </div>
171 % endif
171 % endif
172 </li>
172 </li>
173
173
174 </ul>
174 </ul>
175
175
176 </div>
176 </div>
177
177
178 </div>
178 </div>
179
179
180 </div>
180 </div>
181
181
182 ## versions
182 ## versions
183 <div class="field">
183 <div class="field">
184 <div class="label-pr-detail">
184 <div class="label-pr-detail">
185 <label>${_('Versions')}:</label>
185 <label>${_('Versions')}:</label>
186 </div>
186 </div>
187
187
188 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
188 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
189 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
189 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
190
190
191 <div class="pr-versions">
191 <div class="pr-versions">
192 % if c.show_version_changes:
192 % if c.show_version_changes:
193 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
193 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
194 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
194 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
195 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
195 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
196 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
196 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
197 data-toggle-on="${_('show versions')}."
197 data-toggle-on="${_('show versions')}."
198 data-toggle-off="${_('hide versions')}.">
198 data-toggle-off="${_('hide versions')}.">
199 ${_('show versions')}.
199 ${_('show versions')}.
200 </a>
200 </a>
201 <table>
201 <table>
202 ## SHOW ALL VERSIONS OF PR
202 ## SHOW ALL VERSIONS OF PR
203 <% ver_pr = None %>
203 <% ver_pr = None %>
204
204
205 % for data in reversed(list(enumerate(c.versions, 1))):
205 % for data in reversed(list(enumerate(c.versions, 1))):
206 <% ver_pos = data[0] %>
206 <% ver_pos = data[0] %>
207 <% ver = data[1] %>
207 <% ver = data[1] %>
208 <% ver_pr = ver.pull_request_version_id %>
208 <% ver_pr = ver.pull_request_version_id %>
209 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
209 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
210
210
211 <tr class="version-pr" style="display: ${display_row}">
211 <tr class="version-pr" style="display: ${display_row}">
212 <td>
212 <td>
213 <code>
213 <code>
214 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
214 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
215 </code>
215 </code>
216 </td>
216 </td>
217 <td>
217 <td>
218 <input ${('checked="checked"' if c.from_version_index == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
218 <input ${('checked="checked"' if c.from_version_index == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
219 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
219 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
220 </td>
220 </td>
221 <td>
221 <td>
222 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
222 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
223 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
223 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
224
224
225 </td>
225 </td>
226 <td>
226 <td>
227 % if c.at_version_num != ver_pr:
227 % if c.at_version_num != ver_pr:
228 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
228 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
229 <code>
229 <code>
230 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
230 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
231 </code>
231 </code>
232 % endif
232 % endif
233 </td>
233 </td>
234 <td>
234 <td>
235 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
235 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
236 </td>
236 </td>
237 <td>
237 <td>
238 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
238 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
239 </td>
239 </td>
240 </tr>
240 </tr>
241 % endfor
241 % endfor
242
242
243 <tr>
243 <tr>
244 <td colspan="6">
244 <td colspan="6">
245 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
245 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
246 data-label-text-locked="${_('select versions to show changes')}"
246 data-label-text-locked="${_('select versions to show changes')}"
247 data-label-text-diff="${_('show changes between versions')}"
247 data-label-text-diff="${_('show changes between versions')}"
248 data-label-text-show="${_('show pull request for this version')}"
248 data-label-text-show="${_('show pull request for this version')}"
249 >
249 >
250 ${_('select versions to show changes')}
250 ${_('select versions to show changes')}
251 </button>
251 </button>
252 </td>
252 </td>
253 </tr>
253 </tr>
254 </table>
254 </table>
255 % else:
255 % else:
256 <div>
256 <div>
257 ${_('Pull request versions not available')}.
257 ${_('Pull request versions not available')}.
258 </div>
258 </div>
259 % endif
259 % endif
260 </div>
260 </div>
261 </div>
261 </div>
262
262
263 </div>
263 </div>
264
264
265 </div>
265 </div>
266
266
267
267
268 </div>
268 </div>
269
269
270 </div>
270 </div>
271
271
272 <div class="box">
272 <div class="box">
273
273
274 % if c.state_progressing:
274 % if c.state_progressing:
275
275
276 <h2 style="text-align: center">
276 <h2 style="text-align: center">
277 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
277 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
278
278
279 % if c.is_super_admin:
279 % if c.is_super_admin:
280 <br/>
280 <br/>
281 If you think this is an error try <a href="${h.current_route_path(request, force_state='created')}">forced state reset</a> to <span class="tag tag-merge-state-created">created</span> state.
281 If you think this is an error try <a href="${h.current_route_path(request, force_state='created')}">forced state reset</a> to <span class="tag tag-merge-state-created">created</span> state.
282 % endif
282 % endif
283 </h2>
283 </h2>
284
284
285 % else:
285 % else:
286
286
287 ## Diffs rendered here
287 ## Diffs rendered here
288 <div class="table" >
288 <div class="table" >
289 <div id="changeset_compare_view_content">
289 <div id="changeset_compare_view_content">
290 ##CS
290 ##CS
291 % if c.missing_requirements:
291 % if c.missing_requirements:
292 <div class="box">
292 <div class="box">
293 <div class="alert alert-warning">
293 <div class="alert alert-warning">
294 <div>
294 <div>
295 <strong>${_('Missing requirements:')}</strong>
295 <strong>${_('Missing requirements:')}</strong>
296 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
296 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
297 </div>
297 </div>
298 </div>
298 </div>
299 </div>
299 </div>
300 % elif c.missing_commits:
300 % elif c.missing_commits:
301 <div class="box">
301 <div class="box">
302 <div class="alert alert-warning">
302 <div class="alert alert-warning">
303 <div>
303 <div>
304 <strong>${_('Missing commits')}:</strong>
304 <strong>${_('Missing commits')}:</strong>
305 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
305 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
306 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
306 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
307 ${_('Consider doing a `force update commits` in case you think this is an error.')}
307 ${_('Consider doing a `force update commits` in case you think this is an error.')}
308 </div>
308 </div>
309 </div>
309 </div>
310 </div>
310 </div>
311 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
311 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
312 <div class="box">
312 <div class="box">
313 <div class="alert alert-info">
313 <div class="alert alert-info">
314 <div>
314 <div>
315 <strong>${_('There are new changes for `{}:{}` in source repository, please consider updating this pull request.').format(c.pr_merge_source_commit.ref_spec.type, c.pr_merge_source_commit.ref_spec.name)}</strong>
315 <strong>${_('There are new changes for `{}:{}` in source repository, please consider updating this pull request.').format(c.pr_merge_source_commit.ref_spec.type, c.pr_merge_source_commit.ref_spec.name)}</strong>
316 </div>
316 </div>
317 </div>
317 </div>
318 </div>
318 </div>
319 % endif
319 % endif
320
320
321 <div class="compare_view_commits_title">
321 <div class="compare_view_commits_title">
322 % if not c.compare_mode:
322 % if not c.compare_mode:
323
323
324 % if c.at_version_index:
324 % if c.at_version_index:
325 <h4>
325 <h4>
326 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
326 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
327 </h4>
327 </h4>
328 % endif
328 % endif
329
329
330 <div class="pull-left">
330 <div class="pull-left">
331 <div class="btn-group">
331 <div class="btn-group">
332 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
332 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
333 % if c.collapse_all_commits:
333 % if c.collapse_all_commits:
334 <i class="icon-plus-squared-alt icon-no-margin"></i>
334 <i class="icon-plus-squared-alt icon-no-margin"></i>
335 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
335 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
336 % else:
336 % else:
337 <i class="icon-minus-squared-alt icon-no-margin"></i>
337 <i class="icon-minus-squared-alt icon-no-margin"></i>
338 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
338 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
339 % endif
339 % endif
340 </a>
340 </a>
341 </div>
341 </div>
342 </div>
342 </div>
343
343
344 <div class="pull-right">
344 <div class="pull-right">
345 % if c.allowed_to_update and not c.pull_request.is_closed():
345 % if c.allowed_to_update and not c.pull_request.is_closed():
346
346
347 <div class="btn-group btn-group-actions">
347 <div class="btn-group btn-group-actions">
348 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
348 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
349 ${_('Update commits')}
349 ${_('Update commits')}
350 </a>
350 </a>
351
351
352 <a id="update_commits_switcher" class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
352 <a id="update_commits_switcher" class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
353 <i class="icon-down"></i>
353 <i class="icon-down"></i>
354 </a>
354 </a>
355
355
356 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
356 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
357 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
357 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
358 <li>
358 <li>
359 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
359 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
360 ${_('Force update commits')}
360 ${_('Force update commits')}
361 </a>
361 </a>
362 <div class="action-help-block">
362 <div class="action-help-block">
363 ${_('Update commits and force refresh this pull request.')}
363 ${_('Update commits and force refresh this pull request.')}
364 </div>
364 </div>
365 </li>
365 </li>
366 </ul>
366 </ul>
367 </div>
367 </div>
368 </div>
368 </div>
369
369
370 % else:
370 % else:
371 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
371 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
372 % endif
372 % endif
373
373
374 </div>
374 </div>
375 % endif
375 % endif
376 </div>
376 </div>
377
377
378 % if not c.missing_commits:
378 % if not c.missing_commits:
379 ## COMPARE RANGE DIFF MODE
379 ## COMPARE RANGE DIFF MODE
380 % if c.compare_mode:
380 % if c.compare_mode:
381 % if c.at_version:
381 % if c.at_version:
382 <h4>
382 <h4>
383 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_index, ver_to=c.at_version_index if c.at_version_index else 'latest')}:
383 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_index, ver_to=c.at_version_index if c.at_version_index else 'latest')}:
384 </h4>
384 </h4>
385
385
386 <div class="subtitle-compare">
386 <div class="subtitle-compare">
387 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
387 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
388 </div>
388 </div>
389
389
390 <div class="container">
390 <div class="container">
391 <table class="rctable compare_view_commits">
391 <table class="rctable compare_view_commits">
392 <tr>
392 <tr>
393 <th></th>
393 <th></th>
394 <th>${_('Time')}</th>
394 <th>${_('Time')}</th>
395 <th>${_('Author')}</th>
395 <th>${_('Author')}</th>
396 <th>${_('Commit')}</th>
396 <th>${_('Commit')}</th>
397 <th></th>
397 <th></th>
398 <th>${_('Description')}</th>
398 <th>${_('Description')}</th>
399 </tr>
399 </tr>
400
400
401 % for c_type, commit in c.commit_changes:
401 % for c_type, commit in c.commit_changes:
402 % if c_type in ['a', 'r']:
402 % if c_type in ['a', 'r']:
403 <%
403 <%
404 if c_type == 'a':
404 if c_type == 'a':
405 cc_title = _('Commit added in displayed changes')
405 cc_title = _('Commit added in displayed changes')
406 elif c_type == 'r':
406 elif c_type == 'r':
407 cc_title = _('Commit removed in displayed changes')
407 cc_title = _('Commit removed in displayed changes')
408 else:
408 else:
409 cc_title = ''
409 cc_title = ''
410 %>
410 %>
411 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
411 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
412 <td>
412 <td>
413 <div class="commit-change-indicator color-${c_type}-border">
413 <div class="commit-change-indicator color-${c_type}-border">
414 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
414 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
415 ${c_type.upper()}
415 ${c_type.upper()}
416 </div>
416 </div>
417 </div>
417 </div>
418 </td>
418 </td>
419 <td class="td-time">
419 <td class="td-time">
420 ${h.age_component(commit.date)}
420 ${h.age_component(commit.date)}
421 </td>
421 </td>
422 <td class="td-user">
422 <td class="td-user">
423 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
423 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
424 </td>
424 </td>
425 <td class="td-hash">
425 <td class="td-hash">
426 <code>
426 <code>
427 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
427 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
428 r${commit.idx}:${h.short_id(commit.raw_id)}
428 r${commit.idx}:${h.short_id(commit.raw_id)}
429 </a>
429 </a>
430 ${h.hidden('revisions', commit.raw_id)}
430 ${h.hidden('revisions', commit.raw_id)}
431 </code>
431 </code>
432 </td>
432 </td>
433 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
433 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
434 <i class="icon-expand-linked"></i>
434 <i class="icon-expand-linked"></i>
435 </td>
435 </td>
436 <td class="mid td-description">
436 <td class="mid td-description">
437 <div class="log-container truncate-wrap">
437 <div class="log-container truncate-wrap">
438 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name, issues_container=c.referenced_commit_issues)}</div>
438 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name, issues_container_callback=c.referenced_commit_issues(commit.serialize()))}</div>
439 </div>
439 </div>
440 </td>
440 </td>
441 </tr>
441 </tr>
442 % endif
442 % endif
443 % endfor
443 % endfor
444 </table>
444 </table>
445 </div>
445 </div>
446
446
447 % endif
447 % endif
448
448
449 ## Regular DIFF
449 ## Regular DIFF
450 % else:
450 % else:
451 <%include file="/compare/compare_commits.mako" />
451 <%include file="/compare/compare_commits.mako" />
452 % endif
452 % endif
453
453
454 <div class="cs_files">
454 <div class="cs_files">
455 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
455 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
456
456
457 <%
457 <%
458 pr_menu_data = {
458 pr_menu_data = {
459 'outdated_comm_count_ver': outdated_comm_count_ver,
459 'outdated_comm_count_ver': outdated_comm_count_ver,
460 'pull_request': c.pull_request
460 'pull_request': c.pull_request
461 }
461 }
462 %>
462 %>
463
463
464 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
464 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
465
465
466 % if c.range_diff_on:
466 % if c.range_diff_on:
467 % for commit in c.commit_ranges:
467 % for commit in c.commit_ranges:
468 ${cbdiffs.render_diffset(
468 ${cbdiffs.render_diffset(
469 c.changes[commit.raw_id],
469 c.changes[commit.raw_id],
470 commit=commit, use_comments=True,
470 commit=commit, use_comments=True,
471 collapse_when_files_over=5,
471 collapse_when_files_over=5,
472 disable_new_comments=True,
472 disable_new_comments=True,
473 deleted_files_comments=c.deleted_files_comments,
473 deleted_files_comments=c.deleted_files_comments,
474 inline_comments=c.inline_comments,
474 inline_comments=c.inline_comments,
475 pull_request_menu=pr_menu_data, show_todos=False)}
475 pull_request_menu=pr_menu_data, show_todos=False)}
476 % endfor
476 % endfor
477 % else:
477 % else:
478 ${cbdiffs.render_diffset(
478 ${cbdiffs.render_diffset(
479 c.diffset, use_comments=True,
479 c.diffset, use_comments=True,
480 collapse_when_files_over=30,
480 collapse_when_files_over=30,
481 disable_new_comments=not c.allowed_to_comment,
481 disable_new_comments=not c.allowed_to_comment,
482 deleted_files_comments=c.deleted_files_comments,
482 deleted_files_comments=c.deleted_files_comments,
483 inline_comments=c.inline_comments,
483 inline_comments=c.inline_comments,
484 pull_request_menu=pr_menu_data, show_todos=False)}
484 pull_request_menu=pr_menu_data, show_todos=False)}
485 % endif
485 % endif
486
486
487 </div>
487 </div>
488 % else:
488 % else:
489 ## skipping commits we need to clear the view for missing commits
489 ## skipping commits we need to clear the view for missing commits
490 <div style="clear:both;"></div>
490 <div style="clear:both;"></div>
491 % endif
491 % endif
492
492
493 </div>
493 </div>
494 </div>
494 </div>
495
495
496 ## template for inline comment form
496 ## template for inline comment form
497 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
497 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
498
498
499 ## comments heading with count
499 ## comments heading with count
500 <div class="comments-heading">
500 <div class="comments-heading">
501 <i class="icon-comment"></i>
501 <i class="icon-comment"></i>
502 ${_('General Comments')} ${len(c.comments)}
502 ${_('General Comments')} ${len(c.comments)}
503 </div>
503 </div>
504
504
505 ## render general comments
505 ## render general comments
506 <div id="comment-tr-show">
506 <div id="comment-tr-show">
507 % if general_outdated_comm_count_ver:
507 % if general_outdated_comm_count_ver:
508 <div class="info-box">
508 <div class="info-box">
509 % if general_outdated_comm_count_ver == 1:
509 % if general_outdated_comm_count_ver == 1:
510 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
510 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
511 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
511 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
512 % else:
512 % else:
513 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
513 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
514 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
514 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
515 % endif
515 % endif
516 </div>
516 </div>
517 % endif
517 % endif
518 </div>
518 </div>
519
519
520 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
520 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
521
521
522 % if not c.pull_request.is_closed():
522 % if not c.pull_request.is_closed():
523 ## main comment form and it status
523 ## main comment form and it status
524 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
524 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
525 pull_request_id=c.pull_request.pull_request_id),
525 pull_request_id=c.pull_request.pull_request_id),
526 c.pull_request_review_status,
526 c.pull_request_review_status,
527 is_pull_request=True, change_status=c.allowed_to_change_status)}
527 is_pull_request=True, change_status=c.allowed_to_change_status)}
528
528
529 ## merge status, and merge action
529 ## merge status, and merge action
530 <div class="pull-request-merge">
530 <div class="pull-request-merge">
531 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
531 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
532 </div>
532 </div>
533
533
534 %endif
534 %endif
535
535
536 % endif
536 % endif
537 </div>
537 </div>
538
538
539
539
540 ### NAV SIDEBAR
540 ### NAV SIDEBAR
541 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
541 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
542 <div class="sidenav navbar__inner" >
542 <div class="sidenav navbar__inner" >
543 ## TOGGLE
543 ## TOGGLE
544 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
544 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
545 <a href="#toggleSidebar" class="grey-link-action">
545 <a href="#toggleSidebar" class="grey-link-action">
546
546
547 </a>
547 </a>
548 </div>
548 </div>
549
549
550 ## CONTENT
550 ## CONTENT
551 <div class="sidebar-content">
551 <div class="sidebar-content">
552
552
553 ## Drafts
553 ## Drafts
554 % if c.rhodecode_edition_id == 'EE':
554 % if c.rhodecode_edition_id == 'EE':
555 <div id="draftsTable" class="sidebar-element clear-both" style="display: ${'block' if c.draft_comments else 'none'}">
555 <div id="draftsTable" class="sidebar-element clear-both" style="display: ${'block' if c.draft_comments else 'none'}">
556 <div class="tooltip right-sidebar-collapsed-state" style="display: none;" onclick="toggleSidebar(); return false" title="${_('Drafts')}">
556 <div class="tooltip right-sidebar-collapsed-state" style="display: none;" onclick="toggleSidebar(); return false" title="${_('Drafts')}">
557 <i class="icon-comment icon-draft"></i>
557 <i class="icon-comment icon-draft"></i>
558 <span id="drafts-count">${len(c.draft_comments)}</span>
558 <span id="drafts-count">${len(c.draft_comments)}</span>
559 </div>
559 </div>
560
560
561 <div class="right-sidebar-expanded-state pr-details-title">
561 <div class="right-sidebar-expanded-state pr-details-title">
562 <span style="padding-left: 2px">
562 <span style="padding-left: 2px">
563 <input name="select_all_drafts" type="checkbox" onclick="$('[name=submit_draft]').prop('checked', !$('[name=submit_draft]').prop('checked'))">
563 <input name="select_all_drafts" type="checkbox" onclick="$('[name=submit_draft]').prop('checked', !$('[name=submit_draft]').prop('checked'))">
564 </span>
564 </span>
565 <span class="sidebar-heading noselect" onclick="refreshDraftComments(); return false">
565 <span class="sidebar-heading noselect" onclick="refreshDraftComments(); return false">
566 <i class="icon-comment icon-draft"></i>
566 <i class="icon-comment icon-draft"></i>
567 ${_('Drafts')}
567 ${_('Drafts')}
568 </span>
568 </span>
569 <span class="block-right action_button last-item" onclick="submitDrafts(event)">${_('Submit')}</span>
569 <span class="block-right action_button last-item" onclick="submitDrafts(event)">${_('Submit')}</span>
570 </div>
570 </div>
571
571
572 <div id="drafts" class="right-sidebar-expanded-state pr-details-content reviewers">
572 <div id="drafts" class="right-sidebar-expanded-state pr-details-content reviewers">
573 % if c.draft_comments:
573 % if c.draft_comments:
574 ${sidebar.comments_table(c.draft_comments, len(c.draft_comments), draft_comments=True)}
574 ${sidebar.comments_table(c.draft_comments, len(c.draft_comments), draft_comments=True)}
575 % else:
575 % else:
576 <table class="drafts-content-table">
576 <table class="drafts-content-table">
577 <tr>
577 <tr>
578 <td>
578 <td>
579 ${_('No TODOs yet')}
579 ${_('No TODOs yet')}
580 </td>
580 </td>
581 </tr>
581 </tr>
582 </table>
582 </table>
583 % endif
583 % endif
584 </div>
584 </div>
585
585
586 </div>
586 </div>
587 % endif
587 % endif
588
588
589 ## RULES SUMMARY/RULES
589 ## RULES SUMMARY/RULES
590 <div class="sidebar-element clear-both">
590 <div class="sidebar-element clear-both">
591 <% vote_title = _ungettext(
591 <% vote_title = _ungettext(
592 'Status calculated based on votes from {} reviewer',
592 'Status calculated based on votes from {} reviewer',
593 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
593 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
594 %>
594 %>
595
595
596 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
596 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
597 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
597 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
598 ${c.reviewers_count}
598 ${c.reviewers_count}
599 </div>
599 </div>
600
600
601 ## REVIEWERS
601 ## REVIEWERS
602 <div class="right-sidebar-expanded-state pr-details-title">
602 <div class="right-sidebar-expanded-state pr-details-title">
603 <span class="tooltip sidebar-heading" title="${vote_title}">
603 <span class="tooltip sidebar-heading" title="${vote_title}">
604 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
604 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
605 ${_('Reviewers')}
605 ${_('Reviewers')}
606 </span>
606 </span>
607
607
608 %if c.allowed_to_update:
608 %if c.allowed_to_update:
609 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
609 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
610 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
610 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
611 %else:
611 %else:
612 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
612 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
613 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
613 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
614 %endif
614 %endif
615 </div>
615 </div>
616
616
617 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
617 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
618
618
619 <div id="review_rules" style="display: none" class="">
619 <div id="review_rules" style="display: none" class="">
620
620
621 <strong>${_('Reviewer rules')}</strong>
621 <strong>${_('Reviewer rules')}</strong>
622 <div class="pr-reviewer-rules">
622 <div class="pr-reviewer-rules">
623 ## review rules will be appended here, by default reviewers logic
623 ## review rules will be appended here, by default reviewers logic
624 </div>
624 </div>
625 <input id="review_data" type="hidden" name="review_data" value="">
625 <input id="review_data" type="hidden" name="review_data" value="">
626 </div>
626 </div>
627
627
628 ## members redering block
628 ## members redering block
629 <input type="hidden" name="__start__" value="review_members:sequence">
629 <input type="hidden" name="__start__" value="review_members:sequence">
630
630
631 <table id="review_members" class="group_members">
631 <table id="review_members" class="group_members">
632 ## This content is loaded via JS and ReviewersPanel
632 ## This content is loaded via JS and ReviewersPanel
633 </table>
633 </table>
634
634
635 <input type="hidden" name="__end__" value="review_members:sequence">
635 <input type="hidden" name="__end__" value="review_members:sequence">
636 ## end members redering block
636 ## end members redering block
637
637
638 %if not c.pull_request.is_closed():
638 %if not c.pull_request.is_closed():
639 <div id="add_reviewer" class="ac" style="display: none;">
639 <div id="add_reviewer" class="ac" style="display: none;">
640 %if c.allowed_to_update:
640 %if c.allowed_to_update:
641 % if not c.forbid_adding_reviewers:
641 % if not c.forbid_adding_reviewers:
642 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
642 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
643 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
643 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
644 <div id="reviewers_container"></div>
644 <div id="reviewers_container"></div>
645 </div>
645 </div>
646 % endif
646 % endif
647 <div class="pull-right" style="margin-bottom: 15px">
647 <div class="pull-right" style="margin-bottom: 15px">
648 <button data-role="reviewer" id="update_reviewers" class="btn btn-sm no-margin">${_('Save Changes')}</button>
648 <button data-role="reviewer" id="update_reviewers" class="btn btn-sm no-margin">${_('Save Changes')}</button>
649 </div>
649 </div>
650 %endif
650 %endif
651 </div>
651 </div>
652 %endif
652 %endif
653 </div>
653 </div>
654 </div>
654 </div>
655
655
656 ## OBSERVERS
656 ## OBSERVERS
657 % if c.rhodecode_edition_id == 'EE':
657 % if c.rhodecode_edition_id == 'EE':
658 <div class="sidebar-element clear-both">
658 <div class="sidebar-element clear-both">
659 <% vote_title = _ungettext(
659 <% vote_title = _ungettext(
660 '{} observer without voting right.',
660 '{} observer without voting right.',
661 '{} observers without voting right.', c.observers_count).format(c.observers_count)
661 '{} observers without voting right.', c.observers_count).format(c.observers_count)
662 %>
662 %>
663
663
664 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
664 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
665 <i class="icon-circle-thin"></i>
665 <i class="icon-circle-thin"></i>
666 ${c.observers_count}
666 ${c.observers_count}
667 </div>
667 </div>
668
668
669 <div class="right-sidebar-expanded-state pr-details-title">
669 <div class="right-sidebar-expanded-state pr-details-title">
670 <span class="tooltip sidebar-heading" title="${vote_title}">
670 <span class="tooltip sidebar-heading" title="${vote_title}">
671 <i class="icon-circle-thin"></i>
671 <i class="icon-circle-thin"></i>
672 ${_('Observers')}
672 ${_('Observers')}
673 </span>
673 </span>
674 %if c.allowed_to_update:
674 %if c.allowed_to_update:
675 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
675 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
676 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
676 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
677 %endif
677 %endif
678 </div>
678 </div>
679
679
680 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
680 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
681 ## members redering block
681 ## members redering block
682 <input type="hidden" name="__start__" value="observer_members:sequence">
682 <input type="hidden" name="__start__" value="observer_members:sequence">
683
683
684 <table id="observer_members" class="group_members">
684 <table id="observer_members" class="group_members">
685 ## This content is loaded via JS and ReviewersPanel
685 ## This content is loaded via JS and ReviewersPanel
686 </table>
686 </table>
687
687
688 <input type="hidden" name="__end__" value="observer_members:sequence">
688 <input type="hidden" name="__end__" value="observer_members:sequence">
689 ## end members redering block
689 ## end members redering block
690
690
691 %if not c.pull_request.is_closed():
691 %if not c.pull_request.is_closed():
692 <div id="add_observer" class="ac" style="display: none;">
692 <div id="add_observer" class="ac" style="display: none;">
693 %if c.allowed_to_update:
693 %if c.allowed_to_update:
694 % if not c.forbid_adding_reviewers or 1:
694 % if not c.forbid_adding_reviewers or 1:
695 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
695 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
696 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
696 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
697 <div id="observers_container"></div>
697 <div id="observers_container"></div>
698 </div>
698 </div>
699 % endif
699 % endif
700 <div class="pull-right" style="margin-bottom: 15px">
700 <div class="pull-right" style="margin-bottom: 15px">
701 <button data-role="observer" id="update_observers" class="btn btn-sm no-margin">${_('Save Changes')}</button>
701 <button data-role="observer" id="update_observers" class="btn btn-sm no-margin">${_('Save Changes')}</button>
702 </div>
702 </div>
703 %endif
703 %endif
704 </div>
704 </div>
705 %endif
705 %endif
706 </div>
706 </div>
707 </div>
707 </div>
708 % endif
708 % endif
709
709
710 ## TODOs
710 ## TODOs
711 <div id="todosTable" class="sidebar-element clear-both">
711 <div id="todosTable" class="sidebar-element clear-both">
712 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
712 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
713 <i class="icon-flag-filled"></i>
713 <i class="icon-flag-filled"></i>
714 <span id="todos-count">${len(c.unresolved_comments)}</span>
714 <span id="todos-count">${len(c.unresolved_comments)}</span>
715 </div>
715 </div>
716
716
717 <div class="right-sidebar-expanded-state pr-details-title">
717 <div class="right-sidebar-expanded-state pr-details-title">
718 ## Only show unresolved, that is only what matters
718 ## Only show unresolved, that is only what matters
719 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
719 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
720 <i class="icon-flag-filled"></i>
720 <i class="icon-flag-filled"></i>
721 TODOs
721 TODOs
722 </span>
722 </span>
723
723
724 % if not c.at_version:
724 % if not c.at_version:
725 % if c.resolved_comments:
725 % if c.resolved_comments:
726 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
726 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
727 % else:
727 % else:
728 <span class="block-right last-item noselect">Show resolved</span>
728 <span class="block-right last-item noselect">Show resolved</span>
729 % endif
729 % endif
730 % endif
730 % endif
731 </div>
731 </div>
732
732
733 <div class="right-sidebar-expanded-state pr-details-content">
733 <div class="right-sidebar-expanded-state pr-details-content">
734
734
735 % if c.at_version:
735 % if c.at_version:
736 <table>
736 <table>
737 <tr>
737 <tr>
738 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
738 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
739 </tr>
739 </tr>
740 </table>
740 </table>
741 % else:
741 % else:
742 % if c.unresolved_comments + c.resolved_comments:
742 % if c.unresolved_comments + c.resolved_comments:
743 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
743 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
744 % else:
744 % else:
745 <table class="todos-content-table">
745 <table class="todos-content-table">
746 <tr>
746 <tr>
747 <td>
747 <td>
748 ${_('No TODOs yet')}
748 ${_('No TODOs yet')}
749 </td>
749 </td>
750 </tr>
750 </tr>
751 </table>
751 </table>
752 % endif
752 % endif
753 % endif
753 % endif
754 </div>
754 </div>
755 </div>
755 </div>
756
756
757 ## COMMENTS
757 ## COMMENTS
758 <div id="commentsTable" class="sidebar-element clear-both">
758 <div id="commentsTable" class="sidebar-element clear-both">
759 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
759 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
760 <i class="icon-comment" style="color: #949494"></i>
760 <i class="icon-comment" style="color: #949494"></i>
761 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
761 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
762 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
762 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
763 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
763 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
764 </div>
764 </div>
765
765
766 <div class="right-sidebar-expanded-state pr-details-title">
766 <div class="right-sidebar-expanded-state pr-details-title">
767 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
767 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
768 <i class="icon-comment" style="color: #949494"></i>
768 <i class="icon-comment" style="color: #949494"></i>
769 ${_('Comments')}
769 ${_('Comments')}
770
770
771 ## % if outdated_comm_count_ver:
771 ## % if outdated_comm_count_ver:
772 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
772 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
773 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
773 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
774 ## </a>
774 ## </a>
775 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
775 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
776 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
776 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
777
777
778 ## % else:
778 ## % else:
779 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
779 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
780 ## % endif
780 ## % endif
781
781
782 </span>
782 </span>
783
783
784 % if outdated_comm_count_ver:
784 % if outdated_comm_count_ver:
785 <span class="block-right action_button last-item noselect" onclick="return toggleElement(this, '.hidden-comment');" data-toggle-on="Show outdated" data-toggle-off="Hide outdated">Show outdated</span>
785 <span class="block-right action_button last-item noselect" onclick="return toggleElement(this, '.hidden-comment');" data-toggle-on="Show outdated" data-toggle-off="Hide outdated">Show outdated</span>
786 % else:
786 % else:
787 <span class="block-right last-item noselect">Show hidden</span>
787 <span class="block-right last-item noselect">Show hidden</span>
788 % endif
788 % endif
789
789
790 </div>
790 </div>
791
791
792 <div class="right-sidebar-expanded-state pr-details-content">
792 <div class="right-sidebar-expanded-state pr-details-content">
793 % if c.inline_comments_flat + c.comments:
793 % if c.inline_comments_flat + c.comments:
794 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
794 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
795 % else:
795 % else:
796 <table class="comments-content-table">
796 <table class="comments-content-table">
797 <tr>
797 <tr>
798 <td>
798 <td>
799 ${_('No Comments yet')}
799 ${_('No Comments yet')}
800 </td>
800 </td>
801 </tr>
801 </tr>
802 </table>
802 </table>
803 % endif
803 % endif
804 </div>
804 </div>
805
805
806 </div>
806 </div>
807
807
808 ## Referenced Tickets
808 ## Referenced Tickets
809 <div class="sidebar-element clear-both">
809 <div class="sidebar-element clear-both">
810 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
810 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
811 <i class="icon-info-circled"></i>
811 <i class="icon-info-circled"></i>
812 ${(len(c.referenced_desc_issues) + len(c.referenced_commit_issues))}
812 ${(c.referenced_desc_issues.issues_unique_count + c.referenced_commit_issues.issues_unique_count)}
813 </div>
813 </div>
814
814
815 <div class="right-sidebar-expanded-state pr-details-title">
815 <div class="right-sidebar-expanded-state pr-details-title">
816 <span class="sidebar-heading">
816 <span class="sidebar-heading">
817 <i class="icon-info-circled"></i>
817 <i class="icon-info-circled"></i>
818 ${_('Referenced Tickets')}
818 ${_('Referenced Tickets')}
819 </span>
819 </span>
820 </div>
820 </div>
821 <div class="right-sidebar-expanded-state pr-details-content">
821 <div class="right-sidebar-expanded-state pr-details-content">
822 <table>
822 <table>
823
823
824 <tr><td><code>${_('In pull request description')}:</code></td></tr>
824 <tr><td><code>${_('In pull request description')}:</code></td></tr>
825 % if c.referenced_desc_issues:
825 % if c.referenced_desc_issues.issues:
826 % for ticket_dict in sorted(c.referenced_desc_issues):
826
827 % for ticket_id, ticket_dict in c.referenced_desc_issues.unique_issues.items():
827 <tr>
828 <tr>
828 <td>
829 <td>
829 <a href="${ticket_dict.get('url')}">
830 <a href="${ticket_dict[0].get('url')}">
830 ${ticket_dict.get('id')}
831 ${ticket_id}
831 </a>
832 </a>
832 </td>
833 </td>
833 </tr>
834 </tr>
835
834 % endfor
836 % endfor
835 % else:
837 % else:
836 <tr>
838 <tr>
837 <td>
839 <td>
838 ${_('No Ticket data found.')}
840 ${_('No Ticket data found.')}
839 </td>
841 </td>
840 </tr>
842 </tr>
841 % endif
843 % endif
842
844
843 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
845 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
844 % if c.referenced_commit_issues:
846 % if c.referenced_commit_issues.issues:
845 % for ticket_dict in sorted(c.referenced_commit_issues):
847 % for ticket_id, ticket_dict in c.referenced_commit_issues.unique_issues.items():
846 <tr>
848 <tr>
847 <td>
849 <td>
848 <a href="${ticket_dict.get('url')}">
850 <a href="${ticket_dict[0].get('url')}">
849 ${ticket_dict.get('id')}
851 ${ticket_id}
850 </a>
852 </a>
853 - ${_ungettext('in %s commit', 'in %s commits', len(ticket_dict)) % (len(ticket_dict))}
851 </td>
854 </td>
852 </tr>
855 </tr>
853 % endfor
856 % endfor
854 % else:
857 % else:
855 <tr>
858 <tr>
856 <td>
859 <td>
857 ${_('No Ticket data found.')}
860 ${_('No Ticket data found.')}
858 </td>
861 </td>
859 </tr>
862 </tr>
860 % endif
863 % endif
861 </table>
864 </table>
862
865
863 </div>
866 </div>
864 </div>
867 </div>
865
868
866 </div>
869 </div>
867
870
868 </div>
871 </div>
869 </aside>
872 </aside>
870
873
871 ## This JS needs to be at the end
874 ## This JS needs to be at the end
872 <script type="text/javascript">
875 <script type="text/javascript">
873
876
874 versionController = new VersionController();
877 versionController = new VersionController();
875 versionController.init();
878 versionController.init();
876
879
877 reviewersController = new ReviewersController();
880 reviewersController = new ReviewersController();
878 commitsController = new CommitsController();
881 commitsController = new CommitsController();
879 commentsController = new CommentsController();
882 commentsController = new CommentsController();
880
883
881 updateController = new UpdatePrController();
884 updateController = new UpdatePrController();
882
885
883 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
886 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
884 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
887 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
885 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
888 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
886
889
887 (function () {
890 (function () {
888 "use strict";
891 "use strict";
889
892
890 // custom code mirror
893 // custom code mirror
891 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
894 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
892
895
893 PRDetails.init();
896 PRDetails.init();
894 ReviewersPanel.init(reviewersController, reviewerRulesData, setReviewersData);
897 ReviewersPanel.init(reviewersController, reviewerRulesData, setReviewersData);
895 ObserversPanel.init(reviewersController, reviewerRulesData, setObserversData);
898 ObserversPanel.init(reviewersController, reviewerRulesData, setObserversData);
896
899
897 window.showOutdated = function (self) {
900 window.showOutdated = function (self) {
898 $('.comment-inline.comment-outdated').show();
901 $('.comment-inline.comment-outdated').show();
899 $('.filediff-outdated').show();
902 $('.filediff-outdated').show();
900 $('.showOutdatedComments').hide();
903 $('.showOutdatedComments').hide();
901 $('.hideOutdatedComments').show();
904 $('.hideOutdatedComments').show();
902 };
905 };
903
906
904 window.hideOutdated = function (self) {
907 window.hideOutdated = function (self) {
905 $('.comment-inline.comment-outdated').hide();
908 $('.comment-inline.comment-outdated').hide();
906 $('.filediff-outdated').hide();
909 $('.filediff-outdated').hide();
907 $('.hideOutdatedComments').hide();
910 $('.hideOutdatedComments').hide();
908 $('.showOutdatedComments').show();
911 $('.showOutdatedComments').show();
909 };
912 };
910
913
911 window.refreshMergeChecks = function () {
914 window.refreshMergeChecks = function () {
912 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
915 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
913 $('.pull-request-merge').css('opacity', 0.3);
916 $('.pull-request-merge').css('opacity', 0.3);
914 $('.action-buttons-extra').css('opacity', 0.3);
917 $('.action-buttons-extra').css('opacity', 0.3);
915
918
916 $('.pull-request-merge').load(
919 $('.pull-request-merge').load(
917 loadUrl, function () {
920 loadUrl, function () {
918 $('.pull-request-merge').css('opacity', 1);
921 $('.pull-request-merge').css('opacity', 1);
919
922
920 $('.action-buttons-extra').css('opacity', 1);
923 $('.action-buttons-extra').css('opacity', 1);
921 }
924 }
922 );
925 );
923 };
926 };
924
927
925 window.submitDrafts = function (event) {
928 window.submitDrafts = function (event) {
926 var target = $(event.currentTarget);
929 var target = $(event.currentTarget);
927 var callback = function (result) {
930 var callback = function (result) {
928 target.removeAttr('onclick').html('saving...');
931 target.removeAttr('onclick').html('saving...');
929 }
932 }
930 var draftIds = [];
933 var draftIds = [];
931 $.each($('[name=submit_draft]:checked'), function (idx, val) {
934 $.each($('[name=submit_draft]:checked'), function (idx, val) {
932 draftIds.push(parseInt($(val).val()));
935 draftIds.push(parseInt($(val).val()));
933 })
936 })
934 if (draftIds.length > 0) {
937 if (draftIds.length > 0) {
935 Rhodecode.comments.finalizeDrafts(draftIds, callback);
938 Rhodecode.comments.finalizeDrafts(draftIds, callback);
936 }
939 }
937 else {
940 else {
938
941
939 }
942 }
940 }
943 }
941
944
942 window.closePullRequest = function (status) {
945 window.closePullRequest = function (status) {
943 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
946 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
944 return false;
947 return false;
945 }
948 }
946 // inject closing flag
949 // inject closing flag
947 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
950 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
948 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
951 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
949 $(generalCommentForm.submitForm).submit();
952 $(generalCommentForm.submitForm).submit();
950 };
953 };
951
954
952 //TODO this functionality is now missing
955 //TODO this functionality is now missing
953 $('#show-outdated-comments').on('click', function (e) {
956 $('#show-outdated-comments').on('click', function (e) {
954 var button = $(this);
957 var button = $(this);
955 var outdated = $('.comment-outdated');
958 var outdated = $('.comment-outdated');
956
959
957 if (button.html() === "(Show)") {
960 if (button.html() === "(Show)") {
958 button.html("(Hide)");
961 button.html("(Hide)");
959 outdated.show();
962 outdated.show();
960 } else {
963 } else {
961 button.html("(Show)");
964 button.html("(Show)");
962 outdated.hide();
965 outdated.hide();
963 }
966 }
964 });
967 });
965
968
966 $('#merge_pull_request_form').submit(function () {
969 $('#merge_pull_request_form').submit(function () {
967 if (!$('#merge_pull_request').attr('disabled')) {
970 if (!$('#merge_pull_request').attr('disabled')) {
968 $('#merge_pull_request').attr('disabled', 'disabled');
971 $('#merge_pull_request').attr('disabled', 'disabled');
969 }
972 }
970 return true;
973 return true;
971 });
974 });
972
975
973 $('#edit_pull_request').on('click', function (e) {
976 $('#edit_pull_request').on('click', function (e) {
974 var title = $('#pr-title-input').val();
977 var title = $('#pr-title-input').val();
975 var description = codeMirrorInstance.getValue();
978 var description = codeMirrorInstance.getValue();
976 var renderer = $('#pr-renderer-input').val();
979 var renderer = $('#pr-renderer-input').val();
977 editPullRequest(
980 editPullRequest(
978 "${c.repo_name}", "${c.pull_request.pull_request_id}",
981 "${c.repo_name}", "${c.pull_request.pull_request_id}",
979 title, description, renderer);
982 title, description, renderer);
980 });
983 });
981
984
982 var $updateButtons = $('#update_reviewers,#update_observers');
985 var $updateButtons = $('#update_reviewers,#update_observers');
983 $updateButtons.on('click', function (e) {
986 $updateButtons.on('click', function (e) {
984 var role = $(this).data('role');
987 var role = $(this).data('role');
985 $updateButtons.attr('disabled', 'disabled');
988 $updateButtons.attr('disabled', 'disabled');
986 $updateButtons.addClass('disabled');
989 $updateButtons.addClass('disabled');
987 $updateButtons.html(_gettext('Saving...'));
990 $updateButtons.html(_gettext('Saving...'));
988 reviewersController.updateReviewers(
991 reviewersController.updateReviewers(
989 templateContext.repo_name,
992 templateContext.repo_name,
990 templateContext.pull_request_data.pull_request_id,
993 templateContext.pull_request_data.pull_request_id,
991 role
994 role
992 );
995 );
993 });
996 });
994
997
995 // fixing issue with caches on firefox
998 // fixing issue with caches on firefox
996 $('#update_commits').removeAttr("disabled");
999 $('#update_commits').removeAttr("disabled");
997
1000
998 $('.show-inline-comments').on('click', function (e) {
1001 $('.show-inline-comments').on('click', function (e) {
999 var boxid = $(this).attr('data-comment-id');
1002 var boxid = $(this).attr('data-comment-id');
1000 var button = $(this);
1003 var button = $(this);
1001
1004
1002 if (button.hasClass("comments-visible")) {
1005 if (button.hasClass("comments-visible")) {
1003 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1006 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1004 $(this).hide();
1007 $(this).hide();
1005 });
1008 });
1006 button.removeClass("comments-visible");
1009 button.removeClass("comments-visible");
1007 } else {
1010 } else {
1008 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1011 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1009 $(this).show();
1012 $(this).show();
1010 });
1013 });
1011 button.addClass("comments-visible");
1014 button.addClass("comments-visible");
1012 }
1015 }
1013 });
1016 });
1014
1017
1015 $('.show-inline-comments').on('change', function (e) {
1018 $('.show-inline-comments').on('change', function (e) {
1016 var show = 'none';
1019 var show = 'none';
1017 var target = e.currentTarget;
1020 var target = e.currentTarget;
1018 if (target.checked) {
1021 if (target.checked) {
1019 show = ''
1022 show = ''
1020 }
1023 }
1021 var boxid = $(target).attr('id_for');
1024 var boxid = $(target).attr('id_for');
1022 var comments = $('#{0} .inline-comments'.format(boxid));
1025 var comments = $('#{0} .inline-comments'.format(boxid));
1023 var fn_display = function (idx) {
1026 var fn_display = function (idx) {
1024 $(this).css('display', show);
1027 $(this).css('display', show);
1025 };
1028 };
1026 $(comments).each(fn_display);
1029 $(comments).each(fn_display);
1027 var btns = $('#{0} .inline-comments-button'.format(boxid));
1030 var btns = $('#{0} .inline-comments-button'.format(boxid));
1028 $(btns).each(fn_display);
1031 $(btns).each(fn_display);
1029 });
1032 });
1030
1033
1031 // register submit callback on commentForm form to track TODOs, and refresh mergeChecks conditions
1034 // register submit callback on commentForm form to track TODOs, and refresh mergeChecks conditions
1032 window.commentFormGlobalSubmitSuccessCallback = function (comment) {
1035 window.commentFormGlobalSubmitSuccessCallback = function (comment) {
1033 if (!comment.draft) {
1036 if (!comment.draft) {
1034 refreshMergeChecks();
1037 refreshMergeChecks();
1035 }
1038 }
1036 };
1039 };
1037
1040
1038 ReviewerAutoComplete('#user', reviewersController);
1041 ReviewerAutoComplete('#user', reviewersController);
1039 ObserverAutoComplete('#observer', reviewersController);
1042 ObserverAutoComplete('#observer', reviewersController);
1040
1043
1041 })();
1044 })();
1042
1045
1043 $(document).ready(function () {
1046 $(document).ready(function () {
1044
1047
1045 var channel = '${c.pr_broadcast_channel}';
1048 var channel = '${c.pr_broadcast_channel}';
1046 new ReviewerPresenceController(channel)
1049 new ReviewerPresenceController(channel)
1047 // register globally so inject comment logic can re-use it.
1050 // register globally so inject comment logic can re-use it.
1048 window.commentsController = commentsController;
1051 window.commentsController = commentsController;
1049 })
1052 })
1050 </script>
1053 </script>
1051
1054
1052 </%def>
1055 </%def>
General Comments 0
You need to be logged in to leave comments. Login now