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