##// END OF EJS Templates
show correct values for compare view when multiple git tags are pushed
marcink -
r3507:71fa9a19 beta
parent child Browse files
Show More
@@ -1,1186 +1,1195 b''
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10 import math
11 11 import logging
12 12 import re
13 13 import urlparse
14 14 import textwrap
15 15
16 16 from datetime import datetime
17 17 from pygments.formatters.html import HtmlFormatter
18 18 from pygments import highlight as code_highlight
19 19 from pylons import url, request, config
20 20 from pylons.i18n.translation import _, ungettext
21 21 from hashlib import md5
22 22
23 23 from webhelpers.html import literal, HTML, escape
24 24 from webhelpers.html.tools import *
25 25 from webhelpers.html.builder import make_tag
26 26 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
27 27 end_form, file, form, hidden, image, javascript_link, link_to, \
28 28 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
29 29 submit, text, password, textarea, title, ul, xml_declaration, radio
30 30 from webhelpers.html.tools import auto_link, button_to, highlight, \
31 31 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
32 32 from webhelpers.number import format_byte_size, format_bit_size
33 33 from webhelpers.pylonslib import Flash as _Flash
34 34 from webhelpers.pylonslib.secure_form import secure_form
35 35 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
36 36 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
37 37 replace_whitespace, urlify, truncate, wrap_paragraphs
38 38 from webhelpers.date import time_ago_in_words
39 39 from webhelpers.paginate import Page
40 40 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
41 41 convert_boolean_attrs, NotGiven, _make_safe_id_component
42 42
43 43 from rhodecode.lib.annotate import annotate_highlight
44 44 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
45 45 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
46 46 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict
47 47 from rhodecode.lib.markup_renderer import MarkupRenderer
48 48 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
49 49 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
50 50 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
51 51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 52 from rhodecode.model.db import URL_SEP, Permission
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 html_escape_table = {
58 58 "&": "&",
59 59 '"': """,
60 60 "'": "'",
61 61 ">": ">",
62 62 "<": "&lt;",
63 63 }
64 64
65 65
66 66 def html_escape(text):
67 67 """Produce entities within text."""
68 68 return "".join(html_escape_table.get(c, c) for c in text)
69 69
70 70
71 71 def shorter(text, size=20):
72 72 postfix = '...'
73 73 if len(text) > size:
74 74 return text[:size - len(postfix)] + postfix
75 75 return text
76 76
77 77
78 78 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
79 79 """
80 80 Reset button
81 81 """
82 82 _set_input_attrs(attrs, type, name, value)
83 83 _set_id_attr(attrs, id, name)
84 84 convert_boolean_attrs(attrs, ["disabled"])
85 85 return HTML.input(**attrs)
86 86
87 87 reset = _reset
88 88 safeid = _make_safe_id_component
89 89
90 90
91 91 def FID(raw_id, path):
92 92 """
93 93 Creates a uniqe ID for filenode based on it's hash of path and revision
94 94 it's safe to use in urls
95 95
96 96 :param raw_id:
97 97 :param path:
98 98 """
99 99
100 100 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
101 101
102 102
103 103 def get_token():
104 104 """Return the current authentication token, creating one if one doesn't
105 105 already exist.
106 106 """
107 107 token_key = "_authentication_token"
108 108 from pylons import session
109 109 if not token_key in session:
110 110 try:
111 111 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
112 112 except AttributeError: # Python < 2.4
113 113 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
114 114 session[token_key] = token
115 115 if hasattr(session, 'save'):
116 116 session.save()
117 117 return session[token_key]
118 118
119 119
120 120 class _GetError(object):
121 121 """Get error from form_errors, and represent it as span wrapped error
122 122 message
123 123
124 124 :param field_name: field to fetch errors for
125 125 :param form_errors: form errors dict
126 126 """
127 127
128 128 def __call__(self, field_name, form_errors):
129 129 tmpl = """<span class="error_msg">%s</span>"""
130 130 if form_errors and field_name in form_errors:
131 131 return literal(tmpl % form_errors.get(field_name))
132 132
133 133 get_error = _GetError()
134 134
135 135
136 136 class _ToolTip(object):
137 137
138 138 def __call__(self, tooltip_title, trim_at=50):
139 139 """
140 140 Special function just to wrap our text into nice formatted
141 141 autowrapped text
142 142
143 143 :param tooltip_title:
144 144 """
145 145 tooltip_title = escape(tooltip_title)
146 146 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
147 147 return tooltip_title
148 148 tooltip = _ToolTip()
149 149
150 150
151 151 class _FilesBreadCrumbs(object):
152 152
153 153 def __call__(self, repo_name, rev, paths):
154 154 if isinstance(paths, str):
155 155 paths = safe_unicode(paths)
156 156 url_l = [link_to(repo_name, url('files_home',
157 157 repo_name=repo_name,
158 158 revision=rev, f_path=''),
159 159 class_='ypjax-link')]
160 160 paths_l = paths.split('/')
161 161 for cnt, p in enumerate(paths_l):
162 162 if p != '':
163 163 url_l.append(link_to(p,
164 164 url('files_home',
165 165 repo_name=repo_name,
166 166 revision=rev,
167 167 f_path='/'.join(paths_l[:cnt + 1])
168 168 ),
169 169 class_='ypjax-link'
170 170 )
171 171 )
172 172
173 173 return literal('/'.join(url_l))
174 174
175 175 files_breadcrumbs = _FilesBreadCrumbs()
176 176
177 177
178 178 class CodeHtmlFormatter(HtmlFormatter):
179 179 """
180 180 My code Html Formatter for source codes
181 181 """
182 182
183 183 def wrap(self, source, outfile):
184 184 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
185 185
186 186 def _wrap_code(self, source):
187 187 for cnt, it in enumerate(source):
188 188 i, t = it
189 189 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
190 190 yield i, t
191 191
192 192 def _wrap_tablelinenos(self, inner):
193 193 dummyoutfile = StringIO.StringIO()
194 194 lncount = 0
195 195 for t, line in inner:
196 196 if t:
197 197 lncount += 1
198 198 dummyoutfile.write(line)
199 199
200 200 fl = self.linenostart
201 201 mw = len(str(lncount + fl - 1))
202 202 sp = self.linenospecial
203 203 st = self.linenostep
204 204 la = self.lineanchors
205 205 aln = self.anchorlinenos
206 206 nocls = self.noclasses
207 207 if sp:
208 208 lines = []
209 209
210 210 for i in range(fl, fl + lncount):
211 211 if i % st == 0:
212 212 if i % sp == 0:
213 213 if aln:
214 214 lines.append('<a href="#%s%d" class="special">%*d</a>' %
215 215 (la, i, mw, i))
216 216 else:
217 217 lines.append('<span class="special">%*d</span>' % (mw, i))
218 218 else:
219 219 if aln:
220 220 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
221 221 else:
222 222 lines.append('%*d' % (mw, i))
223 223 else:
224 224 lines.append('')
225 225 ls = '\n'.join(lines)
226 226 else:
227 227 lines = []
228 228 for i in range(fl, fl + lncount):
229 229 if i % st == 0:
230 230 if aln:
231 231 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
232 232 else:
233 233 lines.append('%*d' % (mw, i))
234 234 else:
235 235 lines.append('')
236 236 ls = '\n'.join(lines)
237 237
238 238 # in case you wonder about the seemingly redundant <div> here: since the
239 239 # content in the other cell also is wrapped in a div, some browsers in
240 240 # some configurations seem to mess up the formatting...
241 241 if nocls:
242 242 yield 0, ('<table class="%stable">' % self.cssclass +
243 243 '<tr><td><div class="linenodiv" '
244 244 'style="background-color: #f0f0f0; padding-right: 10px">'
245 245 '<pre style="line-height: 125%">' +
246 246 ls + '</pre></div></td><td id="hlcode" class="code">')
247 247 else:
248 248 yield 0, ('<table class="%stable">' % self.cssclass +
249 249 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
250 250 ls + '</pre></div></td><td id="hlcode" class="code">')
251 251 yield 0, dummyoutfile.getvalue()
252 252 yield 0, '</td></tr></table>'
253 253
254 254
255 255 def pygmentize(filenode, **kwargs):
256 256 """
257 257 pygmentize function using pygments
258 258
259 259 :param filenode:
260 260 """
261 261 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
262 262 return literal(code_highlight(filenode.content, lexer,
263 263 CodeHtmlFormatter(**kwargs)))
264 264
265 265
266 266 def pygmentize_annotation(repo_name, filenode, **kwargs):
267 267 """
268 268 pygmentize function for annotation
269 269
270 270 :param filenode:
271 271 """
272 272
273 273 color_dict = {}
274 274
275 275 def gen_color(n=10000):
276 276 """generator for getting n of evenly distributed colors using
277 277 hsv color and golden ratio. It always return same order of colors
278 278
279 279 :returns: RGB tuple
280 280 """
281 281
282 282 def hsv_to_rgb(h, s, v):
283 283 if s == 0.0:
284 284 return v, v, v
285 285 i = int(h * 6.0) # XXX assume int() truncates!
286 286 f = (h * 6.0) - i
287 287 p = v * (1.0 - s)
288 288 q = v * (1.0 - s * f)
289 289 t = v * (1.0 - s * (1.0 - f))
290 290 i = i % 6
291 291 if i == 0:
292 292 return v, t, p
293 293 if i == 1:
294 294 return q, v, p
295 295 if i == 2:
296 296 return p, v, t
297 297 if i == 3:
298 298 return p, q, v
299 299 if i == 4:
300 300 return t, p, v
301 301 if i == 5:
302 302 return v, p, q
303 303
304 304 golden_ratio = 0.618033988749895
305 305 h = 0.22717784590367374
306 306
307 307 for _ in xrange(n):
308 308 h += golden_ratio
309 309 h %= 1
310 310 HSV_tuple = [h, 0.95, 0.95]
311 311 RGB_tuple = hsv_to_rgb(*HSV_tuple)
312 312 yield map(lambda x: str(int(x * 256)), RGB_tuple)
313 313
314 314 cgenerator = gen_color()
315 315
316 316 def get_color_string(cs):
317 317 if cs in color_dict:
318 318 col = color_dict[cs]
319 319 else:
320 320 col = color_dict[cs] = cgenerator.next()
321 321 return "color: rgb(%s)! important;" % (', '.join(col))
322 322
323 323 def url_func(repo_name):
324 324
325 325 def _url_func(changeset):
326 326 author = changeset.author
327 327 date = changeset.date
328 328 message = tooltip(changeset.message)
329 329
330 330 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
331 331 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
332 332 "</b> %s<br/></div>")
333 333
334 334 tooltip_html = tooltip_html % (author, date, message)
335 335 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
336 336 short_id(changeset.raw_id))
337 337 uri = link_to(
338 338 lnk_format,
339 339 url('changeset_home', repo_name=repo_name,
340 340 revision=changeset.raw_id),
341 341 style=get_color_string(changeset.raw_id),
342 342 class_='tooltip',
343 343 title=tooltip_html
344 344 )
345 345
346 346 uri += '\n'
347 347 return uri
348 348 return _url_func
349 349
350 350 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
351 351
352 352
353 353 def is_following_repo(repo_name, user_id):
354 354 from rhodecode.model.scm import ScmModel
355 355 return ScmModel().is_following_repo(repo_name, user_id)
356 356
357 357 flash = _Flash()
358 358
359 359 #==============================================================================
360 360 # SCM FILTERS available via h.
361 361 #==============================================================================
362 362 from rhodecode.lib.vcs.utils import author_name, author_email
363 363 from rhodecode.lib.utils2 import credentials_filter, age as _age
364 364 from rhodecode.model.db import User, ChangesetStatus
365 365
366 366 age = lambda x: _age(x)
367 367 capitalize = lambda x: x.capitalize()
368 368 email = author_email
369 369 short_id = lambda x: x[:12]
370 370 hide_credentials = lambda x: ''.join(credentials_filter(x))
371 371
372 372
373 373 def fmt_date(date):
374 374 if date:
375 375 _fmt = _(u"%a, %d %b %Y %H:%M:%S").encode('utf8')
376 376 return date.strftime(_fmt).decode('utf8')
377 377
378 378 return ""
379 379
380 380
381 381 def is_git(repository):
382 382 if hasattr(repository, 'alias'):
383 383 _type = repository.alias
384 384 elif hasattr(repository, 'repo_type'):
385 385 _type = repository.repo_type
386 386 else:
387 387 _type = repository
388 388 return _type == 'git'
389 389
390 390
391 391 def is_hg(repository):
392 392 if hasattr(repository, 'alias'):
393 393 _type = repository.alias
394 394 elif hasattr(repository, 'repo_type'):
395 395 _type = repository.repo_type
396 396 else:
397 397 _type = repository
398 398 return _type == 'hg'
399 399
400 400
401 401 def email_or_none(author):
402 402 # extract email from the commit string
403 403 _email = email(author)
404 404 if _email != '':
405 405 # check it against RhodeCode database, and use the MAIN email for this
406 406 # user
407 407 user = User.get_by_email(_email, case_insensitive=True, cache=True)
408 408 if user is not None:
409 409 return user.email
410 410 return _email
411 411
412 412 # See if it contains a username we can get an email from
413 413 user = User.get_by_username(author_name(author), case_insensitive=True,
414 414 cache=True)
415 415 if user is not None:
416 416 return user.email
417 417
418 418 # No valid email, not a valid user in the system, none!
419 419 return None
420 420
421 421
422 422 def person(author, show_attr="username_and_name"):
423 423 # attr to return from fetched user
424 424 person_getter = lambda usr: getattr(usr, show_attr)
425 425
426 426 # Valid email in the attribute passed, see if they're in the system
427 427 _email = email(author)
428 428 if _email != '':
429 429 user = User.get_by_email(_email, case_insensitive=True, cache=True)
430 430 if user is not None:
431 431 return person_getter(user)
432 432 return _email
433 433
434 434 # Maybe it's a username?
435 435 _author = author_name(author)
436 436 user = User.get_by_username(_author, case_insensitive=True,
437 437 cache=True)
438 438 if user is not None:
439 439 return person_getter(user)
440 440
441 441 # Still nothing? Just pass back the author name then
442 442 return _author
443 443
444 444
445 445 def person_by_id(id_, show_attr="username_and_name"):
446 446 # attr to return from fetched user
447 447 person_getter = lambda usr: getattr(usr, show_attr)
448 448
449 449 #maybe it's an ID ?
450 450 if str(id_).isdigit() or isinstance(id_, int):
451 451 id_ = int(id_)
452 452 user = User.get(id_)
453 453 if user is not None:
454 454 return person_getter(user)
455 455 return id_
456 456
457 457
458 458 def desc_stylize(value):
459 459 """
460 460 converts tags from value into html equivalent
461 461
462 462 :param value:
463 463 """
464 464 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
465 465 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
466 466 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
467 467 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
468 468 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
469 469 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
470 470 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
471 471 '<div class="metatag" tag="lang">\\2</div>', value)
472 472 value = re.sub(r'\[([a-z]+)\]',
473 473 '<div class="metatag" tag="\\1">\\1</div>', value)
474 474
475 475 return value
476 476
477 477
478 478 def bool2icon(value):
479 479 """Returns True/False values represented as small html image of true/false
480 480 icons
481 481
482 482 :param value: bool value
483 483 """
484 484
485 485 if value is True:
486 486 return HTML.tag('img', src=url("/images/icons/accept.png"),
487 487 alt=_('True'))
488 488
489 489 if value is False:
490 490 return HTML.tag('img', src=url("/images/icons/cancel.png"),
491 491 alt=_('False'))
492 492
493 493 return value
494 494
495 495
496 496 def action_parser(user_log, feed=False, parse_cs=False):
497 497 """
498 498 This helper will action_map the specified string action into translated
499 499 fancy names with icons and links
500 500
501 501 :param user_log: user log instance
502 502 :param feed: use output for feeds (no html and fancy icons)
503 503 :param parse_cs: parse Changesets into VCS instances
504 504 """
505 505
506 506 action = user_log.action
507 507 action_params = ' '
508 508
509 509 x = action.split(':')
510 510
511 511 if len(x) > 1:
512 512 action, action_params = x
513 513
514 514 def get_cs_links():
515 515 revs_limit = 3 # display this amount always
516 516 revs_top_limit = 50 # show upto this amount of changesets hidden
517 517 revs_ids = action_params.split(',')
518 518 deleted = user_log.repository is None
519 519 if deleted:
520 520 return ','.join(revs_ids)
521 521
522 522 repo_name = user_log.repository.repo_name
523 523
524 524 def lnk(rev, repo_name):
525 525 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
526 526 lazy_cs = True
527 527 if getattr(rev, 'op', None) and getattr(rev, 'ref_name', None):
528 528 lazy_cs = False
529 529 lbl = '?'
530 530 if rev.op == 'delete_branch':
531 531 lbl = '%s' % _('Deleted branch: %s') % rev.ref_name
532 532 title = ''
533 533 elif rev.op == 'tag':
534 534 lbl = '%s' % _('Created tag: %s') % rev.ref_name
535 535 title = ''
536 536 _url = '#'
537 537
538 538 else:
539 539 lbl = '%s' % (rev.short_id[:8])
540 540 _url = url('changeset_home', repo_name=repo_name,
541 541 revision=rev.raw_id)
542 542 title = tooltip(rev.message)
543 543 else:
544 544 ## changeset cannot be found/striped/removed etc.
545 545 lbl = ('%s' % rev)[:12]
546 546 _url = '#'
547 547 title = _('Changeset not found')
548 548 if parse_cs:
549 549 return link_to(lbl, _url, title=title, class_='tooltip')
550 550 return link_to(lbl, _url, raw_id=rev.raw_id, repo_name=repo_name,
551 551 class_='lazy-cs' if lazy_cs else '')
552 552
553 def _get_op(rev_txt):
554 _op = None
555 _name = rev_txt
556 if len(rev_txt.split('=>')) == 2:
557 _op, _name = rev_txt.split('=>')
558 return _op, _name
559
553 560 revs = []
554 561 if len(filter(lambda v: v != '', revs_ids)) > 0:
555 562 repo = None
556 563 for rev in revs_ids[:revs_top_limit]:
557 _op = _name = None
558 if len(rev.split('=>')) == 2:
559 _op, _name = rev.split('=>')
564 _op, _name = _get_op(rev)
560 565
561 566 # we want parsed changesets, or new log store format is bad
562 567 if parse_cs:
563 568 try:
564 569 if repo is None:
565 570 repo = user_log.repository.scm_instance
566 571 _rev = repo.get_changeset(rev)
567 572 revs.append(_rev)
568 573 except ChangesetDoesNotExistError:
569 574 log.error('cannot find revision %s in this repo' % rev)
570 575 revs.append(rev)
571 576 continue
572 577 else:
573 578 _rev = AttributeDict({
574 579 'short_id': rev[:12],
575 580 'raw_id': rev,
576 581 'message': '',
577 582 'op': _op,
578 583 'ref_name': _name
579 584 })
580 585 revs.append(_rev)
581 586 cs_links = []
582 587 cs_links.append(" " + ', '.join(
583 588 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
584 589 )
585 590 )
591 _op1, _name1 = _get_op(revs_ids[0])
592 _op2, _name2 = _get_op(revs_ids[-1])
593
594 _rev = '%s...%s' % (_name1, _name2)
586 595
587 596 compare_view = (
588 597 ' <div class="compare_view tooltip" title="%s">'
589 598 '<a href="%s">%s</a> </div>' % (
590 599 _('Show all combined changesets %s->%s') % (
591 600 revs_ids[0][:12], revs_ids[-1][:12]
592 601 ),
593 602 url('changeset_home', repo_name=repo_name,
594 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
603 revision=_rev
595 604 ),
596 605 _('compare view')
597 606 )
598 607 )
599 608
600 609 # if we have exactly one more than normally displayed
601 610 # just display it, takes less space than displaying
602 611 # "and 1 more revisions"
603 612 if len(revs_ids) == revs_limit + 1:
604 613 rev = revs[revs_limit]
605 614 cs_links.append(", " + lnk(rev, repo_name))
606 615
607 616 # hidden-by-default ones
608 617 if len(revs_ids) > revs_limit + 1:
609 618 uniq_id = revs_ids[0]
610 619 html_tmpl = (
611 620 '<span> %s <a class="show_more" id="_%s" '
612 621 'href="#more">%s</a> %s</span>'
613 622 )
614 623 if not feed:
615 624 cs_links.append(html_tmpl % (
616 625 _('and'),
617 626 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
618 627 _('revisions')
619 628 )
620 629 )
621 630
622 631 if not feed:
623 632 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
624 633 else:
625 634 html_tmpl = '<span id="%s"> %s </span>'
626 635
627 636 morelinks = ', '.join(
628 637 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
629 638 )
630 639
631 640 if len(revs_ids) > revs_top_limit:
632 641 morelinks += ', ...'
633 642
634 643 cs_links.append(html_tmpl % (uniq_id, morelinks))
635 644 if len(revs) > 1:
636 645 cs_links.append(compare_view)
637 646 return ''.join(cs_links)
638 647
639 648 def get_fork_name():
640 649 repo_name = action_params
641 650 _url = url('summary_home', repo_name=repo_name)
642 651 return _('fork name %s') % link_to(action_params, _url)
643 652
644 653 def get_user_name():
645 654 user_name = action_params
646 655 return user_name
647 656
648 657 def get_users_group():
649 658 group_name = action_params
650 659 return group_name
651 660
652 661 def get_pull_request():
653 662 pull_request_id = action_params
654 663 deleted = user_log.repository is None
655 664 if deleted:
656 665 repo_name = user_log.repository_name
657 666 else:
658 667 repo_name = user_log.repository.repo_name
659 668 return link_to(_('Pull request #%s') % pull_request_id,
660 669 url('pullrequest_show', repo_name=repo_name,
661 670 pull_request_id=pull_request_id))
662 671
663 672 # action : translated str, callback(extractor), icon
664 673 action_map = {
665 674 'user_deleted_repo': (_('[deleted] repository'),
666 675 None, 'database_delete.png'),
667 676 'user_created_repo': (_('[created] repository'),
668 677 None, 'database_add.png'),
669 678 'user_created_fork': (_('[created] repository as fork'),
670 679 None, 'arrow_divide.png'),
671 680 'user_forked_repo': (_('[forked] repository'),
672 681 get_fork_name, 'arrow_divide.png'),
673 682 'user_updated_repo': (_('[updated] repository'),
674 683 None, 'database_edit.png'),
675 684 'admin_deleted_repo': (_('[delete] repository'),
676 685 None, 'database_delete.png'),
677 686 'admin_created_repo': (_('[created] repository'),
678 687 None, 'database_add.png'),
679 688 'admin_forked_repo': (_('[forked] repository'),
680 689 None, 'arrow_divide.png'),
681 690 'admin_updated_repo': (_('[updated] repository'),
682 691 None, 'database_edit.png'),
683 692 'admin_created_user': (_('[created] user'),
684 693 get_user_name, 'user_add.png'),
685 694 'admin_updated_user': (_('[updated] user'),
686 695 get_user_name, 'user_edit.png'),
687 696 'admin_created_users_group': (_('[created] user group'),
688 697 get_users_group, 'group_add.png'),
689 698 'admin_updated_users_group': (_('[updated] user group'),
690 699 get_users_group, 'group_edit.png'),
691 700 'user_commented_revision': (_('[commented] on revision in repository'),
692 701 get_cs_links, 'comment_add.png'),
693 702 'user_commented_pull_request': (_('[commented] on pull request for'),
694 703 get_pull_request, 'comment_add.png'),
695 704 'user_closed_pull_request': (_('[closed] pull request for'),
696 705 get_pull_request, 'tick.png'),
697 706 'push': (_('[pushed] into'),
698 707 get_cs_links, 'script_add.png'),
699 708 'push_local': (_('[committed via RhodeCode] into repository'),
700 709 get_cs_links, 'script_edit.png'),
701 710 'push_remote': (_('[pulled from remote] into repository'),
702 711 get_cs_links, 'connect.png'),
703 712 'pull': (_('[pulled] from'),
704 713 None, 'down_16.png'),
705 714 'started_following_repo': (_('[started following] repository'),
706 715 None, 'heart_add.png'),
707 716 'stopped_following_repo': (_('[stopped following] repository'),
708 717 None, 'heart_delete.png'),
709 718 }
710 719
711 720 action_str = action_map.get(action, action)
712 721 if feed:
713 722 action = action_str[0].replace('[', '').replace(']', '')
714 723 else:
715 724 action = action_str[0]\
716 725 .replace('[', '<span class="journal_highlight">')\
717 726 .replace(']', '</span>')
718 727
719 728 action_params_func = lambda: ""
720 729
721 730 if callable(action_str[1]):
722 731 action_params_func = action_str[1]
723 732
724 733 def action_parser_icon():
725 734 action = user_log.action
726 735 action_params = None
727 736 x = action.split(':')
728 737
729 738 if len(x) > 1:
730 739 action, action_params = x
731 740
732 741 tmpl = """<img src="%s%s" alt="%s"/>"""
733 742 ico = action_map.get(action, ['', '', ''])[2]
734 743 return literal(tmpl % ((url('/images/icons/')), ico, action))
735 744
736 745 # returned callbacks we need to call to get
737 746 return [lambda: literal(action), action_params_func, action_parser_icon]
738 747
739 748
740 749
741 750 #==============================================================================
742 751 # PERMS
743 752 #==============================================================================
744 753 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
745 754 HasRepoPermissionAny, HasRepoPermissionAll, HasReposGroupPermissionAll, \
746 755 HasReposGroupPermissionAny
747 756
748 757
749 758 #==============================================================================
750 759 # GRAVATAR URL
751 760 #==============================================================================
752 761
753 762 def gravatar_url(email_address, size=30):
754 763 from pylons import url # doh, we need to re-import url to mock it later
755 764 _def = 'anonymous@rhodecode.org'
756 765 use_gravatar = str2bool(config['app_conf'].get('use_gravatar'))
757 766 email_address = email_address or _def
758 767 if (not use_gravatar or not email_address or email_address == _def):
759 768 f = lambda a, l: min(l, key=lambda x: abs(x - a))
760 769 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
761 770
762 771 if use_gravatar and config['app_conf'].get('alternative_gravatar_url'):
763 772 tmpl = config['app_conf'].get('alternative_gravatar_url', '')
764 773 parsed_url = urlparse.urlparse(url.current(qualified=True))
765 774 tmpl = tmpl.replace('{email}', email_address)\
766 775 .replace('{md5email}', hashlib.md5(email_address.lower()).hexdigest()) \
767 776 .replace('{netloc}', parsed_url.netloc)\
768 777 .replace('{scheme}', parsed_url.scheme)\
769 778 .replace('{size}', str(size))
770 779 return tmpl
771 780
772 781 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
773 782 default = 'identicon'
774 783 baseurl_nossl = "http://www.gravatar.com/avatar/"
775 784 baseurl_ssl = "https://secure.gravatar.com/avatar/"
776 785 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
777 786
778 787 if isinstance(email_address, unicode):
779 788 #hashlib crashes on unicode items
780 789 email_address = safe_str(email_address)
781 790 # construct the url
782 791 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
783 792 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
784 793
785 794 return gravatar_url
786 795
787 796
788 797 #==============================================================================
789 798 # REPO PAGER, PAGER FOR REPOSITORY
790 799 #==============================================================================
791 800 class RepoPage(Page):
792 801
793 802 def __init__(self, collection, page=1, items_per_page=20,
794 803 item_count=None, url=None, **kwargs):
795 804
796 805 """Create a "RepoPage" instance. special pager for paging
797 806 repository
798 807 """
799 808 self._url_generator = url
800 809
801 810 # Safe the kwargs class-wide so they can be used in the pager() method
802 811 self.kwargs = kwargs
803 812
804 813 # Save a reference to the collection
805 814 self.original_collection = collection
806 815
807 816 self.collection = collection
808 817
809 818 # The self.page is the number of the current page.
810 819 # The first page has the number 1!
811 820 try:
812 821 self.page = int(page) # make it int() if we get it as a string
813 822 except (ValueError, TypeError):
814 823 self.page = 1
815 824
816 825 self.items_per_page = items_per_page
817 826
818 827 # Unless the user tells us how many items the collections has
819 828 # we calculate that ourselves.
820 829 if item_count is not None:
821 830 self.item_count = item_count
822 831 else:
823 832 self.item_count = len(self.collection)
824 833
825 834 # Compute the number of the first and last available page
826 835 if self.item_count > 0:
827 836 self.first_page = 1
828 837 self.page_count = int(math.ceil(float(self.item_count) /
829 838 self.items_per_page))
830 839 self.last_page = self.first_page + self.page_count - 1
831 840
832 841 # Make sure that the requested page number is the range of
833 842 # valid pages
834 843 if self.page > self.last_page:
835 844 self.page = self.last_page
836 845 elif self.page < self.first_page:
837 846 self.page = self.first_page
838 847
839 848 # Note: the number of items on this page can be less than
840 849 # items_per_page if the last page is not full
841 850 self.first_item = max(0, (self.item_count) - (self.page *
842 851 items_per_page))
843 852 self.last_item = ((self.item_count - 1) - items_per_page *
844 853 (self.page - 1))
845 854
846 855 self.items = list(self.collection[self.first_item:self.last_item + 1])
847 856
848 857 # Links to previous and next page
849 858 if self.page > self.first_page:
850 859 self.previous_page = self.page - 1
851 860 else:
852 861 self.previous_page = None
853 862
854 863 if self.page < self.last_page:
855 864 self.next_page = self.page + 1
856 865 else:
857 866 self.next_page = None
858 867
859 868 # No items available
860 869 else:
861 870 self.first_page = None
862 871 self.page_count = 0
863 872 self.last_page = None
864 873 self.first_item = None
865 874 self.last_item = None
866 875 self.previous_page = None
867 876 self.next_page = None
868 877 self.items = []
869 878
870 879 # This is a subclass of the 'list' type. Initialise the list now.
871 880 list.__init__(self, reversed(self.items))
872 881
873 882
874 883 def changed_tooltip(nodes):
875 884 """
876 885 Generates a html string for changed nodes in changeset page.
877 886 It limits the output to 30 entries
878 887
879 888 :param nodes: LazyNodesGenerator
880 889 """
881 890 if nodes:
882 891 pref = ': <br/> '
883 892 suf = ''
884 893 if len(nodes) > 30:
885 894 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
886 895 return literal(pref + '<br/> '.join([safe_unicode(x.path)
887 896 for x in nodes[:30]]) + suf)
888 897 else:
889 898 return ': ' + _('No Files')
890 899
891 900
892 901 def repo_link(groups_and_repos, last_url=None):
893 902 """
894 903 Makes a breadcrumbs link to repo within a group
895 904 joins &raquo; on each group to create a fancy link
896 905
897 906 ex::
898 907 group >> subgroup >> repo
899 908
900 909 :param groups_and_repos:
901 910 :param last_url:
902 911 """
903 912 groups, repo_name = groups_and_repos
904 913 last_link = link_to(repo_name, last_url) if last_url else repo_name
905 914
906 915 if not groups:
907 916 if last_url:
908 917 return last_link
909 918 return repo_name
910 919 else:
911 920 def make_link(group):
912 921 return link_to(group.name,
913 922 url('repos_group_home', group_name=group.group_name))
914 923 return literal(' &raquo; '.join(map(make_link, groups) + [last_link]))
915 924
916 925
917 926 def fancy_file_stats(stats):
918 927 """
919 928 Displays a fancy two colored bar for number of added/deleted
920 929 lines of code on file
921 930
922 931 :param stats: two element list of added/deleted lines of code
923 932 """
924 933 def cgen(l_type, a_v, d_v):
925 934 mapping = {'tr': 'top-right-rounded-corner-mid',
926 935 'tl': 'top-left-rounded-corner-mid',
927 936 'br': 'bottom-right-rounded-corner-mid',
928 937 'bl': 'bottom-left-rounded-corner-mid'}
929 938 map_getter = lambda x: mapping[x]
930 939
931 940 if l_type == 'a' and d_v:
932 941 #case when added and deleted are present
933 942 return ' '.join(map(map_getter, ['tl', 'bl']))
934 943
935 944 if l_type == 'a' and not d_v:
936 945 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
937 946
938 947 if l_type == 'd' and a_v:
939 948 return ' '.join(map(map_getter, ['tr', 'br']))
940 949
941 950 if l_type == 'd' and not a_v:
942 951 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
943 952
944 953 a, d = stats[0], stats[1]
945 954 width = 100
946 955
947 956 if a == 'b':
948 957 #binary mode
949 958 b_d = '<div class="bin%s %s" style="width:100%%">%s</div>' % (d, cgen('a', a_v='', d_v=0), 'bin')
950 959 b_a = '<div class="bin1" style="width:0%%">%s</div>' % ('bin')
951 960 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
952 961
953 962 t = stats[0] + stats[1]
954 963 unit = float(width) / (t or 1)
955 964
956 965 # needs > 9% of width to be visible or 0 to be hidden
957 966 a_p = max(9, unit * a) if a > 0 else 0
958 967 d_p = max(9, unit * d) if d > 0 else 0
959 968 p_sum = a_p + d_p
960 969
961 970 if p_sum > width:
962 971 #adjust the percentage to be == 100% since we adjusted to 9
963 972 if a_p > d_p:
964 973 a_p = a_p - (p_sum - width)
965 974 else:
966 975 d_p = d_p - (p_sum - width)
967 976
968 977 a_v = a if a > 0 else ''
969 978 d_v = d if d > 0 else ''
970 979
971 980 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
972 981 cgen('a', a_v, d_v), a_p, a_v
973 982 )
974 983 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
975 984 cgen('d', a_v, d_v), d_p, d_v
976 985 )
977 986 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
978 987
979 988
980 989 def urlify_text(text_, safe=True):
981 990 """
982 991 Extrac urls from text and make html links out of them
983 992
984 993 :param text_:
985 994 """
986 995
987 996 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
988 997 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
989 998
990 999 def url_func(match_obj):
991 1000 url_full = match_obj.groups()[0]
992 1001 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
993 1002 _newtext = url_pat.sub(url_func, text_)
994 1003 if safe:
995 1004 return literal(_newtext)
996 1005 return _newtext
997 1006
998 1007
999 1008 def urlify_changesets(text_, repository):
1000 1009 """
1001 1010 Extract revision ids from changeset and make link from them
1002 1011
1003 1012 :param text_:
1004 1013 :param repository: repo name to build the URL with
1005 1014 """
1006 1015 from pylons import url # doh, we need to re-import url to mock it later
1007 1016 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1008 1017
1009 1018 def url_func(match_obj):
1010 1019 rev = match_obj.groups()[1]
1011 1020 pref = match_obj.groups()[0]
1012 1021 suf = match_obj.groups()[2]
1013 1022
1014 1023 tmpl = (
1015 1024 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1016 1025 '%(rev)s</a>%(suf)s'
1017 1026 )
1018 1027 return tmpl % {
1019 1028 'pref': pref,
1020 1029 'cls': 'revision-link',
1021 1030 'url': url('changeset_home', repo_name=repository, revision=rev),
1022 1031 'rev': rev,
1023 1032 'suf': suf
1024 1033 }
1025 1034
1026 1035 newtext = URL_PAT.sub(url_func, text_)
1027 1036
1028 1037 return newtext
1029 1038
1030 1039
1031 1040 def urlify_commit(text_, repository=None, link_=None):
1032 1041 """
1033 1042 Parses given text message and makes proper links.
1034 1043 issues are linked to given issue-server, and rest is a changeset link
1035 1044 if link_ is given, in other case it's a plain text
1036 1045
1037 1046 :param text_:
1038 1047 :param repository:
1039 1048 :param link_: changeset link
1040 1049 """
1041 1050 import traceback
1042 1051 from pylons import url # doh, we need to re-import url to mock it later
1043 1052
1044 1053 def escaper(string):
1045 1054 return string.replace('<', '&lt;').replace('>', '&gt;')
1046 1055
1047 1056 def linkify_others(t, l):
1048 1057 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1049 1058 links = []
1050 1059 for e in urls.split(t):
1051 1060 if not urls.match(e):
1052 1061 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1053 1062 else:
1054 1063 links.append(e)
1055 1064
1056 1065 return ''.join(links)
1057 1066
1058 1067 # urlify changesets - extrac revisions and make link out of them
1059 1068 newtext = urlify_changesets(escaper(text_), repository)
1060 1069
1061 1070 # extract http/https links and make them real urls
1062 1071 newtext = urlify_text(newtext, safe=False)
1063 1072
1064 1073 try:
1065 1074 from rhodecode import CONFIG
1066 1075 conf = CONFIG
1067 1076
1068 1077 # allow multiple issue servers to be used
1069 1078 valid_indices = [
1070 1079 x.group(1)
1071 1080 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1072 1081 if x and 'issue_server_link%s' % x.group(1) in conf
1073 1082 and 'issue_prefix%s' % x.group(1) in conf
1074 1083 ]
1075 1084
1076 1085 log.debug('found issue server suffixes `%s` during valuation of: %s'
1077 1086 % (','.join(valid_indices), newtext))
1078 1087
1079 1088 for pattern_index in valid_indices:
1080 1089 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1081 1090 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1082 1091 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1083 1092
1084 1093 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1085 1094 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1086 1095 ISSUE_PREFIX))
1087 1096
1088 1097 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1089 1098
1090 1099 def url_func(match_obj):
1091 1100 pref = ''
1092 1101 if match_obj.group().startswith(' '):
1093 1102 pref = ' '
1094 1103
1095 1104 issue_id = ''.join(match_obj.groups())
1096 1105 tmpl = (
1097 1106 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1098 1107 '%(issue-prefix)s%(id-repr)s'
1099 1108 '</a>'
1100 1109 )
1101 1110 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1102 1111 if repository:
1103 1112 url = url.replace('{repo}', repository)
1104 1113 repo_name = repository.split(URL_SEP)[-1]
1105 1114 url = url.replace('{repo_name}', repo_name)
1106 1115
1107 1116 return tmpl % {
1108 1117 'pref': pref,
1109 1118 'cls': 'issue-tracker-link',
1110 1119 'url': url,
1111 1120 'id-repr': issue_id,
1112 1121 'issue-prefix': ISSUE_PREFIX,
1113 1122 'serv': ISSUE_SERVER_LNK,
1114 1123 }
1115 1124 newtext = URL_PAT.sub(url_func, newtext)
1116 1125 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1117 1126
1118 1127 # if we actually did something above
1119 1128 if link_:
1120 1129 # wrap not links into final link => link_
1121 1130 newtext = linkify_others(newtext, link_)
1122 1131 except:
1123 1132 log.error(traceback.format_exc())
1124 1133 pass
1125 1134
1126 1135 return literal(newtext)
1127 1136
1128 1137
1129 1138 def rst(source):
1130 1139 return literal('<div class="rst-block">%s</div>' %
1131 1140 MarkupRenderer.rst(source))
1132 1141
1133 1142
1134 1143 def rst_w_mentions(source):
1135 1144 """
1136 1145 Wrapped rst renderer with @mention highlighting
1137 1146
1138 1147 :param source:
1139 1148 """
1140 1149 return literal('<div class="rst-block">%s</div>' %
1141 1150 MarkupRenderer.rst_with_mentions(source))
1142 1151
1143 1152
1144 1153 def changeset_status(repo, revision):
1145 1154 return ChangesetStatusModel().get_status(repo, revision)
1146 1155
1147 1156
1148 1157 def changeset_status_lbl(changeset_status):
1149 1158 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1150 1159
1151 1160
1152 1161 def get_permission_name(key):
1153 1162 return dict(Permission.PERMS).get(key)
1154 1163
1155 1164
1156 1165 def journal_filter_help():
1157 1166 return _(textwrap.dedent('''
1158 1167 Example filter terms:
1159 1168 repository:vcs
1160 1169 username:marcin
1161 1170 action:*push*
1162 1171 ip:127.0.0.1
1163 1172 date:20120101
1164 1173 date:[20120101100000 TO 20120102]
1165 1174
1166 1175 Generate wildcards using '*' character:
1167 1176 "repositroy:vcs*" - search everything starting with 'vcs'
1168 1177 "repository:*vcs*" - search for repository containing 'vcs'
1169 1178
1170 1179 Optional AND / OR operators in queries
1171 1180 "repository:vcs OR repository:test"
1172 1181 "username:test AND repository:test*"
1173 1182 '''))
1174 1183
1175 1184
1176 1185 def not_mapped_error(repo_name):
1177 1186 flash(_('%s repository is not mapped to db perhaps'
1178 1187 ' it was created or renamed from the filesystem'
1179 1188 ' please run the application again'
1180 1189 ' in order to rescan repositories') % repo_name, category='error')
1181 1190
1182 1191
1183 1192 def ip_range(ip_addr):
1184 1193 from rhodecode.model.db import UserIpMap
1185 1194 s, e = UserIpMap._get_ip_range(ip_addr)
1186 1195 return '%s - %s' % (s, e)
General Comments 0
You need to be logged in to leave comments. Login now