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