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