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