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