##// END OF EJS Templates
helpers: remove bad warning calls introduced in commit c8bec9b70fb9
marcink -
r416:502b7879 default
parent child Browse files
Show More
@@ -1,1933 +1,1931 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))
793 791
794 792 # If we have an email, use it, otherwise
795 793 # see if it contains a username we can get an email from
796 794 if _email != '':
797 795 return _email
798 796 else:
799 user = User.get_by_username(author_name(author), case_insensitive=True,
800 cache=True)
797 user = User.get_by_username(
798 author_name(author), case_insensitive=True, cache=True)
801 799
802 800 if user is not None:
803 801 return user.email
804 802
805 803 # No valid email, not a valid user in the system, none!
806 804 return None
807 805
808 806
809 807 def link_to_user(author, length=0, **kwargs):
810 808 user = discover_user(author)
811 809 # user can be None, but if we have it already it means we can re-use it
812 810 # in the person() function, so we save 1 intensive-query
813 811 if user:
814 812 author = user
815 813
816 814 display_person = person(author, 'username_or_name_or_email')
817 815 if length:
818 816 display_person = shorter(display_person, length)
819 817
820 818 if user:
821 819 return link_to(
822 820 escape(display_person),
823 821 url('user_profile', username=user.username),
824 822 **kwargs)
825 823 else:
826 824 return escape(display_person)
827 825
828 826
829 827 def person(author, show_attr="username_and_name"):
830 828 user = discover_user(author)
831 829 if user:
832 830 return getattr(user, show_attr)
833 831 else:
834 832 _author = author_name(author)
835 833 _email = email(author)
836 834 return _author or _email
837 835
838 836
839 837 def author_string(email):
840 838 if email:
841 839 user = User.get_by_email(email, case_insensitive=True, cache=True)
842 840 if user:
843 841 if user.firstname or user.lastname:
844 842 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
845 843 else:
846 844 return email
847 845 else:
848 846 return email
849 847 else:
850 848 return None
851 849
852 850
853 851 def person_by_id(id_, show_attr="username_and_name"):
854 852 # attr to return from fetched user
855 853 person_getter = lambda usr: getattr(usr, show_attr)
856 854
857 855 #maybe it's an ID ?
858 856 if str(id_).isdigit() or isinstance(id_, int):
859 857 id_ = int(id_)
860 858 user = User.get(id_)
861 859 if user is not None:
862 860 return person_getter(user)
863 861 return id_
864 862
865 863
866 864 def gravatar_with_user(author, show_disabled=False):
867 865 from rhodecode.lib.utils import PartialRenderer
868 866 _render = PartialRenderer('base/base.html')
869 867 return _render('gravatar_with_user', author, show_disabled=show_disabled)
870 868
871 869
872 870 def desc_stylize(value):
873 871 """
874 872 converts tags from value into html equivalent
875 873
876 874 :param value:
877 875 """
878 876 if not value:
879 877 return ''
880 878
881 879 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
882 880 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
883 881 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
884 882 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
885 883 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
886 884 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
887 885 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
888 886 '<div class="metatag" tag="lang">\\2</div>', value)
889 887 value = re.sub(r'\[([a-z]+)\]',
890 888 '<div class="metatag" tag="\\1">\\1</div>', value)
891 889
892 890 return value
893 891
894 892
895 893 def escaped_stylize(value):
896 894 """
897 895 converts tags from value into html equivalent, but escaping its value first
898 896 """
899 897 if not value:
900 898 return ''
901 899
902 900 # Using default webhelper escape method, but has to force it as a
903 901 # plain unicode instead of a markup tag to be used in regex expressions
904 902 value = unicode(escape(safe_unicode(value)))
905 903
906 904 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
907 905 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
908 906 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
909 907 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
910 908 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
911 909 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
912 910 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
913 911 '<div class="metatag" tag="lang">\\2</div>', value)
914 912 value = re.sub(r'\[([a-z]+)\]',
915 913 '<div class="metatag" tag="\\1">\\1</div>', value)
916 914
917 915 return value
918 916
919 917
920 918 def bool2icon(value):
921 919 """
922 920 Returns boolean value of a given value, represented as html element with
923 921 classes that will represent icons
924 922
925 923 :param value: given value to convert to html node
926 924 """
927 925
928 926 if value: # does bool conversion
929 927 return HTML.tag('i', class_="icon-true")
930 928 else: # not true as bool
931 929 return HTML.tag('i', class_="icon-false")
932 930
933 931
934 932 #==============================================================================
935 933 # PERMS
936 934 #==============================================================================
937 935 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
938 936 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
939 937 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token
940 938
941 939
942 940 #==============================================================================
943 941 # GRAVATAR URL
944 942 #==============================================================================
945 943 class InitialsGravatar(object):
946 944 def __init__(self, email_address, first_name, last_name, size=30,
947 945 background=None, text_color='#fff'):
948 946 self.size = size
949 947 self.first_name = first_name
950 948 self.last_name = last_name
951 949 self.email_address = email_address
952 950 self.background = background or self.str2color(email_address)
953 951 self.text_color = text_color
954 952
955 953 def get_color_bank(self):
956 954 """
957 955 returns a predefined list of colors that gravatars can use.
958 956 Those are randomized distinct colors that guarantee readability and
959 957 uniqueness.
960 958
961 959 generated with: http://phrogz.net/css/distinct-colors.html
962 960 """
963 961 return [
964 962 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
965 963 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
966 964 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
967 965 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
968 966 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
969 967 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
970 968 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
971 969 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
972 970 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
973 971 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
974 972 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
975 973 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
976 974 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
977 975 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
978 976 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
979 977 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
980 978 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
981 979 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
982 980 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
983 981 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
984 982 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
985 983 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
986 984 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
987 985 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
988 986 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
989 987 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
990 988 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
991 989 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
992 990 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
993 991 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
994 992 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
995 993 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
996 994 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
997 995 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
998 996 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
999 997 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1000 998 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1001 999 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1002 1000 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1003 1001 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1004 1002 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1005 1003 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1006 1004 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1007 1005 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1008 1006 '#4f8c46', '#368dd9', '#5c0073'
1009 1007 ]
1010 1008
1011 1009 def rgb_to_hex_color(self, rgb_tuple):
1012 1010 """
1013 1011 Converts an rgb_tuple passed to an hex color.
1014 1012
1015 1013 :param rgb_tuple: tuple with 3 ints represents rgb color space
1016 1014 """
1017 1015 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1018 1016
1019 1017 def email_to_int_list(self, email_str):
1020 1018 """
1021 1019 Get every byte of the hex digest value of email and turn it to integer.
1022 1020 It's going to be always between 0-255
1023 1021 """
1024 1022 digest = md5_safe(email_str.lower())
1025 1023 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1026 1024
1027 1025 def pick_color_bank_index(self, email_str, color_bank):
1028 1026 return self.email_to_int_list(email_str)[0] % len(color_bank)
1029 1027
1030 1028 def str2color(self, email_str):
1031 1029 """
1032 1030 Tries to map in a stable algorithm an email to color
1033 1031
1034 1032 :param email_str:
1035 1033 """
1036 1034 color_bank = self.get_color_bank()
1037 1035 # pick position (module it's length so we always find it in the
1038 1036 # bank even if it's smaller than 256 values
1039 1037 pos = self.pick_color_bank_index(email_str, color_bank)
1040 1038 return color_bank[pos]
1041 1039
1042 1040 def normalize_email(self, email_address):
1043 1041 import unicodedata
1044 1042 # default host used to fill in the fake/missing email
1045 1043 default_host = u'localhost'
1046 1044
1047 1045 if not email_address:
1048 1046 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1049 1047
1050 1048 email_address = safe_unicode(email_address)
1051 1049
1052 1050 if u'@' not in email_address:
1053 1051 email_address = u'%s@%s' % (email_address, default_host)
1054 1052
1055 1053 if email_address.endswith(u'@'):
1056 1054 email_address = u'%s%s' % (email_address, default_host)
1057 1055
1058 1056 email_address = unicodedata.normalize('NFKD', email_address)\
1059 1057 .encode('ascii', 'ignore')
1060 1058 return email_address
1061 1059
1062 1060 def get_initials(self):
1063 1061 """
1064 1062 Returns 2 letter initials calculated based on the input.
1065 1063 The algorithm picks first given email address, and takes first letter
1066 1064 of part before @, and then the first letter of server name. In case
1067 1065 the part before @ is in a format of `somestring.somestring2` it replaces
1068 1066 the server letter with first letter of somestring2
1069 1067
1070 1068 In case function was initialized with both first and lastname, this
1071 1069 overrides the extraction from email by first letter of the first and
1072 1070 last name. We add special logic to that functionality, In case Full name
1073 1071 is compound, like Guido Von Rossum, we use last part of the last name
1074 1072 (Von Rossum) picking `R`.
1075 1073
1076 1074 Function also normalizes the non-ascii characters to they ascii
1077 1075 representation, eg Δ„ => A
1078 1076 """
1079 1077 import unicodedata
1080 1078 # replace non-ascii to ascii
1081 1079 first_name = unicodedata.normalize(
1082 1080 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1083 1081 last_name = unicodedata.normalize(
1084 1082 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1085 1083
1086 1084 # do NFKD encoding, and also make sure email has proper format
1087 1085 email_address = self.normalize_email(self.email_address)
1088 1086
1089 1087 # first push the email initials
1090 1088 prefix, server = email_address.split('@', 1)
1091 1089
1092 1090 # check if prefix is maybe a 'firstname.lastname' syntax
1093 1091 _dot_split = prefix.rsplit('.', 1)
1094 1092 if len(_dot_split) == 2:
1095 1093 initials = [_dot_split[0][0], _dot_split[1][0]]
1096 1094 else:
1097 1095 initials = [prefix[0], server[0]]
1098 1096
1099 1097 # then try to replace either firtname or lastname
1100 1098 fn_letter = (first_name or " ")[0].strip()
1101 1099 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1102 1100
1103 1101 if fn_letter:
1104 1102 initials[0] = fn_letter
1105 1103
1106 1104 if ln_letter:
1107 1105 initials[1] = ln_letter
1108 1106
1109 1107 return ''.join(initials).upper()
1110 1108
1111 1109 def get_img_data_by_type(self, font_family, img_type):
1112 1110 default_user = """
1113 1111 <svg xmlns="http://www.w3.org/2000/svg"
1114 1112 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1115 1113 viewBox="-15 -10 439.165 429.164"
1116 1114
1117 1115 xml:space="preserve"
1118 1116 style="background:{background};" >
1119 1117
1120 1118 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1121 1119 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1122 1120 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1123 1121 168.596,153.916,216.671,
1124 1122 204.583,216.671z" fill="{text_color}"/>
1125 1123 <path d="M407.164,374.717L360.88,
1126 1124 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1127 1125 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1128 1126 15.366-44.203,23.488-69.076,23.488c-24.877,
1129 1127 0-48.762-8.122-69.078-23.488
1130 1128 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1131 1129 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1132 1130 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1133 1131 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1134 1132 19.402-10.527 C409.699,390.129,
1135 1133 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1136 1134 </svg>""".format(
1137 1135 size=self.size,
1138 1136 background='#979797', # @grey4
1139 1137 text_color=self.text_color,
1140 1138 font_family=font_family)
1141 1139
1142 1140 return {
1143 1141 "default_user": default_user
1144 1142 }[img_type]
1145 1143
1146 1144 def get_img_data(self, svg_type=None):
1147 1145 """
1148 1146 generates the svg metadata for image
1149 1147 """
1150 1148
1151 1149 font_family = ','.join([
1152 1150 'proximanovaregular',
1153 1151 'Proxima Nova Regular',
1154 1152 'Proxima Nova',
1155 1153 'Arial',
1156 1154 'Lucida Grande',
1157 1155 'sans-serif'
1158 1156 ])
1159 1157 if svg_type:
1160 1158 return self.get_img_data_by_type(font_family, svg_type)
1161 1159
1162 1160 initials = self.get_initials()
1163 1161 img_data = """
1164 1162 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1165 1163 width="{size}" height="{size}"
1166 1164 style="width: 100%; height: 100%; background-color: {background}"
1167 1165 viewBox="0 0 {size} {size}">
1168 1166 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1169 1167 pointer-events="auto" fill="{text_color}"
1170 1168 font-family="{font_family}"
1171 1169 style="font-weight: 400; font-size: {f_size}px;">{text}
1172 1170 </text>
1173 1171 </svg>""".format(
1174 1172 size=self.size,
1175 1173 f_size=self.size/1.85, # scale the text inside the box nicely
1176 1174 background=self.background,
1177 1175 text_color=self.text_color,
1178 1176 text=initials.upper(),
1179 1177 font_family=font_family)
1180 1178
1181 1179 return img_data
1182 1180
1183 1181 def generate_svg(self, svg_type=None):
1184 1182 img_data = self.get_img_data(svg_type)
1185 1183 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1186 1184
1187 1185
1188 1186 def initials_gravatar(email_address, first_name, last_name, size=30):
1189 1187 svg_type = None
1190 1188 if email_address == User.DEFAULT_USER_EMAIL:
1191 1189 svg_type = 'default_user'
1192 1190 klass = InitialsGravatar(email_address, first_name, last_name, size)
1193 1191 return klass.generate_svg(svg_type=svg_type)
1194 1192
1195 1193
1196 1194 def gravatar_url(email_address, size=30):
1197 1195 # doh, we need to re-import those to mock it later
1198 1196 from pylons import tmpl_context as c
1199 1197
1200 1198 _use_gravatar = c.visual.use_gravatar
1201 1199 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1202 1200
1203 1201 email_address = email_address or User.DEFAULT_USER_EMAIL
1204 1202 if isinstance(email_address, unicode):
1205 1203 # hashlib crashes on unicode items
1206 1204 email_address = safe_str(email_address)
1207 1205
1208 1206 # empty email or default user
1209 1207 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1210 1208 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1211 1209
1212 1210 if _use_gravatar:
1213 1211 # TODO: Disuse pyramid thread locals. Think about another solution to
1214 1212 # get the host and schema here.
1215 1213 request = get_current_request()
1216 1214 tmpl = safe_str(_gravatar_url)
1217 1215 tmpl = tmpl.replace('{email}', email_address)\
1218 1216 .replace('{md5email}', md5_safe(email_address.lower())) \
1219 1217 .replace('{netloc}', request.host)\
1220 1218 .replace('{scheme}', request.scheme)\
1221 1219 .replace('{size}', safe_str(size))
1222 1220 return tmpl
1223 1221 else:
1224 1222 return initials_gravatar(email_address, '', '', size=size)
1225 1223
1226 1224
1227 1225 class Page(_Page):
1228 1226 """
1229 1227 Custom pager to match rendering style with paginator
1230 1228 """
1231 1229
1232 1230 def _get_pos(self, cur_page, max_page, items):
1233 1231 edge = (items / 2) + 1
1234 1232 if (cur_page <= edge):
1235 1233 radius = max(items / 2, items - cur_page)
1236 1234 elif (max_page - cur_page) < edge:
1237 1235 radius = (items - 1) - (max_page - cur_page)
1238 1236 else:
1239 1237 radius = items / 2
1240 1238
1241 1239 left = max(1, (cur_page - (radius)))
1242 1240 right = min(max_page, cur_page + (radius))
1243 1241 return left, cur_page, right
1244 1242
1245 1243 def _range(self, regexp_match):
1246 1244 """
1247 1245 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1248 1246
1249 1247 Arguments:
1250 1248
1251 1249 regexp_match
1252 1250 A "re" (regular expressions) match object containing the
1253 1251 radius of linked pages around the current page in
1254 1252 regexp_match.group(1) as a string
1255 1253
1256 1254 This function is supposed to be called as a callable in
1257 1255 re.sub.
1258 1256
1259 1257 """
1260 1258 radius = int(regexp_match.group(1))
1261 1259
1262 1260 # Compute the first and last page number within the radius
1263 1261 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1264 1262 # -> leftmost_page = 5
1265 1263 # -> rightmost_page = 9
1266 1264 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1267 1265 self.last_page,
1268 1266 (radius * 2) + 1)
1269 1267 nav_items = []
1270 1268
1271 1269 # Create a link to the first page (unless we are on the first page
1272 1270 # or there would be no need to insert '..' spacers)
1273 1271 if self.page != self.first_page and self.first_page < leftmost_page:
1274 1272 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1275 1273
1276 1274 # Insert dots if there are pages between the first page
1277 1275 # and the currently displayed page range
1278 1276 if leftmost_page - self.first_page > 1:
1279 1277 # Wrap in a SPAN tag if nolink_attr is set
1280 1278 text = '..'
1281 1279 if self.dotdot_attr:
1282 1280 text = HTML.span(c=text, **self.dotdot_attr)
1283 1281 nav_items.append(text)
1284 1282
1285 1283 for thispage in xrange(leftmost_page, rightmost_page + 1):
1286 1284 # Hilight the current page number and do not use a link
1287 1285 if thispage == self.page:
1288 1286 text = '%s' % (thispage,)
1289 1287 # Wrap in a SPAN tag if nolink_attr is set
1290 1288 if self.curpage_attr:
1291 1289 text = HTML.span(c=text, **self.curpage_attr)
1292 1290 nav_items.append(text)
1293 1291 # Otherwise create just a link to that page
1294 1292 else:
1295 1293 text = '%s' % (thispage,)
1296 1294 nav_items.append(self._pagerlink(thispage, text))
1297 1295
1298 1296 # Insert dots if there are pages between the displayed
1299 1297 # page numbers and the end of the page range
1300 1298 if self.last_page - rightmost_page > 1:
1301 1299 text = '..'
1302 1300 # Wrap in a SPAN tag if nolink_attr is set
1303 1301 if self.dotdot_attr:
1304 1302 text = HTML.span(c=text, **self.dotdot_attr)
1305 1303 nav_items.append(text)
1306 1304
1307 1305 # Create a link to the very last page (unless we are on the last
1308 1306 # page or there would be no need to insert '..' spacers)
1309 1307 if self.page != self.last_page and rightmost_page < self.last_page:
1310 1308 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1311 1309
1312 1310 ## prerender links
1313 1311 #_page_link = url.current()
1314 1312 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1315 1313 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1316 1314 return self.separator.join(nav_items)
1317 1315
1318 1316 def pager(self, format='~2~', page_param='page', partial_param='partial',
1319 1317 show_if_single_page=False, separator=' ', onclick=None,
1320 1318 symbol_first='<<', symbol_last='>>',
1321 1319 symbol_previous='<', symbol_next='>',
1322 1320 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1323 1321 curpage_attr={'class': 'pager_curpage'},
1324 1322 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1325 1323
1326 1324 self.curpage_attr = curpage_attr
1327 1325 self.separator = separator
1328 1326 self.pager_kwargs = kwargs
1329 1327 self.page_param = page_param
1330 1328 self.partial_param = partial_param
1331 1329 self.onclick = onclick
1332 1330 self.link_attr = link_attr
1333 1331 self.dotdot_attr = dotdot_attr
1334 1332
1335 1333 # Don't show navigator if there is no more than one page
1336 1334 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1337 1335 return ''
1338 1336
1339 1337 from string import Template
1340 1338 # Replace ~...~ in token format by range of pages
1341 1339 result = re.sub(r'~(\d+)~', self._range, format)
1342 1340
1343 1341 # Interpolate '%' variables
1344 1342 result = Template(result).safe_substitute({
1345 1343 'first_page': self.first_page,
1346 1344 'last_page': self.last_page,
1347 1345 'page': self.page,
1348 1346 'page_count': self.page_count,
1349 1347 'items_per_page': self.items_per_page,
1350 1348 'first_item': self.first_item,
1351 1349 'last_item': self.last_item,
1352 1350 'item_count': self.item_count,
1353 1351 'link_first': self.page > self.first_page and \
1354 1352 self._pagerlink(self.first_page, symbol_first) or '',
1355 1353 'link_last': self.page < self.last_page and \
1356 1354 self._pagerlink(self.last_page, symbol_last) or '',
1357 1355 'link_previous': self.previous_page and \
1358 1356 self._pagerlink(self.previous_page, symbol_previous) \
1359 1357 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1360 1358 'link_next': self.next_page and \
1361 1359 self._pagerlink(self.next_page, symbol_next) \
1362 1360 or HTML.span(symbol_next, class_="pg-next disabled")
1363 1361 })
1364 1362
1365 1363 return literal(result)
1366 1364
1367 1365
1368 1366 #==============================================================================
1369 1367 # REPO PAGER, PAGER FOR REPOSITORY
1370 1368 #==============================================================================
1371 1369 class RepoPage(Page):
1372 1370
1373 1371 def __init__(self, collection, page=1, items_per_page=20,
1374 1372 item_count=None, url=None, **kwargs):
1375 1373
1376 1374 """Create a "RepoPage" instance. special pager for paging
1377 1375 repository
1378 1376 """
1379 1377 self._url_generator = url
1380 1378
1381 1379 # Safe the kwargs class-wide so they can be used in the pager() method
1382 1380 self.kwargs = kwargs
1383 1381
1384 1382 # Save a reference to the collection
1385 1383 self.original_collection = collection
1386 1384
1387 1385 self.collection = collection
1388 1386
1389 1387 # The self.page is the number of the current page.
1390 1388 # The first page has the number 1!
1391 1389 try:
1392 1390 self.page = int(page) # make it int() if we get it as a string
1393 1391 except (ValueError, TypeError):
1394 1392 self.page = 1
1395 1393
1396 1394 self.items_per_page = items_per_page
1397 1395
1398 1396 # Unless the user tells us how many items the collections has
1399 1397 # we calculate that ourselves.
1400 1398 if item_count is not None:
1401 1399 self.item_count = item_count
1402 1400 else:
1403 1401 self.item_count = len(self.collection)
1404 1402
1405 1403 # Compute the number of the first and last available page
1406 1404 if self.item_count > 0:
1407 1405 self.first_page = 1
1408 1406 self.page_count = int(math.ceil(float(self.item_count) /
1409 1407 self.items_per_page))
1410 1408 self.last_page = self.first_page + self.page_count - 1
1411 1409
1412 1410 # Make sure that the requested page number is the range of
1413 1411 # valid pages
1414 1412 if self.page > self.last_page:
1415 1413 self.page = self.last_page
1416 1414 elif self.page < self.first_page:
1417 1415 self.page = self.first_page
1418 1416
1419 1417 # Note: the number of items on this page can be less than
1420 1418 # items_per_page if the last page is not full
1421 1419 self.first_item = max(0, (self.item_count) - (self.page *
1422 1420 items_per_page))
1423 1421 self.last_item = ((self.item_count - 1) - items_per_page *
1424 1422 (self.page - 1))
1425 1423
1426 1424 self.items = list(self.collection[self.first_item:self.last_item + 1])
1427 1425
1428 1426 # Links to previous and next page
1429 1427 if self.page > self.first_page:
1430 1428 self.previous_page = self.page - 1
1431 1429 else:
1432 1430 self.previous_page = None
1433 1431
1434 1432 if self.page < self.last_page:
1435 1433 self.next_page = self.page + 1
1436 1434 else:
1437 1435 self.next_page = None
1438 1436
1439 1437 # No items available
1440 1438 else:
1441 1439 self.first_page = None
1442 1440 self.page_count = 0
1443 1441 self.last_page = None
1444 1442 self.first_item = None
1445 1443 self.last_item = None
1446 1444 self.previous_page = None
1447 1445 self.next_page = None
1448 1446 self.items = []
1449 1447
1450 1448 # This is a subclass of the 'list' type. Initialise the list now.
1451 1449 list.__init__(self, reversed(self.items))
1452 1450
1453 1451
1454 1452 def changed_tooltip(nodes):
1455 1453 """
1456 1454 Generates a html string for changed nodes in commit page.
1457 1455 It limits the output to 30 entries
1458 1456
1459 1457 :param nodes: LazyNodesGenerator
1460 1458 """
1461 1459 if nodes:
1462 1460 pref = ': <br/> '
1463 1461 suf = ''
1464 1462 if len(nodes) > 30:
1465 1463 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1466 1464 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1467 1465 for x in nodes[:30]]) + suf)
1468 1466 else:
1469 1467 return ': ' + _('No Files')
1470 1468
1471 1469
1472 1470 def breadcrumb_repo_link(repo):
1473 1471 """
1474 1472 Makes a breadcrumbs path link to repo
1475 1473
1476 1474 ex::
1477 1475 group >> subgroup >> repo
1478 1476
1479 1477 :param repo: a Repository instance
1480 1478 """
1481 1479
1482 1480 path = [
1483 1481 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1484 1482 for group in repo.groups_with_parents
1485 1483 ] + [
1486 1484 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1487 1485 ]
1488 1486
1489 1487 return literal(' &raquo; '.join(path))
1490 1488
1491 1489
1492 1490 def format_byte_size_binary(file_size):
1493 1491 """
1494 1492 Formats file/folder sizes to standard.
1495 1493 """
1496 1494 formatted_size = format_byte_size(file_size, binary=True)
1497 1495 return formatted_size
1498 1496
1499 1497
1500 1498 def fancy_file_stats(stats):
1501 1499 """
1502 1500 Displays a fancy two colored bar for number of added/deleted
1503 1501 lines of code on file
1504 1502
1505 1503 :param stats: two element list of added/deleted lines of code
1506 1504 """
1507 1505 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1508 1506 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1509 1507
1510 1508 def cgen(l_type, a_v, d_v):
1511 1509 mapping = {'tr': 'top-right-rounded-corner-mid',
1512 1510 'tl': 'top-left-rounded-corner-mid',
1513 1511 'br': 'bottom-right-rounded-corner-mid',
1514 1512 'bl': 'bottom-left-rounded-corner-mid'}
1515 1513 map_getter = lambda x: mapping[x]
1516 1514
1517 1515 if l_type == 'a' and d_v:
1518 1516 #case when added and deleted are present
1519 1517 return ' '.join(map(map_getter, ['tl', 'bl']))
1520 1518
1521 1519 if l_type == 'a' and not d_v:
1522 1520 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1523 1521
1524 1522 if l_type == 'd' and a_v:
1525 1523 return ' '.join(map(map_getter, ['tr', 'br']))
1526 1524
1527 1525 if l_type == 'd' and not a_v:
1528 1526 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1529 1527
1530 1528 a, d = stats['added'], stats['deleted']
1531 1529 width = 100
1532 1530
1533 1531 if stats['binary']: # binary operations like chmod/rename etc
1534 1532 lbl = []
1535 1533 bin_op = 0 # undefined
1536 1534
1537 1535 # prefix with bin for binary files
1538 1536 if BIN_FILENODE in stats['ops']:
1539 1537 lbl += ['bin']
1540 1538
1541 1539 if NEW_FILENODE in stats['ops']:
1542 1540 lbl += [_('new file')]
1543 1541 bin_op = NEW_FILENODE
1544 1542 elif MOD_FILENODE in stats['ops']:
1545 1543 lbl += [_('mod')]
1546 1544 bin_op = MOD_FILENODE
1547 1545 elif DEL_FILENODE in stats['ops']:
1548 1546 lbl += [_('del')]
1549 1547 bin_op = DEL_FILENODE
1550 1548 elif RENAMED_FILENODE in stats['ops']:
1551 1549 lbl += [_('rename')]
1552 1550 bin_op = RENAMED_FILENODE
1553 1551
1554 1552 # chmod can go with other operations, so we add a + to lbl if needed
1555 1553 if CHMOD_FILENODE in stats['ops']:
1556 1554 lbl += [_('chmod')]
1557 1555 if bin_op == 0:
1558 1556 bin_op = CHMOD_FILENODE
1559 1557
1560 1558 lbl = '+'.join(lbl)
1561 1559 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1562 1560 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1563 1561 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1564 1562 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1565 1563
1566 1564 t = stats['added'] + stats['deleted']
1567 1565 unit = float(width) / (t or 1)
1568 1566
1569 1567 # needs > 9% of width to be visible or 0 to be hidden
1570 1568 a_p = max(9, unit * a) if a > 0 else 0
1571 1569 d_p = max(9, unit * d) if d > 0 else 0
1572 1570 p_sum = a_p + d_p
1573 1571
1574 1572 if p_sum > width:
1575 1573 #adjust the percentage to be == 100% since we adjusted to 9
1576 1574 if a_p > d_p:
1577 1575 a_p = a_p - (p_sum - width)
1578 1576 else:
1579 1577 d_p = d_p - (p_sum - width)
1580 1578
1581 1579 a_v = a if a > 0 else ''
1582 1580 d_v = d if d > 0 else ''
1583 1581
1584 1582 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1585 1583 cgen('a', a_v, d_v), a_p, a_v
1586 1584 )
1587 1585 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1588 1586 cgen('d', a_v, d_v), d_p, d_v
1589 1587 )
1590 1588 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1591 1589
1592 1590
1593 1591 def urlify_text(text_, safe=True):
1594 1592 """
1595 1593 Extrac urls from text and make html links out of them
1596 1594
1597 1595 :param text_:
1598 1596 """
1599 1597
1600 1598 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1601 1599 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1602 1600
1603 1601 def url_func(match_obj):
1604 1602 url_full = match_obj.groups()[0]
1605 1603 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1606 1604 _newtext = url_pat.sub(url_func, text_)
1607 1605 if safe:
1608 1606 return literal(_newtext)
1609 1607 return _newtext
1610 1608
1611 1609
1612 1610 def urlify_commits(text_, repository):
1613 1611 """
1614 1612 Extract commit ids from text and make link from them
1615 1613
1616 1614 :param text_:
1617 1615 :param repository: repo name to build the URL with
1618 1616 """
1619 1617 from pylons import url # doh, we need to re-import url to mock it later
1620 1618 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1621 1619
1622 1620 def url_func(match_obj):
1623 1621 commit_id = match_obj.groups()[1]
1624 1622 pref = match_obj.groups()[0]
1625 1623 suf = match_obj.groups()[2]
1626 1624
1627 1625 tmpl = (
1628 1626 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1629 1627 '%(commit_id)s</a>%(suf)s'
1630 1628 )
1631 1629 return tmpl % {
1632 1630 'pref': pref,
1633 1631 'cls': 'revision-link',
1634 1632 'url': url('changeset_home', repo_name=repository,
1635 1633 revision=commit_id, qualified=True),
1636 1634 'commit_id': commit_id,
1637 1635 'suf': suf
1638 1636 }
1639 1637
1640 1638 newtext = URL_PAT.sub(url_func, text_)
1641 1639
1642 1640 return newtext
1643 1641
1644 1642
1645 1643 def _process_url_func(match_obj, repo_name, uid, entry,
1646 1644 return_raw_data=False):
1647 1645 pref = ''
1648 1646 if match_obj.group().startswith(' '):
1649 1647 pref = ' '
1650 1648
1651 1649 issue_id = ''.join(match_obj.groups())
1652 1650 tmpl = (
1653 1651 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1654 1652 '%(issue-prefix)s%(id-repr)s'
1655 1653 '</a>')
1656 1654
1657 1655 (repo_name_cleaned,
1658 1656 parent_group_name) = RepoGroupModel().\
1659 1657 _get_group_name_and_parent(repo_name)
1660 1658
1661 1659 # variables replacement
1662 1660 named_vars = {
1663 1661 'id': issue_id,
1664 1662 'repo': repo_name,
1665 1663 'repo_name': repo_name_cleaned,
1666 1664 'group_name': parent_group_name
1667 1665 }
1668 1666 # named regex variables
1669 1667 named_vars.update(match_obj.groupdict())
1670 1668 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1671 1669
1672 1670 data = {
1673 1671 'pref': pref,
1674 1672 'cls': 'issue-tracker-link',
1675 1673 'url': _url,
1676 1674 'id-repr': issue_id,
1677 1675 'issue-prefix': entry['pref'],
1678 1676 'serv': entry['url'],
1679 1677 }
1680 1678 if return_raw_data:
1681 1679 return {
1682 1680 'id': issue_id,
1683 1681 'url': _url
1684 1682 }
1685 1683 return tmpl % data
1686 1684
1687 1685
1688 1686 def process_patterns(text_string, repo_name, config=None):
1689 1687 repo = None
1690 1688 if repo_name:
1691 1689 # Retrieving repo_name to avoid invalid repo_name to explode on
1692 1690 # IssueTrackerSettingsModel but still passing invalid name further down
1693 1691 repo = Repository.get_by_repo_name(repo_name, cache=True)
1694 1692
1695 1693 settings_model = IssueTrackerSettingsModel(repo=repo)
1696 1694 active_entries = settings_model.get_settings(cache=True)
1697 1695
1698 1696 issues_data = []
1699 1697 newtext = text_string
1700 1698 for uid, entry in active_entries.items():
1701 1699 log.debug('found issue tracker entry with uid %s' % (uid,))
1702 1700
1703 1701 if not (entry['pat'] and entry['url']):
1704 1702 log.debug('skipping due to missing data')
1705 1703 continue
1706 1704
1707 1705 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1708 1706 % (uid, entry['pat'], entry['url'], entry['pref']))
1709 1707
1710 1708 try:
1711 1709 pattern = re.compile(r'%s' % entry['pat'])
1712 1710 except re.error:
1713 1711 log.exception(
1714 1712 'issue tracker pattern: `%s` failed to compile',
1715 1713 entry['pat'])
1716 1714 continue
1717 1715
1718 1716 data_func = partial(
1719 1717 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1720 1718 return_raw_data=True)
1721 1719
1722 1720 for match_obj in pattern.finditer(text_string):
1723 1721 issues_data.append(data_func(match_obj))
1724 1722
1725 1723 url_func = partial(
1726 1724 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1727 1725
1728 1726 newtext = pattern.sub(url_func, newtext)
1729 1727 log.debug('processed prefix:uid `%s`' % (uid,))
1730 1728
1731 1729 return newtext, issues_data
1732 1730
1733 1731
1734 1732 def urlify_commit_message(commit_text, repository=None):
1735 1733 """
1736 1734 Parses given text message and makes proper links.
1737 1735 issues are linked to given issue-server, and rest is a commit link
1738 1736
1739 1737 :param commit_text:
1740 1738 :param repository:
1741 1739 """
1742 1740 from pylons import url # doh, we need to re-import url to mock it later
1743 1741
1744 1742 def escaper(string):
1745 1743 return string.replace('<', '&lt;').replace('>', '&gt;')
1746 1744
1747 1745 newtext = escaper(commit_text)
1748 1746
1749 1747 # extract http/https links and make them real urls
1750 1748 newtext = urlify_text(newtext, safe=False)
1751 1749
1752 1750 # urlify commits - extract commit ids and make link out of them, if we have
1753 1751 # the scope of repository present.
1754 1752 if repository:
1755 1753 newtext = urlify_commits(newtext, repository)
1756 1754
1757 1755 # process issue tracker patterns
1758 1756 newtext, issues = process_patterns(newtext, repository or '')
1759 1757
1760 1758 return literal(newtext)
1761 1759
1762 1760
1763 1761 def rst(source, mentions=False):
1764 1762 return literal('<div class="rst-block">%s</div>' %
1765 1763 MarkupRenderer.rst(source, mentions=mentions))
1766 1764
1767 1765
1768 1766 def markdown(source, mentions=False):
1769 1767 return literal('<div class="markdown-block">%s</div>' %
1770 1768 MarkupRenderer.markdown(source, flavored=True,
1771 1769 mentions=mentions))
1772 1770
1773 1771 def renderer_from_filename(filename, exclude=None):
1774 1772 return MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1775 1773
1776 1774
1777 1775 def render(source, renderer='rst', mentions=False):
1778 1776 if renderer == 'rst':
1779 1777 return rst(source, mentions=mentions)
1780 1778 if renderer == 'markdown':
1781 1779 return markdown(source, mentions=mentions)
1782 1780
1783 1781
1784 1782 def commit_status(repo, commit_id):
1785 1783 return ChangesetStatusModel().get_status(repo, commit_id)
1786 1784
1787 1785
1788 1786 def commit_status_lbl(commit_status):
1789 1787 return dict(ChangesetStatus.STATUSES).get(commit_status)
1790 1788
1791 1789
1792 1790 def commit_time(repo_name, commit_id):
1793 1791 repo = Repository.get_by_repo_name(repo_name)
1794 1792 commit = repo.get_commit(commit_id=commit_id)
1795 1793 return commit.date
1796 1794
1797 1795
1798 1796 def get_permission_name(key):
1799 1797 return dict(Permission.PERMS).get(key)
1800 1798
1801 1799
1802 1800 def journal_filter_help():
1803 1801 return _(
1804 1802 'Example filter terms:\n' +
1805 1803 ' repository:vcs\n' +
1806 1804 ' username:marcin\n' +
1807 1805 ' action:*push*\n' +
1808 1806 ' ip:127.0.0.1\n' +
1809 1807 ' date:20120101\n' +
1810 1808 ' date:[20120101100000 TO 20120102]\n' +
1811 1809 '\n' +
1812 1810 'Generate wildcards using \'*\' character:\n' +
1813 1811 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1814 1812 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1815 1813 '\n' +
1816 1814 'Optional AND / OR operators in queries\n' +
1817 1815 ' "repository:vcs OR repository:test"\n' +
1818 1816 ' "username:test AND repository:test*"\n'
1819 1817 )
1820 1818
1821 1819
1822 1820 def not_mapped_error(repo_name):
1823 1821 flash(_('%s repository is not mapped to db perhaps'
1824 1822 ' it was created or renamed from the filesystem'
1825 1823 ' please run the application again'
1826 1824 ' in order to rescan repositories') % repo_name, category='error')
1827 1825
1828 1826
1829 1827 def ip_range(ip_addr):
1830 1828 from rhodecode.model.db import UserIpMap
1831 1829 s, e = UserIpMap._get_ip_range(ip_addr)
1832 1830 return '%s - %s' % (s, e)
1833 1831
1834 1832
1835 1833 def form(url, method='post', needs_csrf_token=True, **attrs):
1836 1834 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1837 1835 if method.lower() != 'get' and needs_csrf_token:
1838 1836 raise Exception(
1839 1837 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1840 1838 'CSRF token. If the endpoint does not require such token you can ' +
1841 1839 'explicitly set the parameter needs_csrf_token to false.')
1842 1840
1843 1841 return wh_form(url, method=method, **attrs)
1844 1842
1845 1843
1846 1844 def secure_form(url, method="POST", multipart=False, **attrs):
1847 1845 """Start a form tag that points the action to an url. This
1848 1846 form tag will also include the hidden field containing
1849 1847 the auth token.
1850 1848
1851 1849 The url options should be given either as a string, or as a
1852 1850 ``url()`` function. The method for the form defaults to POST.
1853 1851
1854 1852 Options:
1855 1853
1856 1854 ``multipart``
1857 1855 If set to True, the enctype is set to "multipart/form-data".
1858 1856 ``method``
1859 1857 The method to use when submitting the form, usually either
1860 1858 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1861 1859 hidden input with name _method is added to simulate the verb
1862 1860 over POST.
1863 1861
1864 1862 """
1865 1863 from webhelpers.pylonslib.secure_form import insecure_form
1866 1864 from rhodecode.lib.auth import get_csrf_token, csrf_token_key
1867 1865 form = insecure_form(url, method, multipart, **attrs)
1868 1866 token = HTML.div(hidden(csrf_token_key, get_csrf_token()), style="display: none;")
1869 1867 return literal("%s\n%s" % (form, token))
1870 1868
1871 1869 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1872 1870 select_html = select(name, selected, options, **attrs)
1873 1871 select2 = """
1874 1872 <script>
1875 1873 $(document).ready(function() {
1876 1874 $('#%s').select2({
1877 1875 containerCssClass: 'drop-menu',
1878 1876 dropdownCssClass: 'drop-menu-dropdown',
1879 1877 dropdownAutoWidth: true%s
1880 1878 });
1881 1879 });
1882 1880 </script>
1883 1881 """
1884 1882 filter_option = """,
1885 1883 minimumResultsForSearch: -1
1886 1884 """
1887 1885 input_id = attrs.get('id') or name
1888 1886 filter_enabled = "" if enable_filter else filter_option
1889 1887 select_script = literal(select2 % (input_id, filter_enabled))
1890 1888
1891 1889 return literal(select_html+select_script)
1892 1890
1893 1891
1894 1892 def get_visual_attr(tmpl_context_var, attr_name):
1895 1893 """
1896 1894 A safe way to get a variable from visual variable of template context
1897 1895
1898 1896 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1899 1897 :param attr_name: name of the attribute we fetch from the c.visual
1900 1898 """
1901 1899 visual = getattr(tmpl_context_var, 'visual', None)
1902 1900 if not visual:
1903 1901 return
1904 1902 else:
1905 1903 return getattr(visual, attr_name, None)
1906 1904
1907 1905
1908 1906 def get_last_path_part(file_node):
1909 1907 if not file_node.path:
1910 1908 return u''
1911 1909
1912 1910 path = safe_unicode(file_node.path.split('/')[-1])
1913 1911 return u'../' + path
1914 1912
1915 1913
1916 1914 def route_path(*args, **kwds):
1917 1915 """
1918 1916 Wrapper around pyramids `route_path` function. It is used to generate
1919 1917 URLs from within pylons views or templates. This will be removed when
1920 1918 pyramid migration if finished.
1921 1919 """
1922 1920 req = get_current_request()
1923 1921 return req.route_path(*args, **kwds)
1924 1922
1925 1923
1926 1924 def resource_path(*args, **kwds):
1927 1925 """
1928 1926 Wrapper around pyramids `route_path` function. It is used to generate
1929 1927 URLs from within pylons views or templates. This will be removed when
1930 1928 pyramid migration if finished.
1931 1929 """
1932 1930 req = get_current_request()
1933 1931 return req.resource_path(*args, **kwds)
General Comments 0
You need to be logged in to leave comments. Login now