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