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