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