##// END OF EJS Templates
hovercards: allow hovercards on parsed !PR patterns....
dan -
r4046:a792f9c3 default
parent child Browse files
Show More
@@ -0,0 +1,26 b''
1 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3
4 % if c.can_view_pr:
5 <div class="pr-hovercard-header">
6 <div class="pull-left tagtag">
7 ${c.pull_request.status}
8 </div>
9 <div class="pr-hovercard-user">
10 ${_('Created')}: ${h.format_date(c.pull_request.created_on)}
11 </div>
12 </div>
13
14 <div class="pr-hovercard-title">
15 <h3><a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">!${c.pull_request.pull_request_id}</a> - ${c.pull_request.title}</h3>
16 </div>
17
18 <div class="pr-hovercard-footer">
19 ${_('repo')}: ${c.pull_request.target_repo.repo_name}
20 </div>
21 % else:
22 ## user cannot view this PR we just show the generic info, without any exposed data
23 <div class="pr-hovercard-title">
24 <h3>${_('Pull Request')} !${c.pull_request.pull_request_id}</h3>
25 </div>
26 % endif No newline at end of file
@@ -1,37 +1,41 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 20
21 21
22 22 def includeme(config):
23 23
24 24 config.add_route(
25 25 name='hovercard_user',
26 26 pattern='/_hovercard/user/{user_id}')
27 27
28 28 config.add_route(
29 29 name='hovercard_user_group',
30 30 pattern='/_hovercard/user_group/{user_group_id}')
31 31
32 32 config.add_route(
33 name='hovercard_pull_request',
34 pattern='/_hovercard/pull_request/{pull_request_id}')
35
36 config.add_route(
33 37 name='hovercard_repo_commit',
34 38 pattern='/_hovercard/commit/{repo_name:.*?[^/]}/{commit_id}', repo_route=True)
35 39
36 40 # Scan module for configuration decorators.
37 41 config.scan('.views', ignore='.tests')
@@ -1,90 +1,103 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 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 30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
31 31 HasRepoPermissionAnyDecorator)
32 32 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
33 33 from rhodecode.lib.index import searcher_from_config
34 34 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.vcs.nodes import FileNode
37 37 from rhodecode.model.db import (
38 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup)
38 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup, PullRequest)
39 39 from rhodecode.model.repo import RepoModel
40 40 from rhodecode.model.repo_group import RepoGroupModel
41 41 from rhodecode.model.scm import RepoGroupList, RepoList
42 42 from rhodecode.model.user import UserModel
43 43 from rhodecode.model.user_group import UserGroupModel
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class HoverCardsView(BaseAppView):
49 49
50 50 def load_default_context(self):
51 51 c = self._get_local_tmpl_context()
52 52 return c
53 53
54 54 @LoginRequired()
55 55 @view_config(
56 56 route_name='hovercard_user', request_method='GET', xhr=True,
57 57 renderer='rhodecode:templates/hovercards/hovercard_user.mako')
58 58 def hovercard_user(self):
59 59 c = self.load_default_context()
60 60 user_id = self.request.matchdict['user_id']
61 61 c.user = User.get_or_404(user_id)
62 62 return self._get_template_context(c)
63 63
64 64 @LoginRequired()
65 65 @view_config(
66 66 route_name='hovercard_user_group', request_method='GET', xhr=True,
67 67 renderer='rhodecode:templates/hovercards/hovercard_user_group.mako')
68 68 def hovercard_user_group(self):
69 69 c = self.load_default_context()
70 70 user_group_id = self.request.matchdict['user_group_id']
71 71 c.user_group = UserGroup.get_or_404(user_group_id)
72 72 return self._get_template_context(c)
73 73
74 @LoginRequired()
75 @view_config(
76 route_name='hovercard_pull_request', request_method='GET', xhr=True,
77 renderer='rhodecode:templates/hovercards/hovercard_pull_request.mako')
78 def hovercard_pull_request(self):
79 c = self.load_default_context()
80 c.pull_request = PullRequest.get_or_404(
81 self.request.matchdict['pull_request_id'])
82 perms = ['repository.read', 'repository.write', 'repository.admin']
83 c.can_view_pr = h.HasRepoPermissionAny(*perms)(
84 c.pull_request.target_repo.repo_name)
85 return self._get_template_context(c)
86
74 87
75 88 class HoverCardsRepoView(RepoAppView):
76 89 def load_default_context(self):
77 90 c = self._get_local_tmpl_context()
78 91 return c
79 92
80 93 @LoginRequired()
81 94 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', 'repository.admin')
82 95 @view_config(
83 96 route_name='hovercard_repo_commit', request_method='GET', xhr=True,
84 97 renderer='rhodecode:templates/hovercards/hovercard_repo_commit.mako')
85 98 def hovercard_repo_commit(self):
86 99 c = self.load_default_context()
87 100 commit_id = self.request.matchdict['commit_id']
88 101 pre_load = ['author', 'branch', 'date', 'message']
89 102 c.commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id, pre_load=pre_load)
90 103 return self._get_template_context(c)
@@ -1,2107 +1,2125 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 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
79 AttributeDict, safe_int, md5, md5_safe
77 from rhodecode.lib.utils2 import (
78 str2bool, safe_unicode, safe_str,
79 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
80 AttributeDict, safe_int, md5, md5_safe, get_host_info)
80 81 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 82 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 83 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 84 from rhodecode.lib.index.search_utils import get_matching_line_offsets
84 85 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
85 86 from rhodecode.model.changeset_status import ChangesetStatusModel
86 87 from rhodecode.model.db import Permission, User, Repository
87 88 from rhodecode.model.repo_group import RepoGroupModel
88 89 from rhodecode.model.settings import IssueTrackerSettingsModel
89 90
90 91
91 92 log = logging.getLogger(__name__)
92 93
93 94
94 95 DEFAULT_USER = User.DEFAULT_USER
95 96 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
96 97
97 98
98 99 def asset(path, ver=None, **kwargs):
99 100 """
100 101 Helper to generate a static asset file path for rhodecode assets
101 102
102 103 eg. h.asset('images/image.png', ver='3923')
103 104
104 105 :param path: path of asset
105 106 :param ver: optional version query param to append as ?ver=
106 107 """
107 108 request = get_current_request()
108 109 query = {}
109 110 query.update(kwargs)
110 111 if ver:
111 112 query = {'ver': ver}
112 113 return request.static_path(
113 114 'rhodecode:public/{}'.format(path), _query=query)
114 115
115 116
116 117 default_html_escape_table = {
117 118 ord('&'): u'&amp;',
118 119 ord('<'): u'&lt;',
119 120 ord('>'): u'&gt;',
120 121 ord('"'): u'&quot;',
121 122 ord("'"): u'&#39;',
122 123 }
123 124
124 125
125 126 def html_escape(text, html_escape_table=default_html_escape_table):
126 127 """Produce entities within text."""
127 128 return text.translate(html_escape_table)
128 129
129 130
130 131 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
131 132 """
132 133 Truncate string ``s`` at the first occurrence of ``sub``.
133 134
134 135 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
135 136 """
136 137 suffix_if_chopped = suffix_if_chopped or ''
137 138 pos = s.find(sub)
138 139 if pos == -1:
139 140 return s
140 141
141 142 if inclusive:
142 143 pos += len(sub)
143 144
144 145 chopped = s[:pos]
145 146 left = s[pos:].strip()
146 147
147 148 if left and suffix_if_chopped:
148 149 chopped += suffix_if_chopped
149 150
150 151 return chopped
151 152
152 153
153 154 def shorter(text, size=20, prefix=False):
154 155 postfix = '...'
155 156 if len(text) > size:
156 157 if prefix:
157 158 # shorten in front
158 159 return postfix + text[-(size - len(postfix)):]
159 160 else:
160 161 return text[:size - len(postfix)] + postfix
161 162 return text
162 163
163 164
164 165 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
165 166 """
166 167 Reset button
167 168 """
168 169 _set_input_attrs(attrs, type, name, value)
169 170 _set_id_attr(attrs, id, name)
170 171 convert_boolean_attrs(attrs, ["disabled"])
171 172 return HTML.input(**attrs)
172 173
173 174 reset = _reset
174 175 safeid = _make_safe_id_component
175 176
176 177
177 178 def branding(name, length=40):
178 179 return truncate(name, length, indicator="")
179 180
180 181
181 182 def FID(raw_id, path):
182 183 """
183 184 Creates a unique ID for filenode based on it's hash of path and commit
184 185 it's safe to use in urls
185 186
186 187 :param raw_id:
187 188 :param path:
188 189 """
189 190
190 191 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
191 192
192 193
193 194 class _GetError(object):
194 195 """Get error from form_errors, and represent it as span wrapped error
195 196 message
196 197
197 198 :param field_name: field to fetch errors for
198 199 :param form_errors: form errors dict
199 200 """
200 201
201 202 def __call__(self, field_name, form_errors):
202 203 tmpl = """<span class="error_msg">%s</span>"""
203 204 if form_errors and field_name in form_errors:
204 205 return literal(tmpl % form_errors.get(field_name))
205 206
206 207
207 208 get_error = _GetError()
208 209
209 210
210 211 class _ToolTip(object):
211 212
212 213 def __call__(self, tooltip_title, trim_at=50):
213 214 """
214 215 Special function just to wrap our text into nice formatted
215 216 autowrapped text
216 217
217 218 :param tooltip_title:
218 219 """
219 220 tooltip_title = escape(tooltip_title)
220 221 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
221 222 return tooltip_title
222 223
223 224
224 225 tooltip = _ToolTip()
225 226
226 227 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
227 228
228 229
229 230 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
230 231 if isinstance(file_path, str):
231 232 file_path = safe_unicode(file_path)
232 233
233 234 route_qry = {'at': at_ref} if at_ref else None
234 235
235 236 # first segment is a `..` link to repo files
236 237 root_name = literal(u'<i class="icon-home"></i>')
237 238 url_segments = [
238 239 link_to(
239 240 root_name,
240 241 route_path(
241 242 'repo_files',
242 243 repo_name=repo_name,
243 244 commit_id=commit_id,
244 245 f_path='',
245 246 _query=route_qry),
246 247 )]
247 248
248 249 path_segments = file_path.split('/')
249 250 last_cnt = len(path_segments) - 1
250 251 for cnt, segment in enumerate(path_segments):
251 252 if not segment:
252 253 continue
253 254 segment_html = escape(segment)
254 255
255 256 last_item = cnt == last_cnt
256 257
257 258 if last_item and linkify_last_item is False:
258 259 # plain version
259 260 url_segments.append(segment_html)
260 261 else:
261 262 url_segments.append(
262 263 link_to(
263 264 segment_html,
264 265 route_path(
265 266 'repo_files',
266 267 repo_name=repo_name,
267 268 commit_id=commit_id,
268 269 f_path='/'.join(path_segments[:cnt + 1]),
269 270 _query=route_qry),
270 271 ))
271 272
272 273 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
273 274 if limit_items and len(limited_url_segments) < len(url_segments):
274 275 url_segments = limited_url_segments
275 276
276 277 full_path = file_path
277 278 icon = files_icon.format(escape(full_path))
278 279 if file_path == '':
279 280 return root_name
280 281 else:
281 282 return literal(' / '.join(url_segments) + icon)
282 283
283 284
284 285 def files_url_data(request):
285 286 matchdict = request.matchdict
286 287
287 288 if 'f_path' not in matchdict:
288 289 matchdict['f_path'] = ''
289 290
290 291 if 'commit_id' not in matchdict:
291 292 matchdict['commit_id'] = 'tip'
292 293
293 294 return json.dumps(matchdict)
294 295
295 296
296 297 def code_highlight(code, lexer, formatter, use_hl_filter=False):
297 298 """
298 299 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
299 300
300 301 If ``outfile`` is given and a valid file object (an object
301 302 with a ``write`` method), the result will be written to it, otherwise
302 303 it is returned as a string.
303 304 """
304 305 if use_hl_filter:
305 306 # add HL filter
306 307 from rhodecode.lib.index import search_utils
307 308 lexer.add_filter(search_utils.ElasticSearchHLFilter())
308 309 return pygments.format(pygments.lex(code, lexer), formatter)
309 310
310 311
311 312 class CodeHtmlFormatter(HtmlFormatter):
312 313 """
313 314 My code Html Formatter for source codes
314 315 """
315 316
316 317 def wrap(self, source, outfile):
317 318 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
318 319
319 320 def _wrap_code(self, source):
320 321 for cnt, it in enumerate(source):
321 322 i, t = it
322 323 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
323 324 yield i, t
324 325
325 326 def _wrap_tablelinenos(self, inner):
326 327 dummyoutfile = StringIO.StringIO()
327 328 lncount = 0
328 329 for t, line in inner:
329 330 if t:
330 331 lncount += 1
331 332 dummyoutfile.write(line)
332 333
333 334 fl = self.linenostart
334 335 mw = len(str(lncount + fl - 1))
335 336 sp = self.linenospecial
336 337 st = self.linenostep
337 338 la = self.lineanchors
338 339 aln = self.anchorlinenos
339 340 nocls = self.noclasses
340 341 if sp:
341 342 lines = []
342 343
343 344 for i in range(fl, fl + lncount):
344 345 if i % st == 0:
345 346 if i % sp == 0:
346 347 if aln:
347 348 lines.append('<a href="#%s%d" class="special">%*d</a>' %
348 349 (la, i, mw, i))
349 350 else:
350 351 lines.append('<span class="special">%*d</span>' % (mw, i))
351 352 else:
352 353 if aln:
353 354 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
354 355 else:
355 356 lines.append('%*d' % (mw, i))
356 357 else:
357 358 lines.append('')
358 359 ls = '\n'.join(lines)
359 360 else:
360 361 lines = []
361 362 for i in range(fl, fl + lncount):
362 363 if i % st == 0:
363 364 if aln:
364 365 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
365 366 else:
366 367 lines.append('%*d' % (mw, i))
367 368 else:
368 369 lines.append('')
369 370 ls = '\n'.join(lines)
370 371
371 372 # in case you wonder about the seemingly redundant <div> here: since the
372 373 # content in the other cell also is wrapped in a div, some browsers in
373 374 # some configurations seem to mess up the formatting...
374 375 if nocls:
375 376 yield 0, ('<table class="%stable">' % self.cssclass +
376 377 '<tr><td><div class="linenodiv" '
377 378 'style="background-color: #f0f0f0; padding-right: 10px">'
378 379 '<pre style="line-height: 125%">' +
379 380 ls + '</pre></div></td><td id="hlcode" class="code">')
380 381 else:
381 382 yield 0, ('<table class="%stable">' % self.cssclass +
382 383 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
383 384 ls + '</pre></div></td><td id="hlcode" class="code">')
384 385 yield 0, dummyoutfile.getvalue()
385 386 yield 0, '</td></tr></table>'
386 387
387 388
388 389 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
389 390 def __init__(self, **kw):
390 391 # only show these line numbers if set
391 392 self.only_lines = kw.pop('only_line_numbers', [])
392 393 self.query_terms = kw.pop('query_terms', [])
393 394 self.max_lines = kw.pop('max_lines', 5)
394 395 self.line_context = kw.pop('line_context', 3)
395 396 self.url = kw.pop('url', None)
396 397
397 398 super(CodeHtmlFormatter, self).__init__(**kw)
398 399
399 400 def _wrap_code(self, source):
400 401 for cnt, it in enumerate(source):
401 402 i, t = it
402 403 t = '<pre>%s</pre>' % t
403 404 yield i, t
404 405
405 406 def _wrap_tablelinenos(self, inner):
406 407 yield 0, '<table class="code-highlight %stable">' % self.cssclass
407 408
408 409 last_shown_line_number = 0
409 410 current_line_number = 1
410 411
411 412 for t, line in inner:
412 413 if not t:
413 414 yield t, line
414 415 continue
415 416
416 417 if current_line_number in self.only_lines:
417 418 if last_shown_line_number + 1 != current_line_number:
418 419 yield 0, '<tr>'
419 420 yield 0, '<td class="line">...</td>'
420 421 yield 0, '<td id="hlcode" class="code"></td>'
421 422 yield 0, '</tr>'
422 423
423 424 yield 0, '<tr>'
424 425 if self.url:
425 426 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
426 427 self.url, current_line_number, current_line_number)
427 428 else:
428 429 yield 0, '<td class="line"><a href="">%i</a></td>' % (
429 430 current_line_number)
430 431 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
431 432 yield 0, '</tr>'
432 433
433 434 last_shown_line_number = current_line_number
434 435
435 436 current_line_number += 1
436 437
437 438 yield 0, '</table>'
438 439
439 440
440 441 def hsv_to_rgb(h, s, v):
441 442 """ Convert hsv color values to rgb """
442 443
443 444 if s == 0.0:
444 445 return v, v, v
445 446 i = int(h * 6.0) # XXX assume int() truncates!
446 447 f = (h * 6.0) - i
447 448 p = v * (1.0 - s)
448 449 q = v * (1.0 - s * f)
449 450 t = v * (1.0 - s * (1.0 - f))
450 451 i = i % 6
451 452 if i == 0:
452 453 return v, t, p
453 454 if i == 1:
454 455 return q, v, p
455 456 if i == 2:
456 457 return p, v, t
457 458 if i == 3:
458 459 return p, q, v
459 460 if i == 4:
460 461 return t, p, v
461 462 if i == 5:
462 463 return v, p, q
463 464
464 465
465 466 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
466 467 """
467 468 Generator for getting n of evenly distributed colors using
468 469 hsv color and golden ratio. It always return same order of colors
469 470
470 471 :param n: number of colors to generate
471 472 :param saturation: saturation of returned colors
472 473 :param lightness: lightness of returned colors
473 474 :returns: RGB tuple
474 475 """
475 476
476 477 golden_ratio = 0.618033988749895
477 478 h = 0.22717784590367374
478 479
479 480 for _ in xrange(n):
480 481 h += golden_ratio
481 482 h %= 1
482 483 HSV_tuple = [h, saturation, lightness]
483 484 RGB_tuple = hsv_to_rgb(*HSV_tuple)
484 485 yield map(lambda x: str(int(x * 256)), RGB_tuple)
485 486
486 487
487 488 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
488 489 """
489 490 Returns a function which when called with an argument returns a unique
490 491 color for that argument, eg.
491 492
492 493 :param n: number of colors to generate
493 494 :param saturation: saturation of returned colors
494 495 :param lightness: lightness of returned colors
495 496 :returns: css RGB string
496 497
497 498 >>> color_hash = color_hasher()
498 499 >>> color_hash('hello')
499 500 'rgb(34, 12, 59)'
500 501 >>> color_hash('hello')
501 502 'rgb(34, 12, 59)'
502 503 >>> color_hash('other')
503 504 'rgb(90, 224, 159)'
504 505 """
505 506
506 507 color_dict = {}
507 508 cgenerator = unique_color_generator(
508 509 saturation=saturation, lightness=lightness)
509 510
510 511 def get_color_string(thing):
511 512 if thing in color_dict:
512 513 col = color_dict[thing]
513 514 else:
514 515 col = color_dict[thing] = cgenerator.next()
515 516 return "rgb(%s)" % (', '.join(col))
516 517
517 518 return get_color_string
518 519
519 520
520 521 def get_lexer_safe(mimetype=None, filepath=None):
521 522 """
522 523 Tries to return a relevant pygments lexer using mimetype/filepath name,
523 524 defaulting to plain text if none could be found
524 525 """
525 526 lexer = None
526 527 try:
527 528 if mimetype:
528 529 lexer = get_lexer_for_mimetype(mimetype)
529 530 if not lexer:
530 531 lexer = get_lexer_for_filename(filepath)
531 532 except pygments.util.ClassNotFound:
532 533 pass
533 534
534 535 if not lexer:
535 536 lexer = get_lexer_by_name('text')
536 537
537 538 return lexer
538 539
539 540
540 541 def get_lexer_for_filenode(filenode):
541 542 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
542 543 return lexer
543 544
544 545
545 546 def pygmentize(filenode, **kwargs):
546 547 """
547 548 pygmentize function using pygments
548 549
549 550 :param filenode:
550 551 """
551 552 lexer = get_lexer_for_filenode(filenode)
552 553 return literal(code_highlight(filenode.content, lexer,
553 554 CodeHtmlFormatter(**kwargs)))
554 555
555 556
556 557 def is_following_repo(repo_name, user_id):
557 558 from rhodecode.model.scm import ScmModel
558 559 return ScmModel().is_following_repo(repo_name, user_id)
559 560
560 561
561 562 class _Message(object):
562 563 """A message returned by ``Flash.pop_messages()``.
563 564
564 565 Converting the message to a string returns the message text. Instances
565 566 also have the following attributes:
566 567
567 568 * ``message``: the message text.
568 569 * ``category``: the category specified when the message was created.
569 570 """
570 571
571 572 def __init__(self, category, message):
572 573 self.category = category
573 574 self.message = message
574 575
575 576 def __str__(self):
576 577 return self.message
577 578
578 579 __unicode__ = __str__
579 580
580 581 def __html__(self):
581 582 return escape(safe_unicode(self.message))
582 583
583 584
584 585 class Flash(object):
585 586 # List of allowed categories. If None, allow any category.
586 587 categories = ["warning", "notice", "error", "success"]
587 588
588 589 # Default category if none is specified.
589 590 default_category = "notice"
590 591
591 592 def __init__(self, session_key="flash", categories=None,
592 593 default_category=None):
593 594 """
594 595 Instantiate a ``Flash`` object.
595 596
596 597 ``session_key`` is the key to save the messages under in the user's
597 598 session.
598 599
599 600 ``categories`` is an optional list which overrides the default list
600 601 of categories.
601 602
602 603 ``default_category`` overrides the default category used for messages
603 604 when none is specified.
604 605 """
605 606 self.session_key = session_key
606 607 if categories is not None:
607 608 self.categories = categories
608 609 if default_category is not None:
609 610 self.default_category = default_category
610 611 if self.categories and self.default_category not in self.categories:
611 612 raise ValueError(
612 613 "unrecognized default category %r" % (self.default_category,))
613 614
614 615 def pop_messages(self, session=None, request=None):
615 616 """
616 617 Return all accumulated messages and delete them from the session.
617 618
618 619 The return value is a list of ``Message`` objects.
619 620 """
620 621 messages = []
621 622
622 623 if not session:
623 624 if not request:
624 625 request = get_current_request()
625 626 session = request.session
626 627
627 628 # Pop the 'old' pylons flash messages. They are tuples of the form
628 629 # (category, message)
629 630 for cat, msg in session.pop(self.session_key, []):
630 631 messages.append(_Message(cat, msg))
631 632
632 633 # Pop the 'new' pyramid flash messages for each category as list
633 634 # of strings.
634 635 for cat in self.categories:
635 636 for msg in session.pop_flash(queue=cat):
636 637 messages.append(_Message(cat, msg))
637 638 # Map messages from the default queue to the 'notice' category.
638 639 for msg in session.pop_flash():
639 640 messages.append(_Message('notice', msg))
640 641
641 642 session.save()
642 643 return messages
643 644
644 645 def json_alerts(self, session=None, request=None):
645 646 payloads = []
646 647 messages = flash.pop_messages(session=session, request=request)
647 648 if messages:
648 649 for message in messages:
649 650 subdata = {}
650 651 if hasattr(message.message, 'rsplit'):
651 652 flash_data = message.message.rsplit('|DELIM|', 1)
652 653 org_message = flash_data[0]
653 654 if len(flash_data) > 1:
654 655 subdata = json.loads(flash_data[1])
655 656 else:
656 657 org_message = message.message
657 658 payloads.append({
658 659 'message': {
659 660 'message': u'{}'.format(org_message),
660 661 'level': message.category,
661 662 'force': True,
662 663 'subdata': subdata
663 664 }
664 665 })
665 666 return json.dumps(payloads)
666 667
667 668 def __call__(self, message, category=None, ignore_duplicate=True,
668 669 session=None, request=None):
669 670
670 671 if not session:
671 672 if not request:
672 673 request = get_current_request()
673 674 session = request.session
674 675
675 676 session.flash(
676 677 message, queue=category, allow_duplicate=not ignore_duplicate)
677 678
678 679
679 680 flash = Flash()
680 681
681 682 #==============================================================================
682 683 # SCM FILTERS available via h.
683 684 #==============================================================================
684 685 from rhodecode.lib.vcs.utils import author_name, author_email
685 686 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
686 687 from rhodecode.model.db import User, ChangesetStatus
687 688
688 689 capitalize = lambda x: x.capitalize()
689 690 email = author_email
690 691 short_id = lambda x: x[:12]
691 692 hide_credentials = lambda x: ''.join(credentials_filter(x))
692 693
693 694
694 695 import pytz
695 696 import tzlocal
696 697 local_timezone = tzlocal.get_localzone()
697 698
698 699
699 700 def age_component(datetime_iso, value=None, time_is_local=False):
700 701 title = value or format_date(datetime_iso)
701 702 tzinfo = '+00:00'
702 703
703 704 # detect if we have a timezone info, otherwise, add it
704 705 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
705 706 force_timezone = os.environ.get('RC_TIMEZONE', '')
706 707 if force_timezone:
707 708 force_timezone = pytz.timezone(force_timezone)
708 709 timezone = force_timezone or local_timezone
709 710 offset = timezone.localize(datetime_iso).strftime('%z')
710 711 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
711 712
712 713 return literal(
713 714 '<time class="timeago tooltip" '
714 715 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
715 716 datetime_iso, title, tzinfo))
716 717
717 718
718 719 def _shorten_commit_id(commit_id, commit_len=None):
719 720 if commit_len is None:
720 721 request = get_current_request()
721 722 commit_len = request.call_context.visual.show_sha_length
722 723 return commit_id[:commit_len]
723 724
724 725
725 726 def show_id(commit, show_idx=None, commit_len=None):
726 727 """
727 728 Configurable function that shows ID
728 729 by default it's r123:fffeeefffeee
729 730
730 731 :param commit: commit instance
731 732 """
732 733 if show_idx is None:
733 734 request = get_current_request()
734 735 show_idx = request.call_context.visual.show_revision_number
735 736
736 737 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
737 738 if show_idx:
738 739 return 'r%s:%s' % (commit.idx, raw_id)
739 740 else:
740 741 return '%s' % (raw_id, )
741 742
742 743
743 744 def format_date(date):
744 745 """
745 746 use a standardized formatting for dates used in RhodeCode
746 747
747 748 :param date: date/datetime object
748 749 :return: formatted date
749 750 """
750 751
751 752 if date:
752 753 _fmt = "%a, %d %b %Y %H:%M:%S"
753 754 return safe_unicode(date.strftime(_fmt))
754 755
755 756 return u""
756 757
757 758
758 759 class _RepoChecker(object):
759 760
760 761 def __init__(self, backend_alias):
761 762 self._backend_alias = backend_alias
762 763
763 764 def __call__(self, repository):
764 765 if hasattr(repository, 'alias'):
765 766 _type = repository.alias
766 767 elif hasattr(repository, 'repo_type'):
767 768 _type = repository.repo_type
768 769 else:
769 770 _type = repository
770 771 return _type == self._backend_alias
771 772
772 773
773 774 is_git = _RepoChecker('git')
774 775 is_hg = _RepoChecker('hg')
775 776 is_svn = _RepoChecker('svn')
776 777
777 778
778 779 def get_repo_type_by_name(repo_name):
779 780 repo = Repository.get_by_repo_name(repo_name)
780 781 if repo:
781 782 return repo.repo_type
782 783
783 784
784 785 def is_svn_without_proxy(repository):
785 786 if is_svn(repository):
786 787 from rhodecode.model.settings import VcsSettingsModel
787 788 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
788 789 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
789 790 return False
790 791
791 792
792 793 def discover_user(author):
793 794 """
794 795 Tries to discover RhodeCode User based on the autho string. Author string
795 796 is typically `FirstName LastName <email@address.com>`
796 797 """
797 798
798 799 # if author is already an instance use it for extraction
799 800 if isinstance(author, User):
800 801 return author
801 802
802 803 # Valid email in the attribute passed, see if they're in the system
803 804 _email = author_email(author)
804 805 if _email != '':
805 806 user = User.get_by_email(_email, case_insensitive=True, cache=True)
806 807 if user is not None:
807 808 return user
808 809
809 810 # Maybe it's a username, we try to extract it and fetch by username ?
810 811 _author = author_name(author)
811 812 user = User.get_by_username(_author, case_insensitive=True, cache=True)
812 813 if user is not None:
813 814 return user
814 815
815 816 return None
816 817
817 818
818 819 def email_or_none(author):
819 820 # extract email from the commit string
820 821 _email = author_email(author)
821 822
822 823 # If we have an email, use it, otherwise
823 824 # see if it contains a username we can get an email from
824 825 if _email != '':
825 826 return _email
826 827 else:
827 828 user = User.get_by_username(
828 829 author_name(author), case_insensitive=True, cache=True)
829 830
830 831 if user is not None:
831 832 return user.email
832 833
833 834 # No valid email, not a valid user in the system, none!
834 835 return None
835 836
836 837
837 838 def link_to_user(author, length=0, **kwargs):
838 839 user = discover_user(author)
839 840 # user can be None, but if we have it already it means we can re-use it
840 841 # in the person() function, so we save 1 intensive-query
841 842 if user:
842 843 author = user
843 844
844 845 display_person = person(author, 'username_or_name_or_email')
845 846 if length:
846 847 display_person = shorter(display_person, length)
847 848
848 849 if user:
849 850 return link_to(
850 851 escape(display_person),
851 852 route_path('user_profile', username=user.username),
852 853 **kwargs)
853 854 else:
854 855 return escape(display_person)
855 856
856 857
857 858 def link_to_group(users_group_name, **kwargs):
858 859 return link_to(
859 860 escape(users_group_name),
860 861 route_path('user_group_profile', user_group_name=users_group_name),
861 862 **kwargs)
862 863
863 864
864 865 def person(author, show_attr="username_and_name"):
865 866 user = discover_user(author)
866 867 if user:
867 868 return getattr(user, show_attr)
868 869 else:
869 870 _author = author_name(author)
870 871 _email = email(author)
871 872 return _author or _email
872 873
873 874
874 875 def author_string(email):
875 876 if email:
876 877 user = User.get_by_email(email, case_insensitive=True, cache=True)
877 878 if user:
878 879 if user.first_name or user.last_name:
879 880 return '%s %s &lt;%s&gt;' % (
880 881 user.first_name, user.last_name, email)
881 882 else:
882 883 return email
883 884 else:
884 885 return email
885 886 else:
886 887 return None
887 888
888 889
889 890 def person_by_id(id_, show_attr="username_and_name"):
890 891 # attr to return from fetched user
891 892 person_getter = lambda usr: getattr(usr, show_attr)
892 893
893 894 #maybe it's an ID ?
894 895 if str(id_).isdigit() or isinstance(id_, int):
895 896 id_ = int(id_)
896 897 user = User.get(id_)
897 898 if user is not None:
898 899 return person_getter(user)
899 900 return id_
900 901
901 902
902 903 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
903 904 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
904 905 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
905 906
906 907
907 908 tags_paterns = OrderedDict((
908 909 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
909 910 '<div class="metatag" tag="lang">\\2</div>')),
910 911
911 912 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
912 913 '<div class="metatag" tag="see">see: \\1 </div>')),
913 914
914 915 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
915 916 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
916 917
917 918 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
918 919 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
919 920
920 921 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
921 922 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
922 923
923 924 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
924 925 '<div class="metatag" tag="state \\1">\\1</div>')),
925 926
926 927 # label in grey
927 928 ('label', (re.compile(r'\[([a-z]+)\]'),
928 929 '<div class="metatag" tag="label">\\1</div>')),
929 930
930 931 # generic catch all in grey
931 932 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
932 933 '<div class="metatag" tag="generic">\\1</div>')),
933 934 ))
934 935
935 936
936 937 def extract_metatags(value):
937 938 """
938 939 Extract supported meta-tags from given text value
939 940 """
940 941 tags = []
941 942 if not value:
942 943 return tags, ''
943 944
944 945 for key, val in tags_paterns.items():
945 946 pat, replace_html = val
946 947 tags.extend([(key, x.group()) for x in pat.finditer(value)])
947 948 value = pat.sub('', value)
948 949
949 950 return tags, value
950 951
951 952
952 953 def style_metatag(tag_type, value):
953 954 """
954 955 converts tags from value into html equivalent
955 956 """
956 957 if not value:
957 958 return ''
958 959
959 960 html_value = value
960 961 tag_data = tags_paterns.get(tag_type)
961 962 if tag_data:
962 963 pat, replace_html = tag_data
963 964 # convert to plain `unicode` instead of a markup tag to be used in
964 965 # regex expressions. safe_unicode doesn't work here
965 966 html_value = pat.sub(replace_html, unicode(value))
966 967
967 968 return html_value
968 969
969 970
970 971 def bool2icon(value, show_at_false=True):
971 972 """
972 973 Returns boolean value of a given value, represented as html element with
973 974 classes that will represent icons
974 975
975 976 :param value: given value to convert to html node
976 977 """
977 978
978 979 if value: # does bool conversion
979 980 return HTML.tag('i', class_="icon-true", title='True')
980 981 else: # not true as bool
981 982 if show_at_false:
982 983 return HTML.tag('i', class_="icon-false", title='False')
983 984 return HTML.tag('i')
984 985
985 986 #==============================================================================
986 987 # PERMS
987 988 #==============================================================================
988 989 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
989 990 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
990 991 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
991 992 csrf_token_key
992 993
993 994
994 995 #==============================================================================
995 996 # GRAVATAR URL
996 997 #==============================================================================
997 998 class InitialsGravatar(object):
998 999 def __init__(self, email_address, first_name, last_name, size=30,
999 1000 background=None, text_color='#fff'):
1000 1001 self.size = size
1001 1002 self.first_name = first_name
1002 1003 self.last_name = last_name
1003 1004 self.email_address = email_address
1004 1005 self.background = background or self.str2color(email_address)
1005 1006 self.text_color = text_color
1006 1007
1007 1008 def get_color_bank(self):
1008 1009 """
1009 1010 returns a predefined list of colors that gravatars can use.
1010 1011 Those are randomized distinct colors that guarantee readability and
1011 1012 uniqueness.
1012 1013
1013 1014 generated with: http://phrogz.net/css/distinct-colors.html
1014 1015 """
1015 1016 return [
1016 1017 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1017 1018 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1018 1019 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1019 1020 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1020 1021 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1021 1022 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1022 1023 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1023 1024 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1024 1025 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1025 1026 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1026 1027 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1027 1028 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1028 1029 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1029 1030 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1030 1031 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1031 1032 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1032 1033 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1033 1034 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1034 1035 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1035 1036 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1036 1037 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1037 1038 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1038 1039 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1039 1040 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1040 1041 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1041 1042 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1042 1043 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1043 1044 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1044 1045 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1045 1046 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1046 1047 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1047 1048 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1048 1049 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1049 1050 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1050 1051 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1051 1052 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1052 1053 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1053 1054 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1054 1055 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1055 1056 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1056 1057 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1057 1058 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1058 1059 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1059 1060 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1060 1061 '#4f8c46', '#368dd9', '#5c0073'
1061 1062 ]
1062 1063
1063 1064 def rgb_to_hex_color(self, rgb_tuple):
1064 1065 """
1065 1066 Converts an rgb_tuple passed to an hex color.
1066 1067
1067 1068 :param rgb_tuple: tuple with 3 ints represents rgb color space
1068 1069 """
1069 1070 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1070 1071
1071 1072 def email_to_int_list(self, email_str):
1072 1073 """
1073 1074 Get every byte of the hex digest value of email and turn it to integer.
1074 1075 It's going to be always between 0-255
1075 1076 """
1076 1077 digest = md5_safe(email_str.lower())
1077 1078 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1078 1079
1079 1080 def pick_color_bank_index(self, email_str, color_bank):
1080 1081 return self.email_to_int_list(email_str)[0] % len(color_bank)
1081 1082
1082 1083 def str2color(self, email_str):
1083 1084 """
1084 1085 Tries to map in a stable algorithm an email to color
1085 1086
1086 1087 :param email_str:
1087 1088 """
1088 1089 color_bank = self.get_color_bank()
1089 1090 # pick position (module it's length so we always find it in the
1090 1091 # bank even if it's smaller than 256 values
1091 1092 pos = self.pick_color_bank_index(email_str, color_bank)
1092 1093 return color_bank[pos]
1093 1094
1094 1095 def normalize_email(self, email_address):
1095 1096 import unicodedata
1096 1097 # default host used to fill in the fake/missing email
1097 1098 default_host = u'localhost'
1098 1099
1099 1100 if not email_address:
1100 1101 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1101 1102
1102 1103 email_address = safe_unicode(email_address)
1103 1104
1104 1105 if u'@' not in email_address:
1105 1106 email_address = u'%s@%s' % (email_address, default_host)
1106 1107
1107 1108 if email_address.endswith(u'@'):
1108 1109 email_address = u'%s%s' % (email_address, default_host)
1109 1110
1110 1111 email_address = unicodedata.normalize('NFKD', email_address)\
1111 1112 .encode('ascii', 'ignore')
1112 1113 return email_address
1113 1114
1114 1115 def get_initials(self):
1115 1116 """
1116 1117 Returns 2 letter initials calculated based on the input.
1117 1118 The algorithm picks first given email address, and takes first letter
1118 1119 of part before @, and then the first letter of server name. In case
1119 1120 the part before @ is in a format of `somestring.somestring2` it replaces
1120 1121 the server letter with first letter of somestring2
1121 1122
1122 1123 In case function was initialized with both first and lastname, this
1123 1124 overrides the extraction from email by first letter of the first and
1124 1125 last name. We add special logic to that functionality, In case Full name
1125 1126 is compound, like Guido Von Rossum, we use last part of the last name
1126 1127 (Von Rossum) picking `R`.
1127 1128
1128 1129 Function also normalizes the non-ascii characters to they ascii
1129 1130 representation, eg Δ„ => A
1130 1131 """
1131 1132 import unicodedata
1132 1133 # replace non-ascii to ascii
1133 1134 first_name = unicodedata.normalize(
1134 1135 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1135 1136 last_name = unicodedata.normalize(
1136 1137 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1137 1138
1138 1139 # do NFKD encoding, and also make sure email has proper format
1139 1140 email_address = self.normalize_email(self.email_address)
1140 1141
1141 1142 # first push the email initials
1142 1143 prefix, server = email_address.split('@', 1)
1143 1144
1144 1145 # check if prefix is maybe a 'first_name.last_name' syntax
1145 1146 _dot_split = prefix.rsplit('.', 1)
1146 1147 if len(_dot_split) == 2 and _dot_split[1]:
1147 1148 initials = [_dot_split[0][0], _dot_split[1][0]]
1148 1149 else:
1149 1150 initials = [prefix[0], server[0]]
1150 1151
1151 1152 # then try to replace either first_name or last_name
1152 1153 fn_letter = (first_name or " ")[0].strip()
1153 1154 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1154 1155
1155 1156 if fn_letter:
1156 1157 initials[0] = fn_letter
1157 1158
1158 1159 if ln_letter:
1159 1160 initials[1] = ln_letter
1160 1161
1161 1162 return ''.join(initials).upper()
1162 1163
1163 1164 def get_img_data_by_type(self, font_family, img_type):
1164 1165 default_user = """
1165 1166 <svg xmlns="http://www.w3.org/2000/svg"
1166 1167 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1167 1168 viewBox="-15 -10 439.165 429.164"
1168 1169
1169 1170 xml:space="preserve"
1170 1171 style="background:{background};" >
1171 1172
1172 1173 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1173 1174 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1174 1175 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1175 1176 168.596,153.916,216.671,
1176 1177 204.583,216.671z" fill="{text_color}"/>
1177 1178 <path d="M407.164,374.717L360.88,
1178 1179 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1179 1180 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1180 1181 15.366-44.203,23.488-69.076,23.488c-24.877,
1181 1182 0-48.762-8.122-69.078-23.488
1182 1183 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1183 1184 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1184 1185 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1185 1186 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1186 1187 19.402-10.527 C409.699,390.129,
1187 1188 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1188 1189 </svg>""".format(
1189 1190 size=self.size,
1190 1191 background='#979797', # @grey4
1191 1192 text_color=self.text_color,
1192 1193 font_family=font_family)
1193 1194
1194 1195 return {
1195 1196 "default_user": default_user
1196 1197 }[img_type]
1197 1198
1198 1199 def get_img_data(self, svg_type=None):
1199 1200 """
1200 1201 generates the svg metadata for image
1201 1202 """
1202 1203 fonts = [
1203 1204 '-apple-system',
1204 1205 'BlinkMacSystemFont',
1205 1206 'Segoe UI',
1206 1207 'Roboto',
1207 1208 'Oxygen-Sans',
1208 1209 'Ubuntu',
1209 1210 'Cantarell',
1210 1211 'Helvetica Neue',
1211 1212 'sans-serif'
1212 1213 ]
1213 1214 font_family = ','.join(fonts)
1214 1215 if svg_type:
1215 1216 return self.get_img_data_by_type(font_family, svg_type)
1216 1217
1217 1218 initials = self.get_initials()
1218 1219 img_data = """
1219 1220 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1220 1221 width="{size}" height="{size}"
1221 1222 style="width: 100%; height: 100%; background-color: {background}"
1222 1223 viewBox="0 0 {size} {size}">
1223 1224 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1224 1225 pointer-events="auto" fill="{text_color}"
1225 1226 font-family="{font_family}"
1226 1227 style="font-weight: 400; font-size: {f_size}px;">{text}
1227 1228 </text>
1228 1229 </svg>""".format(
1229 1230 size=self.size,
1230 1231 f_size=self.size/2.05, # scale the text inside the box nicely
1231 1232 background=self.background,
1232 1233 text_color=self.text_color,
1233 1234 text=initials.upper(),
1234 1235 font_family=font_family)
1235 1236
1236 1237 return img_data
1237 1238
1238 1239 def generate_svg(self, svg_type=None):
1239 1240 img_data = self.get_img_data(svg_type)
1240 1241 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1241 1242
1242 1243
1243 1244 def initials_gravatar(email_address, first_name, last_name, size=30):
1244 1245 svg_type = None
1245 1246 if email_address == User.DEFAULT_USER_EMAIL:
1246 1247 svg_type = 'default_user'
1247 1248 klass = InitialsGravatar(email_address, first_name, last_name, size)
1248 1249 return klass.generate_svg(svg_type=svg_type)
1249 1250
1250 1251
1251 1252 def gravatar_url(email_address, size=30, request=None):
1252 1253 request = get_current_request()
1253 1254 _use_gravatar = request.call_context.visual.use_gravatar
1254 1255 _gravatar_url = request.call_context.visual.gravatar_url
1255 1256
1256 1257 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1257 1258
1258 1259 email_address = email_address or User.DEFAULT_USER_EMAIL
1259 1260 if isinstance(email_address, unicode):
1260 1261 # hashlib crashes on unicode items
1261 1262 email_address = safe_str(email_address)
1262 1263
1263 1264 # empty email or default user
1264 1265 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1265 1266 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1266 1267
1267 1268 if _use_gravatar:
1268 1269 # TODO: Disuse pyramid thread locals. Think about another solution to
1269 1270 # get the host and schema here.
1270 1271 request = get_current_request()
1271 1272 tmpl = safe_str(_gravatar_url)
1272 1273 tmpl = tmpl.replace('{email}', email_address)\
1273 1274 .replace('{md5email}', md5_safe(email_address.lower())) \
1274 1275 .replace('{netloc}', request.host)\
1275 1276 .replace('{scheme}', request.scheme)\
1276 1277 .replace('{size}', safe_str(size))
1277 1278 return tmpl
1278 1279 else:
1279 1280 return initials_gravatar(email_address, '', '', size=size)
1280 1281
1281 1282
1282 1283 class Page(_Page):
1283 1284 """
1284 1285 Custom pager to match rendering style with paginator
1285 1286 """
1286 1287
1287 1288 def _get_pos(self, cur_page, max_page, items):
1288 1289 edge = (items / 2) + 1
1289 1290 if (cur_page <= edge):
1290 1291 radius = max(items / 2, items - cur_page)
1291 1292 elif (max_page - cur_page) < edge:
1292 1293 radius = (items - 1) - (max_page - cur_page)
1293 1294 else:
1294 1295 radius = items / 2
1295 1296
1296 1297 left = max(1, (cur_page - (radius)))
1297 1298 right = min(max_page, cur_page + (radius))
1298 1299 return left, cur_page, right
1299 1300
1300 1301 def _range(self, regexp_match):
1301 1302 """
1302 1303 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1303 1304
1304 1305 Arguments:
1305 1306
1306 1307 regexp_match
1307 1308 A "re" (regular expressions) match object containing the
1308 1309 radius of linked pages around the current page in
1309 1310 regexp_match.group(1) as a string
1310 1311
1311 1312 This function is supposed to be called as a callable in
1312 1313 re.sub.
1313 1314
1314 1315 """
1315 1316 radius = int(regexp_match.group(1))
1316 1317
1317 1318 # Compute the first and last page number within the radius
1318 1319 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1319 1320 # -> leftmost_page = 5
1320 1321 # -> rightmost_page = 9
1321 1322 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1322 1323 self.last_page,
1323 1324 (radius * 2) + 1)
1324 1325 nav_items = []
1325 1326
1326 1327 # Create a link to the first page (unless we are on the first page
1327 1328 # or there would be no need to insert '..' spacers)
1328 1329 if self.page != self.first_page and self.first_page < leftmost_page:
1329 1330 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1330 1331
1331 1332 # Insert dots if there are pages between the first page
1332 1333 # and the currently displayed page range
1333 1334 if leftmost_page - self.first_page > 1:
1334 1335 # Wrap in a SPAN tag if nolink_attr is set
1335 1336 text = '..'
1336 1337 if self.dotdot_attr:
1337 1338 text = HTML.span(c=text, **self.dotdot_attr)
1338 1339 nav_items.append(text)
1339 1340
1340 1341 for thispage in xrange(leftmost_page, rightmost_page + 1):
1341 1342 # Hilight the current page number and do not use a link
1342 1343 if thispage == self.page:
1343 1344 text = '%s' % (thispage,)
1344 1345 # Wrap in a SPAN tag if nolink_attr is set
1345 1346 if self.curpage_attr:
1346 1347 text = HTML.span(c=text, **self.curpage_attr)
1347 1348 nav_items.append(text)
1348 1349 # Otherwise create just a link to that page
1349 1350 else:
1350 1351 text = '%s' % (thispage,)
1351 1352 nav_items.append(self._pagerlink(thispage, text))
1352 1353
1353 1354 # Insert dots if there are pages between the displayed
1354 1355 # page numbers and the end of the page range
1355 1356 if self.last_page - rightmost_page > 1:
1356 1357 text = '..'
1357 1358 # Wrap in a SPAN tag if nolink_attr is set
1358 1359 if self.dotdot_attr:
1359 1360 text = HTML.span(c=text, **self.dotdot_attr)
1360 1361 nav_items.append(text)
1361 1362
1362 1363 # Create a link to the very last page (unless we are on the last
1363 1364 # page or there would be no need to insert '..' spacers)
1364 1365 if self.page != self.last_page and rightmost_page < self.last_page:
1365 1366 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1366 1367
1367 1368 ## prerender links
1368 1369 #_page_link = url.current()
1369 1370 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1370 1371 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1371 1372 return self.separator.join(nav_items)
1372 1373
1373 1374 def pager(self, format='~2~', page_param='page', partial_param='partial',
1374 1375 show_if_single_page=False, separator=' ', onclick=None,
1375 1376 symbol_first='<<', symbol_last='>>',
1376 1377 symbol_previous='<', symbol_next='>',
1377 1378 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1378 1379 curpage_attr={'class': 'pager_curpage'},
1379 1380 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1380 1381
1381 1382 self.curpage_attr = curpage_attr
1382 1383 self.separator = separator
1383 1384 self.pager_kwargs = kwargs
1384 1385 self.page_param = page_param
1385 1386 self.partial_param = partial_param
1386 1387 self.onclick = onclick
1387 1388 self.link_attr = link_attr
1388 1389 self.dotdot_attr = dotdot_attr
1389 1390
1390 1391 # Don't show navigator if there is no more than one page
1391 1392 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1392 1393 return ''
1393 1394
1394 1395 from string import Template
1395 1396 # Replace ~...~ in token format by range of pages
1396 1397 result = re.sub(r'~(\d+)~', self._range, format)
1397 1398
1398 1399 # Interpolate '%' variables
1399 1400 result = Template(result).safe_substitute({
1400 1401 'first_page': self.first_page,
1401 1402 'last_page': self.last_page,
1402 1403 'page': self.page,
1403 1404 'page_count': self.page_count,
1404 1405 'items_per_page': self.items_per_page,
1405 1406 'first_item': self.first_item,
1406 1407 'last_item': self.last_item,
1407 1408 'item_count': self.item_count,
1408 1409 'link_first': self.page > self.first_page and \
1409 1410 self._pagerlink(self.first_page, symbol_first) or '',
1410 1411 'link_last': self.page < self.last_page and \
1411 1412 self._pagerlink(self.last_page, symbol_last) or '',
1412 1413 'link_previous': self.previous_page and \
1413 1414 self._pagerlink(self.previous_page, symbol_previous) \
1414 1415 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1415 1416 'link_next': self.next_page and \
1416 1417 self._pagerlink(self.next_page, symbol_next) \
1417 1418 or HTML.span(symbol_next, class_="pg-next disabled")
1418 1419 })
1419 1420
1420 1421 return literal(result)
1421 1422
1422 1423
1423 1424 #==============================================================================
1424 1425 # REPO PAGER, PAGER FOR REPOSITORY
1425 1426 #==============================================================================
1426 1427 class RepoPage(Page):
1427 1428
1428 1429 def __init__(self, collection, page=1, items_per_page=20,
1429 1430 item_count=None, url=None, **kwargs):
1430 1431
1431 1432 """Create a "RepoPage" instance. special pager for paging
1432 1433 repository
1433 1434 """
1434 1435 self._url_generator = url
1435 1436
1436 1437 # Safe the kwargs class-wide so they can be used in the pager() method
1437 1438 self.kwargs = kwargs
1438 1439
1439 1440 # Save a reference to the collection
1440 1441 self.original_collection = collection
1441 1442
1442 1443 self.collection = collection
1443 1444
1444 1445 # The self.page is the number of the current page.
1445 1446 # The first page has the number 1!
1446 1447 try:
1447 1448 self.page = int(page) # make it int() if we get it as a string
1448 1449 except (ValueError, TypeError):
1449 1450 self.page = 1
1450 1451
1451 1452 self.items_per_page = items_per_page
1452 1453
1453 1454 # Unless the user tells us how many items the collections has
1454 1455 # we calculate that ourselves.
1455 1456 if item_count is not None:
1456 1457 self.item_count = item_count
1457 1458 else:
1458 1459 self.item_count = len(self.collection)
1459 1460
1460 1461 # Compute the number of the first and last available page
1461 1462 if self.item_count > 0:
1462 1463 self.first_page = 1
1463 1464 self.page_count = int(math.ceil(float(self.item_count) /
1464 1465 self.items_per_page))
1465 1466 self.last_page = self.first_page + self.page_count - 1
1466 1467
1467 1468 # Make sure that the requested page number is the range of
1468 1469 # valid pages
1469 1470 if self.page > self.last_page:
1470 1471 self.page = self.last_page
1471 1472 elif self.page < self.first_page:
1472 1473 self.page = self.first_page
1473 1474
1474 1475 # Note: the number of items on this page can be less than
1475 1476 # items_per_page if the last page is not full
1476 1477 self.first_item = max(0, (self.item_count) - (self.page *
1477 1478 items_per_page))
1478 1479 self.last_item = ((self.item_count - 1) - items_per_page *
1479 1480 (self.page - 1))
1480 1481
1481 1482 self.items = list(self.collection[self.first_item:self.last_item + 1])
1482 1483
1483 1484 # Links to previous and next page
1484 1485 if self.page > self.first_page:
1485 1486 self.previous_page = self.page - 1
1486 1487 else:
1487 1488 self.previous_page = None
1488 1489
1489 1490 if self.page < self.last_page:
1490 1491 self.next_page = self.page + 1
1491 1492 else:
1492 1493 self.next_page = None
1493 1494
1494 1495 # No items available
1495 1496 else:
1496 1497 self.first_page = None
1497 1498 self.page_count = 0
1498 1499 self.last_page = None
1499 1500 self.first_item = None
1500 1501 self.last_item = None
1501 1502 self.previous_page = None
1502 1503 self.next_page = None
1503 1504 self.items = []
1504 1505
1505 1506 # This is a subclass of the 'list' type. Initialise the list now.
1506 1507 list.__init__(self, reversed(self.items))
1507 1508
1508 1509
1509 1510 def breadcrumb_repo_link(repo):
1510 1511 """
1511 1512 Makes a breadcrumbs path link to repo
1512 1513
1513 1514 ex::
1514 1515 group >> subgroup >> repo
1515 1516
1516 1517 :param repo: a Repository instance
1517 1518 """
1518 1519
1519 1520 path = [
1520 1521 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1521 1522 title='last change:{}'.format(format_date(group.last_commit_change)))
1522 1523 for group in repo.groups_with_parents
1523 1524 ] + [
1524 1525 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1525 1526 title='last change:{}'.format(format_date(repo.last_commit_change)))
1526 1527 ]
1527 1528
1528 1529 return literal(' &raquo; '.join(path))
1529 1530
1530 1531
1531 1532 def breadcrumb_repo_group_link(repo_group):
1532 1533 """
1533 1534 Makes a breadcrumbs path link to repo
1534 1535
1535 1536 ex::
1536 1537 group >> subgroup
1537 1538
1538 1539 :param repo_group: a Repository Group instance
1539 1540 """
1540 1541
1541 1542 path = [
1542 1543 link_to(group.name,
1543 1544 route_path('repo_group_home', repo_group_name=group.group_name),
1544 1545 title='last change:{}'.format(format_date(group.last_commit_change)))
1545 1546 for group in repo_group.parents
1546 1547 ] + [
1547 1548 link_to(repo_group.name,
1548 1549 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1549 1550 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1550 1551 ]
1551 1552
1552 1553 return literal(' &raquo; '.join(path))
1553 1554
1554 1555
1555 1556 def format_byte_size_binary(file_size):
1556 1557 """
1557 1558 Formats file/folder sizes to standard.
1558 1559 """
1559 1560 if file_size is None:
1560 1561 file_size = 0
1561 1562
1562 1563 formatted_size = format_byte_size(file_size, binary=True)
1563 1564 return formatted_size
1564 1565
1565 1566
1566 1567 def urlify_text(text_, safe=True):
1567 1568 """
1568 1569 Extrac urls from text and make html links out of them
1569 1570
1570 1571 :param text_:
1571 1572 """
1572 1573
1573 1574 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1574 1575 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1575 1576
1576 1577 def url_func(match_obj):
1577 1578 url_full = match_obj.groups()[0]
1578 1579 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1579 1580 _newtext = url_pat.sub(url_func, text_)
1580 1581 if safe:
1581 1582 return literal(_newtext)
1582 1583 return _newtext
1583 1584
1584 1585
1585 1586 def urlify_commits(text_, repository):
1586 1587 """
1587 1588 Extract commit ids from text and make link from them
1588 1589
1589 1590 :param text_:
1590 1591 :param repository: repo name to build the URL with
1591 1592 """
1592 1593
1593 1594 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1594 1595
1595 1596 def url_func(match_obj):
1596 1597 commit_id = match_obj.groups()[1]
1597 1598 pref = match_obj.groups()[0]
1598 1599 suf = match_obj.groups()[2]
1599 1600
1600 1601 tmpl = (
1601 1602 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1602 1603 '%(commit_id)s</a>%(suf)s'
1603 1604 )
1604 1605 return tmpl % {
1605 1606 'pref': pref,
1606 1607 'cls': 'revision-link',
1607 1608 'url': route_url('repo_commit', repo_name=repository, commit_id=commit_id),
1608 1609 'commit_id': commit_id,
1609 1610 'suf': suf
1610 1611 }
1611 1612
1612 1613 newtext = URL_PAT.sub(url_func, text_)
1613 1614
1614 1615 return newtext
1615 1616
1616 1617
1617 1618 def _process_url_func(match_obj, repo_name, uid, entry,
1618 1619 return_raw_data=False, link_format='html'):
1619 1620 pref = ''
1620 1621 if match_obj.group().startswith(' '):
1621 1622 pref = ' '
1622 1623
1623 1624 issue_id = ''.join(match_obj.groups())
1624 1625
1625 1626 if link_format == 'html':
1626 1627 tmpl = (
1627 1628 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1628 1629 '%(issue-prefix)s%(id-repr)s'
1629 1630 '</a>')
1630 1631 elif link_format == 'html+hovercard':
1631 1632 tmpl = (
1632 1633 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1633 1634 '%(issue-prefix)s%(id-repr)s'
1634 1635 '</a>')
1635 elif link_format == 'rst':
1636 elif link_format in ['rst', 'rst+hovercard']:
1636 1637 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1637 elif link_format == 'markdown':
1638 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1638 elif link_format in ['markdown', 'markdown+hovercard']:
1639 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1639 1640 else:
1640 1641 raise ValueError('Bad link_format:{}'.format(link_format))
1641 1642
1642 1643 (repo_name_cleaned,
1643 1644 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1644 1645
1645 1646 # variables replacement
1646 1647 named_vars = {
1647 1648 'id': issue_id,
1648 1649 'repo': repo_name,
1649 1650 'repo_name': repo_name_cleaned,
1650 1651 'group_name': parent_group_name,
1652 # set dummy keys so we always have them
1653 'hostname': '',
1654 'netloc': '',
1655 'scheme': ''
1651 1656 }
1657
1658 request = get_current_request()
1659 if request:
1660 # exposes, hostname, netloc, scheme
1661 host_data = get_host_info(request)
1662 named_vars.update(host_data)
1663
1652 1664 # named regex variables
1653 1665 named_vars.update(match_obj.groupdict())
1654 1666 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1655 1667 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1668 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1656 1669
1657 1670 def quote_cleaner(input_str):
1658 1671 """Remove quotes as it's HTML"""
1659 1672 return input_str.replace('"', '')
1660 1673
1661 1674 data = {
1662 1675 'pref': pref,
1663 1676 'cls': quote_cleaner('issue-tracker-link'),
1664 1677 'url': quote_cleaner(_url),
1665 1678 'id-repr': issue_id,
1666 1679 'issue-prefix': entry['pref'],
1667 1680 'serv': entry['url'],
1668 1681 'title': desc,
1669 'hovercard_url': ''
1682 'hovercard_url': hovercard_url
1670 1683 }
1684
1671 1685 if return_raw_data:
1672 1686 return {
1673 1687 'id': issue_id,
1674 1688 'url': _url
1675 1689 }
1676 1690 return tmpl % data
1677 1691
1678 1692
1679 1693 def get_active_pattern_entries(repo_name):
1680 1694 repo = None
1681 1695 if repo_name:
1682 1696 # Retrieving repo_name to avoid invalid repo_name to explode on
1683 1697 # IssueTrackerSettingsModel but still passing invalid name further down
1684 1698 repo = Repository.get_by_repo_name(repo_name, cache=True)
1685 1699
1686 1700 settings_model = IssueTrackerSettingsModel(repo=repo)
1687 1701 active_entries = settings_model.get_settings(cache=True)
1688 1702 return active_entries
1689 1703
1690 1704
1691 1705 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1692 1706
1693 allowed_formats = ['html', 'rst', 'markdown']
1707 allowed_formats = ['html', 'rst', 'markdown',
1708 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1694 1709 if link_format not in allowed_formats:
1695 1710 raise ValueError('Link format can be only one of:{} got {}'.format(
1696 1711 allowed_formats, link_format))
1697 1712
1698 1713 active_entries = active_entries or get_active_pattern_entries(repo_name)
1699 1714 issues_data = []
1700 1715 new_text = text_string
1701 1716
1702 1717 log.debug('Got %s entries to process', len(active_entries))
1703 1718 for uid, entry in active_entries.items():
1704 1719 log.debug('found issue tracker entry with uid %s', uid)
1705 1720
1706 1721 if not (entry['pat'] and entry['url']):
1707 1722 log.debug('skipping due to missing data')
1708 1723 continue
1709 1724
1710 1725 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1711 1726 uid, entry['pat'], entry['url'], entry['pref'])
1712 1727
1713 1728 try:
1714 1729 pattern = re.compile(r'%s' % entry['pat'])
1715 1730 except re.error:
1716 1731 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1717 1732 continue
1718 1733
1719 1734 data_func = partial(
1720 1735 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1721 1736 return_raw_data=True)
1722 1737
1723 1738 for match_obj in pattern.finditer(text_string):
1724 1739 issues_data.append(data_func(match_obj))
1725 1740
1726 1741 url_func = partial(
1727 1742 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1728 1743 link_format=link_format)
1729 1744
1730 1745 new_text = pattern.sub(url_func, new_text)
1731 1746 log.debug('processed prefix:uid `%s`', uid)
1732 1747
1733 1748 # finally use global replace, eg !123 -> pr-link, those will not catch
1734 1749 # if already similar pattern exists
1750 server_url = '${scheme}://${netloc}'
1735 1751 pr_entry = {
1736 1752 'pref': '!',
1737 'url': '/_admin/pull-requests/${id}',
1738 'desc': 'Pull Request !${id}'
1753 'url': server_url + '/_admin/pull-requests/${id}',
1754 'desc': 'Pull Request !${id}',
1755 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1739 1756 }
1740 1757 pr_url_func = partial(
1741 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None)
1758 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1759 link_format=link_format+'+hovercard')
1742 1760 new_text = re.compile(r'(?:(?:^!)|(?: !))(\d+)').sub(pr_url_func, new_text)
1743 1761 log.debug('processed !pr pattern')
1744 1762
1745 1763 return new_text, issues_data
1746 1764
1747 1765
1748 1766 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1749 1767 """
1750 1768 Parses given text message and makes proper links.
1751 1769 issues are linked to given issue-server, and rest is a commit link
1752 1770
1753 1771 :param commit_text:
1754 1772 :param repository:
1755 1773 """
1756 1774 def escaper(_text):
1757 1775 return _text.replace('<', '&lt;').replace('>', '&gt;')
1758 1776
1759 1777 new_text = escaper(commit_text)
1760 1778
1761 1779 # extract http/https links and make them real urls
1762 1780 new_text = urlify_text(new_text, safe=False)
1763 1781
1764 1782 # urlify commits - extract commit ids and make link out of them, if we have
1765 1783 # the scope of repository present.
1766 1784 if repository:
1767 1785 new_text = urlify_commits(new_text, repository)
1768 1786
1769 1787 # process issue tracker patterns
1770 1788 new_text, issues = process_patterns(new_text, repository or '',
1771 1789 active_entries=active_pattern_entries)
1772 1790
1773 1791 return literal(new_text)
1774 1792
1775 1793
1776 1794 def render_binary(repo_name, file_obj):
1777 1795 """
1778 1796 Choose how to render a binary file
1779 1797 """
1780 1798
1781 1799 filename = file_obj.name
1782 1800
1783 1801 # images
1784 1802 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1785 1803 if fnmatch.fnmatch(filename, pat=ext):
1786 1804 alt = escape(filename)
1787 1805 src = route_path(
1788 1806 'repo_file_raw', repo_name=repo_name,
1789 1807 commit_id=file_obj.commit.raw_id,
1790 1808 f_path=file_obj.path)
1791 1809 return literal(
1792 1810 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1793 1811
1794 1812
1795 1813 def renderer_from_filename(filename, exclude=None):
1796 1814 """
1797 1815 choose a renderer based on filename, this works only for text based files
1798 1816 """
1799 1817
1800 1818 # ipython
1801 1819 for ext in ['*.ipynb']:
1802 1820 if fnmatch.fnmatch(filename, pat=ext):
1803 1821 return 'jupyter'
1804 1822
1805 1823 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1806 1824 if is_markup:
1807 1825 return is_markup
1808 1826 return None
1809 1827
1810 1828
1811 1829 def render(source, renderer='rst', mentions=False, relative_urls=None,
1812 1830 repo_name=None):
1813 1831
1814 1832 def maybe_convert_relative_links(html_source):
1815 1833 if relative_urls:
1816 1834 return relative_links(html_source, relative_urls)
1817 1835 return html_source
1818 1836
1819 1837 if renderer == 'plain':
1820 1838 return literal(
1821 1839 MarkupRenderer.plain(source, leading_newline=False))
1822 1840
1823 1841 elif renderer == 'rst':
1824 1842 if repo_name:
1825 1843 # process patterns on comments if we pass in repo name
1826 1844 source, issues = process_patterns(
1827 1845 source, repo_name, link_format='rst')
1828 1846
1829 1847 return literal(
1830 1848 '<div class="rst-block">%s</div>' %
1831 1849 maybe_convert_relative_links(
1832 1850 MarkupRenderer.rst(source, mentions=mentions)))
1833 1851
1834 1852 elif renderer == 'markdown':
1835 1853 if repo_name:
1836 1854 # process patterns on comments if we pass in repo name
1837 1855 source, issues = process_patterns(
1838 1856 source, repo_name, link_format='markdown')
1839 1857
1840 1858 return literal(
1841 1859 '<div class="markdown-block">%s</div>' %
1842 1860 maybe_convert_relative_links(
1843 1861 MarkupRenderer.markdown(source, flavored=True,
1844 1862 mentions=mentions)))
1845 1863
1846 1864 elif renderer == 'jupyter':
1847 1865 return literal(
1848 1866 '<div class="ipynb">%s</div>' %
1849 1867 maybe_convert_relative_links(
1850 1868 MarkupRenderer.jupyter(source)))
1851 1869
1852 1870 # None means just show the file-source
1853 1871 return None
1854 1872
1855 1873
1856 1874 def commit_status(repo, commit_id):
1857 1875 return ChangesetStatusModel().get_status(repo, commit_id)
1858 1876
1859 1877
1860 1878 def commit_status_lbl(commit_status):
1861 1879 return dict(ChangesetStatus.STATUSES).get(commit_status)
1862 1880
1863 1881
1864 1882 def commit_time(repo_name, commit_id):
1865 1883 repo = Repository.get_by_repo_name(repo_name)
1866 1884 commit = repo.get_commit(commit_id=commit_id)
1867 1885 return commit.date
1868 1886
1869 1887
1870 1888 def get_permission_name(key):
1871 1889 return dict(Permission.PERMS).get(key)
1872 1890
1873 1891
1874 1892 def journal_filter_help(request):
1875 1893 _ = request.translate
1876 1894 from rhodecode.lib.audit_logger import ACTIONS
1877 1895 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1878 1896
1879 1897 return _(
1880 1898 'Example filter terms:\n' +
1881 1899 ' repository:vcs\n' +
1882 1900 ' username:marcin\n' +
1883 1901 ' username:(NOT marcin)\n' +
1884 1902 ' action:*push*\n' +
1885 1903 ' ip:127.0.0.1\n' +
1886 1904 ' date:20120101\n' +
1887 1905 ' date:[20120101100000 TO 20120102]\n' +
1888 1906 '\n' +
1889 1907 'Actions: {actions}\n' +
1890 1908 '\n' +
1891 1909 'Generate wildcards using \'*\' character:\n' +
1892 1910 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1893 1911 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1894 1912 '\n' +
1895 1913 'Optional AND / OR operators in queries\n' +
1896 1914 ' "repository:vcs OR repository:test"\n' +
1897 1915 ' "username:test AND repository:test*"\n'
1898 1916 ).format(actions=actions)
1899 1917
1900 1918
1901 1919 def not_mapped_error(repo_name):
1902 1920 from rhodecode.translation import _
1903 1921 flash(_('%s repository is not mapped to db perhaps'
1904 1922 ' it was created or renamed from the filesystem'
1905 1923 ' please run the application again'
1906 1924 ' in order to rescan repositories') % repo_name, category='error')
1907 1925
1908 1926
1909 1927 def ip_range(ip_addr):
1910 1928 from rhodecode.model.db import UserIpMap
1911 1929 s, e = UserIpMap._get_ip_range(ip_addr)
1912 1930 return '%s - %s' % (s, e)
1913 1931
1914 1932
1915 1933 def form(url, method='post', needs_csrf_token=True, **attrs):
1916 1934 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1917 1935 if method.lower() != 'get' and needs_csrf_token:
1918 1936 raise Exception(
1919 1937 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1920 1938 'CSRF token. If the endpoint does not require such token you can ' +
1921 1939 'explicitly set the parameter needs_csrf_token to false.')
1922 1940
1923 1941 return wh_form(url, method=method, **attrs)
1924 1942
1925 1943
1926 1944 def secure_form(form_url, method="POST", multipart=False, **attrs):
1927 1945 """Start a form tag that points the action to an url. This
1928 1946 form tag will also include the hidden field containing
1929 1947 the auth token.
1930 1948
1931 1949 The url options should be given either as a string, or as a
1932 1950 ``url()`` function. The method for the form defaults to POST.
1933 1951
1934 1952 Options:
1935 1953
1936 1954 ``multipart``
1937 1955 If set to True, the enctype is set to "multipart/form-data".
1938 1956 ``method``
1939 1957 The method to use when submitting the form, usually either
1940 1958 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1941 1959 hidden input with name _method is added to simulate the verb
1942 1960 over POST.
1943 1961
1944 1962 """
1945 1963 from webhelpers.pylonslib.secure_form import insecure_form
1946 1964
1947 1965 if 'request' in attrs:
1948 1966 session = attrs['request'].session
1949 1967 del attrs['request']
1950 1968 else:
1951 1969 raise ValueError(
1952 1970 'Calling this form requires request= to be passed as argument')
1953 1971
1954 1972 form = insecure_form(form_url, method, multipart, **attrs)
1955 1973 token = literal(
1956 1974 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1957 1975 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1958 1976
1959 1977 return literal("%s\n%s" % (form, token))
1960 1978
1961 1979
1962 1980 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1963 1981 select_html = select(name, selected, options, **attrs)
1964 1982
1965 1983 select2 = """
1966 1984 <script>
1967 1985 $(document).ready(function() {
1968 1986 $('#%s').select2({
1969 1987 containerCssClass: 'drop-menu %s',
1970 1988 dropdownCssClass: 'drop-menu-dropdown',
1971 1989 dropdownAutoWidth: true%s
1972 1990 });
1973 1991 });
1974 1992 </script>
1975 1993 """
1976 1994
1977 1995 filter_option = """,
1978 1996 minimumResultsForSearch: -1
1979 1997 """
1980 1998 input_id = attrs.get('id') or name
1981 1999 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1982 2000 filter_enabled = "" if enable_filter else filter_option
1983 2001 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1984 2002
1985 2003 return literal(select_html+select_script)
1986 2004
1987 2005
1988 2006 def get_visual_attr(tmpl_context_var, attr_name):
1989 2007 """
1990 2008 A safe way to get a variable from visual variable of template context
1991 2009
1992 2010 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1993 2011 :param attr_name: name of the attribute we fetch from the c.visual
1994 2012 """
1995 2013 visual = getattr(tmpl_context_var, 'visual', None)
1996 2014 if not visual:
1997 2015 return
1998 2016 else:
1999 2017 return getattr(visual, attr_name, None)
2000 2018
2001 2019
2002 2020 def get_last_path_part(file_node):
2003 2021 if not file_node.path:
2004 2022 return u'/'
2005 2023
2006 2024 path = safe_unicode(file_node.path.split('/')[-1])
2007 2025 return u'../' + path
2008 2026
2009 2027
2010 2028 def route_url(*args, **kwargs):
2011 2029 """
2012 Wrapper around pyramids `route_url` (fully qualified url) function.
2030 Wrapper around pyramids `route_url` (fully qualified url) function.
2013 2031 """
2014 2032 req = get_current_request()
2015 2033 return req.route_url(*args, **kwargs)
2016 2034
2017 2035
2018 2036 def route_path(*args, **kwargs):
2019 2037 """
2020 2038 Wrapper around pyramids `route_path` function.
2021 2039 """
2022 2040 req = get_current_request()
2023 2041 return req.route_path(*args, **kwargs)
2024 2042
2025 2043
2026 2044 def route_path_or_none(*args, **kwargs):
2027 2045 try:
2028 2046 return route_path(*args, **kwargs)
2029 2047 except KeyError:
2030 2048 return None
2031 2049
2032 2050
2033 2051 def current_route_path(request, **kw):
2034 2052 new_args = request.GET.mixed()
2035 2053 new_args.update(kw)
2036 2054 return request.current_route_path(_query=new_args)
2037 2055
2038 2056
2039 2057 def curl_api_example(method, args):
2040 2058 args_json = json.dumps(OrderedDict([
2041 2059 ('id', 1),
2042 2060 ('auth_token', 'SECRET'),
2043 2061 ('method', method),
2044 2062 ('args', args)
2045 2063 ]))
2046 2064
2047 2065 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2048 2066 api_url=route_url('apiv2'),
2049 2067 args_json=args_json
2050 2068 )
2051 2069
2052 2070
2053 2071 def api_call_example(method, args):
2054 2072 """
2055 2073 Generates an API call example via CURL
2056 2074 """
2057 2075 curl_call = curl_api_example(method, args)
2058 2076
2059 2077 return literal(
2060 2078 curl_call +
2061 2079 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2062 2080 "and needs to be of `api calls` role."
2063 2081 .format(token_url=route_url('my_account_auth_tokens')))
2064 2082
2065 2083
2066 2084 def notification_description(notification, request):
2067 2085 """
2068 2086 Generate notification human readable description based on notification type
2069 2087 """
2070 2088 from rhodecode.model.notification import NotificationModel
2071 2089 return NotificationModel().make_description(
2072 2090 notification, translate=request.translate)
2073 2091
2074 2092
2075 2093 def go_import_header(request, db_repo=None):
2076 2094 """
2077 2095 Creates a header for go-import functionality in Go Lang
2078 2096 """
2079 2097
2080 2098 if not db_repo:
2081 2099 return
2082 2100 if 'go-get' not in request.GET:
2083 2101 return
2084 2102
2085 2103 clone_url = db_repo.clone_url()
2086 2104 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2087 2105 # we have a repo and go-get flag,
2088 2106 return literal('<meta name="go-import" content="{} {} {}">'.format(
2089 2107 prefix, db_repo.repo_type, clone_url))
2090 2108
2091 2109
2092 2110 def reviewer_as_json(*args, **kwargs):
2093 2111 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2094 2112 return _reviewer_as_json(*args, **kwargs)
2095 2113
2096 2114
2097 2115 def get_repo_view_type(request):
2098 2116 route_name = request.matched_route.name
2099 2117 route_to_view_type = {
2100 2118 'repo_changelog': 'commits',
2101 2119 'repo_commits': 'commits',
2102 2120 'repo_files': 'files',
2103 2121 'repo_summary': 'summary',
2104 2122 'repo_commit': 'commit'
2105 2123 }
2106 2124
2107 2125 return route_to_view_type.get(route_name)
@@ -1,1070 +1,1090 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-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 """
23 23 Some simple helper functions
24 24 """
25 25
26 26 import collections
27 27 import datetime
28 28 import dateutil.relativedelta
29 29 import hashlib
30 30 import logging
31 31 import re
32 32 import sys
33 33 import time
34 34 import urllib
35 35 import urlobject
36 36 import uuid
37 37 import getpass
38 38 from functools import update_wrapper, partial
39 39
40 40 import pygments.lexers
41 41 import sqlalchemy
42 42 import sqlalchemy.engine.url
43 43 import sqlalchemy.exc
44 44 import sqlalchemy.sql
45 45 import webob
46 46 import pyramid.threadlocal
47 47 from pyramid import compat
48 48 from pyramid.settings import asbool
49 49
50 50 import rhodecode
51 51 from rhodecode.translation import _, _pluralize
52 52
53 53
54 54 def md5(s):
55 55 return hashlib.md5(s).hexdigest()
56 56
57 57
58 58 def md5_safe(s):
59 59 return md5(safe_str(s))
60 60
61 61
62 62 def sha1(s):
63 63 return hashlib.sha1(s).hexdigest()
64 64
65 65
66 66 def sha1_safe(s):
67 67 return sha1(safe_str(s))
68 68
69 69
70 70 def __get_lem(extra_mapping=None):
71 71 """
72 72 Get language extension map based on what's inside pygments lexers
73 73 """
74 74 d = collections.defaultdict(lambda: [])
75 75
76 76 def __clean(s):
77 77 s = s.lstrip('*')
78 78 s = s.lstrip('.')
79 79
80 80 if s.find('[') != -1:
81 81 exts = []
82 82 start, stop = s.find('['), s.find(']')
83 83
84 84 for suffix in s[start + 1:stop]:
85 85 exts.append(s[:s.find('[')] + suffix)
86 86 return [e.lower() for e in exts]
87 87 else:
88 88 return [s.lower()]
89 89
90 90 for lx, t in sorted(pygments.lexers.LEXERS.items()):
91 91 m = map(__clean, t[-2])
92 92 if m:
93 93 m = reduce(lambda x, y: x + y, m)
94 94 for ext in m:
95 95 desc = lx.replace('Lexer', '')
96 96 d[ext].append(desc)
97 97
98 98 data = dict(d)
99 99
100 100 extra_mapping = extra_mapping or {}
101 101 if extra_mapping:
102 102 for k, v in extra_mapping.items():
103 103 if k not in data:
104 104 # register new mapping2lexer
105 105 data[k] = [v]
106 106
107 107 return data
108 108
109 109
110 110 def str2bool(_str):
111 111 """
112 112 returns True/False value from given string, it tries to translate the
113 113 string into boolean
114 114
115 115 :param _str: string value to translate into boolean
116 116 :rtype: boolean
117 117 :returns: boolean from given string
118 118 """
119 119 if _str is None:
120 120 return False
121 121 if _str in (True, False):
122 122 return _str
123 123 _str = str(_str).strip().lower()
124 124 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
125 125
126 126
127 127 def aslist(obj, sep=None, strip=True):
128 128 """
129 129 Returns given string separated by sep as list
130 130
131 131 :param obj:
132 132 :param sep:
133 133 :param strip:
134 134 """
135 135 if isinstance(obj, (basestring,)):
136 136 lst = obj.split(sep)
137 137 if strip:
138 138 lst = [v.strip() for v in lst]
139 139 return lst
140 140 elif isinstance(obj, (list, tuple)):
141 141 return obj
142 142 elif obj is None:
143 143 return []
144 144 else:
145 145 return [obj]
146 146
147 147
148 148 def convert_line_endings(line, mode):
149 149 """
150 150 Converts a given line "line end" accordingly to given mode
151 151
152 152 Available modes are::
153 153 0 - Unix
154 154 1 - Mac
155 155 2 - DOS
156 156
157 157 :param line: given line to convert
158 158 :param mode: mode to convert to
159 159 :rtype: str
160 160 :return: converted line according to mode
161 161 """
162 162 if mode == 0:
163 163 line = line.replace('\r\n', '\n')
164 164 line = line.replace('\r', '\n')
165 165 elif mode == 1:
166 166 line = line.replace('\r\n', '\r')
167 167 line = line.replace('\n', '\r')
168 168 elif mode == 2:
169 169 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
170 170 return line
171 171
172 172
173 173 def detect_mode(line, default):
174 174 """
175 175 Detects line break for given line, if line break couldn't be found
176 176 given default value is returned
177 177
178 178 :param line: str line
179 179 :param default: default
180 180 :rtype: int
181 181 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
182 182 """
183 183 if line.endswith('\r\n'):
184 184 return 2
185 185 elif line.endswith('\n'):
186 186 return 0
187 187 elif line.endswith('\r'):
188 188 return 1
189 189 else:
190 190 return default
191 191
192 192
193 193 def safe_int(val, default=None):
194 194 """
195 195 Returns int() of val if val is not convertable to int use default
196 196 instead
197 197
198 198 :param val:
199 199 :param default:
200 200 """
201 201
202 202 try:
203 203 val = int(val)
204 204 except (ValueError, TypeError):
205 205 val = default
206 206
207 207 return val
208 208
209 209
210 210 def safe_unicode(str_, from_encoding=None):
211 211 """
212 212 safe unicode function. Does few trick to turn str_ into unicode
213 213
214 214 In case of UnicodeDecode error, we try to return it with encoding detected
215 215 by chardet library if it fails fallback to unicode with errors replaced
216 216
217 217 :param str_: string to decode
218 218 :rtype: unicode
219 219 :returns: unicode object
220 220 """
221 221 if isinstance(str_, unicode):
222 222 return str_
223 223
224 224 if not from_encoding:
225 225 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
226 226 'utf8'), sep=',')
227 227 from_encoding = DEFAULT_ENCODINGS
228 228
229 229 if not isinstance(from_encoding, (list, tuple)):
230 230 from_encoding = [from_encoding]
231 231
232 232 try:
233 233 return unicode(str_)
234 234 except UnicodeDecodeError:
235 235 pass
236 236
237 237 for enc in from_encoding:
238 238 try:
239 239 return unicode(str_, enc)
240 240 except UnicodeDecodeError:
241 241 pass
242 242
243 243 try:
244 244 import chardet
245 245 encoding = chardet.detect(str_)['encoding']
246 246 if encoding is None:
247 247 raise Exception()
248 248 return str_.decode(encoding)
249 249 except (ImportError, UnicodeDecodeError, Exception):
250 250 return unicode(str_, from_encoding[0], 'replace')
251 251
252 252
253 253 def safe_str(unicode_, to_encoding=None):
254 254 """
255 255 safe str function. Does few trick to turn unicode_ into string
256 256
257 257 In case of UnicodeEncodeError, we try to return it with encoding detected
258 258 by chardet library if it fails fallback to string with errors replaced
259 259
260 260 :param unicode_: unicode to encode
261 261 :rtype: str
262 262 :returns: str object
263 263 """
264 264
265 265 # if it's not basestr cast to str
266 266 if not isinstance(unicode_, compat.string_types):
267 267 return str(unicode_)
268 268
269 269 if isinstance(unicode_, str):
270 270 return unicode_
271 271
272 272 if not to_encoding:
273 273 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
274 274 'utf8'), sep=',')
275 275 to_encoding = DEFAULT_ENCODINGS
276 276
277 277 if not isinstance(to_encoding, (list, tuple)):
278 278 to_encoding = [to_encoding]
279 279
280 280 for enc in to_encoding:
281 281 try:
282 282 return unicode_.encode(enc)
283 283 except UnicodeEncodeError:
284 284 pass
285 285
286 286 try:
287 287 import chardet
288 288 encoding = chardet.detect(unicode_)['encoding']
289 289 if encoding is None:
290 290 raise UnicodeEncodeError()
291 291
292 292 return unicode_.encode(encoding)
293 293 except (ImportError, UnicodeEncodeError):
294 294 return unicode_.encode(to_encoding[0], 'replace')
295 295
296 296
297 297 def remove_suffix(s, suffix):
298 298 if s.endswith(suffix):
299 299 s = s[:-1 * len(suffix)]
300 300 return s
301 301
302 302
303 303 def remove_prefix(s, prefix):
304 304 if s.startswith(prefix):
305 305 s = s[len(prefix):]
306 306 return s
307 307
308 308
309 309 def find_calling_context(ignore_modules=None):
310 310 """
311 311 Look through the calling stack and return the frame which called
312 312 this function and is part of core module ( ie. rhodecode.* )
313 313
314 314 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
315 315 """
316 316
317 317 ignore_modules = ignore_modules or []
318 318
319 319 f = sys._getframe(2)
320 320 while f.f_back is not None:
321 321 name = f.f_globals.get('__name__')
322 322 if name and name.startswith(__name__.split('.')[0]):
323 323 if name not in ignore_modules:
324 324 return f
325 325 f = f.f_back
326 326 return None
327 327
328 328
329 329 def ping_connection(connection, branch):
330 330 if branch:
331 331 # "branch" refers to a sub-connection of a connection,
332 332 # we don't want to bother pinging on these.
333 333 return
334 334
335 335 # turn off "close with result". This flag is only used with
336 336 # "connectionless" execution, otherwise will be False in any case
337 337 save_should_close_with_result = connection.should_close_with_result
338 338 connection.should_close_with_result = False
339 339
340 340 try:
341 341 # run a SELECT 1. use a core select() so that
342 342 # the SELECT of a scalar value without a table is
343 343 # appropriately formatted for the backend
344 344 connection.scalar(sqlalchemy.sql.select([1]))
345 345 except sqlalchemy.exc.DBAPIError as err:
346 346 # catch SQLAlchemy's DBAPIError, which is a wrapper
347 347 # for the DBAPI's exception. It includes a .connection_invalidated
348 348 # attribute which specifies if this connection is a "disconnect"
349 349 # condition, which is based on inspection of the original exception
350 350 # by the dialect in use.
351 351 if err.connection_invalidated:
352 352 # run the same SELECT again - the connection will re-validate
353 353 # itself and establish a new connection. The disconnect detection
354 354 # here also causes the whole connection pool to be invalidated
355 355 # so that all stale connections are discarded.
356 356 connection.scalar(sqlalchemy.sql.select([1]))
357 357 else:
358 358 raise
359 359 finally:
360 360 # restore "close with result"
361 361 connection.should_close_with_result = save_should_close_with_result
362 362
363 363
364 364 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
365 365 """Custom engine_from_config functions."""
366 366 log = logging.getLogger('sqlalchemy.engine')
367 367 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
368 368 debug = asbool(configuration.get('debug'))
369 369
370 370 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
371 371
372 372 def color_sql(sql):
373 373 color_seq = '\033[1;33m' # This is yellow: code 33
374 374 normal = '\x1b[0m'
375 375 return ''.join([color_seq, sql, normal])
376 376
377 377 if use_ping_connection:
378 378 log.debug('Adding ping_connection on the engine config.')
379 379 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
380 380
381 381 if debug:
382 382 # attach events only for debug configuration
383 383 def before_cursor_execute(conn, cursor, statement,
384 384 parameters, context, executemany):
385 385 setattr(conn, 'query_start_time', time.time())
386 386 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
387 387 calling_context = find_calling_context(ignore_modules=[
388 388 'rhodecode.lib.caching_query',
389 389 'rhodecode.model.settings',
390 390 ])
391 391 if calling_context:
392 392 log.info(color_sql('call context %s:%s' % (
393 393 calling_context.f_code.co_filename,
394 394 calling_context.f_lineno,
395 395 )))
396 396
397 397 def after_cursor_execute(conn, cursor, statement,
398 398 parameters, context, executemany):
399 399 delattr(conn, 'query_start_time')
400 400
401 401 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
402 402 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
403 403
404 404 return engine
405 405
406 406
407 407 def get_encryption_key(config):
408 408 secret = config.get('rhodecode.encrypted_values.secret')
409 409 default = config['beaker.session.secret']
410 410 return secret or default
411 411
412 412
413 413 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
414 414 short_format=False):
415 415 """
416 416 Turns a datetime into an age string.
417 417 If show_short_version is True, this generates a shorter string with
418 418 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
419 419
420 420 * IMPORTANT*
421 421 Code of this function is written in special way so it's easier to
422 422 backport it to javascript. If you mean to update it, please also update
423 423 `jquery.timeago-extension.js` file
424 424
425 425 :param prevdate: datetime object
426 426 :param now: get current time, if not define we use
427 427 `datetime.datetime.now()`
428 428 :param show_short_version: if it should approximate the date and
429 429 return a shorter string
430 430 :param show_suffix:
431 431 :param short_format: show short format, eg 2D instead of 2 days
432 432 :rtype: unicode
433 433 :returns: unicode words describing age
434 434 """
435 435
436 436 def _get_relative_delta(now, prevdate):
437 437 base = dateutil.relativedelta.relativedelta(now, prevdate)
438 438 return {
439 439 'year': base.years,
440 440 'month': base.months,
441 441 'day': base.days,
442 442 'hour': base.hours,
443 443 'minute': base.minutes,
444 444 'second': base.seconds,
445 445 }
446 446
447 447 def _is_leap_year(year):
448 448 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
449 449
450 450 def get_month(prevdate):
451 451 return prevdate.month
452 452
453 453 def get_year(prevdate):
454 454 return prevdate.year
455 455
456 456 now = now or datetime.datetime.now()
457 457 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
458 458 deltas = {}
459 459 future = False
460 460
461 461 if prevdate > now:
462 462 now_old = now
463 463 now = prevdate
464 464 prevdate = now_old
465 465 future = True
466 466 if future:
467 467 prevdate = prevdate.replace(microsecond=0)
468 468 # Get date parts deltas
469 469 for part in order:
470 470 rel_delta = _get_relative_delta(now, prevdate)
471 471 deltas[part] = rel_delta[part]
472 472
473 473 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
474 474 # not 1 hour, -59 minutes and -59 seconds)
475 475 offsets = [[5, 60], [4, 60], [3, 24]]
476 476 for element in offsets: # seconds, minutes, hours
477 477 num = element[0]
478 478 length = element[1]
479 479
480 480 part = order[num]
481 481 carry_part = order[num - 1]
482 482
483 483 if deltas[part] < 0:
484 484 deltas[part] += length
485 485 deltas[carry_part] -= 1
486 486
487 487 # Same thing for days except that the increment depends on the (variable)
488 488 # number of days in the month
489 489 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
490 490 if deltas['day'] < 0:
491 491 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
492 492 deltas['day'] += 29
493 493 else:
494 494 deltas['day'] += month_lengths[get_month(prevdate) - 1]
495 495
496 496 deltas['month'] -= 1
497 497
498 498 if deltas['month'] < 0:
499 499 deltas['month'] += 12
500 500 deltas['year'] -= 1
501 501
502 502 # Format the result
503 503 if short_format:
504 504 fmt_funcs = {
505 505 'year': lambda d: u'%dy' % d,
506 506 'month': lambda d: u'%dm' % d,
507 507 'day': lambda d: u'%dd' % d,
508 508 'hour': lambda d: u'%dh' % d,
509 509 'minute': lambda d: u'%dmin' % d,
510 510 'second': lambda d: u'%dsec' % d,
511 511 }
512 512 else:
513 513 fmt_funcs = {
514 514 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
515 515 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
516 516 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
517 517 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
518 518 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
519 519 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
520 520 }
521 521
522 522 i = 0
523 523 for part in order:
524 524 value = deltas[part]
525 525 if value != 0:
526 526
527 527 if i < 5:
528 528 sub_part = order[i + 1]
529 529 sub_value = deltas[sub_part]
530 530 else:
531 531 sub_value = 0
532 532
533 533 if sub_value == 0 or show_short_version:
534 534 _val = fmt_funcs[part](value)
535 535 if future:
536 536 if show_suffix:
537 537 return _(u'in ${ago}', mapping={'ago': _val})
538 538 else:
539 539 return _(_val)
540 540
541 541 else:
542 542 if show_suffix:
543 543 return _(u'${ago} ago', mapping={'ago': _val})
544 544 else:
545 545 return _(_val)
546 546
547 547 val = fmt_funcs[part](value)
548 548 val_detail = fmt_funcs[sub_part](sub_value)
549 549 mapping = {'val': val, 'detail': val_detail}
550 550
551 551 if short_format:
552 552 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
553 553 if show_suffix:
554 554 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
555 555 if future:
556 556 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
557 557 else:
558 558 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
559 559 if show_suffix:
560 560 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
561 561 if future:
562 562 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
563 563
564 564 return datetime_tmpl
565 565 i += 1
566 566 return _(u'just now')
567 567
568 568
569 569 def age_from_seconds(seconds):
570 570 seconds = safe_int(seconds) or 0
571 571 prevdate = time_to_datetime(time.time() + seconds)
572 572 return age(prevdate, show_suffix=False, show_short_version=True)
573 573
574 574
575 575 def cleaned_uri(uri):
576 576 """
577 577 Quotes '[' and ']' from uri if there is only one of them.
578 578 according to RFC3986 we cannot use such chars in uri
579 579 :param uri:
580 580 :return: uri without this chars
581 581 """
582 582 return urllib.quote(uri, safe='@$:/')
583 583
584 584
585 585 def uri_filter(uri):
586 586 """
587 587 Removes user:password from given url string
588 588
589 589 :param uri:
590 590 :rtype: unicode
591 591 :returns: filtered list of strings
592 592 """
593 593 if not uri:
594 594 return ''
595 595
596 596 proto = ''
597 597
598 598 for pat in ('https://', 'http://'):
599 599 if uri.startswith(pat):
600 600 uri = uri[len(pat):]
601 601 proto = pat
602 602 break
603 603
604 604 # remove passwords and username
605 605 uri = uri[uri.find('@') + 1:]
606 606
607 607 # get the port
608 608 cred_pos = uri.find(':')
609 609 if cred_pos == -1:
610 610 host, port = uri, None
611 611 else:
612 612 host, port = uri[:cred_pos], uri[cred_pos + 1:]
613 613
614 614 return filter(None, [proto, host, port])
615 615
616 616
617 617 def credentials_filter(uri):
618 618 """
619 619 Returns a url with removed credentials
620 620
621 621 :param uri:
622 622 """
623 623
624 624 uri = uri_filter(uri)
625 625 # check if we have port
626 626 if len(uri) > 2 and uri[2]:
627 627 uri[2] = ':' + uri[2]
628 628
629 629 return ''.join(uri)
630 630
631 631
632 def get_host_info(request):
633 """
634 Generate host info, to obtain full url e.g https://server.com
635 use this
636 `{scheme}://{netloc}`
637 """
638 if not request:
639 return {}
640
641 qualified_home_url = request.route_url('home')
642 parsed_url = urlobject.URLObject(qualified_home_url)
643 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
644
645 return {
646 'scheme': parsed_url.scheme,
647 'netloc': parsed_url.netloc+decoded_path,
648 'hostname': parsed_url.hostname,
649 }
650
651
632 652 def get_clone_url(request, uri_tmpl, repo_name, repo_id, **override):
633 qualifed_home_url = request.route_url('home')
634 parsed_url = urlobject.URLObject(qualifed_home_url)
653 qualified_home_url = request.route_url('home')
654 parsed_url = urlobject.URLObject(qualified_home_url)
635 655 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
636 656
637 657 args = {
638 658 'scheme': parsed_url.scheme,
639 659 'user': '',
640 660 'sys_user': getpass.getuser(),
641 661 # path if we use proxy-prefix
642 662 'netloc': parsed_url.netloc+decoded_path,
643 663 'hostname': parsed_url.hostname,
644 664 'prefix': decoded_path,
645 665 'repo': repo_name,
646 666 'repoid': str(repo_id)
647 667 }
648 668 args.update(override)
649 669 args['user'] = urllib.quote(safe_str(args['user']))
650 670
651 671 for k, v in args.items():
652 672 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
653 673
654 674 # remove leading @ sign if it's present. Case of empty user
655 675 url_obj = urlobject.URLObject(uri_tmpl)
656 676 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
657 677
658 678 return safe_unicode(url)
659 679
660 680
661 681 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
662 682 """
663 683 Safe version of get_commit if this commit doesn't exists for a
664 684 repository it returns a Dummy one instead
665 685
666 686 :param repo: repository instance
667 687 :param commit_id: commit id as str
668 688 :param pre_load: optional list of commit attributes to load
669 689 """
670 690 # TODO(skreft): remove these circular imports
671 691 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
672 692 from rhodecode.lib.vcs.exceptions import RepositoryError
673 693 if not isinstance(repo, BaseRepository):
674 694 raise Exception('You must pass an Repository '
675 695 'object as first argument got %s', type(repo))
676 696
677 697 try:
678 698 commit = repo.get_commit(
679 699 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
680 700 except (RepositoryError, LookupError):
681 701 commit = EmptyCommit()
682 702 return commit
683 703
684 704
685 705 def datetime_to_time(dt):
686 706 if dt:
687 707 return time.mktime(dt.timetuple())
688 708
689 709
690 710 def time_to_datetime(tm):
691 711 if tm:
692 712 if isinstance(tm, compat.string_types):
693 713 try:
694 714 tm = float(tm)
695 715 except ValueError:
696 716 return
697 717 return datetime.datetime.fromtimestamp(tm)
698 718
699 719
700 720 def time_to_utcdatetime(tm):
701 721 if tm:
702 722 if isinstance(tm, compat.string_types):
703 723 try:
704 724 tm = float(tm)
705 725 except ValueError:
706 726 return
707 727 return datetime.datetime.utcfromtimestamp(tm)
708 728
709 729
710 730 MENTIONS_REGEX = re.compile(
711 731 # ^@ or @ without any special chars in front
712 732 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
713 733 # main body starts with letter, then can be . - _
714 734 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
715 735 re.VERBOSE | re.MULTILINE)
716 736
717 737
718 738 def extract_mentioned_users(s):
719 739 """
720 740 Returns unique usernames from given string s that have @mention
721 741
722 742 :param s: string to get mentions
723 743 """
724 744 usrs = set()
725 745 for username in MENTIONS_REGEX.findall(s):
726 746 usrs.add(username)
727 747
728 748 return sorted(list(usrs), key=lambda k: k.lower())
729 749
730 750
731 751 class AttributeDictBase(dict):
732 752 def __getstate__(self):
733 753 odict = self.__dict__ # get attribute dictionary
734 754 return odict
735 755
736 756 def __setstate__(self, dict):
737 757 self.__dict__ = dict
738 758
739 759 __setattr__ = dict.__setitem__
740 760 __delattr__ = dict.__delitem__
741 761
742 762
743 763 class StrictAttributeDict(AttributeDictBase):
744 764 """
745 765 Strict Version of Attribute dict which raises an Attribute error when
746 766 requested attribute is not set
747 767 """
748 768 def __getattr__(self, attr):
749 769 try:
750 770 return self[attr]
751 771 except KeyError:
752 772 raise AttributeError('%s object has no attribute %s' % (
753 773 self.__class__, attr))
754 774
755 775
756 776 class AttributeDict(AttributeDictBase):
757 777 def __getattr__(self, attr):
758 778 return self.get(attr, None)
759 779
760 780
761 781
762 782 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
763 783 def __init__(self, default_factory=None, *args, **kwargs):
764 784 # in python3 you can omit the args to super
765 785 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
766 786 self.default_factory = default_factory
767 787
768 788
769 789 def fix_PATH(os_=None):
770 790 """
771 791 Get current active python path, and append it to PATH variable to fix
772 792 issues of subprocess calls and different python versions
773 793 """
774 794 if os_ is None:
775 795 import os
776 796 else:
777 797 os = os_
778 798
779 799 cur_path = os.path.split(sys.executable)[0]
780 800 if not os.environ['PATH'].startswith(cur_path):
781 801 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
782 802
783 803
784 804 def obfuscate_url_pw(engine):
785 805 _url = engine or ''
786 806 try:
787 807 _url = sqlalchemy.engine.url.make_url(engine)
788 808 if _url.password:
789 809 _url.password = 'XXXXX'
790 810 except Exception:
791 811 pass
792 812 return unicode(_url)
793 813
794 814
795 815 def get_server_url(environ):
796 816 req = webob.Request(environ)
797 817 return req.host_url + req.script_name
798 818
799 819
800 820 def unique_id(hexlen=32):
801 821 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
802 822 return suuid(truncate_to=hexlen, alphabet=alphabet)
803 823
804 824
805 825 def suuid(url=None, truncate_to=22, alphabet=None):
806 826 """
807 827 Generate and return a short URL safe UUID.
808 828
809 829 If the url parameter is provided, set the namespace to the provided
810 830 URL and generate a UUID.
811 831
812 832 :param url to get the uuid for
813 833 :truncate_to: truncate the basic 22 UUID to shorter version
814 834
815 835 The IDs won't be universally unique any longer, but the probability of
816 836 a collision will still be very low.
817 837 """
818 838 # Define our alphabet.
819 839 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
820 840
821 841 # If no URL is given, generate a random UUID.
822 842 if url is None:
823 843 unique_id = uuid.uuid4().int
824 844 else:
825 845 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
826 846
827 847 alphabet_length = len(_ALPHABET)
828 848 output = []
829 849 while unique_id > 0:
830 850 digit = unique_id % alphabet_length
831 851 output.append(_ALPHABET[digit])
832 852 unique_id = int(unique_id / alphabet_length)
833 853 return "".join(output)[:truncate_to]
834 854
835 855
836 856 def get_current_rhodecode_user(request=None):
837 857 """
838 858 Gets rhodecode user from request
839 859 """
840 860 pyramid_request = request or pyramid.threadlocal.get_current_request()
841 861
842 862 # web case
843 863 if pyramid_request and hasattr(pyramid_request, 'user'):
844 864 return pyramid_request.user
845 865
846 866 # api case
847 867 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
848 868 return pyramid_request.rpc_user
849 869
850 870 return None
851 871
852 872
853 873 def action_logger_generic(action, namespace=''):
854 874 """
855 875 A generic logger for actions useful to the system overview, tries to find
856 876 an acting user for the context of the call otherwise reports unknown user
857 877
858 878 :param action: logging message eg 'comment 5 deleted'
859 879 :param type: string
860 880
861 881 :param namespace: namespace of the logging message eg. 'repo.comments'
862 882 :param type: string
863 883
864 884 """
865 885
866 886 logger_name = 'rhodecode.actions'
867 887
868 888 if namespace:
869 889 logger_name += '.' + namespace
870 890
871 891 log = logging.getLogger(logger_name)
872 892
873 893 # get a user if we can
874 894 user = get_current_rhodecode_user()
875 895
876 896 logfunc = log.info
877 897
878 898 if not user:
879 899 user = '<unknown user>'
880 900 logfunc = log.warning
881 901
882 902 logfunc('Logging action by {}: {}'.format(user, action))
883 903
884 904
885 905 def escape_split(text, sep=',', maxsplit=-1):
886 906 r"""
887 907 Allows for escaping of the separator: e.g. arg='foo\, bar'
888 908
889 909 It should be noted that the way bash et. al. do command line parsing, those
890 910 single quotes are required.
891 911 """
892 912 escaped_sep = r'\%s' % sep
893 913
894 914 if escaped_sep not in text:
895 915 return text.split(sep, maxsplit)
896 916
897 917 before, _mid, after = text.partition(escaped_sep)
898 918 startlist = before.split(sep, maxsplit) # a regular split is fine here
899 919 unfinished = startlist[-1]
900 920 startlist = startlist[:-1]
901 921
902 922 # recurse because there may be more escaped separators
903 923 endlist = escape_split(after, sep, maxsplit)
904 924
905 925 # finish building the escaped value. we use endlist[0] becaue the first
906 926 # part of the string sent in recursion is the rest of the escaped value.
907 927 unfinished += sep + endlist[0]
908 928
909 929 return startlist + [unfinished] + endlist[1:] # put together all the parts
910 930
911 931
912 932 class OptionalAttr(object):
913 933 """
914 934 Special Optional Option that defines other attribute. Example::
915 935
916 936 def test(apiuser, userid=Optional(OAttr('apiuser')):
917 937 user = Optional.extract(userid)
918 938 # calls
919 939
920 940 """
921 941
922 942 def __init__(self, attr_name):
923 943 self.attr_name = attr_name
924 944
925 945 def __repr__(self):
926 946 return '<OptionalAttr:%s>' % self.attr_name
927 947
928 948 def __call__(self):
929 949 return self
930 950
931 951
932 952 # alias
933 953 OAttr = OptionalAttr
934 954
935 955
936 956 class Optional(object):
937 957 """
938 958 Defines an optional parameter::
939 959
940 960 param = param.getval() if isinstance(param, Optional) else param
941 961 param = param() if isinstance(param, Optional) else param
942 962
943 963 is equivalent of::
944 964
945 965 param = Optional.extract(param)
946 966
947 967 """
948 968
949 969 def __init__(self, type_):
950 970 self.type_ = type_
951 971
952 972 def __repr__(self):
953 973 return '<Optional:%s>' % self.type_.__repr__()
954 974
955 975 def __call__(self):
956 976 return self.getval()
957 977
958 978 def getval(self):
959 979 """
960 980 returns value from this Optional instance
961 981 """
962 982 if isinstance(self.type_, OAttr):
963 983 # use params name
964 984 return self.type_.attr_name
965 985 return self.type_
966 986
967 987 @classmethod
968 988 def extract(cls, val):
969 989 """
970 990 Extracts value from Optional() instance
971 991
972 992 :param val:
973 993 :return: original value if it's not Optional instance else
974 994 value of instance
975 995 """
976 996 if isinstance(val, cls):
977 997 return val.getval()
978 998 return val
979 999
980 1000
981 1001 def glob2re(pat):
982 1002 """
983 1003 Translate a shell PATTERN to a regular expression.
984 1004
985 1005 There is no way to quote meta-characters.
986 1006 """
987 1007
988 1008 i, n = 0, len(pat)
989 1009 res = ''
990 1010 while i < n:
991 1011 c = pat[i]
992 1012 i = i+1
993 1013 if c == '*':
994 1014 #res = res + '.*'
995 1015 res = res + '[^/]*'
996 1016 elif c == '?':
997 1017 #res = res + '.'
998 1018 res = res + '[^/]'
999 1019 elif c == '[':
1000 1020 j = i
1001 1021 if j < n and pat[j] == '!':
1002 1022 j = j+1
1003 1023 if j < n and pat[j] == ']':
1004 1024 j = j+1
1005 1025 while j < n and pat[j] != ']':
1006 1026 j = j+1
1007 1027 if j >= n:
1008 1028 res = res + '\\['
1009 1029 else:
1010 1030 stuff = pat[i:j].replace('\\','\\\\')
1011 1031 i = j+1
1012 1032 if stuff[0] == '!':
1013 1033 stuff = '^' + stuff[1:]
1014 1034 elif stuff[0] == '^':
1015 1035 stuff = '\\' + stuff
1016 1036 res = '%s[%s]' % (res, stuff)
1017 1037 else:
1018 1038 res = res + re.escape(c)
1019 1039 return res + '\Z(?ms)'
1020 1040
1021 1041
1022 1042 def parse_byte_string(size_str):
1023 1043 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1024 1044 if not match:
1025 1045 raise ValueError('Given size:%s is invalid, please make sure '
1026 1046 'to use format of <num>(MB|KB)' % size_str)
1027 1047
1028 1048 _parts = match.groups()
1029 1049 num, type_ = _parts
1030 1050 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
1031 1051
1032 1052
1033 1053 class CachedProperty(object):
1034 1054 """
1035 1055 Lazy Attributes. With option to invalidate the cache by running a method
1036 1056
1037 1057 class Foo():
1038 1058
1039 1059 @CachedProperty
1040 1060 def heavy_func():
1041 1061 return 'super-calculation'
1042 1062
1043 1063 foo = Foo()
1044 1064 foo.heavy_func() # first computions
1045 1065 foo.heavy_func() # fetch from cache
1046 1066 foo._invalidate_prop_cache('heavy_func')
1047 1067 # at this point calling foo.heavy_func() will be re-computed
1048 1068 """
1049 1069
1050 1070 def __init__(self, func, func_name=None):
1051 1071
1052 1072 if func_name is None:
1053 1073 func_name = func.__name__
1054 1074 self.data = (func, func_name)
1055 1075 update_wrapper(self, func)
1056 1076
1057 1077 def __get__(self, inst, class_):
1058 1078 if inst is None:
1059 1079 return self
1060 1080
1061 1081 func, func_name = self.data
1062 1082 value = func(inst)
1063 1083 inst.__dict__[func_name] = value
1064 1084 if '_invalidate_prop_cache' not in inst.__dict__:
1065 1085 inst.__dict__['_invalidate_prop_cache'] = partial(
1066 1086 self._invalidate_prop_cache, inst)
1067 1087 return value
1068 1088
1069 1089 def _invalidate_prop_cache(self, inst, name):
1070 1090 inst.__dict__.pop(name, None)
@@ -1,90 +1,94 b''
1 1 //--- RESETS ---//
2 2 :focus { outline: none; }
3 3 a { cursor: pointer; }
4 4
5 5 //--- clearfix --//
6 6 .clearfix {
7 7 &:before,
8 8 &:after {
9 9 content:"";
10 10 width: 100%;
11 11 clear: both;
12 12 float: left;
13 13 }
14 14 }
15 15
16 16 .clearinner:after { /* clears all floating divs inside a block */
17 17 content: "";
18 18 display: table;
19 19 clear: both;
20 20 }
21 21
22 22 .js-template { /* mark a template for javascript use */
23 23 display: none;
24 24 }
25 25
26 26 .linebreak {
27 27 display: block;
28 28 }
29 29
30 .clear-both {
31 clear: both;
32 }
33
30 34 .pull-right {
31 35 float: right !important;
32 36 }
33 37
34 38 .pull-left {
35 39 float: left !important;
36 40 }
37 41
38 42 .block-left {
39 43 float: left;
40 44 }
41 45
42 46 .block-right {
43 47 float: right;
44 48 clear: right;
45 49
46 50 li {
47 51 list-style-type: none;
48 52 }
49 53 }
50 54
51 55 //--- DEVICE-SPECIFIC CLASSES ---------------//
52 56 //regular tablet and up
53 57 @media (min-width:768px) {
54 58 .no-mobile {
55 59 display: block;
56 60 }
57 61 .mobile-only {
58 62 display: none;
59 63 }
60 64 }
61 65 //small tablet and phone
62 66 @media (max-width:767px) {
63 67 .mobile-only {
64 68 display: block;
65 69 }
66 70 .no-mobile {
67 71 display: none;
68 72 }
69 73 }
70 74
71 75 //--- STICKY FOOTER ---//
72 76 html, body {
73 77 height: 100%;
74 78 margin: 0;
75 79 }
76 80 .outerwrapper {
77 81 height: 100%;
78 82 min-height: 100%;
79 83 margin: 0;
80 84 padding-bottom: 3em; /* must be equal to footer height */
81 85 }
82 86 .outerwrapper:after{
83 87 content:" ";
84 88 }
85 89 #footer {
86 90 clear: both;
87 91 position: relative;
88 92 height: 3em; /* footer height */
89 93 margin: -3em 0 0; /* must be equal to footer height */
90 94 }
@@ -1,2897 +1,2913 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'variables';
9 9 @import 'bootstrap-variables';
10 10 @import 'form-bootstrap';
11 11 @import 'codemirror';
12 12 @import 'legacy_code_styles';
13 13 @import 'readme-box';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 29 @import 'tooltips';
30 30
31 31 //--- BASE ------------------//
32 32 .noscript-error {
33 33 top: 0;
34 34 left: 0;
35 35 width: 100%;
36 36 z-index: 101;
37 37 text-align: center;
38 38 font-size: 120%;
39 39 color: white;
40 40 background-color: @alert2;
41 41 padding: 5px 0 5px 0;
42 42 font-weight: @text-semibold-weight;
43 43 font-family: @text-semibold;
44 44 }
45 45
46 46 html {
47 47 display: table;
48 48 height: 100%;
49 49 width: 100%;
50 50 }
51 51
52 52 body {
53 53 display: table-cell;
54 54 width: 100%;
55 55 }
56 56
57 57 //--- LAYOUT ------------------//
58 58
59 59 .hidden{
60 60 display: none !important;
61 61 }
62 62
63 63 .box{
64 64 float: left;
65 65 width: 100%;
66 66 }
67 67
68 68 .browser-header {
69 69 clear: both;
70 70 }
71 71 .main {
72 72 clear: both;
73 73 padding:0 0 @pagepadding;
74 74 height: auto;
75 75
76 76 &:after { //clearfix
77 77 content:"";
78 78 clear:both;
79 79 width:100%;
80 80 display:block;
81 81 }
82 82 }
83 83
84 84 .action-link{
85 85 margin-left: @padding;
86 86 padding-left: @padding;
87 87 border-left: @border-thickness solid @border-default-color;
88 88 }
89 89
90 90 input + .action-link, .action-link.first{
91 91 border-left: none;
92 92 }
93 93
94 94 .action-link.last{
95 95 margin-right: @padding;
96 96 padding-right: @padding;
97 97 }
98 98
99 99 .action-link.active,
100 100 .action-link.active a{
101 101 color: @grey4;
102 102 }
103 103
104 104 .action-link.disabled {
105 105 color: @grey4;
106 106 cursor: inherit;
107 107 }
108 108
109 109 .clipboard-action {
110 110 cursor: pointer;
111 111 color: @grey4;
112 112 margin-left: 5px;
113 113
114 114 &:hover {
115 115 color: @grey2;
116 116 }
117 117 }
118 118
119 119 ul.simple-list{
120 120 list-style: none;
121 121 margin: 0;
122 122 padding: 0;
123 123 }
124 124
125 125 .main-content {
126 126 padding-bottom: @pagepadding;
127 127 }
128 128
129 129 .wide-mode-wrapper {
130 130 max-width:4000px !important;
131 131 }
132 132
133 133 .wrapper {
134 134 position: relative;
135 135 max-width: @wrapper-maxwidth;
136 136 margin: 0 auto;
137 137 }
138 138
139 139 #content {
140 140 clear: both;
141 141 padding: 0 @contentpadding;
142 142 }
143 143
144 144 .advanced-settings-fields{
145 145 input{
146 146 margin-left: @textmargin;
147 147 margin-right: @padding/2;
148 148 }
149 149 }
150 150
151 151 .cs_files_title {
152 152 margin: @pagepadding 0 0;
153 153 }
154 154
155 155 input.inline[type="file"] {
156 156 display: inline;
157 157 }
158 158
159 159 .error_page {
160 160 margin: 10% auto;
161 161
162 162 h1 {
163 163 color: @grey2;
164 164 }
165 165
166 166 .alert {
167 167 margin: @padding 0;
168 168 }
169 169
170 170 .error-branding {
171 171 color: @grey4;
172 172 font-weight: @text-semibold-weight;
173 173 font-family: @text-semibold;
174 174 }
175 175
176 176 .error_message {
177 177 font-family: @text-regular;
178 178 }
179 179
180 180 .sidebar {
181 181 min-height: 275px;
182 182 margin: 0;
183 183 padding: 0 0 @sidebarpadding @sidebarpadding;
184 184 border: none;
185 185 }
186 186
187 187 .main-content {
188 188 position: relative;
189 189 margin: 0 @sidebarpadding @sidebarpadding;
190 190 padding: 0 0 0 @sidebarpadding;
191 191 border-left: @border-thickness solid @grey5;
192 192
193 193 @media (max-width:767px) {
194 194 clear: both;
195 195 width: 100%;
196 196 margin: 0;
197 197 border: none;
198 198 }
199 199 }
200 200
201 201 .inner-column {
202 202 float: left;
203 203 width: 29.75%;
204 204 min-height: 150px;
205 205 margin: @sidebarpadding 2% 0 0;
206 206 padding: 0 2% 0 0;
207 207 border-right: @border-thickness solid @grey5;
208 208
209 209 @media (max-width:767px) {
210 210 clear: both;
211 211 width: 100%;
212 212 border: none;
213 213 }
214 214
215 215 ul {
216 216 padding-left: 1.25em;
217 217 }
218 218
219 219 &:last-child {
220 220 margin: @sidebarpadding 0 0;
221 221 border: none;
222 222 }
223 223
224 224 h4 {
225 225 margin: 0 0 @padding;
226 226 font-weight: @text-semibold-weight;
227 227 font-family: @text-semibold;
228 228 }
229 229 }
230 230 }
231 231 .error-page-logo {
232 232 width: 130px;
233 233 height: 160px;
234 234 }
235 235
236 236 // HEADER
237 237 .header {
238 238
239 239 // TODO: johbo: Fix login pages, so that they work without a min-height
240 240 // for the header and then remove the min-height. I chose a smaller value
241 241 // intentionally here to avoid rendering issues in the main navigation.
242 242 min-height: 49px;
243 243 min-width: 1024px;
244 244
245 245 position: relative;
246 246 vertical-align: bottom;
247 247 padding: 0 @header-padding;
248 248 background-color: @grey1;
249 249 color: @grey5;
250 250
251 251 .title {
252 252 overflow: visible;
253 253 }
254 254
255 255 &:before,
256 256 &:after {
257 257 content: "";
258 258 clear: both;
259 259 width: 100%;
260 260 }
261 261
262 262 // TODO: johbo: Avoids breaking "Repositories" chooser
263 263 .select2-container .select2-choice .select2-arrow {
264 264 display: none;
265 265 }
266 266 }
267 267
268 268 #header-inner {
269 269 &.title {
270 270 margin: 0;
271 271 }
272 272 &:before,
273 273 &:after {
274 274 content: "";
275 275 clear: both;
276 276 }
277 277 }
278 278
279 279 // Gists
280 280 #files_data {
281 281 clear: both; //for firefox
282 282 padding-top: 10px;
283 283 }
284 284
285 285 #gistid {
286 286 margin-right: @padding;
287 287 }
288 288
289 289 // Global Settings Editor
290 290 .textarea.editor {
291 291 float: left;
292 292 position: relative;
293 293 max-width: @texteditor-width;
294 294
295 295 select {
296 296 position: absolute;
297 297 top:10px;
298 298 right:0;
299 299 }
300 300
301 301 .CodeMirror {
302 302 margin: 0;
303 303 }
304 304
305 305 .help-block {
306 306 margin: 0 0 @padding;
307 307 padding:.5em;
308 308 background-color: @grey6;
309 309 &.pre-formatting {
310 310 white-space: pre;
311 311 }
312 312 }
313 313 }
314 314
315 315 ul.auth_plugins {
316 316 margin: @padding 0 @padding @legend-width;
317 317 padding: 0;
318 318
319 319 li {
320 320 margin-bottom: @padding;
321 321 line-height: 1em;
322 322 list-style-type: none;
323 323
324 324 .auth_buttons .btn {
325 325 margin-right: @padding;
326 326 }
327 327
328 328 }
329 329 }
330 330
331 331
332 332 // My Account PR list
333 333
334 334 #show_closed {
335 335 margin: 0 1em 0 0;
336 336 }
337 337
338 338 #pull_request_list_table {
339 339 .closed {
340 340 background-color: @grey6;
341 341 }
342 342
343 343 .state-creating,
344 344 .state-updating,
345 345 .state-merging
346 346 {
347 347 background-color: @grey6;
348 348 }
349 349
350 350 .td-status {
351 351 padding-left: .5em;
352 352 }
353 353 .log-container .truncate {
354 354 height: 2.75em;
355 355 white-space: pre-line;
356 356 }
357 357 table.rctable .user {
358 358 padding-left: 0;
359 359 }
360 360 table.rctable {
361 361 td.td-description,
362 362 .rc-user {
363 363 min-width: auto;
364 364 }
365 365 }
366 366 }
367 367
368 368 // Pull Requests
369 369
370 370 .pullrequests_section_head {
371 371 display: block;
372 372 clear: both;
373 373 margin: @padding 0;
374 374 font-weight: @text-bold-weight;
375 375 font-family: @text-bold;
376 376 }
377 377
378 378 .pr-origininfo, .pr-targetinfo {
379 379 position: relative;
380 380
381 381 .tag {
382 382 display: inline-block;
383 383 margin: 0 1em .5em 0;
384 384 }
385 385
386 386 .clone-url {
387 387 display: inline-block;
388 388 margin: 0 0 .5em 0;
389 389 padding: 0;
390 390 line-height: 1.2em;
391 391 }
392 392 }
393 393
394 394 .pr-mergeinfo {
395 395 min-width: 95% !important;
396 396 padding: 0 !important;
397 397 border: 0;
398 398 }
399 399 .pr-mergeinfo-copy {
400 400 padding: 0 0;
401 401 }
402 402
403 403 .pr-pullinfo {
404 404 min-width: 95% !important;
405 405 padding: 0 !important;
406 406 border: 0;
407 407 }
408 408 .pr-pullinfo-copy {
409 409 padding: 0 0;
410 410 }
411 411
412 412
413 413 #pr-title-input {
414 414 width: 72%;
415 415 font-size: 1em;
416 416 margin: 0;
417 417 padding: 0 0 0 @padding/4;
418 418 line-height: 1.7em;
419 419 color: @text-color;
420 420 letter-spacing: .02em;
421 421 font-weight: @text-bold-weight;
422 422 font-family: @text-bold;
423 423 }
424 424
425 425 #pullrequest_title {
426 426 width: 100%;
427 427 box-sizing: border-box;
428 428 }
429 429
430 430 #pr_open_message {
431 431 border: @border-thickness solid #fff;
432 432 border-radius: @border-radius;
433 433 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
434 434 text-align: left;
435 435 overflow: hidden;
436 436 }
437 437
438 438 .pr-submit-button {
439 439 float: right;
440 440 margin: 0 0 0 5px;
441 441 }
442 442
443 443 .pr-spacing-container {
444 444 padding: 20px;
445 445 clear: both
446 446 }
447 447
448 448 #pr-description-input {
449 449 margin-bottom: 0;
450 450 }
451 451
452 452 .pr-description-label {
453 453 vertical-align: top;
454 454 }
455 455
456 456 .perms_section_head {
457 457 min-width: 625px;
458 458
459 459 h2 {
460 460 margin-bottom: 0;
461 461 }
462 462
463 463 .label-checkbox {
464 464 float: left;
465 465 }
466 466
467 467 &.field {
468 468 margin: @space 0 @padding;
469 469 }
470 470
471 471 &:first-child.field {
472 472 margin-top: 0;
473 473
474 474 .label {
475 475 margin-top: 0;
476 476 padding-top: 0;
477 477 }
478 478
479 479 .radios {
480 480 padding-top: 0;
481 481 }
482 482 }
483 483
484 484 .radios {
485 485 position: relative;
486 486 width: 505px;
487 487 }
488 488 }
489 489
490 490 //--- MODULES ------------------//
491 491
492 492
493 493 // Server Announcement
494 494 #server-announcement {
495 495 width: 95%;
496 496 margin: @padding auto;
497 497 padding: @padding;
498 498 border-width: 2px;
499 499 border-style: solid;
500 500 .border-radius(2px);
501 501 font-weight: @text-bold-weight;
502 502 font-family: @text-bold;
503 503
504 504 &.info { border-color: @alert4; background-color: @alert4-inner; }
505 505 &.warning { border-color: @alert3; background-color: @alert3-inner; }
506 506 &.error { border-color: @alert2; background-color: @alert2-inner; }
507 507 &.success { border-color: @alert1; background-color: @alert1-inner; }
508 508 &.neutral { border-color: @grey3; background-color: @grey6; }
509 509 }
510 510
511 511 // Fixed Sidebar Column
512 512 .sidebar-col-wrapper {
513 513 padding-left: @sidebar-all-width;
514 514
515 515 .sidebar {
516 516 width: @sidebar-width;
517 517 margin-left: -@sidebar-all-width;
518 518 }
519 519 }
520 520
521 521 .sidebar-col-wrapper.scw-small {
522 522 padding-left: @sidebar-small-all-width;
523 523
524 524 .sidebar {
525 525 width: @sidebar-small-width;
526 526 margin-left: -@sidebar-small-all-width;
527 527 }
528 528 }
529 529
530 530
531 531 // FOOTER
532 532 #footer {
533 533 padding: 0;
534 534 text-align: center;
535 535 vertical-align: middle;
536 536 color: @grey2;
537 537 font-size: 11px;
538 538
539 539 p {
540 540 margin: 0;
541 541 padding: 1em;
542 542 line-height: 1em;
543 543 }
544 544
545 545 .server-instance { //server instance
546 546 display: none;
547 547 }
548 548
549 549 .title {
550 550 float: none;
551 551 margin: 0 auto;
552 552 }
553 553 }
554 554
555 555 button.close {
556 556 padding: 0;
557 557 cursor: pointer;
558 558 background: transparent;
559 559 border: 0;
560 560 .box-shadow(none);
561 561 -webkit-appearance: none;
562 562 }
563 563
564 564 .close {
565 565 float: right;
566 566 font-size: 21px;
567 567 font-family: @text-bootstrap;
568 568 line-height: 1em;
569 569 font-weight: bold;
570 570 color: @grey2;
571 571
572 572 &:hover,
573 573 &:focus {
574 574 color: @grey1;
575 575 text-decoration: none;
576 576 cursor: pointer;
577 577 }
578 578 }
579 579
580 580 // GRID
581 581 .sorting,
582 582 .sorting_desc,
583 583 .sorting_asc {
584 584 cursor: pointer;
585 585 }
586 586 .sorting_desc:after {
587 587 content: "\00A0\25B2";
588 588 font-size: .75em;
589 589 }
590 590 .sorting_asc:after {
591 591 content: "\00A0\25BC";
592 592 font-size: .68em;
593 593 }
594 594
595 595
596 596 .user_auth_tokens {
597 597
598 598 &.truncate {
599 599 white-space: nowrap;
600 600 overflow: hidden;
601 601 text-overflow: ellipsis;
602 602 }
603 603
604 604 .fields .field .input {
605 605 margin: 0;
606 606 }
607 607
608 608 input#description {
609 609 width: 100px;
610 610 margin: 0;
611 611 }
612 612
613 613 .drop-menu {
614 614 // TODO: johbo: Remove this, should work out of the box when
615 615 // having multiple inputs inline
616 616 margin: 0 0 0 5px;
617 617 }
618 618 }
619 619 #user_list_table {
620 620 .closed {
621 621 background-color: @grey6;
622 622 }
623 623 }
624 624
625 625
626 626 input, textarea {
627 627 &.disabled {
628 628 opacity: .5;
629 629 }
630 630
631 631 &:hover {
632 632 border-color: @grey3;
633 633 box-shadow: @button-shadow;
634 634 }
635 635
636 636 &:focus {
637 637 border-color: @rcblue;
638 638 box-shadow: @button-shadow;
639 639 }
640 640 }
641 641
642 642 // remove extra padding in firefox
643 643 input::-moz-focus-inner { border:0; padding:0 }
644 644
645 645 .adjacent input {
646 646 margin-bottom: @padding;
647 647 }
648 648
649 649 .permissions_boxes {
650 650 display: block;
651 651 }
652 652
653 653 //FORMS
654 654
655 655 .medium-inline,
656 656 input#description.medium-inline {
657 657 display: inline;
658 658 width: @medium-inline-input-width;
659 659 min-width: 100px;
660 660 }
661 661
662 662 select {
663 663 //reset
664 664 -webkit-appearance: none;
665 665 -moz-appearance: none;
666 666
667 667 display: inline-block;
668 668 height: 28px;
669 669 width: auto;
670 670 margin: 0 @padding @padding 0;
671 671 padding: 0 18px 0 8px;
672 672 line-height:1em;
673 673 font-size: @basefontsize;
674 674 border: @border-thickness solid @grey5;
675 675 border-radius: @border-radius;
676 676 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
677 677 color: @grey4;
678 678 box-shadow: @button-shadow;
679 679
680 680 &:after {
681 681 content: "\00A0\25BE";
682 682 }
683 683
684 684 &:focus, &:hover {
685 685 outline: none;
686 686 border-color: @grey4;
687 687 color: @rcdarkblue;
688 688 }
689 689 }
690 690
691 691 option {
692 692 &:focus {
693 693 outline: none;
694 694 }
695 695 }
696 696
697 697 input,
698 698 textarea {
699 699 padding: @input-padding;
700 700 border: @input-border-thickness solid @border-highlight-color;
701 701 .border-radius (@border-radius);
702 702 font-family: @text-light;
703 703 font-size: @basefontsize;
704 704
705 705 &.input-sm {
706 706 padding: 5px;
707 707 }
708 708
709 709 &#description {
710 710 min-width: @input-description-minwidth;
711 711 min-height: 1em;
712 712 padding: 10px;
713 713 }
714 714 }
715 715
716 716 .field-sm {
717 717 input,
718 718 textarea {
719 719 padding: 5px;
720 720 }
721 721 }
722 722
723 723 textarea {
724 724 display: block;
725 725 clear: both;
726 726 width: 100%;
727 727 min-height: 100px;
728 728 margin-bottom: @padding;
729 729 .box-sizing(border-box);
730 730 overflow: auto;
731 731 }
732 732
733 733 label {
734 734 font-family: @text-light;
735 735 }
736 736
737 737 // GRAVATARS
738 738 // centers gravatar on username to the right
739 739
740 740 .gravatar {
741 741 display: inline;
742 742 min-width: 16px;
743 743 min-height: 16px;
744 744 margin: -5px 0;
745 745 padding: 0;
746 746 line-height: 1em;
747 747 box-sizing: content-box;
748 748 border-radius: 50%;
749 749
750 750 &.gravatar-large {
751 751 margin: -0.5em .25em -0.5em 0;
752 752 }
753 753
754 754 & + .user {
755 755 display: inline;
756 756 margin: 0;
757 757 padding: 0 0 0 .17em;
758 758 line-height: 1em;
759 759 }
760 760 }
761 761
762 762 .user-inline-data {
763 763 display: inline-block;
764 764 float: left;
765 765 padding-left: .5em;
766 766 line-height: 1.3em;
767 767 }
768 768
769 769 .rc-user { // gravatar + user wrapper
770 770 float: left;
771 771 position: relative;
772 772 min-width: 100px;
773 773 max-width: 200px;
774 774 min-height: (@gravatar-size + @border-thickness * 2); // account for border
775 775 display: block;
776 776 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
777 777
778 778
779 779 .gravatar {
780 780 display: block;
781 781 position: absolute;
782 782 top: 0;
783 783 left: 0;
784 784 min-width: @gravatar-size;
785 785 min-height: @gravatar-size;
786 786 margin: 0;
787 787 }
788 788
789 789 .user {
790 790 display: block;
791 791 max-width: 175px;
792 792 padding-top: 2px;
793 793 overflow: hidden;
794 794 text-overflow: ellipsis;
795 795 }
796 796 }
797 797
798 798 .gist-gravatar,
799 799 .journal_container {
800 800 .gravatar-large {
801 801 margin: 0 .5em -10px 0;
802 802 }
803 803 }
804 804
805 805
806 806 // ADMIN SETTINGS
807 807
808 808 // Tag Patterns
809 809 .tag_patterns {
810 810 .tag_input {
811 811 margin-bottom: @padding;
812 812 }
813 813 }
814 814
815 815 .locked_input {
816 816 position: relative;
817 817
818 818 input {
819 819 display: inline;
820 820 margin: 3px 5px 0px 0px;
821 821 }
822 822
823 823 br {
824 824 display: none;
825 825 }
826 826
827 827 .error-message {
828 828 float: left;
829 829 width: 100%;
830 830 }
831 831
832 832 .lock_input_button {
833 833 display: inline;
834 834 }
835 835
836 836 .help-block {
837 837 clear: both;
838 838 }
839 839 }
840 840
841 841 // Notifications
842 842
843 843 .notifications_buttons {
844 844 margin: 0 0 @space 0;
845 845 padding: 0;
846 846
847 847 .btn {
848 848 display: inline-block;
849 849 }
850 850 }
851 851
852 852 .notification-list {
853 853
854 854 div {
855 855 display: inline-block;
856 856 vertical-align: middle;
857 857 }
858 858
859 859 .container {
860 860 display: block;
861 861 margin: 0 0 @padding 0;
862 862 }
863 863
864 864 .delete-notifications {
865 865 margin-left: @padding;
866 866 text-align: right;
867 867 cursor: pointer;
868 868 }
869 869
870 870 .read-notifications {
871 871 margin-left: @padding/2;
872 872 text-align: right;
873 873 width: 35px;
874 874 cursor: pointer;
875 875 }
876 876
877 877 .icon-minus-sign {
878 878 color: @alert2;
879 879 }
880 880
881 881 .icon-ok-sign {
882 882 color: @alert1;
883 883 }
884 884 }
885 885
886 886 .user_settings {
887 887 float: left;
888 888 clear: both;
889 889 display: block;
890 890 width: 100%;
891 891
892 892 .gravatar_box {
893 893 margin-bottom: @padding;
894 894
895 895 &:after {
896 896 content: " ";
897 897 clear: both;
898 898 width: 100%;
899 899 }
900 900 }
901 901
902 902 .fields .field {
903 903 clear: both;
904 904 }
905 905 }
906 906
907 907 .advanced_settings {
908 908 margin-bottom: @space;
909 909
910 910 .help-block {
911 911 margin-left: 0;
912 912 }
913 913
914 914 button + .help-block {
915 915 margin-top: @padding;
916 916 }
917 917 }
918 918
919 919 // admin settings radio buttons and labels
920 920 .label-2 {
921 921 float: left;
922 922 width: @label2-width;
923 923
924 924 label {
925 925 color: @grey1;
926 926 }
927 927 }
928 928 .checkboxes {
929 929 float: left;
930 930 width: @checkboxes-width;
931 931 margin-bottom: @padding;
932 932
933 933 .checkbox {
934 934 width: 100%;
935 935
936 936 label {
937 937 margin: 0;
938 938 padding: 0;
939 939 }
940 940 }
941 941
942 942 .checkbox + .checkbox {
943 943 display: inline-block;
944 944 }
945 945
946 946 label {
947 947 margin-right: 1em;
948 948 }
949 949 }
950 950
951 951 // CHANGELOG
952 952 .container_header {
953 953 float: left;
954 954 display: block;
955 955 width: 100%;
956 956 margin: @padding 0 @padding;
957 957
958 958 #filter_changelog {
959 959 float: left;
960 960 margin-right: @padding;
961 961 }
962 962
963 963 .breadcrumbs_light {
964 964 display: inline-block;
965 965 }
966 966 }
967 967
968 968 .info_box {
969 969 float: right;
970 970 }
971 971
972 972
973 973
974 974 #graph_content{
975 975
976 976 // adjust for table headers so that graph renders properly
977 977 // #graph_nodes padding - table cell padding
978 978 padding-top: (@space - (@basefontsize * 2.4));
979 979
980 980 &.graph_full_width {
981 981 width: 100%;
982 982 max-width: 100%;
983 983 }
984 984 }
985 985
986 986 #graph {
987 987
988 988 .pagination-left {
989 989 float: left;
990 990 clear: both;
991 991 }
992 992
993 993 .log-container {
994 994 max-width: 345px;
995 995
996 996 .message{
997 997 max-width: 340px;
998 998 }
999 999 }
1000 1000
1001 1001 .graph-col-wrapper {
1002 1002
1003 1003 #graph_nodes {
1004 1004 width: 100px;
1005 1005 position: absolute;
1006 1006 left: 70px;
1007 1007 z-index: -1;
1008 1008 }
1009 1009 }
1010 1010
1011 1011 .load-more-commits {
1012 1012 text-align: center;
1013 1013 }
1014 1014 .load-more-commits:hover {
1015 1015 background-color: @grey7;
1016 1016 }
1017 1017 .load-more-commits {
1018 1018 a {
1019 1019 display: block;
1020 1020 }
1021 1021 }
1022 1022 }
1023 1023
1024 1024 .obsolete-toggle {
1025 1025 line-height: 30px;
1026 1026 margin-left: -15px;
1027 1027 }
1028 1028
1029 1029 #rev_range_container, #rev_range_clear, #rev_range_more {
1030 1030 margin-top: -5px;
1031 1031 margin-bottom: -5px;
1032 1032 }
1033 1033
1034 1034 #filter_changelog {
1035 1035 float: left;
1036 1036 }
1037 1037
1038 1038
1039 1039 //--- THEME ------------------//
1040 1040
1041 1041 #logo {
1042 1042 float: left;
1043 1043 margin: 9px 0 0 0;
1044 1044
1045 1045 .header {
1046 1046 background-color: transparent;
1047 1047 }
1048 1048
1049 1049 a {
1050 1050 display: inline-block;
1051 1051 }
1052 1052
1053 1053 img {
1054 1054 height:30px;
1055 1055 }
1056 1056 }
1057 1057
1058 1058 .logo-wrapper {
1059 1059 float:left;
1060 1060 }
1061 1061
1062 1062 .branding {
1063 1063 float: left;
1064 1064 padding: 9px 2px;
1065 1065 line-height: 1em;
1066 1066 font-size: @navigation-fontsize;
1067 1067
1068 1068 a {
1069 1069 color: @grey5
1070 1070 }
1071 1071 @media screen and (max-width: 1200px) {
1072 1072 display: none;
1073 1073 }
1074 1074 }
1075 1075
1076 1076 img {
1077 1077 border: none;
1078 1078 outline: none;
1079 1079 }
1080 1080 user-profile-header
1081 1081 label {
1082 1082
1083 1083 input[type="checkbox"] {
1084 1084 margin-right: 1em;
1085 1085 }
1086 1086 input[type="radio"] {
1087 1087 margin-right: 1em;
1088 1088 }
1089 1089 }
1090 1090
1091 1091 .review-status {
1092 1092 &.under_review {
1093 1093 color: @alert3;
1094 1094 }
1095 1095 &.approved {
1096 1096 color: @alert1;
1097 1097 }
1098 1098 &.rejected,
1099 1099 &.forced_closed{
1100 1100 color: @alert2;
1101 1101 }
1102 1102 &.not_reviewed {
1103 1103 color: @grey5;
1104 1104 }
1105 1105 }
1106 1106
1107 1107 .review-status-under_review {
1108 1108 color: @alert3;
1109 1109 }
1110 1110 .status-tag-under_review {
1111 1111 border-color: @alert3;
1112 1112 }
1113 1113
1114 1114 .review-status-approved {
1115 1115 color: @alert1;
1116 1116 }
1117 1117 .status-tag-approved {
1118 1118 border-color: @alert1;
1119 1119 }
1120 1120
1121 1121 .review-status-rejected,
1122 1122 .review-status-forced_closed {
1123 1123 color: @alert2;
1124 1124 }
1125 1125 .status-tag-rejected,
1126 1126 .status-tag-forced_closed {
1127 1127 border-color: @alert2;
1128 1128 }
1129 1129
1130 1130 .review-status-not_reviewed {
1131 1131 color: @grey5;
1132 1132 }
1133 1133 .status-tag-not_reviewed {
1134 1134 border-color: @grey5;
1135 1135 }
1136 1136
1137 1137 .test_pattern_preview {
1138 1138 margin: @space 0;
1139 1139
1140 1140 p {
1141 1141 margin-bottom: 0;
1142 1142 border-bottom: @border-thickness solid @border-default-color;
1143 1143 color: @grey3;
1144 1144 }
1145 1145
1146 1146 .btn {
1147 1147 margin-bottom: @padding;
1148 1148 }
1149 1149 }
1150 1150 #test_pattern_result {
1151 1151 display: none;
1152 1152 &:extend(pre);
1153 1153 padding: .9em;
1154 1154 color: @grey3;
1155 1155 background-color: @grey7;
1156 1156 border-right: @border-thickness solid @border-default-color;
1157 1157 border-bottom: @border-thickness solid @border-default-color;
1158 1158 border-left: @border-thickness solid @border-default-color;
1159 1159 }
1160 1160
1161 1161 #repo_vcs_settings {
1162 1162 #inherit_overlay_vcs_default {
1163 1163 display: none;
1164 1164 }
1165 1165 #inherit_overlay_vcs_custom {
1166 1166 display: custom;
1167 1167 }
1168 1168 &.inherited {
1169 1169 #inherit_overlay_vcs_default {
1170 1170 display: block;
1171 1171 }
1172 1172 #inherit_overlay_vcs_custom {
1173 1173 display: none;
1174 1174 }
1175 1175 }
1176 1176 }
1177 1177
1178 1178 .issue-tracker-link {
1179 1179 color: @rcblue;
1180 1180 }
1181 1181
1182 1182 // Issue Tracker Table Show/Hide
1183 1183 #repo_issue_tracker {
1184 1184 #inherit_overlay {
1185 1185 display: none;
1186 1186 }
1187 1187 #custom_overlay {
1188 1188 display: custom;
1189 1189 }
1190 1190 &.inherited {
1191 1191 #inherit_overlay {
1192 1192 display: block;
1193 1193 }
1194 1194 #custom_overlay {
1195 1195 display: none;
1196 1196 }
1197 1197 }
1198 1198 }
1199 1199 table.issuetracker {
1200 1200 &.readonly {
1201 1201 tr, td {
1202 1202 color: @grey3;
1203 1203 }
1204 1204 }
1205 1205 .edit {
1206 1206 display: none;
1207 1207 }
1208 1208 .editopen {
1209 1209 .edit {
1210 1210 display: inline;
1211 1211 }
1212 1212 .entry {
1213 1213 display: none;
1214 1214 }
1215 1215 }
1216 1216 tr td.td-action {
1217 1217 min-width: 117px;
1218 1218 }
1219 1219 td input {
1220 1220 max-width: none;
1221 1221 min-width: 30px;
1222 1222 width: 80%;
1223 1223 }
1224 1224 .issuetracker_pref input {
1225 1225 width: 40%;
1226 1226 }
1227 1227 input.edit_issuetracker_update {
1228 1228 margin-right: 0;
1229 1229 width: auto;
1230 1230 }
1231 1231 }
1232 1232
1233 1233 table.integrations {
1234 1234 .td-icon {
1235 1235 width: 20px;
1236 1236 .integration-icon {
1237 1237 height: 20px;
1238 1238 width: 20px;
1239 1239 }
1240 1240 }
1241 1241 }
1242 1242
1243 1243 .integrations {
1244 1244 a.integration-box {
1245 1245 color: @text-color;
1246 1246 &:hover {
1247 1247 .panel {
1248 1248 background: #fbfbfb;
1249 1249 }
1250 1250 }
1251 1251 .integration-icon {
1252 1252 width: 30px;
1253 1253 height: 30px;
1254 1254 margin-right: 20px;
1255 1255 float: left;
1256 1256 }
1257 1257
1258 1258 .panel-body {
1259 1259 padding: 10px;
1260 1260 }
1261 1261 .panel {
1262 1262 margin-bottom: 10px;
1263 1263 }
1264 1264 h2 {
1265 1265 display: inline-block;
1266 1266 margin: 0;
1267 1267 min-width: 140px;
1268 1268 }
1269 1269 }
1270 1270 a.integration-box.dummy-integration {
1271 1271 color: @grey4
1272 1272 }
1273 1273 }
1274 1274
1275 1275 //Permissions Settings
1276 1276 #add_perm {
1277 1277 margin: 0 0 @padding;
1278 1278 cursor: pointer;
1279 1279 }
1280 1280
1281 1281 .perm_ac {
1282 1282 input {
1283 1283 width: 95%;
1284 1284 }
1285 1285 }
1286 1286
1287 1287 .autocomplete-suggestions {
1288 1288 width: auto !important; // overrides autocomplete.js
1289 1289 min-width: 278px;
1290 1290 margin: 0;
1291 1291 border: @border-thickness solid @grey5;
1292 1292 border-radius: @border-radius;
1293 1293 color: @grey2;
1294 1294 background-color: white;
1295 1295 }
1296 1296
1297 1297 .autocomplete-qfilter-suggestions {
1298 1298 width: auto !important; // overrides autocomplete.js
1299 1299 max-height: 100% !important;
1300 1300 min-width: 376px;
1301 1301 margin: 0;
1302 1302 border: @border-thickness solid @grey5;
1303 1303 color: @grey2;
1304 1304 background-color: white;
1305 1305 }
1306 1306
1307 1307 .autocomplete-selected {
1308 1308 background: #F0F0F0;
1309 1309 }
1310 1310
1311 1311 .ac-container-wrap {
1312 1312 margin: 0;
1313 1313 padding: 8px;
1314 1314 border-bottom: @border-thickness solid @grey5;
1315 1315 list-style-type: none;
1316 1316 cursor: pointer;
1317 1317
1318 1318 &:hover {
1319 1319 background-color: @grey7;
1320 1320 }
1321 1321
1322 1322 img {
1323 1323 height: @gravatar-size;
1324 1324 width: @gravatar-size;
1325 1325 margin-right: 1em;
1326 1326 }
1327 1327
1328 1328 strong {
1329 1329 font-weight: normal;
1330 1330 }
1331 1331 }
1332 1332
1333 1333 // Settings Dropdown
1334 1334 .user-menu .container {
1335 1335 padding: 0 4px;
1336 1336 margin: 0;
1337 1337 }
1338 1338
1339 1339 .user-menu .gravatar {
1340 1340 cursor: pointer;
1341 1341 }
1342 1342
1343 1343 .codeblock {
1344 1344 margin-bottom: @padding;
1345 1345 clear: both;
1346 1346
1347 1347 .stats {
1348 1348 overflow: hidden;
1349 1349 }
1350 1350
1351 1351 .message{
1352 1352 textarea{
1353 1353 margin: 0;
1354 1354 }
1355 1355 }
1356 1356
1357 1357 .code-header {
1358 1358 .stats {
1359 1359 line-height: 2em;
1360 1360
1361 1361 .revision_id {
1362 1362 margin-left: 0;
1363 1363 }
1364 1364 .buttons {
1365 1365 padding-right: 0;
1366 1366 }
1367 1367 }
1368 1368
1369 1369 .item{
1370 1370 margin-right: 0.5em;
1371 1371 }
1372 1372 }
1373 1373
1374 1374 #editor_container {
1375 1375 position: relative;
1376 1376 margin: @padding 10px;
1377 1377 }
1378 1378 }
1379 1379
1380 1380 #file_history_container {
1381 1381 display: none;
1382 1382 }
1383 1383
1384 1384 .file-history-inner {
1385 1385 margin-bottom: 10px;
1386 1386 }
1387 1387
1388 1388 // Pull Requests
1389 1389 .summary-details {
1390 1390 width: 72%;
1391 1391 }
1392 1392 .pr-summary {
1393 1393 border-bottom: @border-thickness solid @grey5;
1394 1394 margin-bottom: @space;
1395 1395 }
1396 1396 .reviewers-title {
1397 1397 width: 25%;
1398 1398 min-width: 200px;
1399 1399 }
1400 1400 .reviewers {
1401 1401 width: 25%;
1402 1402 min-width: 200px;
1403 1403 }
1404 1404 .reviewers ul li {
1405 1405 position: relative;
1406 1406 width: 100%;
1407 1407 padding-bottom: 8px;
1408 1408 list-style-type: none;
1409 1409 }
1410 1410
1411 1411 .reviewer_entry {
1412 1412 min-height: 55px;
1413 1413 }
1414 1414
1415 1415 .reviewers_member {
1416 1416 width: 100%;
1417 1417 overflow: auto;
1418 1418 }
1419 1419 .reviewer_reason {
1420 1420 padding-left: 20px;
1421 1421 line-height: 1.5em;
1422 1422 }
1423 1423 .reviewer_status {
1424 1424 display: inline-block;
1425 1425 width: 25px;
1426 1426 min-width: 25px;
1427 1427 height: 1.2em;
1428 1428 line-height: 1em;
1429 1429 }
1430 1430
1431 1431 .reviewer_name {
1432 1432 display: inline-block;
1433 1433 max-width: 83%;
1434 1434 padding-right: 20px;
1435 1435 vertical-align: middle;
1436 1436 line-height: 1;
1437 1437
1438 1438 .rc-user {
1439 1439 min-width: 0;
1440 1440 margin: -2px 1em 0 0;
1441 1441 }
1442 1442
1443 1443 .reviewer {
1444 1444 float: left;
1445 1445 }
1446 1446 }
1447 1447
1448 1448 .reviewer_member_mandatory {
1449 1449 position: absolute;
1450 1450 left: 15px;
1451 1451 top: 8px;
1452 1452 width: 16px;
1453 1453 font-size: 11px;
1454 1454 margin: 0;
1455 1455 padding: 0;
1456 1456 color: black;
1457 1457 }
1458 1458
1459 1459 .reviewer_member_mandatory_remove,
1460 1460 .reviewer_member_remove {
1461 1461 position: absolute;
1462 1462 right: 0;
1463 1463 top: 0;
1464 1464 width: 16px;
1465 1465 margin-bottom: 10px;
1466 1466 padding: 0;
1467 1467 color: black;
1468 1468 }
1469 1469
1470 1470 .reviewer_member_mandatory_remove {
1471 1471 color: @grey4;
1472 1472 }
1473 1473
1474 1474 .reviewer_member_status {
1475 1475 margin-top: 5px;
1476 1476 }
1477 1477 .pr-summary #summary{
1478 1478 width: 100%;
1479 1479 }
1480 1480 .pr-summary .action_button:hover {
1481 1481 border: 0;
1482 1482 cursor: pointer;
1483 1483 }
1484 1484 .pr-details-title {
1485 1485 padding-bottom: 8px;
1486 1486 border-bottom: @border-thickness solid @grey5;
1487 1487
1488 1488 .action_button.disabled {
1489 1489 color: @grey4;
1490 1490 cursor: inherit;
1491 1491 }
1492 1492 .action_button {
1493 1493 color: @rcblue;
1494 1494 }
1495 1495 }
1496 1496 .pr-details-content {
1497 1497 margin-top: @textmargin;
1498 1498 margin-bottom: @textmargin;
1499 1499 }
1500 1500
1501 1501 .pr-reviewer-rules {
1502 1502 padding: 10px 0px 20px 0px;
1503 1503 }
1504 1504
1505 1505 .group_members {
1506 1506 margin-top: 0;
1507 1507 padding: 0;
1508 1508 list-style: outside none none;
1509 1509
1510 1510 img {
1511 1511 height: @gravatar-size;
1512 1512 width: @gravatar-size;
1513 1513 margin-right: .5em;
1514 1514 margin-left: 3px;
1515 1515 }
1516 1516
1517 1517 .to-delete {
1518 1518 .user {
1519 1519 text-decoration: line-through;
1520 1520 }
1521 1521 }
1522 1522 }
1523 1523
1524 1524 .compare_view_commits_title {
1525 1525 .disabled {
1526 1526 cursor: inherit;
1527 1527 &:hover{
1528 1528 background-color: inherit;
1529 1529 color: inherit;
1530 1530 }
1531 1531 }
1532 1532 }
1533 1533
1534 1534 .subtitle-compare {
1535 1535 margin: -15px 0px 0px 0px;
1536 1536 }
1537 1537
1538 1538 // new entry in group_members
1539 1539 .td-author-new-entry {
1540 1540 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1541 1541 }
1542 1542
1543 1543 .usergroup_member_remove {
1544 1544 width: 16px;
1545 1545 margin-bottom: 10px;
1546 1546 padding: 0;
1547 1547 color: black !important;
1548 1548 cursor: pointer;
1549 1549 }
1550 1550
1551 1551 .reviewer_ac .ac-input {
1552 1552 width: 92%;
1553 1553 margin-bottom: 1em;
1554 1554 }
1555 1555
1556 1556 .compare_view_commits tr{
1557 1557 height: 20px;
1558 1558 }
1559 1559 .compare_view_commits td {
1560 1560 vertical-align: top;
1561 1561 padding-top: 10px;
1562 1562 }
1563 1563 .compare_view_commits .author {
1564 1564 margin-left: 5px;
1565 1565 }
1566 1566
1567 1567 .compare_view_commits {
1568 1568 .color-a {
1569 1569 color: @alert1;
1570 1570 }
1571 1571
1572 1572 .color-c {
1573 1573 color: @color3;
1574 1574 }
1575 1575
1576 1576 .color-r {
1577 1577 color: @color5;
1578 1578 }
1579 1579
1580 1580 .color-a-bg {
1581 1581 background-color: @alert1;
1582 1582 }
1583 1583
1584 1584 .color-c-bg {
1585 1585 background-color: @alert3;
1586 1586 }
1587 1587
1588 1588 .color-r-bg {
1589 1589 background-color: @alert2;
1590 1590 }
1591 1591
1592 1592 .color-a-border {
1593 1593 border: 1px solid @alert1;
1594 1594 }
1595 1595
1596 1596 .color-c-border {
1597 1597 border: 1px solid @alert3;
1598 1598 }
1599 1599
1600 1600 .color-r-border {
1601 1601 border: 1px solid @alert2;
1602 1602 }
1603 1603
1604 1604 .commit-change-indicator {
1605 1605 width: 15px;
1606 1606 height: 15px;
1607 1607 position: relative;
1608 1608 left: 15px;
1609 1609 }
1610 1610
1611 1611 .commit-change-content {
1612 1612 text-align: center;
1613 1613 vertical-align: middle;
1614 1614 line-height: 15px;
1615 1615 }
1616 1616 }
1617 1617
1618 1618 .compare_view_filepath {
1619 1619 color: @grey1;
1620 1620 }
1621 1621
1622 1622 .show_more {
1623 1623 display: inline-block;
1624 1624 width: 0;
1625 1625 height: 0;
1626 1626 vertical-align: middle;
1627 1627 content: "";
1628 1628 border: 4px solid;
1629 1629 border-right-color: transparent;
1630 1630 border-bottom-color: transparent;
1631 1631 border-left-color: transparent;
1632 1632 font-size: 0;
1633 1633 }
1634 1634
1635 1635 .journal_more .show_more {
1636 1636 display: inline;
1637 1637
1638 1638 &:after {
1639 1639 content: none;
1640 1640 }
1641 1641 }
1642 1642
1643 1643 .compare_view_commits .collapse_commit:after {
1644 1644 cursor: pointer;
1645 1645 content: "\00A0\25B4";
1646 1646 margin-left: -3px;
1647 1647 font-size: 17px;
1648 1648 color: @grey4;
1649 1649 }
1650 1650
1651 1651 .diff_links {
1652 1652 margin-left: 8px;
1653 1653 }
1654 1654
1655 1655 #pull_request_overview {
1656 1656 div.ancestor {
1657 1657 margin: -33px 0;
1658 1658 }
1659 1659 }
1660 1660
1661 1661 div.ancestor {
1662 1662 line-height: 33px;
1663 1663 }
1664 1664
1665 1665 .cs_icon_td input[type="checkbox"] {
1666 1666 display: none;
1667 1667 }
1668 1668
1669 1669 .cs_icon_td .expand_file_icon:after {
1670 1670 cursor: pointer;
1671 1671 content: "\00A0\25B6";
1672 1672 font-size: 12px;
1673 1673 color: @grey4;
1674 1674 }
1675 1675
1676 1676 .cs_icon_td .collapse_file_icon:after {
1677 1677 cursor: pointer;
1678 1678 content: "\00A0\25BC";
1679 1679 font-size: 12px;
1680 1680 color: @grey4;
1681 1681 }
1682 1682
1683 1683 /*new binary
1684 1684 NEW_FILENODE = 1
1685 1685 DEL_FILENODE = 2
1686 1686 MOD_FILENODE = 3
1687 1687 RENAMED_FILENODE = 4
1688 1688 COPIED_FILENODE = 5
1689 1689 CHMOD_FILENODE = 6
1690 1690 BIN_FILENODE = 7
1691 1691 */
1692 1692 .cs_files_expand {
1693 1693 font-size: @basefontsize + 5px;
1694 1694 line-height: 1.8em;
1695 1695 float: right;
1696 1696 }
1697 1697
1698 1698 .cs_files_expand span{
1699 1699 color: @rcblue;
1700 1700 cursor: pointer;
1701 1701 }
1702 1702 .cs_files {
1703 1703 clear: both;
1704 1704 padding-bottom: @padding;
1705 1705
1706 1706 .cur_cs {
1707 1707 margin: 10px 2px;
1708 1708 font-weight: bold;
1709 1709 }
1710 1710
1711 1711 .node {
1712 1712 float: left;
1713 1713 }
1714 1714
1715 1715 .changes {
1716 1716 float: right;
1717 1717 color: white;
1718 1718 font-size: @basefontsize - 4px;
1719 1719 margin-top: 4px;
1720 1720 opacity: 0.6;
1721 1721 filter: Alpha(opacity=60); /* IE8 and earlier */
1722 1722
1723 1723 .added {
1724 1724 background-color: @alert1;
1725 1725 float: left;
1726 1726 text-align: center;
1727 1727 }
1728 1728
1729 1729 .deleted {
1730 1730 background-color: @alert2;
1731 1731 float: left;
1732 1732 text-align: center;
1733 1733 }
1734 1734
1735 1735 .bin {
1736 1736 background-color: @alert1;
1737 1737 text-align: center;
1738 1738 }
1739 1739
1740 1740 /*new binary*/
1741 1741 .bin.bin1 {
1742 1742 background-color: @alert1;
1743 1743 text-align: center;
1744 1744 }
1745 1745
1746 1746 /*deleted binary*/
1747 1747 .bin.bin2 {
1748 1748 background-color: @alert2;
1749 1749 text-align: center;
1750 1750 }
1751 1751
1752 1752 /*mod binary*/
1753 1753 .bin.bin3 {
1754 1754 background-color: @grey2;
1755 1755 text-align: center;
1756 1756 }
1757 1757
1758 1758 /*rename file*/
1759 1759 .bin.bin4 {
1760 1760 background-color: @alert4;
1761 1761 text-align: center;
1762 1762 }
1763 1763
1764 1764 /*copied file*/
1765 1765 .bin.bin5 {
1766 1766 background-color: @alert4;
1767 1767 text-align: center;
1768 1768 }
1769 1769
1770 1770 /*chmod file*/
1771 1771 .bin.bin6 {
1772 1772 background-color: @grey2;
1773 1773 text-align: center;
1774 1774 }
1775 1775 }
1776 1776 }
1777 1777
1778 1778 .cs_files .cs_added, .cs_files .cs_A,
1779 1779 .cs_files .cs_added, .cs_files .cs_M,
1780 1780 .cs_files .cs_added, .cs_files .cs_D {
1781 1781 height: 16px;
1782 1782 padding-right: 10px;
1783 1783 margin-top: 7px;
1784 1784 text-align: left;
1785 1785 }
1786 1786
1787 1787 .cs_icon_td {
1788 1788 min-width: 16px;
1789 1789 width: 16px;
1790 1790 }
1791 1791
1792 1792 .pull-request-merge {
1793 1793 border: 1px solid @grey5;
1794 1794 padding: 10px 0px 20px;
1795 1795 margin-top: 10px;
1796 1796 margin-bottom: 20px;
1797 1797 }
1798 1798
1799 1799 .pull-request-merge-refresh {
1800 1800 margin: 2px 7px;
1801 1801 }
1802 1802
1803 1803 .pull-request-merge ul {
1804 1804 padding: 0px 0px;
1805 1805 }
1806 1806
1807 1807 .pull-request-merge li {
1808 1808 list-style-type: none;
1809 1809 }
1810 1810
1811 1811 .pull-request-merge .pull-request-wrap {
1812 1812 height: auto;
1813 1813 padding: 0px 0px;
1814 1814 text-align: right;
1815 1815 }
1816 1816
1817 1817 .pull-request-merge span {
1818 1818 margin-right: 5px;
1819 1819 }
1820 1820
1821 1821 .pull-request-merge-actions {
1822 1822 min-height: 30px;
1823 1823 padding: 0px 0px;
1824 1824 }
1825 1825
1826 1826 .pull-request-merge-info {
1827 1827 padding: 0px 5px 5px 0px;
1828 1828 }
1829 1829
1830 1830 .merge-status {
1831 1831 margin-right: 5px;
1832 1832 }
1833 1833
1834 1834 .merge-message {
1835 1835 font-size: 1.2em
1836 1836 }
1837 1837
1838 1838 .merge-message.success i,
1839 1839 .merge-icon.success i {
1840 1840 color:@alert1;
1841 1841 }
1842 1842
1843 1843 .merge-message.warning i,
1844 1844 .merge-icon.warning i {
1845 1845 color: @alert3;
1846 1846 }
1847 1847
1848 1848 .merge-message.error i,
1849 1849 .merge-icon.error i {
1850 1850 color:@alert2;
1851 1851 }
1852 1852
1853 1853 .pr-versions {
1854 1854 font-size: 1.1em;
1855 1855
1856 1856 table {
1857 1857 padding: 0px 5px;
1858 1858 }
1859 1859
1860 1860 td {
1861 1861 line-height: 15px;
1862 1862 }
1863 1863
1864 1864 .compare-radio-button {
1865 1865 position: relative;
1866 1866 top: -3px;
1867 1867 }
1868 1868 }
1869 1869
1870 1870
1871 1871 #close_pull_request {
1872 1872 margin-right: 0px;
1873 1873 }
1874 1874
1875 1875 .empty_data {
1876 1876 color: @grey4;
1877 1877 }
1878 1878
1879 1879 #changeset_compare_view_content {
1880 1880 clear: both;
1881 1881 width: 100%;
1882 1882 box-sizing: border-box;
1883 1883 .border-radius(@border-radius);
1884 1884
1885 1885 .help-block {
1886 1886 margin: @padding 0;
1887 1887 color: @text-color;
1888 1888 &.pre-formatting {
1889 1889 white-space: pre;
1890 1890 }
1891 1891 }
1892 1892
1893 1893 .empty_data {
1894 1894 margin: @padding 0;
1895 1895 }
1896 1896
1897 1897 .alert {
1898 1898 margin-bottom: @space;
1899 1899 }
1900 1900 }
1901 1901
1902 1902 .table_disp {
1903 1903 .status {
1904 1904 width: auto;
1905 1905 }
1906 1906 }
1907 1907
1908 1908
1909 1909 .creation_in_progress {
1910 1910 color: @grey4
1911 1911 }
1912 1912
1913 1913 .status_box_menu {
1914 1914 margin: 0;
1915 1915 }
1916 1916
1917 1917 .notification-table{
1918 1918 margin-bottom: @space;
1919 1919 display: table;
1920 1920 width: 100%;
1921 1921
1922 1922 .container{
1923 1923 display: table-row;
1924 1924
1925 1925 .notification-header{
1926 1926 border-bottom: @border-thickness solid @border-default-color;
1927 1927 }
1928 1928
1929 1929 .notification-subject{
1930 1930 display: table-cell;
1931 1931 }
1932 1932 }
1933 1933 }
1934 1934
1935 1935 // Notifications
1936 1936 .notification-header{
1937 1937 display: table;
1938 1938 width: 100%;
1939 1939 padding: floor(@basefontsize/2) 0;
1940 1940 line-height: 1em;
1941 1941
1942 1942 .desc, .delete-notifications, .read-notifications{
1943 1943 display: table-cell;
1944 1944 text-align: left;
1945 1945 }
1946 1946
1947 1947 .desc{
1948 1948 width: 1163px;
1949 1949 }
1950 1950
1951 1951 .delete-notifications, .read-notifications{
1952 1952 width: 35px;
1953 1953 min-width: 35px; //fixes when only one button is displayed
1954 1954 }
1955 1955 }
1956 1956
1957 1957 .notification-body {
1958 1958 .markdown-block,
1959 1959 .rst-block {
1960 1960 padding: @padding 0;
1961 1961 }
1962 1962
1963 1963 .notification-subject {
1964 1964 padding: @textmargin 0;
1965 1965 border-bottom: @border-thickness solid @border-default-color;
1966 1966 }
1967 1967 }
1968 1968
1969 1969
1970 1970 .notifications_buttons{
1971 1971 float: right;
1972 1972 }
1973 1973
1974 1974 #notification-status{
1975 1975 display: inline;
1976 1976 }
1977 1977
1978 1978 // Repositories
1979 1979
1980 1980 #summary.fields{
1981 1981 display: table;
1982 1982
1983 1983 .field{
1984 1984 display: table-row;
1985 1985
1986 1986 .label-summary{
1987 1987 display: table-cell;
1988 1988 min-width: @label-summary-minwidth;
1989 1989 padding-top: @padding/2;
1990 1990 padding-bottom: @padding/2;
1991 1991 padding-right: @padding/2;
1992 1992 }
1993 1993
1994 1994 .input{
1995 1995 display: table-cell;
1996 1996 padding: @padding/2;
1997 1997
1998 1998 input{
1999 1999 min-width: 29em;
2000 2000 padding: @padding/4;
2001 2001 }
2002 2002 }
2003 2003 .statistics, .downloads{
2004 2004 .disabled{
2005 2005 color: @grey4;
2006 2006 }
2007 2007 }
2008 2008 }
2009 2009 }
2010 2010
2011 2011 #summary{
2012 2012 width: 70%;
2013 2013 }
2014 2014
2015 2015
2016 2016 // Journal
2017 2017 .journal.title {
2018 2018 h5 {
2019 2019 float: left;
2020 2020 margin: 0;
2021 2021 width: 70%;
2022 2022 }
2023 2023
2024 2024 ul {
2025 2025 float: right;
2026 2026 display: inline-block;
2027 2027 margin: 0;
2028 2028 width: 30%;
2029 2029 text-align: right;
2030 2030
2031 2031 li {
2032 2032 display: inline;
2033 2033 font-size: @journal-fontsize;
2034 2034 line-height: 1em;
2035 2035
2036 2036 list-style-type: none;
2037 2037 }
2038 2038 }
2039 2039 }
2040 2040
2041 2041 .filterexample {
2042 2042 position: absolute;
2043 2043 top: 95px;
2044 2044 left: @contentpadding;
2045 2045 color: @rcblue;
2046 2046 font-size: 11px;
2047 2047 font-family: @text-regular;
2048 2048 cursor: help;
2049 2049
2050 2050 &:hover {
2051 2051 color: @rcdarkblue;
2052 2052 }
2053 2053
2054 2054 @media (max-width:768px) {
2055 2055 position: relative;
2056 2056 top: auto;
2057 2057 left: auto;
2058 2058 display: block;
2059 2059 }
2060 2060 }
2061 2061
2062 2062
2063 2063 #journal{
2064 2064 margin-bottom: @space;
2065 2065
2066 2066 .journal_day{
2067 2067 margin-bottom: @textmargin/2;
2068 2068 padding-bottom: @textmargin/2;
2069 2069 font-size: @journal-fontsize;
2070 2070 border-bottom: @border-thickness solid @border-default-color;
2071 2071 }
2072 2072
2073 2073 .journal_container{
2074 2074 margin-bottom: @space;
2075 2075
2076 2076 .journal_user{
2077 2077 display: inline-block;
2078 2078 }
2079 2079 .journal_action_container{
2080 2080 display: block;
2081 2081 margin-top: @textmargin;
2082 2082
2083 2083 div{
2084 2084 display: inline;
2085 2085 }
2086 2086
2087 2087 div.journal_action_params{
2088 2088 display: block;
2089 2089 }
2090 2090
2091 2091 div.journal_repo:after{
2092 2092 content: "\A";
2093 2093 white-space: pre;
2094 2094 }
2095 2095
2096 2096 div.date{
2097 2097 display: block;
2098 2098 margin-bottom: @textmargin;
2099 2099 }
2100 2100 }
2101 2101 }
2102 2102 }
2103 2103
2104 2104 // Files
2105 2105 .edit-file-title {
2106 2106 font-size: 16px;
2107 2107
2108 2108 .title-heading {
2109 2109 padding: 2px;
2110 2110 }
2111 2111 }
2112 2112
2113 2113 .edit-file-fieldset {
2114 2114 margin: @sidebarpadding 0;
2115 2115
2116 2116 .fieldset {
2117 2117 .left-label {
2118 2118 width: 13%;
2119 2119 }
2120 2120 .right-content {
2121 2121 width: 87%;
2122 2122 max-width: 100%;
2123 2123 }
2124 2124 .filename-label {
2125 2125 margin-top: 13px;
2126 2126 }
2127 2127 .commit-message-label {
2128 2128 margin-top: 4px;
2129 2129 }
2130 2130 .file-upload-input {
2131 2131 input {
2132 2132 display: none;
2133 2133 }
2134 2134 margin-top: 10px;
2135 2135 }
2136 2136 .file-upload-label {
2137 2137 margin-top: 10px;
2138 2138 }
2139 2139 p {
2140 2140 margin-top: 5px;
2141 2141 }
2142 2142
2143 2143 }
2144 2144 .custom-path-link {
2145 2145 margin-left: 5px;
2146 2146 }
2147 2147 #commit {
2148 2148 resize: vertical;
2149 2149 }
2150 2150 }
2151 2151
2152 2152 .delete-file-preview {
2153 2153 max-height: 250px;
2154 2154 }
2155 2155
2156 2156 .new-file,
2157 2157 #filter_activate,
2158 2158 #filter_deactivate {
2159 2159 float: right;
2160 2160 margin: 0 0 0 10px;
2161 2161 }
2162 2162
2163 2163 .file-upload-transaction-wrapper {
2164 2164 margin-top: 57px;
2165 2165 clear: both;
2166 2166 }
2167 2167
2168 2168 .file-upload-transaction-wrapper .error {
2169 2169 color: @color5;
2170 2170 }
2171 2171
2172 2172 .file-upload-transaction {
2173 2173 min-height: 200px;
2174 2174 padding: 54px;
2175 2175 border: 1px solid @grey5;
2176 2176 text-align: center;
2177 2177 clear: both;
2178 2178 }
2179 2179
2180 2180 .file-upload-transaction i {
2181 2181 font-size: 48px
2182 2182 }
2183 2183
2184 2184 h3.files_location{
2185 2185 line-height: 2.4em;
2186 2186 }
2187 2187
2188 2188 .browser-nav {
2189 2189 width: 100%;
2190 2190 display: table;
2191 2191 margin-bottom: 20px;
2192 2192
2193 2193 .info_box {
2194 2194 float: left;
2195 2195 display: inline-table;
2196 2196 height: 2.5em;
2197 2197
2198 2198 .browser-cur-rev, .info_box_elem {
2199 2199 display: table-cell;
2200 2200 vertical-align: middle;
2201 2201 }
2202 2202
2203 2203 .drop-menu {
2204 2204 margin: 0 10px;
2205 2205 }
2206 2206
2207 2207 .info_box_elem {
2208 2208 border-top: @border-thickness solid @grey5;
2209 2209 border-bottom: @border-thickness solid @grey5;
2210 2210 box-shadow: @button-shadow;
2211 2211
2212 2212 #at_rev, a {
2213 2213 padding: 0.6em 0.4em;
2214 2214 margin: 0;
2215 2215 .box-shadow(none);
2216 2216 border: 0;
2217 2217 height: 12px;
2218 2218 color: @grey2;
2219 2219 }
2220 2220
2221 2221 input#at_rev {
2222 2222 max-width: 50px;
2223 2223 text-align: center;
2224 2224 }
2225 2225
2226 2226 &.previous {
2227 2227 border: @border-thickness solid @grey5;
2228 2228 border-top-left-radius: @border-radius;
2229 2229 border-bottom-left-radius: @border-radius;
2230 2230
2231 2231 &:hover {
2232 2232 border-color: @grey4;
2233 2233 }
2234 2234
2235 2235 .disabled {
2236 2236 color: @grey5;
2237 2237 cursor: not-allowed;
2238 2238 opacity: 0.5;
2239 2239 }
2240 2240 }
2241 2241
2242 2242 &.next {
2243 2243 border: @border-thickness solid @grey5;
2244 2244 border-top-right-radius: @border-radius;
2245 2245 border-bottom-right-radius: @border-radius;
2246 2246
2247 2247 &:hover {
2248 2248 border-color: @grey4;
2249 2249 }
2250 2250
2251 2251 .disabled {
2252 2252 color: @grey5;
2253 2253 cursor: not-allowed;
2254 2254 opacity: 0.5;
2255 2255 }
2256 2256 }
2257 2257 }
2258 2258
2259 2259 .browser-cur-rev {
2260 2260
2261 2261 span{
2262 2262 margin: 0;
2263 2263 color: @rcblue;
2264 2264 height: 12px;
2265 2265 display: inline-block;
2266 2266 padding: 0.7em 1em ;
2267 2267 border: @border-thickness solid @rcblue;
2268 2268 margin-right: @padding;
2269 2269 }
2270 2270 }
2271 2271
2272 2272 }
2273 2273
2274 2274 .select-index-number {
2275 2275 margin: 0 0 0 20px;
2276 2276 color: @grey3;
2277 2277 }
2278 2278
2279 2279 .search_activate {
2280 2280 display: table-cell;
2281 2281 vertical-align: middle;
2282 2282
2283 2283 input, label{
2284 2284 margin: 0;
2285 2285 padding: 0;
2286 2286 }
2287 2287
2288 2288 input{
2289 2289 margin-left: @textmargin;
2290 2290 }
2291 2291
2292 2292 }
2293 2293 }
2294 2294
2295 2295 .browser-cur-rev{
2296 2296 margin-bottom: @textmargin;
2297 2297 }
2298 2298
2299 2299 #node_filter_box_loading{
2300 2300 .info_text;
2301 2301 }
2302 2302
2303 2303 .browser-search {
2304 2304 margin: -25px 0px 5px 0px;
2305 2305 }
2306 2306
2307 2307 .files-quick-filter {
2308 2308 float: right;
2309 2309 width: 180px;
2310 2310 position: relative;
2311 2311 }
2312 2312
2313 2313 .files-filter-box {
2314 2314 display: flex;
2315 2315 padding: 0px;
2316 2316 border-radius: 3px;
2317 2317 margin-bottom: 0;
2318 2318
2319 2319 a {
2320 2320 border: none !important;
2321 2321 }
2322 2322
2323 2323 li {
2324 2324 list-style-type: none
2325 2325 }
2326 2326 }
2327 2327
2328 2328 .files-filter-box-path {
2329 2329 line-height: 33px;
2330 2330 padding: 0;
2331 2331 width: 20px;
2332 2332 position: absolute;
2333 2333 z-index: 11;
2334 2334 left: 5px;
2335 2335 }
2336 2336
2337 2337 .files-filter-box-input {
2338 2338 margin-right: 0;
2339 2339
2340 2340 input {
2341 2341 border: 1px solid @white;
2342 2342 padding-left: 25px;
2343 2343 width: 145px;
2344 2344
2345 2345 &:hover {
2346 2346 border-color: @grey6;
2347 2347 }
2348 2348
2349 2349 &:focus {
2350 2350 border-color: @grey5;
2351 2351 }
2352 2352 }
2353 2353 }
2354 2354
2355 2355 .browser-result{
2356 2356 td a{
2357 2357 margin-left: 0.5em;
2358 2358 display: inline-block;
2359 2359
2360 2360 em {
2361 2361 font-weight: @text-bold-weight;
2362 2362 font-family: @text-bold;
2363 2363 }
2364 2364 }
2365 2365 }
2366 2366
2367 2367 .browser-highlight{
2368 2368 background-color: @grey5-alpha;
2369 2369 }
2370 2370
2371 2371
2372 2372 .edit-file-fieldset #location,
2373 2373 .edit-file-fieldset #filename {
2374 2374 display: flex;
2375 2375 width: -moz-available; /* WebKit-based browsers will ignore this. */
2376 2376 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2377 2377 width: fill-available;
2378 2378 border: 0;
2379 2379 }
2380 2380
2381 2381 .path-items {
2382 2382 display: flex;
2383 2383 padding: 0;
2384 2384 border: 1px solid #eeeeee;
2385 2385 width: 100%;
2386 2386 float: left;
2387 2387
2388 2388 .breadcrumb-path {
2389 2389 line-height: 30px;
2390 2390 padding: 0 4px;
2391 2391 white-space: nowrap;
2392 2392 }
2393 2393
2394 2394 .location-path {
2395 2395 width: -moz-available; /* WebKit-based browsers will ignore this. */
2396 2396 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2397 2397 width: fill-available;
2398 2398
2399 2399 .file-name-input {
2400 2400 padding: 0.5em 0;
2401 2401 }
2402 2402
2403 2403 }
2404 2404
2405 2405 ul {
2406 2406 display: flex;
2407 2407 margin: 0;
2408 2408 padding: 0;
2409 2409 width: 100%;
2410 2410 }
2411 2411
2412 2412 li {
2413 2413 list-style-type: none;
2414 2414 }
2415 2415
2416 2416 }
2417 2417
2418 2418 .editor-items {
2419 2419 height: 40px;
2420 2420 margin: 10px 0 -17px 10px;
2421 2421
2422 2422 .editor-action {
2423 2423 cursor: pointer;
2424 2424 }
2425 2425
2426 2426 .editor-action.active {
2427 2427 border-bottom: 2px solid #5C5C5C;
2428 2428 }
2429 2429
2430 2430 li {
2431 2431 list-style-type: none;
2432 2432 }
2433 2433 }
2434 2434
2435 2435 .edit-file-fieldset .message textarea {
2436 2436 border: 1px solid #eeeeee;
2437 2437 }
2438 2438
2439 2439 #files_data .codeblock {
2440 2440 background-color: #F5F5F5;
2441 2441 }
2442 2442
2443 2443 #editor_preview {
2444 2444 background: white;
2445 2445 }
2446 2446
2447 2447 .show-editor {
2448 2448 padding: 10px;
2449 2449 background-color: white;
2450 2450
2451 2451 }
2452 2452
2453 2453 .show-preview {
2454 2454 padding: 10px;
2455 2455 background-color: white;
2456 2456 border-left: 1px solid #eeeeee;
2457 2457 }
2458 2458 // quick filter
2459 2459 .grid-quick-filter {
2460 2460 float: right;
2461 2461 position: relative;
2462 2462 }
2463 2463
2464 2464 .grid-filter-box {
2465 2465 display: flex;
2466 2466 padding: 0px;
2467 2467 border-radius: 3px;
2468 2468 margin-bottom: 0;
2469 2469
2470 2470 a {
2471 2471 border: none !important;
2472 2472 }
2473 2473
2474 2474 li {
2475 2475 list-style-type: none
2476 2476 }
2477 2477 }
2478 2478
2479 2479 .grid-filter-box-icon {
2480 2480 line-height: 33px;
2481 2481 padding: 0;
2482 2482 width: 20px;
2483 2483 position: absolute;
2484 2484 z-index: 11;
2485 2485 left: 5px;
2486 2486 }
2487 2487
2488 2488 .grid-filter-box-input {
2489 2489 margin-right: 0;
2490 2490
2491 2491 input {
2492 2492 border: 1px solid @white;
2493 2493 padding-left: 25px;
2494 2494 width: 145px;
2495 2495
2496 2496 &:hover {
2497 2497 border-color: @grey6;
2498 2498 }
2499 2499
2500 2500 &:focus {
2501 2501 border-color: @grey5;
2502 2502 }
2503 2503 }
2504 2504 }
2505 2505
2506 2506
2507 2507
2508 2508 // Search
2509 2509
2510 2510 .search-form{
2511 2511 #q {
2512 2512 width: @search-form-width;
2513 2513 }
2514 2514 .fields{
2515 2515 margin: 0 0 @space;
2516 2516 }
2517 2517
2518 2518 label{
2519 2519 display: inline-block;
2520 2520 margin-right: @textmargin;
2521 2521 padding-top: 0.25em;
2522 2522 }
2523 2523
2524 2524
2525 2525 .results{
2526 2526 clear: both;
2527 2527 margin: 0 0 @padding;
2528 2528 }
2529 2529
2530 2530 .search-tags {
2531 2531 padding: 5px 0;
2532 2532 }
2533 2533 }
2534 2534
2535 2535 div.search-feedback-items {
2536 2536 display: inline-block;
2537 2537 }
2538 2538
2539 2539 div.search-code-body {
2540 2540 background-color: #ffffff; padding: 5px 0 5px 10px;
2541 2541 pre {
2542 2542 .match { background-color: #faffa6;}
2543 2543 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2544 2544 }
2545 2545 }
2546 2546
2547 2547 .expand_commit.search {
2548 2548 .show_more.open {
2549 2549 height: auto;
2550 2550 max-height: none;
2551 2551 }
2552 2552 }
2553 2553
2554 2554 .search-results {
2555 2555
2556 2556 h2 {
2557 2557 margin-bottom: 0;
2558 2558 }
2559 2559 .codeblock {
2560 2560 border: none;
2561 2561 background: transparent;
2562 2562 }
2563 2563
2564 2564 .codeblock-header {
2565 2565 border: none;
2566 2566 background: transparent;
2567 2567 }
2568 2568
2569 2569 .code-body {
2570 2570 border: @border-thickness solid @grey6;
2571 2571 .border-radius(@border-radius);
2572 2572 }
2573 2573
2574 2574 .td-commit {
2575 2575 &:extend(pre);
2576 2576 border-bottom: @border-thickness solid @border-default-color;
2577 2577 }
2578 2578
2579 2579 .message {
2580 2580 height: auto;
2581 2581 max-width: 350px;
2582 2582 white-space: normal;
2583 2583 text-overflow: initial;
2584 2584 overflow: visible;
2585 2585
2586 2586 .match { background-color: #faffa6;}
2587 2587 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2588 2588 }
2589 2589
2590 2590 .path {
2591 2591 border-bottom: none !important;
2592 2592 border-left: 1px solid @grey6 !important;
2593 2593 border-right: 1px solid @grey6 !important;
2594 2594 }
2595 2595 }
2596 2596
2597 2597 table.rctable td.td-search-results div {
2598 2598 max-width: 100%;
2599 2599 }
2600 2600
2601 2601 #tip-box, .tip-box{
2602 2602 padding: @menupadding/2;
2603 2603 display: block;
2604 2604 border: @border-thickness solid @border-highlight-color;
2605 2605 .border-radius(@border-radius);
2606 2606 background-color: white;
2607 2607 z-index: 99;
2608 2608 white-space: pre-wrap;
2609 2609 }
2610 2610
2611 2611 #linktt {
2612 2612 width: 79px;
2613 2613 }
2614 2614
2615 2615 #help_kb .modal-content{
2616 2616 max-width: 750px;
2617 2617 margin: 10% auto;
2618 2618
2619 2619 table{
2620 2620 td,th{
2621 2621 border-bottom: none;
2622 2622 line-height: 2.5em;
2623 2623 }
2624 2624 th{
2625 2625 padding-bottom: @textmargin/2;
2626 2626 }
2627 2627 td.keys{
2628 2628 text-align: center;
2629 2629 }
2630 2630 }
2631 2631
2632 2632 .block-left{
2633 2633 width: 45%;
2634 2634 margin-right: 5%;
2635 2635 }
2636 2636 .modal-footer{
2637 2637 clear: both;
2638 2638 }
2639 2639 .key.tag{
2640 2640 padding: 0.5em;
2641 2641 background-color: @rcblue;
2642 2642 color: white;
2643 2643 border-color: @rcblue;
2644 2644 .box-shadow(none);
2645 2645 }
2646 2646 }
2647 2647
2648 2648
2649 2649
2650 2650 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2651 2651
2652 2652 @import 'statistics-graph';
2653 2653 @import 'tables';
2654 2654 @import 'forms';
2655 2655 @import 'diff';
2656 2656 @import 'summary';
2657 2657 @import 'navigation';
2658 2658
2659 2659 //--- SHOW/HIDE SECTIONS --//
2660 2660
2661 2661 .btn-collapse {
2662 2662 float: right;
2663 2663 text-align: right;
2664 2664 font-family: @text-light;
2665 2665 font-size: @basefontsize;
2666 2666 cursor: pointer;
2667 2667 border: none;
2668 2668 color: @rcblue;
2669 2669 }
2670 2670
2671 2671 table.rctable,
2672 2672 table.dataTable {
2673 2673 .btn-collapse {
2674 2674 float: right;
2675 2675 text-align: right;
2676 2676 }
2677 2677 }
2678 2678
2679 2679 table.rctable {
2680 2680 &.permissions {
2681 2681
2682 2682 th.td-owner {
2683 2683 padding: 0;
2684 2684 }
2685 2685
2686 2686 th {
2687 2687 font-weight: normal;
2688 2688 padding: 0 5px;
2689 2689 }
2690 2690
2691 2691 }
2692 2692 }
2693 2693
2694 2694
2695 2695 // TODO: johbo: Fix for IE10, this avoids that we see a border
2696 2696 // and padding around checkboxes and radio boxes. Move to the right place,
2697 2697 // or better: Remove this once we did the form refactoring.
2698 2698 input[type=checkbox],
2699 2699 input[type=radio] {
2700 2700 padding: 0;
2701 2701 border: none;
2702 2702 }
2703 2703
2704 2704 .toggle-ajax-spinner{
2705 2705 height: 16px;
2706 2706 width: 16px;
2707 2707 }
2708 2708
2709 2709
2710 2710 .markup-form .clearfix {
2711 2711 .border-radius(@border-radius);
2712 2712 margin: 0px;
2713 2713 }
2714 2714
2715 2715 .markup-form-area {
2716 2716 padding: 8px 12px;
2717 2717 border: 1px solid @grey4;
2718 2718 .border-radius(@border-radius);
2719 2719 }
2720 2720
2721 2721 .markup-form-area-header .nav-links {
2722 2722 display: flex;
2723 2723 flex-flow: row wrap;
2724 2724 -webkit-flex-flow: row wrap;
2725 2725 width: 100%;
2726 2726 }
2727 2727
2728 2728 .markup-form-area-footer {
2729 2729 display: flex;
2730 2730 }
2731 2731
2732 2732 .markup-form-area-footer .toolbar {
2733 2733
2734 2734 }
2735 2735
2736 2736 // markup Form
2737 2737 div.markup-form {
2738 2738 margin-top: 20px;
2739 2739 }
2740 2740
2741 2741 .markup-form strong {
2742 2742 display: block;
2743 2743 margin-bottom: 15px;
2744 2744 }
2745 2745
2746 2746 .markup-form textarea {
2747 2747 width: 100%;
2748 2748 height: 100px;
2749 2749 font-family: @text-monospace;
2750 2750 }
2751 2751
2752 2752 form.markup-form {
2753 2753 margin-top: 10px;
2754 2754 margin-left: 10px;
2755 2755 }
2756 2756
2757 2757 .markup-form .comment-block-ta,
2758 2758 .markup-form .preview-box {
2759 2759 .border-radius(@border-radius);
2760 2760 .box-sizing(border-box);
2761 2761 background-color: white;
2762 2762 }
2763 2763
2764 2764 .markup-form .preview-box.unloaded {
2765 2765 height: 50px;
2766 2766 text-align: center;
2767 2767 padding: 20px;
2768 2768 background-color: white;
2769 2769 }
2770 2770
2771 2771
2772 2772 .dropzone-wrapper {
2773 2773 border: 1px solid @grey5;
2774 2774 padding: 20px;
2775 2775 }
2776 2776
2777 2777 .dropzone,
2778 2778 .dropzone-pure {
2779 2779 border: 2px dashed @grey5;
2780 2780 border-radius: 5px;
2781 2781 background: white;
2782 2782 min-height: 200px;
2783 2783 padding: 54px;
2784 2784
2785 2785 .dz-message {
2786 2786 font-weight: 700;
2787 2787 text-align: center;
2788 2788 margin: 2em 0;
2789 2789 }
2790 2790
2791 2791 }
2792 2792
2793 2793 .dz-preview {
2794 2794 margin: 10px 0 !important;
2795 2795 position: relative;
2796 2796 vertical-align: top;
2797 2797 padding: 10px;
2798 2798 border-bottom: 1px solid @grey5;
2799 2799 }
2800 2800
2801 2801 .dz-filename {
2802 2802 font-weight: 700;
2803 2803 float: left;
2804 2804 }
2805 2805
2806 2806 .dz-sending {
2807 2807 float: right;
2808 2808 }
2809 2809
2810 2810 .dz-response {
2811 2811 clear: both
2812 2812 }
2813 2813
2814 2814 .dz-filename-size {
2815 2815 float: right
2816 2816 }
2817 2817
2818 2818 .dz-error-message {
2819 2819 color: @alert2;
2820 2820 padding-top: 10px;
2821 2821 clear: both;
2822 2822 }
2823 2823
2824 2824
2825 2825 .user-hovercard {
2826 2826 padding: 5px;
2827 2827 }
2828 2828
2829 2829 .user-hovercard-icon {
2830 2830 display: inline;
2831 2831 padding: 0;
2832 2832 box-sizing: content-box;
2833 2833 border-radius: 50%;
2834 2834 float: left;
2835 2835 }
2836 2836
2837 2837 .user-hovercard-name {
2838 2838 float: right;
2839 2839 vertical-align: top;
2840 2840 padding-left: 10px;
2841 2841 min-width: 150px;
2842 2842 }
2843 2843
2844 2844 .user-hovercard-bio {
2845 2845 clear: both;
2846 2846 padding-top: 10px;
2847 2847 }
2848 2848
2849 2849 .user-hovercard-header {
2850 2850 clear: both;
2851 2851 min-height: 10px;
2852 2852 }
2853 2853
2854 2854 .user-hovercard-footer {
2855 2855 clear: both;
2856 2856 min-height: 10px;
2857 2857 }
2858 2858
2859 2859 .user-group-hovercard {
2860 2860 padding: 5px;
2861 2861 }
2862 2862
2863 2863 .user-group-hovercard-icon {
2864 2864 display: inline;
2865 2865 padding: 0;
2866 2866 box-sizing: content-box;
2867 2867 border-radius: 50%;
2868 2868 float: left;
2869 2869 }
2870 2870
2871 2871 .user-group-hovercard-name {
2872 2872 float: left;
2873 2873 vertical-align: top;
2874 2874 padding-left: 10px;
2875 2875 min-width: 150px;
2876 2876 }
2877 2877
2878 2878 .user-group-hovercard-icon i {
2879 2879 border: 1px solid @grey4;
2880 2880 border-radius: 4px;
2881 2881 }
2882 2882
2883 2883 .user-group-hovercard-bio {
2884 2884 clear: both;
2885 2885 padding-top: 10px;
2886 2886 line-height: 1.0em;
2887 2887 }
2888 2888
2889 2889 .user-group-hovercard-header {
2890 2890 clear: both;
2891 2891 min-height: 10px;
2892 2892 }
2893 2893
2894 2894 .user-group-hovercard-footer {
2895 2895 clear: both;
2896 2896 min-height: 10px;
2897 2897 }
2898
2899 .pr-hovercard-header {
2900 clear: both;
2901 display: block;
2902 line-height: 20px;
2903 }
2904
2905 .pr-hovercard-user {
2906 display: flex;
2907 align-items: center;
2908 padding-left: 5px;
2909 }
2910
2911 .pr-hovercard-title {
2912 padding-top: 5px;
2913 } No newline at end of file
@@ -1,387 +1,388 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_pull_request', '/_hovercard/pull_request/%(pull_request_id)s', ['pull_request_id']);
35 36 pyroutes.register('hovercard_repo_commit', '/_hovercard/commit/%(repo_name)s/%(commit_id)s', ['repo_name', 'commit_id']);
36 37 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
37 38 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
38 39 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
39 40 pyroutes.register('ops_ping_legacy', '/_admin/ping', []);
40 41 pyroutes.register('ops_error_test_legacy', '/_admin/error_test', []);
41 42 pyroutes.register('admin_home', '/_admin', []);
42 43 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
43 44 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
44 45 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
45 46 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
46 47 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
47 48 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
48 49 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
49 50 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
50 51 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
51 52 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
52 53 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions/delete', []);
53 54 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
54 55 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
55 56 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
56 57 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
57 58 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
58 59 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
59 60 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
60 61 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
61 62 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
62 63 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
63 64 pyroutes.register('admin_settings', '/_admin/settings', []);
64 65 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
65 66 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
66 67 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
67 68 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
68 69 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
69 70 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
70 71 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
71 72 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
72 73 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
73 74 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
74 75 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
75 76 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
76 77 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
77 78 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
78 79 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
79 80 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
80 81 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
81 82 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
82 83 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
83 84 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
84 85 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
85 86 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
86 87 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
87 88 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
88 89 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
89 90 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
90 91 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
91 92 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
92 93 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
93 94 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
94 95 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
95 96 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
96 97 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
97 98 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
98 99 pyroutes.register('users', '/_admin/users', []);
99 100 pyroutes.register('users_data', '/_admin/users_data', []);
100 101 pyroutes.register('users_create', '/_admin/users/create', []);
101 102 pyroutes.register('users_new', '/_admin/users/new', []);
102 103 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
103 104 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
104 105 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
105 106 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
106 107 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
107 108 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
108 109 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
109 110 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
110 111 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
111 112 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
112 113 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
113 114 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
114 115 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
115 116 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
116 117 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
117 118 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
118 119 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
119 120 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
120 121 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
121 122 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
122 123 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
123 124 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
124 125 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
125 126 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
126 127 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
127 128 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
128 129 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
129 130 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
130 131 pyroutes.register('user_groups', '/_admin/user_groups', []);
131 132 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
132 133 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
133 134 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
134 135 pyroutes.register('repos', '/_admin/repos', []);
135 136 pyroutes.register('repo_new', '/_admin/repos/new', []);
136 137 pyroutes.register('repo_create', '/_admin/repos/create', []);
137 138 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
138 139 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
139 140 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
140 141 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
141 142 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
142 143 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
143 144 pyroutes.register('channelstream_proxy', '/_channelstream', []);
144 145 pyroutes.register('upload_file', '/_file_store/upload', []);
145 146 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
146 147 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
147 148 pyroutes.register('logout', '/_admin/logout', []);
148 149 pyroutes.register('reset_password', '/_admin/password_reset', []);
149 150 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
150 151 pyroutes.register('home', '/', []);
151 152 pyroutes.register('user_autocomplete_data', '/_users', []);
152 153 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
153 154 pyroutes.register('repo_list_data', '/_repos', []);
154 155 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
155 156 pyroutes.register('goto_switcher_data', '/_goto_data', []);
156 157 pyroutes.register('markup_preview', '/_markup_preview', []);
157 158 pyroutes.register('file_preview', '/_file_preview', []);
158 159 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
159 160 pyroutes.register('journal', '/_admin/journal', []);
160 161 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
161 162 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
162 163 pyroutes.register('journal_public', '/_admin/public_journal', []);
163 164 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
164 165 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
165 166 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
166 167 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
167 168 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
168 169 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
169 170 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
170 171 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
171 172 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
172 173 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
173 174 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
174 175 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
175 176 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
176 177 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
177 178 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
178 179 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
179 180 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
180 181 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
181 182 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
182 183 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 184 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
184 185 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
185 186 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
186 187 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
187 188 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
188 189 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
189 190 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
190 191 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
191 192 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
192 193 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
193 194 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
194 195 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
195 196 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
196 197 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
197 198 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
198 199 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
199 200 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
200 201 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
201 202 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
202 203 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
203 204 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
204 205 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
205 206 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
206 207 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
207 208 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
208 209 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
209 210 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
210 211 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
211 212 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
212 213 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
213 214 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
214 215 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
215 216 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
216 217 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
217 218 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
218 219 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 220 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
220 221 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
221 222 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
222 223 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
223 224 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
224 225 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
225 226 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
226 227 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
227 228 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
228 229 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
229 230 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
230 231 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
231 232 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
232 233 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
233 234 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
234 235 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
235 236 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
236 237 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
237 238 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 239 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
239 240 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
240 241 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
241 242 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
242 243 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
243 244 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
244 245 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
245 246 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
246 247 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
247 248 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
248 249 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
249 250 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
250 251 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
251 252 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
252 253 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
253 254 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
254 255 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
255 256 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
256 257 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
257 258 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
258 259 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
259 260 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
260 261 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
261 262 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
262 263 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
263 264 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
264 265 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
265 266 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
266 267 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
267 268 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
268 269 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
269 270 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
270 271 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
271 272 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
272 273 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
273 274 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
274 275 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
275 276 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
276 277 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
277 278 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
278 279 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
279 280 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
280 281 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
281 282 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
282 283 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
283 284 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
284 285 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
285 286 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
286 287 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
287 288 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
288 289 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
289 290 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
290 291 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
291 292 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
292 293 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
293 294 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
294 295 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
295 296 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
296 297 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
297 298 pyroutes.register('search', '/_admin/search', []);
298 299 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
299 300 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
300 301 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
301 302 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
302 303 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
303 304 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
304 305 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
305 306 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
306 307 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
307 308 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
308 309 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
309 310 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
310 311 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
311 312 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
312 313 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
313 314 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
314 315 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
315 316 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
316 317 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
317 318 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
318 319 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
319 320 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
320 321 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
321 322 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
322 323 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
323 324 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
324 325 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
325 326 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
326 327 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
327 328 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
328 329 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
329 330 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
330 331 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
331 332 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
332 333 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
333 334 pyroutes.register('gists_show', '/_admin/gists', []);
334 335 pyroutes.register('gists_new', '/_admin/gists/new', []);
335 336 pyroutes.register('gists_create', '/_admin/gists/create', []);
336 337 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
337 338 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
338 339 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
339 340 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
340 341 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
341 342 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
342 343 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
343 344 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 345 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
345 346 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
346 347 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
347 348 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
348 349 pyroutes.register('apiv2', '/_admin/api', []);
349 350 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
350 351 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
351 352 pyroutes.register('login', '/_admin/login', []);
352 353 pyroutes.register('register', '/_admin/register', []);
353 354 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
354 355 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
355 356 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
356 357 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
357 358 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
358 359 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
359 360 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
360 361 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
361 362 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
362 363 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
363 364 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
364 365 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
365 366 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
366 367 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
367 368 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
368 369 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
369 370 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
370 371 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
371 372 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
372 373 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
373 374 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
374 375 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
375 376 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
376 377 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
377 378 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
378 379 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
379 380 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
380 381 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
381 382 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
382 383 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
383 384 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
384 385 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
385 386 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
386 387 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
387 388 }
General Comments 0
You need to be logged in to leave comments. Login now