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