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