##// END OF EJS Templates
search: fixed UI of search items after redesign.
marcink -
r3745:41ddf1f6 new-ui
parent child Browse files
Show More
@@ -1,2071 +1,2075 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import os
29 29 import random
30 30 import hashlib
31 31 import StringIO
32 32 import textwrap
33 33 import urllib
34 34 import math
35 35 import logging
36 36 import re
37 37 import time
38 38 import string
39 39 import hashlib
40 40 from collections import OrderedDict
41 41
42 42 import pygments
43 43 import itertools
44 44 import fnmatch
45 45 import bleach
46 46
47 47 from pyramid import compat
48 48 from datetime import datetime
49 49 from functools import partial
50 50 from pygments.formatters.html import HtmlFormatter
51 51 from pygments.lexers import (
52 52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 53
54 54 from pyramid.threadlocal import get_current_request
55 55
56 56 from webhelpers.html import literal, HTML, escape
57 57 from webhelpers.html.tools import *
58 58 from webhelpers.html.builder import make_tag
59 59 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
60 60 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
61 61 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
62 62 submit, text, password, textarea, title, ul, xml_declaration, radio
63 63 from webhelpers.html.tools import auto_link, button_to, highlight, \
64 64 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
65 65 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
66 66 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
67 67 replace_whitespace, urlify, truncate, wrap_paragraphs
68 68 from webhelpers.date import time_ago_in_words
69 69 from webhelpers.paginate import Page as _Page
70 70 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
71 71 convert_boolean_attrs, NotGiven, _make_safe_id_component
72 72 from webhelpers2.number import format_byte_size
73 73
74 74 from rhodecode.lib.action_parser import action_parser
75 75 from rhodecode.lib.ext_json import json
76 76 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
77 77 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
78 78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
79 79 AttributeDict, safe_int, md5, md5_safe
80 80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 83 from rhodecode.lib.index.search_utils import get_matching_line_offsets
84 84 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
85 85 from rhodecode.model.changeset_status import ChangesetStatusModel
86 86 from rhodecode.model.db import Permission, User, Repository
87 87 from rhodecode.model.repo_group import RepoGroupModel
88 88 from rhodecode.model.settings import IssueTrackerSettingsModel
89 89
90 90
91 91 log = logging.getLogger(__name__)
92 92
93 93
94 94 DEFAULT_USER = User.DEFAULT_USER
95 95 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
96 96
97 97
98 98 def asset(path, ver=None, **kwargs):
99 99 """
100 100 Helper to generate a static asset file path for rhodecode assets
101 101
102 102 eg. h.asset('images/image.png', ver='3923')
103 103
104 104 :param path: path of asset
105 105 :param ver: optional version query param to append as ?ver=
106 106 """
107 107 request = get_current_request()
108 108 query = {}
109 109 query.update(kwargs)
110 110 if ver:
111 111 query = {'ver': ver}
112 112 return request.static_path(
113 113 'rhodecode:public/{}'.format(path), _query=query)
114 114
115 115
116 116 default_html_escape_table = {
117 117 ord('&'): u'&amp;',
118 118 ord('<'): u'&lt;',
119 119 ord('>'): u'&gt;',
120 120 ord('"'): u'&quot;',
121 121 ord("'"): u'&#39;',
122 122 }
123 123
124 124
125 125 def html_escape(text, html_escape_table=default_html_escape_table):
126 126 """Produce entities within text."""
127 127 return text.translate(html_escape_table)
128 128
129 129
130 130 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
131 131 """
132 132 Truncate string ``s`` at the first occurrence of ``sub``.
133 133
134 134 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
135 135 """
136 136 suffix_if_chopped = suffix_if_chopped or ''
137 137 pos = s.find(sub)
138 138 if pos == -1:
139 139 return s
140 140
141 141 if inclusive:
142 142 pos += len(sub)
143 143
144 144 chopped = s[:pos]
145 145 left = s[pos:].strip()
146 146
147 147 if left and suffix_if_chopped:
148 148 chopped += suffix_if_chopped
149 149
150 150 return chopped
151 151
152 152
153 153 def shorter(text, size=20):
154 154 postfix = '...'
155 155 if len(text) > size:
156 156 return text[:size - len(postfix)] + postfix
157 157 return text
158 158
159 159
160 160 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
161 161 """
162 162 Reset button
163 163 """
164 164 _set_input_attrs(attrs, type, name, value)
165 165 _set_id_attr(attrs, id, name)
166 166 convert_boolean_attrs(attrs, ["disabled"])
167 167 return HTML.input(**attrs)
168 168
169 169 reset = _reset
170 170 safeid = _make_safe_id_component
171 171
172 172
173 173 def branding(name, length=40):
174 174 return truncate(name, length, indicator="")
175 175
176 176
177 177 def FID(raw_id, path):
178 178 """
179 179 Creates a unique ID for filenode based on it's hash of path and commit
180 180 it's safe to use in urls
181 181
182 182 :param raw_id:
183 183 :param path:
184 184 """
185 185
186 186 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
187 187
188 188
189 189 class _GetError(object):
190 190 """Get error from form_errors, and represent it as span wrapped error
191 191 message
192 192
193 193 :param field_name: field to fetch errors for
194 194 :param form_errors: form errors dict
195 195 """
196 196
197 197 def __call__(self, field_name, form_errors):
198 198 tmpl = """<span class="error_msg">%s</span>"""
199 199 if form_errors and field_name in form_errors:
200 200 return literal(tmpl % form_errors.get(field_name))
201 201
202 202
203 203 get_error = _GetError()
204 204
205 205
206 206 class _ToolTip(object):
207 207
208 208 def __call__(self, tooltip_title, trim_at=50):
209 209 """
210 210 Special function just to wrap our text into nice formatted
211 211 autowrapped text
212 212
213 213 :param tooltip_title:
214 214 """
215 215 tooltip_title = escape(tooltip_title)
216 216 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
217 217 return tooltip_title
218 218
219 219
220 220 tooltip = _ToolTip()
221 221
222 222 files_icon = icon = '<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
223 223
224 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False):
224
225 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
225 226 if isinstance(file_path, str):
226 227 file_path = safe_unicode(file_path)
227 228
228 229 route_qry = {'at': at_ref} if at_ref else None
229 230
230 231 # first segment is a `..` link to repo files
231 232 root_name = literal(u'<i class="icon-home"></i>')
232 233 url_segments = [
233 234 link_to(
234 235 root_name,
235 236 route_path(
236 237 'repo_files',
237 238 repo_name=repo_name,
238 239 commit_id=commit_id,
239 240 f_path='',
240 241 _query=route_qry),
241 242 )]
242 243
243 244 path_segments = file_path.split('/')
244 245 last_cnt = len(path_segments) - 1
245 246 for cnt, segment in enumerate(path_segments):
246 247 if not segment:
247 248 continue
248 249 segment_html = escape(segment)
249 250
250 if cnt != last_cnt:
251 last_item = cnt == last_cnt
252
253 if last_item and linkify_last_item is False:
254 # plain version
255 url_segments.append(segment_html)
256 else:
251 257 url_segments.append(
252 258 link_to(
253 259 segment_html,
254 260 route_path(
255 261 'repo_files',
256 262 repo_name=repo_name,
257 263 commit_id=commit_id,
258 264 f_path='/'.join(path_segments[:cnt + 1]),
259 265 _query=route_qry),
260 ))
261 else:
262 url_segments.append(segment_html)
266 ))
263 267
264 268 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
265 269 if limit_items and len(limited_url_segments) < len(url_segments):
266 270 url_segments = limited_url_segments
267 271
268 272 full_path = file_path
269 273 icon = files_icon.format(escape(full_path))
270 274 if file_path == '':
271 275 return root_name
272 276 else:
273 277 return literal(' / '.join(url_segments) + icon)
274 278
275 279
276 280 def files_url_data(request):
277 281 matchdict = request.matchdict
278 282
279 283 if 'f_path' not in matchdict:
280 284 matchdict['f_path'] = ''
281 285
282 286 if 'commit_id' not in matchdict:
283 287 matchdict['commit_id'] = 'tip'
284 288
285 289 return json.dumps(matchdict)
286 290
287 291
288 292 def code_highlight(code, lexer, formatter, use_hl_filter=False):
289 293 """
290 294 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
291 295
292 296 If ``outfile`` is given and a valid file object (an object
293 297 with a ``write`` method), the result will be written to it, otherwise
294 298 it is returned as a string.
295 299 """
296 300 if use_hl_filter:
297 301 # add HL filter
298 302 from rhodecode.lib.index import search_utils
299 303 lexer.add_filter(search_utils.ElasticSearchHLFilter())
300 304 return pygments.format(pygments.lex(code, lexer), formatter)
301 305
302 306
303 307 class CodeHtmlFormatter(HtmlFormatter):
304 308 """
305 309 My code Html Formatter for source codes
306 310 """
307 311
308 312 def wrap(self, source, outfile):
309 313 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
310 314
311 315 def _wrap_code(self, source):
312 316 for cnt, it in enumerate(source):
313 317 i, t = it
314 318 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
315 319 yield i, t
316 320
317 321 def _wrap_tablelinenos(self, inner):
318 322 dummyoutfile = StringIO.StringIO()
319 323 lncount = 0
320 324 for t, line in inner:
321 325 if t:
322 326 lncount += 1
323 327 dummyoutfile.write(line)
324 328
325 329 fl = self.linenostart
326 330 mw = len(str(lncount + fl - 1))
327 331 sp = self.linenospecial
328 332 st = self.linenostep
329 333 la = self.lineanchors
330 334 aln = self.anchorlinenos
331 335 nocls = self.noclasses
332 336 if sp:
333 337 lines = []
334 338
335 339 for i in range(fl, fl + lncount):
336 340 if i % st == 0:
337 341 if i % sp == 0:
338 342 if aln:
339 343 lines.append('<a href="#%s%d" class="special">%*d</a>' %
340 344 (la, i, mw, i))
341 345 else:
342 346 lines.append('<span class="special">%*d</span>' % (mw, i))
343 347 else:
344 348 if aln:
345 349 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
346 350 else:
347 351 lines.append('%*d' % (mw, i))
348 352 else:
349 353 lines.append('')
350 354 ls = '\n'.join(lines)
351 355 else:
352 356 lines = []
353 357 for i in range(fl, fl + lncount):
354 358 if i % st == 0:
355 359 if aln:
356 360 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
357 361 else:
358 362 lines.append('%*d' % (mw, i))
359 363 else:
360 364 lines.append('')
361 365 ls = '\n'.join(lines)
362 366
363 367 # in case you wonder about the seemingly redundant <div> here: since the
364 368 # content in the other cell also is wrapped in a div, some browsers in
365 369 # some configurations seem to mess up the formatting...
366 370 if nocls:
367 371 yield 0, ('<table class="%stable">' % self.cssclass +
368 372 '<tr><td><div class="linenodiv" '
369 373 'style="background-color: #f0f0f0; padding-right: 10px">'
370 374 '<pre style="line-height: 125%">' +
371 375 ls + '</pre></div></td><td id="hlcode" class="code">')
372 376 else:
373 377 yield 0, ('<table class="%stable">' % self.cssclass +
374 378 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
375 379 ls + '</pre></div></td><td id="hlcode" class="code">')
376 380 yield 0, dummyoutfile.getvalue()
377 381 yield 0, '</td></tr></table>'
378 382
379 383
380 384 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
381 385 def __init__(self, **kw):
382 386 # only show these line numbers if set
383 387 self.only_lines = kw.pop('only_line_numbers', [])
384 388 self.query_terms = kw.pop('query_terms', [])
385 389 self.max_lines = kw.pop('max_lines', 5)
386 390 self.line_context = kw.pop('line_context', 3)
387 391 self.url = kw.pop('url', None)
388 392
389 393 super(CodeHtmlFormatter, self).__init__(**kw)
390 394
391 395 def _wrap_code(self, source):
392 396 for cnt, it in enumerate(source):
393 397 i, t = it
394 398 t = '<pre>%s</pre>' % t
395 399 yield i, t
396 400
397 401 def _wrap_tablelinenos(self, inner):
398 402 yield 0, '<table class="code-highlight %stable">' % self.cssclass
399 403
400 404 last_shown_line_number = 0
401 405 current_line_number = 1
402 406
403 407 for t, line in inner:
404 408 if not t:
405 409 yield t, line
406 410 continue
407 411
408 412 if current_line_number in self.only_lines:
409 413 if last_shown_line_number + 1 != current_line_number:
410 414 yield 0, '<tr>'
411 415 yield 0, '<td class="line">...</td>'
412 416 yield 0, '<td id="hlcode" class="code"></td>'
413 417 yield 0, '</tr>'
414 418
415 419 yield 0, '<tr>'
416 420 if self.url:
417 421 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
418 422 self.url, current_line_number, current_line_number)
419 423 else:
420 424 yield 0, '<td class="line"><a href="">%i</a></td>' % (
421 425 current_line_number)
422 426 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
423 427 yield 0, '</tr>'
424 428
425 429 last_shown_line_number = current_line_number
426 430
427 431 current_line_number += 1
428 432
429 433 yield 0, '</table>'
430 434
431 435
432 436 def hsv_to_rgb(h, s, v):
433 437 """ Convert hsv color values to rgb """
434 438
435 439 if s == 0.0:
436 440 return v, v, v
437 441 i = int(h * 6.0) # XXX assume int() truncates!
438 442 f = (h * 6.0) - i
439 443 p = v * (1.0 - s)
440 444 q = v * (1.0 - s * f)
441 445 t = v * (1.0 - s * (1.0 - f))
442 446 i = i % 6
443 447 if i == 0:
444 448 return v, t, p
445 449 if i == 1:
446 450 return q, v, p
447 451 if i == 2:
448 452 return p, v, t
449 453 if i == 3:
450 454 return p, q, v
451 455 if i == 4:
452 456 return t, p, v
453 457 if i == 5:
454 458 return v, p, q
455 459
456 460
457 461 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
458 462 """
459 463 Generator for getting n of evenly distributed colors using
460 464 hsv color and golden ratio. It always return same order of colors
461 465
462 466 :param n: number of colors to generate
463 467 :param saturation: saturation of returned colors
464 468 :param lightness: lightness of returned colors
465 469 :returns: RGB tuple
466 470 """
467 471
468 472 golden_ratio = 0.618033988749895
469 473 h = 0.22717784590367374
470 474
471 475 for _ in xrange(n):
472 476 h += golden_ratio
473 477 h %= 1
474 478 HSV_tuple = [h, saturation, lightness]
475 479 RGB_tuple = hsv_to_rgb(*HSV_tuple)
476 480 yield map(lambda x: str(int(x * 256)), RGB_tuple)
477 481
478 482
479 483 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
480 484 """
481 485 Returns a function which when called with an argument returns a unique
482 486 color for that argument, eg.
483 487
484 488 :param n: number of colors to generate
485 489 :param saturation: saturation of returned colors
486 490 :param lightness: lightness of returned colors
487 491 :returns: css RGB string
488 492
489 493 >>> color_hash = color_hasher()
490 494 >>> color_hash('hello')
491 495 'rgb(34, 12, 59)'
492 496 >>> color_hash('hello')
493 497 'rgb(34, 12, 59)'
494 498 >>> color_hash('other')
495 499 'rgb(90, 224, 159)'
496 500 """
497 501
498 502 color_dict = {}
499 503 cgenerator = unique_color_generator(
500 504 saturation=saturation, lightness=lightness)
501 505
502 506 def get_color_string(thing):
503 507 if thing in color_dict:
504 508 col = color_dict[thing]
505 509 else:
506 510 col = color_dict[thing] = cgenerator.next()
507 511 return "rgb(%s)" % (', '.join(col))
508 512
509 513 return get_color_string
510 514
511 515
512 516 def get_lexer_safe(mimetype=None, filepath=None):
513 517 """
514 518 Tries to return a relevant pygments lexer using mimetype/filepath name,
515 519 defaulting to plain text if none could be found
516 520 """
517 521 lexer = None
518 522 try:
519 523 if mimetype:
520 524 lexer = get_lexer_for_mimetype(mimetype)
521 525 if not lexer:
522 526 lexer = get_lexer_for_filename(filepath)
523 527 except pygments.util.ClassNotFound:
524 528 pass
525 529
526 530 if not lexer:
527 531 lexer = get_lexer_by_name('text')
528 532
529 533 return lexer
530 534
531 535
532 536 def get_lexer_for_filenode(filenode):
533 537 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
534 538 return lexer
535 539
536 540
537 541 def pygmentize(filenode, **kwargs):
538 542 """
539 543 pygmentize function using pygments
540 544
541 545 :param filenode:
542 546 """
543 547 lexer = get_lexer_for_filenode(filenode)
544 548 return literal(code_highlight(filenode.content, lexer,
545 549 CodeHtmlFormatter(**kwargs)))
546 550
547 551
548 552 def is_following_repo(repo_name, user_id):
549 553 from rhodecode.model.scm import ScmModel
550 554 return ScmModel().is_following_repo(repo_name, user_id)
551 555
552 556
553 557 class _Message(object):
554 558 """A message returned by ``Flash.pop_messages()``.
555 559
556 560 Converting the message to a string returns the message text. Instances
557 561 also have the following attributes:
558 562
559 563 * ``message``: the message text.
560 564 * ``category``: the category specified when the message was created.
561 565 """
562 566
563 567 def __init__(self, category, message):
564 568 self.category = category
565 569 self.message = message
566 570
567 571 def __str__(self):
568 572 return self.message
569 573
570 574 __unicode__ = __str__
571 575
572 576 def __html__(self):
573 577 return escape(safe_unicode(self.message))
574 578
575 579
576 580 class Flash(object):
577 581 # List of allowed categories. If None, allow any category.
578 582 categories = ["warning", "notice", "error", "success"]
579 583
580 584 # Default category if none is specified.
581 585 default_category = "notice"
582 586
583 587 def __init__(self, session_key="flash", categories=None,
584 588 default_category=None):
585 589 """
586 590 Instantiate a ``Flash`` object.
587 591
588 592 ``session_key`` is the key to save the messages under in the user's
589 593 session.
590 594
591 595 ``categories`` is an optional list which overrides the default list
592 596 of categories.
593 597
594 598 ``default_category`` overrides the default category used for messages
595 599 when none is specified.
596 600 """
597 601 self.session_key = session_key
598 602 if categories is not None:
599 603 self.categories = categories
600 604 if default_category is not None:
601 605 self.default_category = default_category
602 606 if self.categories and self.default_category not in self.categories:
603 607 raise ValueError(
604 608 "unrecognized default category %r" % (self.default_category,))
605 609
606 610 def pop_messages(self, session=None, request=None):
607 611 """
608 612 Return all accumulated messages and delete them from the session.
609 613
610 614 The return value is a list of ``Message`` objects.
611 615 """
612 616 messages = []
613 617
614 618 if not session:
615 619 if not request:
616 620 request = get_current_request()
617 621 session = request.session
618 622
619 623 # Pop the 'old' pylons flash messages. They are tuples of the form
620 624 # (category, message)
621 625 for cat, msg in session.pop(self.session_key, []):
622 626 messages.append(_Message(cat, msg))
623 627
624 628 # Pop the 'new' pyramid flash messages for each category as list
625 629 # of strings.
626 630 for cat in self.categories:
627 631 for msg in session.pop_flash(queue=cat):
628 632 messages.append(_Message(cat, msg))
629 633 # Map messages from the default queue to the 'notice' category.
630 634 for msg in session.pop_flash():
631 635 messages.append(_Message('notice', msg))
632 636
633 637 session.save()
634 638 return messages
635 639
636 640 def json_alerts(self, session=None, request=None):
637 641 payloads = []
638 642 messages = flash.pop_messages(session=session, request=request)
639 643 if messages:
640 644 for message in messages:
641 645 subdata = {}
642 646 if hasattr(message.message, 'rsplit'):
643 647 flash_data = message.message.rsplit('|DELIM|', 1)
644 648 org_message = flash_data[0]
645 649 if len(flash_data) > 1:
646 650 subdata = json.loads(flash_data[1])
647 651 else:
648 652 org_message = message.message
649 653 payloads.append({
650 654 'message': {
651 655 'message': u'{}'.format(org_message),
652 656 'level': message.category,
653 657 'force': True,
654 658 'subdata': subdata
655 659 }
656 660 })
657 661 return json.dumps(payloads)
658 662
659 663 def __call__(self, message, category=None, ignore_duplicate=False,
660 664 session=None, request=None):
661 665
662 666 if not session:
663 667 if not request:
664 668 request = get_current_request()
665 669 session = request.session
666 670
667 671 session.flash(
668 672 message, queue=category, allow_duplicate=not ignore_duplicate)
669 673
670 674
671 675 flash = Flash()
672 676
673 677 #==============================================================================
674 678 # SCM FILTERS available via h.
675 679 #==============================================================================
676 680 from rhodecode.lib.vcs.utils import author_name, author_email
677 681 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
678 682 from rhodecode.model.db import User, ChangesetStatus
679 683
680 684 capitalize = lambda x: x.capitalize()
681 685 email = author_email
682 686 short_id = lambda x: x[:12]
683 687 hide_credentials = lambda x: ''.join(credentials_filter(x))
684 688
685 689
686 690 import pytz
687 691 import tzlocal
688 692 local_timezone = tzlocal.get_localzone()
689 693
690 694
691 695 def age_component(datetime_iso, value=None, time_is_local=False):
692 696 title = value or format_date(datetime_iso)
693 697 tzinfo = '+00:00'
694 698
695 699 # detect if we have a timezone info, otherwise, add it
696 700 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
697 701 force_timezone = os.environ.get('RC_TIMEZONE', '')
698 702 if force_timezone:
699 703 force_timezone = pytz.timezone(force_timezone)
700 704 timezone = force_timezone or local_timezone
701 705 offset = timezone.localize(datetime_iso).strftime('%z')
702 706 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
703 707
704 708 return literal(
705 709 '<time class="timeago tooltip" '
706 710 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
707 711 datetime_iso, title, tzinfo))
708 712
709 713
710 714 def _shorten_commit_id(commit_id, commit_len=None):
711 715 if commit_len is None:
712 716 request = get_current_request()
713 717 commit_len = request.call_context.visual.show_sha_length
714 718 return commit_id[:commit_len]
715 719
716 720
717 721 def show_id(commit, show_idx=None, commit_len=None):
718 722 """
719 723 Configurable function that shows ID
720 724 by default it's r123:fffeeefffeee
721 725
722 726 :param commit: commit instance
723 727 """
724 728 if show_idx is None:
725 729 request = get_current_request()
726 730 show_idx = request.call_context.visual.show_revision_number
727 731
728 732 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
729 733 if show_idx:
730 734 return 'r%s:%s' % (commit.idx, raw_id)
731 735 else:
732 736 return '%s' % (raw_id, )
733 737
734 738
735 739 def format_date(date):
736 740 """
737 741 use a standardized formatting for dates used in RhodeCode
738 742
739 743 :param date: date/datetime object
740 744 :return: formatted date
741 745 """
742 746
743 747 if date:
744 748 _fmt = "%a, %d %b %Y %H:%M:%S"
745 749 return safe_unicode(date.strftime(_fmt))
746 750
747 751 return u""
748 752
749 753
750 754 class _RepoChecker(object):
751 755
752 756 def __init__(self, backend_alias):
753 757 self._backend_alias = backend_alias
754 758
755 759 def __call__(self, repository):
756 760 if hasattr(repository, 'alias'):
757 761 _type = repository.alias
758 762 elif hasattr(repository, 'repo_type'):
759 763 _type = repository.repo_type
760 764 else:
761 765 _type = repository
762 766 return _type == self._backend_alias
763 767
764 768
765 769 is_git = _RepoChecker('git')
766 770 is_hg = _RepoChecker('hg')
767 771 is_svn = _RepoChecker('svn')
768 772
769 773
770 774 def get_repo_type_by_name(repo_name):
771 775 repo = Repository.get_by_repo_name(repo_name)
772 776 if repo:
773 777 return repo.repo_type
774 778
775 779
776 780 def is_svn_without_proxy(repository):
777 781 if is_svn(repository):
778 782 from rhodecode.model.settings import VcsSettingsModel
779 783 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
780 784 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
781 785 return False
782 786
783 787
784 788 def discover_user(author):
785 789 """
786 790 Tries to discover RhodeCode User based on the autho string. Author string
787 791 is typically `FirstName LastName <email@address.com>`
788 792 """
789 793
790 794 # if author is already an instance use it for extraction
791 795 if isinstance(author, User):
792 796 return author
793 797
794 798 # Valid email in the attribute passed, see if they're in the system
795 799 _email = author_email(author)
796 800 if _email != '':
797 801 user = User.get_by_email(_email, case_insensitive=True, cache=True)
798 802 if user is not None:
799 803 return user
800 804
801 805 # Maybe it's a username, we try to extract it and fetch by username ?
802 806 _author = author_name(author)
803 807 user = User.get_by_username(_author, case_insensitive=True, cache=True)
804 808 if user is not None:
805 809 return user
806 810
807 811 return None
808 812
809 813
810 814 def email_or_none(author):
811 815 # extract email from the commit string
812 816 _email = author_email(author)
813 817
814 818 # If we have an email, use it, otherwise
815 819 # see if it contains a username we can get an email from
816 820 if _email != '':
817 821 return _email
818 822 else:
819 823 user = User.get_by_username(
820 824 author_name(author), case_insensitive=True, cache=True)
821 825
822 826 if user is not None:
823 827 return user.email
824 828
825 829 # No valid email, not a valid user in the system, none!
826 830 return None
827 831
828 832
829 833 def link_to_user(author, length=0, **kwargs):
830 834 user = discover_user(author)
831 835 # user can be None, but if we have it already it means we can re-use it
832 836 # in the person() function, so we save 1 intensive-query
833 837 if user:
834 838 author = user
835 839
836 840 display_person = person(author, 'username_or_name_or_email')
837 841 if length:
838 842 display_person = shorter(display_person, length)
839 843
840 844 if user:
841 845 return link_to(
842 846 escape(display_person),
843 847 route_path('user_profile', username=user.username),
844 848 **kwargs)
845 849 else:
846 850 return escape(display_person)
847 851
848 852
849 853 def link_to_group(users_group_name, **kwargs):
850 854 return link_to(
851 855 escape(users_group_name),
852 856 route_path('user_group_profile', user_group_name=users_group_name),
853 857 **kwargs)
854 858
855 859
856 860 def person(author, show_attr="username_and_name"):
857 861 user = discover_user(author)
858 862 if user:
859 863 return getattr(user, show_attr)
860 864 else:
861 865 _author = author_name(author)
862 866 _email = email(author)
863 867 return _author or _email
864 868
865 869
866 870 def author_string(email):
867 871 if email:
868 872 user = User.get_by_email(email, case_insensitive=True, cache=True)
869 873 if user:
870 874 if user.first_name or user.last_name:
871 875 return '%s %s &lt;%s&gt;' % (
872 876 user.first_name, user.last_name, email)
873 877 else:
874 878 return email
875 879 else:
876 880 return email
877 881 else:
878 882 return None
879 883
880 884
881 885 def person_by_id(id_, show_attr="username_and_name"):
882 886 # attr to return from fetched user
883 887 person_getter = lambda usr: getattr(usr, show_attr)
884 888
885 889 #maybe it's an ID ?
886 890 if str(id_).isdigit() or isinstance(id_, int):
887 891 id_ = int(id_)
888 892 user = User.get(id_)
889 893 if user is not None:
890 894 return person_getter(user)
891 895 return id_
892 896
893 897
894 898 def gravatar_with_user(request, author, show_disabled=False):
895 899 _render = request.get_partial_renderer(
896 900 'rhodecode:templates/base/base.mako')
897 901 return _render('gravatar_with_user', author, show_disabled=show_disabled)
898 902
899 903
900 904 tags_paterns = OrderedDict((
901 905 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
902 906 '<div class="metatag" tag="lang">\\2</div>')),
903 907
904 908 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
905 909 '<div class="metatag" tag="see">see: \\1 </div>')),
906 910
907 911 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
908 912 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
909 913
910 914 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
911 915 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
912 916
913 917 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
914 918 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
915 919
916 920 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
917 921 '<div class="metatag" tag="state \\1">\\1</div>')),
918 922
919 923 # label in grey
920 924 ('label', (re.compile(r'\[([a-z]+)\]'),
921 925 '<div class="metatag" tag="label">\\1</div>')),
922 926
923 927 # generic catch all in grey
924 928 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
925 929 '<div class="metatag" tag="generic">\\1</div>')),
926 930 ))
927 931
928 932
929 933 def extract_metatags(value):
930 934 """
931 935 Extract supported meta-tags from given text value
932 936 """
933 937 tags = []
934 938 if not value:
935 939 return tags, ''
936 940
937 941 for key, val in tags_paterns.items():
938 942 pat, replace_html = val
939 943 tags.extend([(key, x.group()) for x in pat.finditer(value)])
940 944 value = pat.sub('', value)
941 945
942 946 return tags, value
943 947
944 948
945 949 def style_metatag(tag_type, value):
946 950 """
947 951 converts tags from value into html equivalent
948 952 """
949 953 if not value:
950 954 return ''
951 955
952 956 html_value = value
953 957 tag_data = tags_paterns.get(tag_type)
954 958 if tag_data:
955 959 pat, replace_html = tag_data
956 960 # convert to plain `unicode` instead of a markup tag to be used in
957 961 # regex expressions. safe_unicode doesn't work here
958 962 html_value = pat.sub(replace_html, unicode(value))
959 963
960 964 return html_value
961 965
962 966
963 967 def bool2icon(value, show_at_false=True):
964 968 """
965 969 Returns boolean value of a given value, represented as html element with
966 970 classes that will represent icons
967 971
968 972 :param value: given value to convert to html node
969 973 """
970 974
971 975 if value: # does bool conversion
972 976 return HTML.tag('i', class_="icon-true")
973 977 else: # not true as bool
974 978 if show_at_false:
975 979 return HTML.tag('i', class_="icon-false")
976 980 return HTML.tag('i')
977 981
978 982 #==============================================================================
979 983 # PERMS
980 984 #==============================================================================
981 985 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
982 986 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
983 987 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
984 988 csrf_token_key
985 989
986 990
987 991 #==============================================================================
988 992 # GRAVATAR URL
989 993 #==============================================================================
990 994 class InitialsGravatar(object):
991 995 def __init__(self, email_address, first_name, last_name, size=30,
992 996 background=None, text_color='#fff'):
993 997 self.size = size
994 998 self.first_name = first_name
995 999 self.last_name = last_name
996 1000 self.email_address = email_address
997 1001 self.background = background or self.str2color(email_address)
998 1002 self.text_color = text_color
999 1003
1000 1004 def get_color_bank(self):
1001 1005 """
1002 1006 returns a predefined list of colors that gravatars can use.
1003 1007 Those are randomized distinct colors that guarantee readability and
1004 1008 uniqueness.
1005 1009
1006 1010 generated with: http://phrogz.net/css/distinct-colors.html
1007 1011 """
1008 1012 return [
1009 1013 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1010 1014 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1011 1015 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1012 1016 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1013 1017 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1014 1018 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1015 1019 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1016 1020 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1017 1021 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1018 1022 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1019 1023 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1020 1024 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1021 1025 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1022 1026 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1023 1027 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1024 1028 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1025 1029 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1026 1030 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1027 1031 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1028 1032 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1029 1033 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1030 1034 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1031 1035 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1032 1036 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1033 1037 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1034 1038 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1035 1039 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1036 1040 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1037 1041 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1038 1042 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1039 1043 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1040 1044 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1041 1045 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1042 1046 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1043 1047 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1044 1048 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1045 1049 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1046 1050 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1047 1051 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1048 1052 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1049 1053 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1050 1054 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1051 1055 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1052 1056 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1053 1057 '#4f8c46', '#368dd9', '#5c0073'
1054 1058 ]
1055 1059
1056 1060 def rgb_to_hex_color(self, rgb_tuple):
1057 1061 """
1058 1062 Converts an rgb_tuple passed to an hex color.
1059 1063
1060 1064 :param rgb_tuple: tuple with 3 ints represents rgb color space
1061 1065 """
1062 1066 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1063 1067
1064 1068 def email_to_int_list(self, email_str):
1065 1069 """
1066 1070 Get every byte of the hex digest value of email and turn it to integer.
1067 1071 It's going to be always between 0-255
1068 1072 """
1069 1073 digest = md5_safe(email_str.lower())
1070 1074 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1071 1075
1072 1076 def pick_color_bank_index(self, email_str, color_bank):
1073 1077 return self.email_to_int_list(email_str)[0] % len(color_bank)
1074 1078
1075 1079 def str2color(self, email_str):
1076 1080 """
1077 1081 Tries to map in a stable algorithm an email to color
1078 1082
1079 1083 :param email_str:
1080 1084 """
1081 1085 color_bank = self.get_color_bank()
1082 1086 # pick position (module it's length so we always find it in the
1083 1087 # bank even if it's smaller than 256 values
1084 1088 pos = self.pick_color_bank_index(email_str, color_bank)
1085 1089 return color_bank[pos]
1086 1090
1087 1091 def normalize_email(self, email_address):
1088 1092 import unicodedata
1089 1093 # default host used to fill in the fake/missing email
1090 1094 default_host = u'localhost'
1091 1095
1092 1096 if not email_address:
1093 1097 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1094 1098
1095 1099 email_address = safe_unicode(email_address)
1096 1100
1097 1101 if u'@' not in email_address:
1098 1102 email_address = u'%s@%s' % (email_address, default_host)
1099 1103
1100 1104 if email_address.endswith(u'@'):
1101 1105 email_address = u'%s%s' % (email_address, default_host)
1102 1106
1103 1107 email_address = unicodedata.normalize('NFKD', email_address)\
1104 1108 .encode('ascii', 'ignore')
1105 1109 return email_address
1106 1110
1107 1111 def get_initials(self):
1108 1112 """
1109 1113 Returns 2 letter initials calculated based on the input.
1110 1114 The algorithm picks first given email address, and takes first letter
1111 1115 of part before @, and then the first letter of server name. In case
1112 1116 the part before @ is in a format of `somestring.somestring2` it replaces
1113 1117 the server letter with first letter of somestring2
1114 1118
1115 1119 In case function was initialized with both first and lastname, this
1116 1120 overrides the extraction from email by first letter of the first and
1117 1121 last name. We add special logic to that functionality, In case Full name
1118 1122 is compound, like Guido Von Rossum, we use last part of the last name
1119 1123 (Von Rossum) picking `R`.
1120 1124
1121 1125 Function also normalizes the non-ascii characters to they ascii
1122 1126 representation, eg Δ„ => A
1123 1127 """
1124 1128 import unicodedata
1125 1129 # replace non-ascii to ascii
1126 1130 first_name = unicodedata.normalize(
1127 1131 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1128 1132 last_name = unicodedata.normalize(
1129 1133 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1130 1134
1131 1135 # do NFKD encoding, and also make sure email has proper format
1132 1136 email_address = self.normalize_email(self.email_address)
1133 1137
1134 1138 # first push the email initials
1135 1139 prefix, server = email_address.split('@', 1)
1136 1140
1137 1141 # check if prefix is maybe a 'first_name.last_name' syntax
1138 1142 _dot_split = prefix.rsplit('.', 1)
1139 1143 if len(_dot_split) == 2 and _dot_split[1]:
1140 1144 initials = [_dot_split[0][0], _dot_split[1][0]]
1141 1145 else:
1142 1146 initials = [prefix[0], server[0]]
1143 1147
1144 1148 # then try to replace either first_name or last_name
1145 1149 fn_letter = (first_name or " ")[0].strip()
1146 1150 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1147 1151
1148 1152 if fn_letter:
1149 1153 initials[0] = fn_letter
1150 1154
1151 1155 if ln_letter:
1152 1156 initials[1] = ln_letter
1153 1157
1154 1158 return ''.join(initials).upper()
1155 1159
1156 1160 def get_img_data_by_type(self, font_family, img_type):
1157 1161 default_user = """
1158 1162 <svg xmlns="http://www.w3.org/2000/svg"
1159 1163 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1160 1164 viewBox="-15 -10 439.165 429.164"
1161 1165
1162 1166 xml:space="preserve"
1163 1167 style="background:{background};" >
1164 1168
1165 1169 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1166 1170 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1167 1171 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1168 1172 168.596,153.916,216.671,
1169 1173 204.583,216.671z" fill="{text_color}"/>
1170 1174 <path d="M407.164,374.717L360.88,
1171 1175 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1172 1176 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1173 1177 15.366-44.203,23.488-69.076,23.488c-24.877,
1174 1178 0-48.762-8.122-69.078-23.488
1175 1179 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1176 1180 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1177 1181 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1178 1182 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1179 1183 19.402-10.527 C409.699,390.129,
1180 1184 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1181 1185 </svg>""".format(
1182 1186 size=self.size,
1183 1187 background='#979797', # @grey4
1184 1188 text_color=self.text_color,
1185 1189 font_family=font_family)
1186 1190
1187 1191 return {
1188 1192 "default_user": default_user
1189 1193 }[img_type]
1190 1194
1191 1195 def get_img_data(self, svg_type=None):
1192 1196 """
1193 1197 generates the svg metadata for image
1194 1198 """
1195 1199 fonts = [
1196 1200 '-apple-system',
1197 1201 'BlinkMacSystemFont',
1198 1202 'Segoe UI',
1199 1203 'Roboto',
1200 1204 'Oxygen-Sans',
1201 1205 'Ubuntu',
1202 1206 'Cantarell',
1203 1207 'Helvetica Neue',
1204 1208 'sans-serif'
1205 1209 ]
1206 1210 font_family = ','.join(fonts)
1207 1211 if svg_type:
1208 1212 return self.get_img_data_by_type(font_family, svg_type)
1209 1213
1210 1214 initials = self.get_initials()
1211 1215 img_data = """
1212 1216 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1213 1217 width="{size}" height="{size}"
1214 1218 style="width: 100%; height: 100%; background-color: {background}"
1215 1219 viewBox="0 0 {size} {size}">
1216 1220 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1217 1221 pointer-events="auto" fill="{text_color}"
1218 1222 font-family="{font_family}"
1219 1223 style="font-weight: 400; font-size: {f_size}px;">{text}
1220 1224 </text>
1221 1225 </svg>""".format(
1222 1226 size=self.size,
1223 1227 f_size=self.size/2.05, # scale the text inside the box nicely
1224 1228 background=self.background,
1225 1229 text_color=self.text_color,
1226 1230 text=initials.upper(),
1227 1231 font_family=font_family)
1228 1232
1229 1233 return img_data
1230 1234
1231 1235 def generate_svg(self, svg_type=None):
1232 1236 img_data = self.get_img_data(svg_type)
1233 1237 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1234 1238
1235 1239
1236 1240 def initials_gravatar(email_address, first_name, last_name, size=30):
1237 1241 svg_type = None
1238 1242 if email_address == User.DEFAULT_USER_EMAIL:
1239 1243 svg_type = 'default_user'
1240 1244 klass = InitialsGravatar(email_address, first_name, last_name, size)
1241 1245 return klass.generate_svg(svg_type=svg_type)
1242 1246
1243 1247
1244 1248 def gravatar_url(email_address, size=30, request=None):
1245 1249 request = get_current_request()
1246 1250 _use_gravatar = request.call_context.visual.use_gravatar
1247 1251 _gravatar_url = request.call_context.visual.gravatar_url
1248 1252
1249 1253 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1250 1254
1251 1255 email_address = email_address or User.DEFAULT_USER_EMAIL
1252 1256 if isinstance(email_address, unicode):
1253 1257 # hashlib crashes on unicode items
1254 1258 email_address = safe_str(email_address)
1255 1259
1256 1260 # empty email or default user
1257 1261 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1258 1262 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1259 1263
1260 1264 if _use_gravatar:
1261 1265 # TODO: Disuse pyramid thread locals. Think about another solution to
1262 1266 # get the host and schema here.
1263 1267 request = get_current_request()
1264 1268 tmpl = safe_str(_gravatar_url)
1265 1269 tmpl = tmpl.replace('{email}', email_address)\
1266 1270 .replace('{md5email}', md5_safe(email_address.lower())) \
1267 1271 .replace('{netloc}', request.host)\
1268 1272 .replace('{scheme}', request.scheme)\
1269 1273 .replace('{size}', safe_str(size))
1270 1274 return tmpl
1271 1275 else:
1272 1276 return initials_gravatar(email_address, '', '', size=size)
1273 1277
1274 1278
1275 1279 class Page(_Page):
1276 1280 """
1277 1281 Custom pager to match rendering style with paginator
1278 1282 """
1279 1283
1280 1284 def _get_pos(self, cur_page, max_page, items):
1281 1285 edge = (items / 2) + 1
1282 1286 if (cur_page <= edge):
1283 1287 radius = max(items / 2, items - cur_page)
1284 1288 elif (max_page - cur_page) < edge:
1285 1289 radius = (items - 1) - (max_page - cur_page)
1286 1290 else:
1287 1291 radius = items / 2
1288 1292
1289 1293 left = max(1, (cur_page - (radius)))
1290 1294 right = min(max_page, cur_page + (radius))
1291 1295 return left, cur_page, right
1292 1296
1293 1297 def _range(self, regexp_match):
1294 1298 """
1295 1299 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1296 1300
1297 1301 Arguments:
1298 1302
1299 1303 regexp_match
1300 1304 A "re" (regular expressions) match object containing the
1301 1305 radius of linked pages around the current page in
1302 1306 regexp_match.group(1) as a string
1303 1307
1304 1308 This function is supposed to be called as a callable in
1305 1309 re.sub.
1306 1310
1307 1311 """
1308 1312 radius = int(regexp_match.group(1))
1309 1313
1310 1314 # Compute the first and last page number within the radius
1311 1315 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1312 1316 # -> leftmost_page = 5
1313 1317 # -> rightmost_page = 9
1314 1318 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1315 1319 self.last_page,
1316 1320 (radius * 2) + 1)
1317 1321 nav_items = []
1318 1322
1319 1323 # Create a link to the first page (unless we are on the first page
1320 1324 # or there would be no need to insert '..' spacers)
1321 1325 if self.page != self.first_page and self.first_page < leftmost_page:
1322 1326 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1323 1327
1324 1328 # Insert dots if there are pages between the first page
1325 1329 # and the currently displayed page range
1326 1330 if leftmost_page - self.first_page > 1:
1327 1331 # Wrap in a SPAN tag if nolink_attr is set
1328 1332 text = '..'
1329 1333 if self.dotdot_attr:
1330 1334 text = HTML.span(c=text, **self.dotdot_attr)
1331 1335 nav_items.append(text)
1332 1336
1333 1337 for thispage in xrange(leftmost_page, rightmost_page + 1):
1334 1338 # Hilight the current page number and do not use a link
1335 1339 if thispage == self.page:
1336 1340 text = '%s' % (thispage,)
1337 1341 # Wrap in a SPAN tag if nolink_attr is set
1338 1342 if self.curpage_attr:
1339 1343 text = HTML.span(c=text, **self.curpage_attr)
1340 1344 nav_items.append(text)
1341 1345 # Otherwise create just a link to that page
1342 1346 else:
1343 1347 text = '%s' % (thispage,)
1344 1348 nav_items.append(self._pagerlink(thispage, text))
1345 1349
1346 1350 # Insert dots if there are pages between the displayed
1347 1351 # page numbers and the end of the page range
1348 1352 if self.last_page - rightmost_page > 1:
1349 1353 text = '..'
1350 1354 # Wrap in a SPAN tag if nolink_attr is set
1351 1355 if self.dotdot_attr:
1352 1356 text = HTML.span(c=text, **self.dotdot_attr)
1353 1357 nav_items.append(text)
1354 1358
1355 1359 # Create a link to the very last page (unless we are on the last
1356 1360 # page or there would be no need to insert '..' spacers)
1357 1361 if self.page != self.last_page and rightmost_page < self.last_page:
1358 1362 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1359 1363
1360 1364 ## prerender links
1361 1365 #_page_link = url.current()
1362 1366 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1363 1367 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1364 1368 return self.separator.join(nav_items)
1365 1369
1366 1370 def pager(self, format='~2~', page_param='page', partial_param='partial',
1367 1371 show_if_single_page=False, separator=' ', onclick=None,
1368 1372 symbol_first='<<', symbol_last='>>',
1369 1373 symbol_previous='<', symbol_next='>',
1370 1374 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1371 1375 curpage_attr={'class': 'pager_curpage'},
1372 1376 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1373 1377
1374 1378 self.curpage_attr = curpage_attr
1375 1379 self.separator = separator
1376 1380 self.pager_kwargs = kwargs
1377 1381 self.page_param = page_param
1378 1382 self.partial_param = partial_param
1379 1383 self.onclick = onclick
1380 1384 self.link_attr = link_attr
1381 1385 self.dotdot_attr = dotdot_attr
1382 1386
1383 1387 # Don't show navigator if there is no more than one page
1384 1388 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1385 1389 return ''
1386 1390
1387 1391 from string import Template
1388 1392 # Replace ~...~ in token format by range of pages
1389 1393 result = re.sub(r'~(\d+)~', self._range, format)
1390 1394
1391 1395 # Interpolate '%' variables
1392 1396 result = Template(result).safe_substitute({
1393 1397 'first_page': self.first_page,
1394 1398 'last_page': self.last_page,
1395 1399 'page': self.page,
1396 1400 'page_count': self.page_count,
1397 1401 'items_per_page': self.items_per_page,
1398 1402 'first_item': self.first_item,
1399 1403 'last_item': self.last_item,
1400 1404 'item_count': self.item_count,
1401 1405 'link_first': self.page > self.first_page and \
1402 1406 self._pagerlink(self.first_page, symbol_first) or '',
1403 1407 'link_last': self.page < self.last_page and \
1404 1408 self._pagerlink(self.last_page, symbol_last) or '',
1405 1409 'link_previous': self.previous_page and \
1406 1410 self._pagerlink(self.previous_page, symbol_previous) \
1407 1411 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1408 1412 'link_next': self.next_page and \
1409 1413 self._pagerlink(self.next_page, symbol_next) \
1410 1414 or HTML.span(symbol_next, class_="pg-next disabled")
1411 1415 })
1412 1416
1413 1417 return literal(result)
1414 1418
1415 1419
1416 1420 #==============================================================================
1417 1421 # REPO PAGER, PAGER FOR REPOSITORY
1418 1422 #==============================================================================
1419 1423 class RepoPage(Page):
1420 1424
1421 1425 def __init__(self, collection, page=1, items_per_page=20,
1422 1426 item_count=None, url=None, **kwargs):
1423 1427
1424 1428 """Create a "RepoPage" instance. special pager for paging
1425 1429 repository
1426 1430 """
1427 1431 self._url_generator = url
1428 1432
1429 1433 # Safe the kwargs class-wide so they can be used in the pager() method
1430 1434 self.kwargs = kwargs
1431 1435
1432 1436 # Save a reference to the collection
1433 1437 self.original_collection = collection
1434 1438
1435 1439 self.collection = collection
1436 1440
1437 1441 # The self.page is the number of the current page.
1438 1442 # The first page has the number 1!
1439 1443 try:
1440 1444 self.page = int(page) # make it int() if we get it as a string
1441 1445 except (ValueError, TypeError):
1442 1446 self.page = 1
1443 1447
1444 1448 self.items_per_page = items_per_page
1445 1449
1446 1450 # Unless the user tells us how many items the collections has
1447 1451 # we calculate that ourselves.
1448 1452 if item_count is not None:
1449 1453 self.item_count = item_count
1450 1454 else:
1451 1455 self.item_count = len(self.collection)
1452 1456
1453 1457 # Compute the number of the first and last available page
1454 1458 if self.item_count > 0:
1455 1459 self.first_page = 1
1456 1460 self.page_count = int(math.ceil(float(self.item_count) /
1457 1461 self.items_per_page))
1458 1462 self.last_page = self.first_page + self.page_count - 1
1459 1463
1460 1464 # Make sure that the requested page number is the range of
1461 1465 # valid pages
1462 1466 if self.page > self.last_page:
1463 1467 self.page = self.last_page
1464 1468 elif self.page < self.first_page:
1465 1469 self.page = self.first_page
1466 1470
1467 1471 # Note: the number of items on this page can be less than
1468 1472 # items_per_page if the last page is not full
1469 1473 self.first_item = max(0, (self.item_count) - (self.page *
1470 1474 items_per_page))
1471 1475 self.last_item = ((self.item_count - 1) - items_per_page *
1472 1476 (self.page - 1))
1473 1477
1474 1478 self.items = list(self.collection[self.first_item:self.last_item + 1])
1475 1479
1476 1480 # Links to previous and next page
1477 1481 if self.page > self.first_page:
1478 1482 self.previous_page = self.page - 1
1479 1483 else:
1480 1484 self.previous_page = None
1481 1485
1482 1486 if self.page < self.last_page:
1483 1487 self.next_page = self.page + 1
1484 1488 else:
1485 1489 self.next_page = None
1486 1490
1487 1491 # No items available
1488 1492 else:
1489 1493 self.first_page = None
1490 1494 self.page_count = 0
1491 1495 self.last_page = None
1492 1496 self.first_item = None
1493 1497 self.last_item = None
1494 1498 self.previous_page = None
1495 1499 self.next_page = None
1496 1500 self.items = []
1497 1501
1498 1502 # This is a subclass of the 'list' type. Initialise the list now.
1499 1503 list.__init__(self, reversed(self.items))
1500 1504
1501 1505
1502 1506 def breadcrumb_repo_link(repo):
1503 1507 """
1504 1508 Makes a breadcrumbs path link to repo
1505 1509
1506 1510 ex::
1507 1511 group >> subgroup >> repo
1508 1512
1509 1513 :param repo: a Repository instance
1510 1514 """
1511 1515
1512 1516 path = [
1513 1517 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1514 1518 title='last change:{}'.format(format_date(group.last_commit_change)))
1515 1519 for group in repo.groups_with_parents
1516 1520 ] + [
1517 1521 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1518 1522 title='last change:{}'.format(format_date(repo.last_commit_change)))
1519 1523 ]
1520 1524
1521 1525 return literal(' &raquo; '.join(path))
1522 1526
1523 1527
1524 1528 def breadcrumb_repo_group_link(repo_group):
1525 1529 """
1526 1530 Makes a breadcrumbs path link to repo
1527 1531
1528 1532 ex::
1529 1533 group >> subgroup
1530 1534
1531 1535 :param repo_group: a Repository Group instance
1532 1536 """
1533 1537
1534 1538 path = [
1535 1539 link_to(group.name,
1536 1540 route_path('repo_group_home', repo_group_name=group.group_name),
1537 1541 title='last change:{}'.format(format_date(group.last_commit_change)))
1538 1542 for group in repo_group.parents
1539 1543 ] + [
1540 1544 link_to(repo_group.name,
1541 1545 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1542 1546 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1543 1547 ]
1544 1548
1545 1549 return literal(' &raquo; '.join(path))
1546 1550
1547 1551
1548 1552 def format_byte_size_binary(file_size):
1549 1553 """
1550 1554 Formats file/folder sizes to standard.
1551 1555 """
1552 1556 if file_size is None:
1553 1557 file_size = 0
1554 1558
1555 1559 formatted_size = format_byte_size(file_size, binary=True)
1556 1560 return formatted_size
1557 1561
1558 1562
1559 1563 def urlify_text(text_, safe=True):
1560 1564 """
1561 1565 Extrac urls from text and make html links out of them
1562 1566
1563 1567 :param text_:
1564 1568 """
1565 1569
1566 1570 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1567 1571 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1568 1572
1569 1573 def url_func(match_obj):
1570 1574 url_full = match_obj.groups()[0]
1571 1575 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1572 1576 _newtext = url_pat.sub(url_func, text_)
1573 1577 if safe:
1574 1578 return literal(_newtext)
1575 1579 return _newtext
1576 1580
1577 1581
1578 1582 def urlify_commits(text_, repository):
1579 1583 """
1580 1584 Extract commit ids from text and make link from them
1581 1585
1582 1586 :param text_:
1583 1587 :param repository: repo name to build the URL with
1584 1588 """
1585 1589
1586 1590 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1587 1591
1588 1592 def url_func(match_obj):
1589 1593 commit_id = match_obj.groups()[1]
1590 1594 pref = match_obj.groups()[0]
1591 1595 suf = match_obj.groups()[2]
1592 1596
1593 1597 tmpl = (
1594 1598 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1595 1599 '%(commit_id)s</a>%(suf)s'
1596 1600 )
1597 1601 return tmpl % {
1598 1602 'pref': pref,
1599 1603 'cls': 'revision-link',
1600 1604 'url': route_url('repo_commit', repo_name=repository, commit_id=commit_id),
1601 1605 'commit_id': commit_id,
1602 1606 'suf': suf
1603 1607 }
1604 1608
1605 1609 newtext = URL_PAT.sub(url_func, text_)
1606 1610
1607 1611 return newtext
1608 1612
1609 1613
1610 1614 def _process_url_func(match_obj, repo_name, uid, entry,
1611 1615 return_raw_data=False, link_format='html'):
1612 1616 pref = ''
1613 1617 if match_obj.group().startswith(' '):
1614 1618 pref = ' '
1615 1619
1616 1620 issue_id = ''.join(match_obj.groups())
1617 1621
1618 1622 if link_format == 'html':
1619 1623 tmpl = (
1620 1624 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1621 1625 '%(issue-prefix)s%(id-repr)s'
1622 1626 '</a>')
1623 1627 elif link_format == 'rst':
1624 1628 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1625 1629 elif link_format == 'markdown':
1626 1630 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1627 1631 else:
1628 1632 raise ValueError('Bad link_format:{}'.format(link_format))
1629 1633
1630 1634 (repo_name_cleaned,
1631 1635 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1632 1636
1633 1637 # variables replacement
1634 1638 named_vars = {
1635 1639 'id': issue_id,
1636 1640 'repo': repo_name,
1637 1641 'repo_name': repo_name_cleaned,
1638 1642 'group_name': parent_group_name
1639 1643 }
1640 1644 # named regex variables
1641 1645 named_vars.update(match_obj.groupdict())
1642 1646 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1643 1647
1644 1648 def quote_cleaner(input_str):
1645 1649 """Remove quotes as it's HTML"""
1646 1650 return input_str.replace('"', '')
1647 1651
1648 1652 data = {
1649 1653 'pref': pref,
1650 1654 'cls': quote_cleaner('issue-tracker-link'),
1651 1655 'url': quote_cleaner(_url),
1652 1656 'id-repr': issue_id,
1653 1657 'issue-prefix': entry['pref'],
1654 1658 'serv': entry['url'],
1655 1659 }
1656 1660 if return_raw_data:
1657 1661 return {
1658 1662 'id': issue_id,
1659 1663 'url': _url
1660 1664 }
1661 1665 return tmpl % data
1662 1666
1663 1667
1664 1668 def get_active_pattern_entries(repo_name):
1665 1669 repo = None
1666 1670 if repo_name:
1667 1671 # Retrieving repo_name to avoid invalid repo_name to explode on
1668 1672 # IssueTrackerSettingsModel but still passing invalid name further down
1669 1673 repo = Repository.get_by_repo_name(repo_name, cache=True)
1670 1674
1671 1675 settings_model = IssueTrackerSettingsModel(repo=repo)
1672 1676 active_entries = settings_model.get_settings(cache=True)
1673 1677 return active_entries
1674 1678
1675 1679
1676 1680 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1677 1681
1678 1682 allowed_formats = ['html', 'rst', 'markdown']
1679 1683 if link_format not in allowed_formats:
1680 1684 raise ValueError('Link format can be only one of:{} got {}'.format(
1681 1685 allowed_formats, link_format))
1682 1686
1683 1687 active_entries = active_entries or get_active_pattern_entries(repo_name)
1684 1688 issues_data = []
1685 1689 newtext = text_string
1686 1690
1687 1691 for uid, entry in active_entries.items():
1688 1692 log.debug('found issue tracker entry with uid %s', uid)
1689 1693
1690 1694 if not (entry['pat'] and entry['url']):
1691 1695 log.debug('skipping due to missing data')
1692 1696 continue
1693 1697
1694 1698 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1695 1699 uid, entry['pat'], entry['url'], entry['pref'])
1696 1700
1697 1701 try:
1698 1702 pattern = re.compile(r'%s' % entry['pat'])
1699 1703 except re.error:
1700 1704 log.exception(
1701 1705 'issue tracker pattern: `%s` failed to compile',
1702 1706 entry['pat'])
1703 1707 continue
1704 1708
1705 1709 data_func = partial(
1706 1710 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1707 1711 return_raw_data=True)
1708 1712
1709 1713 for match_obj in pattern.finditer(text_string):
1710 1714 issues_data.append(data_func(match_obj))
1711 1715
1712 1716 url_func = partial(
1713 1717 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1714 1718 link_format=link_format)
1715 1719
1716 1720 newtext = pattern.sub(url_func, newtext)
1717 1721 log.debug('processed prefix:uid `%s`', uid)
1718 1722
1719 1723 return newtext, issues_data
1720 1724
1721 1725
1722 1726 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1723 1727 """
1724 1728 Parses given text message and makes proper links.
1725 1729 issues are linked to given issue-server, and rest is a commit link
1726 1730
1727 1731 :param commit_text:
1728 1732 :param repository:
1729 1733 """
1730 1734 def escaper(string):
1731 1735 return string.replace('<', '&lt;').replace('>', '&gt;')
1732 1736
1733 1737 newtext = escaper(commit_text)
1734 1738
1735 1739 # extract http/https links and make them real urls
1736 1740 newtext = urlify_text(newtext, safe=False)
1737 1741
1738 1742 # urlify commits - extract commit ids and make link out of them, if we have
1739 1743 # the scope of repository present.
1740 1744 if repository:
1741 1745 newtext = urlify_commits(newtext, repository)
1742 1746
1743 1747 # process issue tracker patterns
1744 1748 newtext, issues = process_patterns(newtext, repository or '',
1745 1749 active_entries=active_pattern_entries)
1746 1750
1747 1751 return literal(newtext)
1748 1752
1749 1753
1750 1754 def render_binary(repo_name, file_obj):
1751 1755 """
1752 1756 Choose how to render a binary file
1753 1757 """
1754 1758
1755 1759 filename = file_obj.name
1756 1760
1757 1761 # images
1758 1762 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1759 1763 if fnmatch.fnmatch(filename, pat=ext):
1760 1764 alt = escape(filename)
1761 1765 src = route_path(
1762 1766 'repo_file_raw', repo_name=repo_name,
1763 1767 commit_id=file_obj.commit.raw_id,
1764 1768 f_path=file_obj.path)
1765 1769 return literal(
1766 1770 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1767 1771
1768 1772
1769 1773 def renderer_from_filename(filename, exclude=None):
1770 1774 """
1771 1775 choose a renderer based on filename, this works only for text based files
1772 1776 """
1773 1777
1774 1778 # ipython
1775 1779 for ext in ['*.ipynb']:
1776 1780 if fnmatch.fnmatch(filename, pat=ext):
1777 1781 return 'jupyter'
1778 1782
1779 1783 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1780 1784 if is_markup:
1781 1785 return is_markup
1782 1786 return None
1783 1787
1784 1788
1785 1789 def render(source, renderer='rst', mentions=False, relative_urls=None,
1786 1790 repo_name=None):
1787 1791
1788 1792 def maybe_convert_relative_links(html_source):
1789 1793 if relative_urls:
1790 1794 return relative_links(html_source, relative_urls)
1791 1795 return html_source
1792 1796
1793 1797 if renderer == 'plain':
1794 1798 return literal(
1795 1799 MarkupRenderer.plain(source, leading_newline=False))
1796 1800
1797 1801 elif renderer == 'rst':
1798 1802 if repo_name:
1799 1803 # process patterns on comments if we pass in repo name
1800 1804 source, issues = process_patterns(
1801 1805 source, repo_name, link_format='rst')
1802 1806
1803 1807 return literal(
1804 1808 '<div class="rst-block">%s</div>' %
1805 1809 maybe_convert_relative_links(
1806 1810 MarkupRenderer.rst(source, mentions=mentions)))
1807 1811
1808 1812 elif renderer == 'markdown':
1809 1813 if repo_name:
1810 1814 # process patterns on comments if we pass in repo name
1811 1815 source, issues = process_patterns(
1812 1816 source, repo_name, link_format='markdown')
1813 1817
1814 1818 return literal(
1815 1819 '<div class="markdown-block">%s</div>' %
1816 1820 maybe_convert_relative_links(
1817 1821 MarkupRenderer.markdown(source, flavored=True,
1818 1822 mentions=mentions)))
1819 1823
1820 1824 elif renderer == 'jupyter':
1821 1825 return literal(
1822 1826 '<div class="ipynb">%s</div>' %
1823 1827 maybe_convert_relative_links(
1824 1828 MarkupRenderer.jupyter(source)))
1825 1829
1826 1830 # None means just show the file-source
1827 1831 return None
1828 1832
1829 1833
1830 1834 def commit_status(repo, commit_id):
1831 1835 return ChangesetStatusModel().get_status(repo, commit_id)
1832 1836
1833 1837
1834 1838 def commit_status_lbl(commit_status):
1835 1839 return dict(ChangesetStatus.STATUSES).get(commit_status)
1836 1840
1837 1841
1838 1842 def commit_time(repo_name, commit_id):
1839 1843 repo = Repository.get_by_repo_name(repo_name)
1840 1844 commit = repo.get_commit(commit_id=commit_id)
1841 1845 return commit.date
1842 1846
1843 1847
1844 1848 def get_permission_name(key):
1845 1849 return dict(Permission.PERMS).get(key)
1846 1850
1847 1851
1848 1852 def journal_filter_help(request):
1849 1853 _ = request.translate
1850 1854 from rhodecode.lib.audit_logger import ACTIONS
1851 1855 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1852 1856
1853 1857 return _(
1854 1858 'Example filter terms:\n' +
1855 1859 ' repository:vcs\n' +
1856 1860 ' username:marcin\n' +
1857 1861 ' username:(NOT marcin)\n' +
1858 1862 ' action:*push*\n' +
1859 1863 ' ip:127.0.0.1\n' +
1860 1864 ' date:20120101\n' +
1861 1865 ' date:[20120101100000 TO 20120102]\n' +
1862 1866 '\n' +
1863 1867 'Actions: {actions}\n' +
1864 1868 '\n' +
1865 1869 'Generate wildcards using \'*\' character:\n' +
1866 1870 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1867 1871 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1868 1872 '\n' +
1869 1873 'Optional AND / OR operators in queries\n' +
1870 1874 ' "repository:vcs OR repository:test"\n' +
1871 1875 ' "username:test AND repository:test*"\n'
1872 1876 ).format(actions=actions)
1873 1877
1874 1878
1875 1879 def not_mapped_error(repo_name):
1876 1880 from rhodecode.translation import _
1877 1881 flash(_('%s repository is not mapped to db perhaps'
1878 1882 ' it was created or renamed from the filesystem'
1879 1883 ' please run the application again'
1880 1884 ' in order to rescan repositories') % repo_name, category='error')
1881 1885
1882 1886
1883 1887 def ip_range(ip_addr):
1884 1888 from rhodecode.model.db import UserIpMap
1885 1889 s, e = UserIpMap._get_ip_range(ip_addr)
1886 1890 return '%s - %s' % (s, e)
1887 1891
1888 1892
1889 1893 def form(url, method='post', needs_csrf_token=True, **attrs):
1890 1894 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1891 1895 if method.lower() != 'get' and needs_csrf_token:
1892 1896 raise Exception(
1893 1897 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1894 1898 'CSRF token. If the endpoint does not require such token you can ' +
1895 1899 'explicitly set the parameter needs_csrf_token to false.')
1896 1900
1897 1901 return wh_form(url, method=method, **attrs)
1898 1902
1899 1903
1900 1904 def secure_form(form_url, method="POST", multipart=False, **attrs):
1901 1905 """Start a form tag that points the action to an url. This
1902 1906 form tag will also include the hidden field containing
1903 1907 the auth token.
1904 1908
1905 1909 The url options should be given either as a string, or as a
1906 1910 ``url()`` function. The method for the form defaults to POST.
1907 1911
1908 1912 Options:
1909 1913
1910 1914 ``multipart``
1911 1915 If set to True, the enctype is set to "multipart/form-data".
1912 1916 ``method``
1913 1917 The method to use when submitting the form, usually either
1914 1918 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1915 1919 hidden input with name _method is added to simulate the verb
1916 1920 over POST.
1917 1921
1918 1922 """
1919 1923 from webhelpers.pylonslib.secure_form import insecure_form
1920 1924
1921 1925 if 'request' in attrs:
1922 1926 session = attrs['request'].session
1923 1927 del attrs['request']
1924 1928 else:
1925 1929 raise ValueError(
1926 1930 'Calling this form requires request= to be passed as argument')
1927 1931
1928 1932 form = insecure_form(form_url, method, multipart, **attrs)
1929 1933 token = literal(
1930 1934 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1931 1935 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1932 1936
1933 1937 return literal("%s\n%s" % (form, token))
1934 1938
1935 1939
1936 1940 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1937 1941 select_html = select(name, selected, options, **attrs)
1938 1942 select2 = """
1939 1943 <script>
1940 1944 $(document).ready(function() {
1941 1945 $('#%s').select2({
1942 1946 containerCssClass: 'drop-menu',
1943 1947 dropdownCssClass: 'drop-menu-dropdown',
1944 1948 dropdownAutoWidth: true%s
1945 1949 });
1946 1950 });
1947 1951 </script>
1948 1952 """
1949 1953 filter_option = """,
1950 1954 minimumResultsForSearch: -1
1951 1955 """
1952 1956 input_id = attrs.get('id') or name
1953 1957 filter_enabled = "" if enable_filter else filter_option
1954 1958 select_script = literal(select2 % (input_id, filter_enabled))
1955 1959
1956 1960 return literal(select_html+select_script)
1957 1961
1958 1962
1959 1963 def get_visual_attr(tmpl_context_var, attr_name):
1960 1964 """
1961 1965 A safe way to get a variable from visual variable of template context
1962 1966
1963 1967 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1964 1968 :param attr_name: name of the attribute we fetch from the c.visual
1965 1969 """
1966 1970 visual = getattr(tmpl_context_var, 'visual', None)
1967 1971 if not visual:
1968 1972 return
1969 1973 else:
1970 1974 return getattr(visual, attr_name, None)
1971 1975
1972 1976
1973 1977 def get_last_path_part(file_node):
1974 1978 if not file_node.path:
1975 1979 return u'/'
1976 1980
1977 1981 path = safe_unicode(file_node.path.split('/')[-1])
1978 1982 return u'../' + path
1979 1983
1980 1984
1981 1985 def route_url(*args, **kwargs):
1982 1986 """
1983 1987 Wrapper around pyramids `route_url` (fully qualified url) function.
1984 1988 """
1985 1989 req = get_current_request()
1986 1990 return req.route_url(*args, **kwargs)
1987 1991
1988 1992
1989 1993 def route_path(*args, **kwargs):
1990 1994 """
1991 1995 Wrapper around pyramids `route_path` function.
1992 1996 """
1993 1997 req = get_current_request()
1994 1998 return req.route_path(*args, **kwargs)
1995 1999
1996 2000
1997 2001 def route_path_or_none(*args, **kwargs):
1998 2002 try:
1999 2003 return route_path(*args, **kwargs)
2000 2004 except KeyError:
2001 2005 return None
2002 2006
2003 2007
2004 2008 def current_route_path(request, **kw):
2005 2009 new_args = request.GET.mixed()
2006 2010 new_args.update(kw)
2007 2011 return request.current_route_path(_query=new_args)
2008 2012
2009 2013
2010 2014 def api_call_example(method, args):
2011 2015 """
2012 2016 Generates an API call example via CURL
2013 2017 """
2014 2018 args_json = json.dumps(OrderedDict([
2015 2019 ('id', 1),
2016 2020 ('auth_token', 'SECRET'),
2017 2021 ('method', method),
2018 2022 ('args', args)
2019 2023 ]))
2020 2024 return literal(
2021 2025 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2022 2026 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2023 2027 "and needs to be of `api calls` role."
2024 2028 .format(
2025 2029 api_url=route_url('apiv2'),
2026 2030 token_url=route_url('my_account_auth_tokens'),
2027 2031 data=args_json))
2028 2032
2029 2033
2030 2034 def notification_description(notification, request):
2031 2035 """
2032 2036 Generate notification human readable description based on notification type
2033 2037 """
2034 2038 from rhodecode.model.notification import NotificationModel
2035 2039 return NotificationModel().make_description(
2036 2040 notification, translate=request.translate)
2037 2041
2038 2042
2039 2043 def go_import_header(request, db_repo=None):
2040 2044 """
2041 2045 Creates a header for go-import functionality in Go Lang
2042 2046 """
2043 2047
2044 2048 if not db_repo:
2045 2049 return
2046 2050 if 'go-get' not in request.GET:
2047 2051 return
2048 2052
2049 2053 clone_url = db_repo.clone_url()
2050 2054 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2051 2055 # we have a repo and go-get flag,
2052 2056 return literal('<meta name="go-import" content="{} {} {}">'.format(
2053 2057 prefix, db_repo.repo_type, clone_url))
2054 2058
2055 2059
2056 2060 def reviewer_as_json(*args, **kwargs):
2057 2061 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2058 2062 return _reviewer_as_json(*args, **kwargs)
2059 2063
2060 2064
2061 2065 def get_repo_view_type(request):
2062 2066 route_name = request.matched_route.name
2063 2067 route_to_view_type = {
2064 2068 'repo_changelog': 'commits',
2065 2069 'repo_commits': 'commits',
2066 2070 'repo_files': 'files',
2067 2071 'repo_summary': 'summary',
2068 2072 'repo_commit': 'commit'
2069 2073 }
2070 2074
2071 2075 return route_to_view_type.get(route_name)
@@ -1,2603 +1,2608 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
30 30 //--- BASE ------------------//
31 31 .noscript-error {
32 32 top: 0;
33 33 left: 0;
34 34 width: 100%;
35 35 z-index: 101;
36 36 text-align: center;
37 37 font-size: 120%;
38 38 color: white;
39 39 background-color: @alert2;
40 40 padding: 5px 0 5px 0;
41 41 font-weight: @text-semibold-weight;
42 42 font-family: @text-semibold;
43 43 }
44 44
45 45 html {
46 46 display: table;
47 47 height: 100%;
48 48 width: 100%;
49 49 }
50 50
51 51 body {
52 52 display: table-cell;
53 53 width: 100%;
54 54 }
55 55
56 56 //--- LAYOUT ------------------//
57 57
58 58 .hidden{
59 59 display: none !important;
60 60 }
61 61
62 62 .box{
63 63 float: left;
64 64 width: 100%;
65 65 }
66 66
67 67 .browser-header {
68 68 clear: both;
69 69 }
70 70 .main {
71 71 clear: both;
72 72 padding:0 0 @pagepadding;
73 73 height: auto;
74 74
75 75 &:after { //clearfix
76 76 content:"";
77 77 clear:both;
78 78 width:100%;
79 79 display:block;
80 80 }
81 81 }
82 82
83 83 .action-link{
84 84 margin-left: @padding;
85 85 padding-left: @padding;
86 86 border-left: @border-thickness solid @border-default-color;
87 87 }
88 88
89 89 input + .action-link, .action-link.first{
90 90 border-left: none;
91 91 }
92 92
93 93 .action-link.last{
94 94 margin-right: @padding;
95 95 padding-right: @padding;
96 96 }
97 97
98 98 .action-link.active,
99 99 .action-link.active a{
100 100 color: @grey4;
101 101 }
102 102
103 103 .action-link.disabled {
104 104 color: @grey4;
105 105 cursor: inherit;
106 106 }
107 107
108 108 .clipboard-action {
109 109 cursor: pointer;
110 110 color: @grey4;
111 111 margin-left: 5px;
112 112
113 113 &:hover {
114 114 color: @grey2;
115 115 }
116 116 }
117 117
118 118 ul.simple-list{
119 119 list-style: none;
120 120 margin: 0;
121 121 padding: 0;
122 122 }
123 123
124 124 .main-content {
125 125 padding-bottom: @pagepadding;
126 126 }
127 127
128 128 .wide-mode-wrapper {
129 129 max-width:4000px !important;
130 130 }
131 131
132 132 .wrapper {
133 133 position: relative;
134 134 max-width: @wrapper-maxwidth;
135 135 margin: 0 auto;
136 136 }
137 137
138 138 #content {
139 139 clear: both;
140 140 padding: 0 @contentpadding;
141 141 }
142 142
143 143 .advanced-settings-fields{
144 144 input{
145 145 margin-left: @textmargin;
146 146 margin-right: @padding/2;
147 147 }
148 148 }
149 149
150 150 .cs_files_title {
151 151 margin: @pagepadding 0 0;
152 152 }
153 153
154 154 input.inline[type="file"] {
155 155 display: inline;
156 156 }
157 157
158 158 .error_page {
159 159 margin: 10% auto;
160 160
161 161 h1 {
162 162 color: @grey2;
163 163 }
164 164
165 165 .alert {
166 166 margin: @padding 0;
167 167 }
168 168
169 169 .error-branding {
170 170 color: @grey4;
171 171 font-weight: @text-semibold-weight;
172 172 font-family: @text-semibold;
173 173 }
174 174
175 175 .error_message {
176 176 font-family: @text-regular;
177 177 }
178 178
179 179 .sidebar {
180 180 min-height: 275px;
181 181 margin: 0;
182 182 padding: 0 0 @sidebarpadding @sidebarpadding;
183 183 border: none;
184 184 }
185 185
186 186 .main-content {
187 187 position: relative;
188 188 margin: 0 @sidebarpadding @sidebarpadding;
189 189 padding: 0 0 0 @sidebarpadding;
190 190 border-left: @border-thickness solid @grey5;
191 191
192 192 @media (max-width:767px) {
193 193 clear: both;
194 194 width: 100%;
195 195 margin: 0;
196 196 border: none;
197 197 }
198 198 }
199 199
200 200 .inner-column {
201 201 float: left;
202 202 width: 29.75%;
203 203 min-height: 150px;
204 204 margin: @sidebarpadding 2% 0 0;
205 205 padding: 0 2% 0 0;
206 206 border-right: @border-thickness solid @grey5;
207 207
208 208 @media (max-width:767px) {
209 209 clear: both;
210 210 width: 100%;
211 211 border: none;
212 212 }
213 213
214 214 ul {
215 215 padding-left: 1.25em;
216 216 }
217 217
218 218 &:last-child {
219 219 margin: @sidebarpadding 0 0;
220 220 border: none;
221 221 }
222 222
223 223 h4 {
224 224 margin: 0 0 @padding;
225 225 font-weight: @text-semibold-weight;
226 226 font-family: @text-semibold;
227 227 }
228 228 }
229 229 }
230 230 .error-page-logo {
231 231 width: 130px;
232 232 height: 160px;
233 233 }
234 234
235 235 // HEADER
236 236 .header {
237 237
238 238 // TODO: johbo: Fix login pages, so that they work without a min-height
239 239 // for the header and then remove the min-height. I chose a smaller value
240 240 // intentionally here to avoid rendering issues in the main navigation.
241 241 min-height: 49px;
242 242
243 243 position: relative;
244 244 vertical-align: bottom;
245 245 padding: 0 @header-padding;
246 246 background-color: @grey1;
247 247 color: @grey5;
248 248
249 249 .title {
250 250 overflow: visible;
251 251 }
252 252
253 253 &:before,
254 254 &:after {
255 255 content: "";
256 256 clear: both;
257 257 width: 100%;
258 258 }
259 259
260 260 // TODO: johbo: Avoids breaking "Repositories" chooser
261 261 .select2-container .select2-choice .select2-arrow {
262 262 display: none;
263 263 }
264 264 }
265 265
266 266 #header-inner {
267 267 &.title {
268 268 margin: 0;
269 269 }
270 270 &:before,
271 271 &:after {
272 272 content: "";
273 273 clear: both;
274 274 }
275 275 }
276 276
277 277 // Gists
278 278 #files_data {
279 279 clear: both; //for firefox
280 280 }
281 281 #gistid {
282 282 margin-right: @padding;
283 283 }
284 284
285 285 // Global Settings Editor
286 286 .textarea.editor {
287 287 float: left;
288 288 position: relative;
289 289 max-width: @texteditor-width;
290 290
291 291 select {
292 292 position: absolute;
293 293 top:10px;
294 294 right:0;
295 295 }
296 296
297 297 .CodeMirror {
298 298 margin: 0;
299 299 }
300 300
301 301 .help-block {
302 302 margin: 0 0 @padding;
303 303 padding:.5em;
304 304 background-color: @grey6;
305 305 &.pre-formatting {
306 306 white-space: pre;
307 307 }
308 308 }
309 309 }
310 310
311 311 ul.auth_plugins {
312 312 margin: @padding 0 @padding @legend-width;
313 313 padding: 0;
314 314
315 315 li {
316 316 margin-bottom: @padding;
317 317 line-height: 1em;
318 318 list-style-type: none;
319 319
320 320 .auth_buttons .btn {
321 321 margin-right: @padding;
322 322 }
323 323
324 324 }
325 325 }
326 326
327 327
328 328 // My Account PR list
329 329
330 330 #show_closed {
331 331 margin: 0 1em 0 0;
332 332 }
333 333
334 334 .pullrequestlist {
335 335 .closed {
336 336 background-color: @grey6;
337 337 }
338 338 .td-status {
339 339 padding-left: .5em;
340 340 }
341 341 .log-container .truncate {
342 342 height: 2.75em;
343 343 white-space: pre-line;
344 344 }
345 345 table.rctable .user {
346 346 padding-left: 0;
347 347 }
348 348 table.rctable {
349 349 td.td-description,
350 350 .rc-user {
351 351 min-width: auto;
352 352 }
353 353 }
354 354 }
355 355
356 356 // Pull Requests
357 357
358 358 .pullrequests_section_head {
359 359 display: block;
360 360 clear: both;
361 361 margin: @padding 0;
362 362 font-weight: @text-bold-weight;
363 363 font-family: @text-bold;
364 364 }
365 365
366 366 .pr-origininfo, .pr-targetinfo {
367 367 position: relative;
368 368
369 369 .tag {
370 370 display: inline-block;
371 371 margin: 0 1em .5em 0;
372 372 }
373 373
374 374 .clone-url {
375 375 display: inline-block;
376 376 margin: 0 0 .5em 0;
377 377 padding: 0;
378 378 line-height: 1.2em;
379 379 }
380 380 }
381 381
382 382 .pr-mergeinfo {
383 383 min-width: 95% !important;
384 384 padding: 0 !important;
385 385 border: 0;
386 386 }
387 387 .pr-mergeinfo-copy {
388 388 padding: 0 0;
389 389 }
390 390
391 391 .pr-pullinfo {
392 392 min-width: 95% !important;
393 393 padding: 0 !important;
394 394 border: 0;
395 395 }
396 396 .pr-pullinfo-copy {
397 397 padding: 0 0;
398 398 }
399 399
400 400
401 401 #pr-title-input {
402 402 width: 72%;
403 403 font-size: 1em;
404 404 margin: 0;
405 405 padding: 0 0 0 @padding/4;
406 406 line-height: 1.7em;
407 407 color: @text-color;
408 408 letter-spacing: .02em;
409 409 font-weight: @text-bold-weight;
410 410 font-family: @text-bold;
411 411 }
412 412
413 413 #pullrequest_title {
414 414 width: 100%;
415 415 box-sizing: border-box;
416 416 }
417 417
418 418 #pr_open_message {
419 419 border: @border-thickness solid #fff;
420 420 border-radius: @border-radius;
421 421 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
422 422 text-align: left;
423 423 overflow: hidden;
424 424 }
425 425
426 426 .pr-submit-button {
427 427 float: right;
428 428 margin: 0 0 0 5px;
429 429 }
430 430
431 431 .pr-spacing-container {
432 432 padding: 20px;
433 433 clear: both
434 434 }
435 435
436 436 #pr-description-input {
437 437 margin-bottom: 0;
438 438 }
439 439
440 440 .pr-description-label {
441 441 vertical-align: top;
442 442 }
443 443
444 444 .perms_section_head {
445 445 min-width: 625px;
446 446
447 447 h2 {
448 448 margin-bottom: 0;
449 449 }
450 450
451 451 .label-checkbox {
452 452 float: left;
453 453 }
454 454
455 455 &.field {
456 456 margin: @space 0 @padding;
457 457 }
458 458
459 459 &:first-child.field {
460 460 margin-top: 0;
461 461
462 462 .label {
463 463 margin-top: 0;
464 464 padding-top: 0;
465 465 }
466 466
467 467 .radios {
468 468 padding-top: 0;
469 469 }
470 470 }
471 471
472 472 .radios {
473 473 position: relative;
474 474 width: 505px;
475 475 }
476 476 }
477 477
478 478 //--- MODULES ------------------//
479 479
480 480
481 481 // Server Announcement
482 482 #server-announcement {
483 483 width: 95%;
484 484 margin: @padding auto;
485 485 padding: @padding;
486 486 border-width: 2px;
487 487 border-style: solid;
488 488 .border-radius(2px);
489 489 font-weight: @text-bold-weight;
490 490 font-family: @text-bold;
491 491
492 492 &.info { border-color: @alert4; background-color: @alert4-inner; }
493 493 &.warning { border-color: @alert3; background-color: @alert3-inner; }
494 494 &.error { border-color: @alert2; background-color: @alert2-inner; }
495 495 &.success { border-color: @alert1; background-color: @alert1-inner; }
496 496 &.neutral { border-color: @grey3; background-color: @grey6; }
497 497 }
498 498
499 499 // Fixed Sidebar Column
500 500 .sidebar-col-wrapper {
501 501 padding-left: @sidebar-all-width;
502 502
503 503 .sidebar {
504 504 width: @sidebar-width;
505 505 margin-left: -@sidebar-all-width;
506 506 }
507 507 }
508 508
509 509 .sidebar-col-wrapper.scw-small {
510 510 padding-left: @sidebar-small-all-width;
511 511
512 512 .sidebar {
513 513 width: @sidebar-small-width;
514 514 margin-left: -@sidebar-small-all-width;
515 515 }
516 516 }
517 517
518 518
519 519 // FOOTER
520 520 #footer {
521 521 padding: 0;
522 522 text-align: center;
523 523 vertical-align: middle;
524 524 color: @grey2;
525 525 font-size: 11px;
526 526
527 527 p {
528 528 margin: 0;
529 529 padding: 1em;
530 530 line-height: 1em;
531 531 }
532 532
533 533 .server-instance { //server instance
534 534 display: none;
535 535 }
536 536
537 537 .title {
538 538 float: none;
539 539 margin: 0 auto;
540 540 }
541 541 }
542 542
543 543 button.close {
544 544 padding: 0;
545 545 cursor: pointer;
546 546 background: transparent;
547 547 border: 0;
548 548 .box-shadow(none);
549 549 -webkit-appearance: none;
550 550 }
551 551
552 552 .close {
553 553 float: right;
554 554 font-size: 21px;
555 555 font-family: @text-bootstrap;
556 556 line-height: 1em;
557 557 font-weight: bold;
558 558 color: @grey2;
559 559
560 560 &:hover,
561 561 &:focus {
562 562 color: @grey1;
563 563 text-decoration: none;
564 564 cursor: pointer;
565 565 }
566 566 }
567 567
568 568 // GRID
569 569 .sorting,
570 570 .sorting_desc,
571 571 .sorting_asc {
572 572 cursor: pointer;
573 573 }
574 574 .sorting_desc:after {
575 575 content: "\00A0\25B2";
576 576 font-size: .75em;
577 577 }
578 578 .sorting_asc:after {
579 579 content: "\00A0\25BC";
580 580 font-size: .68em;
581 581 }
582 582
583 583
584 584 .user_auth_tokens {
585 585
586 586 &.truncate {
587 587 white-space: nowrap;
588 588 overflow: hidden;
589 589 text-overflow: ellipsis;
590 590 }
591 591
592 592 .fields .field .input {
593 593 margin: 0;
594 594 }
595 595
596 596 input#description {
597 597 width: 100px;
598 598 margin: 0;
599 599 }
600 600
601 601 .drop-menu {
602 602 // TODO: johbo: Remove this, should work out of the box when
603 603 // having multiple inputs inline
604 604 margin: 0 0 0 5px;
605 605 }
606 606 }
607 607 #user_list_table {
608 608 .closed {
609 609 background-color: @grey6;
610 610 }
611 611 }
612 612
613 613
614 614 input, textarea {
615 615 &.disabled {
616 616 opacity: .5;
617 617 }
618 618
619 619 &:hover {
620 620 border-color: @grey3;
621 621 box-shadow: @button-shadow;
622 622 }
623 623
624 624 &:focus {
625 625 border-color: @rcblue;
626 626 box-shadow: @button-shadow;
627 627 }
628 628 }
629 629
630 630 // remove extra padding in firefox
631 631 input::-moz-focus-inner { border:0; padding:0 }
632 632
633 633 .adjacent input {
634 634 margin-bottom: @padding;
635 635 }
636 636
637 637 .permissions_boxes {
638 638 display: block;
639 639 }
640 640
641 641 //FORMS
642 642
643 643 .medium-inline,
644 644 input#description.medium-inline {
645 645 display: inline;
646 646 width: @medium-inline-input-width;
647 647 min-width: 100px;
648 648 }
649 649
650 650 select {
651 651 //reset
652 652 -webkit-appearance: none;
653 653 -moz-appearance: none;
654 654
655 655 display: inline-block;
656 656 height: 28px;
657 657 width: auto;
658 658 margin: 0 @padding @padding 0;
659 659 padding: 0 18px 0 8px;
660 660 line-height:1em;
661 661 font-size: @basefontsize;
662 662 border: @border-thickness solid @grey5;
663 663 border-radius: @border-radius;
664 664 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
665 665 color: @grey4;
666 666 box-shadow: @button-shadow;
667 667
668 668 &:after {
669 669 content: "\00A0\25BE";
670 670 }
671 671
672 672 &:focus, &:hover {
673 673 outline: none;
674 674 border-color: @grey4;
675 675 color: @rcdarkblue;
676 676 }
677 677 }
678 678
679 679 option {
680 680 &:focus {
681 681 outline: none;
682 682 }
683 683 }
684 684
685 685 input,
686 686 textarea {
687 687 padding: @input-padding;
688 688 border: @input-border-thickness solid @border-highlight-color;
689 689 .border-radius (@border-radius);
690 690 font-family: @text-light;
691 691 font-size: @basefontsize;
692 692
693 693 &.input-sm {
694 694 padding: 5px;
695 695 }
696 696
697 697 &#description {
698 698 min-width: @input-description-minwidth;
699 699 min-height: 1em;
700 700 padding: 10px;
701 701 }
702 702 }
703 703
704 704 .field-sm {
705 705 input,
706 706 textarea {
707 707 padding: 5px;
708 708 }
709 709 }
710 710
711 711 textarea {
712 712 display: block;
713 713 clear: both;
714 714 width: 100%;
715 715 min-height: 100px;
716 716 margin-bottom: @padding;
717 717 .box-sizing(border-box);
718 718 overflow: auto;
719 719 }
720 720
721 721 label {
722 722 font-family: @text-light;
723 723 }
724 724
725 725 // GRAVATARS
726 726 // centers gravatar on username to the right
727 727
728 728 .gravatar {
729 729 display: inline;
730 730 min-width: 16px;
731 731 min-height: 16px;
732 732 margin: -5px 0;
733 733 padding: 0;
734 734 line-height: 1em;
735 735 box-sizing: content-box;
736 736 border-radius: 50%;
737 737
738 738 &.gravatar-large {
739 739 margin: -0.5em .25em -0.5em 0;
740 740 }
741 741
742 742 & + .user {
743 743 display: inline;
744 744 margin: 0;
745 745 padding: 0 0 0 .17em;
746 746 line-height: 1em;
747 747 }
748 748 }
749 749
750 750 .user-inline-data {
751 751 display: inline-block;
752 752 float: left;
753 753 padding-left: .5em;
754 754 line-height: 1.3em;
755 755 }
756 756
757 757 .rc-user { // gravatar + user wrapper
758 758 float: left;
759 759 position: relative;
760 760 min-width: 100px;
761 761 max-width: 200px;
762 762 min-height: (@gravatar-size + @border-thickness * 2); // account for border
763 763 display: block;
764 764 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
765 765
766 766
767 767 .gravatar {
768 768 display: block;
769 769 position: absolute;
770 770 top: 0;
771 771 left: 0;
772 772 min-width: @gravatar-size;
773 773 min-height: @gravatar-size;
774 774 margin: 0;
775 775 }
776 776
777 777 .user {
778 778 display: block;
779 779 max-width: 175px;
780 780 padding-top: 2px;
781 781 overflow: hidden;
782 782 text-overflow: ellipsis;
783 783 }
784 784 }
785 785
786 786 .gist-gravatar,
787 787 .journal_container {
788 788 .gravatar-large {
789 789 margin: 0 .5em -10px 0;
790 790 }
791 791 }
792 792
793 793
794 794 // ADMIN SETTINGS
795 795
796 796 // Tag Patterns
797 797 .tag_patterns {
798 798 .tag_input {
799 799 margin-bottom: @padding;
800 800 }
801 801 }
802 802
803 803 .locked_input {
804 804 position: relative;
805 805
806 806 input {
807 807 display: inline;
808 808 margin: 3px 5px 0px 0px;
809 809 }
810 810
811 811 br {
812 812 display: none;
813 813 }
814 814
815 815 .error-message {
816 816 float: left;
817 817 width: 100%;
818 818 }
819 819
820 820 .lock_input_button {
821 821 display: inline;
822 822 }
823 823
824 824 .help-block {
825 825 clear: both;
826 826 }
827 827 }
828 828
829 829 // Notifications
830 830
831 831 .notifications_buttons {
832 832 margin: 0 0 @space 0;
833 833 padding: 0;
834 834
835 835 .btn {
836 836 display: inline-block;
837 837 }
838 838 }
839 839
840 840 .notification-list {
841 841
842 842 div {
843 843 display: inline-block;
844 844 vertical-align: middle;
845 845 }
846 846
847 847 .container {
848 848 display: block;
849 849 margin: 0 0 @padding 0;
850 850 }
851 851
852 852 .delete-notifications {
853 853 margin-left: @padding;
854 854 text-align: right;
855 855 cursor: pointer;
856 856 }
857 857
858 858 .read-notifications {
859 859 margin-left: @padding/2;
860 860 text-align: right;
861 861 width: 35px;
862 862 cursor: pointer;
863 863 }
864 864
865 865 .icon-minus-sign {
866 866 color: @alert2;
867 867 }
868 868
869 869 .icon-ok-sign {
870 870 color: @alert1;
871 871 }
872 872 }
873 873
874 874 .user_settings {
875 875 float: left;
876 876 clear: both;
877 877 display: block;
878 878 width: 100%;
879 879
880 880 .gravatar_box {
881 881 margin-bottom: @padding;
882 882
883 883 &:after {
884 884 content: " ";
885 885 clear: both;
886 886 width: 100%;
887 887 }
888 888 }
889 889
890 890 .fields .field {
891 891 clear: both;
892 892 }
893 893 }
894 894
895 895 .advanced_settings {
896 896 margin-bottom: @space;
897 897
898 898 .help-block {
899 899 margin-left: 0;
900 900 }
901 901
902 902 button + .help-block {
903 903 margin-top: @padding;
904 904 }
905 905 }
906 906
907 907 // admin settings radio buttons and labels
908 908 .label-2 {
909 909 float: left;
910 910 width: @label2-width;
911 911
912 912 label {
913 913 color: @grey1;
914 914 }
915 915 }
916 916 .checkboxes {
917 917 float: left;
918 918 width: @checkboxes-width;
919 919 margin-bottom: @padding;
920 920
921 921 .checkbox {
922 922 width: 100%;
923 923
924 924 label {
925 925 margin: 0;
926 926 padding: 0;
927 927 }
928 928 }
929 929
930 930 .checkbox + .checkbox {
931 931 display: inline-block;
932 932 }
933 933
934 934 label {
935 935 margin-right: 1em;
936 936 }
937 937 }
938 938
939 939 // CHANGELOG
940 940 .container_header {
941 941 float: left;
942 942 display: block;
943 943 width: 100%;
944 944 margin: @padding 0 @padding;
945 945
946 946 #filter_changelog {
947 947 float: left;
948 948 margin-right: @padding;
949 949 }
950 950
951 951 .breadcrumbs_light {
952 952 display: inline-block;
953 953 }
954 954 }
955 955
956 956 .info_box {
957 957 float: right;
958 958 }
959 959
960 960
961 961 #graph_nodes {
962 962 padding-top: 43px;
963 963 }
964 964
965 965 #graph_content{
966 966
967 967 // adjust for table headers so that graph renders properly
968 968 // #graph_nodes padding - table cell padding
969 969 padding-top: (@space - (@basefontsize * 2.4));
970 970
971 971 &.graph_full_width {
972 972 width: 100%;
973 973 max-width: 100%;
974 974 }
975 975 }
976 976
977 977 #graph {
978 978 .flag_status {
979 979 margin: 0;
980 980 }
981 981
982 982 .pagination-left {
983 983 float: left;
984 984 clear: both;
985 985 }
986 986
987 987 .log-container {
988 988 max-width: 345px;
989 989
990 990 .message{
991 991 max-width: 340px;
992 992 }
993 993 }
994 994
995 995 .graph-col-wrapper {
996 996 padding-left: 110px;
997 997
998 998 #graph_nodes {
999 999 width: 100px;
1000 1000 margin-left: -110px;
1001 1001 float: left;
1002 1002 clear: left;
1003 1003 }
1004 1004 }
1005 1005
1006 1006 .load-more-commits {
1007 1007 text-align: center;
1008 1008 }
1009 1009 .load-more-commits:hover {
1010 1010 background-color: @grey7;
1011 1011 }
1012 1012 .load-more-commits {
1013 1013 a {
1014 1014 display: block;
1015 1015 }
1016 1016 }
1017 1017 }
1018 1018
1019 1019 #filter_changelog {
1020 1020 float: left;
1021 1021 }
1022 1022
1023 1023
1024 1024 //--- THEME ------------------//
1025 1025
1026 1026 #logo {
1027 1027 float: left;
1028 1028 margin: 9px 0 0 0;
1029 1029
1030 1030 .header {
1031 1031 background-color: transparent;
1032 1032 }
1033 1033
1034 1034 a {
1035 1035 display: inline-block;
1036 1036 }
1037 1037
1038 1038 img {
1039 1039 height:30px;
1040 1040 }
1041 1041 }
1042 1042
1043 1043 .logo-wrapper {
1044 1044 float:left;
1045 1045 }
1046 1046
1047 1047 .branding {
1048 1048 float: left;
1049 1049 padding: 9px 2px;
1050 1050 line-height: 1em;
1051 1051 font-size: @navigation-fontsize;
1052 1052
1053 1053 a {
1054 1054 color: @grey5
1055 1055 }
1056 1056 }
1057 1057
1058 1058 img {
1059 1059 border: none;
1060 1060 outline: none;
1061 1061 }
1062 1062 user-profile-header
1063 1063 label {
1064 1064
1065 1065 input[type="checkbox"] {
1066 1066 margin-right: 1em;
1067 1067 }
1068 1068 input[type="radio"] {
1069 1069 margin-right: 1em;
1070 1070 }
1071 1071 }
1072 1072
1073 1073 .flag_status {
1074 1074 margin: 2px;
1075 1075 &.under_review {
1076 1076 .circle(5px, @alert3);
1077 1077 }
1078 1078 &.approved {
1079 1079 .circle(5px, @alert1);
1080 1080 }
1081 1081 &.rejected,
1082 1082 &.forced_closed{
1083 1083 .circle(5px, @alert2);
1084 1084 }
1085 1085 &.not_reviewed {
1086 1086 .circle(5px, @grey5);
1087 1087 }
1088 1088 }
1089 1089
1090 1090 .flag_status_comment_box {
1091 1091 margin: 5px 6px 0px 2px;
1092 1092 }
1093 1093 .test_pattern_preview {
1094 1094 margin: @space 0;
1095 1095
1096 1096 p {
1097 1097 margin-bottom: 0;
1098 1098 border-bottom: @border-thickness solid @border-default-color;
1099 1099 color: @grey3;
1100 1100 }
1101 1101
1102 1102 .btn {
1103 1103 margin-bottom: @padding;
1104 1104 }
1105 1105 }
1106 1106 #test_pattern_result {
1107 1107 display: none;
1108 1108 &:extend(pre);
1109 1109 padding: .9em;
1110 1110 color: @grey3;
1111 1111 background-color: @grey7;
1112 1112 border-right: @border-thickness solid @border-default-color;
1113 1113 border-bottom: @border-thickness solid @border-default-color;
1114 1114 border-left: @border-thickness solid @border-default-color;
1115 1115 }
1116 1116
1117 1117 #repo_vcs_settings {
1118 1118 #inherit_overlay_vcs_default {
1119 1119 display: none;
1120 1120 }
1121 1121 #inherit_overlay_vcs_custom {
1122 1122 display: custom;
1123 1123 }
1124 1124 &.inherited {
1125 1125 #inherit_overlay_vcs_default {
1126 1126 display: block;
1127 1127 }
1128 1128 #inherit_overlay_vcs_custom {
1129 1129 display: none;
1130 1130 }
1131 1131 }
1132 1132 }
1133 1133
1134 1134 .issue-tracker-link {
1135 1135 color: @rcblue;
1136 1136 }
1137 1137
1138 1138 // Issue Tracker Table Show/Hide
1139 1139 #repo_issue_tracker {
1140 1140 #inherit_overlay {
1141 1141 display: none;
1142 1142 }
1143 1143 #custom_overlay {
1144 1144 display: custom;
1145 1145 }
1146 1146 &.inherited {
1147 1147 #inherit_overlay {
1148 1148 display: block;
1149 1149 }
1150 1150 #custom_overlay {
1151 1151 display: none;
1152 1152 }
1153 1153 }
1154 1154 }
1155 1155 table.issuetracker {
1156 1156 &.readonly {
1157 1157 tr, td {
1158 1158 color: @grey3;
1159 1159 }
1160 1160 }
1161 1161 .edit {
1162 1162 display: none;
1163 1163 }
1164 1164 .editopen {
1165 1165 .edit {
1166 1166 display: inline;
1167 1167 }
1168 1168 .entry {
1169 1169 display: none;
1170 1170 }
1171 1171 }
1172 1172 tr td.td-action {
1173 1173 min-width: 117px;
1174 1174 }
1175 1175 td input {
1176 1176 max-width: none;
1177 1177 min-width: 30px;
1178 1178 width: 80%;
1179 1179 }
1180 1180 .issuetracker_pref input {
1181 1181 width: 40%;
1182 1182 }
1183 1183 input.edit_issuetracker_update {
1184 1184 margin-right: 0;
1185 1185 width: auto;
1186 1186 }
1187 1187 }
1188 1188
1189 1189 table.integrations {
1190 1190 .td-icon {
1191 1191 width: 20px;
1192 1192 .integration-icon {
1193 1193 height: 20px;
1194 1194 width: 20px;
1195 1195 }
1196 1196 }
1197 1197 }
1198 1198
1199 1199 .integrations {
1200 1200 a.integration-box {
1201 1201 color: @text-color;
1202 1202 &:hover {
1203 1203 .panel {
1204 1204 background: #fbfbfb;
1205 1205 }
1206 1206 }
1207 1207 .integration-icon {
1208 1208 width: 30px;
1209 1209 height: 30px;
1210 1210 margin-right: 20px;
1211 1211 float: left;
1212 1212 }
1213 1213
1214 1214 .panel-body {
1215 1215 padding: 10px;
1216 1216 }
1217 1217 .panel {
1218 1218 margin-bottom: 10px;
1219 1219 }
1220 1220 h2 {
1221 1221 display: inline-block;
1222 1222 margin: 0;
1223 1223 min-width: 140px;
1224 1224 }
1225 1225 }
1226 1226 a.integration-box.dummy-integration {
1227 1227 color: @grey4
1228 1228 }
1229 1229 }
1230 1230
1231 1231 //Permissions Settings
1232 1232 #add_perm {
1233 1233 margin: 0 0 @padding;
1234 1234 cursor: pointer;
1235 1235 }
1236 1236
1237 1237 .perm_ac {
1238 1238 input {
1239 1239 width: 95%;
1240 1240 }
1241 1241 }
1242 1242
1243 1243 .autocomplete-suggestions {
1244 1244 width: auto !important; // overrides autocomplete.js
1245 1245 min-width: 278px;
1246 1246 margin: 0;
1247 1247 border: @border-thickness solid @grey5;
1248 1248 border-radius: @border-radius;
1249 1249 color: @grey2;
1250 1250 background-color: white;
1251 1251 }
1252 1252
1253 1253 .autocomplete-selected {
1254 1254 background: #F0F0F0;
1255 1255 }
1256 1256
1257 1257 .ac-container-wrap {
1258 1258 margin: 0;
1259 1259 padding: 8px;
1260 1260 border-bottom: @border-thickness solid @grey5;
1261 1261 list-style-type: none;
1262 1262 cursor: pointer;
1263 1263
1264 1264 &:hover {
1265 1265 background-color: @grey7;
1266 1266 }
1267 1267
1268 1268 img {
1269 1269 height: @gravatar-size;
1270 1270 width: @gravatar-size;
1271 1271 margin-right: 1em;
1272 1272 }
1273 1273
1274 1274 strong {
1275 1275 font-weight: normal;
1276 1276 }
1277 1277 }
1278 1278
1279 1279 // Settings Dropdown
1280 1280 .user-menu .container {
1281 1281 padding: 0 4px;
1282 1282 margin: 0;
1283 1283 }
1284 1284
1285 1285 .user-menu .gravatar {
1286 1286 cursor: pointer;
1287 1287 }
1288 1288
1289 1289 .codeblock {
1290 1290 margin-bottom: @padding;
1291 1291 clear: both;
1292 1292
1293 1293 .stats {
1294 1294 overflow: hidden;
1295 1295 }
1296 1296
1297 1297 .message{
1298 1298 textarea{
1299 1299 margin: 0;
1300 1300 }
1301 1301 }
1302 1302
1303 1303 .code-header {
1304 1304 .stats {
1305 1305 line-height: 2em;
1306 1306
1307 1307 .revision_id {
1308 1308 margin-left: 0;
1309 1309 }
1310 1310 .buttons {
1311 1311 padding-right: 0;
1312 1312 }
1313 1313 }
1314 1314
1315 1315 .item{
1316 1316 margin-right: 0.5em;
1317 1317 }
1318 1318 }
1319 1319
1320 1320 #editor_container{
1321 1321 position: relative;
1322 1322 margin: @padding;
1323 1323 }
1324 1324 }
1325 1325
1326 1326 #file_history_container {
1327 1327 display: none;
1328 1328 }
1329 1329
1330 1330 .file-history-inner {
1331 1331 margin-bottom: 10px;
1332 1332 }
1333 1333
1334 1334 // Pull Requests
1335 1335 .summary-details {
1336 1336 width: 72%;
1337 1337 }
1338 1338 .pr-summary {
1339 1339 border-bottom: @border-thickness solid @grey5;
1340 1340 margin-bottom: @space;
1341 1341 }
1342 1342 .reviewers-title {
1343 1343 width: 25%;
1344 1344 min-width: 200px;
1345 1345 }
1346 1346 .reviewers {
1347 1347 width: 25%;
1348 1348 min-width: 200px;
1349 1349 }
1350 1350 .reviewers ul li {
1351 1351 position: relative;
1352 1352 width: 100%;
1353 1353 padding-bottom: 8px;
1354 1354 list-style-type: none;
1355 1355 }
1356 1356
1357 1357 .reviewer_entry {
1358 1358 min-height: 55px;
1359 1359 }
1360 1360
1361 1361 .reviewers_member {
1362 1362 width: 100%;
1363 1363 overflow: auto;
1364 1364 }
1365 1365 .reviewer_reason {
1366 1366 padding-left: 20px;
1367 1367 line-height: 1.5em;
1368 1368 }
1369 1369 .reviewer_status {
1370 1370 display: inline-block;
1371 1371 vertical-align: top;
1372 1372 width: 25px;
1373 1373 min-width: 25px;
1374 1374 height: 1.2em;
1375 1375 margin-top: 3px;
1376 1376 line-height: 1em;
1377 1377 }
1378 1378
1379 1379 .reviewer_name {
1380 1380 display: inline-block;
1381 1381 max-width: 83%;
1382 1382 padding-right: 20px;
1383 1383 vertical-align: middle;
1384 1384 line-height: 1;
1385 1385
1386 1386 .rc-user {
1387 1387 min-width: 0;
1388 1388 margin: -2px 1em 0 0;
1389 1389 }
1390 1390
1391 1391 .reviewer {
1392 1392 float: left;
1393 1393 }
1394 1394 }
1395 1395
1396 1396 .reviewer_member_mandatory {
1397 1397 position: absolute;
1398 1398 left: 15px;
1399 1399 top: 8px;
1400 1400 width: 16px;
1401 1401 font-size: 11px;
1402 1402 margin: 0;
1403 1403 padding: 0;
1404 1404 color: black;
1405 1405 }
1406 1406
1407 1407 .reviewer_member_mandatory_remove,
1408 1408 .reviewer_member_remove {
1409 1409 position: absolute;
1410 1410 right: 0;
1411 1411 top: 0;
1412 1412 width: 16px;
1413 1413 margin-bottom: 10px;
1414 1414 padding: 0;
1415 1415 color: black;
1416 1416 }
1417 1417
1418 1418 .reviewer_member_mandatory_remove {
1419 1419 color: @grey4;
1420 1420 }
1421 1421
1422 1422 .reviewer_member_status {
1423 1423 margin-top: 5px;
1424 1424 }
1425 1425 .pr-summary #summary{
1426 1426 width: 100%;
1427 1427 }
1428 1428 .pr-summary .action_button:hover {
1429 1429 border: 0;
1430 1430 cursor: pointer;
1431 1431 }
1432 1432 .pr-details-title {
1433 1433 padding-bottom: 8px;
1434 1434 border-bottom: @border-thickness solid @grey5;
1435 1435
1436 1436 .action_button.disabled {
1437 1437 color: @grey4;
1438 1438 cursor: inherit;
1439 1439 }
1440 1440 .action_button {
1441 1441 color: @rcblue;
1442 1442 }
1443 1443 }
1444 1444 .pr-details-content {
1445 1445 margin-top: @textmargin;
1446 1446 margin-bottom: @textmargin;
1447 1447 }
1448 1448
1449 1449 .pr-reviewer-rules {
1450 1450 padding: 10px 0px 20px 0px;
1451 1451 }
1452 1452
1453 1453 .group_members {
1454 1454 margin-top: 0;
1455 1455 padding: 0;
1456 1456 list-style: outside none none;
1457 1457
1458 1458 img {
1459 1459 height: @gravatar-size;
1460 1460 width: @gravatar-size;
1461 1461 margin-right: .5em;
1462 1462 margin-left: 3px;
1463 1463 }
1464 1464
1465 1465 .to-delete {
1466 1466 .user {
1467 1467 text-decoration: line-through;
1468 1468 }
1469 1469 }
1470 1470 }
1471 1471
1472 1472 .compare_view_commits_title {
1473 1473 .disabled {
1474 1474 cursor: inherit;
1475 1475 &:hover{
1476 1476 background-color: inherit;
1477 1477 color: inherit;
1478 1478 }
1479 1479 }
1480 1480 }
1481 1481
1482 1482 .subtitle-compare {
1483 1483 margin: -15px 0px 0px 0px;
1484 1484 }
1485 1485
1486 1486 .comments-summary-td {
1487 1487 border-top: 1px dashed @grey5;
1488 1488 }
1489 1489
1490 1490 // new entry in group_members
1491 1491 .td-author-new-entry {
1492 1492 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1493 1493 }
1494 1494
1495 1495 .usergroup_member_remove {
1496 1496 width: 16px;
1497 1497 margin-bottom: 10px;
1498 1498 padding: 0;
1499 1499 color: black !important;
1500 1500 cursor: pointer;
1501 1501 }
1502 1502
1503 1503 .reviewer_ac .ac-input {
1504 1504 width: 92%;
1505 1505 margin-bottom: 1em;
1506 1506 }
1507 1507
1508 1508 .compare_view_commits tr{
1509 1509 height: 20px;
1510 1510 }
1511 1511 .compare_view_commits td {
1512 1512 vertical-align: top;
1513 1513 padding-top: 10px;
1514 1514 }
1515 1515 .compare_view_commits .author {
1516 1516 margin-left: 5px;
1517 1517 }
1518 1518
1519 1519 .compare_view_commits {
1520 1520 .color-a {
1521 1521 color: @alert1;
1522 1522 }
1523 1523
1524 1524 .color-c {
1525 1525 color: @color3;
1526 1526 }
1527 1527
1528 1528 .color-r {
1529 1529 color: @color5;
1530 1530 }
1531 1531
1532 1532 .color-a-bg {
1533 1533 background-color: @alert1;
1534 1534 }
1535 1535
1536 1536 .color-c-bg {
1537 1537 background-color: @alert3;
1538 1538 }
1539 1539
1540 1540 .color-r-bg {
1541 1541 background-color: @alert2;
1542 1542 }
1543 1543
1544 1544 .color-a-border {
1545 1545 border: 1px solid @alert1;
1546 1546 }
1547 1547
1548 1548 .color-c-border {
1549 1549 border: 1px solid @alert3;
1550 1550 }
1551 1551
1552 1552 .color-r-border {
1553 1553 border: 1px solid @alert2;
1554 1554 }
1555 1555
1556 1556 .commit-change-indicator {
1557 1557 width: 15px;
1558 1558 height: 15px;
1559 1559 position: relative;
1560 1560 left: 15px;
1561 1561 }
1562 1562
1563 1563 .commit-change-content {
1564 1564 text-align: center;
1565 1565 vertical-align: middle;
1566 1566 line-height: 15px;
1567 1567 }
1568 1568 }
1569 1569
1570 1570 .compare_view_filepath {
1571 1571 color: @grey1;
1572 1572 }
1573 1573
1574 1574 .show_more {
1575 1575 display: inline-block;
1576 1576 width: 0;
1577 1577 height: 0;
1578 1578 vertical-align: middle;
1579 1579 content: "";
1580 1580 border: 4px solid;
1581 1581 border-right-color: transparent;
1582 1582 border-bottom-color: transparent;
1583 1583 border-left-color: transparent;
1584 1584 font-size: 0;
1585 1585 }
1586 1586
1587 1587 .journal_more .show_more {
1588 1588 display: inline;
1589 1589
1590 1590 &:after {
1591 1591 content: none;
1592 1592 }
1593 1593 }
1594 1594
1595 1595 .compare_view_commits .collapse_commit:after {
1596 1596 cursor: pointer;
1597 1597 content: "\00A0\25B4";
1598 1598 margin-left: -3px;
1599 1599 font-size: 17px;
1600 1600 color: @grey4;
1601 1601 }
1602 1602
1603 1603 .diff_links {
1604 1604 margin-left: 8px;
1605 1605 }
1606 1606
1607 1607 div.ancestor {
1608 1608 margin: -30px 0px;
1609 1609 }
1610 1610
1611 1611 .cs_icon_td input[type="checkbox"] {
1612 1612 display: none;
1613 1613 }
1614 1614
1615 1615 .cs_icon_td .expand_file_icon:after {
1616 1616 cursor: pointer;
1617 1617 content: "\00A0\25B6";
1618 1618 font-size: 12px;
1619 1619 color: @grey4;
1620 1620 }
1621 1621
1622 1622 .cs_icon_td .collapse_file_icon:after {
1623 1623 cursor: pointer;
1624 1624 content: "\00A0\25BC";
1625 1625 font-size: 12px;
1626 1626 color: @grey4;
1627 1627 }
1628 1628
1629 1629 /*new binary
1630 1630 NEW_FILENODE = 1
1631 1631 DEL_FILENODE = 2
1632 1632 MOD_FILENODE = 3
1633 1633 RENAMED_FILENODE = 4
1634 1634 COPIED_FILENODE = 5
1635 1635 CHMOD_FILENODE = 6
1636 1636 BIN_FILENODE = 7
1637 1637 */
1638 1638 .cs_files_expand {
1639 1639 font-size: @basefontsize + 5px;
1640 1640 line-height: 1.8em;
1641 1641 float: right;
1642 1642 }
1643 1643
1644 1644 .cs_files_expand span{
1645 1645 color: @rcblue;
1646 1646 cursor: pointer;
1647 1647 }
1648 1648 .cs_files {
1649 1649 clear: both;
1650 1650 padding-bottom: @padding;
1651 1651
1652 1652 .cur_cs {
1653 1653 margin: 10px 2px;
1654 1654 font-weight: bold;
1655 1655 }
1656 1656
1657 1657 .node {
1658 1658 float: left;
1659 1659 }
1660 1660
1661 1661 .changes {
1662 1662 float: right;
1663 1663 color: white;
1664 1664 font-size: @basefontsize - 4px;
1665 1665 margin-top: 4px;
1666 1666 opacity: 0.6;
1667 1667 filter: Alpha(opacity=60); /* IE8 and earlier */
1668 1668
1669 1669 .added {
1670 1670 background-color: @alert1;
1671 1671 float: left;
1672 1672 text-align: center;
1673 1673 }
1674 1674
1675 1675 .deleted {
1676 1676 background-color: @alert2;
1677 1677 float: left;
1678 1678 text-align: center;
1679 1679 }
1680 1680
1681 1681 .bin {
1682 1682 background-color: @alert1;
1683 1683 text-align: center;
1684 1684 }
1685 1685
1686 1686 /*new binary*/
1687 1687 .bin.bin1 {
1688 1688 background-color: @alert1;
1689 1689 text-align: center;
1690 1690 }
1691 1691
1692 1692 /*deleted binary*/
1693 1693 .bin.bin2 {
1694 1694 background-color: @alert2;
1695 1695 text-align: center;
1696 1696 }
1697 1697
1698 1698 /*mod binary*/
1699 1699 .bin.bin3 {
1700 1700 background-color: @grey2;
1701 1701 text-align: center;
1702 1702 }
1703 1703
1704 1704 /*rename file*/
1705 1705 .bin.bin4 {
1706 1706 background-color: @alert4;
1707 1707 text-align: center;
1708 1708 }
1709 1709
1710 1710 /*copied file*/
1711 1711 .bin.bin5 {
1712 1712 background-color: @alert4;
1713 1713 text-align: center;
1714 1714 }
1715 1715
1716 1716 /*chmod file*/
1717 1717 .bin.bin6 {
1718 1718 background-color: @grey2;
1719 1719 text-align: center;
1720 1720 }
1721 1721 }
1722 1722 }
1723 1723
1724 1724 .cs_files .cs_added, .cs_files .cs_A,
1725 1725 .cs_files .cs_added, .cs_files .cs_M,
1726 1726 .cs_files .cs_added, .cs_files .cs_D {
1727 1727 height: 16px;
1728 1728 padding-right: 10px;
1729 1729 margin-top: 7px;
1730 1730 text-align: left;
1731 1731 }
1732 1732
1733 1733 .cs_icon_td {
1734 1734 min-width: 16px;
1735 1735 width: 16px;
1736 1736 }
1737 1737
1738 1738 .pull-request-merge {
1739 1739 border: 1px solid @grey5;
1740 1740 padding: 10px 0px 20px;
1741 1741 margin-top: 10px;
1742 1742 margin-bottom: 20px;
1743 1743 }
1744 1744
1745 1745 .pull-request-merge ul {
1746 1746 padding: 0px 0px;
1747 1747 }
1748 1748
1749 1749 .pull-request-merge li {
1750 1750 list-style-type: none;
1751 1751 }
1752 1752
1753 1753 .pull-request-merge .pull-request-wrap {
1754 1754 height: auto;
1755 1755 padding: 0px 0px;
1756 1756 text-align: right;
1757 1757 }
1758 1758
1759 1759 .pull-request-merge span {
1760 1760 margin-right: 5px;
1761 1761 }
1762 1762
1763 1763 .pull-request-merge-actions {
1764 1764 min-height: 30px;
1765 1765 padding: 0px 0px;
1766 1766 }
1767 1767
1768 1768 .pull-request-merge-info {
1769 1769 padding: 0px 5px 5px 0px;
1770 1770 }
1771 1771
1772 1772 .merge-status {
1773 1773 margin-right: 5px;
1774 1774 }
1775 1775
1776 1776 .merge-message {
1777 1777 font-size: 1.2em
1778 1778 }
1779 1779
1780 1780 .merge-message.success i,
1781 1781 .merge-icon.success i {
1782 1782 color:@alert1;
1783 1783 }
1784 1784
1785 1785 .merge-message.warning i,
1786 1786 .merge-icon.warning i {
1787 1787 color: @alert3;
1788 1788 }
1789 1789
1790 1790 .merge-message.error i,
1791 1791 .merge-icon.error i {
1792 1792 color:@alert2;
1793 1793 }
1794 1794
1795 1795 .pr-versions {
1796 1796 font-size: 1.1em;
1797 1797
1798 1798 table {
1799 1799 padding: 0px 5px;
1800 1800 }
1801 1801
1802 1802 td {
1803 1803 line-height: 15px;
1804 1804 }
1805 1805
1806 1806 .flag_status {
1807 1807 margin: 0;
1808 1808 }
1809 1809
1810 1810 .compare-radio-button {
1811 1811 position: relative;
1812 1812 top: -3px;
1813 1813 }
1814 1814 }
1815 1815
1816 1816
1817 1817 #close_pull_request {
1818 1818 margin-right: 0px;
1819 1819 }
1820 1820
1821 1821 .empty_data {
1822 1822 color: @grey4;
1823 1823 }
1824 1824
1825 1825 #changeset_compare_view_content {
1826 1826 clear: both;
1827 1827 width: 100%;
1828 1828 box-sizing: border-box;
1829 1829 .border-radius(@border-radius);
1830 1830
1831 1831 .help-block {
1832 1832 margin: @padding 0;
1833 1833 color: @text-color;
1834 1834 &.pre-formatting {
1835 1835 white-space: pre;
1836 1836 }
1837 1837 }
1838 1838
1839 1839 .empty_data {
1840 1840 margin: @padding 0;
1841 1841 }
1842 1842
1843 1843 .alert {
1844 1844 margin-bottom: @space;
1845 1845 }
1846 1846 }
1847 1847
1848 1848 .table_disp {
1849 1849 .status {
1850 1850 width: auto;
1851 1851
1852 1852 .flag_status {
1853 1853 float: left;
1854 1854 }
1855 1855 }
1856 1856 }
1857 1857
1858 1858
1859 1859 .creation_in_progress {
1860 1860 color: @grey4
1861 1861 }
1862 1862
1863 1863 .status_box_menu {
1864 1864 margin: 0;
1865 1865 }
1866 1866
1867 1867 .notification-table{
1868 1868 margin-bottom: @space;
1869 1869 display: table;
1870 1870 width: 100%;
1871 1871
1872 1872 .container{
1873 1873 display: table-row;
1874 1874
1875 1875 .notification-header{
1876 1876 border-bottom: @border-thickness solid @border-default-color;
1877 1877 }
1878 1878
1879 1879 .notification-subject{
1880 1880 display: table-cell;
1881 1881 }
1882 1882 }
1883 1883 }
1884 1884
1885 1885 // Notifications
1886 1886 .notification-header{
1887 1887 display: table;
1888 1888 width: 100%;
1889 1889 padding: floor(@basefontsize/2) 0;
1890 1890 line-height: 1em;
1891 1891
1892 1892 .desc, .delete-notifications, .read-notifications{
1893 1893 display: table-cell;
1894 1894 text-align: left;
1895 1895 }
1896 1896
1897 1897 .desc{
1898 1898 width: 1163px;
1899 1899 }
1900 1900
1901 1901 .delete-notifications, .read-notifications{
1902 1902 width: 35px;
1903 1903 min-width: 35px; //fixes when only one button is displayed
1904 1904 }
1905 1905 }
1906 1906
1907 1907 .notification-body {
1908 1908 .markdown-block,
1909 1909 .rst-block {
1910 1910 padding: @padding 0;
1911 1911 }
1912 1912
1913 1913 .notification-subject {
1914 1914 padding: @textmargin 0;
1915 1915 border-bottom: @border-thickness solid @border-default-color;
1916 1916 }
1917 1917 }
1918 1918
1919 1919
1920 1920 .notifications_buttons{
1921 1921 float: right;
1922 1922 }
1923 1923
1924 1924 #notification-status{
1925 1925 display: inline;
1926 1926 }
1927 1927
1928 1928 // Repositories
1929 1929
1930 1930 #summary.fields{
1931 1931 display: table;
1932 1932
1933 1933 .field{
1934 1934 display: table-row;
1935 1935
1936 1936 .label-summary{
1937 1937 display: table-cell;
1938 1938 min-width: @label-summary-minwidth;
1939 1939 padding-top: @padding/2;
1940 1940 padding-bottom: @padding/2;
1941 1941 padding-right: @padding/2;
1942 1942 }
1943 1943
1944 1944 .input{
1945 1945 display: table-cell;
1946 1946 padding: @padding/2;
1947 1947
1948 1948 input{
1949 1949 min-width: 29em;
1950 1950 padding: @padding/4;
1951 1951 }
1952 1952 }
1953 1953 .statistics, .downloads{
1954 1954 .disabled{
1955 1955 color: @grey4;
1956 1956 }
1957 1957 }
1958 1958 }
1959 1959 }
1960 1960
1961 1961 #summary{
1962 1962 width: 70%;
1963 1963 }
1964 1964
1965 1965
1966 1966 // Journal
1967 1967 .journal.title {
1968 1968 h5 {
1969 1969 float: left;
1970 1970 margin: 0;
1971 1971 width: 70%;
1972 1972 }
1973 1973
1974 1974 ul {
1975 1975 float: right;
1976 1976 display: inline-block;
1977 1977 margin: 0;
1978 1978 width: 30%;
1979 1979 text-align: right;
1980 1980
1981 1981 li {
1982 1982 display: inline;
1983 1983 font-size: @journal-fontsize;
1984 1984 line-height: 1em;
1985 1985
1986 1986 list-style-type: none;
1987 1987 }
1988 1988 }
1989 1989 }
1990 1990
1991 1991 .filterexample {
1992 1992 position: absolute;
1993 1993 top: 95px;
1994 1994 left: @contentpadding;
1995 1995 color: @rcblue;
1996 1996 font-size: 11px;
1997 1997 font-family: @text-regular;
1998 1998 cursor: help;
1999 1999
2000 2000 &:hover {
2001 2001 color: @rcdarkblue;
2002 2002 }
2003 2003
2004 2004 @media (max-width:768px) {
2005 2005 position: relative;
2006 2006 top: auto;
2007 2007 left: auto;
2008 2008 display: block;
2009 2009 }
2010 2010 }
2011 2011
2012 2012
2013 2013 #journal{
2014 2014 margin-bottom: @space;
2015 2015
2016 2016 .journal_day{
2017 2017 margin-bottom: @textmargin/2;
2018 2018 padding-bottom: @textmargin/2;
2019 2019 font-size: @journal-fontsize;
2020 2020 border-bottom: @border-thickness solid @border-default-color;
2021 2021 }
2022 2022
2023 2023 .journal_container{
2024 2024 margin-bottom: @space;
2025 2025
2026 2026 .journal_user{
2027 2027 display: inline-block;
2028 2028 }
2029 2029 .journal_action_container{
2030 2030 display: block;
2031 2031 margin-top: @textmargin;
2032 2032
2033 2033 div{
2034 2034 display: inline;
2035 2035 }
2036 2036
2037 2037 div.journal_action_params{
2038 2038 display: block;
2039 2039 }
2040 2040
2041 2041 div.journal_repo:after{
2042 2042 content: "\A";
2043 2043 white-space: pre;
2044 2044 }
2045 2045
2046 2046 div.date{
2047 2047 display: block;
2048 2048 margin-bottom: @textmargin;
2049 2049 }
2050 2050 }
2051 2051 }
2052 2052 }
2053 2053
2054 2054 // Files
2055 2055 .edit-file-title {
2056 2056 border-bottom: @border-thickness solid @border-default-color;
2057 2057
2058 2058 .breadcrumbs {
2059 2059 margin-bottom: 0;
2060 2060 }
2061 2061 }
2062 2062
2063 2063 .edit-file-fieldset {
2064 2064 margin-top: @sidebarpadding;
2065 2065
2066 2066 .fieldset {
2067 2067 .left-label {
2068 2068 width: 13%;
2069 2069 }
2070 2070 .right-content {
2071 2071 width: 87%;
2072 2072 max-width: 100%;
2073 2073 }
2074 2074 .filename-label {
2075 2075 margin-top: 13px;
2076 2076 }
2077 2077 .commit-message-label {
2078 2078 margin-top: 4px;
2079 2079 }
2080 2080 .file-upload-input {
2081 2081 input {
2082 2082 display: none;
2083 2083 }
2084 2084 margin-top: 10px;
2085 2085 }
2086 2086 .file-upload-label {
2087 2087 margin-top: 10px;
2088 2088 }
2089 2089 p {
2090 2090 margin-top: 5px;
2091 2091 }
2092 2092
2093 2093 }
2094 2094 .custom-path-link {
2095 2095 margin-left: 5px;
2096 2096 }
2097 2097 #commit {
2098 2098 resize: vertical;
2099 2099 }
2100 2100 }
2101 2101
2102 2102 .delete-file-preview {
2103 2103 max-height: 250px;
2104 2104 }
2105 2105
2106 2106 .new-file,
2107 2107 #filter_activate,
2108 2108 #filter_deactivate {
2109 2109 float: right;
2110 2110 margin: 0 0 0 10px;
2111 2111 }
2112 2112
2113 2113 h3.files_location{
2114 2114 line-height: 2.4em;
2115 2115 }
2116 2116
2117 2117 .browser-nav {
2118 2118 width: 100%;
2119 2119 display: table;
2120 2120 margin-bottom: 20px;
2121 2121
2122 2122 .info_box {
2123 2123 float: left;
2124 2124 display: inline-table;
2125 2125 height: 2.5em;
2126 2126
2127 2127 .browser-cur-rev, .info_box_elem {
2128 2128 display: table-cell;
2129 2129 vertical-align: middle;
2130 2130 }
2131 2131
2132 2132 .drop-menu {
2133 2133 margin: 0 10px;
2134 2134 }
2135 2135
2136 2136 .info_box_elem {
2137 2137 border-top: @border-thickness solid @grey5;
2138 2138 border-bottom: @border-thickness solid @grey5;
2139 2139 box-shadow: @button-shadow;
2140 2140
2141 2141 #at_rev, a {
2142 2142 padding: 0.6em 0.4em;
2143 2143 margin: 0;
2144 2144 .box-shadow(none);
2145 2145 border: 0;
2146 2146 height: 12px;
2147 2147 color: @grey2;
2148 2148 }
2149 2149
2150 2150 input#at_rev {
2151 2151 max-width: 50px;
2152 2152 text-align: center;
2153 2153 }
2154 2154
2155 2155 &.previous {
2156 2156 border: @border-thickness solid @grey5;
2157 2157 border-top-left-radius: @border-radius;
2158 2158 border-bottom-left-radius: @border-radius;
2159 2159
2160 2160 &:hover {
2161 2161 border-color: @grey4;
2162 2162 }
2163 2163
2164 2164 .disabled {
2165 2165 color: @grey5;
2166 2166 cursor: not-allowed;
2167 2167 opacity: 0.5;
2168 2168 }
2169 2169 }
2170 2170
2171 2171 &.next {
2172 2172 border: @border-thickness solid @grey5;
2173 2173 border-top-right-radius: @border-radius;
2174 2174 border-bottom-right-radius: @border-radius;
2175 2175
2176 2176 &:hover {
2177 2177 border-color: @grey4;
2178 2178 }
2179 2179
2180 2180 .disabled {
2181 2181 color: @grey5;
2182 2182 cursor: not-allowed;
2183 2183 opacity: 0.5;
2184 2184 }
2185 2185 }
2186 2186 }
2187 2187
2188 2188 .browser-cur-rev {
2189 2189
2190 2190 span{
2191 2191 margin: 0;
2192 2192 color: @rcblue;
2193 2193 height: 12px;
2194 2194 display: inline-block;
2195 2195 padding: 0.7em 1em ;
2196 2196 border: @border-thickness solid @rcblue;
2197 2197 margin-right: @padding;
2198 2198 }
2199 2199 }
2200 2200
2201 2201 }
2202 2202
2203 2203 .select-index-number {
2204 2204 margin: 0 0 0 20px;
2205 2205 color: @grey3;
2206 2206 }
2207 2207
2208 2208 .search_activate {
2209 2209 display: table-cell;
2210 2210 vertical-align: middle;
2211 2211
2212 2212 input, label{
2213 2213 margin: 0;
2214 2214 padding: 0;
2215 2215 }
2216 2216
2217 2217 input{
2218 2218 margin-left: @textmargin;
2219 2219 }
2220 2220
2221 2221 }
2222 2222 }
2223 2223
2224 2224 .browser-cur-rev{
2225 2225 margin-bottom: @textmargin;
2226 2226 }
2227 2227
2228 2228 #node_filter_box_loading{
2229 2229 .info_text;
2230 2230 }
2231 2231
2232 2232 .browser-search {
2233 2233 margin: -25px 0px 5px 0px;
2234 2234 }
2235 2235
2236 2236 .files-quick-filter {
2237 2237 float: right;
2238 2238 width: 180px;
2239 2239 position: relative;
2240 2240 }
2241 2241
2242 2242 .files-filter-box {
2243 2243 display: flex;
2244 2244 padding: 0px;
2245 2245 border-radius: 3px;
2246 2246 margin-bottom: 0;
2247 2247
2248 2248 a {
2249 2249 border: none !important;
2250 2250 }
2251 2251
2252 2252 li {
2253 2253 list-style-type: none
2254 2254 }
2255 2255 }
2256 2256
2257 2257 .files-filter-box-path {
2258 2258 line-height: 33px;
2259 2259 padding: 0;
2260 2260 width: 20px;
2261 2261 position: absolute;
2262 2262 z-index: 11;
2263 2263 left: 5px;
2264 2264 }
2265 2265
2266 2266 .files-filter-box-input {
2267 2267 margin-right: 0;
2268 2268
2269 2269 input {
2270 2270 border: 1px solid @white;
2271 2271 padding-left: 25px;
2272 2272 width: 145px;
2273 2273
2274 2274 &:hover {
2275 2275 border-color: @grey6;
2276 2276 }
2277 2277
2278 2278 &:focus {
2279 2279 border-color: @grey5;
2280 2280 }
2281 2281 }
2282 2282 }
2283 2283
2284 2284 .browser-result{
2285 2285 td a{
2286 2286 margin-left: 0.5em;
2287 2287 display: inline-block;
2288 2288
2289 2289 em {
2290 2290 font-weight: @text-bold-weight;
2291 2291 font-family: @text-bold;
2292 2292 }
2293 2293 }
2294 2294 }
2295 2295
2296 2296 .browser-highlight{
2297 2297 background-color: @grey5-alpha;
2298 2298 }
2299 2299
2300 2300
2301 2301 // Search
2302 2302
2303 2303 .search-form{
2304 2304 #q {
2305 2305 width: @search-form-width;
2306 2306 }
2307 2307 .fields{
2308 2308 margin: 0 0 @space;
2309 2309 }
2310 2310
2311 2311 label{
2312 2312 display: inline-block;
2313 2313 margin-right: @textmargin;
2314 2314 padding-top: 0.25em;
2315 2315 }
2316 2316
2317 2317
2318 2318 .results{
2319 2319 clear: both;
2320 2320 margin: 0 0 @padding;
2321 2321 }
2322 2322
2323 2323 .search-tags {
2324 2324 padding: 5px 0;
2325 2325 }
2326 2326 }
2327 2327
2328 2328 div.search-feedback-items {
2329 2329 display: inline-block;
2330 2330 }
2331 2331
2332 2332 div.search-code-body {
2333 2333 background-color: #ffffff; padding: 5px 0 5px 10px;
2334 2334 pre {
2335 2335 .match { background-color: #faffa6;}
2336 2336 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2337 2337 }
2338 2338 }
2339 2339
2340 2340 .expand_commit.search {
2341 2341 .show_more.open {
2342 2342 height: auto;
2343 2343 max-height: none;
2344 2344 }
2345 2345 }
2346 2346
2347 2347 .search-results {
2348 2348
2349 2349 h2 {
2350 2350 margin-bottom: 0;
2351 2351 }
2352 2352 .codeblock {
2353 2353 border: none;
2354 2354 background: transparent;
2355 2355 }
2356 2356
2357 2357 .codeblock-header {
2358 2358 border: none;
2359 2359 background: transparent;
2360 2360 }
2361 2361
2362 2362 .code-body {
2363 border: @border-thickness solid @border-default-color;
2363 border: @border-thickness solid @grey6;
2364 2364 .border-radius(@border-radius);
2365 2365 }
2366 2366
2367 2367 .td-commit {
2368 2368 &:extend(pre);
2369 2369 border-bottom: @border-thickness solid @border-default-color;
2370 2370 }
2371 2371
2372 2372 .message {
2373 2373 height: auto;
2374 2374 max-width: 350px;
2375 2375 white-space: normal;
2376 2376 text-overflow: initial;
2377 2377 overflow: visible;
2378 2378
2379 2379 .match { background-color: #faffa6;}
2380 2380 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2381 2381 }
2382 2382
2383 .path {
2384 border-bottom: none !important;
2385 border-left: 1px solid @grey6 !important;
2386 border-right: 1px solid @grey6 !important;
2387 }
2383 2388 }
2384 2389
2385 2390 table.rctable td.td-search-results div {
2386 2391 max-width: 100%;
2387 2392 }
2388 2393
2389 2394 #tip-box, .tip-box{
2390 2395 padding: @menupadding/2;
2391 2396 display: block;
2392 2397 border: @border-thickness solid @border-highlight-color;
2393 2398 .border-radius(@border-radius);
2394 2399 background-color: white;
2395 2400 z-index: 99;
2396 2401 white-space: pre-wrap;
2397 2402 }
2398 2403
2399 2404 #linktt {
2400 2405 width: 79px;
2401 2406 }
2402 2407
2403 2408 #help_kb .modal-content{
2404 2409 max-width: 750px;
2405 2410 margin: 10% auto;
2406 2411
2407 2412 table{
2408 2413 td,th{
2409 2414 border-bottom: none;
2410 2415 line-height: 2.5em;
2411 2416 }
2412 2417 th{
2413 2418 padding-bottom: @textmargin/2;
2414 2419 }
2415 2420 td.keys{
2416 2421 text-align: center;
2417 2422 }
2418 2423 }
2419 2424
2420 2425 .block-left{
2421 2426 width: 45%;
2422 2427 margin-right: 5%;
2423 2428 }
2424 2429 .modal-footer{
2425 2430 clear: both;
2426 2431 }
2427 2432 .key.tag{
2428 2433 padding: 0.5em;
2429 2434 background-color: @rcblue;
2430 2435 color: white;
2431 2436 border-color: @rcblue;
2432 2437 .box-shadow(none);
2433 2438 }
2434 2439 }
2435 2440
2436 2441
2437 2442
2438 2443 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2439 2444
2440 2445 @import 'statistics-graph';
2441 2446 @import 'tables';
2442 2447 @import 'forms';
2443 2448 @import 'diff';
2444 2449 @import 'summary';
2445 2450 @import 'navigation';
2446 2451
2447 2452 //--- SHOW/HIDE SECTIONS --//
2448 2453
2449 2454 .btn-collapse {
2450 2455 float: right;
2451 2456 text-align: right;
2452 2457 font-family: @text-light;
2453 2458 font-size: @basefontsize;
2454 2459 cursor: pointer;
2455 2460 border: none;
2456 2461 color: @rcblue;
2457 2462 }
2458 2463
2459 2464 table.rctable,
2460 2465 table.dataTable {
2461 2466 .btn-collapse {
2462 2467 float: right;
2463 2468 text-align: right;
2464 2469 }
2465 2470 }
2466 2471
2467 2472 table.rctable {
2468 2473 &.permissions {
2469 2474
2470 2475 th.td-owner {
2471 2476 padding: 0;
2472 2477 }
2473 2478
2474 2479 th {
2475 2480 font-weight: normal;
2476 2481 padding: 0 5px;
2477 2482 }
2478 2483
2479 2484 }
2480 2485 }
2481 2486
2482 2487
2483 2488 // TODO: johbo: Fix for IE10, this avoids that we see a border
2484 2489 // and padding around checkboxes and radio boxes. Move to the right place,
2485 2490 // or better: Remove this once we did the form refactoring.
2486 2491 input[type=checkbox],
2487 2492 input[type=radio] {
2488 2493 padding: 0;
2489 2494 border: none;
2490 2495 }
2491 2496
2492 2497 .toggle-ajax-spinner{
2493 2498 height: 16px;
2494 2499 width: 16px;
2495 2500 }
2496 2501
2497 2502
2498 2503 .markup-form .clearfix {
2499 2504 .border-radius(@border-radius);
2500 2505 margin: 0px;
2501 2506 }
2502 2507
2503 2508 .markup-form-area {
2504 2509 padding: 8px 12px;
2505 2510 border: 1px solid @grey4;
2506 2511 .border-radius(@border-radius);
2507 2512 }
2508 2513
2509 2514 .markup-form-area-header .nav-links {
2510 2515 display: flex;
2511 2516 flex-flow: row wrap;
2512 2517 -webkit-flex-flow: row wrap;
2513 2518 width: 100%;
2514 2519 }
2515 2520
2516 2521 .markup-form-area-footer {
2517 2522 display: flex;
2518 2523 }
2519 2524
2520 2525 .markup-form-area-footer .toolbar {
2521 2526
2522 2527 }
2523 2528
2524 2529 // markup Form
2525 2530 div.markup-form {
2526 2531 margin-top: 20px;
2527 2532 }
2528 2533
2529 2534 .markup-form strong {
2530 2535 display: block;
2531 2536 margin-bottom: 15px;
2532 2537 }
2533 2538
2534 2539 .markup-form textarea {
2535 2540 width: 100%;
2536 2541 height: 100px;
2537 2542 font-family: @text-monospace;
2538 2543 }
2539 2544
2540 2545 form.markup-form {
2541 2546 margin-top: 10px;
2542 2547 margin-left: 10px;
2543 2548 }
2544 2549
2545 2550 .markup-form .comment-block-ta,
2546 2551 .markup-form .preview-box {
2547 2552 .border-radius(@border-radius);
2548 2553 .box-sizing(border-box);
2549 2554 background-color: white;
2550 2555 }
2551 2556
2552 2557 .markup-form .preview-box.unloaded {
2553 2558 height: 50px;
2554 2559 text-align: center;
2555 2560 padding: 20px;
2556 2561 background-color: white;
2557 2562 }
2558 2563
2559 2564
2560 2565 .dropzone-wrapper {
2561 2566 border: 1px solid @grey5;
2562 2567 padding: 20px;
2563 2568 }
2564 2569
2565 2570 .dropzone {
2566 2571 border: 2px dashed @grey5;
2567 2572 border-radius: 5px;
2568 2573 background: white;
2569 2574 min-height: 200px;
2570 2575 padding: 54px;
2571 2576 }
2572 2577 .dropzone .dz-message {
2573 2578 font-weight: 700;
2574 2579 }
2575 2580
2576 2581 .dropzone .dz-message {
2577 2582 text-align: center;
2578 2583 margin: 2em 0;
2579 2584 }
2580 2585
2581 2586 .dz-preview {
2582 2587 margin: 10px 0px !important;
2583 2588 position: relative;
2584 2589 vertical-align: top;
2585 2590 padding: 10px;
2586 2591 }
2587 2592
2588 2593 .dz-filename {
2589 2594 font-weight: 700;
2590 2595 float:left;
2591 2596 }
2592 2597
2593 2598 .dz-response {
2594 2599 clear:both
2595 2600 }
2596 2601
2597 2602 .dz-filename-size {
2598 2603 float:right
2599 2604 }
2600 2605
2601 2606 .dz-error-message {
2602 2607 color: @alert2;
2603 2608 } No newline at end of file
@@ -1,180 +1,165 b''
1 1 <%namespace name="search" file="/search/search.mako"/>
2 2
3 3 <%def name="highlight_text_file(has_matched_content, file_content, lexer, html_formatter, matching_lines, shown_matching_lines, url, use_hl_filter)">
4 4 % if has_matched_content:
5 5 ${h.code_highlight(file_content, lexer, html_formatter, use_hl_filter=use_hl_filter)|n}
6 6 % else:
7 7 ${_('No content matched')} <br/>
8 8 % endif
9 9
10 10 %if len(matching_lines) > shown_matching_lines:
11 11 <a href="${url}">
12 12 ${len(matching_lines) - shown_matching_lines} ${_('more matches in this file')}
13 13 </a>
14 14 %endif
15 15 </%def>
16 16
17 17 <div class="search-results">
18 18 <% query_mark = c.searcher.query_to_mark(c.cur_query, 'content') %>
19 19
20 20 %for entry in c.formatted_results:
21 21
22 22 <%
23 23 file_content = entry['content_highlight'] or entry['content']
24 24 mimetype = entry.get('mimetype')
25 25 filepath = entry.get('path')
26 26 max_lines = h.safe_int(request.GET.get('max_lines', '10'))
27 27 line_context = h.safe_int(request.GET.get('line_contenxt', '3'))
28 28
29 29 match_file_url=h.route_path('repo_files',repo_name=entry['repository'], commit_id=entry.get('commit_id', 'tip'),f_path=entry['f_path'], _query={"mark": query_mark})
30 30 terms = c.cur_query
31 31
32 32 if c.searcher.is_es_6:
33 33 # use empty terms so we default to markers usage
34 34 total_lines, matching_lines = h.get_matching_line_offsets(file_content, terms=None)
35 35 else:
36 36 total_lines, matching_lines = h.get_matching_line_offsets(file_content, terms)
37 37
38 38 shown_matching_lines = 0
39 39 lines_of_interest = set()
40 40 for line_number in matching_lines:
41 41 if len(lines_of_interest) < max_lines:
42 42 lines_of_interest |= set(range(
43 43 max(line_number - line_context, 0),
44 44 min(line_number + line_context, total_lines + 1)))
45 45 shown_matching_lines += 1
46 46 lexer = h.get_lexer_safe(mimetype=mimetype, filepath=filepath)
47 47
48 48 html_formatter = h.SearchContentCodeHtmlFormatter(
49 49 linenos=True,
50 50 cssclass="code-highlight",
51 51 url=match_file_url,
52 52 query_terms=terms,
53 53 only_line_numbers=lines_of_interest
54 54 )
55 55
56 56 has_matched_content = len(lines_of_interest) >= 1
57 57
58 58 %>
59 59 ## search results are additionally filtered, and this check is just a safe gate
60 60 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results content check'):
61 <div id="codeblock" class="codeblock">
62 <div class="codeblock-header">
63 <h1>
64 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
65 ${search.repo_icon(repo_type)}
66 ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
67 </h1>
68 ## level 1
69 <div class="file-container">
61 <div class="codeblock">
62 <h1>
63 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
64 ${search.repo_icon(repo_type)}
65 ${h.link_to(entry['repository'], h.route_path('repo_summary', repo_name=entry['repository']))}
66 </h1>
70 67
71 <div class="pull-left">
72 <span class="stats-filename">
73 <strong>
74 <i class="icon-file-text"></i>
75 ${h.link_to(h.literal(entry['f_path']), h.route_path('repo_files',repo_name=entry['repository'],commit_id=entry.get('commit_id', 'tip'),f_path=entry['f_path']))}
76 </strong>
77 </span>
78 <span class="item last">
79 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${entry['f_path']}" title="${_('Copy the full path')}"></i>
80 </span>
68 <div class="codeblock-header">
69
70 <div class="file-filename">
71 <i class="icon-file"></i> ${entry['f_path'].split('/')[-1]}
81 72 </div>
82 73
83 <div class="pull-right">
84 <div class="buttons">
85 <a id="file_history_overview_full" href="${h.route_path('repo_commits_file',repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}">
86 ${_('Show Full History')}
87 </a>
88 | ${h.link_to(_('Annotation'), h.route_path('repo_files:annotated', repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
89 | ${h.link_to(_('Raw'), h.route_path('repo_file_raw', repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
90 | ${h.link_to(_('Download'), h.route_path('repo_file_download',repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
91 </div>
74 <div class="file-stats">
75 <div class="stats-info">
76 <span class="stats-first-item">
77 ${entry.get('lines', 0.)} ${_ungettext('line', 'lines', entry.get('lines', 0.))}
78 (${len(matching_lines)} ${_ungettext('matched', 'matched', len(matching_lines))})
79 </span>
80 <span>
81 % if entry.get('size'):
82 | ${h.format_byte_size_binary(entry['size'])}
83 % endif
84 </span>
85 <span>
86 % if entry.get('mimetype'):
87 | ${entry.get('mimetype', "unknown mimetype")}
88 % endif
89 </span>
90 </div>
91 </div>
92 </div>
93
94 <div class="path clear-fix">
95 <div class="pull-left">
96 ${h.files_breadcrumbs(entry['repository'],entry.get('commit_id', 'tip'),entry['f_path'], linkify_last_item=True)}
92 97 </div>
93 98
94 </div>
95 ## level 2
96 <div class="file-container">
97
98 <div class="pull-left">
99 <span class="stats-first-item">
100 %if entry.get('lines'):
101 ${entry.get('lines', 0.)} ${_ungettext('line', 'lines', entry.get('lines', 0.))}
102 (${len(matching_lines)} ${_ungettext('matched', 'matched', len(matching_lines))})
103 %endif
104 </span>
105
106 <span>
107 %if entry.get('size'):
108 | ${h.format_byte_size_binary(entry['size'])}
109 %endif
110 </span>
111
112 <span>
113 %if entry.get('mimetype'):
114 | ${entry.get('mimetype', "unknown mimetype")}
115 %endif
116 </span>
117 </div>
118
119 <div class="pull-right">
99 <div class="pull-right stats">
100 ## <a id="file_history_overview_full" href="${h.route_path('repo_commits_file',repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}">
101 ## ${_('Show Full History')}
102 ## </a>
103 ## | ${h.link_to(_('Annotation'), h.route_path('repo_files:annotated', repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
104 ## | ${h.link_to(_('Raw'), h.route_path('repo_file_raw', repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
120 105 <div class="search-tags">
121 106
122 107 <% repo_group = entry.get('repository_group')%>
123 108 ## hiden if in repo group view
124 109 % if repo_group and not c.repo_group_name:
125 110 <span class="tag tag8">
126 111 ${search.repo_group_icon()}
127 112 <a href="${h.route_path('search_repo_group', repo_group_name=repo_group, _query={'q': c.cur_query})}">${_('Narrow to this repository group')}</a>
128 113 </span>
129 114 % endif
130 115 ## hiden if in repo view
131 116 % if not c.repo_name:
132 117 <span class="tag tag8">
133 118 ${search.repo_icon(repo_type)}
134 119 <a href="${h.route_path('search_repo', repo_name=entry.get('repo_name'), _query={'q': c.cur_query})}">${_('Narrow to this repository')}</a>
135 120 </span>
136 121 % endif
137 122 </div>
138 </div>
139
140 </div>
141 123
124 </div>
125 <div class="clear-fix"></div>
142 126 </div>
143 <div class="code-body search-code-body">
127
144 128
129 <div class="code-body search-code-body clear-fix">
145 130 ${highlight_text_file(
146 131 has_matched_content=has_matched_content,
147 132 file_content=file_content,
148 133 lexer=lexer,
149 134 html_formatter=html_formatter,
150 135 matching_lines=matching_lines,
151 136 shown_matching_lines=shown_matching_lines,
152 137 url=match_file_url,
153 138 use_hl_filter=c.searcher.is_es_6
154 139 )}
155 140 </div>
156 141
157 142 </div>
158 143 % endif
159 144 %endfor
160 145 </div>
161 146 %if c.cur_query and c.formatted_results:
162 147 <div class="pagination-wh pagination-left" >
163 148 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
164 149 </div>
165 150 %endif
166 151
167 152 %if c.cur_query:
168 153 <script type="text/javascript">
169 154 $(function(){
170 155 $(".search-code-body").mark(
171 156 "${query_mark}",
172 157 {
173 158 "className": 'match',
174 159 "accuracy": "complementary",
175 160 "ignorePunctuation": ":._(){}[]!'+=".split("")
176 161 }
177 162 );
178 163 })
179 164 </script>
180 165 %endif
General Comments 0
You need to be logged in to leave comments. Login now