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