##// END OF EJS Templates
datetimes: fix datetimes to work across app, converting to utc
dan -
r155:bb64dc25 default
parent child Browse files
Show More
@@ -1,1885 +1,1892 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import random
29 29 import hashlib
30 30 import StringIO
31 31 import urllib
32 32 import math
33 33 import logging
34 34 import re
35 35 import urlparse
36 36 import time
37 37 import string
38 38 import hashlib
39 39 import pygments
40 40
41 41 from datetime import datetime
42 42 from functools import partial
43 43 from pygments.formatters.html import HtmlFormatter
44 44 from pygments import highlight as code_highlight
45 45 from pygments.lexers import (
46 46 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
47 47 from pylons import url
48 48 from pylons.i18n.translation import _, ungettext
49 49 from pyramid.threadlocal import get_current_request
50 50
51 51 from webhelpers.html import literal, HTML, escape
52 52 from webhelpers.html.tools import *
53 53 from webhelpers.html.builder import make_tag
54 54 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
55 55 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
56 56 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
57 57 submit, text, password, textarea, title, ul, xml_declaration, radio
58 58 from webhelpers.html.tools import auto_link, button_to, highlight, \
59 59 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
60 60 from webhelpers.pylonslib import Flash as _Flash
61 61 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
62 62 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
63 63 replace_whitespace, urlify, truncate, wrap_paragraphs
64 64 from webhelpers.date import time_ago_in_words
65 65 from webhelpers.paginate import Page as _Page
66 66 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
67 67 convert_boolean_attrs, NotGiven, _make_safe_id_component
68 68 from webhelpers2.number import format_byte_size
69 69
70 70 from rhodecode.lib.annotate import annotate_highlight
71 71 from rhodecode.lib.action_parser import action_parser
72 72 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
73 73 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
74 get_commit_safe, datetime_to_time, time_to_datetime, AttributeDict, \
75 safe_int, md5, md5_safe
74 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
75 AttributeDict, safe_int, md5, md5_safe
76 76 from rhodecode.lib.markup_renderer import MarkupRenderer
77 77 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
78 78 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
79 79 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
80 80 from rhodecode.model.changeset_status import ChangesetStatusModel
81 81 from rhodecode.model.db import Permission, User, Repository
82 82 from rhodecode.model.repo_group import RepoGroupModel
83 83 from rhodecode.model.settings import IssueTrackerSettingsModel
84 84
85 85 log = logging.getLogger(__name__)
86 86
87 87 DEFAULT_USER = User.DEFAULT_USER
88 88 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
89 89
90 90
91 91 def html_escape(text, html_escape_table=None):
92 92 """Produce entities within text."""
93 93 if not html_escape_table:
94 94 html_escape_table = {
95 95 "&": "&amp;",
96 96 '"': "&quot;",
97 97 "'": "&apos;",
98 98 ">": "&gt;",
99 99 "<": "&lt;",
100 100 }
101 101 return "".join(html_escape_table.get(c, c) for c in text)
102 102
103 103
104 104 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
105 105 """
106 106 Truncate string ``s`` at the first occurrence of ``sub``.
107 107
108 108 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
109 109 """
110 110 suffix_if_chopped = suffix_if_chopped or ''
111 111 pos = s.find(sub)
112 112 if pos == -1:
113 113 return s
114 114
115 115 if inclusive:
116 116 pos += len(sub)
117 117
118 118 chopped = s[:pos]
119 119 left = s[pos:].strip()
120 120
121 121 if left and suffix_if_chopped:
122 122 chopped += suffix_if_chopped
123 123
124 124 return chopped
125 125
126 126
127 127 def shorter(text, size=20):
128 128 postfix = '...'
129 129 if len(text) > size:
130 130 return text[:size - len(postfix)] + postfix
131 131 return text
132 132
133 133
134 134 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
135 135 """
136 136 Reset button
137 137 """
138 138 _set_input_attrs(attrs, type, name, value)
139 139 _set_id_attr(attrs, id, name)
140 140 convert_boolean_attrs(attrs, ["disabled"])
141 141 return HTML.input(**attrs)
142 142
143 143 reset = _reset
144 144 safeid = _make_safe_id_component
145 145
146 146
147 147 def branding(name, length=40):
148 148 return truncate(name, length, indicator="")
149 149
150 150
151 151 def FID(raw_id, path):
152 152 """
153 153 Creates a unique ID for filenode based on it's hash of path and commit
154 154 it's safe to use in urls
155 155
156 156 :param raw_id:
157 157 :param path:
158 158 """
159 159
160 160 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
161 161
162 162
163 163 class _GetError(object):
164 164 """Get error from form_errors, and represent it as span wrapped error
165 165 message
166 166
167 167 :param field_name: field to fetch errors for
168 168 :param form_errors: form errors dict
169 169 """
170 170
171 171 def __call__(self, field_name, form_errors):
172 172 tmpl = """<span class="error_msg">%s</span>"""
173 173 if form_errors and field_name in form_errors:
174 174 return literal(tmpl % form_errors.get(field_name))
175 175
176 176 get_error = _GetError()
177 177
178 178
179 179 class _ToolTip(object):
180 180
181 181 def __call__(self, tooltip_title, trim_at=50):
182 182 """
183 183 Special function just to wrap our text into nice formatted
184 184 autowrapped text
185 185
186 186 :param tooltip_title:
187 187 """
188 188 tooltip_title = escape(tooltip_title)
189 189 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
190 190 return tooltip_title
191 191 tooltip = _ToolTip()
192 192
193 193
194 194 def files_breadcrumbs(repo_name, commit_id, file_path):
195 195 if isinstance(file_path, str):
196 196 file_path = safe_unicode(file_path)
197 197
198 198 # TODO: johbo: Is this always a url like path, or is this operating
199 199 # system dependent?
200 200 path_segments = file_path.split('/')
201 201
202 202 repo_name_html = escape(repo_name)
203 203 if len(path_segments) == 1 and path_segments[0] == '':
204 204 url_segments = [repo_name_html]
205 205 else:
206 206 url_segments = [
207 207 link_to(
208 208 repo_name_html,
209 209 url('files_home',
210 210 repo_name=repo_name,
211 211 revision=commit_id,
212 212 f_path=''),
213 213 class_='pjax-link')]
214 214
215 215 last_cnt = len(path_segments) - 1
216 216 for cnt, segment in enumerate(path_segments):
217 217 if not segment:
218 218 continue
219 219 segment_html = escape(segment)
220 220
221 221 if cnt != last_cnt:
222 222 url_segments.append(
223 223 link_to(
224 224 segment_html,
225 225 url('files_home',
226 226 repo_name=repo_name,
227 227 revision=commit_id,
228 228 f_path='/'.join(path_segments[:cnt + 1])),
229 229 class_='pjax-link'))
230 230 else:
231 231 url_segments.append(segment_html)
232 232
233 233 return literal('/'.join(url_segments))
234 234
235 235
236 236 class CodeHtmlFormatter(HtmlFormatter):
237 237 """
238 238 My code Html Formatter for source codes
239 239 """
240 240
241 241 def wrap(self, source, outfile):
242 242 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
243 243
244 244 def _wrap_code(self, source):
245 245 for cnt, it in enumerate(source):
246 246 i, t = it
247 247 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
248 248 yield i, t
249 249
250 250 def _wrap_tablelinenos(self, inner):
251 251 dummyoutfile = StringIO.StringIO()
252 252 lncount = 0
253 253 for t, line in inner:
254 254 if t:
255 255 lncount += 1
256 256 dummyoutfile.write(line)
257 257
258 258 fl = self.linenostart
259 259 mw = len(str(lncount + fl - 1))
260 260 sp = self.linenospecial
261 261 st = self.linenostep
262 262 la = self.lineanchors
263 263 aln = self.anchorlinenos
264 264 nocls = self.noclasses
265 265 if sp:
266 266 lines = []
267 267
268 268 for i in range(fl, fl + lncount):
269 269 if i % st == 0:
270 270 if i % sp == 0:
271 271 if aln:
272 272 lines.append('<a href="#%s%d" class="special">%*d</a>' %
273 273 (la, i, mw, i))
274 274 else:
275 275 lines.append('<span class="special">%*d</span>' % (mw, i))
276 276 else:
277 277 if aln:
278 278 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
279 279 else:
280 280 lines.append('%*d' % (mw, i))
281 281 else:
282 282 lines.append('')
283 283 ls = '\n'.join(lines)
284 284 else:
285 285 lines = []
286 286 for i in range(fl, fl + lncount):
287 287 if i % st == 0:
288 288 if aln:
289 289 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
290 290 else:
291 291 lines.append('%*d' % (mw, i))
292 292 else:
293 293 lines.append('')
294 294 ls = '\n'.join(lines)
295 295
296 296 # in case you wonder about the seemingly redundant <div> here: since the
297 297 # content in the other cell also is wrapped in a div, some browsers in
298 298 # some configurations seem to mess up the formatting...
299 299 if nocls:
300 300 yield 0, ('<table class="%stable">' % self.cssclass +
301 301 '<tr><td><div class="linenodiv" '
302 302 'style="background-color: #f0f0f0; padding-right: 10px">'
303 303 '<pre style="line-height: 125%">' +
304 304 ls + '</pre></div></td><td id="hlcode" class="code">')
305 305 else:
306 306 yield 0, ('<table class="%stable">' % self.cssclass +
307 307 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
308 308 ls + '</pre></div></td><td id="hlcode" class="code">')
309 309 yield 0, dummyoutfile.getvalue()
310 310 yield 0, '</td></tr></table>'
311 311
312 312
313 313 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
314 314 def __init__(self, **kw):
315 315 # only show these line numbers if set
316 316 self.only_lines = kw.pop('only_line_numbers', [])
317 317 self.query_terms = kw.pop('query_terms', [])
318 318 self.max_lines = kw.pop('max_lines', 5)
319 319 self.line_context = kw.pop('line_context', 3)
320 320 self.url = kw.pop('url', None)
321 321
322 322 super(CodeHtmlFormatter, self).__init__(**kw)
323 323
324 324 def _wrap_code(self, source):
325 325 for cnt, it in enumerate(source):
326 326 i, t = it
327 327 t = '<pre>%s</pre>' % t
328 328 yield i, t
329 329
330 330 def _wrap_tablelinenos(self, inner):
331 331 yield 0, '<table class="code-highlight %stable">' % self.cssclass
332 332
333 333 last_shown_line_number = 0
334 334 current_line_number = 1
335 335
336 336 for t, line in inner:
337 337 if not t:
338 338 yield t, line
339 339 continue
340 340
341 341 if current_line_number in self.only_lines:
342 342 if last_shown_line_number + 1 != current_line_number:
343 343 yield 0, '<tr>'
344 344 yield 0, '<td class="line">...</td>'
345 345 yield 0, '<td id="hlcode" class="code"></td>'
346 346 yield 0, '</tr>'
347 347
348 348 yield 0, '<tr>'
349 349 if self.url:
350 350 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
351 351 self.url, current_line_number, current_line_number)
352 352 else:
353 353 yield 0, '<td class="line"><a href="">%i</a></td>' % (
354 354 current_line_number)
355 355 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
356 356 yield 0, '</tr>'
357 357
358 358 last_shown_line_number = current_line_number
359 359
360 360 current_line_number += 1
361 361
362 362
363 363 yield 0, '</table>'
364 364
365 365
366 366 def extract_phrases(text_query):
367 367 """
368 368 Extracts phrases from search term string making sure phrases
369 369 contained in double quotes are kept together - and discarding empty values
370 370 or fully whitespace values eg.
371 371
372 372 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
373 373
374 374 """
375 375
376 376 in_phrase = False
377 377 buf = ''
378 378 phrases = []
379 379 for char in text_query:
380 380 if in_phrase:
381 381 if char == '"': # end phrase
382 382 phrases.append(buf)
383 383 buf = ''
384 384 in_phrase = False
385 385 continue
386 386 else:
387 387 buf += char
388 388 continue
389 389 else:
390 390 if char == '"': # start phrase
391 391 in_phrase = True
392 392 phrases.append(buf)
393 393 buf = ''
394 394 continue
395 395 elif char == ' ':
396 396 phrases.append(buf)
397 397 buf = ''
398 398 continue
399 399 else:
400 400 buf += char
401 401
402 402 phrases.append(buf)
403 403 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
404 404 return phrases
405 405
406 406
407 407 def get_matching_offsets(text, phrases):
408 408 """
409 409 Returns a list of string offsets in `text` that the list of `terms` match
410 410
411 411 >>> get_matching_offsets('some text here', ['some', 'here'])
412 412 [(0, 4), (10, 14)]
413 413
414 414 """
415 415 offsets = []
416 416 for phrase in phrases:
417 417 for match in re.finditer(phrase, text):
418 418 offsets.append((match.start(), match.end()))
419 419
420 420 return offsets
421 421
422 422
423 423 def normalize_text_for_matching(x):
424 424 """
425 425 Replaces all non alnum characters to spaces and lower cases the string,
426 426 useful for comparing two text strings without punctuation
427 427 """
428 428 return re.sub(r'[^\w]', ' ', x.lower())
429 429
430 430
431 431 def get_matching_line_offsets(lines, terms):
432 432 """ Return a set of `lines` indices (starting from 1) matching a
433 433 text search query, along with `context` lines above/below matching lines
434 434
435 435 :param lines: list of strings representing lines
436 436 :param terms: search term string to match in lines eg. 'some text'
437 437 :param context: number of lines above/below a matching line to add to result
438 438 :param max_lines: cut off for lines of interest
439 439 eg.
440 440
441 441 >>> get_matching_line_offsets('''
442 442 words words words
443 443 words words words
444 444 some text some
445 445 words words words
446 446 words words words
447 447 text here what
448 448 ''', 'text', context=1)
449 449 {3: [(5, 9)], 6: [(0, 4)]]
450 450 """
451 451 matching_lines = {}
452 452 phrases = [normalize_text_for_matching(phrase)
453 453 for phrase in extract_phrases(terms)]
454 454
455 455 for line_index, line in enumerate(lines, start=1):
456 456 match_offsets = get_matching_offsets(
457 457 normalize_text_for_matching(line), phrases)
458 458 if match_offsets:
459 459 matching_lines[line_index] = match_offsets
460 460
461 461 return matching_lines
462 462
463 463 def get_lexer_safe(mimetype=None, filepath=None):
464 464 """
465 465 Tries to return a relevant pygments lexer using mimetype/filepath name,
466 466 defaulting to plain text if none could be found
467 467 """
468 468 lexer = None
469 469 try:
470 470 if mimetype:
471 471 lexer = get_lexer_for_mimetype(mimetype)
472 472 if not lexer:
473 473 lexer = get_lexer_for_filename(path)
474 474 except pygments.util.ClassNotFound:
475 475 pass
476 476
477 477 if not lexer:
478 478 lexer = get_lexer_by_name('text')
479 479
480 480 return lexer
481 481
482 482
483 483 def pygmentize(filenode, **kwargs):
484 484 """
485 485 pygmentize function using pygments
486 486
487 487 :param filenode:
488 488 """
489 489 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
490 490 return literal(code_highlight(filenode.content, lexer,
491 491 CodeHtmlFormatter(**kwargs)))
492 492
493 493
494 494 def pygmentize_annotation(repo_name, filenode, **kwargs):
495 495 """
496 496 pygmentize function for annotation
497 497
498 498 :param filenode:
499 499 """
500 500
501 501 color_dict = {}
502 502
503 503 def gen_color(n=10000):
504 504 """generator for getting n of evenly distributed colors using
505 505 hsv color and golden ratio. It always return same order of colors
506 506
507 507 :returns: RGB tuple
508 508 """
509 509
510 510 def hsv_to_rgb(h, s, v):
511 511 if s == 0.0:
512 512 return v, v, v
513 513 i = int(h * 6.0) # XXX assume int() truncates!
514 514 f = (h * 6.0) - i
515 515 p = v * (1.0 - s)
516 516 q = v * (1.0 - s * f)
517 517 t = v * (1.0 - s * (1.0 - f))
518 518 i = i % 6
519 519 if i == 0:
520 520 return v, t, p
521 521 if i == 1:
522 522 return q, v, p
523 523 if i == 2:
524 524 return p, v, t
525 525 if i == 3:
526 526 return p, q, v
527 527 if i == 4:
528 528 return t, p, v
529 529 if i == 5:
530 530 return v, p, q
531 531
532 532 golden_ratio = 0.618033988749895
533 533 h = 0.22717784590367374
534 534
535 535 for _ in xrange(n):
536 536 h += golden_ratio
537 537 h %= 1
538 538 HSV_tuple = [h, 0.95, 0.95]
539 539 RGB_tuple = hsv_to_rgb(*HSV_tuple)
540 540 yield map(lambda x: str(int(x * 256)), RGB_tuple)
541 541
542 542 cgenerator = gen_color()
543 543
544 544 def get_color_string(commit_id):
545 545 if commit_id in color_dict:
546 546 col = color_dict[commit_id]
547 547 else:
548 548 col = color_dict[commit_id] = cgenerator.next()
549 549 return "color: rgb(%s)! important;" % (', '.join(col))
550 550
551 551 def url_func(repo_name):
552 552
553 553 def _url_func(commit):
554 554 author = commit.author
555 555 date = commit.date
556 556 message = tooltip(commit.message)
557 557
558 558 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
559 559 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
560 560 "</b> %s<br/></div>")
561 561
562 562 tooltip_html = tooltip_html % (author, date, message)
563 563 lnk_format = '%5s:%s' % ('r%s' % commit.idx, commit.short_id)
564 564 uri = link_to(
565 565 lnk_format,
566 566 url('changeset_home', repo_name=repo_name,
567 567 revision=commit.raw_id),
568 568 style=get_color_string(commit.raw_id),
569 569 class_='tooltip',
570 570 title=tooltip_html
571 571 )
572 572
573 573 uri += '\n'
574 574 return uri
575 575 return _url_func
576 576
577 577 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
578 578
579 579
580 580 def is_following_repo(repo_name, user_id):
581 581 from rhodecode.model.scm import ScmModel
582 582 return ScmModel().is_following_repo(repo_name, user_id)
583 583
584 584
585 585 class _Message(object):
586 586 """A message returned by ``Flash.pop_messages()``.
587 587
588 588 Converting the message to a string returns the message text. Instances
589 589 also have the following attributes:
590 590
591 591 * ``message``: the message text.
592 592 * ``category``: the category specified when the message was created.
593 593 """
594 594
595 595 def __init__(self, category, message):
596 596 self.category = category
597 597 self.message = message
598 598
599 599 def __str__(self):
600 600 return self.message
601 601
602 602 __unicode__ = __str__
603 603
604 604 def __html__(self):
605 605 return escape(safe_unicode(self.message))
606 606
607 607
608 608 class Flash(_Flash):
609 609
610 610 def pop_messages(self):
611 611 """Return all accumulated messages and delete them from the session.
612 612
613 613 The return value is a list of ``Message`` objects.
614 614 """
615 615 from pylons import session
616 616
617 617 messages = []
618 618
619 619 # Pop the 'old' pylons flash messages. They are tuples of the form
620 620 # (category, message)
621 621 for cat, msg in session.pop(self.session_key, []):
622 622 messages.append(_Message(cat, msg))
623 623
624 624 # Pop the 'new' pyramid flash messages for each category as list
625 625 # of strings.
626 626 for cat in self.categories:
627 627 for msg in session.pop_flash(queue=cat):
628 628 messages.append(_Message(cat, msg))
629 629 # Map messages from the default queue to the 'notice' category.
630 630 for msg in session.pop_flash():
631 631 messages.append(_Message('notice', msg))
632 632
633 633 session.save()
634 634 return messages
635 635
636 636 flash = Flash()
637 637
638 638 #==============================================================================
639 639 # SCM FILTERS available via h.
640 640 #==============================================================================
641 641 from rhodecode.lib.vcs.utils import author_name, author_email
642 642 from rhodecode.lib.utils2 import credentials_filter, age as _age
643 643 from rhodecode.model.db import User, ChangesetStatus
644 644
645 645 age = _age
646 646 capitalize = lambda x: x.capitalize()
647 647 email = author_email
648 648 short_id = lambda x: x[:12]
649 649 hide_credentials = lambda x: ''.join(credentials_filter(x))
650 650
651 651
652 def age_component(datetime_iso, value=None):
652 def age_component(datetime_iso, value=None, time_is_local=False):
653 653 title = value or format_date(datetime_iso)
654 654
655 # detect if we have a timezone info, if not assume UTC
655 # detect if we have a timezone info, otherwise, add it
656 656 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
657 657 tzinfo = '+00:00'
658 658
659 if time_is_local:
660 tzinfo = time.strftime("+%H:%M",
661 time.gmtime(
662 (datetime.now() - datetime.utcnow()).seconds + 1
663 )
664 )
665
659 666 return literal(
660 667 '<time class="timeago tooltip" '
661 668 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
662 669 datetime_iso, title, tzinfo))
663 670
664 671
665 672 def _shorten_commit_id(commit_id):
666 673 from rhodecode import CONFIG
667 674 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
668 675 return commit_id[:def_len]
669 676
670 677
671 678 def get_repo_id_from_name(repo_name):
672 679 repo = get_by_repo_name(repo_name)
673 680 return repo.repo_id
674 681
675 682
676 683 def show_id(commit):
677 684 """
678 685 Configurable function that shows ID
679 686 by default it's r123:fffeeefffeee
680 687
681 688 :param commit: commit instance
682 689 """
683 690 from rhodecode import CONFIG
684 691 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
685 692
686 693 raw_id = _shorten_commit_id(commit.raw_id)
687 694 if show_idx:
688 695 return 'r%s:%s' % (commit.idx, raw_id)
689 696 else:
690 697 return '%s' % (raw_id, )
691 698
692 699
693 700 def format_date(date):
694 701 """
695 702 use a standardized formatting for dates used in RhodeCode
696 703
697 704 :param date: date/datetime object
698 705 :return: formatted date
699 706 """
700 707
701 708 if date:
702 709 _fmt = "%a, %d %b %Y %H:%M:%S"
703 710 return safe_unicode(date.strftime(_fmt))
704 711
705 712 return u""
706 713
707 714
708 715 class _RepoChecker(object):
709 716
710 717 def __init__(self, backend_alias):
711 718 self._backend_alias = backend_alias
712 719
713 720 def __call__(self, repository):
714 721 if hasattr(repository, 'alias'):
715 722 _type = repository.alias
716 723 elif hasattr(repository, 'repo_type'):
717 724 _type = repository.repo_type
718 725 else:
719 726 _type = repository
720 727 return _type == self._backend_alias
721 728
722 729 is_git = _RepoChecker('git')
723 730 is_hg = _RepoChecker('hg')
724 731 is_svn = _RepoChecker('svn')
725 732
726 733
727 734 def get_repo_type_by_name(repo_name):
728 735 repo = Repository.get_by_repo_name(repo_name)
729 736 return repo.repo_type
730 737
731 738
732 739 def is_svn_without_proxy(repository):
733 740 from rhodecode import CONFIG
734 741 if is_svn(repository):
735 742 if not CONFIG.get('rhodecode_proxy_subversion_http_requests', False):
736 743 return True
737 744 return False
738 745
739 746
740 747 def email_or_none(author):
741 748 # extract email from the commit string
742 749 _email = author_email(author)
743 750 if _email != '':
744 751 # check it against RhodeCode database, and use the MAIN email for this
745 752 # user
746 753 user = User.get_by_email(_email, case_insensitive=True, cache=True)
747 754 if user is not None:
748 755 return user.email
749 756 return _email
750 757
751 758 # See if it contains a username we can get an email from
752 759 user = User.get_by_username(author_name(author), case_insensitive=True,
753 760 cache=True)
754 761 if user is not None:
755 762 return user.email
756 763
757 764 # No valid email, not a valid user in the system, none!
758 765 return None
759 766
760 767
761 768 def discover_user(author):
762 769 # if author is already an instance use it for extraction
763 770 if isinstance(author, User):
764 771 return author
765 772
766 773 # Valid email in the attribute passed, see if they're in the system
767 774 _email = email(author)
768 775 if _email != '':
769 776 user = User.get_by_email(_email, case_insensitive=True, cache=True)
770 777 if user is not None:
771 778 return user
772 779
773 780 # Maybe it's a username?
774 781 _author = author_name(author)
775 782 user = User.get_by_username(_author, case_insensitive=True,
776 783 cache=True)
777 784 if user is not None:
778 785 return user
779 786
780 787 return None
781 788
782 789
783 790 def link_to_user(author, length=0, **kwargs):
784 791 user = discover_user(author)
785 792 display_person = person(author, 'username_or_name_or_email')
786 793 if length:
787 794 display_person = shorter(display_person, length)
788 795
789 796 if user:
790 797 return link_to(
791 798 escape(display_person),
792 799 url('user_profile', username=user.username),
793 800 **kwargs)
794 801 else:
795 802 return escape(display_person)
796 803
797 804
798 805 def person(author, show_attr="username_and_name"):
799 806 # attr to return from fetched user
800 807 person_getter = lambda usr: getattr(usr, show_attr)
801 808 user = discover_user(author)
802 809 if user:
803 810 return person_getter(user)
804 811 else:
805 812 _author = author_name(author)
806 813 _email = email(author)
807 814 return _author or _email
808 815
809 816
810 817 def person_by_id(id_, show_attr="username_and_name"):
811 818 # attr to return from fetched user
812 819 person_getter = lambda usr: getattr(usr, show_attr)
813 820
814 821 #maybe it's an ID ?
815 822 if str(id_).isdigit() or isinstance(id_, int):
816 823 id_ = int(id_)
817 824 user = User.get(id_)
818 825 if user is not None:
819 826 return person_getter(user)
820 827 return id_
821 828
822 829
823 830 def gravatar_with_user(author):
824 831 from rhodecode.lib.utils import PartialRenderer
825 832 _render = PartialRenderer('base/base.html')
826 833 return _render('gravatar_with_user', author)
827 834
828 835
829 836 def desc_stylize(value):
830 837 """
831 838 converts tags from value into html equivalent
832 839
833 840 :param value:
834 841 """
835 842 if not value:
836 843 return ''
837 844
838 845 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
839 846 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
840 847 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
841 848 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
842 849 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
843 850 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
844 851 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
845 852 '<div class="metatag" tag="lang">\\2</div>', value)
846 853 value = re.sub(r'\[([a-z]+)\]',
847 854 '<div class="metatag" tag="\\1">\\1</div>', value)
848 855
849 856 return value
850 857
851 858
852 859 def escaped_stylize(value):
853 860 """
854 861 converts tags from value into html equivalent, but escaping its value first
855 862 """
856 863 if not value:
857 864 return ''
858 865
859 866 # Using default webhelper escape method, but has to force it as a
860 867 # plain unicode instead of a markup tag to be used in regex expressions
861 868 value = unicode(escape(safe_unicode(value)))
862 869
863 870 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
864 871 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
865 872 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
866 873 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
867 874 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
868 875 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
869 876 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
870 877 '<div class="metatag" tag="lang">\\2</div>', value)
871 878 value = re.sub(r'\[([a-z]+)\]',
872 879 '<div class="metatag" tag="\\1">\\1</div>', value)
873 880
874 881 return value
875 882
876 883
877 884 def bool2icon(value):
878 885 """
879 886 Returns boolean value of a given value, represented as html element with
880 887 classes that will represent icons
881 888
882 889 :param value: given value to convert to html node
883 890 """
884 891
885 892 if value: # does bool conversion
886 893 return HTML.tag('i', class_="icon-true")
887 894 else: # not true as bool
888 895 return HTML.tag('i', class_="icon-false")
889 896
890 897
891 898 #==============================================================================
892 899 # PERMS
893 900 #==============================================================================
894 901 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
895 902 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
896 903 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token
897 904
898 905
899 906 #==============================================================================
900 907 # GRAVATAR URL
901 908 #==============================================================================
902 909 class InitialsGravatar(object):
903 910 def __init__(self, email_address, first_name, last_name, size=30,
904 911 background=None, text_color='#fff'):
905 912 self.size = size
906 913 self.first_name = first_name
907 914 self.last_name = last_name
908 915 self.email_address = email_address
909 916 self.background = background or self.str2color(email_address)
910 917 self.text_color = text_color
911 918
912 919 def get_color_bank(self):
913 920 """
914 921 returns a predefined list of colors that gravatars can use.
915 922 Those are randomized distinct colors that guarantee readability and
916 923 uniqueness.
917 924
918 925 generated with: http://phrogz.net/css/distinct-colors.html
919 926 """
920 927 return [
921 928 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
922 929 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
923 930 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
924 931 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
925 932 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
926 933 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
927 934 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
928 935 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
929 936 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
930 937 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
931 938 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
932 939 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
933 940 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
934 941 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
935 942 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
936 943 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
937 944 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
938 945 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
939 946 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
940 947 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
941 948 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
942 949 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
943 950 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
944 951 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
945 952 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
946 953 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
947 954 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
948 955 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
949 956 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
950 957 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
951 958 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
952 959 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
953 960 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
954 961 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
955 962 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
956 963 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
957 964 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
958 965 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
959 966 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
960 967 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
961 968 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
962 969 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
963 970 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
964 971 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
965 972 '#4f8c46', '#368dd9', '#5c0073'
966 973 ]
967 974
968 975 def rgb_to_hex_color(self, rgb_tuple):
969 976 """
970 977 Converts an rgb_tuple passed to an hex color.
971 978
972 979 :param rgb_tuple: tuple with 3 ints represents rgb color space
973 980 """
974 981 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
975 982
976 983 def email_to_int_list(self, email_str):
977 984 """
978 985 Get every byte of the hex digest value of email and turn it to integer.
979 986 It's going to be always between 0-255
980 987 """
981 988 digest = md5_safe(email_str.lower())
982 989 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
983 990
984 991 def pick_color_bank_index(self, email_str, color_bank):
985 992 return self.email_to_int_list(email_str)[0] % len(color_bank)
986 993
987 994 def str2color(self, email_str):
988 995 """
989 996 Tries to map in a stable algorithm an email to color
990 997
991 998 :param email_str:
992 999 """
993 1000 color_bank = self.get_color_bank()
994 1001 # pick position (module it's length so we always find it in the
995 1002 # bank even if it's smaller than 256 values
996 1003 pos = self.pick_color_bank_index(email_str, color_bank)
997 1004 return color_bank[pos]
998 1005
999 1006 def normalize_email(self, email_address):
1000 1007 import unicodedata
1001 1008 # default host used to fill in the fake/missing email
1002 1009 default_host = u'localhost'
1003 1010
1004 1011 if not email_address:
1005 1012 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1006 1013
1007 1014 email_address = safe_unicode(email_address)
1008 1015
1009 1016 if u'@' not in email_address:
1010 1017 email_address = u'%s@%s' % (email_address, default_host)
1011 1018
1012 1019 if email_address.endswith(u'@'):
1013 1020 email_address = u'%s%s' % (email_address, default_host)
1014 1021
1015 1022 email_address = unicodedata.normalize('NFKD', email_address)\
1016 1023 .encode('ascii', 'ignore')
1017 1024 return email_address
1018 1025
1019 1026 def get_initials(self):
1020 1027 """
1021 1028 Returns 2 letter initials calculated based on the input.
1022 1029 The algorithm picks first given email address, and takes first letter
1023 1030 of part before @, and then the first letter of server name. In case
1024 1031 the part before @ is in a format of `somestring.somestring2` it replaces
1025 1032 the server letter with first letter of somestring2
1026 1033
1027 1034 In case function was initialized with both first and lastname, this
1028 1035 overrides the extraction from email by first letter of the first and
1029 1036 last name. We add special logic to that functionality, In case Full name
1030 1037 is compound, like Guido Von Rossum, we use last part of the last name
1031 1038 (Von Rossum) picking `R`.
1032 1039
1033 1040 Function also normalizes the non-ascii characters to they ascii
1034 1041 representation, eg Δ„ => A
1035 1042 """
1036 1043 import unicodedata
1037 1044 # replace non-ascii to ascii
1038 1045 first_name = unicodedata.normalize(
1039 1046 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1040 1047 last_name = unicodedata.normalize(
1041 1048 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1042 1049
1043 1050 # do NFKD encoding, and also make sure email has proper format
1044 1051 email_address = self.normalize_email(self.email_address)
1045 1052
1046 1053 # first push the email initials
1047 1054 prefix, server = email_address.split('@', 1)
1048 1055
1049 1056 # check if prefix is maybe a 'firstname.lastname' syntax
1050 1057 _dot_split = prefix.rsplit('.', 1)
1051 1058 if len(_dot_split) == 2:
1052 1059 initials = [_dot_split[0][0], _dot_split[1][0]]
1053 1060 else:
1054 1061 initials = [prefix[0], server[0]]
1055 1062
1056 1063 # then try to replace either firtname or lastname
1057 1064 fn_letter = (first_name or " ")[0].strip()
1058 1065 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1059 1066
1060 1067 if fn_letter:
1061 1068 initials[0] = fn_letter
1062 1069
1063 1070 if ln_letter:
1064 1071 initials[1] = ln_letter
1065 1072
1066 1073 return ''.join(initials).upper()
1067 1074
1068 1075 def get_img_data_by_type(self, font_family, img_type):
1069 1076 default_user = """
1070 1077 <svg xmlns="http://www.w3.org/2000/svg"
1071 1078 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1072 1079 viewBox="-15 -10 439.165 429.164"
1073 1080
1074 1081 xml:space="preserve"
1075 1082 style="background:{background};" >
1076 1083
1077 1084 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1078 1085 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1079 1086 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1080 1087 168.596,153.916,216.671,
1081 1088 204.583,216.671z" fill="{text_color}"/>
1082 1089 <path d="M407.164,374.717L360.88,
1083 1090 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1084 1091 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1085 1092 15.366-44.203,23.488-69.076,23.488c-24.877,
1086 1093 0-48.762-8.122-69.078-23.488
1087 1094 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1088 1095 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1089 1096 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1090 1097 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1091 1098 19.402-10.527 C409.699,390.129,
1092 1099 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1093 1100 </svg>""".format(
1094 1101 size=self.size,
1095 1102 background='#979797', # @grey4
1096 1103 text_color=self.text_color,
1097 1104 font_family=font_family)
1098 1105
1099 1106 return {
1100 1107 "default_user": default_user
1101 1108 }[img_type]
1102 1109
1103 1110 def get_img_data(self, svg_type=None):
1104 1111 """
1105 1112 generates the svg metadata for image
1106 1113 """
1107 1114
1108 1115 font_family = ','.join([
1109 1116 'proximanovaregular',
1110 1117 'Proxima Nova Regular',
1111 1118 'Proxima Nova',
1112 1119 'Arial',
1113 1120 'Lucida Grande',
1114 1121 'sans-serif'
1115 1122 ])
1116 1123 if svg_type:
1117 1124 return self.get_img_data_by_type(font_family, svg_type)
1118 1125
1119 1126 initials = self.get_initials()
1120 1127 img_data = """
1121 1128 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1122 1129 width="{size}" height="{size}"
1123 1130 style="width: 100%; height: 100%; background-color: {background}"
1124 1131 viewBox="0 0 {size} {size}">
1125 1132 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1126 1133 pointer-events="auto" fill="{text_color}"
1127 1134 font-family="{font_family}"
1128 1135 style="font-weight: 400; font-size: {f_size}px;">{text}
1129 1136 </text>
1130 1137 </svg>""".format(
1131 1138 size=self.size,
1132 1139 f_size=self.size/1.85, # scale the text inside the box nicely
1133 1140 background=self.background,
1134 1141 text_color=self.text_color,
1135 1142 text=initials.upper(),
1136 1143 font_family=font_family)
1137 1144
1138 1145 return img_data
1139 1146
1140 1147 def generate_svg(self, svg_type=None):
1141 1148 img_data = self.get_img_data(svg_type)
1142 1149 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1143 1150
1144 1151
1145 1152 def initials_gravatar(email_address, first_name, last_name, size=30):
1146 1153 svg_type = None
1147 1154 if email_address == User.DEFAULT_USER_EMAIL:
1148 1155 svg_type = 'default_user'
1149 1156 klass = InitialsGravatar(email_address, first_name, last_name, size)
1150 1157 return klass.generate_svg(svg_type=svg_type)
1151 1158
1152 1159
1153 1160 def gravatar_url(email_address, size=30):
1154 1161 # doh, we need to re-import those to mock it later
1155 1162 from pylons import tmpl_context as c
1156 1163
1157 1164 _use_gravatar = c.visual.use_gravatar
1158 1165 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1159 1166
1160 1167 email_address = email_address or User.DEFAULT_USER_EMAIL
1161 1168 if isinstance(email_address, unicode):
1162 1169 # hashlib crashes on unicode items
1163 1170 email_address = safe_str(email_address)
1164 1171
1165 1172 # empty email or default user
1166 1173 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1167 1174 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1168 1175
1169 1176 if _use_gravatar:
1170 1177 # TODO: Disuse pyramid thread locals. Think about another solution to
1171 1178 # get the host and schema here.
1172 1179 request = get_current_request()
1173 1180 tmpl = safe_str(_gravatar_url)
1174 1181 tmpl = tmpl.replace('{email}', email_address)\
1175 1182 .replace('{md5email}', md5_safe(email_address.lower())) \
1176 1183 .replace('{netloc}', request.host)\
1177 1184 .replace('{scheme}', request.scheme)\
1178 1185 .replace('{size}', safe_str(size))
1179 1186 return tmpl
1180 1187 else:
1181 1188 return initials_gravatar(email_address, '', '', size=size)
1182 1189
1183 1190
1184 1191 class Page(_Page):
1185 1192 """
1186 1193 Custom pager to match rendering style with paginator
1187 1194 """
1188 1195
1189 1196 def _get_pos(self, cur_page, max_page, items):
1190 1197 edge = (items / 2) + 1
1191 1198 if (cur_page <= edge):
1192 1199 radius = max(items / 2, items - cur_page)
1193 1200 elif (max_page - cur_page) < edge:
1194 1201 radius = (items - 1) - (max_page - cur_page)
1195 1202 else:
1196 1203 radius = items / 2
1197 1204
1198 1205 left = max(1, (cur_page - (radius)))
1199 1206 right = min(max_page, cur_page + (radius))
1200 1207 return left, cur_page, right
1201 1208
1202 1209 def _range(self, regexp_match):
1203 1210 """
1204 1211 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1205 1212
1206 1213 Arguments:
1207 1214
1208 1215 regexp_match
1209 1216 A "re" (regular expressions) match object containing the
1210 1217 radius of linked pages around the current page in
1211 1218 regexp_match.group(1) as a string
1212 1219
1213 1220 This function is supposed to be called as a callable in
1214 1221 re.sub.
1215 1222
1216 1223 """
1217 1224 radius = int(regexp_match.group(1))
1218 1225
1219 1226 # Compute the first and last page number within the radius
1220 1227 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1221 1228 # -> leftmost_page = 5
1222 1229 # -> rightmost_page = 9
1223 1230 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1224 1231 self.last_page,
1225 1232 (radius * 2) + 1)
1226 1233 nav_items = []
1227 1234
1228 1235 # Create a link to the first page (unless we are on the first page
1229 1236 # or there would be no need to insert '..' spacers)
1230 1237 if self.page != self.first_page and self.first_page < leftmost_page:
1231 1238 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1232 1239
1233 1240 # Insert dots if there are pages between the first page
1234 1241 # and the currently displayed page range
1235 1242 if leftmost_page - self.first_page > 1:
1236 1243 # Wrap in a SPAN tag if nolink_attr is set
1237 1244 text = '..'
1238 1245 if self.dotdot_attr:
1239 1246 text = HTML.span(c=text, **self.dotdot_attr)
1240 1247 nav_items.append(text)
1241 1248
1242 1249 for thispage in xrange(leftmost_page, rightmost_page + 1):
1243 1250 # Hilight the current page number and do not use a link
1244 1251 if thispage == self.page:
1245 1252 text = '%s' % (thispage,)
1246 1253 # Wrap in a SPAN tag if nolink_attr is set
1247 1254 if self.curpage_attr:
1248 1255 text = HTML.span(c=text, **self.curpage_attr)
1249 1256 nav_items.append(text)
1250 1257 # Otherwise create just a link to that page
1251 1258 else:
1252 1259 text = '%s' % (thispage,)
1253 1260 nav_items.append(self._pagerlink(thispage, text))
1254 1261
1255 1262 # Insert dots if there are pages between the displayed
1256 1263 # page numbers and the end of the page range
1257 1264 if self.last_page - rightmost_page > 1:
1258 1265 text = '..'
1259 1266 # Wrap in a SPAN tag if nolink_attr is set
1260 1267 if self.dotdot_attr:
1261 1268 text = HTML.span(c=text, **self.dotdot_attr)
1262 1269 nav_items.append(text)
1263 1270
1264 1271 # Create a link to the very last page (unless we are on the last
1265 1272 # page or there would be no need to insert '..' spacers)
1266 1273 if self.page != self.last_page and rightmost_page < self.last_page:
1267 1274 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1268 1275
1269 1276 ## prerender links
1270 1277 #_page_link = url.current()
1271 1278 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1272 1279 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1273 1280 return self.separator.join(nav_items)
1274 1281
1275 1282 def pager(self, format='~2~', page_param='page', partial_param='partial',
1276 1283 show_if_single_page=False, separator=' ', onclick=None,
1277 1284 symbol_first='<<', symbol_last='>>',
1278 1285 symbol_previous='<', symbol_next='>',
1279 1286 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1280 1287 curpage_attr={'class': 'pager_curpage'},
1281 1288 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1282 1289
1283 1290 self.curpage_attr = curpage_attr
1284 1291 self.separator = separator
1285 1292 self.pager_kwargs = kwargs
1286 1293 self.page_param = page_param
1287 1294 self.partial_param = partial_param
1288 1295 self.onclick = onclick
1289 1296 self.link_attr = link_attr
1290 1297 self.dotdot_attr = dotdot_attr
1291 1298
1292 1299 # Don't show navigator if there is no more than one page
1293 1300 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1294 1301 return ''
1295 1302
1296 1303 from string import Template
1297 1304 # Replace ~...~ in token format by range of pages
1298 1305 result = re.sub(r'~(\d+)~', self._range, format)
1299 1306
1300 1307 # Interpolate '%' variables
1301 1308 result = Template(result).safe_substitute({
1302 1309 'first_page': self.first_page,
1303 1310 'last_page': self.last_page,
1304 1311 'page': self.page,
1305 1312 'page_count': self.page_count,
1306 1313 'items_per_page': self.items_per_page,
1307 1314 'first_item': self.first_item,
1308 1315 'last_item': self.last_item,
1309 1316 'item_count': self.item_count,
1310 1317 'link_first': self.page > self.first_page and \
1311 1318 self._pagerlink(self.first_page, symbol_first) or '',
1312 1319 'link_last': self.page < self.last_page and \
1313 1320 self._pagerlink(self.last_page, symbol_last) or '',
1314 1321 'link_previous': self.previous_page and \
1315 1322 self._pagerlink(self.previous_page, symbol_previous) \
1316 1323 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1317 1324 'link_next': self.next_page and \
1318 1325 self._pagerlink(self.next_page, symbol_next) \
1319 1326 or HTML.span(symbol_next, class_="pg-next disabled")
1320 1327 })
1321 1328
1322 1329 return literal(result)
1323 1330
1324 1331
1325 1332 #==============================================================================
1326 1333 # REPO PAGER, PAGER FOR REPOSITORY
1327 1334 #==============================================================================
1328 1335 class RepoPage(Page):
1329 1336
1330 1337 def __init__(self, collection, page=1, items_per_page=20,
1331 1338 item_count=None, url=None, **kwargs):
1332 1339
1333 1340 """Create a "RepoPage" instance. special pager for paging
1334 1341 repository
1335 1342 """
1336 1343 self._url_generator = url
1337 1344
1338 1345 # Safe the kwargs class-wide so they can be used in the pager() method
1339 1346 self.kwargs = kwargs
1340 1347
1341 1348 # Save a reference to the collection
1342 1349 self.original_collection = collection
1343 1350
1344 1351 self.collection = collection
1345 1352
1346 1353 # The self.page is the number of the current page.
1347 1354 # The first page has the number 1!
1348 1355 try:
1349 1356 self.page = int(page) # make it int() if we get it as a string
1350 1357 except (ValueError, TypeError):
1351 1358 self.page = 1
1352 1359
1353 1360 self.items_per_page = items_per_page
1354 1361
1355 1362 # Unless the user tells us how many items the collections has
1356 1363 # we calculate that ourselves.
1357 1364 if item_count is not None:
1358 1365 self.item_count = item_count
1359 1366 else:
1360 1367 self.item_count = len(self.collection)
1361 1368
1362 1369 # Compute the number of the first and last available page
1363 1370 if self.item_count > 0:
1364 1371 self.first_page = 1
1365 1372 self.page_count = int(math.ceil(float(self.item_count) /
1366 1373 self.items_per_page))
1367 1374 self.last_page = self.first_page + self.page_count - 1
1368 1375
1369 1376 # Make sure that the requested page number is the range of
1370 1377 # valid pages
1371 1378 if self.page > self.last_page:
1372 1379 self.page = self.last_page
1373 1380 elif self.page < self.first_page:
1374 1381 self.page = self.first_page
1375 1382
1376 1383 # Note: the number of items on this page can be less than
1377 1384 # items_per_page if the last page is not full
1378 1385 self.first_item = max(0, (self.item_count) - (self.page *
1379 1386 items_per_page))
1380 1387 self.last_item = ((self.item_count - 1) - items_per_page *
1381 1388 (self.page - 1))
1382 1389
1383 1390 self.items = list(self.collection[self.first_item:self.last_item + 1])
1384 1391
1385 1392 # Links to previous and next page
1386 1393 if self.page > self.first_page:
1387 1394 self.previous_page = self.page - 1
1388 1395 else:
1389 1396 self.previous_page = None
1390 1397
1391 1398 if self.page < self.last_page:
1392 1399 self.next_page = self.page + 1
1393 1400 else:
1394 1401 self.next_page = None
1395 1402
1396 1403 # No items available
1397 1404 else:
1398 1405 self.first_page = None
1399 1406 self.page_count = 0
1400 1407 self.last_page = None
1401 1408 self.first_item = None
1402 1409 self.last_item = None
1403 1410 self.previous_page = None
1404 1411 self.next_page = None
1405 1412 self.items = []
1406 1413
1407 1414 # This is a subclass of the 'list' type. Initialise the list now.
1408 1415 list.__init__(self, reversed(self.items))
1409 1416
1410 1417
1411 1418 def changed_tooltip(nodes):
1412 1419 """
1413 1420 Generates a html string for changed nodes in commit page.
1414 1421 It limits the output to 30 entries
1415 1422
1416 1423 :param nodes: LazyNodesGenerator
1417 1424 """
1418 1425 if nodes:
1419 1426 pref = ': <br/> '
1420 1427 suf = ''
1421 1428 if len(nodes) > 30:
1422 1429 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1423 1430 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1424 1431 for x in nodes[:30]]) + suf)
1425 1432 else:
1426 1433 return ': ' + _('No Files')
1427 1434
1428 1435
1429 1436 def breadcrumb_repo_link(repo):
1430 1437 """
1431 1438 Makes a breadcrumbs path link to repo
1432 1439
1433 1440 ex::
1434 1441 group >> subgroup >> repo
1435 1442
1436 1443 :param repo: a Repository instance
1437 1444 """
1438 1445
1439 1446 path = [
1440 1447 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1441 1448 for group in repo.groups_with_parents
1442 1449 ] + [
1443 1450 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1444 1451 ]
1445 1452
1446 1453 return literal(' &raquo; '.join(path))
1447 1454
1448 1455
1449 1456 def format_byte_size_binary(file_size):
1450 1457 """
1451 1458 Formats file/folder sizes to standard.
1452 1459 """
1453 1460 formatted_size = format_byte_size(file_size, binary=True)
1454 1461 return formatted_size
1455 1462
1456 1463
1457 1464 def fancy_file_stats(stats):
1458 1465 """
1459 1466 Displays a fancy two colored bar for number of added/deleted
1460 1467 lines of code on file
1461 1468
1462 1469 :param stats: two element list of added/deleted lines of code
1463 1470 """
1464 1471 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1465 1472 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1466 1473
1467 1474 def cgen(l_type, a_v, d_v):
1468 1475 mapping = {'tr': 'top-right-rounded-corner-mid',
1469 1476 'tl': 'top-left-rounded-corner-mid',
1470 1477 'br': 'bottom-right-rounded-corner-mid',
1471 1478 'bl': 'bottom-left-rounded-corner-mid'}
1472 1479 map_getter = lambda x: mapping[x]
1473 1480
1474 1481 if l_type == 'a' and d_v:
1475 1482 #case when added and deleted are present
1476 1483 return ' '.join(map(map_getter, ['tl', 'bl']))
1477 1484
1478 1485 if l_type == 'a' and not d_v:
1479 1486 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1480 1487
1481 1488 if l_type == 'd' and a_v:
1482 1489 return ' '.join(map(map_getter, ['tr', 'br']))
1483 1490
1484 1491 if l_type == 'd' and not a_v:
1485 1492 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1486 1493
1487 1494 a, d = stats['added'], stats['deleted']
1488 1495 width = 100
1489 1496
1490 1497 if stats['binary']: # binary operations like chmod/rename etc
1491 1498 lbl = []
1492 1499 bin_op = 0 # undefined
1493 1500
1494 1501 # prefix with bin for binary files
1495 1502 if BIN_FILENODE in stats['ops']:
1496 1503 lbl += ['bin']
1497 1504
1498 1505 if NEW_FILENODE in stats['ops']:
1499 1506 lbl += [_('new file')]
1500 1507 bin_op = NEW_FILENODE
1501 1508 elif MOD_FILENODE in stats['ops']:
1502 1509 lbl += [_('mod')]
1503 1510 bin_op = MOD_FILENODE
1504 1511 elif DEL_FILENODE in stats['ops']:
1505 1512 lbl += [_('del')]
1506 1513 bin_op = DEL_FILENODE
1507 1514 elif RENAMED_FILENODE in stats['ops']:
1508 1515 lbl += [_('rename')]
1509 1516 bin_op = RENAMED_FILENODE
1510 1517
1511 1518 # chmod can go with other operations, so we add a + to lbl if needed
1512 1519 if CHMOD_FILENODE in stats['ops']:
1513 1520 lbl += [_('chmod')]
1514 1521 if bin_op == 0:
1515 1522 bin_op = CHMOD_FILENODE
1516 1523
1517 1524 lbl = '+'.join(lbl)
1518 1525 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1519 1526 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1520 1527 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1521 1528 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1522 1529
1523 1530 t = stats['added'] + stats['deleted']
1524 1531 unit = float(width) / (t or 1)
1525 1532
1526 1533 # needs > 9% of width to be visible or 0 to be hidden
1527 1534 a_p = max(9, unit * a) if a > 0 else 0
1528 1535 d_p = max(9, unit * d) if d > 0 else 0
1529 1536 p_sum = a_p + d_p
1530 1537
1531 1538 if p_sum > width:
1532 1539 #adjust the percentage to be == 100% since we adjusted to 9
1533 1540 if a_p > d_p:
1534 1541 a_p = a_p - (p_sum - width)
1535 1542 else:
1536 1543 d_p = d_p - (p_sum - width)
1537 1544
1538 1545 a_v = a if a > 0 else ''
1539 1546 d_v = d if d > 0 else ''
1540 1547
1541 1548 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1542 1549 cgen('a', a_v, d_v), a_p, a_v
1543 1550 )
1544 1551 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1545 1552 cgen('d', a_v, d_v), d_p, d_v
1546 1553 )
1547 1554 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1548 1555
1549 1556
1550 1557 def urlify_text(text_, safe=True):
1551 1558 """
1552 1559 Extrac urls from text and make html links out of them
1553 1560
1554 1561 :param text_:
1555 1562 """
1556 1563
1557 1564 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1558 1565 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1559 1566
1560 1567 def url_func(match_obj):
1561 1568 url_full = match_obj.groups()[0]
1562 1569 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1563 1570 _newtext = url_pat.sub(url_func, text_)
1564 1571 if safe:
1565 1572 return literal(_newtext)
1566 1573 return _newtext
1567 1574
1568 1575
1569 1576 def urlify_commits(text_, repository):
1570 1577 """
1571 1578 Extract commit ids from text and make link from them
1572 1579
1573 1580 :param text_:
1574 1581 :param repository: repo name to build the URL with
1575 1582 """
1576 1583 from pylons import url # doh, we need to re-import url to mock it later
1577 1584 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1578 1585
1579 1586 def url_func(match_obj):
1580 1587 commit_id = match_obj.groups()[1]
1581 1588 pref = match_obj.groups()[0]
1582 1589 suf = match_obj.groups()[2]
1583 1590
1584 1591 tmpl = (
1585 1592 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1586 1593 '%(commit_id)s</a>%(suf)s'
1587 1594 )
1588 1595 return tmpl % {
1589 1596 'pref': pref,
1590 1597 'cls': 'revision-link',
1591 1598 'url': url('changeset_home', repo_name=repository,
1592 1599 revision=commit_id),
1593 1600 'commit_id': commit_id,
1594 1601 'suf': suf
1595 1602 }
1596 1603
1597 1604 newtext = URL_PAT.sub(url_func, text_)
1598 1605
1599 1606 return newtext
1600 1607
1601 1608
1602 1609 def _process_url_func(match_obj, repo_name, uid, entry):
1603 1610 pref = ''
1604 1611 if match_obj.group().startswith(' '):
1605 1612 pref = ' '
1606 1613
1607 1614 issue_id = ''.join(match_obj.groups())
1608 1615 tmpl = (
1609 1616 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1610 1617 '%(issue-prefix)s%(id-repr)s'
1611 1618 '</a>')
1612 1619
1613 1620 (repo_name_cleaned,
1614 1621 parent_group_name) = RepoGroupModel().\
1615 1622 _get_group_name_and_parent(repo_name)
1616 1623
1617 1624 # variables replacement
1618 1625 named_vars = {
1619 1626 'id': issue_id,
1620 1627 'repo': repo_name,
1621 1628 'repo_name': repo_name_cleaned,
1622 1629 'group_name': parent_group_name
1623 1630 }
1624 1631 # named regex variables
1625 1632 named_vars.update(match_obj.groupdict())
1626 1633 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1627 1634
1628 1635 return tmpl % {
1629 1636 'pref': pref,
1630 1637 'cls': 'issue-tracker-link',
1631 1638 'url': _url,
1632 1639 'id-repr': issue_id,
1633 1640 'issue-prefix': entry['pref'],
1634 1641 'serv': entry['url'],
1635 1642 }
1636 1643
1637 1644
1638 1645 def process_patterns(text_string, repo_name, config):
1639 1646 repo = None
1640 1647 if repo_name:
1641 1648 # Retrieving repo_name to avoid invalid repo_name to explode on
1642 1649 # IssueTrackerSettingsModel but still passing invalid name further down
1643 1650 repo = Repository.get_by_repo_name(repo_name)
1644 1651
1645 1652 settings_model = IssueTrackerSettingsModel(repo=repo)
1646 1653 active_entries = settings_model.get_settings()
1647 1654
1648 1655 newtext = text_string
1649 1656 for uid, entry in active_entries.items():
1650 1657 url_func = partial(
1651 1658 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1652 1659
1653 1660 log.debug('found issue tracker entry with uid %s' % (uid,))
1654 1661
1655 1662 if not (entry['pat'] and entry['url']):
1656 1663 log.debug('skipping due to missing data')
1657 1664 continue
1658 1665
1659 1666 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1660 1667 % (uid, entry['pat'], entry['url'], entry['pref']))
1661 1668
1662 1669 try:
1663 1670 pattern = re.compile(r'%s' % entry['pat'])
1664 1671 except re.error:
1665 1672 log.exception(
1666 1673 'issue tracker pattern: `%s` failed to compile',
1667 1674 entry['pat'])
1668 1675 continue
1669 1676
1670 1677 newtext = pattern.sub(url_func, newtext)
1671 1678 log.debug('processed prefix:uid `%s`' % (uid,))
1672 1679
1673 1680 return newtext
1674 1681
1675 1682
1676 1683 def urlify_commit_message(commit_text, repository=None):
1677 1684 """
1678 1685 Parses given text message and makes proper links.
1679 1686 issues are linked to given issue-server, and rest is a commit link
1680 1687
1681 1688 :param commit_text:
1682 1689 :param repository:
1683 1690 """
1684 1691 from pylons import url # doh, we need to re-import url to mock it later
1685 1692 from rhodecode import CONFIG
1686 1693
1687 1694 def escaper(string):
1688 1695 return string.replace('<', '&lt;').replace('>', '&gt;')
1689 1696
1690 1697 newtext = escaper(commit_text)
1691 1698 # urlify commits - extract commit ids and make link out of them, if we have
1692 1699 # the scope of repository present.
1693 1700 if repository:
1694 1701 newtext = urlify_commits(newtext, repository)
1695 1702
1696 1703 # extract http/https links and make them real urls
1697 1704 newtext = urlify_text(newtext, safe=False)
1698 1705
1699 1706 # process issue tracker patterns
1700 1707 newtext = process_patterns(newtext, repository or '', CONFIG)
1701 1708
1702 1709 return literal(newtext)
1703 1710
1704 1711
1705 1712 def rst(source, mentions=False):
1706 1713 return literal('<div class="rst-block">%s</div>' %
1707 1714 MarkupRenderer.rst(source, mentions=mentions))
1708 1715
1709 1716
1710 1717 def markdown(source, mentions=False):
1711 1718 return literal('<div class="markdown-block">%s</div>' %
1712 1719 MarkupRenderer.markdown(source, flavored=False,
1713 1720 mentions=mentions))
1714 1721
1715 1722 def renderer_from_filename(filename, exclude=None):
1716 1723 from rhodecode.config.conf import MARKDOWN_EXTS, RST_EXTS
1717 1724
1718 1725 def _filter(elements):
1719 1726 if isinstance(exclude, (list, tuple)):
1720 1727 return [x for x in elements if x not in exclude]
1721 1728 return elements
1722 1729
1723 1730 if filename.endswith(tuple(_filter([x[0] for x in MARKDOWN_EXTS if x[0]]))):
1724 1731 return 'markdown'
1725 1732 if filename.endswith(tuple(_filter([x[0] for x in RST_EXTS if x[0]]))):
1726 1733 return 'rst'
1727 1734
1728 1735
1729 1736 def render(source, renderer='rst', mentions=False):
1730 1737 if renderer == 'rst':
1731 1738 return rst(source, mentions=mentions)
1732 1739 if renderer == 'markdown':
1733 1740 return markdown(source, mentions=mentions)
1734 1741
1735 1742
1736 1743 def commit_status(repo, commit_id):
1737 1744 return ChangesetStatusModel().get_status(repo, commit_id)
1738 1745
1739 1746
1740 1747 def commit_status_lbl(commit_status):
1741 1748 return dict(ChangesetStatus.STATUSES).get(commit_status)
1742 1749
1743 1750
1744 1751 def commit_time(repo_name, commit_id):
1745 1752 repo = Repository.get_by_repo_name(repo_name)
1746 1753 commit = repo.get_commit(commit_id=commit_id)
1747 1754 return commit.date
1748 1755
1749 1756
1750 1757 def get_permission_name(key):
1751 1758 return dict(Permission.PERMS).get(key)
1752 1759
1753 1760
1754 1761 def journal_filter_help():
1755 1762 return _(
1756 1763 'Example filter terms:\n' +
1757 1764 ' repository:vcs\n' +
1758 1765 ' username:marcin\n' +
1759 1766 ' action:*push*\n' +
1760 1767 ' ip:127.0.0.1\n' +
1761 1768 ' date:20120101\n' +
1762 1769 ' date:[20120101100000 TO 20120102]\n' +
1763 1770 '\n' +
1764 1771 'Generate wildcards using \'*\' character:\n' +
1765 1772 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1766 1773 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1767 1774 '\n' +
1768 1775 'Optional AND / OR operators in queries\n' +
1769 1776 ' "repository:vcs OR repository:test"\n' +
1770 1777 ' "username:test AND repository:test*"\n'
1771 1778 )
1772 1779
1773 1780
1774 1781 def not_mapped_error(repo_name):
1775 1782 flash(_('%s repository is not mapped to db perhaps'
1776 1783 ' it was created or renamed from the filesystem'
1777 1784 ' please run the application again'
1778 1785 ' in order to rescan repositories') % repo_name, category='error')
1779 1786
1780 1787
1781 1788 def ip_range(ip_addr):
1782 1789 from rhodecode.model.db import UserIpMap
1783 1790 s, e = UserIpMap._get_ip_range(ip_addr)
1784 1791 return '%s - %s' % (s, e)
1785 1792
1786 1793
1787 1794 def form(url, method='post', needs_csrf_token=True, **attrs):
1788 1795 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1789 1796 if method.lower() != 'get' and needs_csrf_token:
1790 1797 raise Exception(
1791 1798 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1792 1799 'CSRF token. If the endpoint does not require such token you can ' +
1793 1800 'explicitly set the parameter needs_csrf_token to false.')
1794 1801
1795 1802 return wh_form(url, method=method, **attrs)
1796 1803
1797 1804
1798 1805 def secure_form(url, method="POST", multipart=False, **attrs):
1799 1806 """Start a form tag that points the action to an url. This
1800 1807 form tag will also include the hidden field containing
1801 1808 the auth token.
1802 1809
1803 1810 The url options should be given either as a string, or as a
1804 1811 ``url()`` function. The method for the form defaults to POST.
1805 1812
1806 1813 Options:
1807 1814
1808 1815 ``multipart``
1809 1816 If set to True, the enctype is set to "multipart/form-data".
1810 1817 ``method``
1811 1818 The method to use when submitting the form, usually either
1812 1819 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1813 1820 hidden input with name _method is added to simulate the verb
1814 1821 over POST.
1815 1822
1816 1823 """
1817 1824 from webhelpers.pylonslib.secure_form import insecure_form
1818 1825 from rhodecode.lib.auth import get_csrf_token, csrf_token_key
1819 1826 form = insecure_form(url, method, multipart, **attrs)
1820 1827 token = HTML.div(hidden(csrf_token_key, get_csrf_token()), style="display: none;")
1821 1828 return literal("%s\n%s" % (form, token))
1822 1829
1823 1830 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1824 1831 select_html = select(name, selected, options, **attrs)
1825 1832 select2 = """
1826 1833 <script>
1827 1834 $(document).ready(function() {
1828 1835 $('#%s').select2({
1829 1836 containerCssClass: 'drop-menu',
1830 1837 dropdownCssClass: 'drop-menu-dropdown',
1831 1838 dropdownAutoWidth: true%s
1832 1839 });
1833 1840 });
1834 1841 </script>
1835 1842 """
1836 1843 filter_option = """,
1837 1844 minimumResultsForSearch: -1
1838 1845 """
1839 1846 input_id = attrs.get('id') or name
1840 1847 filter_enabled = "" if enable_filter else filter_option
1841 1848 select_script = literal(select2 % (input_id, filter_enabled))
1842 1849
1843 1850 return literal(select_html+select_script)
1844 1851
1845 1852
1846 1853 def get_visual_attr(tmpl_context_var, attr_name):
1847 1854 """
1848 1855 A safe way to get a variable from visual variable of template context
1849 1856
1850 1857 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1851 1858 :param attr_name: name of the attribute we fetch from the c.visual
1852 1859 """
1853 1860 visual = getattr(tmpl_context_var, 'visual', None)
1854 1861 if not visual:
1855 1862 return
1856 1863 else:
1857 1864 return getattr(visual, attr_name, None)
1858 1865
1859 1866
1860 1867 def get_last_path_part(file_node):
1861 1868 if not file_node.path:
1862 1869 return u''
1863 1870
1864 1871 path = safe_unicode(file_node.path.split('/')[-1])
1865 1872 return u'../' + path
1866 1873
1867 1874
1868 1875 def route_path(*args, **kwds):
1869 1876 """
1870 1877 Wrapper around pyramids `route_path` function. It is used to generate
1871 1878 URLs from within pylons views or templates. This will be removed when
1872 1879 pyramid migration if finished.
1873 1880 """
1874 1881 req = get_current_request()
1875 1882 return req.route_path(*args, **kwds)
1876 1883
1877 1884
1878 1885 def resource_path(*args, **kwds):
1879 1886 """
1880 1887 Wrapper around pyramids `route_path` function. It is used to generate
1881 1888 URLs from within pylons views or templates. This will be removed when
1882 1889 pyramid migration if finished.
1883 1890 """
1884 1891 req = get_current_request()
1885 1892 return req.resource_path(*args, **kwds)
@@ -1,843 +1,853 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Some simple helper functions
24 24 """
25 25
26 26
27 27 import collections
28 28 import datetime
29 29 import dateutil.relativedelta
30 30 import hashlib
31 31 import logging
32 32 import re
33 33 import sys
34 34 import time
35 35 import threading
36 36 import urllib
37 37 import urlobject
38 38 import uuid
39 39
40 40 import pygments.lexers
41 41 import sqlalchemy
42 42 import sqlalchemy.engine.url
43 43 import webob
44 44
45 45 import rhodecode
46 46
47 47
48 48 def md5(s):
49 49 return hashlib.md5(s).hexdigest()
50 50
51 51
52 52 def md5_safe(s):
53 53 return md5(safe_str(s))
54 54
55 55
56 56 def __get_lem():
57 57 """
58 58 Get language extension map based on what's inside pygments lexers
59 59 """
60 60 d = collections.defaultdict(lambda: [])
61 61
62 62 def __clean(s):
63 63 s = s.lstrip('*')
64 64 s = s.lstrip('.')
65 65
66 66 if s.find('[') != -1:
67 67 exts = []
68 68 start, stop = s.find('['), s.find(']')
69 69
70 70 for suffix in s[start + 1:stop]:
71 71 exts.append(s[:s.find('[')] + suffix)
72 72 return [e.lower() for e in exts]
73 73 else:
74 74 return [s.lower()]
75 75
76 76 for lx, t in sorted(pygments.lexers.LEXERS.items()):
77 77 m = map(__clean, t[-2])
78 78 if m:
79 79 m = reduce(lambda x, y: x + y, m)
80 80 for ext in m:
81 81 desc = lx.replace('Lexer', '')
82 82 d[ext].append(desc)
83 83
84 84 return dict(d)
85 85
86 86
87 87 def str2bool(_str):
88 88 """
89 89 returs True/False value from given string, it tries to translate the
90 90 string into boolean
91 91
92 92 :param _str: string value to translate into boolean
93 93 :rtype: boolean
94 94 :returns: boolean from given string
95 95 """
96 96 if _str is None:
97 97 return False
98 98 if _str in (True, False):
99 99 return _str
100 100 _str = str(_str).strip().lower()
101 101 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
102 102
103 103
104 104 def aslist(obj, sep=None, strip=True):
105 105 """
106 106 Returns given string separated by sep as list
107 107
108 108 :param obj:
109 109 :param sep:
110 110 :param strip:
111 111 """
112 112 if isinstance(obj, (basestring)):
113 113 lst = obj.split(sep)
114 114 if strip:
115 115 lst = [v.strip() for v in lst]
116 116 return lst
117 117 elif isinstance(obj, (list, tuple)):
118 118 return obj
119 119 elif obj is None:
120 120 return []
121 121 else:
122 122 return [obj]
123 123
124 124
125 125 def convert_line_endings(line, mode):
126 126 """
127 127 Converts a given line "line end" accordingly to given mode
128 128
129 129 Available modes are::
130 130 0 - Unix
131 131 1 - Mac
132 132 2 - DOS
133 133
134 134 :param line: given line to convert
135 135 :param mode: mode to convert to
136 136 :rtype: str
137 137 :return: converted line according to mode
138 138 """
139 139 if mode == 0:
140 140 line = line.replace('\r\n', '\n')
141 141 line = line.replace('\r', '\n')
142 142 elif mode == 1:
143 143 line = line.replace('\r\n', '\r')
144 144 line = line.replace('\n', '\r')
145 145 elif mode == 2:
146 146 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
147 147 return line
148 148
149 149
150 150 def detect_mode(line, default):
151 151 """
152 152 Detects line break for given line, if line break couldn't be found
153 153 given default value is returned
154 154
155 155 :param line: str line
156 156 :param default: default
157 157 :rtype: int
158 158 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
159 159 """
160 160 if line.endswith('\r\n'):
161 161 return 2
162 162 elif line.endswith('\n'):
163 163 return 0
164 164 elif line.endswith('\r'):
165 165 return 1
166 166 else:
167 167 return default
168 168
169 169
170 170 def safe_int(val, default=None):
171 171 """
172 172 Returns int() of val if val is not convertable to int use default
173 173 instead
174 174
175 175 :param val:
176 176 :param default:
177 177 """
178 178
179 179 try:
180 180 val = int(val)
181 181 except (ValueError, TypeError):
182 182 val = default
183 183
184 184 return val
185 185
186 186
187 187 def safe_unicode(str_, from_encoding=None):
188 188 """
189 189 safe unicode function. Does few trick to turn str_ into unicode
190 190
191 191 In case of UnicodeDecode error, we try to return it with encoding detected
192 192 by chardet library if it fails fallback to unicode with errors replaced
193 193
194 194 :param str_: string to decode
195 195 :rtype: unicode
196 196 :returns: unicode object
197 197 """
198 198 if isinstance(str_, unicode):
199 199 return str_
200 200
201 201 if not from_encoding:
202 202 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
203 203 'utf8'), sep=',')
204 204 from_encoding = DEFAULT_ENCODINGS
205 205
206 206 if not isinstance(from_encoding, (list, tuple)):
207 207 from_encoding = [from_encoding]
208 208
209 209 try:
210 210 return unicode(str_)
211 211 except UnicodeDecodeError:
212 212 pass
213 213
214 214 for enc in from_encoding:
215 215 try:
216 216 return unicode(str_, enc)
217 217 except UnicodeDecodeError:
218 218 pass
219 219
220 220 try:
221 221 import chardet
222 222 encoding = chardet.detect(str_)['encoding']
223 223 if encoding is None:
224 224 raise Exception()
225 225 return str_.decode(encoding)
226 226 except (ImportError, UnicodeDecodeError, Exception):
227 227 return unicode(str_, from_encoding[0], 'replace')
228 228
229 229
230 230 def safe_str(unicode_, to_encoding=None):
231 231 """
232 232 safe str function. Does few trick to turn unicode_ into string
233 233
234 234 In case of UnicodeEncodeError, we try to return it with encoding detected
235 235 by chardet library if it fails fallback to string with errors replaced
236 236
237 237 :param unicode_: unicode to encode
238 238 :rtype: str
239 239 :returns: str object
240 240 """
241 241
242 242 # if it's not basestr cast to str
243 243 if not isinstance(unicode_, basestring):
244 244 return str(unicode_)
245 245
246 246 if isinstance(unicode_, str):
247 247 return unicode_
248 248
249 249 if not to_encoding:
250 250 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
251 251 'utf8'), sep=',')
252 252 to_encoding = DEFAULT_ENCODINGS
253 253
254 254 if not isinstance(to_encoding, (list, tuple)):
255 255 to_encoding = [to_encoding]
256 256
257 257 for enc in to_encoding:
258 258 try:
259 259 return unicode_.encode(enc)
260 260 except UnicodeEncodeError:
261 261 pass
262 262
263 263 try:
264 264 import chardet
265 265 encoding = chardet.detect(unicode_)['encoding']
266 266 if encoding is None:
267 267 raise UnicodeEncodeError()
268 268
269 269 return unicode_.encode(encoding)
270 270 except (ImportError, UnicodeEncodeError):
271 271 return unicode_.encode(to_encoding[0], 'replace')
272 272
273 273
274 274 def remove_suffix(s, suffix):
275 275 if s.endswith(suffix):
276 276 s = s[:-1 * len(suffix)]
277 277 return s
278 278
279 279
280 280 def remove_prefix(s, prefix):
281 281 if s.startswith(prefix):
282 282 s = s[len(prefix):]
283 283 return s
284 284
285 285
286 286 def find_calling_context(ignore_modules=None):
287 287 """
288 288 Look through the calling stack and return the frame which called
289 289 this function and is part of core module ( ie. rhodecode.* )
290 290
291 291 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
292 292 """
293 293
294 294 ignore_modules = ignore_modules or []
295 295
296 296 f = sys._getframe(2)
297 297 while f.f_back is not None:
298 298 name = f.f_globals.get('__name__')
299 299 if name and name.startswith(__name__.split('.')[0]):
300 300 if name not in ignore_modules:
301 301 return f
302 302 f = f.f_back
303 303 return None
304 304
305 305
306 306 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
307 307 """Custom engine_from_config functions."""
308 308 log = logging.getLogger('sqlalchemy.engine')
309 309 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
310 310
311 311 def color_sql(sql):
312 312 color_seq = '\033[1;33m' # This is yellow: code 33
313 313 normal = '\x1b[0m'
314 314 return ''.join([color_seq, sql, normal])
315 315
316 316 if configuration['debug']:
317 317 # attach events only for debug configuration
318 318
319 319 def before_cursor_execute(conn, cursor, statement,
320 320 parameters, context, executemany):
321 321 setattr(conn, 'query_start_time', time.time())
322 322 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
323 323 calling_context = find_calling_context(ignore_modules=[
324 324 'rhodecode.lib.caching_query'
325 325 ])
326 326 if calling_context:
327 327 log.info(color_sql('call context %s:%s' % (
328 328 calling_context.f_code.co_filename,
329 329 calling_context.f_lineno,
330 330 )))
331 331
332 332 def after_cursor_execute(conn, cursor, statement,
333 333 parameters, context, executemany):
334 334 delattr(conn, 'query_start_time')
335 335
336 336 sqlalchemy.event.listen(engine, "before_cursor_execute",
337 337 before_cursor_execute)
338 338 sqlalchemy.event.listen(engine, "after_cursor_execute",
339 339 after_cursor_execute)
340 340
341 341 return engine
342 342
343 343
344 344 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
345 345 short_format=False):
346 346 """
347 347 Turns a datetime into an age string.
348 348 If show_short_version is True, this generates a shorter string with
349 349 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
350 350
351 351 * IMPORTANT*
352 352 Code of this function is written in special way so it's easier to
353 353 backport it to javascript. If you mean to update it, please also update
354 354 `jquery.timeago-extension.js` file
355 355
356 356 :param prevdate: datetime object
357 357 :param now: get current time, if not define we use
358 358 `datetime.datetime.now()`
359 359 :param show_short_version: if it should approximate the date and
360 360 return a shorter string
361 361 :param show_suffix:
362 362 :param short_format: show short format, eg 2D instead of 2 days
363 363 :rtype: unicode
364 364 :returns: unicode words describing age
365 365 """
366 366 from pylons.i18n.translation import _, ungettext
367 367
368 368 def _get_relative_delta(now, prevdate):
369 369 base = dateutil.relativedelta.relativedelta(now, prevdate)
370 370 return {
371 371 'year': base.years,
372 372 'month': base.months,
373 373 'day': base.days,
374 374 'hour': base.hours,
375 375 'minute': base.minutes,
376 376 'second': base.seconds,
377 377 }
378 378
379 379 def _is_leap_year(year):
380 380 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
381 381
382 382 def get_month(prevdate):
383 383 return prevdate.month
384 384
385 385 def get_year(prevdate):
386 386 return prevdate.year
387 387
388 388 now = now or datetime.datetime.now()
389 389 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
390 390 deltas = {}
391 391 future = False
392 392
393 393 if prevdate > now:
394 394 now_old = now
395 395 now = prevdate
396 396 prevdate = now_old
397 397 future = True
398 398 if future:
399 399 prevdate = prevdate.replace(microsecond=0)
400 400 # Get date parts deltas
401 401 for part in order:
402 402 rel_delta = _get_relative_delta(now, prevdate)
403 403 deltas[part] = rel_delta[part]
404 404
405 405 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
406 406 # not 1 hour, -59 minutes and -59 seconds)
407 407 offsets = [[5, 60], [4, 60], [3, 24]]
408 408 for element in offsets: # seconds, minutes, hours
409 409 num = element[0]
410 410 length = element[1]
411 411
412 412 part = order[num]
413 413 carry_part = order[num - 1]
414 414
415 415 if deltas[part] < 0:
416 416 deltas[part] += length
417 417 deltas[carry_part] -= 1
418 418
419 419 # Same thing for days except that the increment depends on the (variable)
420 420 # number of days in the month
421 421 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
422 422 if deltas['day'] < 0:
423 423 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
424 424 deltas['day'] += 29
425 425 else:
426 426 deltas['day'] += month_lengths[get_month(prevdate) - 1]
427 427
428 428 deltas['month'] -= 1
429 429
430 430 if deltas['month'] < 0:
431 431 deltas['month'] += 12
432 432 deltas['year'] -= 1
433 433
434 434 # Format the result
435 435 if short_format:
436 436 fmt_funcs = {
437 437 'year': lambda d: u'%dy' % d,
438 438 'month': lambda d: u'%dm' % d,
439 439 'day': lambda d: u'%dd' % d,
440 440 'hour': lambda d: u'%dh' % d,
441 441 'minute': lambda d: u'%dmin' % d,
442 442 'second': lambda d: u'%dsec' % d,
443 443 }
444 444 else:
445 445 fmt_funcs = {
446 446 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
447 447 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
448 448 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
449 449 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
450 450 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
451 451 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
452 452 }
453 453
454 454 i = 0
455 455 for part in order:
456 456 value = deltas[part]
457 457 if value != 0:
458 458
459 459 if i < 5:
460 460 sub_part = order[i + 1]
461 461 sub_value = deltas[sub_part]
462 462 else:
463 463 sub_value = 0
464 464
465 465 if sub_value == 0 or show_short_version:
466 466 _val = fmt_funcs[part](value)
467 467 if future:
468 468 if show_suffix:
469 469 return _(u'in %s') % _val
470 470 else:
471 471 return _val
472 472
473 473 else:
474 474 if show_suffix:
475 475 return _(u'%s ago') % _val
476 476 else:
477 477 return _val
478 478
479 479 val = fmt_funcs[part](value)
480 480 val_detail = fmt_funcs[sub_part](sub_value)
481 481
482 482 if short_format:
483 483 datetime_tmpl = u'%s, %s'
484 484 if show_suffix:
485 485 datetime_tmpl = _(u'%s, %s ago')
486 486 if future:
487 487 datetime_tmpl = _(u'in %s, %s')
488 488 else:
489 489 datetime_tmpl = _(u'%s and %s')
490 490 if show_suffix:
491 491 datetime_tmpl = _(u'%s and %s ago')
492 492 if future:
493 493 datetime_tmpl = _(u'in %s and %s')
494 494
495 495 return datetime_tmpl % (val, val_detail)
496 496 i += 1
497 497 return _(u'just now')
498 498
499 499
500 500 def uri_filter(uri):
501 501 """
502 502 Removes user:password from given url string
503 503
504 504 :param uri:
505 505 :rtype: unicode
506 506 :returns: filtered list of strings
507 507 """
508 508 if not uri:
509 509 return ''
510 510
511 511 proto = ''
512 512
513 513 for pat in ('https://', 'http://'):
514 514 if uri.startswith(pat):
515 515 uri = uri[len(pat):]
516 516 proto = pat
517 517 break
518 518
519 519 # remove passwords and username
520 520 uri = uri[uri.find('@') + 1:]
521 521
522 522 # get the port
523 523 cred_pos = uri.find(':')
524 524 if cred_pos == -1:
525 525 host, port = uri, None
526 526 else:
527 527 host, port = uri[:cred_pos], uri[cred_pos + 1:]
528 528
529 529 return filter(None, [proto, host, port])
530 530
531 531
532 532 def credentials_filter(uri):
533 533 """
534 534 Returns a url with removed credentials
535 535
536 536 :param uri:
537 537 """
538 538
539 539 uri = uri_filter(uri)
540 540 # check if we have port
541 541 if len(uri) > 2 and uri[2]:
542 542 uri[2] = ':' + uri[2]
543 543
544 544 return ''.join(uri)
545 545
546 546
547 547 def get_clone_url(uri_tmpl, qualifed_home_url, repo_name, repo_id, **override):
548 548 parsed_url = urlobject.URLObject(qualifed_home_url)
549 549 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
550 550 args = {
551 551 'scheme': parsed_url.scheme,
552 552 'user': '',
553 553 # path if we use proxy-prefix
554 554 'netloc': parsed_url.netloc+decoded_path,
555 555 'prefix': decoded_path,
556 556 'repo': repo_name,
557 557 'repoid': str(repo_id)
558 558 }
559 559 args.update(override)
560 560 args['user'] = urllib.quote(safe_str(args['user']))
561 561
562 562 for k, v in args.items():
563 563 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
564 564
565 565 # remove leading @ sign if it's present. Case of empty user
566 566 url_obj = urlobject.URLObject(uri_tmpl)
567 567 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
568 568
569 569 return safe_unicode(url)
570 570
571 571
572 572 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
573 573 """
574 574 Safe version of get_commit if this commit doesn't exists for a
575 575 repository it returns a Dummy one instead
576 576
577 577 :param repo: repository instance
578 578 :param commit_id: commit id as str
579 579 :param pre_load: optional list of commit attributes to load
580 580 """
581 581 # TODO(skreft): remove these circular imports
582 582 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
583 583 from rhodecode.lib.vcs.exceptions import RepositoryError
584 584 if not isinstance(repo, BaseRepository):
585 585 raise Exception('You must pass an Repository '
586 586 'object as first argument got %s', type(repo))
587 587
588 588 try:
589 589 commit = repo.get_commit(
590 590 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
591 591 except (RepositoryError, LookupError):
592 592 commit = EmptyCommit()
593 593 return commit
594 594
595 595
596 596 def datetime_to_time(dt):
597 597 if dt:
598 598 return time.mktime(dt.timetuple())
599 599
600 600
601 601 def time_to_datetime(tm):
602 602 if tm:
603 603 if isinstance(tm, basestring):
604 604 try:
605 605 tm = float(tm)
606 606 except ValueError:
607 607 return
608 608 return datetime.datetime.fromtimestamp(tm)
609 609
610 610
611 def time_to_utcdatetime(tm):
612 if tm:
613 if isinstance(tm, basestring):
614 try:
615 tm = float(tm)
616 except ValueError:
617 return
618 return datetime.datetime.utcfromtimestamp(tm)
619
620
611 621 MENTIONS_REGEX = re.compile(
612 622 # ^@ or @ without any special chars in front
613 623 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
614 624 # main body starts with letter, then can be . - _
615 625 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
616 626 re.VERBOSE | re.MULTILINE)
617 627
618 628
619 629 def extract_mentioned_users(s):
620 630 """
621 631 Returns unique usernames from given string s that have @mention
622 632
623 633 :param s: string to get mentions
624 634 """
625 635 usrs = set()
626 636 for username in MENTIONS_REGEX.findall(s):
627 637 usrs.add(username)
628 638
629 639 return sorted(list(usrs), key=lambda k: k.lower())
630 640
631 641
632 642 class AttributeDict(dict):
633 643 def __getattr__(self, attr):
634 644 return self.get(attr, None)
635 645 __setattr__ = dict.__setitem__
636 646 __delattr__ = dict.__delitem__
637 647
638 648
639 649 def fix_PATH(os_=None):
640 650 """
641 651 Get current active python path, and append it to PATH variable to fix
642 652 issues of subprocess calls and different python versions
643 653 """
644 654 if os_ is None:
645 655 import os
646 656 else:
647 657 os = os_
648 658
649 659 cur_path = os.path.split(sys.executable)[0]
650 660 if not os.environ['PATH'].startswith(cur_path):
651 661 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
652 662
653 663
654 664 def obfuscate_url_pw(engine):
655 665 _url = engine or ''
656 666 try:
657 667 _url = sqlalchemy.engine.url.make_url(engine)
658 668 if _url.password:
659 669 _url.password = 'XXXXX'
660 670 except Exception:
661 671 pass
662 672 return unicode(_url)
663 673
664 674
665 675 def get_server_url(environ):
666 676 req = webob.Request(environ)
667 677 return req.host_url + req.script_name
668 678
669 679
670 680 def unique_id(hexlen=32):
671 681 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
672 682 return suuid(truncate_to=hexlen, alphabet=alphabet)
673 683
674 684
675 685 def suuid(url=None, truncate_to=22, alphabet=None):
676 686 """
677 687 Generate and return a short URL safe UUID.
678 688
679 689 If the url parameter is provided, set the namespace to the provided
680 690 URL and generate a UUID.
681 691
682 692 :param url to get the uuid for
683 693 :truncate_to: truncate the basic 22 UUID to shorter version
684 694
685 695 The IDs won't be universally unique any longer, but the probability of
686 696 a collision will still be very low.
687 697 """
688 698 # Define our alphabet.
689 699 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
690 700
691 701 # If no URL is given, generate a random UUID.
692 702 if url is None:
693 703 unique_id = uuid.uuid4().int
694 704 else:
695 705 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
696 706
697 707 alphabet_length = len(_ALPHABET)
698 708 output = []
699 709 while unique_id > 0:
700 710 digit = unique_id % alphabet_length
701 711 output.append(_ALPHABET[digit])
702 712 unique_id = int(unique_id / alphabet_length)
703 713 return "".join(output)[:truncate_to]
704 714
705 715
706 716 def get_current_rhodecode_user():
707 717 """
708 718 Gets rhodecode user from threadlocal tmpl_context variable if it's
709 719 defined, else returns None.
710 720 """
711 721 from pylons import tmpl_context as c
712 722 if hasattr(c, 'rhodecode_user'):
713 723 return c.rhodecode_user
714 724
715 725 return None
716 726
717 727
718 728 def action_logger_generic(action, namespace=''):
719 729 """
720 730 A generic logger for actions useful to the system overview, tries to find
721 731 an acting user for the context of the call otherwise reports unknown user
722 732
723 733 :param action: logging message eg 'comment 5 deleted'
724 734 :param type: string
725 735
726 736 :param namespace: namespace of the logging message eg. 'repo.comments'
727 737 :param type: string
728 738
729 739 """
730 740
731 741 logger_name = 'rhodecode.actions'
732 742
733 743 if namespace:
734 744 logger_name += '.' + namespace
735 745
736 746 log = logging.getLogger(logger_name)
737 747
738 748 # get a user if we can
739 749 user = get_current_rhodecode_user()
740 750
741 751 logfunc = log.info
742 752
743 753 if not user:
744 754 user = '<unknown user>'
745 755 logfunc = log.warning
746 756
747 757 logfunc('Logging action by {}: {}'.format(user, action))
748 758
749 759
750 760 def escape_split(text, sep=',', maxsplit=-1):
751 761 r"""
752 762 Allows for escaping of the separator: e.g. arg='foo\, bar'
753 763
754 764 It should be noted that the way bash et. al. do command line parsing, those
755 765 single quotes are required.
756 766 """
757 767 escaped_sep = r'\%s' % sep
758 768
759 769 if escaped_sep not in text:
760 770 return text.split(sep, maxsplit)
761 771
762 772 before, _mid, after = text.partition(escaped_sep)
763 773 startlist = before.split(sep, maxsplit) # a regular split is fine here
764 774 unfinished = startlist[-1]
765 775 startlist = startlist[:-1]
766 776
767 777 # recurse because there may be more escaped separators
768 778 endlist = escape_split(after, sep, maxsplit)
769 779
770 780 # finish building the escaped value. we use endlist[0] becaue the first
771 781 # part of the string sent in recursion is the rest of the escaped value.
772 782 unfinished += sep + endlist[0]
773 783
774 784 return startlist + [unfinished] + endlist[1:] # put together all the parts
775 785
776 786
777 787 class OptionalAttr(object):
778 788 """
779 789 Special Optional Option that defines other attribute. Example::
780 790
781 791 def test(apiuser, userid=Optional(OAttr('apiuser')):
782 792 user = Optional.extract(userid)
783 793 # calls
784 794
785 795 """
786 796
787 797 def __init__(self, attr_name):
788 798 self.attr_name = attr_name
789 799
790 800 def __repr__(self):
791 801 return '<OptionalAttr:%s>' % self.attr_name
792 802
793 803 def __call__(self):
794 804 return self
795 805
796 806
797 807 # alias
798 808 OAttr = OptionalAttr
799 809
800 810
801 811 class Optional(object):
802 812 """
803 813 Defines an optional parameter::
804 814
805 815 param = param.getval() if isinstance(param, Optional) else param
806 816 param = param() if isinstance(param, Optional) else param
807 817
808 818 is equivalent of::
809 819
810 820 param = Optional.extract(param)
811 821
812 822 """
813 823
814 824 def __init__(self, type_):
815 825 self.type_ = type_
816 826
817 827 def __repr__(self):
818 828 return '<Optional:%s>' % self.type_.__repr__()
819 829
820 830 def __call__(self):
821 831 return self.getval()
822 832
823 833 def getval(self):
824 834 """
825 835 returns value from this Optional instance
826 836 """
827 837 if isinstance(self.type_, OAttr):
828 838 # use params name
829 839 return self.type_.attr_name
830 840 return self.type_
831 841
832 842 @classmethod
833 843 def extract(cls, val):
834 844 """
835 845 Extracts value from Optional() instance
836 846
837 847 :param val:
838 848 :return: original value if it's not Optional instance else
839 849 value of instance
840 850 """
841 851 if isinstance(val, cls):
842 852 return val.getval()
843 853 return val
@@ -1,205 +1,205 b''
1 1 // define module
2 2 var AgeModule = (function () {
3 3 return {
4 4 age: function(prevdate, now, show_short_version, show_suffix, short_format) {
5 5
6 6 var prevdate = moment(prevdate);
7 7 var now = now || moment().utc();
8 8
9 9 var show_short_version = show_short_version || false;
10 10 var show_suffix = show_suffix || true;
11 11 var short_format = short_format || false;
12 12
13 13 // alias for backward compat
14 14 var _ = function(s) {
15 15 if (_TM.hasOwnProperty(s)) {
16 16 return _TM[s];
17 17 }
18 18 return s
19 19 };
20 20
21 21 var ungettext = function (singular, plural, n) {
22 22 if (n === 1){
23 23 return _(singular)
24 24 }
25 25 return _(plural)
26 26 };
27 27
28 28 var _get_relative_delta = function(now, prevdate) {
29 29
30 30 var duration = moment.duration(moment(now).diff(prevdate));
31 31 return {
32 32 'year': duration.years(),
33 33 'month': duration.months(),
34 34 'day': duration.days(),
35 35 'hour': duration.hours(),
36 36 'minute': duration.minutes(),
37 37 'second': duration.seconds()
38 38 };
39 39
40 40 };
41 41
42 42 var _is_leap_year = function(year){
43 43 return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
44 44 };
45 45
46 46 var get_month = function(prevdate) {
47 47 return prevdate.getMonth()
48 48 };
49 49
50 50 var get_year = function(prevdate) {
51 51 return prevdate.getYear()
52 52 };
53 53
54 54 var order = ['year', 'month', 'day', 'hour', 'minute', 'second'];
55 55 var deltas = {};
56 56 var future = false;
57 57
58 58 if (prevdate > now) {
59 59 var now_old = now;
60 60 now = prevdate;
61 61 prevdate = now_old;
62 62 future = true;
63 63 }
64 64 if (future) {
65 65 // ? remove microseconds, we don't have it in JS
66 66 }
67 67
68 68 // Get date parts deltas
69 69 for (part in order) {
70 70 var part = order[part];
71 71 var rel_delta = _get_relative_delta(now, prevdate);
72 72 deltas[part] = rel_delta[part]
73 73 }
74 74
75 75 //# Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
76 76 //# not 1 hour, -59 minutes and -59 seconds)
77 77 var offsets = [[5, 60], [4, 60], [3, 24]];
78 78 for (element in offsets) { //# seconds, minutes, hours
79 79 var element = offsets[element];
80 80 var num = element[0];
81 81 var length = element[1];
82 82
83 83 var part = order[num];
84 84 var carry_part = order[num - 1];
85 85
86 86 if (deltas[part] < 0){
87 87 deltas[part] += length;
88 88 deltas[carry_part] -= 1
89 89 }
90 90
91 91 }
92 92
93 93 // # Same thing for days except that the increment depends on the (variable)
94 94 // # number of days in the month
95 95 var month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
96 96 if (deltas['day'] < 0) {
97 97 if (get_month(prevdate) == 2 && _is_leap_year(get_year(prevdate))) {
98 98 deltas['day'] += 29;
99 99 } else {
100 100 deltas['day'] += month_lengths[get_month(prevdate) - 1];
101 101 }
102 102
103 103 deltas['month'] -= 1
104 104 }
105 105
106 106 if (deltas['month'] < 0) {
107 107 deltas['month'] += 12;
108 108 deltas['year'] -= 1;
109 109 }
110 110
111 111 //# Format the result
112 112 if (short_format) {
113 113 var fmt_funcs = {
114 114 'year': function(d) {return '{0}y'.format(d)},
115 115 'month': function(d) {return '{0}m'.format(d)},
116 116 'day': function(d) {return '{0}d'.format(d)},
117 117 'hour': function(d) {return '{0}h'.format(d)},
118 118 'minute': function(d) {return '{0}min'.format(d)},
119 119 'second': function(d) {return '{0}sec'.format(d)}
120 120 }
121 121
122 122 } else {
123 123 var fmt_funcs = {
124 124 'year': function(d) {return ungettext('{0} year', '{0} years', d).format(d)},
125 125 'month': function(d) {return ungettext('{0} month', '{0} months', d).format(d)},
126 126 'day': function(d) {return ungettext('{0} day', '{0} days', d).format(d)},
127 127 'hour': function(d) {return ungettext('{0} hour', '{0} hours', d).format(d)},
128 128 'minute': function(d) {return ungettext('{0} min', '{0} min', d).format(d)},
129 129 'second': function(d) {return ungettext('{0} sec', '{0} sec', d).format(d)}
130 130 }
131 131
132 132 }
133 133 var i = 0;
134 134 for (part in order){
135 135 var part = order[part];
136 136 var value = deltas[part];
137 137 if (value !== 0) {
138 138
139 139 if (i < 5) {
140 140 var sub_part = order[i + 1];
141 141 var sub_value = deltas[sub_part]
142 142 } else {
143 143 var sub_value = 0
144 144 }
145 145 if (sub_value == 0 || show_short_version) {
146 146 var _val = fmt_funcs[part](value);
147 147 if (future) {
148 148 if (show_suffix) {
149 149 return _('in {0}').format(_val)
150 150 } else {
151 151 return _val
152 152 }
153 153
154 154 }
155 155 else {
156 156 if (show_suffix) {
157 157 return _('{0} ago').format(_val)
158 158 } else {
159 159 return _val
160 160 }
161 161 }
162 162 }
163 163
164 164 var val = fmt_funcs[part](value);
165 165 var val_detail = fmt_funcs[sub_part](sub_value);
166 166 if (short_format) {
167 167 var datetime_tmpl = '{0}, {1}';
168 168 if (show_suffix) {
169 169 datetime_tmpl = _('{0}, {1} ago');
170 170 if (future) {
171 171 datetime_tmpl = _('in {0}, {1}');
172 172 }
173 173 }
174 174 } else {
175 175 var datetime_tmpl = _('{0} and {1}');
176 176 if (show_suffix) {
177 177 datetime_tmpl = _('{0} and {1} ago');
178 178 if (future) {
179 179 datetime_tmpl = _('in {0} and {1}')
180 180 }
181 181 }
182 182 }
183 183
184 184 return datetime_tmpl.format(val, val_detail)
185 185 }
186 186 i += 1;
187 187 }
188 188
189 189 return _('just now')
190 190
191 191 },
192 192 createTimeComponent: function(dateTime, text) {
193 return '<time class="timeago tooltip" title="{1}" datetime="{0}">{1}</time>'.format(dateTime, text);
193 return '<time class="timeago tooltip" title="{1}" datetime="{0}+0000">{1}</time>'.format(dateTime, text);
194 194 }
195 195 }
196 196 })();
197 197
198 198
199 199 jQuery.timeago.settings.localeTitle = false;
200 200
201 201 // auto refresh the components every Ns
202 202 jQuery.timeago.settings.refreshMillis = templateContext.timeago.refresh_time;
203 203
204 204 // Display original dates older than N days
205 205 jQuery.timeago.settings.cutoff = templateContext.timeago.cutoff_limit;
@@ -1,108 +1,108 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="robots()">
5 5 %if c.gist.gist_type != 'public':
6 6 <meta name="robots" content="noindex, nofollow">
7 7 %else:
8 8 ${parent.robots()}
9 9 %endif
10 10 </%def>
11 11
12 12 <%def name="title()">
13 13 ${_('Gist')} &middot; ${c.gist.gist_access_id}
14 14 %if c.rhodecode_name:
15 15 &middot; ${h.branding(c.rhodecode_name)}
16 16 %endif
17 17 </%def>
18 18
19 19 <%def name="breadcrumbs_links()">
20 20 ${_('Gist')} &middot; ${c.gist.gist_access_id}
21 21 / ${_('URL')}: ${c.gist.gist_url()}
22 22 </%def>
23 23
24 24 <%def name="menu_bar_nav()">
25 25 ${self.menu_items(active='gists')}
26 26 </%def>
27 27
28 28 <%def name="main()">
29 29 <div class="box">
30 30 <!-- box / title -->
31 31 <div class="title">
32 32 ${self.breadcrumbs()}
33 33 %if c.rhodecode_user.username != h.DEFAULT_USER:
34 34 <ul class="links">
35 35 <li>
36 36 <a href="${h.url('new_gist')}" class="btn btn-primary">${_(u'Create New Gist')}</a>
37 37 </li>
38 38 </ul>
39 39 %endif
40 40 </div>
41 41 <div class="table">
42 42 <div id="files_data">
43 43 <div id="codeblock" class="codeblock">
44 44 <div class="code-header">
45 45 <div class="stats">
46 46 %if h.HasPermissionAny('hg.admin')() or c.gist.gist_owner == c.rhodecode_user.user_id:
47 47 <div class="remove_gist">
48 48 ${h.secure_form(url('gist', gist_id=c.gist.gist_access_id),method='delete')}
49 49 ${h.submit('remove_gist', _('Delete'),class_="btn btn-mini btn-danger",onclick="return confirm('"+_('Confirm to delete this Gist')+"');")}
50 50 ${h.end_form()}
51 51 </div>
52 52 %endif
53 53 <div class="buttons">
54 54 ## only owner should see that
55 55 %if h.HasPermissionAny('hg.admin')() or c.gist.gist_owner == c.rhodecode_user.user_id:
56 56 ${h.link_to(_('Edit'),h.url('edit_gist', gist_id=c.gist.gist_access_id),class_="btn btn-mini")}
57 57 %endif
58 58 ${h.link_to(_('Show as Raw'),h.url('formatted_gist', gist_id=c.gist.gist_access_id, format='raw'),class_="btn btn-mini")}
59 59 </div>
60 60 <div class="left" >
61 61 %if c.gist.gist_type != 'public':
62 62 <span class="tag tag-ok disabled">${_('Private Gist')}</span>
63 63 %endif
64 64 <span> ${c.gist.gist_description}</span>
65 65 <span>${_('Expires')}:
66 66 %if c.gist.gist_expires == -1:
67 67 ${_('never')}
68 68 %else:
69 ${h.age_component(h.time_to_datetime(c.gist.gist_expires))}
69 ${h.age_component(h.time_to_utcdatetime(c.gist.gist_expires))}
70 70 %endif
71 71 </span>
72 72 </div>
73 73 </div>
74 74
75 75 <div class="author">
76 76 <div title="${c.file_last_commit.author}">
77 77 ${self.gravatar_with_user(c.file_last_commit.author, 16)} - ${_('created')} ${h.age_component(c.file_last_commit.date)}
78 78 </div>
79 79
80 80 </div>
81 81 <div class="commit">${h.urlify_commit_message(c.file_last_commit.message,c.repo_name)}</div>
82 82 </div>
83 83
84 84 ## iterate over the files
85 85 % for file in c.files:
86 86 <% renderer = c.render and h.renderer_from_filename(file.path, exclude=['.txt', '.TXT'])%>
87 87 <!-- <div id="${h.FID('G', file.path)}" class="stats" >
88 88 <a href="${c.gist.gist_url()}">ΒΆ</a>
89 89 <b >${file.path}</b>
90 90 <div>
91 91 ${h.link_to(_('Show as raw'),h.url('formatted_gist_file', gist_id=c.gist.gist_access_id, format='raw', revision=file.commit.raw_id, f_path=file.path),class_="btn btn-mini")}
92 92 </div>
93 93 </div> -->
94 94 <div class="code-body textarea text-area editor">
95 95 %if renderer:
96 96 ${h.render(file.content, renderer=renderer)}
97 97 %else:
98 98 ${h.pygmentize(file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
99 99 %endif
100 100 </div>
101 101 %endfor
102 102 </div>
103 103 </div>
104 104 </div>
105 105
106 106
107 107 </div>
108 108 </%def>
@@ -1,102 +1,102 b''
1 1 <div class="panel panel-default">
2 2 <div class="panel-heading">
3 3 <h3 class="panel-title">${_('Authentication Tokens')}</h3>
4 4 </div>
5 5 <div class="panel-body">
6 6 <p>
7 7 ${_('Built-in tokens can be used to authenticate with all possible options.')}<br/>
8 8 ${_('Each token can have a role. VCS tokens can be used together with the authtoken auth plugin for git/hg operations.')}
9 9 </p>
10 10 <table class="rctable auth_tokens">
11 11 <tr>
12 12 <td class="truncate-wrap td-authtoken"><div class="user_auth_tokens truncate autoexpand"><code>${c.user.api_key}</code></div></td>
13 13 <td class="td-buttons">
14 14 <span class="btn btn-mini btn-info disabled">${_('Built-in')}</span>
15 15 </td>
16 16 <td class="td-buttons">
17 17 <span class="btn btn-mini btn-info disabled">all</span>
18 18 </td>
19 19 <td class="td-exp">${_('expires')}: ${_('never')}</td>
20 20 <td class="td-action">
21 21 ${h.secure_form(url('my_account_auth_tokens'),method='delete')}
22 22 ${h.hidden('del_auth_token',c.user.api_key)}
23 23 ${h.hidden('del_auth_token_builtin',1)}
24 24 <button class="btn-link btn-danger" type="submit"
25 25 onclick="return confirm('${_('Confirm to reset this auth token: %s') % c.user.api_key}');">
26 26 <i class="icon-refresh"></i>
27 27 ${_('Reset')}
28 28 </button>
29 29 ${h.end_form()}
30 30 </td>
31 31 </tr>
32 32 %if c.user_auth_tokens:
33 33 %for auth_token in c.user_auth_tokens:
34 34 <tr class="${'expired' if auth_token.expired else ''}">
35 35 <td class="truncate-wrap td-authtoken"><div class="user_auth_tokens truncate autoexpand"><code>${auth_token.api_key}</code></div></td>
36 36 <td class="td-wrap">${auth_token.description}</td>
37 37 <td class="td-buttons">
38 38 <span class="btn btn-mini btn-info disabled">${auth_token.role_humanized}</span>
39 39 </td>
40 40 <td class="td-exp">
41 41 %if auth_token.expires == -1:
42 42 ${_('expires')}: ${_('never')}
43 43 %else:
44 44 %if auth_token.expired:
45 ${_('expired')}: ${h.age_component(h.time_to_datetime(auth_token.expires))}
45 ${_('expired')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
46 46 %else:
47 ${_('expires')}: ${h.age_component(h.time_to_datetime(auth_token.expires))}
47 ${_('expires')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
48 48 %endif
49 49 %endif
50 50 </td>
51 51 <td class="td-action">
52 52 ${h.secure_form(url('my_account_auth_tokens'),method='delete')}
53 53 ${h.hidden('del_auth_token',auth_token.api_key)}
54 54 <button class="btn btn-link btn-danger" type="submit"
55 55 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.api_key}');">
56 56 ${_('Delete')}
57 57 </button>
58 58 ${h.end_form()}
59 59 </td>
60 60 </tr>
61 61 %endfor
62 62 %else:
63 63 <tr><td><div class="ip">${_('No additional auth token specified')}</div></td></tr>
64 64 %endif
65 65 </table>
66 66
67 67 <div class="user_auth_tokens">
68 68 ${h.secure_form(url('my_account_auth_tokens'), method='post')}
69 69 <div class="form form-vertical">
70 70 <!-- fields -->
71 71 <div class="fields">
72 72 <div class="field">
73 73 <div class="label">
74 74 <label for="new_email">${_('New authentication token')}:</label>
75 75 </div>
76 76 <div class="input">
77 77 ${h.text('description', placeholder=_('Description'))}
78 78 ${h.select('lifetime', '', c.lifetime_options)}
79 79 ${h.select('role', '', c.role_options)}
80 80 </div>
81 81 </div>
82 82 <div class="buttons">
83 83 ${h.submit('save',_('Add'),class_="btn")}
84 84 ${h.reset('reset',_('Reset'),class_="btn")}
85 85 </div>
86 86 </div>
87 87 </div>
88 88 ${h.end_form()}
89 89 </div>
90 90 </div>
91 91 </div>
92 92 <script>
93 93 $(document).ready(function(){
94 94 var select2Options = {
95 95 'containerCssClass': "drop-menu",
96 96 'dropdownCssClass': "drop-menu-dropdown",
97 97 'dropdownAutoWidth': true
98 98 };
99 99 $("#lifetime").select2(select2Options);
100 100 $("#role").select2(select2Options);
101 101 });
102 102 </script>
@@ -1,103 +1,103 b''
1 1 <div class="panel panel-default">
2 2 <div class="panel-heading">
3 3 <h3 class="panel-title">${_('Authentication Access Tokens')}</h3>
4 4 </div>
5 5 <div class="panel-body">
6 6 <div class="apikeys_wrap">
7 7 <table class="rctable auth_tokens">
8 8 <tr>
9 9 <td class="truncate-wrap td-authtoken"><div class="user_auth_tokens truncate autoexpand"><code>${c.user.api_key}</code></div></td>
10 10 <td class="td-tags">
11 11 <span class="tag disabled">${_('Built-in')}</span>
12 12 </td>
13 13 <td class="td-tags">
14 14 <span class="tag disabled">all</span>
15 15 </td>
16 16 <td class="td-exp">${_('expires')}: ${_('never')}</td>
17 17 <td class="td-action">
18 18 ${h.secure_form(url('edit_user_auth_tokens', user_id=c.user.user_id),method='delete')}
19 19 ${h.hidden('del_auth_token',c.user.api_key)}
20 20 ${h.hidden('del_auth_token_builtin',1)}
21 21 <button class="btn btn-link btn-danger" type="submit"
22 22 onclick="return confirm('${_('Confirm to reset this auth token: %s') % c.user.api_key}');">
23 23 ${_('Reset')}
24 24 </button>
25 25 ${h.end_form()}
26 26 </td>
27 27 </tr>
28 28 %if c.user_auth_tokens:
29 29 %for auth_token in c.user_auth_tokens:
30 30 <tr class="${'expired' if auth_token.expired else ''}">
31 31 <td class="truncate-wrap td-authtoken"><div class="user_auth_tokens truncate autoexpand"><code>${auth_token.api_key}</code></div></td>
32 32 <td class="td-wrap">${auth_token.description}</td>
33 33 <td class="td-tags">
34 34 <span class="tag">${auth_token.role_humanized}</span>
35 35 </td>
36 36 <td class="td-exp">
37 37 %if auth_token.expires == -1:
38 38 ${_('expires')}: ${_('never')}
39 39 %else:
40 40 %if auth_token.expired:
41 ${_('expired')}: ${h.age_component(h.time_to_datetime(auth_token.expires))}
41 ${_('expired')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
42 42 %else:
43 ${_('expires')}: ${h.age_component(h.time_to_datetime(auth_token.expires))}
43 ${_('expires')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
44 44 %endif
45 45 %endif
46 46 </td>
47 47 <td>
48 48 ${h.secure_form(url('edit_user_auth_tokens', user_id=c.user.user_id),method='delete')}
49 49 ${h.hidden('del_auth_token',auth_token.api_key)}
50 50 <button class="btn btn-link btn-danger" type="submit"
51 51 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.api_key}');">
52 52 ${_('Delete')}
53 53 </button>
54 54 ${h.end_form()}
55 55 </td>
56 56 </tr>
57 57 %endfor
58 58 %else:
59 59 <tr><td><div class="ip">${_('No additional auth tokens specified')}</div></td></tr>
60 60 %endif
61 61 </table>
62 62 </div>
63 63
64 64 <div class="user_auth_tokens">
65 65 ${h.secure_form(url('edit_user_auth_tokens', user_id=c.user.user_id), method='put')}
66 66 <div class="form form-vertical">
67 67 <!-- fields -->
68 68 <div class="fields">
69 69 <div class="field">
70 70 <div class="label">
71 71 <label for="new_email">${_('New auth token')}:</label>
72 72 </div>
73 73 <div class="input">
74 74 ${h.text('description', class_='medium', placeholder=_('Description'))}
75 75 ${h.select('lifetime', '', c.lifetime_options)}
76 76 ${h.select('role', '', c.role_options)}
77 77 </div>
78 78 </div>
79 79 <div class="buttons">
80 80 ${h.submit('save',_('Add'),class_="btn btn-small")}
81 81 ${h.reset('reset',_('Reset'),class_="btn btn-small")}
82 82 </div>
83 83 </div>
84 84 </div>
85 85 ${h.end_form()}
86 86 </div>
87 87 </div>
88 88 </div>
89 89
90 90 <script>
91 91 $(document).ready(function(){
92 92 $("#lifetime").select2({
93 93 'containerCssClass': "drop-menu",
94 94 'dropdownCssClass': "drop-menu-dropdown",
95 95 'dropdownAutoWidth': true
96 96 });
97 97 $("#role").select2({
98 98 'containerCssClass': "drop-menu",
99 99 'dropdownCssClass': "drop-menu-dropdown",
100 100 'dropdownAutoWidth': true
101 101 });
102 102 })
103 103 </script>
@@ -1,312 +1,312 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.html"/>
7 7
8 8 <%def name="comment_block(comment, inline=False)">
9 9 <div class="comment ${'comment-inline' if inline else ''}" id="comment-${comment.comment_id}" line="${comment.line_no}" data-comment-id="${comment.comment_id}">
10 10 <div class="meta">
11 11 <div class="author">
12 12 ${base.gravatar_with_user(comment.author.email, 16)}
13 13 </div>
14 14 <div class="date">
15 ${h.age_component(comment.modified_at)}
15 ${h.age_component(comment.modified_at, time_is_local=True)}
16 16 </div>
17 17 <div class="status-change">
18 18 %if comment.pull_request:
19 19 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
20 20 %if comment.status_change:
21 21 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
22 22 %else:
23 23 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
24 24 %endif
25 25 </a>
26 26 %else:
27 27 %if comment.status_change:
28 28 ${_('Status change on commit')}:
29 29 %else:
30 30 ${_('Comment on commit')}
31 31 %endif
32 32 %endif
33 33 </div>
34 34 %if comment.status_change:
35 35 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
36 36 <div title="${_('Commit status')}" class="changeset-status-lbl">
37 37 ${comment.status_change[0].status_lbl}
38 38 </div>
39 39 %endif
40 40 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
41 41
42 42
43 43 <div class="comment-links-block">
44 44
45 45 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
46 46 ## only super-admin, repo admin OR comment owner can delete
47 47 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
48 48 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
49 49 <div onClick="deleteComment(${comment.comment_id})" class="delete-comment"> ${_('Delete')}</div>
50 50 %if inline:
51 51 <div class="comment-links-divider"> | </div>
52 52 %endif
53 53 %endif
54 54 %endif
55 55
56 56 %if inline:
57 57
58 58 <div id="prev_c_${comment.comment_id}" class="comment-previous-link" title="${_('Previous comment')}">
59 59 <a class="arrow_comment_link disabled"><i class="icon-left"></i></a>
60 60 </div>
61 61
62 62 <div id="next_c_${comment.comment_id}" class="comment-next-link" title="${_('Next comment')}">
63 63 <a class="arrow_comment_link disabled"><i class="icon-right"></i></a>
64 64 </div>
65 65 %endif
66 66
67 67 </div>
68 68 </div>
69 69 <div class="text">
70 70 ${comment.render(mentions=True)|n}
71 71 </div>
72 72 </div>
73 73 </%def>
74 74
75 75 <%def name="comment_block_outdated(comment)">
76 76 <div class="comments" id="comment-${comment.comment_id}">
77 77 <div class="comment comment-wrapp">
78 78 <div class="meta">
79 79 <div class="author">
80 80 ${base.gravatar_with_user(comment.author.email, 16)}
81 81 </div>
82 82 <div class="date">
83 ${h.age_component(comment.modified_at)}
83 ${h.age_component(comment.modified_at, time_is_local=True)}
84 84 </div>
85 85 %if comment.status_change:
86 86 <span class="changeset-status-container">
87 87 <span class="changeset-status-ico">
88 88 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
89 89 </span>
90 90 <span title="${_('Commit status')}" class="changeset-status-lbl"> ${comment.status_change[0].status_lbl}</span>
91 91 </span>
92 92 %endif
93 93 <a class="permalink" href="#comment-${comment.comment_id}">&para;</a>
94 94 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
95 95 ## only super-admin, repo admin OR comment owner can delete
96 96 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
97 97 <div class="comment-links-block">
98 98 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
99 99 <div data-comment-id=${comment.comment_id} class="delete-comment">${_('Delete')}</div>
100 100 %endif
101 101 </div>
102 102 %endif
103 103 </div>
104 104 <div class="text">
105 105 ${comment.render(mentions=True)|n}
106 106 </div>
107 107 </div>
108 108 </div>
109 109 </%def>
110 110
111 111 <%def name="comment_inline_form()">
112 112 <div id="comment-inline-form-template" style="display: none;">
113 113 <div class="comment-inline-form ac">
114 114 %if c.rhodecode_user.username != h.DEFAULT_USER:
115 115 ${h.form('#', class_='inline-form', method='get')}
116 116 <div id="edit-container_{1}" class="clearfix">
117 117 <div class="comment-title pull-left">
118 118 ${_('Create a comment on line {1}.')}
119 119 </div>
120 120 <div class="comment-help pull-right">
121 121 ${(_('Comments parsed using %s syntax with %s support.') % (
122 122 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
123 123 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
124 124 )
125 125 )|n
126 126 }
127 127 </div>
128 128 <div style="clear: both"></div>
129 129 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
130 130 </div>
131 131 <div id="preview-container_{1}" class="clearfix" style="display: none;">
132 132 <div class="comment-help">
133 133 ${_('Comment preview')}
134 134 </div>
135 135 <div id="preview-box_{1}" class="preview-box"></div>
136 136 </div>
137 137 <div class="comment-footer">
138 138 <div class="comment-button hide-inline-form-button cancel-button">
139 139 ${h.reset('hide-inline-form', _('Cancel'), class_='btn hide-inline-form', id_="cancel-btn_{1}")}
140 140 </div>
141 141 <div class="action-buttons">
142 142 <input type="hidden" name="f_path" value="{0}">
143 143 <input type="hidden" name="line" value="{1}">
144 144 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
145 145 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
146 146 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
147 147 </div>
148 148 ${h.end_form()}
149 149 </div>
150 150 %else:
151 151 ${h.form('', class_='inline-form comment-form-login', method='get')}
152 152 <div class="pull-left">
153 153 <div class="comment-help pull-right">
154 154 ${_('You need to be logged in to comment.')} <a href="${h.url('login_home',came_from=h.url.current())}">${_('Login now')}</a>
155 155 </div>
156 156 </div>
157 157 <div class="comment-button pull-right">
158 158 ${h.reset('hide-inline-form', _('Hide'), class_='btn hide-inline-form')}
159 159 </div>
160 160 <div class="clearfix"></div>
161 161 ${h.end_form()}
162 162 %endif
163 163 </div>
164 164 </div>
165 165 </%def>
166 166
167 167
168 168 ## generates inlines taken from c.comments var
169 169 <%def name="inlines(is_pull_request=False)">
170 170 %if is_pull_request:
171 171 <h2 id="comments">${ungettext("%d Pull Request Comment", "%d Pull Request Comments", len(c.comments)) % len(c.comments)}</h2>
172 172 %else:
173 173 <h2 id="comments">${ungettext("%d Commit Comment", "%d Commit Comments", len(c.comments)) % len(c.comments)}</h2>
174 174 %endif
175 175 %for path, lines_comments in c.inline_comments:
176 176 % for line, comments in lines_comments.iteritems():
177 177 <div style="display: none;" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
178 178 ## for each comment in particular line
179 179 %for comment in comments:
180 180 ${comment_block(comment, inline=True)}
181 181 %endfor
182 182 </div>
183 183 %endfor
184 184 %endfor
185 185
186 186 </%def>
187 187
188 188 ## generate inline comments and the main ones
189 189 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
190 190 ## generate inlines for this changeset
191 191 ${inlines(is_pull_request)}
192 192
193 193 %for comment in c.comments:
194 194 <div id="comment-tr-${comment.comment_id}">
195 195 ## only render comments that are not from pull request, or from
196 196 ## pull request and a status change
197 197 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
198 198 ${comment_block(comment)}
199 199 %endif
200 200 </div>
201 201 %endfor
202 202 ## to anchor ajax comments
203 203 <div id="injected_page_comments"></div>
204 204 </%def>
205 205
206 206 ## MAIN COMMENT FORM
207 207 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
208 208 %if is_compare:
209 209 <% form_id = "comments_form_compare" %>
210 210 %else:
211 211 <% form_id = "comments_form" %>
212 212 %endif
213 213
214 214
215 215 %if is_pull_request:
216 216 <div class="pull-request-merge">
217 217 %if c.allowed_to_merge:
218 218 <div class="pull-request-wrap">
219 219 <div class="pull-right">
220 220 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
221 221 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
222 222 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
223 223 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
224 224 ${h.end_form()}
225 225 </div>
226 226 </div>
227 227 %else:
228 228 <div class="pull-request-wrap">
229 229 <div class="pull-right">
230 230 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
231 231 </div>
232 232 </div>
233 233 %endif
234 234 </div>
235 235 %endif
236 236 <div class="comments">
237 237 %if c.rhodecode_user.username != h.DEFAULT_USER:
238 238 <div class="comment-form ac">
239 239 ${h.secure_form(post_url, id_=form_id)}
240 240 <div id="edit-container" class="clearfix">
241 241 <div class="comment-title pull-left">
242 242 %if is_pull_request:
243 243 ${(_('Create a comment on this Pull Request.'))}
244 244 %elif is_compare:
245 245 ${(_('Create comments on this Commit range.'))}
246 246 %else:
247 247 ${(_('Create a comment on this Commit.'))}
248 248 %endif
249 249 </div>
250 250 <div class="comment-help pull-right">
251 251 ${(_('Comments parsed using %s syntax with %s support.') % (
252 252 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
253 253 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
254 254 )
255 255 )|n
256 256 }
257 257 </div>
258 258 <div style="clear: both"></div>
259 259 ${h.textarea('text', class_="comment-block-ta")}
260 260 </div>
261 261
262 262 <div id="preview-container" class="clearfix" style="display: none;">
263 263 <div class="comment-title">
264 264 ${_('Comment preview')}
265 265 </div>
266 266 <div id="preview-box" class="preview-box"></div>
267 267 </div>
268 268
269 269 <div id="comment_form_extras">
270 270 %if form_extras and isinstance(form_extras, (list, tuple)):
271 271 % for form_ex_el in form_extras:
272 272 ${form_ex_el|n}
273 273 % endfor
274 274 %endif
275 275 </div>
276 276 <div class="comment-footer">
277 277 %if change_status:
278 278 <div class="status_box">
279 279 <select id="change_status" name="changeset_status">
280 280 <option></option> # Placeholder
281 281 %for status,lbl in c.commit_statuses:
282 282 <option value="${status}" data-status="${status}">${lbl}</option>
283 283 %if is_pull_request and change_status and status in ('approved', 'rejected'):
284 284 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
285 285 %endif
286 286 %endfor
287 287 </select>
288 288 </div>
289 289 %endif
290 290 <div class="action-buttons">
291 291 <button id="preview-btn" class="btn btn-secondary">${_('Preview')}</button>
292 292 <button id="edit-btn" class="btn btn-secondary" style="display:none;">${_('Edit')}</button>
293 293 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
294 294 </div>
295 295 </div>
296 296 ${h.end_form()}
297 297 </div>
298 298 %endif
299 299 </div>
300 300 <script>
301 301 // init active elements of commentForm
302 302 var commitId = templateContext.commit_data.commit_id;
303 303 var pullRequestId = templateContext.pull_request_data.pull_request_id;
304 304 var lineNo;
305 305
306 306 var mainCommentForm = new CommentForm(
307 307 "#${form_id}", commitId, pullRequestId, lineNo, true);
308 308
309 309 mainCommentForm.initStatusChangeSelector();
310 310
311 311 </script>
312 312 </%def>
@@ -1,297 +1,297 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.html"/>
4 4 <%namespace name="base" file="/base/base.html"/>
5 5
6 6 ## REPOSITORY RENDERERS
7 7 <%def name="quick_menu(repo_name)">
8 8 <i class="pointer icon-more"></i>
9 9 <div class="menu_items_container hidden">
10 10 <ul class="menu_items">
11 11 <li>
12 12 <a title="${_('Summary')}" href="${h.url('summary_home',repo_name=repo_name)}">
13 13 <span>${_('Summary')}</span>
14 14 </a>
15 15 </li>
16 16 <li>
17 17 <a title="${_('Changelog')}" href="${h.url('changelog_home',repo_name=repo_name)}">
18 18 <span>${_('Changelog')}</span>
19 19 </a>
20 20 </li>
21 21 <li>
22 22 <a title="${_('Files')}" href="${h.url('files_home',repo_name=repo_name)}">
23 23 <span>${_('Files')}</span>
24 24 </a>
25 25 </li>
26 26 <li>
27 27 <a title="${_('Fork')}" href="${h.url('repo_fork_home',repo_name=repo_name)}">
28 28 <span>${_('Fork')}</span>
29 29 </a>
30 30 </li>
31 31 </ul>
32 32 </div>
33 33 </%def>
34 34
35 35 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
36 36 <%
37 37 def get_name(name,short_name=short_name):
38 38 if short_name:
39 39 return name.split('/')[-1]
40 40 else:
41 41 return name
42 42 %>
43 43 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
44 44 ##NAME
45 45 <a href="${h.url('edit_repo' if admin else 'summary_home',repo_name=name)}">
46 46
47 47 ##TYPE OF REPO
48 48 %if h.is_hg(rtype):
49 49 <span title="${_('Mercurial repository')}"><i class="icon-hg"></i></span>
50 50 %elif h.is_git(rtype):
51 51 <span title="${_('Git repository')}"><i class="icon-git"></i></span>
52 52 %elif h.is_svn(rtype):
53 53 <span title="${_('Subversion repository')}"><i class="icon-svn"></i></span>
54 54 %endif
55 55
56 56 ##PRIVATE/PUBLIC
57 57 %if private and c.visual.show_private_icon:
58 58 <i class="icon-lock" title="${_('Private repository')}"></i>
59 59 %elif not private and c.visual.show_public_icon:
60 60 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
61 61 %else:
62 62 <span></span>
63 63 %endif
64 64 ${get_name(name)}
65 65 </a>
66 66 %if fork_of:
67 67 <a href="${h.url('summary_home',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
68 68 %endif
69 69 %if rstate == 'repo_state_pending':
70 70 <i class="icon-cogs" title="${_('Repository creating in progress...')}"></i>
71 71 %endif
72 72 </div>
73 73 </%def>
74 74
75 75 <%def name="last_change(last_change)">
76 76 ${h.age_component(last_change)}
77 77 </%def>
78 78
79 79 <%def name="revision(name,rev,tip,author,last_msg)">
80 80 <div>
81 81 %if rev >= 0:
82 82 <code><a title="${h.tooltip('%s:\n\n%s' % (author,last_msg))}" class="tooltip" href="${h.url('changeset_home',repo_name=name,revision=tip)}">${'r%s:%s' % (rev,h.short_id(tip))}</a></code>
83 83 %else:
84 84 ${_('No commits yet')}
85 85 %endif
86 86 </div>
87 87 </%def>
88 88
89 89 <%def name="rss(name)">
90 90 %if c.rhodecode_user.username != h.DEFAULT_USER:
91 91 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
92 92 %else:
93 93 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
94 94 %endif
95 95 </%def>
96 96
97 97 <%def name="atom(name)">
98 98 %if c.rhodecode_user.username != h.DEFAULT_USER:
99 99 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
100 100 %else:
101 101 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
102 102 %endif
103 103 </%def>
104 104
105 105 <%def name="user_gravatar(email, size=16)">
106 106 ${base.gravatar(email, 16)}
107 107 </%def>
108 108
109 109 <%def name="repo_actions(repo_name, super_user=True)">
110 110 <div>
111 111 <div class="grid_edit">
112 112 <a href="${h.url('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
113 113 <i class="icon-pencil"></i>Edit</a>
114 114 </div>
115 115 <div class="grid_delete">
116 116 ${h.secure_form(h.url('repo', repo_name=repo_name),method='delete')}
117 117 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
118 118 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
119 119 ${h.end_form()}
120 120 </div>
121 121 </div>
122 122 </%def>
123 123
124 124 <%def name="repo_state(repo_state)">
125 125 <div>
126 126 %if repo_state == 'repo_state_pending':
127 127 <div class="tag tag4">${_('Creating')}</div>
128 128 %elif repo_state == 'repo_state_created':
129 129 <div class="tag tag1">${_('Created')}</div>
130 130 %else:
131 131 <div class="tag alert2" title="${repo_state}">invalid</div>
132 132 %endif
133 133 </div>
134 134 </%def>
135 135
136 136
137 137 ## REPO GROUP RENDERERS
138 138 <%def name="quick_repo_group_menu(repo_group_name)">
139 139 <i class="pointer icon-more"></i>
140 140 <div class="menu_items_container hidden">
141 141 <ul class="menu_items">
142 142 <li>
143 143 <a href="${h.url('repo_group_home',group_name=repo_group_name)}">
144 144 <span class="icon">
145 145 <i class="icon-file-text"></i>
146 146 </span>
147 147 <span>${_('Summary')}</span>
148 148 </a>
149 149 </li>
150 150
151 151 </ul>
152 152 </div>
153 153 </%def>
154 154
155 155 <%def name="repo_group_name(repo_group_name, children_groups=None)">
156 156 <div>
157 157 <a href="${h.url('repo_group_home',group_name=repo_group_name)}">
158 158 <i class="icon-folder-close" title="${_('Repository group')}"></i>
159 159 %if children_groups:
160 160 ${h.literal(' &raquo; '.join(children_groups))}
161 161 %else:
162 162 ${repo_group_name}
163 163 %endif
164 164 </a>
165 165 </div>
166 166 </%def>
167 167
168 168 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
169 169 <div class="grid_edit">
170 170 <a href="${h.url('edit_repo_group',group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
171 171 </div>
172 172 <div class="grid_delete">
173 173 ${h.secure_form(h.url('delete_repo_group', group_name=repo_group_name),method='delete')}
174 174 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
175 175 onclick="return confirm('"+ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
176 176 ${h.end_form()}
177 177 </div>
178 178 </%def>
179 179
180 180
181 181 <%def name="user_actions(user_id, username)">
182 182 <div class="grid_edit">
183 183 <a href="${h.url('edit_user',user_id=user_id)}" title="${_('Edit')}">
184 184 <i class="icon-pencil"></i>Edit</a>
185 185 </div>
186 186 <div class="grid_delete">
187 187 ${h.secure_form(h.url('delete_user', user_id=user_id),method='delete')}
188 188 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
189 189 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
190 190 ${h.end_form()}
191 191 </div>
192 192 </%def>
193 193
194 194 <%def name="user_group_actions(user_group_id, user_group_name)">
195 195 <div class="grid_edit">
196 196 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
197 197 </div>
198 198 <div class="grid_delete">
199 199 ${h.secure_form(h.url('delete_users_group', user_group_id=user_group_id),method='delete')}
200 200 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
201 201 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
202 202 ${h.end_form()}
203 203 </div>
204 204 </%def>
205 205
206 206
207 207 <%def name="user_name(user_id, username)">
208 208 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.url('edit_user', user_id=user_id))}
209 209 </%def>
210 210
211 211 <%def name="user_profile(username)">
212 212 ${base.gravatar_with_user(username, 16)}
213 213 </%def>
214 214
215 215 <%def name="user_group_name(user_group_id, user_group_name)">
216 216 <div>
217 217 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}">
218 218 <i class="icon-group" title="${_('User group')}"></i> ${user_group_name}</a>
219 219 </div>
220 220 </%def>
221 221
222 222
223 223 ## GISTS
224 224
225 225 <%def name="gist_gravatar(full_contact)">
226 226 <div class="gist_gravatar">
227 227 ${base.gravatar(full_contact, 30)}
228 228 </div>
229 229 </%def>
230 230
231 231 <%def name="gist_access_id(gist_access_id, full_contact)">
232 232 <div>
233 233 <b>
234 234 <a href="${h.url('gist',gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
235 235 </b>
236 236 </div>
237 237 </%def>
238 238
239 239 <%def name="gist_author(full_contact, created_on, expires)">
240 240 ${base.gravatar_with_user(full_contact, 16)}
241 241 </%def>
242 242
243 243
244 244 <%def name="gist_created(created_on)">
245 245 <div class="created">
246 ${h.age_component(created_on)}
246 ${h.age_component(created_on, time_is_local=True)}
247 247 </div>
248 248 </%def>
249 249
250 250 <%def name="gist_expires(expires)">
251 251 <div class="created">
252 252 %if expires == -1:
253 253 ${_('never')}
254 254 %else:
255 ${h.age_component(h.time_to_datetime(expires))}
255 ${h.age_component(h.time_to_utcdatetime(expires))}
256 256 %endif
257 257 </div>
258 258 </%def>
259 259
260 260 <%def name="gist_type(gist_type)">
261 261 %if gist_type != 'public':
262 262 <div class="tag">${_('Private')}</div>
263 263 %endif
264 264 </%def>
265 265
266 266 <%def name="gist_description(gist_description)">
267 267 ${gist_description}
268 268 </%def>
269 269
270 270
271 271 ## PULL REQUESTS GRID RENDERERS
272 272 <%def name="pullrequest_status(status)">
273 273 <div class="${'flag_status %s' % status} pull-left"></div>
274 274 </%def>
275 275
276 276 <%def name="pullrequest_title(title, description)">
277 277 ${title} <br/>
278 278 ${h.shorter(description, 40)}
279 279 </%def>
280 280
281 281 <%def name="pullrequest_comments(comments_nr)">
282 282 <i class="icon-comment icon-comment-colored"></i> ${comments_nr}
283 283 </%def>
284 284
285 285 <%def name="pullrequest_name(pull_request_id, target_repo_name)">
286 286 <a href="${h.url('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
287 287 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
288 288 </a>
289 289 </%def>
290 290
291 291 <%def name="pullrequest_updated_on(updated_on)">
292 ${h.age_component(h.time_to_datetime(updated_on))}
292 ${h.age_component(h.time_to_utcdatetime(updated_on))}
293 293 </%def>
294 294
295 295 <%def name="pullrequest_author(full_contact)">
296 296 ${base.gravatar_with_user(full_contact, 16)}
297 297 </%def>
@@ -1,31 +1,31 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%namespace name="base" file="/base/base.html"/>
3 3 <table class="rctable followers_data">
4 4 <tr>
5 5 <th>${_('Follower Name')}</th>
6 6 <th>${_('Following Since')}</th>
7 7 </tr>
8 8 % for f in c.followers_pager:
9 9 <tr>
10 10 <td class="td-user follower_user">
11 11 ${base.gravatar_with_user(f.user.email, 16)}
12 12 </td>
13 13 <td class="td-time follower_date">
14 ${h.age_component(f.follows_from)}
14 ${h.age_component(f.follows_from, time_is_local=True)}
15 15 </td>
16 16 </tr>
17 17 % endfor
18 18 </table>
19 19
20 20 <div class="pagination-wh pagination-left">
21 21 <script type="text/javascript">
22 22 $(document).pjax('#followers.pager_link','#followers');
23 23 $(document).on('pjax:success',function(){
24 24 show_more_event();
25 25 timeagoActivate();
26 26 tooltip_activate();
27 27 show_changeset_tooltip();
28 28 });
29 29 </script>
30 30 ${c.followers_pager.pager('$link_previous ~2~ $link_next')}
31 31 </div>
@@ -1,49 +1,49 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%namespace name="base" file="/base/base.html"/>
3 3
4 4 % if c.forks_pager:
5 5 <table class="rctable fork_summary">
6 6 <tr>
7 7 <th>${_('Owner')}</th>
8 8 <th>${_('Fork')}</th>
9 9 <th>${_('Description')}</th>
10 10 <th>${_('Forked')}</th>
11 11 <th></th>
12 12 </tr>
13 13 % for f in c.forks_pager:
14 14 <tr>
15 15 <td class="td-user fork_user">
16 16 ${base.gravatar_with_user(f.user.email, 16)}
17 17 </td>
18 18 <td class="td-componentname">
19 19 ${h.link_to(f.repo_name,h.url('summary_home',repo_name=f.repo_name))}
20 20 </td>
21 21 <td class="td-description">
22 22 <div class="truncate">${f.description}</div>
23 23 </td>
24 24 <td class="td-time follower_date">
25 ${h.age_component(f.created_on)}
25 ${h.age_component(f.created_on, time_is_local=True)}
26 26 </td>
27 27 <td class="td-compare">
28 28 <a title="${_('Compare fork with %s' % c.repo_name)}"
29 29 href="${h.url('compare_url',repo_name=c.repo_name, source_ref_type=c.rhodecode_db_repo.landing_rev[0],source_ref=c.rhodecode_db_repo.landing_rev[1],target_repo=f.repo_name,target_ref_type=c.rhodecode_db_repo.landing_rev[0],target_ref=c.rhodecode_db_repo.landing_rev[1], merge=1)}"
30 30 class="btn-link"><i class="icon-loop"></i> ${_('Compare fork')}</a>
31 31 </td>
32 32 </tr>
33 33 % endfor
34 34 </table>
35 35 <div class="pagination-wh pagination-left">
36 36 <script type="text/javascript">
37 37 $(document).pjax('#forks .pager_link','#forks');
38 38 $(document).on('pjax:success',function(){
39 39 show_more_event();
40 40 timeagoActivate();
41 41 tooltip_activate();
42 42 show_changeset_tooltip();
43 43 });
44 44 </script>
45 45 ${c.forks_pager.pager('$link_previous ~2~ $link_next')}
46 46 </div>
47 47 % else:
48 48 ${_('There are no forks yet')}
49 49 % endif
@@ -1,55 +1,55 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%namespace name="base" file="/base/base.html"/>
3 3
4 4 %if c.journal_day_aggreagate:
5 5 %for day,items in c.journal_day_aggreagate:
6 6 <div class="journal_day">${day}</div>
7 7 % for user,entries in items:
8 8 <div class="journal_container">
9 9 ${base.gravatar(user.email if user else '', 30)}
10 10 %if user:
11 11 <div class="journal_user user">${h.link_to_user(user.username)}</div>
12 12 %else:
13 13 <div class="journal_user user deleted">${entries[0].username}</div>
14 14 %endif
15 15 <div class="journal_action_container">
16 16 % for entry in entries:
17 17 <div class="journal_icon"> ${h.action_parser(entry)[2]()}</div>
18 18 <div class="journal_action">${h.action_parser(entry)[0]()}</div>
19 19 <div class="journal_repo">
20 20 <span class="journal_repo_name">
21 21 %if entry.repository is not None:
22 22 ${h.link_to(entry.repository.repo_name,
23 23 h.url('summary_home',repo_name=entry.repository.repo_name))}
24 24 %else:
25 25 ${entry.repository_name}
26 26 %endif
27 27 </span>
28 28 </div>
29 29 <div class="journal_action_params">${h.literal(h.action_parser(entry)[1]())}</div>
30 30 <div class="date">
31 ${h.age_component(entry.action_date)}
31 ${h.age_component(entry.action_date, time_is_local=True)}
32 32 </div>
33 33 %endfor
34 34 </div>
35 35 </div>
36 36 %endfor
37 37 %endfor
38 38
39 39 <div class="pagination-wh pagination-left" >
40 40 ${c.journal_pager.pager('$link_previous ~2~ $link_next')}
41 41 </div>
42 42 <script type="text/javascript">
43 43 $(document).pjax('#journal .pager_link','#journal');
44 44 $(document).on('pjax:success',function(){
45 45 show_more_event();
46 46 timeagoActivate();
47 47 tooltip_activate();
48 48 show_changeset_tooltip();
49 49 });
50 50 </script>
51 51 %else:
52 52 <div>
53 53 ${_('No entries yet')}
54 54 </div>
55 55 %endif
@@ -1,82 +1,82 b''
1 1 <%namespace name="base" file="/base/base.html"/>
2 2
3 3 <table class="rctable search-results">
4 4 <tr>
5 5 <th>${_('Repository')}</th>
6 6 <th>${_('Commit')}</th>
7 7 <th></th>
8 8 <th>${_('Commit message')}</th>
9 9 <th>
10 10 %if c.sort == 'newfirst':
11 11 <a href="${c.url_generator(sort='oldfirst')}">${_('Age (new first)')}</a>
12 12 %else:
13 13 <a href="${c.url_generator(sort='newfirst')}">${_('Age (old first)')}</a>
14 14 %endif
15 15 </th>
16 16 <th>${_('Author')}</th>
17 17 </tr>
18 18 %for entry in c.formatted_results:
19 19 ## search results are additionally filtered, and this check is just a safe gate
20 20 % if h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results commit check'):
21 21 <tr class="body">
22 22 <td class="td-componentname">
23 23 %if h.get_repo_type_by_name(entry.get('repository')) == 'hg':
24 24 <i class="icon-hg"></i>
25 25 %elif h.get_repo_type_by_name(entry.get('repository')) == 'git':
26 26 <i class="icon-git"></i>
27 27 %elif h.get_repo_type_by_name(entry.get('repository')) == 'svn':
28 28 <i class="icon-svn"></i>
29 29 %endif
30 30 ${h.link_to(entry['repository'], h.url('summary_home',repo_name=entry['repository']))}
31 31 </td>
32 32 <td class="td-commit">
33 33 ${h.link_to(h._shorten_commit_id(entry['commit_id']),
34 34 h.url('changeset_home',repo_name=entry['repository'],revision=entry['commit_id']))}
35 35 </td>
36 36 <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')}">
37 37 <div class="show_more_col">
38 38 <i class="show_more"></i>&nbsp;
39 39 </div>
40 40 </td>
41 41 <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">
42 42 %if entry.get('message_hl'):
43 43 ${h.literal(entry['message_hl'])}
44 44 %else:
45 45 ${h.urlify_commit_message(entry['message'], entry['repository'])}
46 46 %endif
47 47 </td>
48 48 <td class="td-time">
49 ${h.age_component(h.time_to_datetime(entry['date']))}
49 ${h.age_component(h.time_to_utcdatetime(entry['date']))}
50 50 </td>
51 51
52 52 <td class="td-user author">
53 53 ${base.gravatar_with_user(entry['author'])}
54 54 </td>
55 55 </tr>
56 56 % endif
57 57 %endfor
58 58 </table>
59 59
60 60 %if c.cur_query and c.formatted_results:
61 61 <div class="pagination-wh pagination-left">
62 62 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
63 63 </div>
64 64 %endif
65 65
66 66 <script>
67 67 $('.expand_commit').on('click',function(e){
68 68 var target_expand = $(this);
69 69 var cid = target_expand.data('commit-id');
70 70
71 71 if (target_expand.hasClass('open')){
72 72 $('#c-'+cid).css({'height': '1.5em', 'white-space': 'nowrap', 'text-overflow': 'ellipsis', 'overflow':'hidden'})
73 73 $('#t-'+cid).css({'height': 'auto', 'line-height': '.9em', 'text-overflow': 'ellipsis', 'overflow':'hidden'})
74 74 target_expand.removeClass('open');
75 75 }
76 76 else {
77 77 $('#c-'+cid).css({'height': 'auto', 'white-space': 'normal', 'text-overflow': 'initial', 'overflow':'visible'})
78 78 $('#t-'+cid).css({'height': 'auto', 'max-height': 'none', 'text-overflow': 'initial', 'overflow':'visible'})
79 79 target_expand.addClass('open');
80 80 }
81 81 });
82 82 </script>
General Comments 0
You need to be logged in to leave comments. Login now