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