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