##// END OF EJS Templates
hovercards: added commit hovercard for files, and dashboard views.
marcink -
r4032:07c1bd09 default
parent child Browse files
Show More
@@ -0,0 +1,8 b''
1 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3
4 <div class="clear-fix">${base.gravatar_with_user(c.commit.author, tooltip=True)}</div>
5 <br/>
6 <a href="${h.route_path('repo_commit', repo_name=c.repo_name, commit_id=c.commit.raw_id)}">${h.show_id(c.commit)}</a> - ${c.commit.date}
7 <br/><br/>
8 <pre>${h.urlify_commit_message(c.commit.message, c.repo_name)}</pre>
@@ -1,38 +1,37 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2018-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.config import routing_links
21 20
22 21
23 22 def includeme(config):
24 23
25 24 config.add_route(
26 25 name='hovercard_user',
27 26 pattern='/_hovercard/user/{user_id}')
28 27
29 28 config.add_route(
30 29 name='hovercard_user_group',
31 30 pattern='/_hovercard/user_group/{user_group_id}')
32 31
33 32 config.add_route(
34 name='hovercard_commit',
35 pattern='/_hovercard/commit/{repo_name}/{user_id}')
33 name='hovercard_repo_commit',
34 pattern='/_hovercard/commit/{repo_name:.*?[^/]}/{commit_id}', repo_route=True)
36 35
37 36 # Scan module for configuration decorators.
38 37 config.scan('.views', ignore='.tests')
@@ -1,71 +1,90 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import logging
23 23 import collections
24 24
25 25 from pyramid.view import view_config
26 26
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView, RepoAppView
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.auth import (
30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
31 HasRepoPermissionAnyDecorator)
31 32 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
32 33 from rhodecode.lib.index import searcher_from_config
33 34 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
34 35 from rhodecode.lib.ext_json import json
35 36 from rhodecode.lib.vcs.nodes import FileNode
36 37 from rhodecode.model.db import (
37 38 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup)
38 39 from rhodecode.model.repo import RepoModel
39 40 from rhodecode.model.repo_group import RepoGroupModel
40 41 from rhodecode.model.scm import RepoGroupList, RepoList
41 42 from rhodecode.model.user import UserModel
42 43 from rhodecode.model.user_group import UserGroupModel
43 44
44 45 log = logging.getLogger(__name__)
45 46
46 47
47 48 class HoverCardsView(BaseAppView):
48 49
49 50 def load_default_context(self):
50 51 c = self._get_local_tmpl_context()
51 52 return c
52 53
53 54 @LoginRequired()
54 55 @view_config(
55 56 route_name='hovercard_user', request_method='GET', xhr=True,
56 57 renderer='rhodecode:templates/hovercards/hovercard_user.mako')
57 58 def hovercard_user(self):
58 59 c = self.load_default_context()
59 60 user_id = self.request.matchdict['user_id']
60 61 c.user = User.get_or_404(user_id)
61 62 return self._get_template_context(c)
62 63
63 64 @LoginRequired()
64 65 @view_config(
65 66 route_name='hovercard_user_group', request_method='GET', xhr=True,
66 67 renderer='rhodecode:templates/hovercards/hovercard_user_group.mako')
67 68 def hovercard_user_group(self):
68 69 c = self.load_default_context()
69 70 user_group_id = self.request.matchdict['user_group_id']
70 71 c.user_group = UserGroup.get_or_404(user_group_id)
71 72 return self._get_template_context(c)
73
74
75 class HoverCardsRepoView(RepoAppView):
76 def load_default_context(self):
77 c = self._get_local_tmpl_context()
78 return c
79
80 @LoginRequired()
81 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', 'repository.admin')
82 @view_config(
83 route_name='hovercard_repo_commit', request_method='GET', xhr=True,
84 renderer='rhodecode:templates/hovercards/hovercard_repo_commit.mako')
85 def hovercard_repo_commit(self):
86 c = self.load_default_context()
87 commit_id = self.request.matchdict['commit_id']
88 pre_load = ['author', 'branch', 'date', 'message']
89 c.commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id, pre_load=pre_load)
90 return self._get_template_context(c)
@@ -1,2089 +1,2088 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import os
29 29 import random
30 30 import hashlib
31 31 import StringIO
32 32 import textwrap
33 33 import urllib
34 34 import math
35 35 import logging
36 36 import re
37 37 import time
38 38 import string
39 39 import hashlib
40 40 from collections import OrderedDict
41 41
42 42 import pygments
43 43 import itertools
44 44 import fnmatch
45 45 import bleach
46 46
47 47 from pyramid import compat
48 48 from datetime import datetime
49 49 from functools import partial
50 50 from pygments.formatters.html import HtmlFormatter
51 51 from pygments.lexers import (
52 52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 53
54 54 from pyramid.threadlocal import get_current_request
55 55
56 56 from webhelpers.html import literal, HTML, escape
57 57 from webhelpers.html.tools import *
58 58 from webhelpers.html.builder import make_tag
59 59 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
60 60 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
61 61 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
62 62 submit, text, password, textarea, title, ul, xml_declaration, radio
63 63 from webhelpers.html.tools import auto_link, button_to, highlight, \
64 64 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
65 65 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
66 66 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
67 67 replace_whitespace, urlify, truncate, wrap_paragraphs
68 68 from webhelpers.date import time_ago_in_words
69 69 from webhelpers.paginate import Page as _Page
70 70 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
71 71 convert_boolean_attrs, NotGiven, _make_safe_id_component
72 72 from webhelpers2.number import format_byte_size
73 73
74 74 from rhodecode.lib.action_parser import action_parser
75 75 from rhodecode.lib.ext_json import json
76 76 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
77 77 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
78 78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
79 79 AttributeDict, safe_int, md5, md5_safe
80 80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 83 from rhodecode.lib.index.search_utils import get_matching_line_offsets
84 84 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
85 85 from rhodecode.model.changeset_status import ChangesetStatusModel
86 86 from rhodecode.model.db import Permission, User, Repository
87 87 from rhodecode.model.repo_group import RepoGroupModel
88 88 from rhodecode.model.settings import IssueTrackerSettingsModel
89 89
90 90
91 91 log = logging.getLogger(__name__)
92 92
93 93
94 94 DEFAULT_USER = User.DEFAULT_USER
95 95 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
96 96
97 97
98 98 def asset(path, ver=None, **kwargs):
99 99 """
100 100 Helper to generate a static asset file path for rhodecode assets
101 101
102 102 eg. h.asset('images/image.png', ver='3923')
103 103
104 104 :param path: path of asset
105 105 :param ver: optional version query param to append as ?ver=
106 106 """
107 107 request = get_current_request()
108 108 query = {}
109 109 query.update(kwargs)
110 110 if ver:
111 111 query = {'ver': ver}
112 112 return request.static_path(
113 113 'rhodecode:public/{}'.format(path), _query=query)
114 114
115 115
116 116 default_html_escape_table = {
117 117 ord('&'): u'&amp;',
118 118 ord('<'): u'&lt;',
119 119 ord('>'): u'&gt;',
120 120 ord('"'): u'&quot;',
121 121 ord("'"): u'&#39;',
122 122 }
123 123
124 124
125 125 def html_escape(text, html_escape_table=default_html_escape_table):
126 126 """Produce entities within text."""
127 127 return text.translate(html_escape_table)
128 128
129 129
130 130 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
131 131 """
132 132 Truncate string ``s`` at the first occurrence of ``sub``.
133 133
134 134 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
135 135 """
136 136 suffix_if_chopped = suffix_if_chopped or ''
137 137 pos = s.find(sub)
138 138 if pos == -1:
139 139 return s
140 140
141 141 if inclusive:
142 142 pos += len(sub)
143 143
144 144 chopped = s[:pos]
145 145 left = s[pos:].strip()
146 146
147 147 if left and suffix_if_chopped:
148 148 chopped += suffix_if_chopped
149 149
150 150 return chopped
151 151
152 152
153 153 def shorter(text, size=20, prefix=False):
154 154 postfix = '...'
155 155 if len(text) > size:
156 156 if prefix:
157 157 # shorten in front
158 158 return postfix + text[-(size - len(postfix)):]
159 159 else:
160 160 return text[:size - len(postfix)] + postfix
161 161 return text
162 162
163 163
164 164 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
165 165 """
166 166 Reset button
167 167 """
168 168 _set_input_attrs(attrs, type, name, value)
169 169 _set_id_attr(attrs, id, name)
170 170 convert_boolean_attrs(attrs, ["disabled"])
171 171 return HTML.input(**attrs)
172 172
173 173 reset = _reset
174 174 safeid = _make_safe_id_component
175 175
176 176
177 177 def branding(name, length=40):
178 178 return truncate(name, length, indicator="")
179 179
180 180
181 181 def FID(raw_id, path):
182 182 """
183 183 Creates a unique ID for filenode based on it's hash of path and commit
184 184 it's safe to use in urls
185 185
186 186 :param raw_id:
187 187 :param path:
188 188 """
189 189
190 190 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
191 191
192 192
193 193 class _GetError(object):
194 194 """Get error from form_errors, and represent it as span wrapped error
195 195 message
196 196
197 197 :param field_name: field to fetch errors for
198 198 :param form_errors: form errors dict
199 199 """
200 200
201 201 def __call__(self, field_name, form_errors):
202 202 tmpl = """<span class="error_msg">%s</span>"""
203 203 if form_errors and field_name in form_errors:
204 204 return literal(tmpl % form_errors.get(field_name))
205 205
206 206
207 207 get_error = _GetError()
208 208
209 209
210 210 class _ToolTip(object):
211 211
212 212 def __call__(self, tooltip_title, trim_at=50):
213 213 """
214 214 Special function just to wrap our text into nice formatted
215 215 autowrapped text
216 216
217 217 :param tooltip_title:
218 218 """
219 219 tooltip_title = escape(tooltip_title)
220 220 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
221 221 return tooltip_title
222 222
223 223
224 224 tooltip = _ToolTip()
225 225
226 226 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
227 227
228 228
229 229 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
230 230 if isinstance(file_path, str):
231 231 file_path = safe_unicode(file_path)
232 232
233 233 route_qry = {'at': at_ref} if at_ref else None
234 234
235 235 # first segment is a `..` link to repo files
236 236 root_name = literal(u'<i class="icon-home"></i>')
237 237 url_segments = [
238 238 link_to(
239 239 root_name,
240 240 route_path(
241 241 'repo_files',
242 242 repo_name=repo_name,
243 243 commit_id=commit_id,
244 244 f_path='',
245 245 _query=route_qry),
246 246 )]
247 247
248 248 path_segments = file_path.split('/')
249 249 last_cnt = len(path_segments) - 1
250 250 for cnt, segment in enumerate(path_segments):
251 251 if not segment:
252 252 continue
253 253 segment_html = escape(segment)
254 254
255 255 last_item = cnt == last_cnt
256 256
257 257 if last_item and linkify_last_item is False:
258 258 # plain version
259 259 url_segments.append(segment_html)
260 260 else:
261 261 url_segments.append(
262 262 link_to(
263 263 segment_html,
264 264 route_path(
265 265 'repo_files',
266 266 repo_name=repo_name,
267 267 commit_id=commit_id,
268 268 f_path='/'.join(path_segments[:cnt + 1]),
269 269 _query=route_qry),
270 270 ))
271 271
272 272 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
273 273 if limit_items and len(limited_url_segments) < len(url_segments):
274 274 url_segments = limited_url_segments
275 275
276 276 full_path = file_path
277 277 icon = files_icon.format(escape(full_path))
278 278 if file_path == '':
279 279 return root_name
280 280 else:
281 281 return literal(' / '.join(url_segments) + icon)
282 282
283 283
284 284 def files_url_data(request):
285 285 matchdict = request.matchdict
286 286
287 287 if 'f_path' not in matchdict:
288 288 matchdict['f_path'] = ''
289 289
290 290 if 'commit_id' not in matchdict:
291 291 matchdict['commit_id'] = 'tip'
292 292
293 293 return json.dumps(matchdict)
294 294
295 295
296 296 def code_highlight(code, lexer, formatter, use_hl_filter=False):
297 297 """
298 298 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
299 299
300 300 If ``outfile`` is given and a valid file object (an object
301 301 with a ``write`` method), the result will be written to it, otherwise
302 302 it is returned as a string.
303 303 """
304 304 if use_hl_filter:
305 305 # add HL filter
306 306 from rhodecode.lib.index import search_utils
307 307 lexer.add_filter(search_utils.ElasticSearchHLFilter())
308 308 return pygments.format(pygments.lex(code, lexer), formatter)
309 309
310 310
311 311 class CodeHtmlFormatter(HtmlFormatter):
312 312 """
313 313 My code Html Formatter for source codes
314 314 """
315 315
316 316 def wrap(self, source, outfile):
317 317 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
318 318
319 319 def _wrap_code(self, source):
320 320 for cnt, it in enumerate(source):
321 321 i, t = it
322 322 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
323 323 yield i, t
324 324
325 325 def _wrap_tablelinenos(self, inner):
326 326 dummyoutfile = StringIO.StringIO()
327 327 lncount = 0
328 328 for t, line in inner:
329 329 if t:
330 330 lncount += 1
331 331 dummyoutfile.write(line)
332 332
333 333 fl = self.linenostart
334 334 mw = len(str(lncount + fl - 1))
335 335 sp = self.linenospecial
336 336 st = self.linenostep
337 337 la = self.lineanchors
338 338 aln = self.anchorlinenos
339 339 nocls = self.noclasses
340 340 if sp:
341 341 lines = []
342 342
343 343 for i in range(fl, fl + lncount):
344 344 if i % st == 0:
345 345 if i % sp == 0:
346 346 if aln:
347 347 lines.append('<a href="#%s%d" class="special">%*d</a>' %
348 348 (la, i, mw, i))
349 349 else:
350 350 lines.append('<span class="special">%*d</span>' % (mw, i))
351 351 else:
352 352 if aln:
353 353 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
354 354 else:
355 355 lines.append('%*d' % (mw, i))
356 356 else:
357 357 lines.append('')
358 358 ls = '\n'.join(lines)
359 359 else:
360 360 lines = []
361 361 for i in range(fl, fl + lncount):
362 362 if i % st == 0:
363 363 if aln:
364 364 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
365 365 else:
366 366 lines.append('%*d' % (mw, i))
367 367 else:
368 368 lines.append('')
369 369 ls = '\n'.join(lines)
370 370
371 371 # in case you wonder about the seemingly redundant <div> here: since the
372 372 # content in the other cell also is wrapped in a div, some browsers in
373 373 # some configurations seem to mess up the formatting...
374 374 if nocls:
375 375 yield 0, ('<table class="%stable">' % self.cssclass +
376 376 '<tr><td><div class="linenodiv" '
377 377 'style="background-color: #f0f0f0; padding-right: 10px">'
378 378 '<pre style="line-height: 125%">' +
379 379 ls + '</pre></div></td><td id="hlcode" class="code">')
380 380 else:
381 381 yield 0, ('<table class="%stable">' % self.cssclass +
382 382 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
383 383 ls + '</pre></div></td><td id="hlcode" class="code">')
384 384 yield 0, dummyoutfile.getvalue()
385 385 yield 0, '</td></tr></table>'
386 386
387 387
388 388 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
389 389 def __init__(self, **kw):
390 390 # only show these line numbers if set
391 391 self.only_lines = kw.pop('only_line_numbers', [])
392 392 self.query_terms = kw.pop('query_terms', [])
393 393 self.max_lines = kw.pop('max_lines', 5)
394 394 self.line_context = kw.pop('line_context', 3)
395 395 self.url = kw.pop('url', None)
396 396
397 397 super(CodeHtmlFormatter, self).__init__(**kw)
398 398
399 399 def _wrap_code(self, source):
400 400 for cnt, it in enumerate(source):
401 401 i, t = it
402 402 t = '<pre>%s</pre>' % t
403 403 yield i, t
404 404
405 405 def _wrap_tablelinenos(self, inner):
406 406 yield 0, '<table class="code-highlight %stable">' % self.cssclass
407 407
408 408 last_shown_line_number = 0
409 409 current_line_number = 1
410 410
411 411 for t, line in inner:
412 412 if not t:
413 413 yield t, line
414 414 continue
415 415
416 416 if current_line_number in self.only_lines:
417 417 if last_shown_line_number + 1 != current_line_number:
418 418 yield 0, '<tr>'
419 419 yield 0, '<td class="line">...</td>'
420 420 yield 0, '<td id="hlcode" class="code"></td>'
421 421 yield 0, '</tr>'
422 422
423 423 yield 0, '<tr>'
424 424 if self.url:
425 425 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
426 426 self.url, current_line_number, current_line_number)
427 427 else:
428 428 yield 0, '<td class="line"><a href="">%i</a></td>' % (
429 429 current_line_number)
430 430 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
431 431 yield 0, '</tr>'
432 432
433 433 last_shown_line_number = current_line_number
434 434
435 435 current_line_number += 1
436 436
437 437 yield 0, '</table>'
438 438
439 439
440 440 def hsv_to_rgb(h, s, v):
441 441 """ Convert hsv color values to rgb """
442 442
443 443 if s == 0.0:
444 444 return v, v, v
445 445 i = int(h * 6.0) # XXX assume int() truncates!
446 446 f = (h * 6.0) - i
447 447 p = v * (1.0 - s)
448 448 q = v * (1.0 - s * f)
449 449 t = v * (1.0 - s * (1.0 - f))
450 450 i = i % 6
451 451 if i == 0:
452 452 return v, t, p
453 453 if i == 1:
454 454 return q, v, p
455 455 if i == 2:
456 456 return p, v, t
457 457 if i == 3:
458 458 return p, q, v
459 459 if i == 4:
460 460 return t, p, v
461 461 if i == 5:
462 462 return v, p, q
463 463
464 464
465 465 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
466 466 """
467 467 Generator for getting n of evenly distributed colors using
468 468 hsv color and golden ratio. It always return same order of colors
469 469
470 470 :param n: number of colors to generate
471 471 :param saturation: saturation of returned colors
472 472 :param lightness: lightness of returned colors
473 473 :returns: RGB tuple
474 474 """
475 475
476 476 golden_ratio = 0.618033988749895
477 477 h = 0.22717784590367374
478 478
479 479 for _ in xrange(n):
480 480 h += golden_ratio
481 481 h %= 1
482 482 HSV_tuple = [h, saturation, lightness]
483 483 RGB_tuple = hsv_to_rgb(*HSV_tuple)
484 484 yield map(lambda x: str(int(x * 256)), RGB_tuple)
485 485
486 486
487 487 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
488 488 """
489 489 Returns a function which when called with an argument returns a unique
490 490 color for that argument, eg.
491 491
492 492 :param n: number of colors to generate
493 493 :param saturation: saturation of returned colors
494 494 :param lightness: lightness of returned colors
495 495 :returns: css RGB string
496 496
497 497 >>> color_hash = color_hasher()
498 498 >>> color_hash('hello')
499 499 'rgb(34, 12, 59)'
500 500 >>> color_hash('hello')
501 501 'rgb(34, 12, 59)'
502 502 >>> color_hash('other')
503 503 'rgb(90, 224, 159)'
504 504 """
505 505
506 506 color_dict = {}
507 507 cgenerator = unique_color_generator(
508 508 saturation=saturation, lightness=lightness)
509 509
510 510 def get_color_string(thing):
511 511 if thing in color_dict:
512 512 col = color_dict[thing]
513 513 else:
514 514 col = color_dict[thing] = cgenerator.next()
515 515 return "rgb(%s)" % (', '.join(col))
516 516
517 517 return get_color_string
518 518
519 519
520 520 def get_lexer_safe(mimetype=None, filepath=None):
521 521 """
522 522 Tries to return a relevant pygments lexer using mimetype/filepath name,
523 523 defaulting to plain text if none could be found
524 524 """
525 525 lexer = None
526 526 try:
527 527 if mimetype:
528 528 lexer = get_lexer_for_mimetype(mimetype)
529 529 if not lexer:
530 530 lexer = get_lexer_for_filename(filepath)
531 531 except pygments.util.ClassNotFound:
532 532 pass
533 533
534 534 if not lexer:
535 535 lexer = get_lexer_by_name('text')
536 536
537 537 return lexer
538 538
539 539
540 540 def get_lexer_for_filenode(filenode):
541 541 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
542 542 return lexer
543 543
544 544
545 545 def pygmentize(filenode, **kwargs):
546 546 """
547 547 pygmentize function using pygments
548 548
549 549 :param filenode:
550 550 """
551 551 lexer = get_lexer_for_filenode(filenode)
552 552 return literal(code_highlight(filenode.content, lexer,
553 553 CodeHtmlFormatter(**kwargs)))
554 554
555 555
556 556 def is_following_repo(repo_name, user_id):
557 557 from rhodecode.model.scm import ScmModel
558 558 return ScmModel().is_following_repo(repo_name, user_id)
559 559
560 560
561 561 class _Message(object):
562 562 """A message returned by ``Flash.pop_messages()``.
563 563
564 564 Converting the message to a string returns the message text. Instances
565 565 also have the following attributes:
566 566
567 567 * ``message``: the message text.
568 568 * ``category``: the category specified when the message was created.
569 569 """
570 570
571 571 def __init__(self, category, message):
572 572 self.category = category
573 573 self.message = message
574 574
575 575 def __str__(self):
576 576 return self.message
577 577
578 578 __unicode__ = __str__
579 579
580 580 def __html__(self):
581 581 return escape(safe_unicode(self.message))
582 582
583 583
584 584 class Flash(object):
585 585 # List of allowed categories. If None, allow any category.
586 586 categories = ["warning", "notice", "error", "success"]
587 587
588 588 # Default category if none is specified.
589 589 default_category = "notice"
590 590
591 591 def __init__(self, session_key="flash", categories=None,
592 592 default_category=None):
593 593 """
594 594 Instantiate a ``Flash`` object.
595 595
596 596 ``session_key`` is the key to save the messages under in the user's
597 597 session.
598 598
599 599 ``categories`` is an optional list which overrides the default list
600 600 of categories.
601 601
602 602 ``default_category`` overrides the default category used for messages
603 603 when none is specified.
604 604 """
605 605 self.session_key = session_key
606 606 if categories is not None:
607 607 self.categories = categories
608 608 if default_category is not None:
609 609 self.default_category = default_category
610 610 if self.categories and self.default_category not in self.categories:
611 611 raise ValueError(
612 612 "unrecognized default category %r" % (self.default_category,))
613 613
614 614 def pop_messages(self, session=None, request=None):
615 615 """
616 616 Return all accumulated messages and delete them from the session.
617 617
618 618 The return value is a list of ``Message`` objects.
619 619 """
620 620 messages = []
621 621
622 622 if not session:
623 623 if not request:
624 624 request = get_current_request()
625 625 session = request.session
626 626
627 627 # Pop the 'old' pylons flash messages. They are tuples of the form
628 628 # (category, message)
629 629 for cat, msg in session.pop(self.session_key, []):
630 630 messages.append(_Message(cat, msg))
631 631
632 632 # Pop the 'new' pyramid flash messages for each category as list
633 633 # of strings.
634 634 for cat in self.categories:
635 635 for msg in session.pop_flash(queue=cat):
636 636 messages.append(_Message(cat, msg))
637 637 # Map messages from the default queue to the 'notice' category.
638 638 for msg in session.pop_flash():
639 639 messages.append(_Message('notice', msg))
640 640
641 641 session.save()
642 642 return messages
643 643
644 644 def json_alerts(self, session=None, request=None):
645 645 payloads = []
646 646 messages = flash.pop_messages(session=session, request=request)
647 647 if messages:
648 648 for message in messages:
649 649 subdata = {}
650 650 if hasattr(message.message, 'rsplit'):
651 651 flash_data = message.message.rsplit('|DELIM|', 1)
652 652 org_message = flash_data[0]
653 653 if len(flash_data) > 1:
654 654 subdata = json.loads(flash_data[1])
655 655 else:
656 656 org_message = message.message
657 657 payloads.append({
658 658 'message': {
659 659 'message': u'{}'.format(org_message),
660 660 'level': message.category,
661 661 'force': True,
662 662 'subdata': subdata
663 663 }
664 664 })
665 665 return json.dumps(payloads)
666 666
667 667 def __call__(self, message, category=None, ignore_duplicate=True,
668 668 session=None, request=None):
669 669
670 670 if not session:
671 671 if not request:
672 672 request = get_current_request()
673 673 session = request.session
674 674
675 675 session.flash(
676 676 message, queue=category, allow_duplicate=not ignore_duplicate)
677 677
678 678
679 679 flash = Flash()
680 680
681 681 #==============================================================================
682 682 # SCM FILTERS available via h.
683 683 #==============================================================================
684 684 from rhodecode.lib.vcs.utils import author_name, author_email
685 685 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
686 686 from rhodecode.model.db import User, ChangesetStatus
687 687
688 688 capitalize = lambda x: x.capitalize()
689 689 email = author_email
690 690 short_id = lambda x: x[:12]
691 691 hide_credentials = lambda x: ''.join(credentials_filter(x))
692 692
693 693
694 694 import pytz
695 695 import tzlocal
696 696 local_timezone = tzlocal.get_localzone()
697 697
698 698
699 699 def age_component(datetime_iso, value=None, time_is_local=False):
700 700 title = value or format_date(datetime_iso)
701 701 tzinfo = '+00:00'
702 702
703 703 # detect if we have a timezone info, otherwise, add it
704 704 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
705 705 force_timezone = os.environ.get('RC_TIMEZONE', '')
706 706 if force_timezone:
707 707 force_timezone = pytz.timezone(force_timezone)
708 708 timezone = force_timezone or local_timezone
709 709 offset = timezone.localize(datetime_iso).strftime('%z')
710 710 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
711 711
712 712 return literal(
713 713 '<time class="timeago tooltip" '
714 714 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
715 715 datetime_iso, title, tzinfo))
716 716
717 717
718 718 def _shorten_commit_id(commit_id, commit_len=None):
719 719 if commit_len is None:
720 720 request = get_current_request()
721 721 commit_len = request.call_context.visual.show_sha_length
722 722 return commit_id[:commit_len]
723 723
724 724
725 725 def show_id(commit, show_idx=None, commit_len=None):
726 726 """
727 727 Configurable function that shows ID
728 728 by default it's r123:fffeeefffeee
729 729
730 730 :param commit: commit instance
731 731 """
732 732 if show_idx is None:
733 733 request = get_current_request()
734 734 show_idx = request.call_context.visual.show_revision_number
735 735
736 736 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
737 737 if show_idx:
738 738 return 'r%s:%s' % (commit.idx, raw_id)
739 739 else:
740 740 return '%s' % (raw_id, )
741 741
742 742
743 743 def format_date(date):
744 744 """
745 745 use a standardized formatting for dates used in RhodeCode
746 746
747 747 :param date: date/datetime object
748 748 :return: formatted date
749 749 """
750 750
751 751 if date:
752 752 _fmt = "%a, %d %b %Y %H:%M:%S"
753 753 return safe_unicode(date.strftime(_fmt))
754 754
755 755 return u""
756 756
757 757
758 758 class _RepoChecker(object):
759 759
760 760 def __init__(self, backend_alias):
761 761 self._backend_alias = backend_alias
762 762
763 763 def __call__(self, repository):
764 764 if hasattr(repository, 'alias'):
765 765 _type = repository.alias
766 766 elif hasattr(repository, 'repo_type'):
767 767 _type = repository.repo_type
768 768 else:
769 769 _type = repository
770 770 return _type == self._backend_alias
771 771
772 772
773 773 is_git = _RepoChecker('git')
774 774 is_hg = _RepoChecker('hg')
775 775 is_svn = _RepoChecker('svn')
776 776
777 777
778 778 def get_repo_type_by_name(repo_name):
779 779 repo = Repository.get_by_repo_name(repo_name)
780 780 if repo:
781 781 return repo.repo_type
782 782
783 783
784 784 def is_svn_without_proxy(repository):
785 785 if is_svn(repository):
786 786 from rhodecode.model.settings import VcsSettingsModel
787 787 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
788 788 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
789 789 return False
790 790
791 791
792 792 def discover_user(author):
793 793 """
794 794 Tries to discover RhodeCode User based on the autho string. Author string
795 795 is typically `FirstName LastName <email@address.com>`
796 796 """
797 797
798 798 # if author is already an instance use it for extraction
799 799 if isinstance(author, User):
800 800 return author
801 801
802 802 # Valid email in the attribute passed, see if they're in the system
803 803 _email = author_email(author)
804 804 if _email != '':
805 805 user = User.get_by_email(_email, case_insensitive=True, cache=True)
806 806 if user is not None:
807 807 return user
808 808
809 809 # Maybe it's a username, we try to extract it and fetch by username ?
810 810 _author = author_name(author)
811 811 user = User.get_by_username(_author, case_insensitive=True, cache=True)
812 812 if user is not None:
813 813 return user
814 814
815 815 return None
816 816
817 817
818 818 def email_or_none(author):
819 819 # extract email from the commit string
820 820 _email = author_email(author)
821 821
822 822 # If we have an email, use it, otherwise
823 823 # see if it contains a username we can get an email from
824 824 if _email != '':
825 825 return _email
826 826 else:
827 827 user = User.get_by_username(
828 828 author_name(author), case_insensitive=True, cache=True)
829 829
830 830 if user is not None:
831 831 return user.email
832 832
833 833 # No valid email, not a valid user in the system, none!
834 834 return None
835 835
836 836
837 837 def link_to_user(author, length=0, **kwargs):
838 838 user = discover_user(author)
839 839 # user can be None, but if we have it already it means we can re-use it
840 840 # in the person() function, so we save 1 intensive-query
841 841 if user:
842 842 author = user
843 843
844 844 display_person = person(author, 'username_or_name_or_email')
845 845 if length:
846 846 display_person = shorter(display_person, length)
847 847
848 848 if user:
849 849 return link_to(
850 850 escape(display_person),
851 851 route_path('user_profile', username=user.username),
852 852 **kwargs)
853 853 else:
854 854 return escape(display_person)
855 855
856 856
857 857 def link_to_group(users_group_name, **kwargs):
858 858 return link_to(
859 859 escape(users_group_name),
860 860 route_path('user_group_profile', user_group_name=users_group_name),
861 861 **kwargs)
862 862
863 863
864 864 def person(author, show_attr="username_and_name"):
865 865 user = discover_user(author)
866 866 if user:
867 867 return getattr(user, show_attr)
868 868 else:
869 869 _author = author_name(author)
870 870 _email = email(author)
871 871 return _author or _email
872 872
873 873
874 874 def author_string(email):
875 875 if email:
876 876 user = User.get_by_email(email, case_insensitive=True, cache=True)
877 877 if user:
878 878 if user.first_name or user.last_name:
879 879 return '%s %s &lt;%s&gt;' % (
880 880 user.first_name, user.last_name, email)
881 881 else:
882 882 return email
883 883 else:
884 884 return email
885 885 else:
886 886 return None
887 887
888 888
889 889 def person_by_id(id_, show_attr="username_and_name"):
890 890 # attr to return from fetched user
891 891 person_getter = lambda usr: getattr(usr, show_attr)
892 892
893 893 #maybe it's an ID ?
894 894 if str(id_).isdigit() or isinstance(id_, int):
895 895 id_ = int(id_)
896 896 user = User.get(id_)
897 897 if user is not None:
898 898 return person_getter(user)
899 899 return id_
900 900
901 901
902 902 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
903 903 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
904 904 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
905 905
906 906
907 907 tags_paterns = OrderedDict((
908 908 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
909 909 '<div class="metatag" tag="lang">\\2</div>')),
910 910
911 911 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
912 912 '<div class="metatag" tag="see">see: \\1 </div>')),
913 913
914 914 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
915 915 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
916 916
917 917 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
918 918 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
919 919
920 920 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
921 921 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
922 922
923 923 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
924 924 '<div class="metatag" tag="state \\1">\\1</div>')),
925 925
926 926 # label in grey
927 927 ('label', (re.compile(r'\[([a-z]+)\]'),
928 928 '<div class="metatag" tag="label">\\1</div>')),
929 929
930 930 # generic catch all in grey
931 931 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
932 932 '<div class="metatag" tag="generic">\\1</div>')),
933 933 ))
934 934
935 935
936 936 def extract_metatags(value):
937 937 """
938 938 Extract supported meta-tags from given text value
939 939 """
940 940 tags = []
941 941 if not value:
942 942 return tags, ''
943 943
944 944 for key, val in tags_paterns.items():
945 945 pat, replace_html = val
946 946 tags.extend([(key, x.group()) for x in pat.finditer(value)])
947 947 value = pat.sub('', value)
948 948
949 949 return tags, value
950 950
951 951
952 952 def style_metatag(tag_type, value):
953 953 """
954 954 converts tags from value into html equivalent
955 955 """
956 956 if not value:
957 957 return ''
958 958
959 959 html_value = value
960 960 tag_data = tags_paterns.get(tag_type)
961 961 if tag_data:
962 962 pat, replace_html = tag_data
963 963 # convert to plain `unicode` instead of a markup tag to be used in
964 964 # regex expressions. safe_unicode doesn't work here
965 965 html_value = pat.sub(replace_html, unicode(value))
966 966
967 967 return html_value
968 968
969 969
970 970 def bool2icon(value, show_at_false=True):
971 971 """
972 972 Returns boolean value of a given value, represented as html element with
973 973 classes that will represent icons
974 974
975 975 :param value: given value to convert to html node
976 976 """
977 977
978 978 if value: # does bool conversion
979 979 return HTML.tag('i', class_="icon-true", title='True')
980 980 else: # not true as bool
981 981 if show_at_false:
982 982 return HTML.tag('i', class_="icon-false", title='False')
983 983 return HTML.tag('i')
984 984
985 985 #==============================================================================
986 986 # PERMS
987 987 #==============================================================================
988 988 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
989 989 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
990 990 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
991 991 csrf_token_key
992 992
993 993
994 994 #==============================================================================
995 995 # GRAVATAR URL
996 996 #==============================================================================
997 997 class InitialsGravatar(object):
998 998 def __init__(self, email_address, first_name, last_name, size=30,
999 999 background=None, text_color='#fff'):
1000 1000 self.size = size
1001 1001 self.first_name = first_name
1002 1002 self.last_name = last_name
1003 1003 self.email_address = email_address
1004 1004 self.background = background or self.str2color(email_address)
1005 1005 self.text_color = text_color
1006 1006
1007 1007 def get_color_bank(self):
1008 1008 """
1009 1009 returns a predefined list of colors that gravatars can use.
1010 1010 Those are randomized distinct colors that guarantee readability and
1011 1011 uniqueness.
1012 1012
1013 1013 generated with: http://phrogz.net/css/distinct-colors.html
1014 1014 """
1015 1015 return [
1016 1016 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1017 1017 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1018 1018 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1019 1019 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1020 1020 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1021 1021 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1022 1022 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1023 1023 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1024 1024 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1025 1025 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1026 1026 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1027 1027 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1028 1028 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1029 1029 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1030 1030 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1031 1031 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1032 1032 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1033 1033 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1034 1034 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1035 1035 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1036 1036 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1037 1037 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1038 1038 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1039 1039 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1040 1040 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1041 1041 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1042 1042 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1043 1043 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1044 1044 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1045 1045 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1046 1046 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1047 1047 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1048 1048 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1049 1049 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1050 1050 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1051 1051 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1052 1052 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1053 1053 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1054 1054 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1055 1055 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1056 1056 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1057 1057 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1058 1058 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1059 1059 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1060 1060 '#4f8c46', '#368dd9', '#5c0073'
1061 1061 ]
1062 1062
1063 1063 def rgb_to_hex_color(self, rgb_tuple):
1064 1064 """
1065 1065 Converts an rgb_tuple passed to an hex color.
1066 1066
1067 1067 :param rgb_tuple: tuple with 3 ints represents rgb color space
1068 1068 """
1069 1069 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1070 1070
1071 1071 def email_to_int_list(self, email_str):
1072 1072 """
1073 1073 Get every byte of the hex digest value of email and turn it to integer.
1074 1074 It's going to be always between 0-255
1075 1075 """
1076 1076 digest = md5_safe(email_str.lower())
1077 1077 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1078 1078
1079 1079 def pick_color_bank_index(self, email_str, color_bank):
1080 1080 return self.email_to_int_list(email_str)[0] % len(color_bank)
1081 1081
1082 1082 def str2color(self, email_str):
1083 1083 """
1084 1084 Tries to map in a stable algorithm an email to color
1085 1085
1086 1086 :param email_str:
1087 1087 """
1088 1088 color_bank = self.get_color_bank()
1089 1089 # pick position (module it's length so we always find it in the
1090 1090 # bank even if it's smaller than 256 values
1091 1091 pos = self.pick_color_bank_index(email_str, color_bank)
1092 1092 return color_bank[pos]
1093 1093
1094 1094 def normalize_email(self, email_address):
1095 1095 import unicodedata
1096 1096 # default host used to fill in the fake/missing email
1097 1097 default_host = u'localhost'
1098 1098
1099 1099 if not email_address:
1100 1100 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1101 1101
1102 1102 email_address = safe_unicode(email_address)
1103 1103
1104 1104 if u'@' not in email_address:
1105 1105 email_address = u'%s@%s' % (email_address, default_host)
1106 1106
1107 1107 if email_address.endswith(u'@'):
1108 1108 email_address = u'%s%s' % (email_address, default_host)
1109 1109
1110 1110 email_address = unicodedata.normalize('NFKD', email_address)\
1111 1111 .encode('ascii', 'ignore')
1112 1112 return email_address
1113 1113
1114 1114 def get_initials(self):
1115 1115 """
1116 1116 Returns 2 letter initials calculated based on the input.
1117 1117 The algorithm picks first given email address, and takes first letter
1118 1118 of part before @, and then the first letter of server name. In case
1119 1119 the part before @ is in a format of `somestring.somestring2` it replaces
1120 1120 the server letter with first letter of somestring2
1121 1121
1122 1122 In case function was initialized with both first and lastname, this
1123 1123 overrides the extraction from email by first letter of the first and
1124 1124 last name. We add special logic to that functionality, In case Full name
1125 1125 is compound, like Guido Von Rossum, we use last part of the last name
1126 1126 (Von Rossum) picking `R`.
1127 1127
1128 1128 Function also normalizes the non-ascii characters to they ascii
1129 1129 representation, eg Δ„ => A
1130 1130 """
1131 1131 import unicodedata
1132 1132 # replace non-ascii to ascii
1133 1133 first_name = unicodedata.normalize(
1134 1134 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1135 1135 last_name = unicodedata.normalize(
1136 1136 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1137 1137
1138 1138 # do NFKD encoding, and also make sure email has proper format
1139 1139 email_address = self.normalize_email(self.email_address)
1140 1140
1141 1141 # first push the email initials
1142 1142 prefix, server = email_address.split('@', 1)
1143 1143
1144 1144 # check if prefix is maybe a 'first_name.last_name' syntax
1145 1145 _dot_split = prefix.rsplit('.', 1)
1146 1146 if len(_dot_split) == 2 and _dot_split[1]:
1147 1147 initials = [_dot_split[0][0], _dot_split[1][0]]
1148 1148 else:
1149 1149 initials = [prefix[0], server[0]]
1150 1150
1151 1151 # then try to replace either first_name or last_name
1152 1152 fn_letter = (first_name or " ")[0].strip()
1153 1153 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1154 1154
1155 1155 if fn_letter:
1156 1156 initials[0] = fn_letter
1157 1157
1158 1158 if ln_letter:
1159 1159 initials[1] = ln_letter
1160 1160
1161 1161 return ''.join(initials).upper()
1162 1162
1163 1163 def get_img_data_by_type(self, font_family, img_type):
1164 1164 default_user = """
1165 1165 <svg xmlns="http://www.w3.org/2000/svg"
1166 1166 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1167 1167 viewBox="-15 -10 439.165 429.164"
1168 1168
1169 1169 xml:space="preserve"
1170 1170 style="background:{background};" >
1171 1171
1172 1172 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1173 1173 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1174 1174 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1175 1175 168.596,153.916,216.671,
1176 1176 204.583,216.671z" fill="{text_color}"/>
1177 1177 <path d="M407.164,374.717L360.88,
1178 1178 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1179 1179 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1180 1180 15.366-44.203,23.488-69.076,23.488c-24.877,
1181 1181 0-48.762-8.122-69.078-23.488
1182 1182 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1183 1183 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1184 1184 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1185 1185 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1186 1186 19.402-10.527 C409.699,390.129,
1187 1187 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1188 1188 </svg>""".format(
1189 1189 size=self.size,
1190 1190 background='#979797', # @grey4
1191 1191 text_color=self.text_color,
1192 1192 font_family=font_family)
1193 1193
1194 1194 return {
1195 1195 "default_user": default_user
1196 1196 }[img_type]
1197 1197
1198 1198 def get_img_data(self, svg_type=None):
1199 1199 """
1200 1200 generates the svg metadata for image
1201 1201 """
1202 1202 fonts = [
1203 1203 '-apple-system',
1204 1204 'BlinkMacSystemFont',
1205 1205 'Segoe UI',
1206 1206 'Roboto',
1207 1207 'Oxygen-Sans',
1208 1208 'Ubuntu',
1209 1209 'Cantarell',
1210 1210 'Helvetica Neue',
1211 1211 'sans-serif'
1212 1212 ]
1213 1213 font_family = ','.join(fonts)
1214 1214 if svg_type:
1215 1215 return self.get_img_data_by_type(font_family, svg_type)
1216 1216
1217 1217 initials = self.get_initials()
1218 1218 img_data = """
1219 1219 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1220 1220 width="{size}" height="{size}"
1221 1221 style="width: 100%; height: 100%; background-color: {background}"
1222 1222 viewBox="0 0 {size} {size}">
1223 1223 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1224 1224 pointer-events="auto" fill="{text_color}"
1225 1225 font-family="{font_family}"
1226 1226 style="font-weight: 400; font-size: {f_size}px;">{text}
1227 1227 </text>
1228 1228 </svg>""".format(
1229 1229 size=self.size,
1230 1230 f_size=self.size/2.05, # scale the text inside the box nicely
1231 1231 background=self.background,
1232 1232 text_color=self.text_color,
1233 1233 text=initials.upper(),
1234 1234 font_family=font_family)
1235 1235
1236 1236 return img_data
1237 1237
1238 1238 def generate_svg(self, svg_type=None):
1239 1239 img_data = self.get_img_data(svg_type)
1240 1240 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1241 1241
1242 1242
1243 1243 def initials_gravatar(email_address, first_name, last_name, size=30):
1244 1244 svg_type = None
1245 1245 if email_address == User.DEFAULT_USER_EMAIL:
1246 1246 svg_type = 'default_user'
1247 1247 klass = InitialsGravatar(email_address, first_name, last_name, size)
1248 1248 return klass.generate_svg(svg_type=svg_type)
1249 1249
1250 1250
1251 1251 def gravatar_url(email_address, size=30, request=None):
1252 1252 request = get_current_request()
1253 1253 _use_gravatar = request.call_context.visual.use_gravatar
1254 1254 _gravatar_url = request.call_context.visual.gravatar_url
1255 1255
1256 1256 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1257 1257
1258 1258 email_address = email_address or User.DEFAULT_USER_EMAIL
1259 1259 if isinstance(email_address, unicode):
1260 1260 # hashlib crashes on unicode items
1261 1261 email_address = safe_str(email_address)
1262 1262
1263 1263 # empty email or default user
1264 1264 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1265 1265 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1266 1266
1267 1267 if _use_gravatar:
1268 1268 # TODO: Disuse pyramid thread locals. Think about another solution to
1269 1269 # get the host and schema here.
1270 1270 request = get_current_request()
1271 1271 tmpl = safe_str(_gravatar_url)
1272 1272 tmpl = tmpl.replace('{email}', email_address)\
1273 1273 .replace('{md5email}', md5_safe(email_address.lower())) \
1274 1274 .replace('{netloc}', request.host)\
1275 1275 .replace('{scheme}', request.scheme)\
1276 1276 .replace('{size}', safe_str(size))
1277 1277 return tmpl
1278 1278 else:
1279 1279 return initials_gravatar(email_address, '', '', size=size)
1280 1280
1281 1281
1282 1282 class Page(_Page):
1283 1283 """
1284 1284 Custom pager to match rendering style with paginator
1285 1285 """
1286 1286
1287 1287 def _get_pos(self, cur_page, max_page, items):
1288 1288 edge = (items / 2) + 1
1289 1289 if (cur_page <= edge):
1290 1290 radius = max(items / 2, items - cur_page)
1291 1291 elif (max_page - cur_page) < edge:
1292 1292 radius = (items - 1) - (max_page - cur_page)
1293 1293 else:
1294 1294 radius = items / 2
1295 1295
1296 1296 left = max(1, (cur_page - (radius)))
1297 1297 right = min(max_page, cur_page + (radius))
1298 1298 return left, cur_page, right
1299 1299
1300 1300 def _range(self, regexp_match):
1301 1301 """
1302 1302 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1303 1303
1304 1304 Arguments:
1305 1305
1306 1306 regexp_match
1307 1307 A "re" (regular expressions) match object containing the
1308 1308 radius of linked pages around the current page in
1309 1309 regexp_match.group(1) as a string
1310 1310
1311 1311 This function is supposed to be called as a callable in
1312 1312 re.sub.
1313 1313
1314 1314 """
1315 1315 radius = int(regexp_match.group(1))
1316 1316
1317 1317 # Compute the first and last page number within the radius
1318 1318 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1319 1319 # -> leftmost_page = 5
1320 1320 # -> rightmost_page = 9
1321 1321 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1322 1322 self.last_page,
1323 1323 (radius * 2) + 1)
1324 1324 nav_items = []
1325 1325
1326 1326 # Create a link to the first page (unless we are on the first page
1327 1327 # or there would be no need to insert '..' spacers)
1328 1328 if self.page != self.first_page and self.first_page < leftmost_page:
1329 1329 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1330 1330
1331 1331 # Insert dots if there are pages between the first page
1332 1332 # and the currently displayed page range
1333 1333 if leftmost_page - self.first_page > 1:
1334 1334 # Wrap in a SPAN tag if nolink_attr is set
1335 1335 text = '..'
1336 1336 if self.dotdot_attr:
1337 1337 text = HTML.span(c=text, **self.dotdot_attr)
1338 1338 nav_items.append(text)
1339 1339
1340 1340 for thispage in xrange(leftmost_page, rightmost_page + 1):
1341 1341 # Hilight the current page number and do not use a link
1342 1342 if thispage == self.page:
1343 1343 text = '%s' % (thispage,)
1344 1344 # Wrap in a SPAN tag if nolink_attr is set
1345 1345 if self.curpage_attr:
1346 1346 text = HTML.span(c=text, **self.curpage_attr)
1347 1347 nav_items.append(text)
1348 1348 # Otherwise create just a link to that page
1349 1349 else:
1350 1350 text = '%s' % (thispage,)
1351 1351 nav_items.append(self._pagerlink(thispage, text))
1352 1352
1353 1353 # Insert dots if there are pages between the displayed
1354 1354 # page numbers and the end of the page range
1355 1355 if self.last_page - rightmost_page > 1:
1356 1356 text = '..'
1357 1357 # Wrap in a SPAN tag if nolink_attr is set
1358 1358 if self.dotdot_attr:
1359 1359 text = HTML.span(c=text, **self.dotdot_attr)
1360 1360 nav_items.append(text)
1361 1361
1362 1362 # Create a link to the very last page (unless we are on the last
1363 1363 # page or there would be no need to insert '..' spacers)
1364 1364 if self.page != self.last_page and rightmost_page < self.last_page:
1365 1365 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1366 1366
1367 1367 ## prerender links
1368 1368 #_page_link = url.current()
1369 1369 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1370 1370 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1371 1371 return self.separator.join(nav_items)
1372 1372
1373 1373 def pager(self, format='~2~', page_param='page', partial_param='partial',
1374 1374 show_if_single_page=False, separator=' ', onclick=None,
1375 1375 symbol_first='<<', symbol_last='>>',
1376 1376 symbol_previous='<', symbol_next='>',
1377 1377 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1378 1378 curpage_attr={'class': 'pager_curpage'},
1379 1379 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1380 1380
1381 1381 self.curpage_attr = curpage_attr
1382 1382 self.separator = separator
1383 1383 self.pager_kwargs = kwargs
1384 1384 self.page_param = page_param
1385 1385 self.partial_param = partial_param
1386 1386 self.onclick = onclick
1387 1387 self.link_attr = link_attr
1388 1388 self.dotdot_attr = dotdot_attr
1389 1389
1390 1390 # Don't show navigator if there is no more than one page
1391 1391 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1392 1392 return ''
1393 1393
1394 1394 from string import Template
1395 1395 # Replace ~...~ in token format by range of pages
1396 1396 result = re.sub(r'~(\d+)~', self._range, format)
1397 1397
1398 1398 # Interpolate '%' variables
1399 1399 result = Template(result).safe_substitute({
1400 1400 'first_page': self.first_page,
1401 1401 'last_page': self.last_page,
1402 1402 'page': self.page,
1403 1403 'page_count': self.page_count,
1404 1404 'items_per_page': self.items_per_page,
1405 1405 'first_item': self.first_item,
1406 1406 'last_item': self.last_item,
1407 1407 'item_count': self.item_count,
1408 1408 'link_first': self.page > self.first_page and \
1409 1409 self._pagerlink(self.first_page, symbol_first) or '',
1410 1410 'link_last': self.page < self.last_page and \
1411 1411 self._pagerlink(self.last_page, symbol_last) or '',
1412 1412 'link_previous': self.previous_page and \
1413 1413 self._pagerlink(self.previous_page, symbol_previous) \
1414 1414 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1415 1415 'link_next': self.next_page and \
1416 1416 self._pagerlink(self.next_page, symbol_next) \
1417 1417 or HTML.span(symbol_next, class_="pg-next disabled")
1418 1418 })
1419 1419
1420 1420 return literal(result)
1421 1421
1422 1422
1423 1423 #==============================================================================
1424 1424 # REPO PAGER, PAGER FOR REPOSITORY
1425 1425 #==============================================================================
1426 1426 class RepoPage(Page):
1427 1427
1428 1428 def __init__(self, collection, page=1, items_per_page=20,
1429 1429 item_count=None, url=None, **kwargs):
1430 1430
1431 1431 """Create a "RepoPage" instance. special pager for paging
1432 1432 repository
1433 1433 """
1434 1434 self._url_generator = url
1435 1435
1436 1436 # Safe the kwargs class-wide so they can be used in the pager() method
1437 1437 self.kwargs = kwargs
1438 1438
1439 1439 # Save a reference to the collection
1440 1440 self.original_collection = collection
1441 1441
1442 1442 self.collection = collection
1443 1443
1444 1444 # The self.page is the number of the current page.
1445 1445 # The first page has the number 1!
1446 1446 try:
1447 1447 self.page = int(page) # make it int() if we get it as a string
1448 1448 except (ValueError, TypeError):
1449 1449 self.page = 1
1450 1450
1451 1451 self.items_per_page = items_per_page
1452 1452
1453 1453 # Unless the user tells us how many items the collections has
1454 1454 # we calculate that ourselves.
1455 1455 if item_count is not None:
1456 1456 self.item_count = item_count
1457 1457 else:
1458 1458 self.item_count = len(self.collection)
1459 1459
1460 1460 # Compute the number of the first and last available page
1461 1461 if self.item_count > 0:
1462 1462 self.first_page = 1
1463 1463 self.page_count = int(math.ceil(float(self.item_count) /
1464 1464 self.items_per_page))
1465 1465 self.last_page = self.first_page + self.page_count - 1
1466 1466
1467 1467 # Make sure that the requested page number is the range of
1468 1468 # valid pages
1469 1469 if self.page > self.last_page:
1470 1470 self.page = self.last_page
1471 1471 elif self.page < self.first_page:
1472 1472 self.page = self.first_page
1473 1473
1474 1474 # Note: the number of items on this page can be less than
1475 1475 # items_per_page if the last page is not full
1476 1476 self.first_item = max(0, (self.item_count) - (self.page *
1477 1477 items_per_page))
1478 1478 self.last_item = ((self.item_count - 1) - items_per_page *
1479 1479 (self.page - 1))
1480 1480
1481 1481 self.items = list(self.collection[self.first_item:self.last_item + 1])
1482 1482
1483 1483 # Links to previous and next page
1484 1484 if self.page > self.first_page:
1485 1485 self.previous_page = self.page - 1
1486 1486 else:
1487 1487 self.previous_page = None
1488 1488
1489 1489 if self.page < self.last_page:
1490 1490 self.next_page = self.page + 1
1491 1491 else:
1492 1492 self.next_page = None
1493 1493
1494 1494 # No items available
1495 1495 else:
1496 1496 self.first_page = None
1497 1497 self.page_count = 0
1498 1498 self.last_page = None
1499 1499 self.first_item = None
1500 1500 self.last_item = None
1501 1501 self.previous_page = None
1502 1502 self.next_page = None
1503 1503 self.items = []
1504 1504
1505 1505 # This is a subclass of the 'list' type. Initialise the list now.
1506 1506 list.__init__(self, reversed(self.items))
1507 1507
1508 1508
1509 1509 def breadcrumb_repo_link(repo):
1510 1510 """
1511 1511 Makes a breadcrumbs path link to repo
1512 1512
1513 1513 ex::
1514 1514 group >> subgroup >> repo
1515 1515
1516 1516 :param repo: a Repository instance
1517 1517 """
1518 1518
1519 1519 path = [
1520 1520 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1521 1521 title='last change:{}'.format(format_date(group.last_commit_change)))
1522 1522 for group in repo.groups_with_parents
1523 1523 ] + [
1524 1524 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1525 1525 title='last change:{}'.format(format_date(repo.last_commit_change)))
1526 1526 ]
1527 1527
1528 1528 return literal(' &raquo; '.join(path))
1529 1529
1530 1530
1531 1531 def breadcrumb_repo_group_link(repo_group):
1532 1532 """
1533 1533 Makes a breadcrumbs path link to repo
1534 1534
1535 1535 ex::
1536 1536 group >> subgroup
1537 1537
1538 1538 :param repo_group: a Repository Group instance
1539 1539 """
1540 1540
1541 1541 path = [
1542 1542 link_to(group.name,
1543 1543 route_path('repo_group_home', repo_group_name=group.group_name),
1544 1544 title='last change:{}'.format(format_date(group.last_commit_change)))
1545 1545 for group in repo_group.parents
1546 1546 ] + [
1547 1547 link_to(repo_group.name,
1548 1548 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1549 1549 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1550 1550 ]
1551 1551
1552 1552 return literal(' &raquo; '.join(path))
1553 1553
1554 1554
1555 1555 def format_byte_size_binary(file_size):
1556 1556 """
1557 1557 Formats file/folder sizes to standard.
1558 1558 """
1559 1559 if file_size is None:
1560 1560 file_size = 0
1561 1561
1562 1562 formatted_size = format_byte_size(file_size, binary=True)
1563 1563 return formatted_size
1564 1564
1565 1565
1566 1566 def urlify_text(text_, safe=True):
1567 1567 """
1568 1568 Extrac urls from text and make html links out of them
1569 1569
1570 1570 :param text_:
1571 1571 """
1572 1572
1573 1573 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1574 1574 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1575 1575
1576 1576 def url_func(match_obj):
1577 1577 url_full = match_obj.groups()[0]
1578 1578 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1579 1579 _newtext = url_pat.sub(url_func, text_)
1580 1580 if safe:
1581 1581 return literal(_newtext)
1582 1582 return _newtext
1583 1583
1584 1584
1585 1585 def urlify_commits(text_, repository):
1586 1586 """
1587 1587 Extract commit ids from text and make link from them
1588 1588
1589 1589 :param text_:
1590 1590 :param repository: repo name to build the URL with
1591 1591 """
1592 1592
1593 1593 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1594 1594
1595 1595 def url_func(match_obj):
1596 1596 commit_id = match_obj.groups()[1]
1597 1597 pref = match_obj.groups()[0]
1598 1598 suf = match_obj.groups()[2]
1599 1599
1600 1600 tmpl = (
1601 1601 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1602 1602 '%(commit_id)s</a>%(suf)s'
1603 1603 )
1604 1604 return tmpl % {
1605 1605 'pref': pref,
1606 1606 'cls': 'revision-link',
1607 1607 'url': route_url('repo_commit', repo_name=repository, commit_id=commit_id),
1608 1608 'commit_id': commit_id,
1609 1609 'suf': suf
1610 1610 }
1611 1611
1612 1612 newtext = URL_PAT.sub(url_func, text_)
1613 1613
1614 1614 return newtext
1615 1615
1616 1616
1617 1617 def _process_url_func(match_obj, repo_name, uid, entry,
1618 1618 return_raw_data=False, link_format='html'):
1619 1619 pref = ''
1620 1620 if match_obj.group().startswith(' '):
1621 1621 pref = ' '
1622 1622
1623 1623 issue_id = ''.join(match_obj.groups())
1624 1624
1625 1625 if link_format == 'html':
1626 1626 tmpl = (
1627 1627 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1628 1628 '%(issue-prefix)s%(id-repr)s'
1629 1629 '</a>')
1630 1630 elif link_format == 'rst':
1631 1631 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1632 1632 elif link_format == 'markdown':
1633 1633 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1634 1634 else:
1635 1635 raise ValueError('Bad link_format:{}'.format(link_format))
1636 1636
1637 1637 (repo_name_cleaned,
1638 1638 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1639 1639
1640 1640 # variables replacement
1641 1641 named_vars = {
1642 1642 'id': issue_id,
1643 1643 'repo': repo_name,
1644 1644 'repo_name': repo_name_cleaned,
1645 1645 'group_name': parent_group_name,
1646 1646 }
1647 1647 # named regex variables
1648 1648 named_vars.update(match_obj.groupdict())
1649 1649 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1650 1650
1651 1651 def quote_cleaner(input_str):
1652 1652 """Remove quotes as it's HTML"""
1653 1653 return input_str.replace('"', '')
1654 1654
1655 1655 data = {
1656 1656 'pref': pref,
1657 1657 'cls': quote_cleaner('issue-tracker-link'),
1658 1658 'url': quote_cleaner(_url),
1659 1659 'id-repr': issue_id,
1660 1660 'issue-prefix': entry['pref'],
1661 1661 'serv': entry['url'],
1662 1662 'title': entry['desc']
1663 1663 }
1664 1664 if return_raw_data:
1665 1665 return {
1666 1666 'id': issue_id,
1667 1667 'url': _url
1668 1668 }
1669 1669 return tmpl % data
1670 1670
1671 1671
1672 1672 def get_active_pattern_entries(repo_name):
1673 1673 repo = None
1674 1674 if repo_name:
1675 1675 # Retrieving repo_name to avoid invalid repo_name to explode on
1676 1676 # IssueTrackerSettingsModel but still passing invalid name further down
1677 1677 repo = Repository.get_by_repo_name(repo_name, cache=True)
1678 1678
1679 1679 settings_model = IssueTrackerSettingsModel(repo=repo)
1680 1680 active_entries = settings_model.get_settings(cache=True)
1681 1681 return active_entries
1682 1682
1683 1683
1684 1684 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1685 1685
1686 1686 allowed_formats = ['html', 'rst', 'markdown']
1687 1687 if link_format not in allowed_formats:
1688 1688 raise ValueError('Link format can be only one of:{} got {}'.format(
1689 1689 allowed_formats, link_format))
1690 1690
1691 1691 active_entries = active_entries or get_active_pattern_entries(repo_name)
1692 1692 issues_data = []
1693 newtext = text_string
1693 new_text = text_string
1694 1694
1695 log.debug('Got %s entries to process', len(active_entries))
1695 1696 for uid, entry in active_entries.items():
1696 1697 log.debug('found issue tracker entry with uid %s', uid)
1697 1698
1698 1699 if not (entry['pat'] and entry['url']):
1699 1700 log.debug('skipping due to missing data')
1700 1701 continue
1701 1702
1702 1703 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1703 1704 uid, entry['pat'], entry['url'], entry['pref'])
1704 1705
1705 1706 try:
1706 1707 pattern = re.compile(r'%s' % entry['pat'])
1707 1708 except re.error:
1708 log.exception(
1709 'issue tracker pattern: `%s` failed to compile',
1710 entry['pat'])
1709 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1711 1710 continue
1712 1711
1713 1712 data_func = partial(
1714 1713 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1715 1714 return_raw_data=True)
1716 1715
1717 1716 for match_obj in pattern.finditer(text_string):
1718 1717 issues_data.append(data_func(match_obj))
1719 1718
1720 1719 url_func = partial(
1721 1720 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1722 1721 link_format=link_format)
1723 1722
1724 newtext = pattern.sub(url_func, newtext)
1723 new_text = pattern.sub(url_func, new_text)
1725 1724 log.debug('processed prefix:uid `%s`', uid)
1726 1725
1727 return newtext, issues_data
1726 return new_text, issues_data
1728 1727
1729 1728
1730 1729 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1731 1730 """
1732 1731 Parses given text message and makes proper links.
1733 1732 issues are linked to given issue-server, and rest is a commit link
1734 1733
1735 1734 :param commit_text:
1736 1735 :param repository:
1737 1736 """
1738 1737 def escaper(string):
1739 1738 return string.replace('<', '&lt;').replace('>', '&gt;')
1740 1739
1741 1740 newtext = escaper(commit_text)
1742 1741
1743 1742 # extract http/https links and make them real urls
1744 1743 newtext = urlify_text(newtext, safe=False)
1745 1744
1746 1745 # urlify commits - extract commit ids and make link out of them, if we have
1747 1746 # the scope of repository present.
1748 1747 if repository:
1749 1748 newtext = urlify_commits(newtext, repository)
1750 1749
1751 1750 # process issue tracker patterns
1752 1751 newtext, issues = process_patterns(newtext, repository or '',
1753 1752 active_entries=active_pattern_entries)
1754 1753
1755 1754 return literal(newtext)
1756 1755
1757 1756
1758 1757 def render_binary(repo_name, file_obj):
1759 1758 """
1760 1759 Choose how to render a binary file
1761 1760 """
1762 1761
1763 1762 filename = file_obj.name
1764 1763
1765 1764 # images
1766 1765 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1767 1766 if fnmatch.fnmatch(filename, pat=ext):
1768 1767 alt = escape(filename)
1769 1768 src = route_path(
1770 1769 'repo_file_raw', repo_name=repo_name,
1771 1770 commit_id=file_obj.commit.raw_id,
1772 1771 f_path=file_obj.path)
1773 1772 return literal(
1774 1773 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1775 1774
1776 1775
1777 1776 def renderer_from_filename(filename, exclude=None):
1778 1777 """
1779 1778 choose a renderer based on filename, this works only for text based files
1780 1779 """
1781 1780
1782 1781 # ipython
1783 1782 for ext in ['*.ipynb']:
1784 1783 if fnmatch.fnmatch(filename, pat=ext):
1785 1784 return 'jupyter'
1786 1785
1787 1786 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1788 1787 if is_markup:
1789 1788 return is_markup
1790 1789 return None
1791 1790
1792 1791
1793 1792 def render(source, renderer='rst', mentions=False, relative_urls=None,
1794 1793 repo_name=None):
1795 1794
1796 1795 def maybe_convert_relative_links(html_source):
1797 1796 if relative_urls:
1798 1797 return relative_links(html_source, relative_urls)
1799 1798 return html_source
1800 1799
1801 1800 if renderer == 'plain':
1802 1801 return literal(
1803 1802 MarkupRenderer.plain(source, leading_newline=False))
1804 1803
1805 1804 elif renderer == 'rst':
1806 1805 if repo_name:
1807 1806 # process patterns on comments if we pass in repo name
1808 1807 source, issues = process_patterns(
1809 1808 source, repo_name, link_format='rst')
1810 1809
1811 1810 return literal(
1812 1811 '<div class="rst-block">%s</div>' %
1813 1812 maybe_convert_relative_links(
1814 1813 MarkupRenderer.rst(source, mentions=mentions)))
1815 1814
1816 1815 elif renderer == 'markdown':
1817 1816 if repo_name:
1818 1817 # process patterns on comments if we pass in repo name
1819 1818 source, issues = process_patterns(
1820 1819 source, repo_name, link_format='markdown')
1821 1820
1822 1821 return literal(
1823 1822 '<div class="markdown-block">%s</div>' %
1824 1823 maybe_convert_relative_links(
1825 1824 MarkupRenderer.markdown(source, flavored=True,
1826 1825 mentions=mentions)))
1827 1826
1828 1827 elif renderer == 'jupyter':
1829 1828 return literal(
1830 1829 '<div class="ipynb">%s</div>' %
1831 1830 maybe_convert_relative_links(
1832 1831 MarkupRenderer.jupyter(source)))
1833 1832
1834 1833 # None means just show the file-source
1835 1834 return None
1836 1835
1837 1836
1838 1837 def commit_status(repo, commit_id):
1839 1838 return ChangesetStatusModel().get_status(repo, commit_id)
1840 1839
1841 1840
1842 1841 def commit_status_lbl(commit_status):
1843 1842 return dict(ChangesetStatus.STATUSES).get(commit_status)
1844 1843
1845 1844
1846 1845 def commit_time(repo_name, commit_id):
1847 1846 repo = Repository.get_by_repo_name(repo_name)
1848 1847 commit = repo.get_commit(commit_id=commit_id)
1849 1848 return commit.date
1850 1849
1851 1850
1852 1851 def get_permission_name(key):
1853 1852 return dict(Permission.PERMS).get(key)
1854 1853
1855 1854
1856 1855 def journal_filter_help(request):
1857 1856 _ = request.translate
1858 1857 from rhodecode.lib.audit_logger import ACTIONS
1859 1858 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1860 1859
1861 1860 return _(
1862 1861 'Example filter terms:\n' +
1863 1862 ' repository:vcs\n' +
1864 1863 ' username:marcin\n' +
1865 1864 ' username:(NOT marcin)\n' +
1866 1865 ' action:*push*\n' +
1867 1866 ' ip:127.0.0.1\n' +
1868 1867 ' date:20120101\n' +
1869 1868 ' date:[20120101100000 TO 20120102]\n' +
1870 1869 '\n' +
1871 1870 'Actions: {actions}\n' +
1872 1871 '\n' +
1873 1872 'Generate wildcards using \'*\' character:\n' +
1874 1873 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1875 1874 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1876 1875 '\n' +
1877 1876 'Optional AND / OR operators in queries\n' +
1878 1877 ' "repository:vcs OR repository:test"\n' +
1879 1878 ' "username:test AND repository:test*"\n'
1880 1879 ).format(actions=actions)
1881 1880
1882 1881
1883 1882 def not_mapped_error(repo_name):
1884 1883 from rhodecode.translation import _
1885 1884 flash(_('%s repository is not mapped to db perhaps'
1886 1885 ' it was created or renamed from the filesystem'
1887 1886 ' please run the application again'
1888 1887 ' in order to rescan repositories') % repo_name, category='error')
1889 1888
1890 1889
1891 1890 def ip_range(ip_addr):
1892 1891 from rhodecode.model.db import UserIpMap
1893 1892 s, e = UserIpMap._get_ip_range(ip_addr)
1894 1893 return '%s - %s' % (s, e)
1895 1894
1896 1895
1897 1896 def form(url, method='post', needs_csrf_token=True, **attrs):
1898 1897 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1899 1898 if method.lower() != 'get' and needs_csrf_token:
1900 1899 raise Exception(
1901 1900 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1902 1901 'CSRF token. If the endpoint does not require such token you can ' +
1903 1902 'explicitly set the parameter needs_csrf_token to false.')
1904 1903
1905 1904 return wh_form(url, method=method, **attrs)
1906 1905
1907 1906
1908 1907 def secure_form(form_url, method="POST", multipart=False, **attrs):
1909 1908 """Start a form tag that points the action to an url. This
1910 1909 form tag will also include the hidden field containing
1911 1910 the auth token.
1912 1911
1913 1912 The url options should be given either as a string, or as a
1914 1913 ``url()`` function. The method for the form defaults to POST.
1915 1914
1916 1915 Options:
1917 1916
1918 1917 ``multipart``
1919 1918 If set to True, the enctype is set to "multipart/form-data".
1920 1919 ``method``
1921 1920 The method to use when submitting the form, usually either
1922 1921 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1923 1922 hidden input with name _method is added to simulate the verb
1924 1923 over POST.
1925 1924
1926 1925 """
1927 1926 from webhelpers.pylonslib.secure_form import insecure_form
1928 1927
1929 1928 if 'request' in attrs:
1930 1929 session = attrs['request'].session
1931 1930 del attrs['request']
1932 1931 else:
1933 1932 raise ValueError(
1934 1933 'Calling this form requires request= to be passed as argument')
1935 1934
1936 1935 form = insecure_form(form_url, method, multipart, **attrs)
1937 1936 token = literal(
1938 1937 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1939 1938 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1940 1939
1941 1940 return literal("%s\n%s" % (form, token))
1942 1941
1943 1942
1944 1943 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1945 1944 select_html = select(name, selected, options, **attrs)
1946 1945
1947 1946 select2 = """
1948 1947 <script>
1949 1948 $(document).ready(function() {
1950 1949 $('#%s').select2({
1951 1950 containerCssClass: 'drop-menu %s',
1952 1951 dropdownCssClass: 'drop-menu-dropdown',
1953 1952 dropdownAutoWidth: true%s
1954 1953 });
1955 1954 });
1956 1955 </script>
1957 1956 """
1958 1957
1959 1958 filter_option = """,
1960 1959 minimumResultsForSearch: -1
1961 1960 """
1962 1961 input_id = attrs.get('id') or name
1963 1962 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1964 1963 filter_enabled = "" if enable_filter else filter_option
1965 1964 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1966 1965
1967 1966 return literal(select_html+select_script)
1968 1967
1969 1968
1970 1969 def get_visual_attr(tmpl_context_var, attr_name):
1971 1970 """
1972 1971 A safe way to get a variable from visual variable of template context
1973 1972
1974 1973 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1975 1974 :param attr_name: name of the attribute we fetch from the c.visual
1976 1975 """
1977 1976 visual = getattr(tmpl_context_var, 'visual', None)
1978 1977 if not visual:
1979 1978 return
1980 1979 else:
1981 1980 return getattr(visual, attr_name, None)
1982 1981
1983 1982
1984 1983 def get_last_path_part(file_node):
1985 1984 if not file_node.path:
1986 1985 return u'/'
1987 1986
1988 1987 path = safe_unicode(file_node.path.split('/')[-1])
1989 1988 return u'../' + path
1990 1989
1991 1990
1992 1991 def route_url(*args, **kwargs):
1993 1992 """
1994 1993 Wrapper around pyramids `route_url` (fully qualified url) function.
1995 1994 """
1996 1995 req = get_current_request()
1997 1996 return req.route_url(*args, **kwargs)
1998 1997
1999 1998
2000 1999 def route_path(*args, **kwargs):
2001 2000 """
2002 2001 Wrapper around pyramids `route_path` function.
2003 2002 """
2004 2003 req = get_current_request()
2005 2004 return req.route_path(*args, **kwargs)
2006 2005
2007 2006
2008 2007 def route_path_or_none(*args, **kwargs):
2009 2008 try:
2010 2009 return route_path(*args, **kwargs)
2011 2010 except KeyError:
2012 2011 return None
2013 2012
2014 2013
2015 2014 def current_route_path(request, **kw):
2016 2015 new_args = request.GET.mixed()
2017 2016 new_args.update(kw)
2018 2017 return request.current_route_path(_query=new_args)
2019 2018
2020 2019
2021 2020 def curl_api_example(method, args):
2022 2021 args_json = json.dumps(OrderedDict([
2023 2022 ('id', 1),
2024 2023 ('auth_token', 'SECRET'),
2025 2024 ('method', method),
2026 2025 ('args', args)
2027 2026 ]))
2028 2027
2029 2028 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2030 2029 api_url=route_url('apiv2'),
2031 2030 args_json=args_json
2032 2031 )
2033 2032
2034 2033
2035 2034 def api_call_example(method, args):
2036 2035 """
2037 2036 Generates an API call example via CURL
2038 2037 """
2039 2038 curl_call = curl_api_example(method, args)
2040 2039
2041 2040 return literal(
2042 2041 curl_call +
2043 2042 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2044 2043 "and needs to be of `api calls` role."
2045 2044 .format(token_url=route_url('my_account_auth_tokens')))
2046 2045
2047 2046
2048 2047 def notification_description(notification, request):
2049 2048 """
2050 2049 Generate notification human readable description based on notification type
2051 2050 """
2052 2051 from rhodecode.model.notification import NotificationModel
2053 2052 return NotificationModel().make_description(
2054 2053 notification, translate=request.translate)
2055 2054
2056 2055
2057 2056 def go_import_header(request, db_repo=None):
2058 2057 """
2059 2058 Creates a header for go-import functionality in Go Lang
2060 2059 """
2061 2060
2062 2061 if not db_repo:
2063 2062 return
2064 2063 if 'go-get' not in request.GET:
2065 2064 return
2066 2065
2067 2066 clone_url = db_repo.clone_url()
2068 2067 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2069 2068 # we have a repo and go-get flag,
2070 2069 return literal('<meta name="go-import" content="{} {} {}">'.format(
2071 2070 prefix, db_repo.repo_type, clone_url))
2072 2071
2073 2072
2074 2073 def reviewer_as_json(*args, **kwargs):
2075 2074 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2076 2075 return _reviewer_as_json(*args, **kwargs)
2077 2076
2078 2077
2079 2078 def get_repo_view_type(request):
2080 2079 route_name = request.matched_route.name
2081 2080 route_to_view_type = {
2082 2081 'repo_changelog': 'commits',
2083 2082 'repo_commits': 'commits',
2084 2083 'repo_files': 'files',
2085 2084 'repo_summary': 'summary',
2086 2085 'repo_commit': 'commit'
2087 2086 }
2088 2087
2089 2088 return route_to_view_type.get(route_name)
@@ -1,385 +1,385 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('favicon', '/favicon.ico', []);
16 16 pyroutes.register('robots', '/robots.txt', []);
17 17 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
18 18 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
19 19 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
20 20 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
21 21 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
22 22 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
23 23 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
24 24 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
25 25 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
26 26 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
27 27 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
28 28 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
29 29 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
30 30 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
31 31 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
32 32 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
33 33 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
34 34 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
35 pyroutes.register('hovercard_commit', '/_hovercard/commit/%(repo_name)s/%(user_id)s', ['repo_name', 'user_id']);
35 pyroutes.register('hovercard_repo_commit', '/_hovercard/commit/%(repo_name)s/%(commit_id)s', ['repo_name', 'commit_id']);
36 36 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
37 37 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
38 38 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
39 39 pyroutes.register('ops_ping_legacy', '/_admin/ping', []);
40 40 pyroutes.register('ops_error_test_legacy', '/_admin/error_test', []);
41 41 pyroutes.register('admin_home', '/_admin', []);
42 42 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
43 43 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
44 44 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
45 45 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
46 46 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
47 47 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
48 48 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
49 49 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
50 50 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
51 51 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
52 52 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions/delete', []);
53 53 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
54 54 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
55 55 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
56 56 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
57 57 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
58 58 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
59 59 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
60 60 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
61 61 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
62 62 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
63 63 pyroutes.register('admin_settings', '/_admin/settings', []);
64 64 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
65 65 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
66 66 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
67 67 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
68 68 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
69 69 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
70 70 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
71 71 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
72 72 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
73 73 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
74 74 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
75 75 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
76 76 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
77 77 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
78 78 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
79 79 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
80 80 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
81 81 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
82 82 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
83 83 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
84 84 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
85 85 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
86 86 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
87 87 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
88 88 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
89 89 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
90 90 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
91 91 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
92 92 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
93 93 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
94 94 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
95 95 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
96 96 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
97 97 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
98 98 pyroutes.register('users', '/_admin/users', []);
99 99 pyroutes.register('users_data', '/_admin/users_data', []);
100 100 pyroutes.register('users_create', '/_admin/users/create', []);
101 101 pyroutes.register('users_new', '/_admin/users/new', []);
102 102 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
103 103 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
104 104 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
105 105 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
106 106 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
107 107 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
108 108 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
109 109 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
110 110 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
111 111 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
112 112 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
113 113 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
114 114 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
115 115 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
116 116 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
117 117 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
118 118 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
119 119 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
120 120 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
121 121 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
122 122 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
123 123 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
124 124 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
125 125 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
126 126 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
127 127 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
128 128 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
129 129 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
130 130 pyroutes.register('user_groups', '/_admin/user_groups', []);
131 131 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
132 132 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
133 133 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
134 134 pyroutes.register('repos', '/_admin/repos', []);
135 135 pyroutes.register('repo_new', '/_admin/repos/new', []);
136 136 pyroutes.register('repo_create', '/_admin/repos/create', []);
137 137 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
138 138 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
139 139 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
140 140 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
141 141 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
142 142 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
143 143 pyroutes.register('channelstream_proxy', '/_channelstream', []);
144 144 pyroutes.register('upload_file', '/_file_store/upload', []);
145 145 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
146 146 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
147 147 pyroutes.register('logout', '/_admin/logout', []);
148 148 pyroutes.register('reset_password', '/_admin/password_reset', []);
149 149 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
150 150 pyroutes.register('home', '/', []);
151 151 pyroutes.register('user_autocomplete_data', '/_users', []);
152 152 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
153 153 pyroutes.register('repo_list_data', '/_repos', []);
154 154 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
155 155 pyroutes.register('goto_switcher_data', '/_goto_data', []);
156 156 pyroutes.register('markup_preview', '/_markup_preview', []);
157 157 pyroutes.register('file_preview', '/_file_preview', []);
158 158 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
159 159 pyroutes.register('journal', '/_admin/journal', []);
160 160 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
161 161 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
162 162 pyroutes.register('journal_public', '/_admin/public_journal', []);
163 163 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
164 164 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
165 165 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
166 166 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
167 167 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
168 168 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
169 169 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
170 170 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
171 171 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
172 172 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
173 173 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
174 174 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
175 175 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
176 176 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
177 177 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
178 178 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
179 179 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
180 180 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
181 181 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
182 182 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
183 183 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
184 184 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
185 185 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
186 186 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
187 187 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
188 188 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
189 189 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
190 190 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
191 191 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
192 192 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
193 193 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
194 194 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
195 195 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
196 196 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
197 197 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
198 198 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
199 199 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
200 200 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
201 201 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
202 202 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
203 203 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
204 204 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
205 205 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
206 206 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
207 207 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
208 208 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
209 209 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
210 210 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
211 211 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
212 212 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
213 213 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
214 214 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
215 215 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
216 216 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
217 217 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
218 218 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
219 219 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
220 220 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
221 221 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
222 222 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
223 223 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
224 224 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
225 225 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
226 226 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
227 227 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
228 228 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
229 229 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
230 230 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
231 231 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
232 232 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
233 233 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
234 234 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
235 235 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
236 236 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
237 237 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
238 238 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
239 239 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
240 240 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
241 241 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
242 242 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
243 243 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
244 244 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
245 245 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
246 246 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
247 247 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
248 248 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
249 249 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
250 250 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
251 251 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
252 252 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
253 253 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
254 254 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
255 255 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
256 256 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
257 257 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
258 258 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
259 259 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
260 260 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
261 261 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
262 262 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
263 263 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
264 264 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
265 265 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
266 266 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
267 267 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
268 268 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
269 269 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
270 270 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
271 271 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
272 272 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
273 273 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
274 274 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
275 275 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
276 276 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
277 277 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
278 278 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
279 279 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
280 280 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
281 281 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
282 282 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
283 283 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
284 284 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
285 285 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
286 286 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
287 287 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
288 288 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
289 289 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
290 290 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
291 291 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
292 292 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
293 293 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
294 294 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
295 295 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
296 296 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
297 297 pyroutes.register('search', '/_admin/search', []);
298 298 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
299 299 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
300 300 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
301 301 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
302 302 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
303 303 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
304 304 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
305 305 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
306 306 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
307 307 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
308 308 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
309 309 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
310 310 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
311 311 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
312 312 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
313 313 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
314 314 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
315 315 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
316 316 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
317 317 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
318 318 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
319 319 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
320 320 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
321 321 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
322 322 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
323 323 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
324 324 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
325 325 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
326 326 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
327 327 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
328 328 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
329 329 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
330 330 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
331 331 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
332 332 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
333 333 pyroutes.register('gists_show', '/_admin/gists', []);
334 334 pyroutes.register('gists_new', '/_admin/gists/new', []);
335 335 pyroutes.register('gists_create', '/_admin/gists/create', []);
336 336 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
337 337 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
338 338 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
339 339 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
340 340 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
341 341 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
342 342 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
343 343 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
344 344 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
345 345 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
346 346 pyroutes.register('apiv2', '/_admin/api', []);
347 347 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
348 348 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
349 349 pyroutes.register('login', '/_admin/login', []);
350 350 pyroutes.register('register', '/_admin/register', []);
351 351 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
352 352 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
353 353 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
354 354 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
355 355 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
356 356 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
357 357 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
358 358 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
359 359 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
360 360 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
361 361 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
362 362 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
363 363 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
364 364 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
365 365 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
366 366 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
367 367 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
368 368 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
369 369 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
370 370 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
371 371 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
372 372 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
373 373 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
374 374 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
375 375 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
376 376 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
377 377 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
378 378 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
379 379 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
380 380 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
381 381 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
382 382 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
383 383 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
384 384 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
385 385 }
@@ -1,661 +1,663 b''
1 1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 RhodeCode JS Files
21 21 **/
22 22
23 23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 24 console = { log: function() {} }
25 25 }
26 26
27 27 // TODO: move the following function to submodules
28 28
29 29 /**
30 30 * show more
31 31 */
32 32 var show_more_event = function(){
33 33 $('table .show_more').click(function(e) {
34 34 var cid = e.target.id.substring(1);
35 35 var button = $(this);
36 36 if (button.hasClass('open')) {
37 37 $('#'+cid).hide();
38 38 button.removeClass('open');
39 39 } else {
40 40 $('#'+cid).show();
41 41 button.addClass('open one');
42 42 }
43 43 });
44 44 };
45 45
46 46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 47 $('#compare_action').on('click', function(e){
48 48 e.preventDefault();
49 49
50 50 var source = $('input[name=compare_source]:checked').val();
51 51 var target = $('input[name=compare_target]:checked').val();
52 52 if(source && target){
53 53 var url_data = {
54 54 repo_name: repo_name,
55 55 source_ref: source,
56 56 source_ref_type: compare_ref_type,
57 57 target_ref: target,
58 58 target_ref_type: compare_ref_type,
59 59 merge: 1
60 60 };
61 61 window.location = pyroutes.url('repo_compare', url_data);
62 62 }
63 63 });
64 64 $('.compare-radio-button').on('click', function(e){
65 65 var source = $('input[name=compare_source]:checked').val();
66 66 var target = $('input[name=compare_target]:checked').val();
67 67 if(source && target){
68 68 $('#compare_action').removeAttr("disabled");
69 69 $('#compare_action').removeClass("disabled");
70 70 }
71 71 })
72 72 };
73 73
74 74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 75 var container = $('#' + target);
76 76 var url = pyroutes.url('repo_stats',
77 77 {"repo_name": repo_name, "commit_id": commit_id});
78 78
79 79 container.show();
80 80 if (!container.hasClass('loaded')) {
81 81 $.ajax({url: url})
82 82 .complete(function (data) {
83 83 var responseJSON = data.responseJSON;
84 84 container.addClass('loaded');
85 85 container.html(responseJSON.size);
86 86 callback(responseJSON.code_stats)
87 87 })
88 88 .fail(function (data) {
89 89 console.log('failed to load repo stats');
90 90 });
91 91 }
92 92
93 93 };
94 94
95 95 var showRepoStats = function(target, data){
96 96 var container = $('#' + target);
97 97
98 98 if (container.hasClass('loaded')) {
99 99 return
100 100 }
101 101
102 102 var total = 0;
103 103 var no_data = true;
104 104 var tbl = document.createElement('table');
105 105 tbl.setAttribute('class', 'trending_language_tbl rctable');
106 106
107 107 $.each(data, function(key, val){
108 108 total += val.count;
109 109 });
110 110
111 111 var sortedStats = [];
112 112 for (var obj in data){
113 113 sortedStats.push([obj, data[obj]])
114 114 }
115 115 var sortedData = sortedStats.sort(function (a, b) {
116 116 return b[1].count - a[1].count
117 117 });
118 118 var cnt = 0;
119 119 $.each(sortedData, function(idx, val){
120 120 cnt += 1;
121 121 no_data = false;
122 122
123 123 var tr = document.createElement('tr');
124 124
125 125 var key = val[0];
126 126 var obj = {"desc": val[1].desc, "count": val[1].count};
127 127
128 128 // meta language names
129 129 var td1 = document.createElement('td');
130 130 var trending_language_label = document.createElement('div');
131 131 trending_language_label.innerHTML = obj.desc;
132 132 td1.appendChild(trending_language_label);
133 133
134 134 // extensions
135 135 var td2 = document.createElement('td');
136 136 var extension = document.createElement('div');
137 137 extension.innerHTML = ".{0}".format(key)
138 138 td2.appendChild(extension);
139 139
140 140 // number of files
141 141 var td3 = document.createElement('td');
142 142 var file_count = document.createElement('div');
143 143 var percentage_num = Math.round((obj.count / total * 100), 2);
144 144 var label = _ngettext('file', 'files', obj.count);
145 145 file_count.innerHTML = "{0} {1} ({2}%)".format(obj.count, label, percentage_num) ;
146 146 td3.appendChild(file_count);
147 147
148 148 // percentage
149 149 var td4 = document.createElement('td');
150 150 td4.setAttribute("class", 'trending_language');
151 151
152 152 var percentage = document.createElement('div');
153 153 percentage.setAttribute('class', 'lang-bar');
154 154 percentage.innerHTML = "&nbsp;";
155 155 percentage.style.width = percentage_num + '%';
156 156 td4.appendChild(percentage);
157 157
158 158 tr.appendChild(td1);
159 159 tr.appendChild(td2);
160 160 tr.appendChild(td3);
161 161 tr.appendChild(td4);
162 162 tbl.appendChild(tr);
163 163
164 164 });
165 165
166 166 $(container).html(tbl);
167 167 $(container).addClass('loaded');
168 168
169 169 $('#code_stats_show_more').on('click', function (e) {
170 170 e.preventDefault();
171 171 $('.stats_hidden').each(function (idx) {
172 172 $(this).css("display", "");
173 173 });
174 174 $('#code_stats_show_more').hide();
175 175 });
176 176
177 177 };
178 178
179 179 // returns a node from given html;
180 180 var fromHTML = function(html){
181 181 var _html = document.createElement('element');
182 182 _html.innerHTML = html;
183 183 return _html;
184 184 };
185 185
186 186 // Toggle Collapsable Content
187 187 function collapsableContent() {
188 188
189 189 $('.collapsable-content').not('.no-hide').hide();
190 190
191 191 $('.btn-collapse').unbind(); //in case we've been here before
192 192 $('.btn-collapse').click(function() {
193 193 var button = $(this);
194 194 var togglename = $(this).data("toggle");
195 195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
196 196 if ($(this).html()=="Show Less")
197 197 $(this).html("Show More");
198 198 else
199 199 $(this).html("Show Less");
200 200 });
201 201 };
202 202
203 203 var timeagoActivate = function() {
204 204 $("time.timeago").timeago();
205 205 };
206 206
207 207
208 208 var clipboardActivate = function() {
209 209 /*
210 210 *
211 211 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
212 212 * */
213 213 var clipboard = new ClipboardJS('.clipboard-action');
214 214
215 215 clipboard.on('success', function(e) {
216 216 var callback = function () {
217 217 $(e.trigger).animate({'opacity': 1.00}, 200)
218 218 };
219 219 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
220 220 e.clearSelection();
221 221 });
222 222 };
223 223
224 224 var tooltipActivate = function () {
225 225 var delay = 50;
226 226 var animation = 'fade';
227 227 var theme = 'tooltipster-shadow';
228 228 var debug = false;
229 229
230 230 $('.tooltip').tooltipster({
231 231 debug: debug,
232 232 theme: theme,
233 233 animation: animation,
234 234 delay: delay,
235 235 contentCloning: true,
236 236 contentAsHTML: true,
237 237
238 238 functionBefore: function (instance, helper) {
239 239 var $origin = $(helper.origin);
240 240 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(instance.content());
241 241 instance.content(data);
242 242 }
243 243 });
244 244 var hovercardCache = {};
245 245
246 246 var loadHoverCard = function (url, callback) {
247 247 var id = url;
248 248
249 249 if (hovercardCache[id] !== undefined) {
250 250 callback(hovercardCache[id]);
251 return;
251 return true;
252 252 }
253 253
254 254 hovercardCache[id] = undefined;
255 255 $.get(url, function (data) {
256 256 hovercardCache[id] = data;
257 257 callback(hovercardCache[id]);
258 return true;
258 259 }).fail(function (data, textStatus, errorThrown) {
259 var msg = "Error while fetching hovercard.\nError code {0} ({1}).".format(data.status,data.statusText);
260 var msg = "<p class='error-message'>Error while fetching hovercard.\nError code {0} ({1}).</p>".format(data.status,data.statusText);
260 261 callback(msg);
262 return false
261 263 });
262 264 };
263 265
264 266 $('.tooltip-hovercard').tooltipster({
265 267 debug: debug,
266 268 theme: theme,
267 269 animation: animation,
268 270 delay: delay,
269 271 interactive: true,
270 272 contentCloning: true,
271 273
272 274 trigger: 'custom',
273 275 triggerOpen: {
274 276 mouseenter: true,
275 277 },
276 278 triggerClose: {
277 279 mouseleave: true,
278 280 originClick: true,
279 281 touchleave: true
280 282 },
281 283 content: _gettext('Loading...'),
282 284 contentAsHTML: true,
283 285 updateAnimation: null,
284 286
285 287 functionBefore: function (instance, helper) {
286 288
287 289 var $origin = $(helper.origin);
288 290
289 291 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
290 292 if ($origin.data('loaded') !== true) {
291 293 var hovercardUrl = $origin.data('hovercardUrl');
292 294
293 295 if (hovercardUrl !== undefined && hovercardUrl !== "") {
294 loadHoverCard(hovercardUrl, function (data) {
296 var loaded = loadHoverCard(hovercardUrl, function (data) {
295 297 instance.content(data);
296 298 })
297 299 } else {
298 300 if ($origin.data('hovercardAltHtml')) {
299 301 var data = atob($origin.data('hovercardAltHtml'));
300 302 } else {
301 303 var data = '<div style="white-space: pre-wrap">{0}</div>'.format($origin.data('hovercardAlt'))
302 304 }
303
305 var loaded = true;
304 306 instance.content(data);
305 307 }
306 308
307 309 // to remember that the data has been loaded
308 $origin.data('loaded', true);
310 $origin.data('loaded', loaded);
309 311 }
310 312 }
311 313 })
312 314 };
313 315
314 316 // Formatting values in a Select2 dropdown of commit references
315 317 var formatSelect2SelectionRefs = function(commit_ref){
316 318 var tmpl = '';
317 319 if (!commit_ref.text || commit_ref.type === 'sha'){
318 320 return commit_ref.text;
319 321 }
320 322 if (commit_ref.type === 'branch'){
321 323 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
322 324 } else if (commit_ref.type === 'tag'){
323 325 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
324 326 } else if (commit_ref.type === 'book'){
325 327 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
326 328 }
327 329 return tmpl.concat(escapeHtml(commit_ref.text));
328 330 };
329 331
330 332 // takes a given html element and scrolls it down offset pixels
331 333 function offsetScroll(element, offset) {
332 334 setTimeout(function() {
333 335 var location = element.offset().top;
334 336 // some browsers use body, some use html
335 337 $('html, body').animate({ scrollTop: (location - offset) });
336 338 }, 100);
337 339 }
338 340
339 341 // scroll an element `percent`% from the top of page in `time` ms
340 342 function scrollToElement(element, percent, time) {
341 343 percent = (percent === undefined ? 25 : percent);
342 344 time = (time === undefined ? 100 : time);
343 345
344 346 var $element = $(element);
345 347 if ($element.length == 0) {
346 348 throw('Cannot scroll to {0}'.format(element))
347 349 }
348 350 var elOffset = $element.offset().top;
349 351 var elHeight = $element.height();
350 352 var windowHeight = $(window).height();
351 353 var offset = elOffset;
352 354 if (elHeight < windowHeight) {
353 355 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
354 356 }
355 357 setTimeout(function() {
356 358 $('html, body').animate({ scrollTop: offset});
357 359 }, time);
358 360 }
359 361
360 362 /**
361 363 * global hooks after DOM is loaded
362 364 */
363 365 $(document).ready(function() {
364 366 firefoxAnchorFix();
365 367
366 368 $('.navigation a.menulink').on('click', function(e){
367 369 var menuitem = $(this).parent('li');
368 370 if (menuitem.hasClass('open')) {
369 371 menuitem.removeClass('open');
370 372 } else {
371 373 menuitem.addClass('open');
372 374 $(document).on('click', function(event) {
373 375 if (!$(event.target).closest(menuitem).length) {
374 376 menuitem.removeClass('open');
375 377 }
376 378 });
377 379 }
378 380 });
379 381
380 382 $('body').on('click', '.cb-lineno a', function(event) {
381 383 function sortNumber(a,b) {
382 384 return a - b;
383 385 }
384 386
385 387 var lineNo = $(this).data('lineNo');
386 388 var lineName = $(this).attr('name');
387 389
388 390 if (lineNo) {
389 391 var prevLine = $('.cb-line-selected a').data('lineNo');
390 392
391 393 // on shift, we do a range selection, if we got previous line
392 394 if (event.shiftKey && prevLine !== undefined) {
393 395 var prevLine = parseInt(prevLine);
394 396 var nextLine = parseInt(lineNo);
395 397 var pos = [prevLine, nextLine].sort(sortNumber);
396 398 var anchor = '#L{0}-{1}'.format(pos[0], pos[1]);
397 399
398 400 // single click
399 401 } else {
400 402 var nextLine = parseInt(lineNo);
401 403 var pos = [nextLine, nextLine];
402 404 var anchor = '#L{0}'.format(pos[0]);
403 405
404 406 }
405 407 // highlight
406 408 var range = [];
407 409 for (var i = pos[0]; i <= pos[1]; i++) {
408 410 range.push(i);
409 411 }
410 412 // clear old selected lines
411 413 $('.cb-line-selected').removeClass('cb-line-selected');
412 414
413 415 $.each(range, function (i, lineNo) {
414 416 var line_td = $('td.cb-lineno#L' + lineNo);
415 417
416 418 if (line_td.length) {
417 419 line_td.addClass('cb-line-selected'); // line number td
418 420 line_td.prev().addClass('cb-line-selected'); // line data
419 421 line_td.next().addClass('cb-line-selected'); // line content
420 422 }
421 423 });
422 424
423 425 } else if (lineName !== undefined) { // lineName only occurs in diffs
424 426 // clear old selected lines
425 427 $('td.cb-line-selected').removeClass('cb-line-selected');
426 428 var anchor = '#{0}'.format(lineName);
427 429 var diffmode = templateContext.session_attrs.diffmode || "sideside";
428 430
429 431 if (diffmode === "unified") {
430 432 $(this).closest('tr').find('td').addClass('cb-line-selected');
431 433 } else {
432 434 var activeTd = $(this).closest('td');
433 435 activeTd.addClass('cb-line-selected');
434 436 activeTd.next('td').addClass('cb-line-selected');
435 437 }
436 438
437 439 }
438 440
439 441 // Replace URL without jumping to it if browser supports.
440 442 // Default otherwise
441 443 if (history.pushState && anchor !== undefined) {
442 444 var new_location = location.href.rstrip('#');
443 445 if (location.hash) {
444 446 // location without hash
445 447 new_location = new_location.replace(location.hash, "");
446 448 }
447 449
448 450 // Make new anchor url
449 451 new_location = new_location + anchor;
450 452 history.pushState(true, document.title, new_location);
451 453
452 454 return false;
453 455 }
454 456
455 457 });
456 458
457 459 $('.collapse_file').on('click', function(e) {
458 460 e.stopPropagation();
459 461 if ($(e.target).is('a')) { return; }
460 462 var node = $(e.delegateTarget).first();
461 463 var icon = $($(node.children().first()).children().first());
462 464 var id = node.attr('fid');
463 465 var target = $('#'+id);
464 466 var tr = $('#tr_'+id);
465 467 var diff = $('#diff_'+id);
466 468 if(node.hasClass('expand_file')){
467 469 node.removeClass('expand_file');
468 470 icon.removeClass('expand_file_icon');
469 471 node.addClass('collapse_file');
470 472 icon.addClass('collapse_file_icon');
471 473 diff.show();
472 474 tr.show();
473 475 target.show();
474 476 } else {
475 477 node.removeClass('collapse_file');
476 478 icon.removeClass('collapse_file_icon');
477 479 node.addClass('expand_file');
478 480 icon.addClass('expand_file_icon');
479 481 diff.hide();
480 482 tr.hide();
481 483 target.hide();
482 484 }
483 485 });
484 486
485 487 $('#expand_all_files').click(function() {
486 488 $('.expand_file').each(function() {
487 489 var node = $(this);
488 490 var icon = $($(node.children().first()).children().first());
489 491 var id = $(this).attr('fid');
490 492 var target = $('#'+id);
491 493 var tr = $('#tr_'+id);
492 494 var diff = $('#diff_'+id);
493 495 node.removeClass('expand_file');
494 496 icon.removeClass('expand_file_icon');
495 497 node.addClass('collapse_file');
496 498 icon.addClass('collapse_file_icon');
497 499 diff.show();
498 500 tr.show();
499 501 target.show();
500 502 });
501 503 });
502 504
503 505 $('#collapse_all_files').click(function() {
504 506 $('.collapse_file').each(function() {
505 507 var node = $(this);
506 508 var icon = $($(node.children().first()).children().first());
507 509 var id = $(this).attr('fid');
508 510 var target = $('#'+id);
509 511 var tr = $('#tr_'+id);
510 512 var diff = $('#diff_'+id);
511 513 node.removeClass('collapse_file');
512 514 icon.removeClass('collapse_file_icon');
513 515 node.addClass('expand_file');
514 516 icon.addClass('expand_file_icon');
515 517 diff.hide();
516 518 tr.hide();
517 519 target.hide();
518 520 });
519 521 });
520 522
521 523 // Mouse over behavior for comments and line selection
522 524
523 525 // Select the line that comes from the url anchor
524 526 // At the time of development, Chrome didn't seem to support jquery's :target
525 527 // element, so I had to scroll manually
526 528
527 529 if (location.hash) {
528 530 var result = splitDelimitedHash(location.hash);
529 531 var loc = result.loc;
530 532 if (loc.length > 1) {
531 533
532 534 var highlightable_line_tds = [];
533 535
534 536 // source code line format
535 537 var page_highlights = loc.substring(
536 538 loc.indexOf('#') + 1).split('L');
537 539
538 540 if (page_highlights.length > 1) {
539 541 var highlight_ranges = page_highlights[1].split(",");
540 542 var h_lines = [];
541 543 for (var pos in highlight_ranges) {
542 544 var _range = highlight_ranges[pos].split('-');
543 545 if (_range.length === 2) {
544 546 var start = parseInt(_range[0]);
545 547 var end = parseInt(_range[1]);
546 548 if (start < end) {
547 549 for (var i = start; i <= end; i++) {
548 550 h_lines.push(i);
549 551 }
550 552 }
551 553 }
552 554 else {
553 555 h_lines.push(parseInt(highlight_ranges[pos]));
554 556 }
555 557 }
556 558 for (pos in h_lines) {
557 559 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
558 560 if (line_td.length) {
559 561 highlightable_line_tds.push(line_td);
560 562 }
561 563 }
562 564 }
563 565
564 566 // now check a direct id reference (diff page)
565 567 if ($(loc).length && $(loc).hasClass('cb-lineno')) {
566 568 highlightable_line_tds.push($(loc));
567 569 }
568 570 $.each(highlightable_line_tds, function (i, $td) {
569 571 $td.addClass('cb-line-selected'); // line number td
570 572 $td.prev().addClass('cb-line-selected'); // line data
571 573 $td.next().addClass('cb-line-selected'); // line content
572 574 });
573 575
574 576 if (highlightable_line_tds.length) {
575 577 var $first_line_td = highlightable_line_tds[0];
576 578 scrollToElement($first_line_td);
577 579 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
578 580 td: $first_line_td,
579 581 remainder: result.remainder
580 582 });
581 583 }
582 584 }
583 585 }
584 586 collapsableContent();
585 587 });
586 588
587 589 var feedLifetimeOptions = function(query, initialData){
588 590 var data = {results: []};
589 591 var isQuery = typeof query.term !== 'undefined';
590 592
591 593 var section = _gettext('Lifetime');
592 594 var children = [];
593 595
594 596 //filter results
595 597 $.each(initialData.results, function(idx, value) {
596 598
597 599 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
598 600 children.push({
599 601 'id': this.id,
600 602 'text': this.text
601 603 })
602 604 }
603 605
604 606 });
605 607 data.results.push({
606 608 'text': section,
607 609 'children': children
608 610 });
609 611
610 612 if (isQuery) {
611 613
612 614 var now = moment.utc();
613 615
614 616 var parseQuery = function(entry, now){
615 617 var fmt = 'DD/MM/YYYY H:mm';
616 618 var parsed = moment.utc(entry, fmt);
617 619 var diffInMin = parsed.diff(now, 'minutes');
618 620
619 621 if (diffInMin > 0){
620 622 return {
621 623 id: diffInMin,
622 624 text: parsed.format(fmt)
623 625 }
624 626 } else {
625 627 return {
626 628 id: undefined,
627 629 text: parsed.format('DD/MM/YYYY') + ' ' + _gettext('date not in future')
628 630 }
629 631 }
630 632
631 633
632 634 };
633 635
634 636 data.results.push({
635 637 'text': _gettext('Specified expiration date'),
636 638 'children': [{
637 639 'id': parseQuery(query.term, now).id,
638 640 'text': parseQuery(query.term, now).text
639 641 }]
640 642 });
641 643 }
642 644
643 645 query.callback(data);
644 646 };
645 647
646 648
647 649 var storeUserSessionAttr = function (key, val) {
648 650
649 651 var postData = {
650 652 'key': key,
651 653 'val': val,
652 654 'csrf_token': CSRF_TOKEN
653 655 };
654 656
655 657 var success = function(o) {
656 658 return true
657 659 };
658 660
659 661 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
660 662 return false;
661 663 };
@@ -1,469 +1,469 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5
6 6 <%def name="metatags_help()">
7 7 <table>
8 8 <%
9 9 example_tags = [
10 10 ('state','[stable]'),
11 11 ('state','[stale]'),
12 12 ('state','[featured]'),
13 13 ('state','[dev]'),
14 14 ('state','[dead]'),
15 15 ('state','[deprecated]'),
16 16
17 17 ('label','[personal]'),
18 18 ('generic','[v2.0.0]'),
19 19
20 20 ('lang','[lang =&gt; JavaScript]'),
21 21 ('license','[license =&gt; LicenseName]'),
22 22
23 23 ('ref','[requires =&gt; RepoName]'),
24 24 ('ref','[recommends =&gt; GroupName]'),
25 25 ('ref','[conflicts =&gt; SomeName]'),
26 26 ('ref','[base =&gt; SomeName]'),
27 27 ('url','[url =&gt; [linkName](https://rhodecode.com)]'),
28 28 ('see','[see =&gt; http://rhodecode.com]'),
29 29 ]
30 30 %>
31 31 % for tag_type, tag in example_tags:
32 32 <tr>
33 33 <td>${tag|n}</td>
34 34 <td>${h.style_metatag(tag_type, tag)|n}</td>
35 35 </tr>
36 36 % endfor
37 37 </table>
38 38 </%def>
39 39
40 40 <%def name="render_description(description, stylify_metatags)">
41 41 <%
42 42 tags = []
43 43 if stylify_metatags:
44 44 tags, description = h.extract_metatags(description)
45 45 %>
46 46 % for tag_type, tag in tags:
47 47 ${h.style_metatag(tag_type, tag)|n,trim}
48 48 % endfor
49 49 <code style="white-space: pre-wrap">${description}</code>
50 50 </%def>
51 51
52 52 ## REPOSITORY RENDERERS
53 53 <%def name="quick_menu(repo_name)">
54 54 <i class="icon-more"></i>
55 55 <div class="menu_items_container hidden">
56 56 <ul class="menu_items">
57 57 <li>
58 58 <a title="${_('Summary')}" href="${h.route_path('repo_summary',repo_name=repo_name)}">
59 59 <span>${_('Summary')}</span>
60 60 </a>
61 61 </li>
62 62 <li>
63 63 <a title="${_('Commits')}" href="${h.route_path('repo_commits',repo_name=repo_name)}">
64 64 <span>${_('Commits')}</span>
65 65 </a>
66 66 </li>
67 67 <li>
68 68 <a title="${_('Files')}" href="${h.route_path('repo_files:default_commit',repo_name=repo_name)}">
69 69 <span>${_('Files')}</span>
70 70 </a>
71 71 </li>
72 72 <li>
73 73 <a title="${_('Fork')}" href="${h.route_path('repo_fork_new',repo_name=repo_name)}">
74 74 <span>${_('Fork')}</span>
75 75 </a>
76 76 </li>
77 77 </ul>
78 78 </div>
79 79 </%def>
80 80
81 81 <%def name="repo_name(name,rtype,rstate,private,archived,fork_of,short_name=False,admin=False)">
82 82 <%
83 83 def get_name(name,short_name=short_name):
84 84 if short_name:
85 85 return name.split('/')[-1]
86 86 else:
87 87 return name
88 88 %>
89 89 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
90 90 ##NAME
91 91 <a href="${h.route_path('edit_repo',repo_name=name) if admin else h.route_path('repo_summary',repo_name=name)}">
92 92
93 93 ##TYPE OF REPO
94 94 %if h.is_hg(rtype):
95 95 <span title="${_('Mercurial repository')}"><i class="icon-hg" style="font-size: 14px;"></i></span>
96 96 %elif h.is_git(rtype):
97 97 <span title="${_('Git repository')}"><i class="icon-git" style="font-size: 14px"></i></span>
98 98 %elif h.is_svn(rtype):
99 99 <span title="${_('Subversion repository')}"><i class="icon-svn" style="font-size: 14px"></i></span>
100 100 %endif
101 101
102 102 ##PRIVATE/PUBLIC
103 103 %if private is True and c.visual.show_private_icon:
104 104 <i class="icon-lock" title="${_('Private repository')}"></i>
105 105 %elif private is False and c.visual.show_public_icon:
106 106 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
107 107 %else:
108 108 <span></span>
109 109 %endif
110 110 ${get_name(name)}
111 111 </a>
112 112 %if fork_of:
113 113 <a href="${h.route_path('repo_summary',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
114 114 %endif
115 115 %if rstate == 'repo_state_pending':
116 116 <span class="creation_in_progress tooltip" title="${_('This repository is being created in a background task')}">
117 117 (${_('creating...')})
118 118 </span>
119 119 %endif
120 120
121 121 </div>
122 122 </%def>
123 123
124 124 <%def name="repo_desc(description, stylify_metatags)">
125 125 <%
126 126 tags, description = h.extract_metatags(description)
127 127 %>
128 128
129 129 <div class="truncate-wrap">
130 130 % if stylify_metatags:
131 131 % for tag_type, tag in tags:
132 132 ${h.style_metatag(tag_type, tag)|n}
133 133 % endfor
134 134 % endif
135 135 ${description}
136 136 </div>
137 137
138 138 </%def>
139 139
140 140 <%def name="last_change(last_change)">
141 141 ${h.age_component(last_change, time_is_local=True)}
142 142 </%def>
143 143
144 <%def name="revision(name,rev,tip,author,last_msg, commit_date)">
144 <%def name="revision(repo_name, rev, commit_id, author, last_msg, commit_date)">
145 145 <div>
146 146 %if rev >= 0:
147 <code><a title="${h.tooltip('%s\n%s\n\n%s' % (author, commit_date, last_msg))}" class="tooltip" href="${h.route_path('repo_commit',repo_name=name,commit_id=tip)}">${'r%s:%s' % (rev,h.short_id(tip))}</a></code>
147 <code><a class="tooltip-hovercard" data-hovercard-alt="${last_msg}" data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)}" href="${h.route_path('repo_commit',repo_name=repo_name,commit_id=commit_id)}">${'r{}:{}'.format(rev,h.short_id(commit_id))}</a></code>
148 148 %else:
149 149 ${_('No commits yet')}
150 150 %endif
151 151 </div>
152 152 </%def>
153 153
154 154 <%def name="rss(name)">
155 155 %if c.rhodecode_user.username != h.DEFAULT_USER:
156 156 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
157 157 %else:
158 158 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
159 159 %endif
160 160 </%def>
161 161
162 162 <%def name="atom(name)">
163 163 %if c.rhodecode_user.username != h.DEFAULT_USER:
164 164 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
165 165 %else:
166 166 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
167 167 %endif
168 168 </%def>
169 169
170 170 <%def name="repo_actions(repo_name, super_user=True)">
171 171 <div>
172 172 <div class="grid_edit">
173 173 <a href="${h.route_path('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
174 174 Edit
175 175 </a>
176 176 </div>
177 177 <div class="grid_delete">
178 178 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), request=request)}
179 179 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
180 180 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
181 181 ${h.end_form()}
182 182 </div>
183 183 </div>
184 184 </%def>
185 185
186 186 <%def name="repo_state(repo_state)">
187 187 <div>
188 188 %if repo_state == 'repo_state_pending':
189 189 <div class="tag tag4">${_('Creating')}</div>
190 190 %elif repo_state == 'repo_state_created':
191 191 <div class="tag tag1">${_('Created')}</div>
192 192 %else:
193 193 <div class="tag alert2" title="${h.tooltip(repo_state)}">invalid</div>
194 194 %endif
195 195 </div>
196 196 </%def>
197 197
198 198
199 199 ## REPO GROUP RENDERERS
200 200 <%def name="quick_repo_group_menu(repo_group_name)">
201 201 <i class="icon-more"></i>
202 202 <div class="menu_items_container hidden">
203 203 <ul class="menu_items">
204 204 <li>
205 205 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">${_('Summary')}</a>
206 206 </li>
207 207
208 208 </ul>
209 209 </div>
210 210 </%def>
211 211
212 212 <%def name="repo_group_name(repo_group_name, children_groups=None)">
213 213 <div>
214 214 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
215 215 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
216 216 %if children_groups:
217 217 ${h.literal(' &raquo; '.join(children_groups))}
218 218 %else:
219 219 ${repo_group_name}
220 220 %endif
221 221 </a>
222 222 </div>
223 223 </%def>
224 224
225 225 <%def name="repo_group_desc(description, personal, stylify_metatags)">
226 226
227 227 <%
228 228 if stylify_metatags:
229 229 tags, description = h.extract_metatags(description)
230 230 %>
231 231
232 232 <div class="truncate-wrap">
233 233 % if personal:
234 234 <div class="metatag" tag="personal">${_('personal')}</div>
235 235 % endif
236 236
237 237 % if stylify_metatags:
238 238 % for tag_type, tag in tags:
239 239 ${h.style_metatag(tag_type, tag)|n}
240 240 % endfor
241 241 % endif
242 242 ${description}
243 243 </div>
244 244
245 245 </%def>
246 246
247 247 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
248 248 <div class="grid_edit">
249 249 <a href="${h.route_path('edit_repo_group',repo_group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
250 250 </div>
251 251 <div class="grid_delete">
252 252 ${h.secure_form(h.route_path('edit_repo_group_advanced_delete', repo_group_name=repo_group_name), request=request)}
253 253 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
254 254 onclick="return confirm('"+_ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
255 255 ${h.end_form()}
256 256 </div>
257 257 </%def>
258 258
259 259
260 260 <%def name="user_actions(user_id, username)">
261 261 <div class="grid_edit">
262 262 <a href="${h.route_path('user_edit',user_id=user_id)}" title="${_('Edit')}">
263 263 ${_('Edit')}
264 264 </a>
265 265 </div>
266 266 <div class="grid_delete">
267 267 ${h.secure_form(h.route_path('user_delete', user_id=user_id), request=request)}
268 268 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
269 269 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
270 270 ${h.end_form()}
271 271 </div>
272 272 </%def>
273 273
274 274 <%def name="user_group_actions(user_group_id, user_group_name)">
275 275 <div class="grid_edit">
276 276 <a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
277 277 </div>
278 278 <div class="grid_delete">
279 279 ${h.secure_form(h.route_path('user_groups_delete', user_group_id=user_group_id), request=request)}
280 280 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
281 281 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
282 282 ${h.end_form()}
283 283 </div>
284 284 </%def>
285 285
286 286
287 287 <%def name="user_name(user_id, username)">
288 288 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.route_path('user_edit', user_id=user_id))}
289 289 </%def>
290 290
291 291 <%def name="user_profile(username)">
292 292 ${base.gravatar_with_user(username, 16, tooltip=True)}
293 293 </%def>
294 294
295 295 <%def name="user_group_name(user_group_name)">
296 296 <div>
297 297 <i class="icon-user-group" title="${_('User group')}"></i>
298 298 ${h.link_to_group(user_group_name)}
299 299 </div>
300 300 </%def>
301 301
302 302
303 303 ## GISTS
304 304
305 305 <%def name="gist_gravatar(full_contact)">
306 306 <div class="gist_gravatar">
307 307 ${base.gravatar(full_contact, 30)}
308 308 </div>
309 309 </%def>
310 310
311 311 <%def name="gist_access_id(gist_access_id, full_contact)">
312 312 <div>
313 313 <b>
314 314 <a href="${h.route_path('gist_show', gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
315 315 </b>
316 316 </div>
317 317 </%def>
318 318
319 319 <%def name="gist_author(full_contact, created_on, expires)">
320 320 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
321 321 </%def>
322 322
323 323
324 324 <%def name="gist_created(created_on)">
325 325 <div class="created">
326 326 ${h.age_component(created_on, time_is_local=True)}
327 327 </div>
328 328 </%def>
329 329
330 330 <%def name="gist_expires(expires)">
331 331 <div class="created">
332 332 %if expires == -1:
333 333 ${_('never')}
334 334 %else:
335 335 ${h.age_component(h.time_to_utcdatetime(expires))}
336 336 %endif
337 337 </div>
338 338 </%def>
339 339
340 340 <%def name="gist_type(gist_type)">
341 341 %if gist_type != 'public':
342 342 <div class="tag">${_('Private')}</div>
343 343 %endif
344 344 </%def>
345 345
346 346 <%def name="gist_description(gist_description)">
347 347 ${gist_description}
348 348 </%def>
349 349
350 350
351 351 ## PULL REQUESTS GRID RENDERERS
352 352
353 353 <%def name="pullrequest_target_repo(repo_name)">
354 354 <div class="truncate">
355 355 ${h.link_to(repo_name,h.route_path('repo_summary',repo_name=repo_name))}
356 356 </div>
357 357 </%def>
358 358
359 359 <%def name="pullrequest_status(status)">
360 360 <i class="icon-circle review-status-${status}"></i>
361 361 </%def>
362 362
363 363 <%def name="pullrequest_title(title, description)">
364 364 ${title}
365 365 </%def>
366 366
367 367 <%def name="pullrequest_comments(comments_nr)">
368 368 <i class="icon-comment"></i> ${comments_nr}
369 369 </%def>
370 370
371 371 <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
372 372 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
373 373 % if short:
374 374 #${pull_request_id}
375 375 % else:
376 376 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
377 377 % endif
378 378 </a>
379 379 </%def>
380 380
381 381 <%def name="pullrequest_updated_on(updated_on)">
382 382 ${h.age_component(h.time_to_utcdatetime(updated_on))}
383 383 </%def>
384 384
385 385 <%def name="pullrequest_author(full_contact)">
386 386 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
387 387 </%def>
388 388
389 389
390 390 ## ARTIFACT RENDERERS
391 391 <%def name="repo_artifact_name(repo_name, file_uid, artifact_display_name)">
392 392 <a href="${h.route_path('repo_artifacts_get', repo_name=repo_name, uid=file_uid)}">
393 393 ${artifact_display_name or '_EMPTY_NAME_'}
394 394 </a>
395 395 </%def>
396 396
397 397 <%def name="repo_artifact_uid(repo_name, file_uid)">
398 398 <code>${h.shorter(file_uid, size=24, prefix=True)}</code>
399 399 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${h.route_url('repo_artifacts_get', repo_name=repo_name, uid=file_uid)}" title="${_('Copy the full url')}"></i>
400 400 </%def>
401 401
402 402 <%def name="repo_artifact_sha256(artifact_sha256)">
403 403 <div class="code">${h.shorter(artifact_sha256, 12)}<i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${artifact_sha256}" title="${_('Copy the sha256 ({})').format(artifact_sha256)}"></i></div>
404 404 </%def>
405 405
406 406 <%def name="repo_artifact_actions(repo_name, file_store_id, file_uid)">
407 407 ## <div class="grid_edit">
408 408 ## <a href="#Edit" title="${_('Edit')}">${_('Edit')}</a>
409 409 ## </div>
410 410 <div class="grid_edit">
411 411 <a href="${h.route_path('repo_artifacts_info', repo_name=repo_name, uid=file_store_id)}" title="${_('Info')}">${_('Info')}</a>
412 412 </div>
413 413 % if h.HasRepoPermissionAny('repository.admin')(c.repo_name):
414 414 <div class="grid_delete">
415 415 ${h.secure_form(h.route_path('repo_artifacts_delete', repo_name=repo_name, uid=file_store_id), request=request)}
416 416 ${h.submit('remove_',_('Delete'),id="remove_artifact_%s" % file_store_id, class_="btn btn-link btn-danger",
417 417 onclick="return confirm('"+_('Confirm to delete this artifact: %s') % file_uid+"');")}
418 418 ${h.end_form()}
419 419 </div>
420 420 % endif
421 421 </%def>
422 422
423 423 <%def name="markup_form(form_id, form_text='', help_text=None)">
424 424
425 425 <div class="markup-form">
426 426 <div class="markup-form-area">
427 427 <div class="markup-form-area-header">
428 428 <ul class="nav-links clearfix">
429 429 <li class="active">
430 430 <a href="#edit-text" tabindex="-1" id="edit-btn_${form_id}">${_('Write')}</a>
431 431 </li>
432 432 <li class="">
433 433 <a href="#preview-text" tabindex="-1" id="preview-btn_${form_id}">${_('Preview')}</a>
434 434 </li>
435 435 </ul>
436 436 </div>
437 437
438 438 <div class="markup-form-area-write" style="display: block;">
439 439 <div id="edit-container_${form_id}">
440 440 <textarea id="${form_id}" name="${form_id}" class="comment-block-ta ac-input">${form_text if form_text else ''}</textarea>
441 441 </div>
442 442 <div id="preview-container_${form_id}" class="clearfix" style="display: none;">
443 443 <div id="preview-box_${form_id}" class="preview-box"></div>
444 444 </div>
445 445 </div>
446 446
447 447 <div class="markup-form-area-footer">
448 448 <div class="toolbar">
449 449 <div class="toolbar-text">
450 450 ${(_('Parsed using %s syntax') % (
451 451 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
452 452 )
453 453 )|n}
454 454 </div>
455 455 </div>
456 456 </div>
457 457 </div>
458 458
459 459 <div class="markup-form-footer">
460 460 % if help_text:
461 461 <span class="help-block">${help_text}</span>
462 462 % endif
463 463 </div>
464 464 </div>
465 465 <script type="text/javascript">
466 466 new MarkupForm('${form_id}');
467 467 </script>
468 468
469 469 </%def>
@@ -1,94 +1,96 b''
1 <%namespace name="base" file="/base/base.mako"/>
2
1 3 <%
2 4 if request.GET.get('at'):
3 5 query={'at': request.GET.get('at')}
4 6 else:
5 7 query=None
6 8 %>
7 9 <div id="file-tree-wrapper" class="browser-body ${('full-load' if c.full_load else '')}">
8 10 <table class="code-browser rctable repo_summary">
9 11 <thead>
10 12 <tr>
11 13 <th>${_('Name')}</th>
12 14 <th>${_('Size')}</th>
13 15 <th>${_('Modified')}</th>
14 16 <th>${_('Last Commit')}</th>
15 17 <th>${_('Author')}</th>
16 18 </tr>
17 19 </thead>
18 20
19 21 <tbody id="tbody">
20 22 <tr>
21 23 <td colspan="5">
22 24 ${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.file.path, request.GET.get('at'), limit_items=True)}
23 25 </td>
24 26 </tr>
25 27
26 28 <% has_files = False %>
27 29 % for cnt,node in enumerate(c.file):
28 30 <% has_files = True %>
29 31 <tr class="parity${(cnt % 2)}">
30 32 <td class="td-componentname">
31 33 % if node.is_submodule():
32 34 <span class="submodule-dir">
33 35 % if node.url.startswith('http://') or node.url.startswith('https://'):
34 36 <a href="${node.url}">
35 37 <i class="icon-directory browser-dir"></i>${node.name}
36 38 </a>
37 39 % else:
38 40 <i class="icon-directory browser-dir"></i>${node.name}
39 41 % endif
40 42 </span>
41 43 % else:
42 44
43 45 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=h.safe_unicode(node.path), _query=query)}">
44 46 <i class="${('icon-file-text browser-file' if node.is_file() else 'icon-directory browser-dir')}"></i>${node.name}
45 47 </a>
46 48 % endif
47 49 </td>
48 50 %if node.is_file():
49 51 <td class="td-size" data-attr-name="size">
50 52 % if c.full_load:
51 53 <span data-size="${node.size}">${h.format_byte_size_binary(node.size)}</span>
52 54 % else:
53 55 ${_('Loading ...')}
54 56 % endif
55 57 </td>
56 58 <td class="td-time" data-attr-name="modified_at">
57 59 % if c.full_load:
58 60 <span data-date="${node.last_commit.date}">${h.age_component(node.last_commit.date)}</span>
59 61 % endif
60 62 </td>
61 63 <td class="td-hash" data-attr-name="commit_id">
62 64 % if c.full_load:
63 <div class="tooltip" title="${h.tooltip(node.last_commit.message)}">
65 <div class="tooltip-hovercard" data-hovercard-alt="${node.last_commit.message}" data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=c.repo_name, commit_id=node.last_commit.raw_id)}">
64 66 <pre data-commit-id="${node.last_commit.raw_id}">r${node.last_commit.idx}:${node.last_commit.short_id}</pre>
65 67 </div>
66 68 % endif
67 69 </td>
68 70 <td class="td-user" data-attr-name="author">
69 71 % if c.full_load:
70 72 <span data-author="${node.last_commit.author}">${h.gravatar_with_user(request, node.last_commit.author, tooltip=True)|n}</span>
71 73 % endif
72 74 </td>
73 75 %else:
74 76 <td></td>
75 77 <td></td>
76 78 <td></td>
77 79 <td></td>
78 80 %endif
79 81 </tr>
80 82 % endfor
81 83
82 84 % if not has_files:
83 85 <tr>
84 86 <td colspan="5">
85 87 ##empty-dir mostly SVN
86 88 &nbsp;
87 89 </td>
88 90 </tr>
89 91 % endif
90 92
91 93 </tbody>
92 94 <tbody id="tbody_filtered"></tbody>
93 95 </table>
94 96 </div>
General Comments 0
You need to be logged in to leave comments. Login now