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