##// END OF EJS Templates
notifications: move all notifications into polymer for consistency fix #4201
lisaq -
r822:133118c1 default
parent child Browse files
Show More
@@ -1,1978 +1,1992 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 def json_alerts(self):
682 payloads = []
683 messages = flash.pop_messages()
684 if messages:
685 for message in messages:
686 payloads.append({
687 'message': {
688 'message': u'{}'.format(message.message),
689 'level': message.category,
690 'force': 'true'
691 }
692 })
693 return json.dumps(payloads)
694
681 695 flash = Flash()
682 696
683 697 #==============================================================================
684 698 # SCM FILTERS available via h.
685 699 #==============================================================================
686 700 from rhodecode.lib.vcs.utils import author_name, author_email
687 701 from rhodecode.lib.utils2 import credentials_filter, age as _age
688 702 from rhodecode.model.db import User, ChangesetStatus
689 703
690 704 age = _age
691 705 capitalize = lambda x: x.capitalize()
692 706 email = author_email
693 707 short_id = lambda x: x[:12]
694 708 hide_credentials = lambda x: ''.join(credentials_filter(x))
695 709
696 710
697 711 def age_component(datetime_iso, value=None, time_is_local=False):
698 712 title = value or format_date(datetime_iso)
699 713
700 714 # detect if we have a timezone info, otherwise, add it
701 715 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
702 716 tzinfo = '+00:00'
703 717
704 718 if time_is_local:
705 719 tzinfo = time.strftime("+%H:%M",
706 720 time.gmtime(
707 721 (datetime.now() - datetime.utcnow()).seconds + 1
708 722 )
709 723 )
710 724
711 725 return literal(
712 726 '<time class="timeago tooltip" '
713 727 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
714 728 datetime_iso, title, tzinfo))
715 729
716 730
717 731 def _shorten_commit_id(commit_id):
718 732 from rhodecode import CONFIG
719 733 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
720 734 return commit_id[:def_len]
721 735
722 736
723 737 def show_id(commit):
724 738 """
725 739 Configurable function that shows ID
726 740 by default it's r123:fffeeefffeee
727 741
728 742 :param commit: commit instance
729 743 """
730 744 from rhodecode import CONFIG
731 745 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
732 746
733 747 raw_id = _shorten_commit_id(commit.raw_id)
734 748 if show_idx:
735 749 return 'r%s:%s' % (commit.idx, raw_id)
736 750 else:
737 751 return '%s' % (raw_id, )
738 752
739 753
740 754 def format_date(date):
741 755 """
742 756 use a standardized formatting for dates used in RhodeCode
743 757
744 758 :param date: date/datetime object
745 759 :return: formatted date
746 760 """
747 761
748 762 if date:
749 763 _fmt = "%a, %d %b %Y %H:%M:%S"
750 764 return safe_unicode(date.strftime(_fmt))
751 765
752 766 return u""
753 767
754 768
755 769 class _RepoChecker(object):
756 770
757 771 def __init__(self, backend_alias):
758 772 self._backend_alias = backend_alias
759 773
760 774 def __call__(self, repository):
761 775 if hasattr(repository, 'alias'):
762 776 _type = repository.alias
763 777 elif hasattr(repository, 'repo_type'):
764 778 _type = repository.repo_type
765 779 else:
766 780 _type = repository
767 781 return _type == self._backend_alias
768 782
769 783 is_git = _RepoChecker('git')
770 784 is_hg = _RepoChecker('hg')
771 785 is_svn = _RepoChecker('svn')
772 786
773 787
774 788 def get_repo_type_by_name(repo_name):
775 789 repo = Repository.get_by_repo_name(repo_name)
776 790 return repo.repo_type
777 791
778 792
779 793 def is_svn_without_proxy(repository):
780 794 if is_svn(repository):
781 795 from rhodecode.model.settings import VcsSettingsModel
782 796 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
783 797 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
784 798 return False
785 799
786 800
787 801 def discover_user(author):
788 802 """
789 803 Tries to discover RhodeCode User based on the autho string. Author string
790 804 is typically `FirstName LastName <email@address.com>`
791 805 """
792 806
793 807 # if author is already an instance use it for extraction
794 808 if isinstance(author, User):
795 809 return author
796 810
797 811 # Valid email in the attribute passed, see if they're in the system
798 812 _email = author_email(author)
799 813 if _email != '':
800 814 user = User.get_by_email(_email, case_insensitive=True, cache=True)
801 815 if user is not None:
802 816 return user
803 817
804 818 # Maybe it's a username, we try to extract it and fetch by username ?
805 819 _author = author_name(author)
806 820 user = User.get_by_username(_author, case_insensitive=True, cache=True)
807 821 if user is not None:
808 822 return user
809 823
810 824 return None
811 825
812 826
813 827 def email_or_none(author):
814 828 # extract email from the commit string
815 829 _email = author_email(author)
816 830
817 831 # If we have an email, use it, otherwise
818 832 # see if it contains a username we can get an email from
819 833 if _email != '':
820 834 return _email
821 835 else:
822 836 user = User.get_by_username(
823 837 author_name(author), case_insensitive=True, cache=True)
824 838
825 839 if user is not None:
826 840 return user.email
827 841
828 842 # No valid email, not a valid user in the system, none!
829 843 return None
830 844
831 845
832 846 def link_to_user(author, length=0, **kwargs):
833 847 user = discover_user(author)
834 848 # user can be None, but if we have it already it means we can re-use it
835 849 # in the person() function, so we save 1 intensive-query
836 850 if user:
837 851 author = user
838 852
839 853 display_person = person(author, 'username_or_name_or_email')
840 854 if length:
841 855 display_person = shorter(display_person, length)
842 856
843 857 if user:
844 858 return link_to(
845 859 escape(display_person),
846 860 url('user_profile', username=user.username),
847 861 **kwargs)
848 862 else:
849 863 return escape(display_person)
850 864
851 865
852 866 def person(author, show_attr="username_and_name"):
853 867 user = discover_user(author)
854 868 if user:
855 869 return getattr(user, show_attr)
856 870 else:
857 871 _author = author_name(author)
858 872 _email = email(author)
859 873 return _author or _email
860 874
861 875
862 876 def author_string(email):
863 877 if email:
864 878 user = User.get_by_email(email, case_insensitive=True, cache=True)
865 879 if user:
866 880 if user.firstname or user.lastname:
867 881 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
868 882 else:
869 883 return email
870 884 else:
871 885 return email
872 886 else:
873 887 return None
874 888
875 889
876 890 def person_by_id(id_, show_attr="username_and_name"):
877 891 # attr to return from fetched user
878 892 person_getter = lambda usr: getattr(usr, show_attr)
879 893
880 894 #maybe it's an ID ?
881 895 if str(id_).isdigit() or isinstance(id_, int):
882 896 id_ = int(id_)
883 897 user = User.get(id_)
884 898 if user is not None:
885 899 return person_getter(user)
886 900 return id_
887 901
888 902
889 903 def gravatar_with_user(author, show_disabled=False):
890 904 from rhodecode.lib.utils import PartialRenderer
891 905 _render = PartialRenderer('base/base.html')
892 906 return _render('gravatar_with_user', author, show_disabled=show_disabled)
893 907
894 908
895 909 def desc_stylize(value):
896 910 """
897 911 converts tags from value into html equivalent
898 912
899 913 :param value:
900 914 """
901 915 if not value:
902 916 return ''
903 917
904 918 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
905 919 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
906 920 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
907 921 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
908 922 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
909 923 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
910 924 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
911 925 '<div class="metatag" tag="lang">\\2</div>', value)
912 926 value = re.sub(r'\[([a-z]+)\]',
913 927 '<div class="metatag" tag="\\1">\\1</div>', value)
914 928
915 929 return value
916 930
917 931
918 932 def escaped_stylize(value):
919 933 """
920 934 converts tags from value into html equivalent, but escaping its value first
921 935 """
922 936 if not value:
923 937 return ''
924 938
925 939 # Using default webhelper escape method, but has to force it as a
926 940 # plain unicode instead of a markup tag to be used in regex expressions
927 941 value = unicode(escape(safe_unicode(value)))
928 942
929 943 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
930 944 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
931 945 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
932 946 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
933 947 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
934 948 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
935 949 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
936 950 '<div class="metatag" tag="lang">\\2</div>', value)
937 951 value = re.sub(r'\[([a-z]+)\]',
938 952 '<div class="metatag" tag="\\1">\\1</div>', value)
939 953
940 954 return value
941 955
942 956
943 957 def bool2icon(value):
944 958 """
945 959 Returns boolean value of a given value, represented as html element with
946 960 classes that will represent icons
947 961
948 962 :param value: given value to convert to html node
949 963 """
950 964
951 965 if value: # does bool conversion
952 966 return HTML.tag('i', class_="icon-true")
953 967 else: # not true as bool
954 968 return HTML.tag('i', class_="icon-false")
955 969
956 970
957 971 #==============================================================================
958 972 # PERMS
959 973 #==============================================================================
960 974 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
961 975 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
962 976 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
963 977 csrf_token_key
964 978
965 979
966 980 #==============================================================================
967 981 # GRAVATAR URL
968 982 #==============================================================================
969 983 class InitialsGravatar(object):
970 984 def __init__(self, email_address, first_name, last_name, size=30,
971 985 background=None, text_color='#fff'):
972 986 self.size = size
973 987 self.first_name = first_name
974 988 self.last_name = last_name
975 989 self.email_address = email_address
976 990 self.background = background or self.str2color(email_address)
977 991 self.text_color = text_color
978 992
979 993 def get_color_bank(self):
980 994 """
981 995 returns a predefined list of colors that gravatars can use.
982 996 Those are randomized distinct colors that guarantee readability and
983 997 uniqueness.
984 998
985 999 generated with: http://phrogz.net/css/distinct-colors.html
986 1000 """
987 1001 return [
988 1002 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
989 1003 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
990 1004 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
991 1005 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
992 1006 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
993 1007 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
994 1008 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
995 1009 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
996 1010 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
997 1011 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
998 1012 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
999 1013 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1000 1014 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1001 1015 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1002 1016 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1003 1017 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1004 1018 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1005 1019 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1006 1020 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1007 1021 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1008 1022 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1009 1023 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1010 1024 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1011 1025 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1012 1026 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1013 1027 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1014 1028 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1015 1029 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1016 1030 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1017 1031 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1018 1032 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1019 1033 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1020 1034 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1021 1035 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1022 1036 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1023 1037 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1024 1038 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1025 1039 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1026 1040 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1027 1041 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1028 1042 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1029 1043 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1030 1044 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1031 1045 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1032 1046 '#4f8c46', '#368dd9', '#5c0073'
1033 1047 ]
1034 1048
1035 1049 def rgb_to_hex_color(self, rgb_tuple):
1036 1050 """
1037 1051 Converts an rgb_tuple passed to an hex color.
1038 1052
1039 1053 :param rgb_tuple: tuple with 3 ints represents rgb color space
1040 1054 """
1041 1055 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1042 1056
1043 1057 def email_to_int_list(self, email_str):
1044 1058 """
1045 1059 Get every byte of the hex digest value of email and turn it to integer.
1046 1060 It's going to be always between 0-255
1047 1061 """
1048 1062 digest = md5_safe(email_str.lower())
1049 1063 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1050 1064
1051 1065 def pick_color_bank_index(self, email_str, color_bank):
1052 1066 return self.email_to_int_list(email_str)[0] % len(color_bank)
1053 1067
1054 1068 def str2color(self, email_str):
1055 1069 """
1056 1070 Tries to map in a stable algorithm an email to color
1057 1071
1058 1072 :param email_str:
1059 1073 """
1060 1074 color_bank = self.get_color_bank()
1061 1075 # pick position (module it's length so we always find it in the
1062 1076 # bank even if it's smaller than 256 values
1063 1077 pos = self.pick_color_bank_index(email_str, color_bank)
1064 1078 return color_bank[pos]
1065 1079
1066 1080 def normalize_email(self, email_address):
1067 1081 import unicodedata
1068 1082 # default host used to fill in the fake/missing email
1069 1083 default_host = u'localhost'
1070 1084
1071 1085 if not email_address:
1072 1086 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1073 1087
1074 1088 email_address = safe_unicode(email_address)
1075 1089
1076 1090 if u'@' not in email_address:
1077 1091 email_address = u'%s@%s' % (email_address, default_host)
1078 1092
1079 1093 if email_address.endswith(u'@'):
1080 1094 email_address = u'%s%s' % (email_address, default_host)
1081 1095
1082 1096 email_address = unicodedata.normalize('NFKD', email_address)\
1083 1097 .encode('ascii', 'ignore')
1084 1098 return email_address
1085 1099
1086 1100 def get_initials(self):
1087 1101 """
1088 1102 Returns 2 letter initials calculated based on the input.
1089 1103 The algorithm picks first given email address, and takes first letter
1090 1104 of part before @, and then the first letter of server name. In case
1091 1105 the part before @ is in a format of `somestring.somestring2` it replaces
1092 1106 the server letter with first letter of somestring2
1093 1107
1094 1108 In case function was initialized with both first and lastname, this
1095 1109 overrides the extraction from email by first letter of the first and
1096 1110 last name. We add special logic to that functionality, In case Full name
1097 1111 is compound, like Guido Von Rossum, we use last part of the last name
1098 1112 (Von Rossum) picking `R`.
1099 1113
1100 1114 Function also normalizes the non-ascii characters to they ascii
1101 1115 representation, eg Δ„ => A
1102 1116 """
1103 1117 import unicodedata
1104 1118 # replace non-ascii to ascii
1105 1119 first_name = unicodedata.normalize(
1106 1120 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1107 1121 last_name = unicodedata.normalize(
1108 1122 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1109 1123
1110 1124 # do NFKD encoding, and also make sure email has proper format
1111 1125 email_address = self.normalize_email(self.email_address)
1112 1126
1113 1127 # first push the email initials
1114 1128 prefix, server = email_address.split('@', 1)
1115 1129
1116 1130 # check if prefix is maybe a 'firstname.lastname' syntax
1117 1131 _dot_split = prefix.rsplit('.', 1)
1118 1132 if len(_dot_split) == 2:
1119 1133 initials = [_dot_split[0][0], _dot_split[1][0]]
1120 1134 else:
1121 1135 initials = [prefix[0], server[0]]
1122 1136
1123 1137 # then try to replace either firtname or lastname
1124 1138 fn_letter = (first_name or " ")[0].strip()
1125 1139 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1126 1140
1127 1141 if fn_letter:
1128 1142 initials[0] = fn_letter
1129 1143
1130 1144 if ln_letter:
1131 1145 initials[1] = ln_letter
1132 1146
1133 1147 return ''.join(initials).upper()
1134 1148
1135 1149 def get_img_data_by_type(self, font_family, img_type):
1136 1150 default_user = """
1137 1151 <svg xmlns="http://www.w3.org/2000/svg"
1138 1152 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1139 1153 viewBox="-15 -10 439.165 429.164"
1140 1154
1141 1155 xml:space="preserve"
1142 1156 style="background:{background};" >
1143 1157
1144 1158 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1145 1159 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1146 1160 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1147 1161 168.596,153.916,216.671,
1148 1162 204.583,216.671z" fill="{text_color}"/>
1149 1163 <path d="M407.164,374.717L360.88,
1150 1164 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1151 1165 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1152 1166 15.366-44.203,23.488-69.076,23.488c-24.877,
1153 1167 0-48.762-8.122-69.078-23.488
1154 1168 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1155 1169 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1156 1170 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1157 1171 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1158 1172 19.402-10.527 C409.699,390.129,
1159 1173 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1160 1174 </svg>""".format(
1161 1175 size=self.size,
1162 1176 background='#979797', # @grey4
1163 1177 text_color=self.text_color,
1164 1178 font_family=font_family)
1165 1179
1166 1180 return {
1167 1181 "default_user": default_user
1168 1182 }[img_type]
1169 1183
1170 1184 def get_img_data(self, svg_type=None):
1171 1185 """
1172 1186 generates the svg metadata for image
1173 1187 """
1174 1188
1175 1189 font_family = ','.join([
1176 1190 'proximanovaregular',
1177 1191 'Proxima Nova Regular',
1178 1192 'Proxima Nova',
1179 1193 'Arial',
1180 1194 'Lucida Grande',
1181 1195 'sans-serif'
1182 1196 ])
1183 1197 if svg_type:
1184 1198 return self.get_img_data_by_type(font_family, svg_type)
1185 1199
1186 1200 initials = self.get_initials()
1187 1201 img_data = """
1188 1202 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1189 1203 width="{size}" height="{size}"
1190 1204 style="width: 100%; height: 100%; background-color: {background}"
1191 1205 viewBox="0 0 {size} {size}">
1192 1206 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1193 1207 pointer-events="auto" fill="{text_color}"
1194 1208 font-family="{font_family}"
1195 1209 style="font-weight: 400; font-size: {f_size}px;">{text}
1196 1210 </text>
1197 1211 </svg>""".format(
1198 1212 size=self.size,
1199 1213 f_size=self.size/1.85, # scale the text inside the box nicely
1200 1214 background=self.background,
1201 1215 text_color=self.text_color,
1202 1216 text=initials.upper(),
1203 1217 font_family=font_family)
1204 1218
1205 1219 return img_data
1206 1220
1207 1221 def generate_svg(self, svg_type=None):
1208 1222 img_data = self.get_img_data(svg_type)
1209 1223 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1210 1224
1211 1225
1212 1226 def initials_gravatar(email_address, first_name, last_name, size=30):
1213 1227 svg_type = None
1214 1228 if email_address == User.DEFAULT_USER_EMAIL:
1215 1229 svg_type = 'default_user'
1216 1230 klass = InitialsGravatar(email_address, first_name, last_name, size)
1217 1231 return klass.generate_svg(svg_type=svg_type)
1218 1232
1219 1233
1220 1234 def gravatar_url(email_address, size=30):
1221 1235 # doh, we need to re-import those to mock it later
1222 1236 from pylons import tmpl_context as c
1223 1237
1224 1238 _use_gravatar = c.visual.use_gravatar
1225 1239 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1226 1240
1227 1241 email_address = email_address or User.DEFAULT_USER_EMAIL
1228 1242 if isinstance(email_address, unicode):
1229 1243 # hashlib crashes on unicode items
1230 1244 email_address = safe_str(email_address)
1231 1245
1232 1246 # empty email or default user
1233 1247 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1234 1248 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1235 1249
1236 1250 if _use_gravatar:
1237 1251 # TODO: Disuse pyramid thread locals. Think about another solution to
1238 1252 # get the host and schema here.
1239 1253 request = get_current_request()
1240 1254 tmpl = safe_str(_gravatar_url)
1241 1255 tmpl = tmpl.replace('{email}', email_address)\
1242 1256 .replace('{md5email}', md5_safe(email_address.lower())) \
1243 1257 .replace('{netloc}', request.host)\
1244 1258 .replace('{scheme}', request.scheme)\
1245 1259 .replace('{size}', safe_str(size))
1246 1260 return tmpl
1247 1261 else:
1248 1262 return initials_gravatar(email_address, '', '', size=size)
1249 1263
1250 1264
1251 1265 class Page(_Page):
1252 1266 """
1253 1267 Custom pager to match rendering style with paginator
1254 1268 """
1255 1269
1256 1270 def _get_pos(self, cur_page, max_page, items):
1257 1271 edge = (items / 2) + 1
1258 1272 if (cur_page <= edge):
1259 1273 radius = max(items / 2, items - cur_page)
1260 1274 elif (max_page - cur_page) < edge:
1261 1275 radius = (items - 1) - (max_page - cur_page)
1262 1276 else:
1263 1277 radius = items / 2
1264 1278
1265 1279 left = max(1, (cur_page - (radius)))
1266 1280 right = min(max_page, cur_page + (radius))
1267 1281 return left, cur_page, right
1268 1282
1269 1283 def _range(self, regexp_match):
1270 1284 """
1271 1285 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1272 1286
1273 1287 Arguments:
1274 1288
1275 1289 regexp_match
1276 1290 A "re" (regular expressions) match object containing the
1277 1291 radius of linked pages around the current page in
1278 1292 regexp_match.group(1) as a string
1279 1293
1280 1294 This function is supposed to be called as a callable in
1281 1295 re.sub.
1282 1296
1283 1297 """
1284 1298 radius = int(regexp_match.group(1))
1285 1299
1286 1300 # Compute the first and last page number within the radius
1287 1301 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1288 1302 # -> leftmost_page = 5
1289 1303 # -> rightmost_page = 9
1290 1304 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1291 1305 self.last_page,
1292 1306 (radius * 2) + 1)
1293 1307 nav_items = []
1294 1308
1295 1309 # Create a link to the first page (unless we are on the first page
1296 1310 # or there would be no need to insert '..' spacers)
1297 1311 if self.page != self.first_page and self.first_page < leftmost_page:
1298 1312 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1299 1313
1300 1314 # Insert dots if there are pages between the first page
1301 1315 # and the currently displayed page range
1302 1316 if leftmost_page - self.first_page > 1:
1303 1317 # Wrap in a SPAN tag if nolink_attr is set
1304 1318 text = '..'
1305 1319 if self.dotdot_attr:
1306 1320 text = HTML.span(c=text, **self.dotdot_attr)
1307 1321 nav_items.append(text)
1308 1322
1309 1323 for thispage in xrange(leftmost_page, rightmost_page + 1):
1310 1324 # Hilight the current page number and do not use a link
1311 1325 if thispage == self.page:
1312 1326 text = '%s' % (thispage,)
1313 1327 # Wrap in a SPAN tag if nolink_attr is set
1314 1328 if self.curpage_attr:
1315 1329 text = HTML.span(c=text, **self.curpage_attr)
1316 1330 nav_items.append(text)
1317 1331 # Otherwise create just a link to that page
1318 1332 else:
1319 1333 text = '%s' % (thispage,)
1320 1334 nav_items.append(self._pagerlink(thispage, text))
1321 1335
1322 1336 # Insert dots if there are pages between the displayed
1323 1337 # page numbers and the end of the page range
1324 1338 if self.last_page - rightmost_page > 1:
1325 1339 text = '..'
1326 1340 # Wrap in a SPAN tag if nolink_attr is set
1327 1341 if self.dotdot_attr:
1328 1342 text = HTML.span(c=text, **self.dotdot_attr)
1329 1343 nav_items.append(text)
1330 1344
1331 1345 # Create a link to the very last page (unless we are on the last
1332 1346 # page or there would be no need to insert '..' spacers)
1333 1347 if self.page != self.last_page and rightmost_page < self.last_page:
1334 1348 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1335 1349
1336 1350 ## prerender links
1337 1351 #_page_link = url.current()
1338 1352 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1339 1353 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1340 1354 return self.separator.join(nav_items)
1341 1355
1342 1356 def pager(self, format='~2~', page_param='page', partial_param='partial',
1343 1357 show_if_single_page=False, separator=' ', onclick=None,
1344 1358 symbol_first='<<', symbol_last='>>',
1345 1359 symbol_previous='<', symbol_next='>',
1346 1360 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1347 1361 curpage_attr={'class': 'pager_curpage'},
1348 1362 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1349 1363
1350 1364 self.curpage_attr = curpage_attr
1351 1365 self.separator = separator
1352 1366 self.pager_kwargs = kwargs
1353 1367 self.page_param = page_param
1354 1368 self.partial_param = partial_param
1355 1369 self.onclick = onclick
1356 1370 self.link_attr = link_attr
1357 1371 self.dotdot_attr = dotdot_attr
1358 1372
1359 1373 # Don't show navigator if there is no more than one page
1360 1374 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1361 1375 return ''
1362 1376
1363 1377 from string import Template
1364 1378 # Replace ~...~ in token format by range of pages
1365 1379 result = re.sub(r'~(\d+)~', self._range, format)
1366 1380
1367 1381 # Interpolate '%' variables
1368 1382 result = Template(result).safe_substitute({
1369 1383 'first_page': self.first_page,
1370 1384 'last_page': self.last_page,
1371 1385 'page': self.page,
1372 1386 'page_count': self.page_count,
1373 1387 'items_per_page': self.items_per_page,
1374 1388 'first_item': self.first_item,
1375 1389 'last_item': self.last_item,
1376 1390 'item_count': self.item_count,
1377 1391 'link_first': self.page > self.first_page and \
1378 1392 self._pagerlink(self.first_page, symbol_first) or '',
1379 1393 'link_last': self.page < self.last_page and \
1380 1394 self._pagerlink(self.last_page, symbol_last) or '',
1381 1395 'link_previous': self.previous_page and \
1382 1396 self._pagerlink(self.previous_page, symbol_previous) \
1383 1397 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1384 1398 'link_next': self.next_page and \
1385 1399 self._pagerlink(self.next_page, symbol_next) \
1386 1400 or HTML.span(symbol_next, class_="pg-next disabled")
1387 1401 })
1388 1402
1389 1403 return literal(result)
1390 1404
1391 1405
1392 1406 #==============================================================================
1393 1407 # REPO PAGER, PAGER FOR REPOSITORY
1394 1408 #==============================================================================
1395 1409 class RepoPage(Page):
1396 1410
1397 1411 def __init__(self, collection, page=1, items_per_page=20,
1398 1412 item_count=None, url=None, **kwargs):
1399 1413
1400 1414 """Create a "RepoPage" instance. special pager for paging
1401 1415 repository
1402 1416 """
1403 1417 self._url_generator = url
1404 1418
1405 1419 # Safe the kwargs class-wide so they can be used in the pager() method
1406 1420 self.kwargs = kwargs
1407 1421
1408 1422 # Save a reference to the collection
1409 1423 self.original_collection = collection
1410 1424
1411 1425 self.collection = collection
1412 1426
1413 1427 # The self.page is the number of the current page.
1414 1428 # The first page has the number 1!
1415 1429 try:
1416 1430 self.page = int(page) # make it int() if we get it as a string
1417 1431 except (ValueError, TypeError):
1418 1432 self.page = 1
1419 1433
1420 1434 self.items_per_page = items_per_page
1421 1435
1422 1436 # Unless the user tells us how many items the collections has
1423 1437 # we calculate that ourselves.
1424 1438 if item_count is not None:
1425 1439 self.item_count = item_count
1426 1440 else:
1427 1441 self.item_count = len(self.collection)
1428 1442
1429 1443 # Compute the number of the first and last available page
1430 1444 if self.item_count > 0:
1431 1445 self.first_page = 1
1432 1446 self.page_count = int(math.ceil(float(self.item_count) /
1433 1447 self.items_per_page))
1434 1448 self.last_page = self.first_page + self.page_count - 1
1435 1449
1436 1450 # Make sure that the requested page number is the range of
1437 1451 # valid pages
1438 1452 if self.page > self.last_page:
1439 1453 self.page = self.last_page
1440 1454 elif self.page < self.first_page:
1441 1455 self.page = self.first_page
1442 1456
1443 1457 # Note: the number of items on this page can be less than
1444 1458 # items_per_page if the last page is not full
1445 1459 self.first_item = max(0, (self.item_count) - (self.page *
1446 1460 items_per_page))
1447 1461 self.last_item = ((self.item_count - 1) - items_per_page *
1448 1462 (self.page - 1))
1449 1463
1450 1464 self.items = list(self.collection[self.first_item:self.last_item + 1])
1451 1465
1452 1466 # Links to previous and next page
1453 1467 if self.page > self.first_page:
1454 1468 self.previous_page = self.page - 1
1455 1469 else:
1456 1470 self.previous_page = None
1457 1471
1458 1472 if self.page < self.last_page:
1459 1473 self.next_page = self.page + 1
1460 1474 else:
1461 1475 self.next_page = None
1462 1476
1463 1477 # No items available
1464 1478 else:
1465 1479 self.first_page = None
1466 1480 self.page_count = 0
1467 1481 self.last_page = None
1468 1482 self.first_item = None
1469 1483 self.last_item = None
1470 1484 self.previous_page = None
1471 1485 self.next_page = None
1472 1486 self.items = []
1473 1487
1474 1488 # This is a subclass of the 'list' type. Initialise the list now.
1475 1489 list.__init__(self, reversed(self.items))
1476 1490
1477 1491
1478 1492 def changed_tooltip(nodes):
1479 1493 """
1480 1494 Generates a html string for changed nodes in commit page.
1481 1495 It limits the output to 30 entries
1482 1496
1483 1497 :param nodes: LazyNodesGenerator
1484 1498 """
1485 1499 if nodes:
1486 1500 pref = ': <br/> '
1487 1501 suf = ''
1488 1502 if len(nodes) > 30:
1489 1503 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1490 1504 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1491 1505 for x in nodes[:30]]) + suf)
1492 1506 else:
1493 1507 return ': ' + _('No Files')
1494 1508
1495 1509
1496 1510 def breadcrumb_repo_link(repo):
1497 1511 """
1498 1512 Makes a breadcrumbs path link to repo
1499 1513
1500 1514 ex::
1501 1515 group >> subgroup >> repo
1502 1516
1503 1517 :param repo: a Repository instance
1504 1518 """
1505 1519
1506 1520 path = [
1507 1521 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1508 1522 for group in repo.groups_with_parents
1509 1523 ] + [
1510 1524 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1511 1525 ]
1512 1526
1513 1527 return literal(' &raquo; '.join(path))
1514 1528
1515 1529
1516 1530 def format_byte_size_binary(file_size):
1517 1531 """
1518 1532 Formats file/folder sizes to standard.
1519 1533 """
1520 1534 formatted_size = format_byte_size(file_size, binary=True)
1521 1535 return formatted_size
1522 1536
1523 1537
1524 1538 def fancy_file_stats(stats):
1525 1539 """
1526 1540 Displays a fancy two colored bar for number of added/deleted
1527 1541 lines of code on file
1528 1542
1529 1543 :param stats: two element list of added/deleted lines of code
1530 1544 """
1531 1545 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1532 1546 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1533 1547
1534 1548 def cgen(l_type, a_v, d_v):
1535 1549 mapping = {'tr': 'top-right-rounded-corner-mid',
1536 1550 'tl': 'top-left-rounded-corner-mid',
1537 1551 'br': 'bottom-right-rounded-corner-mid',
1538 1552 'bl': 'bottom-left-rounded-corner-mid'}
1539 1553 map_getter = lambda x: mapping[x]
1540 1554
1541 1555 if l_type == 'a' and d_v:
1542 1556 #case when added and deleted are present
1543 1557 return ' '.join(map(map_getter, ['tl', 'bl']))
1544 1558
1545 1559 if l_type == 'a' and not d_v:
1546 1560 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1547 1561
1548 1562 if l_type == 'd' and a_v:
1549 1563 return ' '.join(map(map_getter, ['tr', 'br']))
1550 1564
1551 1565 if l_type == 'd' and not a_v:
1552 1566 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1553 1567
1554 1568 a, d = stats['added'], stats['deleted']
1555 1569 width = 100
1556 1570
1557 1571 if stats['binary']: # binary operations like chmod/rename etc
1558 1572 lbl = []
1559 1573 bin_op = 0 # undefined
1560 1574
1561 1575 # prefix with bin for binary files
1562 1576 if BIN_FILENODE in stats['ops']:
1563 1577 lbl += ['bin']
1564 1578
1565 1579 if NEW_FILENODE in stats['ops']:
1566 1580 lbl += [_('new file')]
1567 1581 bin_op = NEW_FILENODE
1568 1582 elif MOD_FILENODE in stats['ops']:
1569 1583 lbl += [_('mod')]
1570 1584 bin_op = MOD_FILENODE
1571 1585 elif DEL_FILENODE in stats['ops']:
1572 1586 lbl += [_('del')]
1573 1587 bin_op = DEL_FILENODE
1574 1588 elif RENAMED_FILENODE in stats['ops']:
1575 1589 lbl += [_('rename')]
1576 1590 bin_op = RENAMED_FILENODE
1577 1591
1578 1592 # chmod can go with other operations, so we add a + to lbl if needed
1579 1593 if CHMOD_FILENODE in stats['ops']:
1580 1594 lbl += [_('chmod')]
1581 1595 if bin_op == 0:
1582 1596 bin_op = CHMOD_FILENODE
1583 1597
1584 1598 lbl = '+'.join(lbl)
1585 1599 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1586 1600 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1587 1601 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1588 1602 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1589 1603
1590 1604 t = stats['added'] + stats['deleted']
1591 1605 unit = float(width) / (t or 1)
1592 1606
1593 1607 # needs > 9% of width to be visible or 0 to be hidden
1594 1608 a_p = max(9, unit * a) if a > 0 else 0
1595 1609 d_p = max(9, unit * d) if d > 0 else 0
1596 1610 p_sum = a_p + d_p
1597 1611
1598 1612 if p_sum > width:
1599 1613 #adjust the percentage to be == 100% since we adjusted to 9
1600 1614 if a_p > d_p:
1601 1615 a_p = a_p - (p_sum - width)
1602 1616 else:
1603 1617 d_p = d_p - (p_sum - width)
1604 1618
1605 1619 a_v = a if a > 0 else ''
1606 1620 d_v = d if d > 0 else ''
1607 1621
1608 1622 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1609 1623 cgen('a', a_v, d_v), a_p, a_v
1610 1624 )
1611 1625 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1612 1626 cgen('d', a_v, d_v), d_p, d_v
1613 1627 )
1614 1628 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1615 1629
1616 1630
1617 1631 def urlify_text(text_, safe=True):
1618 1632 """
1619 1633 Extrac urls from text and make html links out of them
1620 1634
1621 1635 :param text_:
1622 1636 """
1623 1637
1624 1638 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1625 1639 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1626 1640
1627 1641 def url_func(match_obj):
1628 1642 url_full = match_obj.groups()[0]
1629 1643 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1630 1644 _newtext = url_pat.sub(url_func, text_)
1631 1645 if safe:
1632 1646 return literal(_newtext)
1633 1647 return _newtext
1634 1648
1635 1649
1636 1650 def urlify_commits(text_, repository):
1637 1651 """
1638 1652 Extract commit ids from text and make link from them
1639 1653
1640 1654 :param text_:
1641 1655 :param repository: repo name to build the URL with
1642 1656 """
1643 1657 from pylons import url # doh, we need to re-import url to mock it later
1644 1658 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1645 1659
1646 1660 def url_func(match_obj):
1647 1661 commit_id = match_obj.groups()[1]
1648 1662 pref = match_obj.groups()[0]
1649 1663 suf = match_obj.groups()[2]
1650 1664
1651 1665 tmpl = (
1652 1666 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1653 1667 '%(commit_id)s</a>%(suf)s'
1654 1668 )
1655 1669 return tmpl % {
1656 1670 'pref': pref,
1657 1671 'cls': 'revision-link',
1658 1672 'url': url('changeset_home', repo_name=repository,
1659 1673 revision=commit_id, qualified=True),
1660 1674 'commit_id': commit_id,
1661 1675 'suf': suf
1662 1676 }
1663 1677
1664 1678 newtext = URL_PAT.sub(url_func, text_)
1665 1679
1666 1680 return newtext
1667 1681
1668 1682
1669 1683 def _process_url_func(match_obj, repo_name, uid, entry,
1670 1684 return_raw_data=False):
1671 1685 pref = ''
1672 1686 if match_obj.group().startswith(' '):
1673 1687 pref = ' '
1674 1688
1675 1689 issue_id = ''.join(match_obj.groups())
1676 1690 tmpl = (
1677 1691 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1678 1692 '%(issue-prefix)s%(id-repr)s'
1679 1693 '</a>')
1680 1694
1681 1695 (repo_name_cleaned,
1682 1696 parent_group_name) = RepoGroupModel().\
1683 1697 _get_group_name_and_parent(repo_name)
1684 1698
1685 1699 # variables replacement
1686 1700 named_vars = {
1687 1701 'id': issue_id,
1688 1702 'repo': repo_name,
1689 1703 'repo_name': repo_name_cleaned,
1690 1704 'group_name': parent_group_name
1691 1705 }
1692 1706 # named regex variables
1693 1707 named_vars.update(match_obj.groupdict())
1694 1708 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1695 1709
1696 1710 data = {
1697 1711 'pref': pref,
1698 1712 'cls': 'issue-tracker-link',
1699 1713 'url': _url,
1700 1714 'id-repr': issue_id,
1701 1715 'issue-prefix': entry['pref'],
1702 1716 'serv': entry['url'],
1703 1717 }
1704 1718 if return_raw_data:
1705 1719 return {
1706 1720 'id': issue_id,
1707 1721 'url': _url
1708 1722 }
1709 1723 return tmpl % data
1710 1724
1711 1725
1712 1726 def process_patterns(text_string, repo_name, config=None):
1713 1727 repo = None
1714 1728 if repo_name:
1715 1729 # Retrieving repo_name to avoid invalid repo_name to explode on
1716 1730 # IssueTrackerSettingsModel but still passing invalid name further down
1717 1731 repo = Repository.get_by_repo_name(repo_name, cache=True)
1718 1732
1719 1733 settings_model = IssueTrackerSettingsModel(repo=repo)
1720 1734 active_entries = settings_model.get_settings(cache=True)
1721 1735
1722 1736 issues_data = []
1723 1737 newtext = text_string
1724 1738 for uid, entry in active_entries.items():
1725 1739 log.debug('found issue tracker entry with uid %s' % (uid,))
1726 1740
1727 1741 if not (entry['pat'] and entry['url']):
1728 1742 log.debug('skipping due to missing data')
1729 1743 continue
1730 1744
1731 1745 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1732 1746 % (uid, entry['pat'], entry['url'], entry['pref']))
1733 1747
1734 1748 try:
1735 1749 pattern = re.compile(r'%s' % entry['pat'])
1736 1750 except re.error:
1737 1751 log.exception(
1738 1752 'issue tracker pattern: `%s` failed to compile',
1739 1753 entry['pat'])
1740 1754 continue
1741 1755
1742 1756 data_func = partial(
1743 1757 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1744 1758 return_raw_data=True)
1745 1759
1746 1760 for match_obj in pattern.finditer(text_string):
1747 1761 issues_data.append(data_func(match_obj))
1748 1762
1749 1763 url_func = partial(
1750 1764 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1751 1765
1752 1766 newtext = pattern.sub(url_func, newtext)
1753 1767 log.debug('processed prefix:uid `%s`' % (uid,))
1754 1768
1755 1769 return newtext, issues_data
1756 1770
1757 1771
1758 1772 def urlify_commit_message(commit_text, repository=None):
1759 1773 """
1760 1774 Parses given text message and makes proper links.
1761 1775 issues are linked to given issue-server, and rest is a commit link
1762 1776
1763 1777 :param commit_text:
1764 1778 :param repository:
1765 1779 """
1766 1780 from pylons import url # doh, we need to re-import url to mock it later
1767 1781
1768 1782 def escaper(string):
1769 1783 return string.replace('<', '&lt;').replace('>', '&gt;')
1770 1784
1771 1785 newtext = escaper(commit_text)
1772 1786
1773 1787 # extract http/https links and make them real urls
1774 1788 newtext = urlify_text(newtext, safe=False)
1775 1789
1776 1790 # urlify commits - extract commit ids and make link out of them, if we have
1777 1791 # the scope of repository present.
1778 1792 if repository:
1779 1793 newtext = urlify_commits(newtext, repository)
1780 1794
1781 1795 # process issue tracker patterns
1782 1796 newtext, issues = process_patterns(newtext, repository or '')
1783 1797
1784 1798 return literal(newtext)
1785 1799
1786 1800
1787 1801 def rst(source, mentions=False):
1788 1802 return literal('<div class="rst-block">%s</div>' %
1789 1803 MarkupRenderer.rst(source, mentions=mentions))
1790 1804
1791 1805
1792 1806 def markdown(source, mentions=False):
1793 1807 return literal('<div class="markdown-block">%s</div>' %
1794 1808 MarkupRenderer.markdown(source, flavored=True,
1795 1809 mentions=mentions))
1796 1810
1797 1811 def renderer_from_filename(filename, exclude=None):
1798 1812 return MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1799 1813
1800 1814
1801 1815 def render(source, renderer='rst', mentions=False):
1802 1816 if renderer == 'rst':
1803 1817 return rst(source, mentions=mentions)
1804 1818 if renderer == 'markdown':
1805 1819 return markdown(source, mentions=mentions)
1806 1820
1807 1821
1808 1822 def commit_status(repo, commit_id):
1809 1823 return ChangesetStatusModel().get_status(repo, commit_id)
1810 1824
1811 1825
1812 1826 def commit_status_lbl(commit_status):
1813 1827 return dict(ChangesetStatus.STATUSES).get(commit_status)
1814 1828
1815 1829
1816 1830 def commit_time(repo_name, commit_id):
1817 1831 repo = Repository.get_by_repo_name(repo_name)
1818 1832 commit = repo.get_commit(commit_id=commit_id)
1819 1833 return commit.date
1820 1834
1821 1835
1822 1836 def get_permission_name(key):
1823 1837 return dict(Permission.PERMS).get(key)
1824 1838
1825 1839
1826 1840 def journal_filter_help():
1827 1841 return _(
1828 1842 'Example filter terms:\n' +
1829 1843 ' repository:vcs\n' +
1830 1844 ' username:marcin\n' +
1831 1845 ' action:*push*\n' +
1832 1846 ' ip:127.0.0.1\n' +
1833 1847 ' date:20120101\n' +
1834 1848 ' date:[20120101100000 TO 20120102]\n' +
1835 1849 '\n' +
1836 1850 'Generate wildcards using \'*\' character:\n' +
1837 1851 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1838 1852 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1839 1853 '\n' +
1840 1854 'Optional AND / OR operators in queries\n' +
1841 1855 ' "repository:vcs OR repository:test"\n' +
1842 1856 ' "username:test AND repository:test*"\n'
1843 1857 )
1844 1858
1845 1859
1846 1860 def not_mapped_error(repo_name):
1847 1861 flash(_('%s repository is not mapped to db perhaps'
1848 1862 ' it was created or renamed from the filesystem'
1849 1863 ' please run the application again'
1850 1864 ' in order to rescan repositories') % repo_name, category='error')
1851 1865
1852 1866
1853 1867 def ip_range(ip_addr):
1854 1868 from rhodecode.model.db import UserIpMap
1855 1869 s, e = UserIpMap._get_ip_range(ip_addr)
1856 1870 return '%s - %s' % (s, e)
1857 1871
1858 1872
1859 1873 def form(url, method='post', needs_csrf_token=True, **attrs):
1860 1874 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1861 1875 if method.lower() != 'get' and needs_csrf_token:
1862 1876 raise Exception(
1863 1877 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1864 1878 'CSRF token. If the endpoint does not require such token you can ' +
1865 1879 'explicitly set the parameter needs_csrf_token to false.')
1866 1880
1867 1881 return wh_form(url, method=method, **attrs)
1868 1882
1869 1883
1870 1884 def secure_form(url, method="POST", multipart=False, **attrs):
1871 1885 """Start a form tag that points the action to an url. This
1872 1886 form tag will also include the hidden field containing
1873 1887 the auth token.
1874 1888
1875 1889 The url options should be given either as a string, or as a
1876 1890 ``url()`` function. The method for the form defaults to POST.
1877 1891
1878 1892 Options:
1879 1893
1880 1894 ``multipart``
1881 1895 If set to True, the enctype is set to "multipart/form-data".
1882 1896 ``method``
1883 1897 The method to use when submitting the form, usually either
1884 1898 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1885 1899 hidden input with name _method is added to simulate the verb
1886 1900 over POST.
1887 1901
1888 1902 """
1889 1903 from webhelpers.pylonslib.secure_form import insecure_form
1890 1904 form = insecure_form(url, method, multipart, **attrs)
1891 1905 token = csrf_input()
1892 1906 return literal("%s\n%s" % (form, token))
1893 1907
1894 1908 def csrf_input():
1895 1909 return literal(
1896 1910 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1897 1911 csrf_token_key, csrf_token_key, get_csrf_token()))
1898 1912
1899 1913 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1900 1914 select_html = select(name, selected, options, **attrs)
1901 1915 select2 = """
1902 1916 <script>
1903 1917 $(document).ready(function() {
1904 1918 $('#%s').select2({
1905 1919 containerCssClass: 'drop-menu',
1906 1920 dropdownCssClass: 'drop-menu-dropdown',
1907 1921 dropdownAutoWidth: true%s
1908 1922 });
1909 1923 });
1910 1924 </script>
1911 1925 """
1912 1926 filter_option = """,
1913 1927 minimumResultsForSearch: -1
1914 1928 """
1915 1929 input_id = attrs.get('id') or name
1916 1930 filter_enabled = "" if enable_filter else filter_option
1917 1931 select_script = literal(select2 % (input_id, filter_enabled))
1918 1932
1919 1933 return literal(select_html+select_script)
1920 1934
1921 1935
1922 1936 def get_visual_attr(tmpl_context_var, attr_name):
1923 1937 """
1924 1938 A safe way to get a variable from visual variable of template context
1925 1939
1926 1940 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1927 1941 :param attr_name: name of the attribute we fetch from the c.visual
1928 1942 """
1929 1943 visual = getattr(tmpl_context_var, 'visual', None)
1930 1944 if not visual:
1931 1945 return
1932 1946 else:
1933 1947 return getattr(visual, attr_name, None)
1934 1948
1935 1949
1936 1950 def get_last_path_part(file_node):
1937 1951 if not file_node.path:
1938 1952 return u''
1939 1953
1940 1954 path = safe_unicode(file_node.path.split('/')[-1])
1941 1955 return u'../' + path
1942 1956
1943 1957
1944 1958 def route_path(*args, **kwds):
1945 1959 """
1946 1960 Wrapper around pyramids `route_path` function. It is used to generate
1947 1961 URLs from within pylons views or templates. This will be removed when
1948 1962 pyramid migration if finished.
1949 1963 """
1950 1964 req = get_current_request()
1951 1965 return req.route_path(*args, **kwds)
1952 1966
1953 1967
1954 1968 def route_path_or_none(*args, **kwargs):
1955 1969 try:
1956 1970 return route_path(*args, **kwargs)
1957 1971 except KeyError:
1958 1972 return None
1959 1973
1960 1974
1961 1975 def static_url(*args, **kwds):
1962 1976 """
1963 1977 Wrapper around pyramids `route_path` function. It is used to generate
1964 1978 URLs from within pylons views or templates. This will be removed when
1965 1979 pyramid migration if finished.
1966 1980 """
1967 1981 req = get_current_request()
1968 1982 return req.static_url(*args, **kwds)
1969 1983
1970 1984
1971 1985 def resource_path(*args, **kwds):
1972 1986 """
1973 1987 Wrapper around pyramids `route_path` function. It is used to generate
1974 1988 URLs from within pylons views or templates. This will be removed when
1975 1989 pyramid migration if finished.
1976 1990 """
1977 1991 req = get_current_request()
1978 1992 return req.resource_path(*args, **kwds)
@@ -1,129 +1,133 b''
1 1 ccLog = Logger.get('RhodeCodeApp');
2 2 ccLog.setLevel(Logger.OFF);
3 3
4 4 var rhodeCodeApp = Polymer({
5 5 is: 'rhodecode-app',
6 created: function () {
6 attached: function () {
7 7 ccLog.debug('rhodeCodeApp created');
8 8 $.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
9 9
10 for (var i = 0; i < alertMessagePayloads.length; i++) {
11 $.Topic('/notifications').publish(alertMessagePayloads[i]);
12 }
13
10 14 $.Topic('/plugins/__REGISTER__').subscribe(
11 15 this.kickoffChannelstreamPlugin.bind(this)
12 16 );
13 17
14 18 $.Topic('/connection_controller/subscribe').subscribe(
15 19 this.subscribeToChannelTopic.bind(this));
16 20 },
17 21
18 22 /** proxy to channelstream connection */
19 23 getChannelStreamConnection: function () {
20 24 return this.$['channelstream-connection'];
21 25 },
22 26
23 27 handleNotifications: function (data) {
24 28 this.$['notifications'].handleNotification(data);
25 29 },
26 30
27 31 /** opens connection to ws server */
28 32 kickoffChannelstreamPlugin: function (data) {
29 33 ccLog.debug('kickoffChannelstreamPlugin');
30 34 var channels = ['broadcast'];
31 35 var addChannels = this.checkViewChannels();
32 36 for (var i = 0; i < addChannels.length; i++) {
33 37 channels.push(addChannels[i]);
34 38 }
35 39 if (window.CHANNELSTREAM_SETTINGS && CHANNELSTREAM_SETTINGS.enabled){
36 40 var channelstreamConnection = this.$['channelstream-connection'];
37 41 channelstreamConnection.connectUrl = CHANNELSTREAM_URLS.connect;
38 42 channelstreamConnection.subscribeUrl = CHANNELSTREAM_URLS.subscribe;
39 43 channelstreamConnection.websocketUrl = CHANNELSTREAM_URLS.ws + '/ws';
40 44 channelstreamConnection.longPollUrl = CHANNELSTREAM_URLS.longpoll + '/listen';
41 45 // some channels might already be registered by topic
42 46 for (var i = 0; i < channels.length; i++) {
43 47 channelstreamConnection.push('channels', channels[i]);
44 48 }
45 49 // append any additional channels registered in other plugins
46 50 $.Topic('/connection_controller/subscribe').processPrepared();
47 51 channelstreamConnection.connect();
48 52 }
49 53 },
50 54
51 55 checkViewChannels: function () {
52 56 var channels = []
53 57 // subscribe to PR repo channel for PR's'
54 58 if (templateContext.pull_request_data.pull_request_id) {
55 59 var channelName = '/repo$' + templateContext.repo_name + '$/pr/' +
56 60 String(templateContext.pull_request_data.pull_request_id);
57 61 channels.push(channelName);
58 62 }
59 63 return channels;
60 64 },
61 65
62 66 /** subscribes users from channels in channelstream */
63 67 subscribeToChannelTopic: function (channels) {
64 68 var channelstreamConnection = this.$['channelstream-connection'];
65 69 var toSubscribe = channelstreamConnection.calculateSubscribe(channels);
66 70 ccLog.debug('subscribeToChannelTopic', toSubscribe);
67 71 if (toSubscribe.length > 0) {
68 72 // if we are connected then subscribe
69 73 if (channelstreamConnection.connected) {
70 74 channelstreamConnection.subscribe(toSubscribe);
71 75 }
72 76 // not connected? just push channels onto the stack
73 77 else {
74 78 for (var i = 0; i < toSubscribe.length; i++) {
75 79 channelstreamConnection.push('channels', toSubscribe[i]);
76 80 }
77 81 }
78 82 }
79 83 },
80 84
81 85 /** publish received messages into correct topic */
82 86 receivedMessage: function (event) {
83 87 for (var i = 0; i < event.detail.length; i++) {
84 88 var message = event.detail[i];
85 89 if (message.message.topic) {
86 90 ccLog.debug('publishing', message.message.topic);
87 91 $.Topic(message.message.topic).publish(message);
88 92 }
89 93 else if (message.type === 'presence'){
90 94 $.Topic('/connection_controller/presence').publish(message);
91 95 }
92 96 else {
93 97 ccLog.warn('unhandled message', message);
94 98 }
95 99 }
96 100 },
97 101
98 102 handleConnected: function (event) {
99 103 var channelstreamConnection = this.$['channelstream-connection'];
100 104 channelstreamConnection.set('channelsState',
101 105 event.detail.channels_info);
102 106 channelstreamConnection.set('userState', event.detail.state);
103 107 channelstreamConnection.set('channels', event.detail.channels);
104 108 this.propagageChannelsState();
105 109 },
106 110 handleSubscribed: function (event) {
107 111 var channelstreamConnection = this.$['channelstream-connection'];
108 112 var channelInfo = event.detail.channels_info;
109 113 var channelKeys = Object.keys(event.detail.channels_info);
110 114 for (var i = 0; i < channelKeys.length; i++) {
111 115 var key = channelKeys[i];
112 116 channelstreamConnection.set(['channelsState', key], channelInfo[key]);
113 117 }
114 118 channelstreamConnection.set('channels', event.detail.channels);
115 119 this.propagageChannelsState();
116 120 },
117 121 /** propagates channel states on topics */
118 122 propagageChannelsState: function (event) {
119 123 var channelstreamConnection = this.$['channelstream-connection'];
120 124 var channel_data = channelstreamConnection.channelsState;
121 125 var channels = channelstreamConnection.channels;
122 126 for (var i = 0; i < channels.length; i++) {
123 127 var key = channels[i];
124 128 $.Topic('/connection_controller/channel_update').publish(
125 129 {channel: key, state: channel_data[key]}
126 130 );
127 131 }
128 132 }
129 133 });
@@ -1,30 +1,30 b''
1 1 <link rel="import" href="../../../../../../bower_components/paper-button/paper-button.html">
2 2 <link rel="import" href="../../../../../../bower_components/paper-toast/paper-toast.html">
3 3 <link rel="import" href="../rhodecode-unsafe-html/rhodecode-unsafe-html.html">
4 4 <dom-module id="rhodecode-toast">
5 5 <template>
6 6 <style include="shared-styles"></style>
7 7 <link rel="stylesheet" href="rhodecode-toast.css">
8 8
9 9 <paper-toast id="p-toast"
10 10 duration="0"
11 11 horizontal-offset="100"
12 12 horizontal-align="auto"
13 13 vertical-align="top"
14 14 on-iron-overlay-closed="handleClosed"
15 15 always-on-top>
16 16 <div class="toast-message-holder">
17 17 <template is="dom-repeat" items="{{toasts}}">
18 18 <div class$="alert alert-{{item.level}}">
19 19 <rhodecode-unsafe-html text="{{item.message}}"></rhodecode-unsafe-html>
20 20 </div>
21 21 </template>
22 22 </div>
23 23 <div class="toast-close">
24 <button on-tap="dismissNotifications" class="btn btn-default">{{_gettext('Close now')}}</button>
24 <button on-tap="dismissNotifications" class="btn btn-default">{{_gettext('Close')}}</button>
25 25 </div>
26 26 </paper-toast>
27 27 </template>
28 28
29 29 <script src="rhodecode-toast.js"></script>
30 30 </dom-module>
@@ -1,16 +0,0 b''
1 <div class="flash_msg">
2 <% messages = h.flash.pop_messages() %>
3 % if messages:
4 % for message in messages:
5 <div class="alert alert-dismissable alert-${message.category}">
6 <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
7 ${message}
8 </div>
9 % endfor
10 % endif
11 <script>
12 if (typeof jQuery != 'undefined') {
13 $(".alert").alert();
14 }
15 </script>
16 </div>
@@ -1,181 +1,182 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <!DOCTYPE html>
3 3
4 4 <%
5 5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
6 6
7 7 if hasattr(c, 'rhodecode_db_repo'):
8 8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
10 10
11 11 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
12 12 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
13 13 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
14 14 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
15 15
16 16 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
17 17 %>
18 18 <html xmlns="http://www.w3.org/1999/xhtml">
19 19 <head>
20 20 <title>${self.title()}</title>
21 21 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
22 22 <%def name="robots()">
23 23 <meta name="robots" content="index, nofollow"/>
24 24 </%def>
25 25 ${self.robots()}
26 26 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
27 27
28 28 ## CSS definitions
29 29 <%def name="css()">
30 30 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
31 31 <!--[if lt IE 9]>
32 32 <link rel="stylesheet" type="text/css" href="${h.asset('css/ie.css', ver=c.rhodecode_version_hash)}" media="screen"/>
33 33 <![endif]-->
34 34 ## EXTRA FOR CSS
35 35 ${self.css_extra()}
36 36 </%def>
37 37 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
38 38 <%def name="css_extra()">
39 39 </%def>
40 40
41 41 ${self.css()}
42 42
43 43 ## JAVASCRIPT
44 44 <%def name="js()">
45 45 <script>
46 46 // setup Polymer options
47 47 window.Polymer = {lazyRegister: true, dom: 'shadow'};
48 48
49 49 // Load webcomponentsjs polyfill if browser does not support native Web Components
50 50 (function() {
51 51 'use strict';
52 52 var onload = function() {
53 53 // For native Imports, manually fire WebComponentsReady so user code
54 54 // can use the same code path for native and polyfill'd imports.
55 55 if (!window.HTMLImports) {
56 56 document.dispatchEvent(
57 57 new CustomEvent('WebComponentsReady', {bubbles: true})
58 58 );
59 59 }
60 60 };
61 61 var webComponentsSupported = (
62 62 'registerElement' in document
63 63 && 'import' in document.createElement('link')
64 64 && 'content' in document.createElement('template')
65 65 );
66 66 if (!webComponentsSupported) {
67 67 var e = document.createElement('script');
68 68 e.async = true;
69 69 e.src = '${h.asset('js/vendors/webcomponentsjs/webcomponents-lite.min.js', ver=c.rhodecode_version_hash)}';
70 70 document.head.appendChild(e);
71 71 } else {
72 72 onload();
73 73 }
74 74 })();
75 75 </script>
76 76
77 77 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
78 78 <script type="text/javascript">
79 79 // register templateContext to pass template variables to JS
80 80 var templateContext = ${h.json.dumps(c.template_context)|n};
81 81
82 82 var REPO_NAME = "${getattr(c, 'repo_name', '')}";
83 83 %if hasattr(c, 'rhodecode_db_repo'):
84 84 var REPO_LANDING_REV = '${c.rhodecode_db_repo.landing_rev[1]}';
85 85 var REPO_TYPE = '${c.rhodecode_db_repo.repo_type}';
86 86 %else:
87 87 var REPO_LANDING_REV = '';
88 88 var REPO_TYPE = '';
89 89 %endif
90 90 var APPLICATION_URL = "${h.url('home').rstrip('/')}";
91 91 var ASSET_URL = "${h.asset('')}";
92 92 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
93 93 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
94 94 % if getattr(c, 'rhodecode_user', None):
95 95 var USER = {name:'${c.rhodecode_user.username}'};
96 96 % else:
97 97 var USER = {name:null};
98 98 % endif
99 99
100 100 var APPENLIGHT = {
101 101 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
102 102 key: '${getattr(c, "appenlight_api_public_key", "")}',
103 103 % if getattr(c, 'appenlight_server_url', None):
104 104 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
105 105 % endif
106 106 requestInfo: {
107 107 % if getattr(c, 'rhodecode_user', None):
108 108 ip: '${c.rhodecode_user.ip_addr}',
109 109 username: '${c.rhodecode_user.username}'
110 110 % endif
111 111 }
112 112 };
113 113 </script>
114 114 <%include file="/base/plugins_base.html"/>
115 115 <!--[if lt IE 9]>
116 116 <script language="javascript" type="text/javascript" src="${h.asset('js/excanvas.min.js')}"></script>
117 117 <![endif]-->
118 118 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
119 <script> var alertMessagePayloads = ${h.flash.json_alerts()|n}; </script>
119 120 <script language="javascript" type="text/javascript" src="${h.asset('js/scripts.js', ver=c.rhodecode_version_hash)}"></script>
120 121 <script>
121 122 var e = document.createElement('script');
122 123 e.src = '${h.asset('js/rhodecode-components.js', ver=c.rhodecode_version_hash)}';
123 124 document.head.appendChild(e);
124 125 </script>
125 126 <link rel="import" href="${h.asset('js/rhodecode-components.html', ver=c.rhodecode_version_hash)}">
126 127 ## avoide escaping the %N
127 128 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
128 129
129 130
130 131 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
131 132 ${self.js_extra()}
132 133
133 134 <script type="text/javascript">
134 135 $(document).ready(function(){
135 136 show_more_event();
136 137 timeagoActivate();
137 138 })
138 139 </script>
139 140
140 141 </%def>
141 142
142 143 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
143 144 <%def name="js_extra()"></%def>
144 145 ${self.js()}
145 146
146 147 <%def name="head_extra()"></%def>
147 148 ${self.head_extra()}
148 149 <script>
149 150 window.addEventListener("load", function(event) {
150 151 $.Topic('/plugins/__REGISTER__').prepareOrPublish({});
151 152 });
152 153 </script>
153 154 ## extra stuff
154 155 %if c.pre_code:
155 156 ${c.pre_code|n}
156 157 %endif
157 158 </head>
158 159 <body id="body">
159 160 <noscript>
160 161 <div class="noscript-error">
161 162 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
162 163 </div>
163 164 </noscript>
164 165 ## IE hacks
165 166 <!--[if IE 7]>
166 167 <script>$(document.body).addClass('ie7')</script>
167 168 <![endif]-->
168 169 <!--[if IE 8]>
169 170 <script>$(document.body).addClass('ie8')</script>
170 171 <![endif]-->
171 172 <!--[if IE 9]>
172 173 <script>$(document.body).addClass('ie9')</script>
173 174 <![endif]-->
174 175
175 176 ${next.body()}
176 177 %if c.post_code:
177 178 ${c.post_code|n}
178 179 %endif
179 180 <rhodecode-app></rhodecode-app>
180 181 </body>
181 182 </html>
General Comments 0
You need to be logged in to leave comments. Login now