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