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