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