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