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