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