##// END OF EJS Templates
routing: remove usage of url.current from pylons.
marcink -
r2104:d5420119 default
parent child Browse files
Show More
@@ -1,2102 +1,2093 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 random
29 29 import hashlib
30 30 import StringIO
31 31 import urllib
32 32 import math
33 33 import logging
34 34 import re
35 35 import urlparse
36 36 import time
37 37 import string
38 38 import hashlib
39 39 from collections import OrderedDict
40 40
41 41 import pygments
42 42 import itertools
43 43 import fnmatch
44 44
45 45 from datetime import datetime
46 46 from functools import partial
47 47 from pygments.formatters.html import HtmlFormatter
48 48 from pygments import highlight as code_highlight
49 49 from pygments.lexers import (
50 50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
51 51
52 52 from pyramid.threadlocal import get_current_request
53 53
54 54 from webhelpers.html import literal, HTML, escape
55 55 from webhelpers.html.tools import *
56 56 from webhelpers.html.builder import make_tag
57 57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
58 58 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
59 59 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
60 60 submit, text, password, textarea, title, ul, xml_declaration, radio
61 61 from webhelpers.html.tools import auto_link, button_to, highlight, \
62 62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
63 63 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
64 64 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
65 65 replace_whitespace, urlify, truncate, wrap_paragraphs
66 66 from webhelpers.date import time_ago_in_words
67 67 from webhelpers.paginate import Page as _Page
68 68 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
69 69 convert_boolean_attrs, NotGiven, _make_safe_id_component
70 70 from webhelpers2.number import format_byte_size
71 71
72 72 from rhodecode.lib.action_parser import action_parser
73 73 from rhodecode.lib.ext_json import json
74 74 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
75 75 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
76 76 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
77 77 AttributeDict, safe_int, md5, md5_safe
78 78 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
79 79 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
80 80 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
81 81 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
82 82 from rhodecode.model.changeset_status import ChangesetStatusModel
83 83 from rhodecode.model.db import Permission, User, Repository
84 84 from rhodecode.model.repo_group import RepoGroupModel
85 85 from rhodecode.model.settings import IssueTrackerSettingsModel
86 86
87 87 log = logging.getLogger(__name__)
88 88
89 89
90 90 DEFAULT_USER = User.DEFAULT_USER
91 91 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
92 92
93 93
94 94 def url(*args, **kw):
95 95 from pylons import url as pylons_url
96 96 return pylons_url(*args, **kw)
97 97
98 98
99 def pylons_url_current(*args, **kw):
100 """
101 This function overrides pylons.url.current() which returns the current
102 path so that it will also work from a pyramid only context. This
103 should be removed once port to pyramid is complete.
104 """
105 from pylons import url as pylons_url
106 if not args and not kw:
107 request = get_current_request()
108 return request.path
109 return pylons_url.current(*args, **kw)
110
111 url.current = pylons_url_current
112
113
114 99 def url_replace(**qargs):
115 100 """ Returns the current request url while replacing query string args """
116 101
117 102 request = get_current_request()
118 103 new_args = request.GET.mixed()
119 104 new_args.update(qargs)
120 105 return url('', **new_args)
121 106
122 107
123 108 def asset(path, ver=None, **kwargs):
124 109 """
125 110 Helper to generate a static asset file path for rhodecode assets
126 111
127 112 eg. h.asset('images/image.png', ver='3923')
128 113
129 114 :param path: path of asset
130 115 :param ver: optional version query param to append as ?ver=
131 116 """
132 117 request = get_current_request()
133 118 query = {}
134 119 query.update(kwargs)
135 120 if ver:
136 121 query = {'ver': ver}
137 122 return request.static_path(
138 123 'rhodecode:public/{}'.format(path), _query=query)
139 124
140 125
141 126 default_html_escape_table = {
142 127 ord('&'): u'&amp;',
143 128 ord('<'): u'&lt;',
144 129 ord('>'): u'&gt;',
145 130 ord('"'): u'&quot;',
146 131 ord("'"): u'&#39;',
147 132 }
148 133
149 134
150 135 def html_escape(text, html_escape_table=default_html_escape_table):
151 136 """Produce entities within text."""
152 137 return text.translate(html_escape_table)
153 138
154 139
155 140 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
156 141 """
157 142 Truncate string ``s`` at the first occurrence of ``sub``.
158 143
159 144 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
160 145 """
161 146 suffix_if_chopped = suffix_if_chopped or ''
162 147 pos = s.find(sub)
163 148 if pos == -1:
164 149 return s
165 150
166 151 if inclusive:
167 152 pos += len(sub)
168 153
169 154 chopped = s[:pos]
170 155 left = s[pos:].strip()
171 156
172 157 if left and suffix_if_chopped:
173 158 chopped += suffix_if_chopped
174 159
175 160 return chopped
176 161
177 162
178 163 def shorter(text, size=20):
179 164 postfix = '...'
180 165 if len(text) > size:
181 166 return text[:size - len(postfix)] + postfix
182 167 return text
183 168
184 169
185 170 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
186 171 """
187 172 Reset button
188 173 """
189 174 _set_input_attrs(attrs, type, name, value)
190 175 _set_id_attr(attrs, id, name)
191 176 convert_boolean_attrs(attrs, ["disabled"])
192 177 return HTML.input(**attrs)
193 178
194 179 reset = _reset
195 180 safeid = _make_safe_id_component
196 181
197 182
198 183 def branding(name, length=40):
199 184 return truncate(name, length, indicator="")
200 185
201 186
202 187 def FID(raw_id, path):
203 188 """
204 189 Creates a unique ID for filenode based on it's hash of path and commit
205 190 it's safe to use in urls
206 191
207 192 :param raw_id:
208 193 :param path:
209 194 """
210 195
211 196 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
212 197
213 198
214 199 class _GetError(object):
215 200 """Get error from form_errors, and represent it as span wrapped error
216 201 message
217 202
218 203 :param field_name: field to fetch errors for
219 204 :param form_errors: form errors dict
220 205 """
221 206
222 207 def __call__(self, field_name, form_errors):
223 208 tmpl = """<span class="error_msg">%s</span>"""
224 209 if form_errors and field_name in form_errors:
225 210 return literal(tmpl % form_errors.get(field_name))
226 211
227 212 get_error = _GetError()
228 213
229 214
230 215 class _ToolTip(object):
231 216
232 217 def __call__(self, tooltip_title, trim_at=50):
233 218 """
234 219 Special function just to wrap our text into nice formatted
235 220 autowrapped text
236 221
237 222 :param tooltip_title:
238 223 """
239 224 tooltip_title = escape(tooltip_title)
240 225 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
241 226 return tooltip_title
242 227 tooltip = _ToolTip()
243 228
244 229
245 230 def files_breadcrumbs(repo_name, commit_id, file_path):
246 231 if isinstance(file_path, str):
247 232 file_path = safe_unicode(file_path)
248 233
249 234 # TODO: johbo: Is this always a url like path, or is this operating
250 235 # system dependent?
251 236 path_segments = file_path.split('/')
252 237
253 238 repo_name_html = escape(repo_name)
254 239 if len(path_segments) == 1 and path_segments[0] == '':
255 240 url_segments = [repo_name_html]
256 241 else:
257 242 url_segments = [
258 243 link_to(
259 244 repo_name_html,
260 245 route_path(
261 246 'repo_files',
262 247 repo_name=repo_name,
263 248 commit_id=commit_id,
264 249 f_path=''),
265 250 class_='pjax-link')]
266 251
267 252 last_cnt = len(path_segments) - 1
268 253 for cnt, segment in enumerate(path_segments):
269 254 if not segment:
270 255 continue
271 256 segment_html = escape(segment)
272 257
273 258 if cnt != last_cnt:
274 259 url_segments.append(
275 260 link_to(
276 261 segment_html,
277 262 route_path(
278 263 'repo_files',
279 264 repo_name=repo_name,
280 265 commit_id=commit_id,
281 266 f_path='/'.join(path_segments[:cnt + 1])),
282 267 class_='pjax-link'))
283 268 else:
284 269 url_segments.append(segment_html)
285 270
286 271 return literal('/'.join(url_segments))
287 272
288 273
289 274 class CodeHtmlFormatter(HtmlFormatter):
290 275 """
291 276 My code Html Formatter for source codes
292 277 """
293 278
294 279 def wrap(self, source, outfile):
295 280 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
296 281
297 282 def _wrap_code(self, source):
298 283 for cnt, it in enumerate(source):
299 284 i, t = it
300 285 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
301 286 yield i, t
302 287
303 288 def _wrap_tablelinenos(self, inner):
304 289 dummyoutfile = StringIO.StringIO()
305 290 lncount = 0
306 291 for t, line in inner:
307 292 if t:
308 293 lncount += 1
309 294 dummyoutfile.write(line)
310 295
311 296 fl = self.linenostart
312 297 mw = len(str(lncount + fl - 1))
313 298 sp = self.linenospecial
314 299 st = self.linenostep
315 300 la = self.lineanchors
316 301 aln = self.anchorlinenos
317 302 nocls = self.noclasses
318 303 if sp:
319 304 lines = []
320 305
321 306 for i in range(fl, fl + lncount):
322 307 if i % st == 0:
323 308 if i % sp == 0:
324 309 if aln:
325 310 lines.append('<a href="#%s%d" class="special">%*d</a>' %
326 311 (la, i, mw, i))
327 312 else:
328 313 lines.append('<span class="special">%*d</span>' % (mw, i))
329 314 else:
330 315 if aln:
331 316 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
332 317 else:
333 318 lines.append('%*d' % (mw, i))
334 319 else:
335 320 lines.append('')
336 321 ls = '\n'.join(lines)
337 322 else:
338 323 lines = []
339 324 for i in range(fl, fl + lncount):
340 325 if i % st == 0:
341 326 if aln:
342 327 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
343 328 else:
344 329 lines.append('%*d' % (mw, i))
345 330 else:
346 331 lines.append('')
347 332 ls = '\n'.join(lines)
348 333
349 334 # in case you wonder about the seemingly redundant <div> here: since the
350 335 # content in the other cell also is wrapped in a div, some browsers in
351 336 # some configurations seem to mess up the formatting...
352 337 if nocls:
353 338 yield 0, ('<table class="%stable">' % self.cssclass +
354 339 '<tr><td><div class="linenodiv" '
355 340 'style="background-color: #f0f0f0; padding-right: 10px">'
356 341 '<pre style="line-height: 125%">' +
357 342 ls + '</pre></div></td><td id="hlcode" class="code">')
358 343 else:
359 344 yield 0, ('<table class="%stable">' % self.cssclass +
360 345 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
361 346 ls + '</pre></div></td><td id="hlcode" class="code">')
362 347 yield 0, dummyoutfile.getvalue()
363 348 yield 0, '</td></tr></table>'
364 349
365 350
366 351 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
367 352 def __init__(self, **kw):
368 353 # only show these line numbers if set
369 354 self.only_lines = kw.pop('only_line_numbers', [])
370 355 self.query_terms = kw.pop('query_terms', [])
371 356 self.max_lines = kw.pop('max_lines', 5)
372 357 self.line_context = kw.pop('line_context', 3)
373 358 self.url = kw.pop('url', None)
374 359
375 360 super(CodeHtmlFormatter, self).__init__(**kw)
376 361
377 362 def _wrap_code(self, source):
378 363 for cnt, it in enumerate(source):
379 364 i, t = it
380 365 t = '<pre>%s</pre>' % t
381 366 yield i, t
382 367
383 368 def _wrap_tablelinenos(self, inner):
384 369 yield 0, '<table class="code-highlight %stable">' % self.cssclass
385 370
386 371 last_shown_line_number = 0
387 372 current_line_number = 1
388 373
389 374 for t, line in inner:
390 375 if not t:
391 376 yield t, line
392 377 continue
393 378
394 379 if current_line_number in self.only_lines:
395 380 if last_shown_line_number + 1 != current_line_number:
396 381 yield 0, '<tr>'
397 382 yield 0, '<td class="line">...</td>'
398 383 yield 0, '<td id="hlcode" class="code"></td>'
399 384 yield 0, '</tr>'
400 385
401 386 yield 0, '<tr>'
402 387 if self.url:
403 388 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
404 389 self.url, current_line_number, current_line_number)
405 390 else:
406 391 yield 0, '<td class="line"><a href="">%i</a></td>' % (
407 392 current_line_number)
408 393 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
409 394 yield 0, '</tr>'
410 395
411 396 last_shown_line_number = current_line_number
412 397
413 398 current_line_number += 1
414 399
415 400
416 401 yield 0, '</table>'
417 402
418 403
419 404 def extract_phrases(text_query):
420 405 """
421 406 Extracts phrases from search term string making sure phrases
422 407 contained in double quotes are kept together - and discarding empty values
423 408 or fully whitespace values eg.
424 409
425 410 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
426 411
427 412 """
428 413
429 414 in_phrase = False
430 415 buf = ''
431 416 phrases = []
432 417 for char in text_query:
433 418 if in_phrase:
434 419 if char == '"': # end phrase
435 420 phrases.append(buf)
436 421 buf = ''
437 422 in_phrase = False
438 423 continue
439 424 else:
440 425 buf += char
441 426 continue
442 427 else:
443 428 if char == '"': # start phrase
444 429 in_phrase = True
445 430 phrases.append(buf)
446 431 buf = ''
447 432 continue
448 433 elif char == ' ':
449 434 phrases.append(buf)
450 435 buf = ''
451 436 continue
452 437 else:
453 438 buf += char
454 439
455 440 phrases.append(buf)
456 441 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
457 442 return phrases
458 443
459 444
460 445 def get_matching_offsets(text, phrases):
461 446 """
462 447 Returns a list of string offsets in `text` that the list of `terms` match
463 448
464 449 >>> get_matching_offsets('some text here', ['some', 'here'])
465 450 [(0, 4), (10, 14)]
466 451
467 452 """
468 453 offsets = []
469 454 for phrase in phrases:
470 455 for match in re.finditer(phrase, text):
471 456 offsets.append((match.start(), match.end()))
472 457
473 458 return offsets
474 459
475 460
476 461 def normalize_text_for_matching(x):
477 462 """
478 463 Replaces all non alnum characters to spaces and lower cases the string,
479 464 useful for comparing two text strings without punctuation
480 465 """
481 466 return re.sub(r'[^\w]', ' ', x.lower())
482 467
483 468
484 469 def get_matching_line_offsets(lines, terms):
485 470 """ Return a set of `lines` indices (starting from 1) matching a
486 471 text search query, along with `context` lines above/below matching lines
487 472
488 473 :param lines: list of strings representing lines
489 474 :param terms: search term string to match in lines eg. 'some text'
490 475 :param context: number of lines above/below a matching line to add to result
491 476 :param max_lines: cut off for lines of interest
492 477 eg.
493 478
494 479 text = '''
495 480 words words words
496 481 words words words
497 482 some text some
498 483 words words words
499 484 words words words
500 485 text here what
501 486 '''
502 487 get_matching_line_offsets(text, 'text', context=1)
503 488 {3: [(5, 9)], 6: [(0, 4)]]
504 489
505 490 """
506 491 matching_lines = {}
507 492 phrases = [normalize_text_for_matching(phrase)
508 493 for phrase in extract_phrases(terms)]
509 494
510 495 for line_index, line in enumerate(lines, start=1):
511 496 match_offsets = get_matching_offsets(
512 497 normalize_text_for_matching(line), phrases)
513 498 if match_offsets:
514 499 matching_lines[line_index] = match_offsets
515 500
516 501 return matching_lines
517 502
518 503
519 504 def hsv_to_rgb(h, s, v):
520 505 """ Convert hsv color values to rgb """
521 506
522 507 if s == 0.0:
523 508 return v, v, v
524 509 i = int(h * 6.0) # XXX assume int() truncates!
525 510 f = (h * 6.0) - i
526 511 p = v * (1.0 - s)
527 512 q = v * (1.0 - s * f)
528 513 t = v * (1.0 - s * (1.0 - f))
529 514 i = i % 6
530 515 if i == 0:
531 516 return v, t, p
532 517 if i == 1:
533 518 return q, v, p
534 519 if i == 2:
535 520 return p, v, t
536 521 if i == 3:
537 522 return p, q, v
538 523 if i == 4:
539 524 return t, p, v
540 525 if i == 5:
541 526 return v, p, q
542 527
543 528
544 529 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
545 530 """
546 531 Generator for getting n of evenly distributed colors using
547 532 hsv color and golden ratio. It always return same order of colors
548 533
549 534 :param n: number of colors to generate
550 535 :param saturation: saturation of returned colors
551 536 :param lightness: lightness of returned colors
552 537 :returns: RGB tuple
553 538 """
554 539
555 540 golden_ratio = 0.618033988749895
556 541 h = 0.22717784590367374
557 542
558 543 for _ in xrange(n):
559 544 h += golden_ratio
560 545 h %= 1
561 546 HSV_tuple = [h, saturation, lightness]
562 547 RGB_tuple = hsv_to_rgb(*HSV_tuple)
563 548 yield map(lambda x: str(int(x * 256)), RGB_tuple)
564 549
565 550
566 551 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
567 552 """
568 553 Returns a function which when called with an argument returns a unique
569 554 color for that argument, eg.
570 555
571 556 :param n: number of colors to generate
572 557 :param saturation: saturation of returned colors
573 558 :param lightness: lightness of returned colors
574 559 :returns: css RGB string
575 560
576 561 >>> color_hash = color_hasher()
577 562 >>> color_hash('hello')
578 563 'rgb(34, 12, 59)'
579 564 >>> color_hash('hello')
580 565 'rgb(34, 12, 59)'
581 566 >>> color_hash('other')
582 567 'rgb(90, 224, 159)'
583 568 """
584 569
585 570 color_dict = {}
586 571 cgenerator = unique_color_generator(
587 572 saturation=saturation, lightness=lightness)
588 573
589 574 def get_color_string(thing):
590 575 if thing in color_dict:
591 576 col = color_dict[thing]
592 577 else:
593 578 col = color_dict[thing] = cgenerator.next()
594 579 return "rgb(%s)" % (', '.join(col))
595 580
596 581 return get_color_string
597 582
598 583
599 584 def get_lexer_safe(mimetype=None, filepath=None):
600 585 """
601 586 Tries to return a relevant pygments lexer using mimetype/filepath name,
602 587 defaulting to plain text if none could be found
603 588 """
604 589 lexer = None
605 590 try:
606 591 if mimetype:
607 592 lexer = get_lexer_for_mimetype(mimetype)
608 593 if not lexer:
609 594 lexer = get_lexer_for_filename(filepath)
610 595 except pygments.util.ClassNotFound:
611 596 pass
612 597
613 598 if not lexer:
614 599 lexer = get_lexer_by_name('text')
615 600
616 601 return lexer
617 602
618 603
619 604 def get_lexer_for_filenode(filenode):
620 605 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
621 606 return lexer
622 607
623 608
624 609 def pygmentize(filenode, **kwargs):
625 610 """
626 611 pygmentize function using pygments
627 612
628 613 :param filenode:
629 614 """
630 615 lexer = get_lexer_for_filenode(filenode)
631 616 return literal(code_highlight(filenode.content, lexer,
632 617 CodeHtmlFormatter(**kwargs)))
633 618
634 619
635 620 def is_following_repo(repo_name, user_id):
636 621 from rhodecode.model.scm import ScmModel
637 622 return ScmModel().is_following_repo(repo_name, user_id)
638 623
639 624
640 625 class _Message(object):
641 626 """A message returned by ``Flash.pop_messages()``.
642 627
643 628 Converting the message to a string returns the message text. Instances
644 629 also have the following attributes:
645 630
646 631 * ``message``: the message text.
647 632 * ``category``: the category specified when the message was created.
648 633 """
649 634
650 635 def __init__(self, category, message):
651 636 self.category = category
652 637 self.message = message
653 638
654 639 def __str__(self):
655 640 return self.message
656 641
657 642 __unicode__ = __str__
658 643
659 644 def __html__(self):
660 645 return escape(safe_unicode(self.message))
661 646
662 647
663 648 class Flash(object):
664 649 # List of allowed categories. If None, allow any category.
665 650 categories = ["warning", "notice", "error", "success"]
666 651
667 652 # Default category if none is specified.
668 653 default_category = "notice"
669 654
670 655 def __init__(self, session_key="flash", categories=None,
671 656 default_category=None):
672 657 """
673 658 Instantiate a ``Flash`` object.
674 659
675 660 ``session_key`` is the key to save the messages under in the user's
676 661 session.
677 662
678 663 ``categories`` is an optional list which overrides the default list
679 664 of categories.
680 665
681 666 ``default_category`` overrides the default category used for messages
682 667 when none is specified.
683 668 """
684 669 self.session_key = session_key
685 670 if categories is not None:
686 671 self.categories = categories
687 672 if default_category is not None:
688 673 self.default_category = default_category
689 674 if self.categories and self.default_category not in self.categories:
690 675 raise ValueError(
691 676 "unrecognized default category %r" % (self.default_category,))
692 677
693 678 def pop_messages(self, session=None, request=None):
694 679 """
695 680 Return all accumulated messages and delete them from the session.
696 681
697 682 The return value is a list of ``Message`` objects.
698 683 """
699 684 messages = []
700 685
701 686 if not session:
702 687 if not request:
703 688 request = get_current_request()
704 689 session = request.session
705 690
706 691 # Pop the 'old' pylons flash messages. They are tuples of the form
707 692 # (category, message)
708 693 for cat, msg in session.pop(self.session_key, []):
709 694 messages.append(_Message(cat, msg))
710 695
711 696 # Pop the 'new' pyramid flash messages for each category as list
712 697 # of strings.
713 698 for cat in self.categories:
714 699 for msg in session.pop_flash(queue=cat):
715 700 messages.append(_Message(cat, msg))
716 701 # Map messages from the default queue to the 'notice' category.
717 702 for msg in session.pop_flash():
718 703 messages.append(_Message('notice', msg))
719 704
720 705 session.save()
721 706 return messages
722 707
723 708 def json_alerts(self, session=None, request=None):
724 709 payloads = []
725 710 messages = flash.pop_messages(session=session, request=request)
726 711 if messages:
727 712 for message in messages:
728 713 subdata = {}
729 714 if hasattr(message.message, 'rsplit'):
730 715 flash_data = message.message.rsplit('|DELIM|', 1)
731 716 org_message = flash_data[0]
732 717 if len(flash_data) > 1:
733 718 subdata = json.loads(flash_data[1])
734 719 else:
735 720 org_message = message.message
736 721 payloads.append({
737 722 'message': {
738 723 'message': u'{}'.format(org_message),
739 724 'level': message.category,
740 725 'force': True,
741 726 'subdata': subdata
742 727 }
743 728 })
744 729 return json.dumps(payloads)
745 730
746 731 def __call__(self, message, category=None, ignore_duplicate=False,
747 732 session=None, request=None):
748 733
749 734 if not session:
750 735 if not request:
751 736 request = get_current_request()
752 737 session = request.session
753 738
754 739 session.flash(
755 740 message, queue=category, allow_duplicate=not ignore_duplicate)
756 741
757 742
758 743 flash = Flash()
759 744
760 745 #==============================================================================
761 746 # SCM FILTERS available via h.
762 747 #==============================================================================
763 748 from rhodecode.lib.vcs.utils import author_name, author_email
764 749 from rhodecode.lib.utils2 import credentials_filter, age as _age
765 750 from rhodecode.model.db import User, ChangesetStatus
766 751
767 752 age = _age
768 753 capitalize = lambda x: x.capitalize()
769 754 email = author_email
770 755 short_id = lambda x: x[:12]
771 756 hide_credentials = lambda x: ''.join(credentials_filter(x))
772 757
773 758
774 759 def age_component(datetime_iso, value=None, time_is_local=False):
775 760 title = value or format_date(datetime_iso)
776 761 tzinfo = '+00:00'
777 762
778 763 # detect if we have a timezone info, otherwise, add it
779 764 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
780 765 if time_is_local:
781 766 tzinfo = time.strftime("+%H:%M",
782 767 time.gmtime(
783 768 (datetime.now() - datetime.utcnow()).seconds + 1
784 769 )
785 770 )
786 771
787 772 return literal(
788 773 '<time class="timeago tooltip" '
789 774 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
790 775 datetime_iso, title, tzinfo))
791 776
792 777
793 778 def _shorten_commit_id(commit_id):
794 779 from rhodecode import CONFIG
795 780 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
796 781 return commit_id[:def_len]
797 782
798 783
799 784 def show_id(commit):
800 785 """
801 786 Configurable function that shows ID
802 787 by default it's r123:fffeeefffeee
803 788
804 789 :param commit: commit instance
805 790 """
806 791 from rhodecode import CONFIG
807 792 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
808 793
809 794 raw_id = _shorten_commit_id(commit.raw_id)
810 795 if show_idx:
811 796 return 'r%s:%s' % (commit.idx, raw_id)
812 797 else:
813 798 return '%s' % (raw_id, )
814 799
815 800
816 801 def format_date(date):
817 802 """
818 803 use a standardized formatting for dates used in RhodeCode
819 804
820 805 :param date: date/datetime object
821 806 :return: formatted date
822 807 """
823 808
824 809 if date:
825 810 _fmt = "%a, %d %b %Y %H:%M:%S"
826 811 return safe_unicode(date.strftime(_fmt))
827 812
828 813 return u""
829 814
830 815
831 816 class _RepoChecker(object):
832 817
833 818 def __init__(self, backend_alias):
834 819 self._backend_alias = backend_alias
835 820
836 821 def __call__(self, repository):
837 822 if hasattr(repository, 'alias'):
838 823 _type = repository.alias
839 824 elif hasattr(repository, 'repo_type'):
840 825 _type = repository.repo_type
841 826 else:
842 827 _type = repository
843 828 return _type == self._backend_alias
844 829
845 830 is_git = _RepoChecker('git')
846 831 is_hg = _RepoChecker('hg')
847 832 is_svn = _RepoChecker('svn')
848 833
849 834
850 835 def get_repo_type_by_name(repo_name):
851 836 repo = Repository.get_by_repo_name(repo_name)
852 837 return repo.repo_type
853 838
854 839
855 840 def is_svn_without_proxy(repository):
856 841 if is_svn(repository):
857 842 from rhodecode.model.settings import VcsSettingsModel
858 843 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
859 844 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
860 845 return False
861 846
862 847
863 848 def discover_user(author):
864 849 """
865 850 Tries to discover RhodeCode User based on the autho string. Author string
866 851 is typically `FirstName LastName <email@address.com>`
867 852 """
868 853
869 854 # if author is already an instance use it for extraction
870 855 if isinstance(author, User):
871 856 return author
872 857
873 858 # Valid email in the attribute passed, see if they're in the system
874 859 _email = author_email(author)
875 860 if _email != '':
876 861 user = User.get_by_email(_email, case_insensitive=True, cache=True)
877 862 if user is not None:
878 863 return user
879 864
880 865 # Maybe it's a username, we try to extract it and fetch by username ?
881 866 _author = author_name(author)
882 867 user = User.get_by_username(_author, case_insensitive=True, cache=True)
883 868 if user is not None:
884 869 return user
885 870
886 871 return None
887 872
888 873
889 874 def email_or_none(author):
890 875 # extract email from the commit string
891 876 _email = author_email(author)
892 877
893 878 # If we have an email, use it, otherwise
894 879 # see if it contains a username we can get an email from
895 880 if _email != '':
896 881 return _email
897 882 else:
898 883 user = User.get_by_username(
899 884 author_name(author), case_insensitive=True, cache=True)
900 885
901 886 if user is not None:
902 887 return user.email
903 888
904 889 # No valid email, not a valid user in the system, none!
905 890 return None
906 891
907 892
908 893 def link_to_user(author, length=0, **kwargs):
909 894 user = discover_user(author)
910 895 # user can be None, but if we have it already it means we can re-use it
911 896 # in the person() function, so we save 1 intensive-query
912 897 if user:
913 898 author = user
914 899
915 900 display_person = person(author, 'username_or_name_or_email')
916 901 if length:
917 902 display_person = shorter(display_person, length)
918 903
919 904 if user:
920 905 return link_to(
921 906 escape(display_person),
922 907 route_path('user_profile', username=user.username),
923 908 **kwargs)
924 909 else:
925 910 return escape(display_person)
926 911
927 912
928 913 def person(author, show_attr="username_and_name"):
929 914 user = discover_user(author)
930 915 if user:
931 916 return getattr(user, show_attr)
932 917 else:
933 918 _author = author_name(author)
934 919 _email = email(author)
935 920 return _author or _email
936 921
937 922
938 923 def author_string(email):
939 924 if email:
940 925 user = User.get_by_email(email, case_insensitive=True, cache=True)
941 926 if user:
942 927 if user.first_name or user.last_name:
943 928 return '%s %s &lt;%s&gt;' % (
944 929 user.first_name, user.last_name, email)
945 930 else:
946 931 return email
947 932 else:
948 933 return email
949 934 else:
950 935 return None
951 936
952 937
953 938 def person_by_id(id_, show_attr="username_and_name"):
954 939 # attr to return from fetched user
955 940 person_getter = lambda usr: getattr(usr, show_attr)
956 941
957 942 #maybe it's an ID ?
958 943 if str(id_).isdigit() or isinstance(id_, int):
959 944 id_ = int(id_)
960 945 user = User.get(id_)
961 946 if user is not None:
962 947 return person_getter(user)
963 948 return id_
964 949
965 950
966 951 def gravatar_with_user(request, author, show_disabled=False):
967 952 _render = request.get_partial_renderer('base/base.mako')
968 953 return _render('gravatar_with_user', author, show_disabled=show_disabled)
969 954
970 955
971 956 tags_paterns = OrderedDict((
972 957 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
973 958 '<div class="metatag" tag="lang">\\2</div>')),
974 959
975 960 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
976 961 '<div class="metatag" tag="see">see: \\1 </div>')),
977 962
978 963 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((.*?)\)\]'),
979 964 '<div class="metatag" tag="url"> <a href="\\2">\\1</a> </div>')),
980 965
981 966 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
982 967 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
983 968
984 969 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
985 970 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
986 971
987 972 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
988 973 '<div class="metatag" tag="state \\1">\\1</div>')),
989 974
990 975 # label in grey
991 976 ('label', (re.compile(r'\[([a-z]+)\]'),
992 977 '<div class="metatag" tag="label">\\1</div>')),
993 978
994 979 # generic catch all in grey
995 980 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
996 981 '<div class="metatag" tag="generic">\\1</div>')),
997 982 ))
998 983
999 984
1000 985 def extract_metatags(value):
1001 986 """
1002 987 Extract supported meta-tags from given text value
1003 988 """
1004 989 if not value:
1005 990 return ''
1006 991
1007 992 tags = []
1008 993 for key, val in tags_paterns.items():
1009 994 pat, replace_html = val
1010 995 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1011 996 value = pat.sub('', value)
1012 997
1013 998 return tags, value
1014 999
1015 1000
1016 1001 def style_metatag(tag_type, value):
1017 1002 """
1018 1003 converts tags from value into html equivalent
1019 1004 """
1020 1005 if not value:
1021 1006 return ''
1022 1007
1023 1008 html_value = value
1024 1009 tag_data = tags_paterns.get(tag_type)
1025 1010 if tag_data:
1026 1011 pat, replace_html = tag_data
1027 1012 # convert to plain `unicode` instead of a markup tag to be used in
1028 1013 # regex expressions. safe_unicode doesn't work here
1029 1014 html_value = pat.sub(replace_html, unicode(value))
1030 1015
1031 1016 return html_value
1032 1017
1033 1018
1034 1019 def bool2icon(value):
1035 1020 """
1036 1021 Returns boolean value of a given value, represented as html element with
1037 1022 classes that will represent icons
1038 1023
1039 1024 :param value: given value to convert to html node
1040 1025 """
1041 1026
1042 1027 if value: # does bool conversion
1043 1028 return HTML.tag('i', class_="icon-true")
1044 1029 else: # not true as bool
1045 1030 return HTML.tag('i', class_="icon-false")
1046 1031
1047 1032
1048 1033 #==============================================================================
1049 1034 # PERMS
1050 1035 #==============================================================================
1051 1036 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
1052 1037 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
1053 1038 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
1054 1039 csrf_token_key
1055 1040
1056 1041
1057 1042 #==============================================================================
1058 1043 # GRAVATAR URL
1059 1044 #==============================================================================
1060 1045 class InitialsGravatar(object):
1061 1046 def __init__(self, email_address, first_name, last_name, size=30,
1062 1047 background=None, text_color='#fff'):
1063 1048 self.size = size
1064 1049 self.first_name = first_name
1065 1050 self.last_name = last_name
1066 1051 self.email_address = email_address
1067 1052 self.background = background or self.str2color(email_address)
1068 1053 self.text_color = text_color
1069 1054
1070 1055 def get_color_bank(self):
1071 1056 """
1072 1057 returns a predefined list of colors that gravatars can use.
1073 1058 Those are randomized distinct colors that guarantee readability and
1074 1059 uniqueness.
1075 1060
1076 1061 generated with: http://phrogz.net/css/distinct-colors.html
1077 1062 """
1078 1063 return [
1079 1064 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1080 1065 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1081 1066 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1082 1067 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1083 1068 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1084 1069 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1085 1070 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1086 1071 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1087 1072 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1088 1073 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1089 1074 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1090 1075 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1091 1076 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1092 1077 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1093 1078 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1094 1079 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1095 1080 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1096 1081 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1097 1082 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1098 1083 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1099 1084 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1100 1085 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1101 1086 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1102 1087 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1103 1088 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1104 1089 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1105 1090 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1106 1091 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1107 1092 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1108 1093 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1109 1094 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1110 1095 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1111 1096 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1112 1097 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1113 1098 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1114 1099 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1115 1100 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1116 1101 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1117 1102 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1118 1103 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1119 1104 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1120 1105 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1121 1106 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1122 1107 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1123 1108 '#4f8c46', '#368dd9', '#5c0073'
1124 1109 ]
1125 1110
1126 1111 def rgb_to_hex_color(self, rgb_tuple):
1127 1112 """
1128 1113 Converts an rgb_tuple passed to an hex color.
1129 1114
1130 1115 :param rgb_tuple: tuple with 3 ints represents rgb color space
1131 1116 """
1132 1117 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1133 1118
1134 1119 def email_to_int_list(self, email_str):
1135 1120 """
1136 1121 Get every byte of the hex digest value of email and turn it to integer.
1137 1122 It's going to be always between 0-255
1138 1123 """
1139 1124 digest = md5_safe(email_str.lower())
1140 1125 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1141 1126
1142 1127 def pick_color_bank_index(self, email_str, color_bank):
1143 1128 return self.email_to_int_list(email_str)[0] % len(color_bank)
1144 1129
1145 1130 def str2color(self, email_str):
1146 1131 """
1147 1132 Tries to map in a stable algorithm an email to color
1148 1133
1149 1134 :param email_str:
1150 1135 """
1151 1136 color_bank = self.get_color_bank()
1152 1137 # pick position (module it's length so we always find it in the
1153 1138 # bank even if it's smaller than 256 values
1154 1139 pos = self.pick_color_bank_index(email_str, color_bank)
1155 1140 return color_bank[pos]
1156 1141
1157 1142 def normalize_email(self, email_address):
1158 1143 import unicodedata
1159 1144 # default host used to fill in the fake/missing email
1160 1145 default_host = u'localhost'
1161 1146
1162 1147 if not email_address:
1163 1148 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1164 1149
1165 1150 email_address = safe_unicode(email_address)
1166 1151
1167 1152 if u'@' not in email_address:
1168 1153 email_address = u'%s@%s' % (email_address, default_host)
1169 1154
1170 1155 if email_address.endswith(u'@'):
1171 1156 email_address = u'%s%s' % (email_address, default_host)
1172 1157
1173 1158 email_address = unicodedata.normalize('NFKD', email_address)\
1174 1159 .encode('ascii', 'ignore')
1175 1160 return email_address
1176 1161
1177 1162 def get_initials(self):
1178 1163 """
1179 1164 Returns 2 letter initials calculated based on the input.
1180 1165 The algorithm picks first given email address, and takes first letter
1181 1166 of part before @, and then the first letter of server name. In case
1182 1167 the part before @ is in a format of `somestring.somestring2` it replaces
1183 1168 the server letter with first letter of somestring2
1184 1169
1185 1170 In case function was initialized with both first and lastname, this
1186 1171 overrides the extraction from email by first letter of the first and
1187 1172 last name. We add special logic to that functionality, In case Full name
1188 1173 is compound, like Guido Von Rossum, we use last part of the last name
1189 1174 (Von Rossum) picking `R`.
1190 1175
1191 1176 Function also normalizes the non-ascii characters to they ascii
1192 1177 representation, eg Δ„ => A
1193 1178 """
1194 1179 import unicodedata
1195 1180 # replace non-ascii to ascii
1196 1181 first_name = unicodedata.normalize(
1197 1182 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1198 1183 last_name = unicodedata.normalize(
1199 1184 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1200 1185
1201 1186 # do NFKD encoding, and also make sure email has proper format
1202 1187 email_address = self.normalize_email(self.email_address)
1203 1188
1204 1189 # first push the email initials
1205 1190 prefix, server = email_address.split('@', 1)
1206 1191
1207 1192 # check if prefix is maybe a 'first_name.last_name' syntax
1208 1193 _dot_split = prefix.rsplit('.', 1)
1209 1194 if len(_dot_split) == 2 and _dot_split[1]:
1210 1195 initials = [_dot_split[0][0], _dot_split[1][0]]
1211 1196 else:
1212 1197 initials = [prefix[0], server[0]]
1213 1198
1214 1199 # then try to replace either first_name or last_name
1215 1200 fn_letter = (first_name or " ")[0].strip()
1216 1201 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1217 1202
1218 1203 if fn_letter:
1219 1204 initials[0] = fn_letter
1220 1205
1221 1206 if ln_letter:
1222 1207 initials[1] = ln_letter
1223 1208
1224 1209 return ''.join(initials).upper()
1225 1210
1226 1211 def get_img_data_by_type(self, font_family, img_type):
1227 1212 default_user = """
1228 1213 <svg xmlns="http://www.w3.org/2000/svg"
1229 1214 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1230 1215 viewBox="-15 -10 439.165 429.164"
1231 1216
1232 1217 xml:space="preserve"
1233 1218 style="background:{background};" >
1234 1219
1235 1220 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1236 1221 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1237 1222 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1238 1223 168.596,153.916,216.671,
1239 1224 204.583,216.671z" fill="{text_color}"/>
1240 1225 <path d="M407.164,374.717L360.88,
1241 1226 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1242 1227 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1243 1228 15.366-44.203,23.488-69.076,23.488c-24.877,
1244 1229 0-48.762-8.122-69.078-23.488
1245 1230 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1246 1231 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1247 1232 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1248 1233 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1249 1234 19.402-10.527 C409.699,390.129,
1250 1235 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1251 1236 </svg>""".format(
1252 1237 size=self.size,
1253 1238 background='#979797', # @grey4
1254 1239 text_color=self.text_color,
1255 1240 font_family=font_family)
1256 1241
1257 1242 return {
1258 1243 "default_user": default_user
1259 1244 }[img_type]
1260 1245
1261 1246 def get_img_data(self, svg_type=None):
1262 1247 """
1263 1248 generates the svg metadata for image
1264 1249 """
1265 1250
1266 1251 font_family = ','.join([
1267 1252 'proximanovaregular',
1268 1253 'Proxima Nova Regular',
1269 1254 'Proxima Nova',
1270 1255 'Arial',
1271 1256 'Lucida Grande',
1272 1257 'sans-serif'
1273 1258 ])
1274 1259 if svg_type:
1275 1260 return self.get_img_data_by_type(font_family, svg_type)
1276 1261
1277 1262 initials = self.get_initials()
1278 1263 img_data = """
1279 1264 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1280 1265 width="{size}" height="{size}"
1281 1266 style="width: 100%; height: 100%; background-color: {background}"
1282 1267 viewBox="0 0 {size} {size}">
1283 1268 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1284 1269 pointer-events="auto" fill="{text_color}"
1285 1270 font-family="{font_family}"
1286 1271 style="font-weight: 400; font-size: {f_size}px;">{text}
1287 1272 </text>
1288 1273 </svg>""".format(
1289 1274 size=self.size,
1290 1275 f_size=self.size/1.85, # scale the text inside the box nicely
1291 1276 background=self.background,
1292 1277 text_color=self.text_color,
1293 1278 text=initials.upper(),
1294 1279 font_family=font_family)
1295 1280
1296 1281 return img_data
1297 1282
1298 1283 def generate_svg(self, svg_type=None):
1299 1284 img_data = self.get_img_data(svg_type)
1300 1285 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1301 1286
1302 1287
1303 1288 def initials_gravatar(email_address, first_name, last_name, size=30):
1304 1289 svg_type = None
1305 1290 if email_address == User.DEFAULT_USER_EMAIL:
1306 1291 svg_type = 'default_user'
1307 1292 klass = InitialsGravatar(email_address, first_name, last_name, size)
1308 1293 return klass.generate_svg(svg_type=svg_type)
1309 1294
1310 1295
1311 1296 def gravatar_url(email_address, size=30, request=None):
1312 1297 request = get_current_request()
1313 1298 if request and hasattr(request, 'call_context'):
1314 1299 _use_gravatar = request.call_context.visual.use_gravatar
1315 1300 _gravatar_url = request.call_context.visual.gravatar_url
1316 1301 else:
1317 1302 # doh, we need to re-import those to mock it later
1318 1303 from pylons import tmpl_context as c
1319 1304
1320 1305 _use_gravatar = c.visual.use_gravatar
1321 1306 _gravatar_url = c.visual.gravatar_url
1322 1307
1323 1308 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1324 1309
1325 1310 email_address = email_address or User.DEFAULT_USER_EMAIL
1326 1311 if isinstance(email_address, unicode):
1327 1312 # hashlib crashes on unicode items
1328 1313 email_address = safe_str(email_address)
1329 1314
1330 1315 # empty email or default user
1331 1316 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1332 1317 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1333 1318
1334 1319 if _use_gravatar:
1335 1320 # TODO: Disuse pyramid thread locals. Think about another solution to
1336 1321 # get the host and schema here.
1337 1322 request = get_current_request()
1338 1323 tmpl = safe_str(_gravatar_url)
1339 1324 tmpl = tmpl.replace('{email}', email_address)\
1340 1325 .replace('{md5email}', md5_safe(email_address.lower())) \
1341 1326 .replace('{netloc}', request.host)\
1342 1327 .replace('{scheme}', request.scheme)\
1343 1328 .replace('{size}', safe_str(size))
1344 1329 return tmpl
1345 1330 else:
1346 1331 return initials_gravatar(email_address, '', '', size=size)
1347 1332
1348 1333
1349 1334 class Page(_Page):
1350 1335 """
1351 1336 Custom pager to match rendering style with paginator
1352 1337 """
1353 1338
1354 1339 def _get_pos(self, cur_page, max_page, items):
1355 1340 edge = (items / 2) + 1
1356 1341 if (cur_page <= edge):
1357 1342 radius = max(items / 2, items - cur_page)
1358 1343 elif (max_page - cur_page) < edge:
1359 1344 radius = (items - 1) - (max_page - cur_page)
1360 1345 else:
1361 1346 radius = items / 2
1362 1347
1363 1348 left = max(1, (cur_page - (radius)))
1364 1349 right = min(max_page, cur_page + (radius))
1365 1350 return left, cur_page, right
1366 1351
1367 1352 def _range(self, regexp_match):
1368 1353 """
1369 1354 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1370 1355
1371 1356 Arguments:
1372 1357
1373 1358 regexp_match
1374 1359 A "re" (regular expressions) match object containing the
1375 1360 radius of linked pages around the current page in
1376 1361 regexp_match.group(1) as a string
1377 1362
1378 1363 This function is supposed to be called as a callable in
1379 1364 re.sub.
1380 1365
1381 1366 """
1382 1367 radius = int(regexp_match.group(1))
1383 1368
1384 1369 # Compute the first and last page number within the radius
1385 1370 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1386 1371 # -> leftmost_page = 5
1387 1372 # -> rightmost_page = 9
1388 1373 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1389 1374 self.last_page,
1390 1375 (radius * 2) + 1)
1391 1376 nav_items = []
1392 1377
1393 1378 # Create a link to the first page (unless we are on the first page
1394 1379 # or there would be no need to insert '..' spacers)
1395 1380 if self.page != self.first_page and self.first_page < leftmost_page:
1396 1381 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1397 1382
1398 1383 # Insert dots if there are pages between the first page
1399 1384 # and the currently displayed page range
1400 1385 if leftmost_page - self.first_page > 1:
1401 1386 # Wrap in a SPAN tag if nolink_attr is set
1402 1387 text = '..'
1403 1388 if self.dotdot_attr:
1404 1389 text = HTML.span(c=text, **self.dotdot_attr)
1405 1390 nav_items.append(text)
1406 1391
1407 1392 for thispage in xrange(leftmost_page, rightmost_page + 1):
1408 1393 # Hilight the current page number and do not use a link
1409 1394 if thispage == self.page:
1410 1395 text = '%s' % (thispage,)
1411 1396 # Wrap in a SPAN tag if nolink_attr is set
1412 1397 if self.curpage_attr:
1413 1398 text = HTML.span(c=text, **self.curpage_attr)
1414 1399 nav_items.append(text)
1415 1400 # Otherwise create just a link to that page
1416 1401 else:
1417 1402 text = '%s' % (thispage,)
1418 1403 nav_items.append(self._pagerlink(thispage, text))
1419 1404
1420 1405 # Insert dots if there are pages between the displayed
1421 1406 # page numbers and the end of the page range
1422 1407 if self.last_page - rightmost_page > 1:
1423 1408 text = '..'
1424 1409 # Wrap in a SPAN tag if nolink_attr is set
1425 1410 if self.dotdot_attr:
1426 1411 text = HTML.span(c=text, **self.dotdot_attr)
1427 1412 nav_items.append(text)
1428 1413
1429 1414 # Create a link to the very last page (unless we are on the last
1430 1415 # page or there would be no need to insert '..' spacers)
1431 1416 if self.page != self.last_page and rightmost_page < self.last_page:
1432 1417 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1433 1418
1434 1419 ## prerender links
1435 1420 #_page_link = url.current()
1436 1421 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1437 1422 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1438 1423 return self.separator.join(nav_items)
1439 1424
1440 1425 def pager(self, format='~2~', page_param='page', partial_param='partial',
1441 1426 show_if_single_page=False, separator=' ', onclick=None,
1442 1427 symbol_first='<<', symbol_last='>>',
1443 1428 symbol_previous='<', symbol_next='>',
1444 1429 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1445 1430 curpage_attr={'class': 'pager_curpage'},
1446 1431 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1447 1432
1448 1433 self.curpage_attr = curpage_attr
1449 1434 self.separator = separator
1450 1435 self.pager_kwargs = kwargs
1451 1436 self.page_param = page_param
1452 1437 self.partial_param = partial_param
1453 1438 self.onclick = onclick
1454 1439 self.link_attr = link_attr
1455 1440 self.dotdot_attr = dotdot_attr
1456 1441
1457 1442 # Don't show navigator if there is no more than one page
1458 1443 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1459 1444 return ''
1460 1445
1461 1446 from string import Template
1462 1447 # Replace ~...~ in token format by range of pages
1463 1448 result = re.sub(r'~(\d+)~', self._range, format)
1464 1449
1465 1450 # Interpolate '%' variables
1466 1451 result = Template(result).safe_substitute({
1467 1452 'first_page': self.first_page,
1468 1453 'last_page': self.last_page,
1469 1454 'page': self.page,
1470 1455 'page_count': self.page_count,
1471 1456 'items_per_page': self.items_per_page,
1472 1457 'first_item': self.first_item,
1473 1458 'last_item': self.last_item,
1474 1459 'item_count': self.item_count,
1475 1460 'link_first': self.page > self.first_page and \
1476 1461 self._pagerlink(self.first_page, symbol_first) or '',
1477 1462 'link_last': self.page < self.last_page and \
1478 1463 self._pagerlink(self.last_page, symbol_last) or '',
1479 1464 'link_previous': self.previous_page and \
1480 1465 self._pagerlink(self.previous_page, symbol_previous) \
1481 1466 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1482 1467 'link_next': self.next_page and \
1483 1468 self._pagerlink(self.next_page, symbol_next) \
1484 1469 or HTML.span(symbol_next, class_="pg-next disabled")
1485 1470 })
1486 1471
1487 1472 return literal(result)
1488 1473
1489 1474
1490 1475 #==============================================================================
1491 1476 # REPO PAGER, PAGER FOR REPOSITORY
1492 1477 #==============================================================================
1493 1478 class RepoPage(Page):
1494 1479
1495 1480 def __init__(self, collection, page=1, items_per_page=20,
1496 1481 item_count=None, url=None, **kwargs):
1497 1482
1498 1483 """Create a "RepoPage" instance. special pager for paging
1499 1484 repository
1500 1485 """
1501 1486 self._url_generator = url
1502 1487
1503 1488 # Safe the kwargs class-wide so they can be used in the pager() method
1504 1489 self.kwargs = kwargs
1505 1490
1506 1491 # Save a reference to the collection
1507 1492 self.original_collection = collection
1508 1493
1509 1494 self.collection = collection
1510 1495
1511 1496 # The self.page is the number of the current page.
1512 1497 # The first page has the number 1!
1513 1498 try:
1514 1499 self.page = int(page) # make it int() if we get it as a string
1515 1500 except (ValueError, TypeError):
1516 1501 self.page = 1
1517 1502
1518 1503 self.items_per_page = items_per_page
1519 1504
1520 1505 # Unless the user tells us how many items the collections has
1521 1506 # we calculate that ourselves.
1522 1507 if item_count is not None:
1523 1508 self.item_count = item_count
1524 1509 else:
1525 1510 self.item_count = len(self.collection)
1526 1511
1527 1512 # Compute the number of the first and last available page
1528 1513 if self.item_count > 0:
1529 1514 self.first_page = 1
1530 1515 self.page_count = int(math.ceil(float(self.item_count) /
1531 1516 self.items_per_page))
1532 1517 self.last_page = self.first_page + self.page_count - 1
1533 1518
1534 1519 # Make sure that the requested page number is the range of
1535 1520 # valid pages
1536 1521 if self.page > self.last_page:
1537 1522 self.page = self.last_page
1538 1523 elif self.page < self.first_page:
1539 1524 self.page = self.first_page
1540 1525
1541 1526 # Note: the number of items on this page can be less than
1542 1527 # items_per_page if the last page is not full
1543 1528 self.first_item = max(0, (self.item_count) - (self.page *
1544 1529 items_per_page))
1545 1530 self.last_item = ((self.item_count - 1) - items_per_page *
1546 1531 (self.page - 1))
1547 1532
1548 1533 self.items = list(self.collection[self.first_item:self.last_item + 1])
1549 1534
1550 1535 # Links to previous and next page
1551 1536 if self.page > self.first_page:
1552 1537 self.previous_page = self.page - 1
1553 1538 else:
1554 1539 self.previous_page = None
1555 1540
1556 1541 if self.page < self.last_page:
1557 1542 self.next_page = self.page + 1
1558 1543 else:
1559 1544 self.next_page = None
1560 1545
1561 1546 # No items available
1562 1547 else:
1563 1548 self.first_page = None
1564 1549 self.page_count = 0
1565 1550 self.last_page = None
1566 1551 self.first_item = None
1567 1552 self.last_item = None
1568 1553 self.previous_page = None
1569 1554 self.next_page = None
1570 1555 self.items = []
1571 1556
1572 1557 # This is a subclass of the 'list' type. Initialise the list now.
1573 1558 list.__init__(self, reversed(self.items))
1574 1559
1575 1560
1576 1561 def breadcrumb_repo_link(repo):
1577 1562 """
1578 1563 Makes a breadcrumbs path link to repo
1579 1564
1580 1565 ex::
1581 1566 group >> subgroup >> repo
1582 1567
1583 1568 :param repo: a Repository instance
1584 1569 """
1585 1570
1586 1571 path = [
1587 1572 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1588 1573 for group in repo.groups_with_parents
1589 1574 ] + [
1590 1575 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1591 1576 ]
1592 1577
1593 1578 return literal(' &raquo; '.join(path))
1594 1579
1595 1580
1596 1581 def format_byte_size_binary(file_size):
1597 1582 """
1598 1583 Formats file/folder sizes to standard.
1599 1584 """
1600 1585 if file_size is None:
1601 1586 file_size = 0
1602 1587
1603 1588 formatted_size = format_byte_size(file_size, binary=True)
1604 1589 return formatted_size
1605 1590
1606 1591
1607 1592 def urlify_text(text_, safe=True):
1608 1593 """
1609 1594 Extrac urls from text and make html links out of them
1610 1595
1611 1596 :param text_:
1612 1597 """
1613 1598
1614 1599 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1615 1600 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1616 1601
1617 1602 def url_func(match_obj):
1618 1603 url_full = match_obj.groups()[0]
1619 1604 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1620 1605 _newtext = url_pat.sub(url_func, text_)
1621 1606 if safe:
1622 1607 return literal(_newtext)
1623 1608 return _newtext
1624 1609
1625 1610
1626 1611 def urlify_commits(text_, repository):
1627 1612 """
1628 1613 Extract commit ids from text and make link from them
1629 1614
1630 1615 :param text_:
1631 1616 :param repository: repo name to build the URL with
1632 1617 """
1633 1618
1634 1619 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1635 1620
1636 1621 def url_func(match_obj):
1637 1622 commit_id = match_obj.groups()[1]
1638 1623 pref = match_obj.groups()[0]
1639 1624 suf = match_obj.groups()[2]
1640 1625
1641 1626 tmpl = (
1642 1627 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1643 1628 '%(commit_id)s</a>%(suf)s'
1644 1629 )
1645 1630 return tmpl % {
1646 1631 'pref': pref,
1647 1632 'cls': 'revision-link',
1648 1633 'url': route_url('repo_commit', repo_name=repository,
1649 1634 commit_id=commit_id),
1650 1635 'commit_id': commit_id,
1651 1636 'suf': suf
1652 1637 }
1653 1638
1654 1639 newtext = URL_PAT.sub(url_func, text_)
1655 1640
1656 1641 return newtext
1657 1642
1658 1643
1659 1644 def _process_url_func(match_obj, repo_name, uid, entry,
1660 1645 return_raw_data=False, link_format='html'):
1661 1646 pref = ''
1662 1647 if match_obj.group().startswith(' '):
1663 1648 pref = ' '
1664 1649
1665 1650 issue_id = ''.join(match_obj.groups())
1666 1651
1667 1652 if link_format == 'html':
1668 1653 tmpl = (
1669 1654 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1670 1655 '%(issue-prefix)s%(id-repr)s'
1671 1656 '</a>')
1672 1657 elif link_format == 'rst':
1673 1658 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1674 1659 elif link_format == 'markdown':
1675 1660 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1676 1661 else:
1677 1662 raise ValueError('Bad link_format:{}'.format(link_format))
1678 1663
1679 1664 (repo_name_cleaned,
1680 1665 parent_group_name) = RepoGroupModel().\
1681 1666 _get_group_name_and_parent(repo_name)
1682 1667
1683 1668 # variables replacement
1684 1669 named_vars = {
1685 1670 'id': issue_id,
1686 1671 'repo': repo_name,
1687 1672 'repo_name': repo_name_cleaned,
1688 1673 'group_name': parent_group_name
1689 1674 }
1690 1675 # named regex variables
1691 1676 named_vars.update(match_obj.groupdict())
1692 1677 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1693 1678
1694 1679 data = {
1695 1680 'pref': pref,
1696 1681 'cls': 'issue-tracker-link',
1697 1682 'url': _url,
1698 1683 'id-repr': issue_id,
1699 1684 'issue-prefix': entry['pref'],
1700 1685 'serv': entry['url'],
1701 1686 }
1702 1687 if return_raw_data:
1703 1688 return {
1704 1689 'id': issue_id,
1705 1690 'url': _url
1706 1691 }
1707 1692 return tmpl % data
1708 1693
1709 1694
1710 1695 def process_patterns(text_string, repo_name, link_format='html'):
1711 1696 allowed_formats = ['html', 'rst', 'markdown']
1712 1697 if link_format not in allowed_formats:
1713 1698 raise ValueError('Link format can be only one of:{} got {}'.format(
1714 1699 allowed_formats, link_format))
1715 1700
1716 1701 repo = None
1717 1702 if repo_name:
1718 1703 # Retrieving repo_name to avoid invalid repo_name to explode on
1719 1704 # IssueTrackerSettingsModel but still passing invalid name further down
1720 1705 repo = Repository.get_by_repo_name(repo_name, cache=True)
1721 1706
1722 1707 settings_model = IssueTrackerSettingsModel(repo=repo)
1723 1708 active_entries = settings_model.get_settings(cache=True)
1724 1709
1725 1710 issues_data = []
1726 1711 newtext = text_string
1727 1712
1728 1713 for uid, entry in active_entries.items():
1729 1714 log.debug('found issue tracker entry with uid %s' % (uid,))
1730 1715
1731 1716 if not (entry['pat'] and entry['url']):
1732 1717 log.debug('skipping due to missing data')
1733 1718 continue
1734 1719
1735 1720 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1736 1721 % (uid, entry['pat'], entry['url'], entry['pref']))
1737 1722
1738 1723 try:
1739 1724 pattern = re.compile(r'%s' % entry['pat'])
1740 1725 except re.error:
1741 1726 log.exception(
1742 1727 'issue tracker pattern: `%s` failed to compile',
1743 1728 entry['pat'])
1744 1729 continue
1745 1730
1746 1731 data_func = partial(
1747 1732 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1748 1733 return_raw_data=True)
1749 1734
1750 1735 for match_obj in pattern.finditer(text_string):
1751 1736 issues_data.append(data_func(match_obj))
1752 1737
1753 1738 url_func = partial(
1754 1739 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1755 1740 link_format=link_format)
1756 1741
1757 1742 newtext = pattern.sub(url_func, newtext)
1758 1743 log.debug('processed prefix:uid `%s`' % (uid,))
1759 1744
1760 1745 return newtext, issues_data
1761 1746
1762 1747
1763 1748 def urlify_commit_message(commit_text, repository=None):
1764 1749 """
1765 1750 Parses given text message and makes proper links.
1766 1751 issues are linked to given issue-server, and rest is a commit link
1767 1752
1768 1753 :param commit_text:
1769 1754 :param repository:
1770 1755 """
1771 1756 from pylons import url # doh, we need to re-import url to mock it later
1772 1757
1773 1758 def escaper(string):
1774 1759 return string.replace('<', '&lt;').replace('>', '&gt;')
1775 1760
1776 1761 newtext = escaper(commit_text)
1777 1762
1778 1763 # extract http/https links and make them real urls
1779 1764 newtext = urlify_text(newtext, safe=False)
1780 1765
1781 1766 # urlify commits - extract commit ids and make link out of them, if we have
1782 1767 # the scope of repository present.
1783 1768 if repository:
1784 1769 newtext = urlify_commits(newtext, repository)
1785 1770
1786 1771 # process issue tracker patterns
1787 1772 newtext, issues = process_patterns(newtext, repository or '')
1788 1773
1789 1774 return literal(newtext)
1790 1775
1791 1776
1792 1777 def render_binary(repo_name, file_obj):
1793 1778 """
1794 1779 Choose how to render a binary file
1795 1780 """
1796 1781 filename = file_obj.name
1797 1782
1798 1783 # images
1799 1784 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1800 1785 if fnmatch.fnmatch(filename, pat=ext):
1801 1786 alt = filename
1802 1787 src = route_path(
1803 1788 'repo_file_raw', repo_name=repo_name,
1804 1789 commit_id=file_obj.commit.raw_id, f_path=file_obj.path)
1805 1790 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1806 1791
1807 1792
1808 1793 def renderer_from_filename(filename, exclude=None):
1809 1794 """
1810 1795 choose a renderer based on filename, this works only for text based files
1811 1796 """
1812 1797
1813 1798 # ipython
1814 1799 for ext in ['*.ipynb']:
1815 1800 if fnmatch.fnmatch(filename, pat=ext):
1816 1801 return 'jupyter'
1817 1802
1818 1803 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1819 1804 if is_markup:
1820 1805 return is_markup
1821 1806 return None
1822 1807
1823 1808
1824 1809 def render(source, renderer='rst', mentions=False, relative_urls=None,
1825 1810 repo_name=None):
1826 1811
1827 1812 def maybe_convert_relative_links(html_source):
1828 1813 if relative_urls:
1829 1814 return relative_links(html_source, relative_urls)
1830 1815 return html_source
1831 1816
1832 1817 if renderer == 'rst':
1833 1818 if repo_name:
1834 1819 # process patterns on comments if we pass in repo name
1835 1820 source, issues = process_patterns(
1836 1821 source, repo_name, link_format='rst')
1837 1822
1838 1823 return literal(
1839 1824 '<div class="rst-block">%s</div>' %
1840 1825 maybe_convert_relative_links(
1841 1826 MarkupRenderer.rst(source, mentions=mentions)))
1842 1827 elif renderer == 'markdown':
1843 1828 if repo_name:
1844 1829 # process patterns on comments if we pass in repo name
1845 1830 source, issues = process_patterns(
1846 1831 source, repo_name, link_format='markdown')
1847 1832
1848 1833 return literal(
1849 1834 '<div class="markdown-block">%s</div>' %
1850 1835 maybe_convert_relative_links(
1851 1836 MarkupRenderer.markdown(source, flavored=True,
1852 1837 mentions=mentions)))
1853 1838 elif renderer == 'jupyter':
1854 1839 return literal(
1855 1840 '<div class="ipynb">%s</div>' %
1856 1841 maybe_convert_relative_links(
1857 1842 MarkupRenderer.jupyter(source)))
1858 1843
1859 1844 # None means just show the file-source
1860 1845 return None
1861 1846
1862 1847
1863 1848 def commit_status(repo, commit_id):
1864 1849 return ChangesetStatusModel().get_status(repo, commit_id)
1865 1850
1866 1851
1867 1852 def commit_status_lbl(commit_status):
1868 1853 return dict(ChangesetStatus.STATUSES).get(commit_status)
1869 1854
1870 1855
1871 1856 def commit_time(repo_name, commit_id):
1872 1857 repo = Repository.get_by_repo_name(repo_name)
1873 1858 commit = repo.get_commit(commit_id=commit_id)
1874 1859 return commit.date
1875 1860
1876 1861
1877 1862 def get_permission_name(key):
1878 1863 return dict(Permission.PERMS).get(key)
1879 1864
1880 1865
1881 1866 def journal_filter_help(request):
1882 1867 _ = request.translate
1883 1868
1884 1869 return _(
1885 1870 'Example filter terms:\n' +
1886 1871 ' repository:vcs\n' +
1887 1872 ' username:marcin\n' +
1888 1873 ' username:(NOT marcin)\n' +
1889 1874 ' action:*push*\n' +
1890 1875 ' ip:127.0.0.1\n' +
1891 1876 ' date:20120101\n' +
1892 1877 ' date:[20120101100000 TO 20120102]\n' +
1893 1878 '\n' +
1894 1879 'Generate wildcards using \'*\' character:\n' +
1895 1880 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1896 1881 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1897 1882 '\n' +
1898 1883 'Optional AND / OR operators in queries\n' +
1899 1884 ' "repository:vcs OR repository:test"\n' +
1900 1885 ' "username:test AND repository:test*"\n'
1901 1886 )
1902 1887
1903 1888
1904 1889 def search_filter_help(searcher, request):
1905 1890 _ = request.translate
1906 1891
1907 1892 terms = ''
1908 1893 return _(
1909 1894 'Example filter terms for `{searcher}` search:\n' +
1910 1895 '{terms}\n' +
1911 1896 'Generate wildcards using \'*\' character:\n' +
1912 1897 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1913 1898 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1914 1899 '\n' +
1915 1900 'Optional AND / OR operators in queries\n' +
1916 1901 ' "repo_name:vcs OR repo_name:test"\n' +
1917 1902 ' "owner:test AND repo_name:test*"\n' +
1918 1903 'More: {search_doc}'
1919 1904 ).format(searcher=searcher.name,
1920 1905 terms=terms, search_doc=searcher.query_lang_doc)
1921 1906
1922 1907
1923 1908 def not_mapped_error(repo_name):
1924 1909 from rhodecode.translation import _
1925 1910 flash(_('%s repository is not mapped to db perhaps'
1926 1911 ' it was created or renamed from the filesystem'
1927 1912 ' please run the application again'
1928 1913 ' in order to rescan repositories') % repo_name, category='error')
1929 1914
1930 1915
1931 1916 def ip_range(ip_addr):
1932 1917 from rhodecode.model.db import UserIpMap
1933 1918 s, e = UserIpMap._get_ip_range(ip_addr)
1934 1919 return '%s - %s' % (s, e)
1935 1920
1936 1921
1937 1922 def form(url, method='post', needs_csrf_token=True, **attrs):
1938 1923 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1939 1924 if method.lower() != 'get' and needs_csrf_token:
1940 1925 raise Exception(
1941 1926 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1942 1927 'CSRF token. If the endpoint does not require such token you can ' +
1943 1928 'explicitly set the parameter needs_csrf_token to false.')
1944 1929
1945 1930 return wh_form(url, method=method, **attrs)
1946 1931
1947 1932
1948 1933 def secure_form(form_url, method="POST", multipart=False, **attrs):
1949 1934 """Start a form tag that points the action to an url. This
1950 1935 form tag will also include the hidden field containing
1951 1936 the auth token.
1952 1937
1953 1938 The url options should be given either as a string, or as a
1954 1939 ``url()`` function. The method for the form defaults to POST.
1955 1940
1956 1941 Options:
1957 1942
1958 1943 ``multipart``
1959 1944 If set to True, the enctype is set to "multipart/form-data".
1960 1945 ``method``
1961 1946 The method to use when submitting the form, usually either
1962 1947 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1963 1948 hidden input with name _method is added to simulate the verb
1964 1949 over POST.
1965 1950
1966 1951 """
1967 1952 from webhelpers.pylonslib.secure_form import insecure_form
1968 1953
1969 1954 session = None
1970 1955
1971 1956 # TODO(marcink): after pyramid migration require request variable ALWAYS
1972 1957 if 'request' in attrs:
1973 1958 session = attrs['request'].session
1974 1959 del attrs['request']
1975 1960
1976 1961 form = insecure_form(form_url, method, multipart, **attrs)
1977 1962 token = literal(
1978 1963 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1979 1964 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1980 1965
1981 1966 return literal("%s\n%s" % (form, token))
1982 1967
1983 1968
1984 1969 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1985 1970 select_html = select(name, selected, options, **attrs)
1986 1971 select2 = """
1987 1972 <script>
1988 1973 $(document).ready(function() {
1989 1974 $('#%s').select2({
1990 1975 containerCssClass: 'drop-menu',
1991 1976 dropdownCssClass: 'drop-menu-dropdown',
1992 1977 dropdownAutoWidth: true%s
1993 1978 });
1994 1979 });
1995 1980 </script>
1996 1981 """
1997 1982 filter_option = """,
1998 1983 minimumResultsForSearch: -1
1999 1984 """
2000 1985 input_id = attrs.get('id') or name
2001 1986 filter_enabled = "" if enable_filter else filter_option
2002 1987 select_script = literal(select2 % (input_id, filter_enabled))
2003 1988
2004 1989 return literal(select_html+select_script)
2005 1990
2006 1991
2007 1992 def get_visual_attr(tmpl_context_var, attr_name):
2008 1993 """
2009 1994 A safe way to get a variable from visual variable of template context
2010 1995
2011 1996 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
2012 1997 :param attr_name: name of the attribute we fetch from the c.visual
2013 1998 """
2014 1999 visual = getattr(tmpl_context_var, 'visual', None)
2015 2000 if not visual:
2016 2001 return
2017 2002 else:
2018 2003 return getattr(visual, attr_name, None)
2019 2004
2020 2005
2021 2006 def get_last_path_part(file_node):
2022 2007 if not file_node.path:
2023 2008 return u''
2024 2009
2025 2010 path = safe_unicode(file_node.path.split('/')[-1])
2026 2011 return u'../' + path
2027 2012
2028 2013
2029 2014 def route_url(*args, **kwargs):
2030 2015 """
2031 2016 Wrapper around pyramids `route_url` (fully qualified url) function.
2032 2017 It is used to generate URLs from within pylons views or templates.
2033 2018 This will be removed when pyramid migration if finished.
2034 2019 """
2035 2020 req = get_current_request()
2036 2021 return req.route_url(*args, **kwargs)
2037 2022
2038 2023
2039 2024 def route_path(*args, **kwargs):
2040 2025 """
2041 2026 Wrapper around pyramids `route_path` function. It is used to generate
2042 2027 URLs from within pylons views or templates. This will be removed when
2043 2028 pyramid migration if finished.
2044 2029 """
2045 2030 req = get_current_request()
2046 2031 return req.route_path(*args, **kwargs)
2047 2032
2048 2033
2049 2034 def route_path_or_none(*args, **kwargs):
2050 2035 try:
2051 2036 return route_path(*args, **kwargs)
2052 2037 except KeyError:
2053 2038 return None
2054 2039
2055 2040
2041 def current_route_path(request, **kw):
2042 new_args = request.GET.mixed()
2043 new_args.update(kw)
2044 return request.current_route_path(_query=new_args)
2045
2046
2056 2047 def static_url(*args, **kwds):
2057 2048 """
2058 2049 Wrapper around pyramids `route_path` function. It is used to generate
2059 2050 URLs from within pylons views or templates. This will be removed when
2060 2051 pyramid migration if finished.
2061 2052 """
2062 2053 req = get_current_request()
2063 2054 return req.static_url(*args, **kwds)
2064 2055
2065 2056
2066 2057 def resource_path(*args, **kwds):
2067 2058 """
2068 2059 Wrapper around pyramids `route_path` function. It is used to generate
2069 2060 URLs from within pylons views or templates. This will be removed when
2070 2061 pyramid migration if finished.
2071 2062 """
2072 2063 req = get_current_request()
2073 2064 return req.resource_path(*args, **kwds)
2074 2065
2075 2066
2076 2067 def api_call_example(method, args):
2077 2068 """
2078 2069 Generates an API call example via CURL
2079 2070 """
2080 2071 args_json = json.dumps(OrderedDict([
2081 2072 ('id', 1),
2082 2073 ('auth_token', 'SECRET'),
2083 2074 ('method', method),
2084 2075 ('args', args)
2085 2076 ]))
2086 2077 return literal(
2087 2078 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2088 2079 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2089 2080 "and needs to be of `api calls` role."
2090 2081 .format(
2091 2082 api_url=route_url('apiv2'),
2092 2083 token_url=route_url('my_account_auth_tokens'),
2093 2084 data=args_json))
2094 2085
2095 2086
2096 2087 def notification_description(notification, request):
2097 2088 """
2098 2089 Generate notification human readable description based on notification type
2099 2090 """
2100 2091 from rhodecode.model.notification import NotificationModel
2101 2092 return NotificationModel().make_description(
2102 2093 notification, translate=request.translate)
@@ -1,609 +1,609 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <div class="outerwrapper">
5 5 <!-- HEADER -->
6 6 <div class="header">
7 7 <div id="header-inner" class="wrapper">
8 8 <div id="logo">
9 9 <div class="logo-wrapper">
10 10 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
11 11 </div>
12 12 %if c.rhodecode_name:
13 13 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
14 14 %endif
15 15 </div>
16 16 <!-- MENU BAR NAV -->
17 17 ${self.menu_bar_nav()}
18 18 <!-- END MENU BAR NAV -->
19 19 </div>
20 20 </div>
21 21 ${self.menu_bar_subnav()}
22 22 <!-- END HEADER -->
23 23
24 24 <!-- CONTENT -->
25 25 <div id="content" class="wrapper">
26 26
27 27 <rhodecode-toast id="notifications"></rhodecode-toast>
28 28
29 29 <div class="main">
30 30 ${next.main()}
31 31 </div>
32 32 </div>
33 33 <!-- END CONTENT -->
34 34
35 35 </div>
36 36 <!-- FOOTER -->
37 37 <div id="footer">
38 38 <div id="footer-inner" class="title wrapper">
39 39 <div>
40 40 <p class="footer-link-right">
41 41 % if c.visual.show_version:
42 42 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
43 43 % endif
44 44 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
45 45 % if c.visual.rhodecode_support_url:
46 46 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
47 47 % endif
48 48 </p>
49 49 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
50 50 <p class="server-instance" style="display:${sid}">
51 51 ## display hidden instance ID if specially defined
52 52 % if c.rhodecode_instanceid:
53 53 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
54 54 % endif
55 55 </p>
56 56 </div>
57 57 </div>
58 58 </div>
59 59
60 60 <!-- END FOOTER -->
61 61
62 62 ### MAKO DEFS ###
63 63
64 64 <%def name="menu_bar_subnav()">
65 65 </%def>
66 66
67 67 <%def name="breadcrumbs(class_='breadcrumbs')">
68 68 <div class="${class_}">
69 69 ${self.breadcrumbs_links()}
70 70 </div>
71 71 </%def>
72 72
73 73 <%def name="admin_menu()">
74 74 <ul class="admin_menu submenu">
75 75 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
76 76 <li><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
77 77 <li><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
78 78 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
79 79 <li><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
80 80 <li><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
81 81 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
82 82 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
83 83 <li><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
84 84 <li class="last"><a href="${h.url('admin_settings')}">${_('Settings')}</a></li>
85 85 </ul>
86 86 </%def>
87 87
88 88
89 89 <%def name="dt_info_panel(elements)">
90 90 <dl class="dl-horizontal">
91 91 %for dt, dd, title, show_items in elements:
92 92 <dt>${dt}:</dt>
93 93 <dd title="${h.tooltip(title)}">
94 94 %if callable(dd):
95 95 ## allow lazy evaluation of elements
96 96 ${dd()}
97 97 %else:
98 98 ${dd}
99 99 %endif
100 100 %if show_items:
101 101 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
102 102 %endif
103 103 </dd>
104 104
105 105 %if show_items:
106 106 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
107 107 %for item in show_items:
108 108 <dt></dt>
109 109 <dd>${item}</dd>
110 110 %endfor
111 111 </div>
112 112 %endif
113 113
114 114 %endfor
115 115 </dl>
116 116 </%def>
117 117
118 118
119 119 <%def name="gravatar(email, size=16)">
120 120 <%
121 121 if (size > 16):
122 122 gravatar_class = 'gravatar gravatar-large'
123 123 else:
124 124 gravatar_class = 'gravatar'
125 125 %>
126 126 <%doc>
127 127 TODO: johbo: For now we serve double size images to make it smooth
128 128 for retina. This is how it worked until now. Should be replaced
129 129 with a better solution at some point.
130 130 </%doc>
131 131 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
132 132 </%def>
133 133
134 134
135 135 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
136 136 <% email = h.email_or_none(contact) %>
137 137 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
138 138 ${self.gravatar(email, size)}
139 139 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
140 140 </div>
141 141 </%def>
142 142
143 143
144 144 ## admin menu used for people that have some admin resources
145 145 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
146 146 <ul class="submenu">
147 147 %if repositories:
148 148 <li class="local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
149 149 %endif
150 150 %if repository_groups:
151 151 <li class="local-admin-repo-groups"><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
152 152 %endif
153 153 %if user_groups:
154 154 <li class="local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
155 155 %endif
156 156 </ul>
157 157 </%def>
158 158
159 159 <%def name="repo_page_title(repo_instance)">
160 160 <div class="title-content">
161 161 <div class="title-main">
162 162 ## SVN/HG/GIT icons
163 163 %if h.is_hg(repo_instance):
164 164 <i class="icon-hg"></i>
165 165 %endif
166 166 %if h.is_git(repo_instance):
167 167 <i class="icon-git"></i>
168 168 %endif
169 169 %if h.is_svn(repo_instance):
170 170 <i class="icon-svn"></i>
171 171 %endif
172 172
173 173 ## public/private
174 174 %if repo_instance.private:
175 175 <i class="icon-repo-private"></i>
176 176 %else:
177 177 <i class="icon-repo-public"></i>
178 178 %endif
179 179
180 180 ## repo name with group name
181 181 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
182 182
183 183 </div>
184 184
185 185 ## FORKED
186 186 %if repo_instance.fork:
187 187 <p>
188 188 <i class="icon-code-fork"></i> ${_('Fork of')}
189 189 <a href="${h.route_path('repo_summary',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
190 190 </p>
191 191 %endif
192 192
193 193 ## IMPORTED FROM REMOTE
194 194 %if repo_instance.clone_uri:
195 195 <p>
196 196 <i class="icon-code-fork"></i> ${_('Clone from')}
197 197 <a href="${h.url(h.safe_str(h.hide_credentials(repo_instance.clone_uri)))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
198 198 </p>
199 199 %endif
200 200
201 201 ## LOCKING STATUS
202 202 %if repo_instance.locked[0]:
203 203 <p class="locking_locked">
204 204 <i class="icon-repo-lock"></i>
205 205 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
206 206 </p>
207 207 %elif repo_instance.enable_locking:
208 208 <p class="locking_unlocked">
209 209 <i class="icon-repo-unlock"></i>
210 210 ${_('Repository not locked. Pull repository to lock it.')}
211 211 </p>
212 212 %endif
213 213
214 214 </div>
215 215 </%def>
216 216
217 217 <%def name="repo_menu(active=None)">
218 218 <%
219 219 def is_active(selected):
220 220 if selected == active:
221 221 return "active"
222 222 %>
223 223
224 224 <!--- CONTEXT BAR -->
225 225 <div id="context-bar">
226 226 <div class="wrapper">
227 227 <ul id="context-pages" class="horizontal-list navigation">
228 228 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
229 229 <li class="${is_active('changelog')}"><a class="menulink" href="${h.route_path('repo_changelog', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
230 230 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
231 231 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
232 232 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
233 233 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
234 234 <li class="${is_active('showpullrequest')}">
235 235 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
236 236 %if c.repository_pull_requests:
237 237 <span class="pr_notifications">${c.repository_pull_requests}</span>
238 238 %endif
239 239 <div class="menulabel">${_('Pull Requests')}</div>
240 240 </a>
241 241 </li>
242 242 %endif
243 243 <li class="${is_active('options')}">
244 244 <a class="menulink dropdown">
245 245 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
246 246 </a>
247 247 <ul class="submenu">
248 248 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
249 249 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
250 250 %endif
251 251 %if c.rhodecode_db_repo.fork:
252 252 <li>
253 253 <a title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
254 254 href="${h.route_path('repo_compare',
255 255 repo_name=c.rhodecode_db_repo.fork.repo_name,
256 256 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
257 257 source_ref=c.rhodecode_db_repo.landing_rev[1],
258 258 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
259 259 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
260 260 _query=dict(merge=1))}"
261 261 >
262 262 ${_('Compare fork')}
263 263 </a>
264 264 </li>
265 265 %endif
266 266
267 267 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
268 268
269 269 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
270 270 %if c.rhodecode_db_repo.locked[0]:
271 271 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
272 272 %else:
273 273 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
274 274 %endif
275 275 %endif
276 276 %if c.rhodecode_user.username != h.DEFAULT_USER:
277 277 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
278 278 <li><a href="${h.route_path('repo_fork_new',repo_name=c.repo_name)}">${_('Fork')}</a></li>
279 279 <li><a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
280 280 %endif
281 281 %endif
282 282 </ul>
283 283 </li>
284 284 </ul>
285 285 </div>
286 286 <div class="clear"></div>
287 287 </div>
288 288 <!--- END CONTEXT BAR -->
289 289
290 290 </%def>
291 291
292 292 <%def name="usermenu(active=False)">
293 293 ## USER MENU
294 294 <li id="quick_login_li" class="${'active' if active else ''}">
295 295 <a id="quick_login_link" class="menulink childs">
296 296 ${gravatar(c.rhodecode_user.email, 20)}
297 297 <span class="user">
298 298 %if c.rhodecode_user.username != h.DEFAULT_USER:
299 299 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
300 300 %else:
301 301 <span>${_('Sign in')}</span>
302 302 %endif
303 303 </span>
304 304 </a>
305 305
306 306 <div class="user-menu submenu">
307 307 <div id="quick_login">
308 308 %if c.rhodecode_user.username == h.DEFAULT_USER:
309 309 <h4>${_('Sign in to your account')}</h4>
310 ${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)}
310 ${h.form(h.route_path('login', _query={'came_from': h.current_route_path(request)}), needs_csrf_token=False)}
311 311 <div class="form form-vertical">
312 312 <div class="fields">
313 313 <div class="field">
314 314 <div class="label">
315 315 <label for="username">${_('Username')}:</label>
316 316 </div>
317 317 <div class="input">
318 318 ${h.text('username',class_='focus',tabindex=1)}
319 319 </div>
320 320
321 321 </div>
322 322 <div class="field">
323 323 <div class="label">
324 324 <label for="password">${_('Password')}:</label>
325 325 %if h.HasPermissionAny('hg.password_reset.enabled')():
326 326 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
327 327 %endif
328 328 </div>
329 329 <div class="input">
330 330 ${h.password('password',class_='focus',tabindex=2)}
331 331 </div>
332 332 </div>
333 333 <div class="buttons">
334 334 <div class="register">
335 335 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
336 336 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
337 337 %endif
338 338 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
339 339 </div>
340 340 <div class="submit">
341 341 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
342 342 </div>
343 343 </div>
344 344 </div>
345 345 </div>
346 346 ${h.end_form()}
347 347 %else:
348 348 <div class="">
349 349 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
350 350 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
351 351 <div class="email">${c.rhodecode_user.email}</div>
352 352 </div>
353 353 <div class="">
354 354 <ol class="links">
355 355 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
356 356 % if c.rhodecode_user.personal_repo_group:
357 357 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
358 358 % endif
359 359 <li class="logout">
360 360 ${h.secure_form(h.route_path('logout'), request=request)}
361 361 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
362 362 ${h.end_form()}
363 363 </li>
364 364 </ol>
365 365 </div>
366 366 %endif
367 367 </div>
368 368 </div>
369 369 %if c.rhodecode_user.username != h.DEFAULT_USER:
370 370 <div class="pill_container">
371 371 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
372 372 </div>
373 373 % endif
374 374 </li>
375 375 </%def>
376 376
377 377 <%def name="menu_items(active=None)">
378 378 <%
379 379 def is_active(selected):
380 380 if selected == active:
381 381 return "active"
382 382 return ""
383 383 %>
384 384 <ul id="quick" class="main_nav navigation horizontal-list">
385 385 <!-- repo switcher -->
386 386 <li class="${is_active('repositories')} repo_switcher_li has_select2">
387 387 <input id="repo_switcher" name="repo_switcher" type="hidden">
388 388 </li>
389 389
390 390 ## ROOT MENU
391 391 %if c.rhodecode_user.username != h.DEFAULT_USER:
392 392 <li class="${is_active('journal')}">
393 393 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
394 394 <div class="menulabel">${_('Journal')}</div>
395 395 </a>
396 396 </li>
397 397 %else:
398 398 <li class="${is_active('journal')}">
399 399 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
400 400 <div class="menulabel">${_('Public journal')}</div>
401 401 </a>
402 402 </li>
403 403 %endif
404 404 <li class="${is_active('gists')}">
405 405 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
406 406 <div class="menulabel">${_('Gists')}</div>
407 407 </a>
408 408 </li>
409 409 <li class="${is_active('search')}">
410 410 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
411 411 <div class="menulabel">${_('Search')}</div>
412 412 </a>
413 413 </li>
414 414 % if h.HasPermissionAll('hg.admin')('access admin main page'):
415 415 <li class="${is_active('admin')}">
416 416 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
417 417 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
418 418 </a>
419 419 ${admin_menu()}
420 420 </li>
421 421 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
422 422 <li class="${is_active('admin')}">
423 423 <a class="menulink childs" title="${_('Delegated Admin settings')}">
424 424 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
425 425 </a>
426 426 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
427 427 c.rhodecode_user.repository_groups_admin,
428 428 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
429 429 </li>
430 430 % endif
431 431 % if c.debug_style:
432 432 <li class="${is_active('debug_style')}">
433 433 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
434 434 <div class="menulabel">${_('Style')}</div>
435 435 </a>
436 436 </li>
437 437 % endif
438 438 ## render extra user menu
439 439 ${usermenu(active=(active=='my_account'))}
440 440 </ul>
441 441
442 442 <script type="text/javascript">
443 443 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
444 444
445 445 /*format the look of items in the list*/
446 446 var format = function(state, escapeMarkup){
447 447 if (!state.id){
448 448 return state.text; // optgroup
449 449 }
450 450 var obj_dict = state.obj;
451 451 var tmpl = '';
452 452
453 453 if(obj_dict && state.type == 'repo'){
454 454 if(obj_dict['repo_type'] === 'hg'){
455 455 tmpl += '<i class="icon-hg"></i> ';
456 456 }
457 457 else if(obj_dict['repo_type'] === 'git'){
458 458 tmpl += '<i class="icon-git"></i> ';
459 459 }
460 460 else if(obj_dict['repo_type'] === 'svn'){
461 461 tmpl += '<i class="icon-svn"></i> ';
462 462 }
463 463 if(obj_dict['private']){
464 464 tmpl += '<i class="icon-lock" ></i> ';
465 465 }
466 466 else if(visual_show_public_icon){
467 467 tmpl += '<i class="icon-unlock-alt"></i> ';
468 468 }
469 469 }
470 470 if(obj_dict && state.type == 'commit') {
471 471 tmpl += '<i class="icon-tag"></i>';
472 472 }
473 473 if(obj_dict && state.type == 'group'){
474 474 tmpl += '<i class="icon-folder-close"></i> ';
475 475 }
476 476 tmpl += escapeMarkup(state.text);
477 477 return tmpl;
478 478 };
479 479
480 480 var formatResult = function(result, container, query, escapeMarkup) {
481 481 return format(result, escapeMarkup);
482 482 };
483 483
484 484 var formatSelection = function(data, container, escapeMarkup) {
485 485 return format(data, escapeMarkup);
486 486 };
487 487
488 488 $("#repo_switcher").select2({
489 489 cachedDataSource: {},
490 490 minimumInputLength: 2,
491 491 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
492 492 dropdownAutoWidth: true,
493 493 formatResult: formatResult,
494 494 formatSelection: formatSelection,
495 495 containerCssClass: "repo-switcher",
496 496 dropdownCssClass: "repo-switcher-dropdown",
497 497 escapeMarkup: function(m){
498 498 // don't escape our custom placeholder
499 499 if(m.substr(0,23) == '<div class="menulabel">'){
500 500 return m;
501 501 }
502 502
503 503 return Select2.util.escapeMarkup(m);
504 504 },
505 505 query: $.debounce(250, function(query){
506 506 self = this;
507 507 var cacheKey = query.term;
508 508 var cachedData = self.cachedDataSource[cacheKey];
509 509
510 510 if (cachedData) {
511 511 query.callback({results: cachedData.results});
512 512 } else {
513 513 $.ajax({
514 514 url: pyroutes.url('goto_switcher_data'),
515 515 data: {'query': query.term},
516 516 dataType: 'json',
517 517 type: 'GET',
518 518 success: function(data) {
519 519 self.cachedDataSource[cacheKey] = data;
520 520 query.callback({results: data.results});
521 521 },
522 522 error: function(data, textStatus, errorThrown) {
523 523 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
524 524 }
525 525 })
526 526 }
527 527 })
528 528 });
529 529
530 530 $("#repo_switcher").on('select2-selecting', function(e){
531 531 e.preventDefault();
532 532 window.location = e.choice.url;
533 533 });
534 534
535 535 </script>
536 536 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
537 537 </%def>
538 538
539 539 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
540 540 <div class="modal-dialog">
541 541 <div class="modal-content">
542 542 <div class="modal-header">
543 543 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
544 544 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
545 545 </div>
546 546 <div class="modal-body">
547 547 <div class="block-left">
548 548 <table class="keyboard-mappings">
549 549 <tbody>
550 550 <tr>
551 551 <th></th>
552 552 <th>${_('Site-wide shortcuts')}</th>
553 553 </tr>
554 554 <%
555 555 elems = [
556 556 ('/', 'Open quick search box'),
557 557 ('g h', 'Goto home page'),
558 558 ('g g', 'Goto my private gists page'),
559 559 ('g G', 'Goto my public gists page'),
560 560 ('n r', 'New repository page'),
561 561 ('n g', 'New gist page'),
562 562 ]
563 563 %>
564 564 %for key, desc in elems:
565 565 <tr>
566 566 <td class="keys">
567 567 <span class="key tag">${key}</span>
568 568 </td>
569 569 <td>${desc}</td>
570 570 </tr>
571 571 %endfor
572 572 </tbody>
573 573 </table>
574 574 </div>
575 575 <div class="block-left">
576 576 <table class="keyboard-mappings">
577 577 <tbody>
578 578 <tr>
579 579 <th></th>
580 580 <th>${_('Repositories')}</th>
581 581 </tr>
582 582 <%
583 583 elems = [
584 584 ('g s', 'Goto summary page'),
585 585 ('g c', 'Goto changelog page'),
586 586 ('g f', 'Goto files page'),
587 587 ('g F', 'Goto files page with file search activated'),
588 588 ('g p', 'Goto pull requests page'),
589 589 ('g o', 'Goto repository settings'),
590 590 ('g O', 'Goto repository permissions settings'),
591 591 ]
592 592 %>
593 593 %for key, desc in elems:
594 594 <tr>
595 595 <td class="keys">
596 596 <span class="key tag">${key}</span>
597 597 </td>
598 598 <td>${desc}</td>
599 599 </tr>
600 600 %endfor
601 601 </tbody>
602 602 </table>
603 603 </div>
604 604 </div>
605 605 <div class="modal-footer">
606 606 </div>
607 607 </div><!-- /.modal-content -->
608 608 </div><!-- /.modal-dialog -->
609 609 </div><!-- /.modal -->
@@ -1,352 +1,352 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.mako"/>
4 4 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
5 5
6 6 <%def name="title()">
7 7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
8 8 %if c.rhodecode_name:
9 9 &middot; ${h.branding(c.rhodecode_name)}
10 10 %endif
11 11 </%def>
12 12
13 13 <%def name="menu_bar_nav()">
14 14 ${self.menu_items(active='repositories')}
15 15 </%def>
16 16
17 17 <%def name="menu_bar_subnav()">
18 18 ${self.repo_menu(active='changelog')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <script>
23 23 // TODO: marcink switch this to pyroutes
24 24 AJAX_COMMENT_DELETE_URL = "${h.route_path('repo_commit_comment_delete',repo_name=c.repo_name,commit_id=c.commit.raw_id,comment_id='__COMMENT_ID__')}";
25 25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
26 26 </script>
27 27 <div class="box">
28 28 <div class="title">
29 29 ${self.repo_page_title(c.rhodecode_db_repo)}
30 30 </div>
31 31
32 32 <div id="changeset_compare_view_content" class="summary changeset">
33 33 <div class="summary-detail">
34 34 <div class="summary-detail-header">
35 35 <span class="breadcrumbs files_location">
36 36 <h4>${_('Commit')}
37 37 <code>
38 38 ${h.show_id(c.commit)}
39 39 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.commit.raw_id}" title="${_('Copy the full commit id')}"></i>
40 40 % if hasattr(c.commit, 'phase'):
41 41 <span class="tag phase-${c.commit.phase} tooltip" title="${_('Commit phase')}">${c.commit.phase}</span>
42 42 % endif
43 43
44 44 ## obsolete commits
45 45 % if hasattr(c.commit, 'obsolete'):
46 46 % if c.commit.obsolete:
47 47 <span class="tag obsolete-${c.commit.obsolete} tooltip" title="${_('Evolve State')}">${_('obsolete')}</span>
48 48 % endif
49 49 % endif
50 50
51 51 ## hidden commits
52 52 % if hasattr(c.commit, 'hidden'):
53 53 % if c.commit.hidden:
54 54 <span class="tag hidden-${c.commit.hidden} tooltip" title="${_('Evolve State')}">${_('hidden')}</span>
55 55 % endif
56 56 % endif
57 57
58 58 </code>
59 59 </h4>
60 60 </span>
61 61 <div class="pull-right">
62 62 <span id="parent_link">
63 <a href="#" title="${_('Parent Commit')}">${_('Parent')}</a>
63 <a href="#parentCommit" title="${_('Parent Commit')}">${_('Parent')}</a>
64 64 </span>
65 65 |
66 66 <span id="child_link">
67 <a href="#" title="${_('Child Commit')}">${_('Child')}</a>
67 <a href="#childCommit" title="${_('Child Commit')}">${_('Child')}</a>
68 68 </span>
69 69 </div>
70 70 </div>
71 71
72 72 <div class="fieldset">
73 73 <div class="left-label">
74 74 ${_('Description')}:
75 75 </div>
76 76 <div class="right-content">
77 77 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
78 78 <div id="message_expand" style="display:none;">
79 79 ${_('Expand')}
80 80 </div>
81 81 </div>
82 82 </div>
83 83
84 84 %if c.statuses:
85 85 <div class="fieldset">
86 86 <div class="left-label">
87 87 ${_('Commit status')}:
88 88 </div>
89 89 <div class="right-content">
90 90 <div class="changeset-status-ico">
91 91 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
92 92 </div>
93 93 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
94 94 </div>
95 95 </div>
96 96 %endif
97 97
98 98 <div class="fieldset">
99 99 <div class="left-label">
100 100 ${_('References')}:
101 101 </div>
102 102 <div class="right-content">
103 103 <div class="tags">
104 104
105 105 %if c.commit.merge:
106 106 <span class="mergetag tag">
107 107 <i class="icon-merge"></i>${_('merge')}
108 108 </span>
109 109 %endif
110 110
111 111 %if h.is_hg(c.rhodecode_repo):
112 112 %for book in c.commit.bookmarks:
113 113 <span class="booktag tag" title="${h.tooltip(_('Bookmark %s') % book)}">
114 114 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=book))}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
115 115 </span>
116 116 %endfor
117 117 %endif
118 118
119 119 %for tag in c.commit.tags:
120 120 <span class="tagtag tag" title="${h.tooltip(_('Tag %s') % tag)}">
121 121 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=tag))}"><i class="icon-tag"></i>${tag}</a>
122 122 </span>
123 123 %endfor
124 124
125 125 %if c.commit.branch:
126 126 <span class="branchtag tag" title="${h.tooltip(_('Branch %s') % c.commit.branch)}">
127 127 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=c.commit.branch))}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
128 128 </span>
129 129 %endif
130 130 </div>
131 131 </div>
132 132 </div>
133 133
134 134 <div class="fieldset">
135 135 <div class="left-label">
136 136 ${_('Diff options')}:
137 137 </div>
138 138 <div class="right-content">
139 139 <div class="diff-actions">
140 140 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
141 141 ${_('Raw Diff')}
142 142 </a>
143 143 |
144 144 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
145 145 ${_('Patch Diff')}
146 146 </a>
147 147 |
148 148 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(diff='download'))}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
149 149 ${_('Download Diff')}
150 150 </a>
151 151 |
152 152 ${c.ignorews_url(request)}
153 153 |
154 154 ${c.context_url(request)}
155 155 </div>
156 156 </div>
157 157 </div>
158 158
159 159 <div class="fieldset">
160 160 <div class="left-label">
161 161 ${_('Comments')}:
162 162 </div>
163 163 <div class="right-content">
164 164 <div class="comments-number">
165 165 %if c.comments:
166 166 <a href="#comments">${_ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
167 167 %else:
168 168 ${_ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
169 169 %endif
170 170 %if c.inline_cnt:
171 171 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
172 172 %else:
173 173 ${_ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
174 174 %endif
175 175 </div>
176 176 </div>
177 177 </div>
178 178
179 179 <div class="fieldset">
180 180 <div class="left-label">
181 181 ${_('Unresolved TODOs')}:
182 182 </div>
183 183 <div class="right-content">
184 184 <div class="comments-number">
185 185 % if c.unresolved_comments:
186 186 % for co in c.unresolved_comments:
187 187 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))"> #${co.comment_id}</a>${'' if loop.last else ','}
188 188 % endfor
189 189 % else:
190 190 ${_('There are no unresolved TODOs')}
191 191 % endif
192 192 </div>
193 193 </div>
194 194 </div>
195 195
196 196 </div> <!-- end summary-detail -->
197 197
198 198 <div id="commit-stats" class="sidebar-right">
199 199 <div class="summary-detail-header">
200 200 <h4 class="item">
201 201 ${_('Author')}
202 202 </h4>
203 203 </div>
204 204 <div class="sidebar-right-content">
205 205 ${self.gravatar_with_user(c.commit.author)}
206 206 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
207 207 </div>
208 208 </div><!-- end sidebar -->
209 209 </div> <!-- end summary -->
210 210 <div class="cs_files">
211 211 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
212 212 ${cbdiffs.render_diffset_menu()}
213 213 ${cbdiffs.render_diffset(
214 214 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True)}
215 215 </div>
216 216
217 217 ## template for inline comment form
218 218 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
219 219
220 220 ## render comments
221 221 ${comment.generate_comments(c.comments)}
222 222
223 223 ## main comment form and it status
224 224 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id=c.commit.raw_id),
225 225 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
226 226 </div>
227 227
228 228 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
229 229 <script type="text/javascript">
230 230
231 231 $(document).ready(function() {
232 232
233 233 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
234 234 if($('#trimmed_message_box').height() === boxmax){
235 235 $('#message_expand').show();
236 236 }
237 237
238 238 $('#message_expand').on('click', function(e){
239 239 $('#trimmed_message_box').css('max-height', 'none');
240 240 $(this).hide();
241 241 });
242 242
243 243 $('.show-inline-comments').on('click', function(e){
244 244 var boxid = $(this).attr('data-comment-id');
245 245 var button = $(this);
246 246
247 247 if(button.hasClass("comments-visible")) {
248 248 $('#{0} .inline-comments'.format(boxid)).each(function(index){
249 249 $(this).hide();
250 250 });
251 251 button.removeClass("comments-visible");
252 252 } else {
253 253 $('#{0} .inline-comments'.format(boxid)).each(function(index){
254 254 $(this).show();
255 255 });
256 256 button.addClass("comments-visible");
257 257 }
258 258 });
259 259
260 260
261 261 // next links
262 262 $('#child_link').on('click', function(e){
263 263 // fetch via ajax what is going to be the next link, if we have
264 264 // >1 links show them to user to choose
265 265 if(!$('#child_link').hasClass('disabled')){
266 266 $.ajax({
267 267 url: '${h.route_path('repo_commit_children',repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
268 268 success: function(data) {
269 269 if(data.results.length === 0){
270 270 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
271 271 }
272 272 if(data.results.length === 1){
273 273 var commit = data.results[0];
274 274 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
275 275 }
276 276 else if(data.results.length === 2){
277 277 $('#child_link').addClass('disabled');
278 278 $('#child_link').addClass('double');
279 279 var _html = '';
280 280 _html +='<a title="__title__" href="__url__">__rev__</a> '
281 281 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
282 282 .replace('__title__', data.results[0].message)
283 283 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
284 284 _html +=' | ';
285 285 _html +='<a title="__title__" href="__url__">__rev__</a> '
286 286 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
287 287 .replace('__title__', data.results[1].message)
288 288 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
289 289 $('#child_link').html(_html);
290 290 }
291 291 }
292 292 });
293 293 e.preventDefault();
294 294 }
295 295 });
296 296
297 297 // prev links
298 298 $('#parent_link').on('click', function(e){
299 299 // fetch via ajax what is going to be the next link, if we have
300 300 // >1 links show them to user to choose
301 301 if(!$('#parent_link').hasClass('disabled')){
302 302 $.ajax({
303 303 url: '${h.route_path("repo_commit_parents",repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
304 304 success: function(data) {
305 305 if(data.results.length === 0){
306 306 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
307 307 }
308 308 if(data.results.length === 1){
309 309 var commit = data.results[0];
310 310 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
311 311 }
312 312 else if(data.results.length === 2){
313 313 $('#parent_link').addClass('disabled');
314 314 $('#parent_link').addClass('double');
315 315 var _html = '';
316 316 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
317 317 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
318 318 .replace('__title__', data.results[0].message)
319 319 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
320 320 _html +=' | ';
321 321 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
322 322 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
323 323 .replace('__title__', data.results[1].message)
324 324 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
325 325 $('#parent_link').html(_html);
326 326 }
327 327 }
328 328 });
329 329 e.preventDefault();
330 330 }
331 331 });
332 332
333 333 if (location.hash) {
334 334 var result = splitDelimitedHash(location.hash);
335 335 var line = $('html').find(result.loc);
336 336 if (line.length > 0){
337 337 offsetScroll(line, 70);
338 338 }
339 339 }
340 340
341 341 // browse tree @ revision
342 342 $('#files_link').on('click', function(e){
343 343 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
344 344 e.preventDefault();
345 345 });
346 346
347 347 // inject comments into their proper positions
348 348 var file_comments = $('.inline-comment-placeholder');
349 349 })
350 350 </script>
351 351
352 352 </%def>
@@ -1,405 +1,405 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.mako"/>
7 7
8 8 <%def name="comment_block(comment, inline=False)">
9 9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 10 % if inline:
11 11 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
12 12 % else:
13 13 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
14 14 % endif
15 15
16 16
17 17 <div class="comment
18 18 ${'comment-inline' if inline else 'comment-general'}
19 19 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
20 20 id="comment-${comment.comment_id}"
21 21 line="${comment.line_no}"
22 22 data-comment-id="${comment.comment_id}"
23 23 data-comment-type="${comment.comment_type}"
24 24 data-comment-inline=${h.json.dumps(inline)}
25 25 style="${'display: none;' if outdated_at_ver else ''}">
26 26
27 27 <div class="meta">
28 28 <div class="comment-type-label">
29 29 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
30 30 % if comment.comment_type == 'todo':
31 31 % if comment.resolved:
32 32 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
33 33 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
34 34 </div>
35 35 % else:
36 36 <div class="resolved tooltip" style="display: none">
37 37 <span>${comment.comment_type}</span>
38 38 </div>
39 39 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
40 40 ${comment.comment_type}
41 41 </div>
42 42 % endif
43 43 % else:
44 44 % if comment.resolved_comment:
45 45 fix
46 46 % else:
47 47 ${comment.comment_type or 'note'}
48 48 % endif
49 49 % endif
50 50 </div>
51 51 </div>
52 52
53 53 <div class="author ${'author-inline' if inline else 'author-general'}">
54 54 ${base.gravatar_with_user(comment.author.email, 16)}
55 55 </div>
56 56 <div class="date">
57 57 ${h.age_component(comment.modified_at, time_is_local=True)}
58 58 </div>
59 59 % if inline:
60 60 <span></span>
61 61 % else:
62 62 <div class="status-change">
63 63 % if comment.pull_request:
64 64 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
65 65 % if comment.status_change:
66 66 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
67 67 % else:
68 68 ${_('pull request #%s') % comment.pull_request.pull_request_id}
69 69 % endif
70 70 </a>
71 71 % else:
72 72 % if comment.status_change:
73 73 ${_('Status change on commit')}:
74 74 % endif
75 75 % endif
76 76 </div>
77 77 % endif
78 78
79 79 % if comment.status_change:
80 80 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
81 81 <div title="${_('Commit status')}" class="changeset-status-lbl">
82 82 ${comment.status_change[0].status_lbl}
83 83 </div>
84 84 % endif
85 85
86 86 % if comment.resolved_comment:
87 87 <a class="has-spacer-before" href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
88 88 ${_('resolves comment #{}').format(comment.resolved_comment.comment_id)}
89 89 </a>
90 90 % endif
91 91
92 92 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
93 93
94 94 <div class="comment-links-block">
95 95 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
96 96 <span class="tag authortag tooltip" title="${_('Pull request author')}">
97 97 ${_('author')}
98 98 </span>
99 99 |
100 100 % endif
101 101 % if inline:
102 102 <div class="pr-version-inline">
103 103 <a href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
104 104 % if outdated_at_ver:
105 105 <code class="pr-version-num" title="${_('Outdated comment from pull request version {0}').format(pr_index_ver)}">
106 106 outdated ${'v{}'.format(pr_index_ver)} |
107 107 </code>
108 108 % elif pr_index_ver:
109 109 <code class="pr-version-num" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
110 110 ${'v{}'.format(pr_index_ver)} |
111 111 </code>
112 112 % endif
113 113 </a>
114 114 </div>
115 115 % else:
116 116 % if comment.pull_request_version_id and pr_index_ver:
117 117 |
118 118 <div class="pr-version">
119 119 % if comment.outdated:
120 120 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
121 121 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
122 122 </a>
123 123 % else:
124 124 <div title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
125 125 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
126 126 <code class="pr-version-num">
127 127 ${'v{}'.format(pr_index_ver)}
128 128 </code>
129 129 </a>
130 130 </div>
131 131 % endif
132 132 </div>
133 133 % endif
134 134 % endif
135 135
136 136 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
137 137 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
138 138 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
139 139 ## permissions to delete
140 140 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
141 141 ## TODO: dan: add edit comment here
142 142 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
143 143 %else:
144 144 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
145 145 %endif
146 146 %else:
147 147 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
148 148 %endif
149 149
150 150 % if outdated_at_ver:
151 151 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
152 152 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
153 153 % else:
154 154 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
155 155 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
156 156 % endif
157 157
158 158 </div>
159 159 </div>
160 160 <div class="text">
161 161 ${h.render(comment.text, renderer=comment.renderer, mentions=True)}
162 162 </div>
163 163
164 164 </div>
165 165 </%def>
166 166
167 167 ## generate main comments
168 168 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
169 169 <div class="general-comments" id="comments">
170 170 %for comment in comments:
171 171 <div id="comment-tr-${comment.comment_id}">
172 172 ## only render comments that are not from pull request, or from
173 173 ## pull request and a status change
174 174 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
175 175 ${comment_block(comment)}
176 176 %endif
177 177 </div>
178 178 %endfor
179 179 ## to anchor ajax comments
180 180 <div id="injected_page_comments"></div>
181 181 </div>
182 182 </%def>
183 183
184 184
185 185 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
186 186
187 187 <div class="comments">
188 188 <%
189 189 if is_pull_request:
190 190 placeholder = _('Leave a comment on this Pull Request.')
191 191 elif is_compare:
192 192 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
193 193 else:
194 194 placeholder = _('Leave a comment on this Commit.')
195 195 %>
196 196
197 197 % if c.rhodecode_user.username != h.DEFAULT_USER:
198 198 <div class="js-template" id="cb-comment-general-form-template">
199 199 ## template generated for injection
200 200 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
201 201 </div>
202 202
203 203 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
204 204 ## inject form here
205 205 </div>
206 206 <script type="text/javascript">
207 207 var lineNo = 'general';
208 208 var resolvesCommentId = null;
209 209 var generalCommentForm = Rhodecode.comments.createGeneralComment(
210 210 lineNo, "${placeholder}", resolvesCommentId);
211 211
212 212 // set custom success callback on rangeCommit
213 213 % if is_compare:
214 214 generalCommentForm.setHandleFormSubmit(function(o) {
215 215 var self = generalCommentForm;
216 216
217 217 var text = self.cm.getValue();
218 218 var status = self.getCommentStatus();
219 219 var commentType = self.getCommentType();
220 220
221 221 if (text === "" && !status) {
222 222 return;
223 223 }
224 224
225 225 // we can pick which commits we want to make the comment by
226 226 // selecting them via click on preview pane, this will alter the hidden inputs
227 227 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
228 228
229 229 var commitIds = [];
230 230 $('#changeset_compare_view_content .compare_select').each(function(el) {
231 231 var commitId = this.id.replace('row-', '');
232 232 if ($(this).hasClass('hl') || !cherryPicked) {
233 233 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
234 234 commitIds.push(commitId);
235 235 } else {
236 236 $("input[data-commit-id='{0}']".format(commitId)).val('')
237 237 }
238 238 });
239 239
240 240 self.setActionButtonsDisabled(true);
241 241 self.cm.setOption("readOnly", true);
242 242 var postData = {
243 243 'text': text,
244 244 'changeset_status': status,
245 245 'comment_type': commentType,
246 246 'commit_ids': commitIds,
247 247 'csrf_token': CSRF_TOKEN
248 248 };
249 249
250 250 var submitSuccessCallback = function(o) {
251 251 location.reload(true);
252 252 };
253 253 var submitFailCallback = function(){
254 254 self.resetCommentFormState(text)
255 255 };
256 256 self.submitAjaxPOST(
257 257 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
258 258 });
259 259 % endif
260 260
261 261
262 262 </script>
263 263 % else:
264 264 ## form state when not logged in
265 265 <div class="comment-form ac">
266 266
267 267 <div class="comment-area">
268 268 <div class="comment-area-header">
269 269 <ul class="nav-links clearfix">
270 270 <li class="active">
271 271 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
272 272 </li>
273 273 <li class="">
274 274 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
275 275 </li>
276 276 </ul>
277 277 </div>
278 278
279 279 <div class="comment-area-write" style="display: block;">
280 280 <div id="edit-container">
281 281 <div style="padding: 40px 0">
282 282 ${_('You need to be logged in to leave comments.')}
283 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
283 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
284 284 </div>
285 285 </div>
286 286 <div id="preview-container" class="clearfix" style="display: none;">
287 287 <div id="preview-box" class="preview-box"></div>
288 288 </div>
289 289 </div>
290 290
291 291 <div class="comment-area-footer">
292 292 <div class="toolbar">
293 293 <div class="toolbar-text">
294 294 </div>
295 295 </div>
296 296 </div>
297 297 </div>
298 298
299 299 <div class="comment-footer">
300 300 </div>
301 301
302 302 </div>
303 303 % endif
304 304
305 305 <script type="text/javascript">
306 306 bindToggleButtons();
307 307 </script>
308 308 </div>
309 309 </%def>
310 310
311 311
312 312 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
313 313 ## comment injected based on assumption that user is logged in
314 314
315 315 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
316 316
317 317 <div class="comment-area">
318 318 <div class="comment-area-header">
319 319 <ul class="nav-links clearfix">
320 320 <li class="active">
321 321 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
322 322 </li>
323 323 <li class="">
324 324 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
325 325 </li>
326 326 <li class="pull-right">
327 327 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
328 328 % for val in c.visual.comment_types:
329 329 <option value="${val}">${val.upper()}</option>
330 330 % endfor
331 331 </select>
332 332 </li>
333 333 </ul>
334 334 </div>
335 335
336 336 <div class="comment-area-write" style="display: block;">
337 337 <div id="edit-container_${lineno_id}">
338 338 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
339 339 </div>
340 340 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
341 341 <div id="preview-box_${lineno_id}" class="preview-box"></div>
342 342 </div>
343 343 </div>
344 344
345 345 <div class="comment-area-footer">
346 346 <div class="toolbar">
347 347 <div class="toolbar-text">
348 348 ${(_('Comments parsed using %s syntax with %s, and %s actions support.') % (
349 349 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
350 350 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user')),
351 351 ('<span class="tooltip" title="%s">`/`</span>' % _('Start typing with / for certain actions to be triggered via text box.'))
352 352 )
353 353 )|n}
354 354 </div>
355 355 </div>
356 356 </div>
357 357 </div>
358 358
359 359 <div class="comment-footer">
360 360
361 361 % if review_statuses:
362 362 <div class="status_box">
363 363 <select id="change_status_${lineno_id}" name="changeset_status">
364 364 <option></option> ## Placeholder
365 365 % for status, lbl in review_statuses:
366 366 <option value="${status}" data-status="${status}">${lbl}</option>
367 367 %if is_pull_request and change_status and status in ('approved', 'rejected'):
368 368 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
369 369 %endif
370 370 % endfor
371 371 </select>
372 372 </div>
373 373 % endif
374 374
375 375 ## inject extra inputs into the form
376 376 % if form_extras and isinstance(form_extras, (list, tuple)):
377 377 <div id="comment_form_extras">
378 378 % for form_ex_el in form_extras:
379 379 ${form_ex_el|n}
380 380 % endfor
381 381 </div>
382 382 % endif
383 383
384 384 <div class="action-buttons">
385 385 ## inline for has a file, and line-number together with cancel hide button.
386 386 % if form_type == 'inline':
387 387 <input type="hidden" name="f_path" value="{0}">
388 388 <input type="hidden" name="line" value="${lineno_id}">
389 389 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
390 390 ${_('Cancel')}
391 391 </button>
392 392 % endif
393 393
394 394 % if form_type != 'inline':
395 395 <div class="action-buttons-extra"></div>
396 396 % endif
397 397
398 398 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
399 399
400 400 </div>
401 401 </div>
402 402
403 403 </form>
404 404
405 405 </%def> No newline at end of file
@@ -1,64 +1,64 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ##usage:
3 3 ## <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
4 4 ## ${diff_block.diff_block_changeset_table(change)}
5 5 ##
6 6 <%def name="changeset_message()">
7 <h5>${_('The requested commit is too big and content was truncated.')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a></h5>
7 <h5>${_('The requested commit is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a></h5>
8 8 </%def>
9 9 <%def name="file_message()">
10 <h5>${_('The requested file is too big and its content is not shown.')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a></h5>
10 <h5>${_('The requested file is too big and its content is not shown.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a></h5>
11 11 </%def>
12 12
13 13 <%def name="diff_block_changeset_table(change)">
14 14 <div class="diff-container" id="${'diff-container-%s' % (id(change))}">
15 15 %for FID,(cs1, cs2, change, filenode_path, diff, stats, file_data) in change.iteritems():
16 16 <div id="${h.FID('',filenode_path)}_target" ></div>
17 17 <div id="${h.FID('',filenode_path)}" class="diffblock margined comm">
18 18 <div class="code-body">
19 19 <div class="full_f_path" path="${h.safe_unicode(filenode_path)}" style="display: none"></div>
20 20 ${diff|n}
21 21 % if file_data["is_limited_diff"]:
22 22 % if file_data["exceeds_limit"]:
23 23 ${self.file_message()}
24 24 % else:
25 <h5>${_('Diff was truncated. File content available only in full diff.')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a></h5>
25 <h5>${_('Diff was truncated. File content available only in full diff.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a></h5>
26 26 % endif
27 27 % endif
28 28 </div>
29 29 </div>
30 30 %endfor
31 31 </div>
32 32 </%def>
33 33
34 34 <%def name="diff_block_simple(change)">
35 35 <div class="diff-container" id="${'diff-container-%s' % (id(change))}">
36 36 %for op,filenode_path,diff,file_data in change:
37 37 <div id="${h.FID('',filenode_path)}_target" ></div>
38 38 <div id="${h.FID('',filenode_path)}" class="diffblock margined comm" >
39 39 <div class="code-body">
40 40 <div class="full_f_path" path="${h.safe_unicode(filenode_path)}" style="display: none;"></div>
41 41 ${diff|n}
42 42 % if file_data["is_limited_diff"]:
43 43 % if file_data["exceeds_limit"]:
44 44 ${self.file_message()}
45 45 % else:
46 <h5>${_('Diff was truncated. File content available only in full diff.')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a></h5>
46 <h5>${_('Diff was truncated. File content available only in full diff.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a></h5>
47 47 % endif
48 48 % endif
49 49 </div>
50 50 </div>
51 51 %endfor
52 52 </div>
53 53 </%def>
54 54
55 55
56 56 <%def name="diff_summary_text(changed_files, lines_added, lines_deleted, limited_diff=False)">
57 57 % if limited_diff:
58 58 ${_ungettext('%(num)s file changed', '%(num)s files changed', changed_files) % {'num': changed_files}}
59 59 % else:
60 60 ${_ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
61 61 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', changed_files) % {'num': changed_files, 'linesadd': lines_added, 'linesdel': lines_deleted}}
62 62 %endif
63 63 </%def>
64 64
@@ -1,672 +1,666 b''
1 1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 2
3 3 <%def name="diff_line_anchor(filename, line, type)"><%
4 4 return '%s_%s_%i' % (h.safeid(filename), type, line)
5 5 %></%def>
6 6
7 7 <%def name="action_class(action)">
8 8 <%
9 9 return {
10 10 '-': 'cb-deletion',
11 11 '+': 'cb-addition',
12 12 ' ': 'cb-context',
13 13 }.get(action, 'cb-empty')
14 14 %>
15 15 </%def>
16 16
17 17 <%def name="op_class(op_id)">
18 18 <%
19 19 return {
20 20 DEL_FILENODE: 'deletion', # file deleted
21 21 BIN_FILENODE: 'warning' # binary diff hidden
22 22 }.get(op_id, 'addition')
23 23 %>
24 24 </%def>
25 25
26 <%def name="link_for(**kw)">
27 <%
28 new_args = request.GET.mixed()
29 new_args.update(kw)
30 return request.current_route_path(_query=new_args)
31 %>
32 </%def>
26
33 27
34 28 <%def name="render_diffset(diffset, commit=None,
35 29
36 30 # collapse all file diff entries when there are more than this amount of files in the diff
37 31 collapse_when_files_over=20,
38 32
39 33 # collapse lines in the diff when more than this amount of lines changed in the file diff
40 34 lines_changed_limit=500,
41 35
42 36 # add a ruler at to the output
43 37 ruler_at_chars=0,
44 38
45 39 # show inline comments
46 40 use_comments=False,
47 41
48 42 # disable new comments
49 43 disable_new_comments=False,
50 44
51 45 # special file-comments that were deleted in previous versions
52 46 # it's used for showing outdated comments for deleted files in a PR
53 47 deleted_files_comments=None
54 48
55 49 )">
56 50
57 51 %if use_comments:
58 52 <div id="cb-comments-inline-container-template" class="js-template">
59 53 ${inline_comments_container([])}
60 54 </div>
61 55 <div class="js-template" id="cb-comment-inline-form-template">
62 56 <div class="comment-inline-form ac">
63 57
64 58 %if c.rhodecode_user.username != h.DEFAULT_USER:
65 59 ## render template for inline comments
66 60 ${commentblock.comment_form(form_type='inline')}
67 61 %else:
68 62 ${h.form('', class_='inline-form comment-form-login', method='get')}
69 63 <div class="pull-left">
70 64 <div class="comment-help pull-right">
71 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
65 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
72 66 </div>
73 67 </div>
74 68 <div class="comment-button pull-right">
75 69 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
76 70 ${_('Cancel')}
77 71 </button>
78 72 </div>
79 73 <div class="clearfix"></div>
80 74 ${h.end_form()}
81 75 %endif
82 76 </div>
83 77 </div>
84 78
85 79 %endif
86 80 <%
87 81 collapse_all = len(diffset.files) > collapse_when_files_over
88 82 %>
89 83
90 84 %if c.diffmode == 'sideside':
91 85 <style>
92 86 .wrapper {
93 87 max-width: 1600px !important;
94 88 }
95 89 </style>
96 90 %endif
97 91
98 92 %if ruler_at_chars:
99 93 <style>
100 94 .diff table.cb .cb-content:after {
101 95 content: "";
102 96 border-left: 1px solid blue;
103 97 position: absolute;
104 98 top: 0;
105 99 height: 18px;
106 100 opacity: .2;
107 101 z-index: 10;
108 102 //## +5 to account for diff action (+/-)
109 103 left: ${ruler_at_chars + 5}ch;
110 104 </style>
111 105 %endif
112 106
113 107 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
114 108 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
115 109 %if commit:
116 110 <div class="pull-right">
117 111 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
118 112 ${_('Browse Files')}
119 113 </a>
120 114 </div>
121 115 %endif
122 116 <h2 class="clearinner">
123 117 %if commit:
124 118 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
125 119 ${h.age_component(commit.date)} -
126 120 %endif
127 121
128 122 %if diffset.limited_diff:
129 123 ${_('The requested commit is too big and content was truncated.')}
130 124
131 125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
132 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
126 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
133 127 %else:
134 128 ${_ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
135 129 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
136 130 %endif
137 131
138 132 </h2>
139 133 </div>
140 134
141 135 %if not diffset.files:
142 136 <p class="empty_data">${_('No files')}</p>
143 137 %endif
144 138
145 139 <div class="filediffs">
146 140 ## initial value could be marked as False later on
147 141 <% over_lines_changed_limit = False %>
148 142 %for i, filediff in enumerate(diffset.files):
149 143
150 144 <%
151 145 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
152 146 over_lines_changed_limit = lines_changed > lines_changed_limit
153 147 %>
154 148 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
155 149 <div
156 150 class="filediff"
157 151 data-f-path="${filediff.patch['filename']}"
158 152 id="a_${h.FID('', filediff.patch['filename'])}">
159 153 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
160 154 <div class="filediff-collapse-indicator"></div>
161 155 ${diff_ops(filediff)}
162 156 </label>
163 157 ${diff_menu(filediff, use_comments=use_comments)}
164 158 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
165 159 %if not filediff.hunks:
166 160 %for op_id, op_text in filediff.patch['stats']['ops'].items():
167 161 <tr>
168 162 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
169 163 %if op_id == DEL_FILENODE:
170 164 ${_('File was deleted')}
171 165 %elif op_id == BIN_FILENODE:
172 166 ${_('Binary file hidden')}
173 167 %else:
174 168 ${op_text}
175 169 %endif
176 170 </td>
177 171 </tr>
178 172 %endfor
179 173 %endif
180 174 %if filediff.limited_diff:
181 175 <tr class="cb-warning cb-collapser">
182 176 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
183 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
177 ${_('The requested commit is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
184 178 </td>
185 179 </tr>
186 180 %else:
187 181 %if over_lines_changed_limit:
188 182 <tr class="cb-warning cb-collapser">
189 183 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
190 184 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
191 185 <a href="#" class="cb-expand"
192 186 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
193 187 </a>
194 188 <a href="#" class="cb-collapse"
195 189 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
196 190 </a>
197 191 </td>
198 192 </tr>
199 193 %endif
200 194 %endif
201 195
202 196 %for hunk in filediff.hunks:
203 197 <tr class="cb-hunk">
204 198 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
205 199 ## TODO: dan: add ajax loading of more context here
206 200 ## <a href="#">
207 201 <i class="icon-more"></i>
208 202 ## </a>
209 203 </td>
210 204 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
211 205 @@
212 206 -${hunk.source_start},${hunk.source_length}
213 207 +${hunk.target_start},${hunk.target_length}
214 208 ${hunk.section_header}
215 209 </td>
216 210 </tr>
217 211 %if c.diffmode == 'unified':
218 212 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
219 213 %elif c.diffmode == 'sideside':
220 214 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
221 215 %else:
222 216 <tr class="cb-line">
223 217 <td>unknown diff mode</td>
224 218 </tr>
225 219 %endif
226 220 %endfor
227 221
228 222 ## outdated comments that do not fit into currently displayed lines
229 223 % for lineno, comments in filediff.left_comments.items():
230 224
231 225 %if c.diffmode == 'unified':
232 226 <tr class="cb-line">
233 227 <td class="cb-data cb-context"></td>
234 228 <td class="cb-lineno cb-context"></td>
235 229 <td class="cb-lineno cb-context"></td>
236 230 <td class="cb-content cb-context">
237 231 ${inline_comments_container(comments)}
238 232 </td>
239 233 </tr>
240 234 %elif c.diffmode == 'sideside':
241 235 <tr class="cb-line">
242 236 <td class="cb-data cb-context"></td>
243 237 <td class="cb-lineno cb-context"></td>
244 238 <td class="cb-content cb-context"></td>
245 239
246 240 <td class="cb-data cb-context"></td>
247 241 <td class="cb-lineno cb-context"></td>
248 242 <td class="cb-content cb-context">
249 243 ${inline_comments_container(comments)}
250 244 </td>
251 245 </tr>
252 246 %endif
253 247
254 248 % endfor
255 249
256 250 </table>
257 251 </div>
258 252 %endfor
259 253
260 254 ## outdated comments that are made for a file that has been deleted
261 255 % for filename, comments_dict in (deleted_files_comments or {}).items():
262 256
263 257 <div class="filediffs filediff-outdated" style="display: none">
264 258 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
265 259 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
266 260 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
267 261 <div class="filediff-collapse-indicator"></div>
268 262 <span class="pill">
269 263 ## file was deleted
270 264 <strong>${filename}</strong>
271 265 </span>
272 266 <span class="pill-group" style="float: left">
273 267 ## file op, doesn't need translation
274 268 <span class="pill" op="removed">removed in this version</span>
275 269 </span>
276 270 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
277 271 <span class="pill-group" style="float: right">
278 272 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
279 273 </span>
280 274 </label>
281 275
282 276 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
283 277 <tr>
284 278 % if c.diffmode == 'unified':
285 279 <td></td>
286 280 %endif
287 281
288 282 <td></td>
289 283 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
290 284 ${_('File was deleted in this version, and outdated comments were made on it')}
291 285 </td>
292 286 </tr>
293 287 %if c.diffmode == 'unified':
294 288 <tr class="cb-line">
295 289 <td class="cb-data cb-context"></td>
296 290 <td class="cb-lineno cb-context"></td>
297 291 <td class="cb-lineno cb-context"></td>
298 292 <td class="cb-content cb-context">
299 293 ${inline_comments_container(comments_dict['comments'])}
300 294 </td>
301 295 </tr>
302 296 %elif c.diffmode == 'sideside':
303 297 <tr class="cb-line">
304 298 <td class="cb-data cb-context"></td>
305 299 <td class="cb-lineno cb-context"></td>
306 300 <td class="cb-content cb-context"></td>
307 301
308 302 <td class="cb-data cb-context"></td>
309 303 <td class="cb-lineno cb-context"></td>
310 304 <td class="cb-content cb-context">
311 305 ${inline_comments_container(comments_dict['comments'])}
312 306 </td>
313 307 </tr>
314 308 %endif
315 309 </table>
316 310 </div>
317 311 </div>
318 312 % endfor
319 313
320 314 </div>
321 315 </div>
322 316 </%def>
323 317
324 318 <%def name="diff_ops(filediff)">
325 319 <%
326 320 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
327 321 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
328 322 %>
329 323 <span class="pill">
330 324 %if filediff.source_file_path and filediff.target_file_path:
331 325 %if filediff.source_file_path != filediff.target_file_path:
332 326 ## file was renamed, or copied
333 327 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
334 328 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
335 329 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
336 330 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
337 331 %endif
338 332 %else:
339 333 ## file was modified
340 334 <strong>${filediff.source_file_path}</strong>
341 335 %endif
342 336 %else:
343 337 %if filediff.source_file_path:
344 338 ## file was deleted
345 339 <strong>${filediff.source_file_path}</strong>
346 340 %else:
347 341 ## file was added
348 342 <strong>${filediff.target_file_path}</strong>
349 343 %endif
350 344 %endif
351 345 </span>
352 346 <span class="pill-group" style="float: left">
353 347 %if filediff.limited_diff:
354 348 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
355 349 %endif
356 350
357 351 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
358 352 <span class="pill" op="renamed">renamed</span>
359 353 %endif
360 354
361 355 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
362 356 <span class="pill" op="copied">copied</span>
363 357 %endif
364 358
365 359 %if NEW_FILENODE in filediff.patch['stats']['ops']:
366 360 <span class="pill" op="created">created</span>
367 361 %if filediff['target_mode'].startswith('120'):
368 362 <span class="pill" op="symlink">symlink</span>
369 363 %else:
370 364 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
371 365 %endif
372 366 %endif
373 367
374 368 %if DEL_FILENODE in filediff.patch['stats']['ops']:
375 369 <span class="pill" op="removed">removed</span>
376 370 %endif
377 371
378 372 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
379 373 <span class="pill" op="mode">
380 374 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
381 375 </span>
382 376 %endif
383 377 </span>
384 378
385 379 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
386 380
387 381 <span class="pill-group" style="float: right">
388 382 %if BIN_FILENODE in filediff.patch['stats']['ops']:
389 383 <span class="pill" op="binary">binary</span>
390 384 %if MOD_FILENODE in filediff.patch['stats']['ops']:
391 385 <span class="pill" op="modified">modified</span>
392 386 %endif
393 387 %endif
394 388 %if filediff.patch['stats']['added']:
395 389 <span class="pill" op="added">+${filediff.patch['stats']['added']}</span>
396 390 %endif
397 391 %if filediff.patch['stats']['deleted']:
398 392 <span class="pill" op="deleted">-${filediff.patch['stats']['deleted']}</span>
399 393 %endif
400 394 </span>
401 395
402 396 </%def>
403 397
404 398 <%def name="nice_mode(filemode)">
405 399 ${filemode.startswith('100') and filemode[3:] or filemode}
406 400 </%def>
407 401
408 402 <%def name="diff_menu(filediff, use_comments=False)">
409 403 <div class="filediff-menu">
410 404 %if filediff.diffset.source_ref:
411 405 %if filediff.operation in ['D', 'M']:
412 406 <a
413 407 class="tooltip"
414 408 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
415 409 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
416 410 >
417 411 ${_('Show file before')}
418 412 </a> |
419 413 %else:
420 414 <span
421 415 class="tooltip"
422 416 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
423 417 >
424 418 ${_('Show file before')}
425 419 </span> |
426 420 %endif
427 421 %if filediff.operation in ['A', 'M']:
428 422 <a
429 423 class="tooltip"
430 424 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
431 425 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
432 426 >
433 427 ${_('Show file after')}
434 428 </a> |
435 429 %else:
436 430 <span
437 431 class="tooltip"
438 432 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
439 433 >
440 434 ${_('Show file after')}
441 435 </span> |
442 436 %endif
443 437 <a
444 438 class="tooltip"
445 439 title="${h.tooltip(_('Raw diff'))}"
446 440 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw'))}"
447 441 >
448 442 ${_('Raw diff')}
449 443 </a> |
450 444 <a
451 445 class="tooltip"
452 446 title="${h.tooltip(_('Download diff'))}"
453 447 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download'))}"
454 448 >
455 449 ${_('Download diff')}
456 450 </a>
457 451 % if use_comments:
458 452 |
459 453 % endif
460 454
461 455 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
462 456 %if hasattr(c, 'ignorews_url'):
463 457 ${c.ignorews_url(request, h.FID('', filediff.patch['filename']))}
464 458 %endif
465 459 %if hasattr(c, 'context_url'):
466 460 ${c.context_url(request, h.FID('', filediff.patch['filename']))}
467 461 %endif
468 462
469 463 %if use_comments:
470 464 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
471 465 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
472 466 </a>
473 467 %endif
474 468 %endif
475 469 </div>
476 470 </%def>
477 471
478 472
479 473 <%def name="inline_comments_container(comments)">
480 474 <div class="inline-comments">
481 475 %for comment in comments:
482 476 ${commentblock.comment_block(comment, inline=True)}
483 477 %endfor
484 478
485 479 % if comments and comments[-1].outdated:
486 480 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
487 481 style="display: none;}">
488 482 ${_('Add another comment')}
489 483 </span>
490 484 % else:
491 485 <span onclick="return Rhodecode.comments.createComment(this)"
492 486 class="btn btn-secondary cb-comment-add-button">
493 487 ${_('Add another comment')}
494 488 </span>
495 489 % endif
496 490
497 491 </div>
498 492 </%def>
499 493
500 494
501 495 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
502 496 %for i, line in enumerate(hunk.sideside):
503 497 <%
504 498 old_line_anchor, new_line_anchor = None, None
505 499 if line.original.lineno:
506 500 old_line_anchor = diff_line_anchor(hunk.source_file_path, line.original.lineno, 'o')
507 501 if line.modified.lineno:
508 502 new_line_anchor = diff_line_anchor(hunk.target_file_path, line.modified.lineno, 'n')
509 503 %>
510 504
511 505 <tr class="cb-line">
512 506 <td class="cb-data ${action_class(line.original.action)}"
513 507 data-line-number="${line.original.lineno}"
514 508 >
515 509 <div>
516 510 %if line.original.comments:
517 511 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
518 512 %endif
519 513 </div>
520 514 </td>
521 515 <td class="cb-lineno ${action_class(line.original.action)}"
522 516 data-line-number="${line.original.lineno}"
523 517 %if old_line_anchor:
524 518 id="${old_line_anchor}"
525 519 %endif
526 520 >
527 521 %if line.original.lineno:
528 522 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
529 523 %endif
530 524 </td>
531 525 <td class="cb-content ${action_class(line.original.action)}"
532 526 data-line-number="o${line.original.lineno}"
533 527 >
534 528 %if use_comments and line.original.lineno:
535 529 ${render_add_comment_button()}
536 530 %endif
537 531 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
538 532 %if use_comments and line.original.lineno and line.original.comments:
539 533 ${inline_comments_container(line.original.comments)}
540 534 %endif
541 535 </td>
542 536 <td class="cb-data ${action_class(line.modified.action)}"
543 537 data-line-number="${line.modified.lineno}"
544 538 >
545 539 <div>
546 540 %if line.modified.comments:
547 541 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
548 542 %endif
549 543 </div>
550 544 </td>
551 545 <td class="cb-lineno ${action_class(line.modified.action)}"
552 546 data-line-number="${line.modified.lineno}"
553 547 %if new_line_anchor:
554 548 id="${new_line_anchor}"
555 549 %endif
556 550 >
557 551 %if line.modified.lineno:
558 552 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
559 553 %endif
560 554 </td>
561 555 <td class="cb-content ${action_class(line.modified.action)}"
562 556 data-line-number="n${line.modified.lineno}"
563 557 >
564 558 %if use_comments and line.modified.lineno:
565 559 ${render_add_comment_button()}
566 560 %endif
567 561 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
568 562 %if use_comments and line.modified.lineno and line.modified.comments:
569 563 ${inline_comments_container(line.modified.comments)}
570 564 %endif
571 565 </td>
572 566 </tr>
573 567 %endfor
574 568 </%def>
575 569
576 570
577 571 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
578 572 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
579 573 <%
580 574 old_line_anchor, new_line_anchor = None, None
581 575 if old_line_no:
582 576 old_line_anchor = diff_line_anchor(hunk.source_file_path, old_line_no, 'o')
583 577 if new_line_no:
584 578 new_line_anchor = diff_line_anchor(hunk.target_file_path, new_line_no, 'n')
585 579 %>
586 580 <tr class="cb-line">
587 581 <td class="cb-data ${action_class(action)}">
588 582 <div>
589 583 %if comments:
590 584 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
591 585 %endif
592 586 </div>
593 587 </td>
594 588 <td class="cb-lineno ${action_class(action)}"
595 589 data-line-number="${old_line_no}"
596 590 %if old_line_anchor:
597 591 id="${old_line_anchor}"
598 592 %endif
599 593 >
600 594 %if old_line_anchor:
601 595 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
602 596 %endif
603 597 </td>
604 598 <td class="cb-lineno ${action_class(action)}"
605 599 data-line-number="${new_line_no}"
606 600 %if new_line_anchor:
607 601 id="${new_line_anchor}"
608 602 %endif
609 603 >
610 604 %if new_line_anchor:
611 605 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
612 606 %endif
613 607 </td>
614 608 <td class="cb-content ${action_class(action)}"
615 609 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
616 610 >
617 611 %if use_comments:
618 612 ${render_add_comment_button()}
619 613 %endif
620 614 <span class="cb-code">${action} ${content or '' | n}</span>
621 615 %if use_comments and comments:
622 616 ${inline_comments_container(comments)}
623 617 %endif
624 618 </td>
625 619 </tr>
626 620 %endfor
627 621 </%def>
628 622
629 623 <%def name="render_add_comment_button()">
630 624 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
631 625 <span><i class="icon-comment"></i></span>
632 626 </button>
633 627 </%def>
634 628
635 629 <%def name="render_diffset_menu()">
636 630
637 631 <div class="diffset-menu clearinner">
638 632 <div class="pull-right">
639 633 <div class="btn-group">
640 634
641 635 <a
642 636 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
643 637 title="${h.tooltip(_('View side by side'))}"
644 638 href="${h.url_replace(diffmode='sideside')}">
645 639 <span>${_('Side by Side')}</span>
646 640 </a>
647 641 <a
648 642 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
649 643 title="${h.tooltip(_('View unified'))}" href="${h.url_replace(diffmode='unified')}">
650 644 <span>${_('Unified')}</span>
651 645 </a>
652 646 </div>
653 647 </div>
654 648
655 649 <div class="pull-left">
656 650 <div class="btn-group">
657 651 <a
658 652 class="btn"
659 653 href="#"
660 654 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
661 655 <a
662 656 class="btn"
663 657 href="#"
664 658 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
665 659 <a
666 660 class="btn"
667 661 href="#"
668 662 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
669 663 </div>
670 664 </div>
671 665 </div>
672 666 </%def>
@@ -1,49 +1,49 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.url.current(), method='GET', id='at_rev_form')}
5 ${h.form(h.current_route_path(request), method='GET', id='at_rev_form')}
6 6 <div class="info_box">
7 7 ${h.hidden('refs_filter')}
8 8 <div class="info_box_elem previous">
9 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 11 <div class="info_box_elem">${h.text('at_rev',value=c.commit.revision)}</div>
12 12 <div class="info_box_elem next">
13 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>
14 14 </div>
15 15 </div>
16 16 ${h.end_form()}
17 17
18 18 <div id="search_activate_id" class="search_activate">
19 19 <a class="btn btn-default" id="filter_activate" href="javascript:void(0)">${_('Search File List')}</a>
20 20 </div>
21 21 <div id="search_deactivate_id" class="search_activate hidden">
22 22 <a class="btn btn-default" id="filter_deactivate" href="javascript:void(0)">${_('Close File List')}</a>
23 23 </div>
24 24 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
25 25 <div title="${_('Add New File')}" class="btn btn-primary new-file">
26 26 <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 27 ${_('Add File')}</a>
28 28 </div>
29 29 % endif
30 30 </div>
31 31
32 32 <div class="browser-search">
33 33 <div class="node-filter">
34 34 <div class="node_filter_box hidden" id="node_filter_box_loading" >${_('Loading file list...')}</div>
35 35 <div class="node_filter_box hidden" id="node_filter_box" >
36 36 <div class="node-filter-path">${h.get_last_path_part(c.file)}/</div>
37 37 <div class="node-filter-input">
38 38 <input class="init" type="text" name="filter" size="25" id="node_filter" autocomplete="off">
39 39 </div>
40 40 </div>
41 41 </div>
42 42 </div>
43 43 </div>
44 44 ## file tree is computed from caches, and filled in
45 45 <div id="file-tree">
46 46 ${c.file_tree |n}
47 47 </div>
48 48
49 49 </div>
General Comments 0
You need to be logged in to leave comments. Login now