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