##// END OF EJS Templates
helpers: add automatic logging of Flash messages shown to users...
Thomas De Schampheleire -
r5185:a6accd29 default
parent child Browse files
Show More
@@ -1,1455 +1,1474 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 Helper functions
16 16
17 17 Consists of functions to typically be used within templates, but also
18 18 available to Controllers. This module is available to both as 'h'.
19 19 """
20 20 import random
21 21 import hashlib
22 22 import StringIO
23 23 import math
24 24 import logging
25 25 import re
26 26 import urlparse
27 27 import textwrap
28 28
29 29 from pygments.formatters.html import HtmlFormatter
30 30 from pygments import highlight as code_highlight
31 31 from pylons import url
32 32 from pylons.i18n.translation import _, ungettext
33 33 from hashlib import md5
34 34
35 35 from webhelpers.html import literal, HTML, escape
36 36 from webhelpers.html.tools import *
37 37 from webhelpers.html.builder import make_tag
38 38 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
39 39 end_form, file, hidden, image, javascript_link, link_to, \
40 40 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
41 41 submit, text, password, textarea, title, ul, xml_declaration, radio
42 42 from webhelpers.html.tools import auto_link, button_to, highlight, \
43 43 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
44 44 from webhelpers.number import format_byte_size, format_bit_size
45 45 from webhelpers.pylonslib import Flash as _Flash
46 46 from webhelpers.pylonslib.secure_form import secure_form as form, authentication_token
47 47 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
48 48 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
49 49 replace_whitespace, urlify, truncate, wrap_paragraphs
50 50 from webhelpers.date import time_ago_in_words
51 51 from webhelpers.paginate import Page as _Page
52 52 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
53 53 convert_boolean_attrs, NotGiven, _make_safe_id_component
54 54
55 55 from kallithea.lib.annotate import annotate_highlight
56 56 from kallithea.lib.utils import repo_name_slug, get_custom_lexer
57 57 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
58 58 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict,\
59 59 safe_int
60 60 from kallithea.lib.markup_renderer import MarkupRenderer, url_re
61 61 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
62 62 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
63 63 from kallithea.config.conf import DATE_FORMAT, DATETIME_FORMAT
64 64 from kallithea.model.changeset_status import ChangesetStatusModel
65 65 from kallithea.model.db import URL_SEP, Permission
66 66
67 67 log = logging.getLogger(__name__)
68 68
69 69
70 70 def canonical_url(*args, **kargs):
71 71 '''Like url(x, qualified=True), but returns url that not only is qualified
72 72 but also canonical, as configured in canonical_url'''
73 73 from kallithea import CONFIG
74 74 try:
75 75 parts = CONFIG.get('canonical_url', '').split('://', 1)
76 76 kargs['host'] = parts[1].split('/', 1)[0]
77 77 kargs['protocol'] = parts[0]
78 78 except IndexError:
79 79 kargs['qualified'] = True
80 80 return url(*args, **kargs)
81 81
82 82 def canonical_hostname():
83 83 '''Return canonical hostname of system'''
84 84 from kallithea import CONFIG
85 85 try:
86 86 parts = CONFIG.get('canonical_url', '').split('://', 1)
87 87 return parts[1].split('/', 1)[0]
88 88 except IndexError:
89 89 parts = url('home', qualified=True).split('://', 1)
90 90 return parts[1].split('/', 1)[0]
91 91
92 92 def html_escape(text, html_escape_table=None):
93 93 """Produce entities within text."""
94 94 if not html_escape_table:
95 95 html_escape_table = {
96 96 "&": "&amp;",
97 97 '"': "&quot;",
98 98 "'": "&apos;",
99 99 ">": "&gt;",
100 100 "<": "&lt;",
101 101 }
102 102 return "".join(html_escape_table.get(c, c) for c in text)
103 103
104 104
105 105 def shorter(text, size=20):
106 106 postfix = '...'
107 107 if len(text) > size:
108 108 return text[:size - len(postfix)] + postfix
109 109 return text
110 110
111 111
112 112 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
113 113 """
114 114 Reset button
115 115 """
116 116 _set_input_attrs(attrs, type, name, value)
117 117 _set_id_attr(attrs, id, name)
118 118 convert_boolean_attrs(attrs, ["disabled"])
119 119 return HTML.input(**attrs)
120 120
121 121 reset = _reset
122 122 safeid = _make_safe_id_component
123 123
124 124
125 125 def FID(raw_id, path):
126 126 """
127 127 Creates a unique ID for filenode based on it's hash of path and revision
128 128 it's safe to use in urls
129 129
130 130 :param raw_id:
131 131 :param path:
132 132 """
133 133
134 134 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
135 135
136 136
137 137 class _GetError(object):
138 138 """Get error from form_errors, and represent it as span wrapped error
139 139 message
140 140
141 141 :param field_name: field to fetch errors for
142 142 :param form_errors: form errors dict
143 143 """
144 144
145 145 def __call__(self, field_name, form_errors):
146 146 tmpl = """<span class="error_msg">%s</span>"""
147 147 if form_errors and field_name in form_errors:
148 148 return literal(tmpl % form_errors.get(field_name))
149 149
150 150 get_error = _GetError()
151 151
152 152
153 153 class _ToolTip(object):
154 154
155 155 def __call__(self, tooltip_title, trim_at=50):
156 156 """
157 157 Special function just to wrap our text into nice formatted
158 158 autowrapped text
159 159
160 160 :param tooltip_title:
161 161 """
162 162 tooltip_title = escape(tooltip_title)
163 163 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
164 164 return tooltip_title
165 165 tooltip = _ToolTip()
166 166
167 167
168 168 class _FilesBreadCrumbs(object):
169 169
170 170 def __call__(self, repo_name, rev, paths):
171 171 if isinstance(paths, str):
172 172 paths = safe_unicode(paths)
173 173 url_l = [link_to(repo_name, url('files_home',
174 174 repo_name=repo_name,
175 175 revision=rev, f_path=''),
176 176 class_='ypjax-link')]
177 177 paths_l = paths.split('/')
178 178 for cnt, p in enumerate(paths_l):
179 179 if p != '':
180 180 url_l.append(link_to(p,
181 181 url('files_home',
182 182 repo_name=repo_name,
183 183 revision=rev,
184 184 f_path='/'.join(paths_l[:cnt + 1])
185 185 ),
186 186 class_='ypjax-link'
187 187 )
188 188 )
189 189
190 190 return literal('/'.join(url_l))
191 191
192 192 files_breadcrumbs = _FilesBreadCrumbs()
193 193
194 194
195 195 class CodeHtmlFormatter(HtmlFormatter):
196 196 """
197 197 My code Html Formatter for source codes
198 198 """
199 199
200 200 def wrap(self, source, outfile):
201 201 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
202 202
203 203 def _wrap_code(self, source):
204 204 for cnt, it in enumerate(source):
205 205 i, t = it
206 206 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
207 207 yield i, t
208 208
209 209 def _wrap_tablelinenos(self, inner):
210 210 dummyoutfile = StringIO.StringIO()
211 211 lncount = 0
212 212 for t, line in inner:
213 213 if t:
214 214 lncount += 1
215 215 dummyoutfile.write(line)
216 216
217 217 fl = self.linenostart
218 218 mw = len(str(lncount + fl - 1))
219 219 sp = self.linenospecial
220 220 st = self.linenostep
221 221 la = self.lineanchors
222 222 aln = self.anchorlinenos
223 223 nocls = self.noclasses
224 224 if sp:
225 225 lines = []
226 226
227 227 for i in range(fl, fl + lncount):
228 228 if i % st == 0:
229 229 if i % sp == 0:
230 230 if aln:
231 231 lines.append('<a href="#%s%d" class="special">%*d</a>' %
232 232 (la, i, mw, i))
233 233 else:
234 234 lines.append('<span class="special">%*d</span>' % (mw, i))
235 235 else:
236 236 if aln:
237 237 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
238 238 else:
239 239 lines.append('%*d' % (mw, i))
240 240 else:
241 241 lines.append('')
242 242 ls = '\n'.join(lines)
243 243 else:
244 244 lines = []
245 245 for i in range(fl, fl + lncount):
246 246 if i % st == 0:
247 247 if aln:
248 248 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
249 249 else:
250 250 lines.append('%*d' % (mw, i))
251 251 else:
252 252 lines.append('')
253 253 ls = '\n'.join(lines)
254 254
255 255 # in case you wonder about the seemingly redundant <div> here: since the
256 256 # content in the other cell also is wrapped in a div, some browsers in
257 257 # some configurations seem to mess up the formatting...
258 258 if nocls:
259 259 yield 0, ('<table class="%stable">' % self.cssclass +
260 260 '<tr><td><div class="linenodiv" '
261 261 'style="background-color: #f0f0f0; padding-right: 10px">'
262 262 '<pre style="line-height: 125%">' +
263 263 ls + '</pre></div></td><td id="hlcode" class="code">')
264 264 else:
265 265 yield 0, ('<table class="%stable">' % self.cssclass +
266 266 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
267 267 ls + '</pre></div></td><td id="hlcode" class="code">')
268 268 yield 0, dummyoutfile.getvalue()
269 269 yield 0, '</td></tr></table>'
270 270
271 271
272 272 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
273 273
274 274 def _markup_whitespace(m):
275 275 groups = m.groups()
276 276 if groups[0]:
277 277 return '<u>\t</u>'
278 278 if groups[1]:
279 279 return ' <i></i>'
280 280
281 281 def markup_whitespace(s):
282 282 return _whitespace_re.sub(_markup_whitespace, s)
283 283
284 284 def pygmentize(filenode, **kwargs):
285 285 """
286 286 pygmentize function using pygments
287 287
288 288 :param filenode:
289 289 """
290 290 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
291 291 return literal(markup_whitespace(
292 292 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
293 293
294 294
295 295 def pygmentize_annotation(repo_name, filenode, **kwargs):
296 296 """
297 297 pygmentize function for annotation
298 298
299 299 :param filenode:
300 300 """
301 301
302 302 color_dict = {}
303 303
304 304 def gen_color(n=10000):
305 305 """generator for getting n of evenly distributed colors using
306 306 hsv color and golden ratio. It always return same order of colors
307 307
308 308 :returns: RGB tuple
309 309 """
310 310
311 311 def hsv_to_rgb(h, s, v):
312 312 if s == 0.0:
313 313 return v, v, v
314 314 i = int(h * 6.0) # XXX assume int() truncates!
315 315 f = (h * 6.0) - i
316 316 p = v * (1.0 - s)
317 317 q = v * (1.0 - s * f)
318 318 t = v * (1.0 - s * (1.0 - f))
319 319 i = i % 6
320 320 if i == 0:
321 321 return v, t, p
322 322 if i == 1:
323 323 return q, v, p
324 324 if i == 2:
325 325 return p, v, t
326 326 if i == 3:
327 327 return p, q, v
328 328 if i == 4:
329 329 return t, p, v
330 330 if i == 5:
331 331 return v, p, q
332 332
333 333 golden_ratio = 0.618033988749895
334 334 h = 0.22717784590367374
335 335
336 336 for _unused in xrange(n):
337 337 h += golden_ratio
338 338 h %= 1
339 339 HSV_tuple = [h, 0.95, 0.95]
340 340 RGB_tuple = hsv_to_rgb(*HSV_tuple)
341 341 yield map(lambda x: str(int(x * 256)), RGB_tuple)
342 342
343 343 cgenerator = gen_color()
344 344
345 345 def get_color_string(cs):
346 346 if cs in color_dict:
347 347 col = color_dict[cs]
348 348 else:
349 349 col = color_dict[cs] = cgenerator.next()
350 350 return "color: rgb(%s)! important;" % (', '.join(col))
351 351
352 352 def url_func(repo_name):
353 353
354 354 def _url_func(changeset):
355 355 author = changeset.author
356 356 date = changeset.date
357 357 message = tooltip(changeset.message)
358 358
359 359 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
360 360 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
361 361 "</b> %s<br/></div>")
362 362
363 363 tooltip_html = tooltip_html % (author, date, message)
364 364 lnk_format = show_id(changeset)
365 365 uri = link_to(
366 366 lnk_format,
367 367 url('changeset_home', repo_name=repo_name,
368 368 revision=changeset.raw_id),
369 369 style=get_color_string(changeset.raw_id),
370 370 class_='tooltip',
371 371 title=tooltip_html
372 372 )
373 373
374 374 uri += '\n'
375 375 return uri
376 376 return _url_func
377 377
378 378 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
379 379
380 380
381 381 def is_following_repo(repo_name, user_id):
382 382 from kallithea.model.scm import ScmModel
383 383 return ScmModel().is_following_repo(repo_name, user_id)
384 384
385 385 class _Message(object):
386 386 """A message returned by ``Flash.pop_messages()``.
387 387
388 388 Converting the message to a string returns the message text. Instances
389 389 also have the following attributes:
390 390
391 391 * ``message``: the message text.
392 392 * ``category``: the category specified when the message was created.
393 393 """
394 394
395 395 def __init__(self, category, message):
396 396 self.category = category
397 397 self.message = message
398 398
399 399 def __str__(self):
400 400 return self.message
401 401
402 402 __unicode__ = __str__
403 403
404 404 def __html__(self):
405 405 return escape(safe_unicode(self.message))
406 406
407 407 class Flash(_Flash):
408 408
409 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
410 """
411 Show a message to the user _and_ log it through the specified function
412
413 category: notice (default), warning, error, success
414 logf: a custom log function - such as log.debug
415
416 logf defaults to log.info, unless category equals 'success', in which
417 case logf defaults to log.debug.
418 """
419 if logf is None:
420 logf = log.info
421 if category == 'success':
422 logf = log.debug
423
424 logf('Flash %s: %s', category, message)
425
426 super(Flash, self).__call__(message, category, ignore_duplicate)
427
409 428 def pop_messages(self):
410 429 """Return all accumulated messages and delete them from the session.
411 430
412 431 The return value is a list of ``Message`` objects.
413 432 """
414 433 from pylons import session
415 434 messages = session.pop(self.session_key, [])
416 435 session.save()
417 436 return [_Message(*m) for m in messages]
418 437
419 438 flash = Flash()
420 439
421 440 #==============================================================================
422 441 # SCM FILTERS available via h.
423 442 #==============================================================================
424 443 from kallithea.lib.vcs.utils import author_name, author_email
425 444 from kallithea.lib.utils2 import credentials_filter, age as _age
426 445 from kallithea.model.db import User, ChangesetStatus, PullRequest
427 446
428 447 age = lambda x, y=False: _age(x, y)
429 448 capitalize = lambda x: x.capitalize()
430 449 email = author_email
431 450 short_id = lambda x: x[:12]
432 451 hide_credentials = lambda x: ''.join(credentials_filter(x))
433 452
434 453
435 454 def show_id(cs):
436 455 """
437 456 Configurable function that shows ID
438 457 by default it's r123:fffeeefffeee
439 458
440 459 :param cs: changeset instance
441 460 """
442 461 from kallithea import CONFIG
443 462 def_len = safe_int(CONFIG.get('show_sha_length', 12))
444 463 show_rev = str2bool(CONFIG.get('show_revision_number', False))
445 464
446 465 raw_id = cs.raw_id[:def_len]
447 466 if show_rev:
448 467 return 'r%s:%s' % (cs.revision, raw_id)
449 468 else:
450 469 return '%s' % (raw_id)
451 470
452 471
453 472 def fmt_date(date):
454 473 if date:
455 474 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
456 475
457 476 return ""
458 477
459 478
460 479 def is_git(repository):
461 480 if hasattr(repository, 'alias'):
462 481 _type = repository.alias
463 482 elif hasattr(repository, 'repo_type'):
464 483 _type = repository.repo_type
465 484 else:
466 485 _type = repository
467 486 return _type == 'git'
468 487
469 488
470 489 def is_hg(repository):
471 490 if hasattr(repository, 'alias'):
472 491 _type = repository.alias
473 492 elif hasattr(repository, 'repo_type'):
474 493 _type = repository.repo_type
475 494 else:
476 495 _type = repository
477 496 return _type == 'hg'
478 497
479 498
480 499 def user_or_none(author):
481 500 email = author_email(author)
482 501 if email:
483 502 user = User.get_by_email(email, case_insensitive=True, cache=True)
484 503 if user is not None:
485 504 return user
486 505
487 506 user = User.get_by_username(author_name(author), case_insensitive=True, cache=True)
488 507 if user is not None:
489 508 return user
490 509
491 510 return None
492 511
493 512 def email_or_none(author):
494 513 if not author:
495 514 return None
496 515 user = user_or_none(author)
497 516 if user is not None:
498 517 return user.email # always use main email address - not necessarily the one used to find user
499 518
500 519 # extract email from the commit string
501 520 email = author_email(author)
502 521 if email:
503 522 return email
504 523
505 524 # No valid email, not a valid user in the system, none!
506 525 return None
507 526
508 527 def person(author, show_attr="username"):
509 528 """Find the user identified by 'author', return one of the users attributes,
510 529 default to the username attribute, None if there is no user"""
511 530 # attr to return from fetched user
512 531 person_getter = lambda usr: getattr(usr, show_attr)
513 532
514 533 # if author is already an instance use it for extraction
515 534 if isinstance(author, User):
516 535 return person_getter(author)
517 536
518 537 user = user_or_none(author)
519 538 if user is not None:
520 539 return person_getter(user)
521 540
522 541 # Still nothing? Just pass back the author name if any, else the email
523 542 return author_name(author) or email(author)
524 543
525 544
526 545 def person_by_id(id_, show_attr="username"):
527 546 # attr to return from fetched user
528 547 person_getter = lambda usr: getattr(usr, show_attr)
529 548
530 549 #maybe it's an ID ?
531 550 if str(id_).isdigit() or isinstance(id_, int):
532 551 id_ = int(id_)
533 552 user = User.get(id_)
534 553 if user is not None:
535 554 return person_getter(user)
536 555 return id_
537 556
538 557
539 558 def desc_stylize(value):
540 559 """
541 560 converts tags from value into html equivalent
542 561
543 562 :param value:
544 563 """
545 564 if not value:
546 565 return ''
547 566
548 567 value = re.sub(r'\[see\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
549 568 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
550 569 value = re.sub(r'\[license\ \=&gt;\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
551 570 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
552 571 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=&gt;\ *([a-zA-Z0-9\-\/]*)\]',
553 572 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
554 573 value = re.sub(r'\[(lang|language)\ \=&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
555 574 '<div class="metatag" tag="lang">\\2</div>', value)
556 575 value = re.sub(r'\[([a-z]+)\]',
557 576 '<div class="metatag" tag="\\1">\\1</div>', value)
558 577
559 578 return value
560 579
561 580
562 581 def boolicon(value):
563 582 """Returns boolean value of a value, represented as small html image of true/false
564 583 icons
565 584
566 585 :param value: value
567 586 """
568 587
569 588 if value:
570 589 return HTML.tag('i', class_="icon-ok")
571 590 else:
572 591 return HTML.tag('i', class_="icon-minus-circled")
573 592
574 593
575 594 def action_parser(user_log, feed=False, parse_cs=False):
576 595 """
577 596 This helper will action_map the specified string action into translated
578 597 fancy names with icons and links
579 598
580 599 :param user_log: user log instance
581 600 :param feed: use output for feeds (no html and fancy icons)
582 601 :param parse_cs: parse Changesets into VCS instances
583 602 """
584 603
585 604 action = user_log.action
586 605 action_params = ' '
587 606
588 607 x = action.split(':')
589 608
590 609 if len(x) > 1:
591 610 action, action_params = x
592 611
593 612 def get_cs_links():
594 613 revs_limit = 3 # display this amount always
595 614 revs_top_limit = 50 # show upto this amount of changesets hidden
596 615 revs_ids = action_params.split(',')
597 616 deleted = user_log.repository is None
598 617 if deleted:
599 618 return ','.join(revs_ids)
600 619
601 620 repo_name = user_log.repository.repo_name
602 621
603 622 def lnk(rev, repo_name):
604 623 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
605 624 lazy_cs = True
606 625 if getattr(rev, 'op', None) and getattr(rev, 'ref_name', None):
607 626 lazy_cs = False
608 627 lbl = '?'
609 628 if rev.op == 'delete_branch':
610 629 lbl = '%s' % _('Deleted branch: %s') % rev.ref_name
611 630 title = ''
612 631 elif rev.op == 'tag':
613 632 lbl = '%s' % _('Created tag: %s') % rev.ref_name
614 633 title = ''
615 634 _url = '#'
616 635
617 636 else:
618 637 lbl = '%s' % (rev.short_id[:8])
619 638 _url = url('changeset_home', repo_name=repo_name,
620 639 revision=rev.raw_id)
621 640 title = tooltip(rev.message)
622 641 else:
623 642 ## changeset cannot be found/striped/removed etc.
624 643 lbl = ('%s' % rev)[:12]
625 644 _url = '#'
626 645 title = _('Changeset not found')
627 646 if parse_cs:
628 647 return link_to(lbl, _url, title=title, class_='tooltip')
629 648 return link_to(lbl, _url, raw_id=rev.raw_id, repo_name=repo_name,
630 649 class_='lazy-cs' if lazy_cs else '')
631 650
632 651 def _get_op(rev_txt):
633 652 _op = None
634 653 _name = rev_txt
635 654 if len(rev_txt.split('=>')) == 2:
636 655 _op, _name = rev_txt.split('=>')
637 656 return _op, _name
638 657
639 658 revs = []
640 659 if len(filter(lambda v: v != '', revs_ids)) > 0:
641 660 repo = None
642 661 for rev in revs_ids[:revs_top_limit]:
643 662 _op, _name = _get_op(rev)
644 663
645 664 # we want parsed changesets, or new log store format is bad
646 665 if parse_cs:
647 666 try:
648 667 if repo is None:
649 668 repo = user_log.repository.scm_instance
650 669 _rev = repo.get_changeset(rev)
651 670 revs.append(_rev)
652 671 except ChangesetDoesNotExistError:
653 672 log.error('cannot find revision %s in this repo' % rev)
654 673 revs.append(rev)
655 674 continue
656 675 else:
657 676 _rev = AttributeDict({
658 677 'short_id': rev[:12],
659 678 'raw_id': rev,
660 679 'message': '',
661 680 'op': _op,
662 681 'ref_name': _name
663 682 })
664 683 revs.append(_rev)
665 684 cs_links = [" " + ', '.join(
666 685 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
667 686 )]
668 687 _op1, _name1 = _get_op(revs_ids[0])
669 688 _op2, _name2 = _get_op(revs_ids[-1])
670 689
671 690 _rev = '%s...%s' % (_name1, _name2)
672 691
673 692 compare_view = (
674 693 ' <div class="compare_view tooltip" title="%s">'
675 694 '<a href="%s">%s</a> </div>' % (
676 695 _('Show all combined changesets %s->%s') % (
677 696 revs_ids[0][:12], revs_ids[-1][:12]
678 697 ),
679 698 url('changeset_home', repo_name=repo_name,
680 699 revision=_rev
681 700 ),
682 701 _('Compare view')
683 702 )
684 703 )
685 704
686 705 # if we have exactly one more than normally displayed
687 706 # just display it, takes less space than displaying
688 707 # "and 1 more revisions"
689 708 if len(revs_ids) == revs_limit + 1:
690 709 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
691 710
692 711 # hidden-by-default ones
693 712 if len(revs_ids) > revs_limit + 1:
694 713 uniq_id = revs_ids[0]
695 714 html_tmpl = (
696 715 '<span> %s <a class="show_more" id="_%s" '
697 716 'href="#more">%s</a> %s</span>'
698 717 )
699 718 if not feed:
700 719 cs_links.append(html_tmpl % (
701 720 _('and'),
702 721 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
703 722 _('revisions')
704 723 )
705 724 )
706 725
707 726 if not feed:
708 727 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
709 728 else:
710 729 html_tmpl = '<span id="%s"> %s </span>'
711 730
712 731 morelinks = ', '.join(
713 732 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
714 733 )
715 734
716 735 if len(revs_ids) > revs_top_limit:
717 736 morelinks += ', ...'
718 737
719 738 cs_links.append(html_tmpl % (uniq_id, morelinks))
720 739 if len(revs) > 1:
721 740 cs_links.append(compare_view)
722 741 return ''.join(cs_links)
723 742
724 743 def get_fork_name():
725 744 repo_name = action_params
726 745 _url = url('summary_home', repo_name=repo_name)
727 746 return _('Fork name %s') % link_to(action_params, _url)
728 747
729 748 def get_user_name():
730 749 user_name = action_params
731 750 return user_name
732 751
733 752 def get_users_group():
734 753 group_name = action_params
735 754 return group_name
736 755
737 756 def get_pull_request():
738 757 pull_request_id = action_params
739 758 nice_id = PullRequest.make_nice_id(pull_request_id)
740 759
741 760 deleted = user_log.repository is None
742 761 if deleted:
743 762 repo_name = user_log.repository_name
744 763 else:
745 764 repo_name = user_log.repository.repo_name
746 765
747 766 return link_to(_('Pull request %s') % nice_id,
748 767 url('pullrequest_show', repo_name=repo_name,
749 768 pull_request_id=pull_request_id))
750 769
751 770 def get_archive_name():
752 771 archive_name = action_params
753 772 return archive_name
754 773
755 774 # action : translated str, callback(extractor), icon
756 775 action_map = {
757 776 'user_deleted_repo': (_('[deleted] repository'),
758 777 None, 'icon-trashcan'),
759 778 'user_created_repo': (_('[created] repository'),
760 779 None, 'icon-plus'),
761 780 'user_created_fork': (_('[created] repository as fork'),
762 781 None, 'icon-fork'),
763 782 'user_forked_repo': (_('[forked] repository'),
764 783 get_fork_name, 'icon-fork'),
765 784 'user_updated_repo': (_('[updated] repository'),
766 785 None, 'icon-pencil'),
767 786 'user_downloaded_archive': (_('[downloaded] archive from repository'),
768 787 get_archive_name, 'icon-download-cloud'),
769 788 'admin_deleted_repo': (_('[delete] repository'),
770 789 None, 'icon-trashcan'),
771 790 'admin_created_repo': (_('[created] repository'),
772 791 None, 'icon-plus'),
773 792 'admin_forked_repo': (_('[forked] repository'),
774 793 None, 'icon-fork'),
775 794 'admin_updated_repo': (_('[updated] repository'),
776 795 None, 'icon-pencil'),
777 796 'admin_created_user': (_('[created] user'),
778 797 get_user_name, 'icon-user'),
779 798 'admin_updated_user': (_('[updated] user'),
780 799 get_user_name, 'icon-user'),
781 800 'admin_created_users_group': (_('[created] user group'),
782 801 get_users_group, 'icon-pencil'),
783 802 'admin_updated_users_group': (_('[updated] user group'),
784 803 get_users_group, 'icon-pencil'),
785 804 'user_commented_revision': (_('[commented] on revision in repository'),
786 805 get_cs_links, 'icon-comment'),
787 806 'user_commented_pull_request': (_('[commented] on pull request for'),
788 807 get_pull_request, 'icon-comment'),
789 808 'user_closed_pull_request': (_('[closed] pull request for'),
790 809 get_pull_request, 'icon-ok'),
791 810 'push': (_('[pushed] into'),
792 811 get_cs_links, 'icon-move-up'),
793 812 'push_local': (_('[committed via Kallithea] into repository'),
794 813 get_cs_links, 'icon-pencil'),
795 814 'push_remote': (_('[pulled from remote] into repository'),
796 815 get_cs_links, 'icon-move-up'),
797 816 'pull': (_('[pulled] from'),
798 817 None, 'icon-move-down'),
799 818 'started_following_repo': (_('[started following] repository'),
800 819 None, 'icon-heart'),
801 820 'stopped_following_repo': (_('[stopped following] repository'),
802 821 None, 'icon-heart-empty'),
803 822 }
804 823
805 824 action_str = action_map.get(action, action)
806 825 if feed:
807 826 action = action_str[0].replace('[', '').replace(']', '')
808 827 else:
809 828 action = action_str[0]\
810 829 .replace('[', '<span class="journal_highlight">')\
811 830 .replace(']', '</span>')
812 831
813 832 action_params_func = lambda: ""
814 833
815 834 if callable(action_str[1]):
816 835 action_params_func = action_str[1]
817 836
818 837 def action_parser_icon():
819 838 action = user_log.action
820 839 action_params = None
821 840 x = action.split(':')
822 841
823 842 if len(x) > 1:
824 843 action, action_params = x
825 844
826 845 tmpl = """<i class="%s" alt="%s"></i>"""
827 846 ico = action_map.get(action, ['', '', ''])[2]
828 847 return literal(tmpl % (ico, action))
829 848
830 849 # returned callbacks we need to call to get
831 850 return [lambda: literal(action), action_params_func, action_parser_icon]
832 851
833 852
834 853
835 854 #==============================================================================
836 855 # PERMS
837 856 #==============================================================================
838 857 from kallithea.lib.auth import HasPermissionAny, HasPermissionAll, \
839 858 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
840 859 HasRepoGroupPermissionAny
841 860
842 861
843 862 #==============================================================================
844 863 # GRAVATAR URL
845 864 #==============================================================================
846 865 def gravatar(email_address, cls='', size=30, ssl_enabled=True):
847 866 """return html element of the gravatar
848 867
849 868 This method will return an <img> with the resolution double the size (for
850 869 retina screens) of the image. If the url returned from gravatar_url is
851 870 empty then we fallback to using an icon.
852 871
853 872 """
854 873 src = gravatar_url(email_address, size*2, ssl_enabled)
855 874
856 875 # here it makes sense to use style="width: ..." (instead of, say, a
857 876 # stylesheet) because we using this to generate a high-res (retina) size
858 877 tmpl = """<img alt="gravatar" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>"""
859 878
860 879 # if src is empty then there was no gravatar, so we use a font icon
861 880 if not src:
862 881 tmpl = """<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
863 882
864 883 tmpl = tmpl.format(cls=cls, size=size, src=src)
865 884 return literal(tmpl)
866 885
867 886 def gravatar_url(email_address, size=30, ssl_enabled=True):
868 887 # doh, we need to re-import those to mock it later
869 888 from pylons import url
870 889 from pylons import tmpl_context as c
871 890
872 891 _def = 'anonymous@kallithea-scm.org' # default gravatar
873 892 _use_gravatar = c.visual.use_gravatar
874 893 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
875 894
876 895 email_address = email_address or _def
877 896
878 897 if not _use_gravatar or not email_address or email_address == _def:
879 898 return ""
880 899
881 900 if _use_gravatar:
882 901 _md5 = lambda s: hashlib.md5(s).hexdigest()
883 902
884 903 tmpl = _gravatar_url
885 904 parsed_url = urlparse.urlparse(url.current(qualified=True))
886 905 tmpl = tmpl.replace('{email}', email_address)\
887 906 .replace('{md5email}', _md5(safe_str(email_address).lower())) \
888 907 .replace('{netloc}', parsed_url.netloc)\
889 908 .replace('{scheme}', parsed_url.scheme)\
890 909 .replace('{size}', safe_str(size))
891 910 return tmpl
892 911
893 912 class Page(_Page):
894 913 """
895 914 Custom pager to match rendering style with YUI paginator
896 915 """
897 916
898 917 def _get_pos(self, cur_page, max_page, items):
899 918 edge = (items / 2) + 1
900 919 if (cur_page <= edge):
901 920 radius = max(items / 2, items - cur_page)
902 921 elif (max_page - cur_page) < edge:
903 922 radius = (items - 1) - (max_page - cur_page)
904 923 else:
905 924 radius = items / 2
906 925
907 926 left = max(1, (cur_page - (radius)))
908 927 right = min(max_page, cur_page + (radius))
909 928 return left, cur_page, right
910 929
911 930 def _range(self, regexp_match):
912 931 """
913 932 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
914 933
915 934 Arguments:
916 935
917 936 regexp_match
918 937 A "re" (regular expressions) match object containing the
919 938 radius of linked pages around the current page in
920 939 regexp_match.group(1) as a string
921 940
922 941 This function is supposed to be called as a callable in
923 942 re.sub.
924 943
925 944 """
926 945 radius = int(regexp_match.group(1))
927 946
928 947 # Compute the first and last page number within the radius
929 948 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
930 949 # -> leftmost_page = 5
931 950 # -> rightmost_page = 9
932 951 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
933 952 self.last_page,
934 953 (radius * 2) + 1)
935 954 nav_items = []
936 955
937 956 # Create a link to the first page (unless we are on the first page
938 957 # or there would be no need to insert '..' spacers)
939 958 if self.page != self.first_page and self.first_page < leftmost_page:
940 959 nav_items.append(self._pagerlink(self.first_page, self.first_page))
941 960
942 961 # Insert dots if there are pages between the first page
943 962 # and the currently displayed page range
944 963 if leftmost_page - self.first_page > 1:
945 964 # Wrap in a SPAN tag if nolink_attr is set
946 965 text = '..'
947 966 if self.dotdot_attr:
948 967 text = HTML.span(c=text, **self.dotdot_attr)
949 968 nav_items.append(text)
950 969
951 970 for thispage in xrange(leftmost_page, rightmost_page + 1):
952 971 # Highlight the current page number and do not use a link
953 972 if thispage == self.page:
954 973 text = '%s' % (thispage,)
955 974 # Wrap in a SPAN tag if nolink_attr is set
956 975 if self.curpage_attr:
957 976 text = HTML.span(c=text, **self.curpage_attr)
958 977 nav_items.append(text)
959 978 # Otherwise create just a link to that page
960 979 else:
961 980 text = '%s' % (thispage,)
962 981 nav_items.append(self._pagerlink(thispage, text))
963 982
964 983 # Insert dots if there are pages between the displayed
965 984 # page numbers and the end of the page range
966 985 if self.last_page - rightmost_page > 1:
967 986 text = '..'
968 987 # Wrap in a SPAN tag if nolink_attr is set
969 988 if self.dotdot_attr:
970 989 text = HTML.span(c=text, **self.dotdot_attr)
971 990 nav_items.append(text)
972 991
973 992 # Create a link to the very last page (unless we are on the last
974 993 # page or there would be no need to insert '..' spacers)
975 994 if self.page != self.last_page and rightmost_page < self.last_page:
976 995 nav_items.append(self._pagerlink(self.last_page, self.last_page))
977 996
978 997 #_page_link = url.current()
979 998 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
980 999 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
981 1000 return self.separator.join(nav_items)
982 1001
983 1002 def pager(self, format='~2~', page_param='page', partial_param='partial',
984 1003 show_if_single_page=False, separator=' ', onclick=None,
985 1004 symbol_first='<<', symbol_last='>>',
986 1005 symbol_previous='<', symbol_next='>',
987 1006 link_attr={'class': 'pager_link', 'rel': 'prerender'},
988 1007 curpage_attr={'class': 'pager_curpage'},
989 1008 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
990 1009
991 1010 self.curpage_attr = curpage_attr
992 1011 self.separator = separator
993 1012 self.pager_kwargs = kwargs
994 1013 self.page_param = page_param
995 1014 self.partial_param = partial_param
996 1015 self.onclick = onclick
997 1016 self.link_attr = link_attr
998 1017 self.dotdot_attr = dotdot_attr
999 1018
1000 1019 # Don't show navigator if there is no more than one page
1001 1020 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1002 1021 return ''
1003 1022
1004 1023 from string import Template
1005 1024 # Replace ~...~ in token format by range of pages
1006 1025 result = re.sub(r'~(\d+)~', self._range, format)
1007 1026
1008 1027 # Interpolate '%' variables
1009 1028 result = Template(result).safe_substitute({
1010 1029 'first_page': self.first_page,
1011 1030 'last_page': self.last_page,
1012 1031 'page': self.page,
1013 1032 'page_count': self.page_count,
1014 1033 'items_per_page': self.items_per_page,
1015 1034 'first_item': self.first_item,
1016 1035 'last_item': self.last_item,
1017 1036 'item_count': self.item_count,
1018 1037 'link_first': self.page > self.first_page and \
1019 1038 self._pagerlink(self.first_page, symbol_first) or '',
1020 1039 'link_last': self.page < self.last_page and \
1021 1040 self._pagerlink(self.last_page, symbol_last) or '',
1022 1041 'link_previous': self.previous_page and \
1023 1042 self._pagerlink(self.previous_page, symbol_previous) \
1024 1043 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1025 1044 'link_next': self.next_page and \
1026 1045 self._pagerlink(self.next_page, symbol_next) \
1027 1046 or HTML.span(symbol_next, class_="yui-pg-next")
1028 1047 })
1029 1048
1030 1049 return literal(result)
1031 1050
1032 1051
1033 1052 #==============================================================================
1034 1053 # REPO PAGER, PAGER FOR REPOSITORY
1035 1054 #==============================================================================
1036 1055 class RepoPage(Page):
1037 1056
1038 1057 def __init__(self, collection, page=1, items_per_page=20,
1039 1058 item_count=None, url=None, **kwargs):
1040 1059
1041 1060 """Create a "RepoPage" instance. special pager for paging
1042 1061 repository
1043 1062 """
1044 1063 self._url_generator = url
1045 1064
1046 1065 # Safe the kwargs class-wide so they can be used in the pager() method
1047 1066 self.kwargs = kwargs
1048 1067
1049 1068 # Save a reference to the collection
1050 1069 self.original_collection = collection
1051 1070
1052 1071 self.collection = collection
1053 1072
1054 1073 # The self.page is the number of the current page.
1055 1074 # The first page has the number 1!
1056 1075 try:
1057 1076 self.page = int(page) # make it int() if we get it as a string
1058 1077 except (ValueError, TypeError):
1059 1078 self.page = 1
1060 1079
1061 1080 self.items_per_page = items_per_page
1062 1081
1063 1082 # Unless the user tells us how many items the collections has
1064 1083 # we calculate that ourselves.
1065 1084 if item_count is not None:
1066 1085 self.item_count = item_count
1067 1086 else:
1068 1087 self.item_count = len(self.collection)
1069 1088
1070 1089 # Compute the number of the first and last available page
1071 1090 if self.item_count > 0:
1072 1091 self.first_page = 1
1073 1092 self.page_count = int(math.ceil(float(self.item_count) /
1074 1093 self.items_per_page))
1075 1094 self.last_page = self.first_page + self.page_count - 1
1076 1095
1077 1096 # Make sure that the requested page number is the range of
1078 1097 # valid pages
1079 1098 if self.page > self.last_page:
1080 1099 self.page = self.last_page
1081 1100 elif self.page < self.first_page:
1082 1101 self.page = self.first_page
1083 1102
1084 1103 # Note: the number of items on this page can be less than
1085 1104 # items_per_page if the last page is not full
1086 1105 self.first_item = max(0, (self.item_count) - (self.page *
1087 1106 items_per_page))
1088 1107 self.last_item = ((self.item_count - 1) - items_per_page *
1089 1108 (self.page - 1))
1090 1109
1091 1110 self.items = list(self.collection[self.first_item:self.last_item + 1])
1092 1111
1093 1112 # Links to previous and next page
1094 1113 if self.page > self.first_page:
1095 1114 self.previous_page = self.page - 1
1096 1115 else:
1097 1116 self.previous_page = None
1098 1117
1099 1118 if self.page < self.last_page:
1100 1119 self.next_page = self.page + 1
1101 1120 else:
1102 1121 self.next_page = None
1103 1122
1104 1123 # No items available
1105 1124 else:
1106 1125 self.first_page = None
1107 1126 self.page_count = 0
1108 1127 self.last_page = None
1109 1128 self.first_item = None
1110 1129 self.last_item = None
1111 1130 self.previous_page = None
1112 1131 self.next_page = None
1113 1132 self.items = []
1114 1133
1115 1134 # This is a subclass of the 'list' type. Initialise the list now.
1116 1135 list.__init__(self, reversed(self.items))
1117 1136
1118 1137
1119 1138 def changed_tooltip(nodes):
1120 1139 """
1121 1140 Generates a html string for changed nodes in changeset page.
1122 1141 It limits the output to 30 entries
1123 1142
1124 1143 :param nodes: LazyNodesGenerator
1125 1144 """
1126 1145 if nodes:
1127 1146 pref = ': <br/> '
1128 1147 suf = ''
1129 1148 if len(nodes) > 30:
1130 1149 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1131 1150 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1132 1151 for x in nodes[:30]]) + suf)
1133 1152 else:
1134 1153 return ': ' + _('No Files')
1135 1154
1136 1155
1137 1156 def repo_link(groups_and_repos):
1138 1157 """
1139 1158 Makes a breadcrumbs link to repo within a group
1140 1159 joins &raquo; on each group to create a fancy link
1141 1160
1142 1161 ex::
1143 1162 group >> subgroup >> repo
1144 1163
1145 1164 :param groups_and_repos:
1146 1165 :param last_url:
1147 1166 """
1148 1167 groups, just_name, repo_name = groups_and_repos
1149 1168 last_url = url('summary_home', repo_name=repo_name)
1150 1169 last_link = link_to(just_name, last_url)
1151 1170
1152 1171 def make_link(group):
1153 1172 return link_to(group.name,
1154 1173 url('repos_group_home', group_name=group.group_name))
1155 1174 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1156 1175
1157 1176
1158 1177 def fancy_file_stats(stats):
1159 1178 """
1160 1179 Displays a fancy two colored bar for number of added/deleted
1161 1180 lines of code on file
1162 1181
1163 1182 :param stats: two element list of added/deleted lines of code
1164 1183 """
1165 1184 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1166 1185 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1167 1186
1168 1187 def cgen(l_type, a_v, d_v):
1169 1188 mapping = {'tr': 'top-right-rounded-corner-mid',
1170 1189 'tl': 'top-left-rounded-corner-mid',
1171 1190 'br': 'bottom-right-rounded-corner-mid',
1172 1191 'bl': 'bottom-left-rounded-corner-mid'}
1173 1192 map_getter = lambda x: mapping[x]
1174 1193
1175 1194 if l_type == 'a' and d_v:
1176 1195 #case when added and deleted are present
1177 1196 return ' '.join(map(map_getter, ['tl', 'bl']))
1178 1197
1179 1198 if l_type == 'a' and not d_v:
1180 1199 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1181 1200
1182 1201 if l_type == 'd' and a_v:
1183 1202 return ' '.join(map(map_getter, ['tr', 'br']))
1184 1203
1185 1204 if l_type == 'd' and not a_v:
1186 1205 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1187 1206
1188 1207 a, d = stats['added'], stats['deleted']
1189 1208 width = 100
1190 1209
1191 1210 if stats['binary']:
1192 1211 #binary mode
1193 1212 lbl = ''
1194 1213 bin_op = 1
1195 1214
1196 1215 if BIN_FILENODE in stats['ops']:
1197 1216 lbl = 'bin+'
1198 1217
1199 1218 if NEW_FILENODE in stats['ops']:
1200 1219 lbl += _('new file')
1201 1220 bin_op = NEW_FILENODE
1202 1221 elif MOD_FILENODE in stats['ops']:
1203 1222 lbl += _('mod')
1204 1223 bin_op = MOD_FILENODE
1205 1224 elif DEL_FILENODE in stats['ops']:
1206 1225 lbl += _('del')
1207 1226 bin_op = DEL_FILENODE
1208 1227 elif RENAMED_FILENODE in stats['ops']:
1209 1228 lbl += _('rename')
1210 1229 bin_op = RENAMED_FILENODE
1211 1230
1212 1231 #chmod can go with other operations
1213 1232 if CHMOD_FILENODE in stats['ops']:
1214 1233 _org_lbl = _('chmod')
1215 1234 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1216 1235
1217 1236 #import ipdb;ipdb.set_trace()
1218 1237 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1219 1238 b_a = '<div class="bin bin1" style="width:0%%"></div>'
1220 1239 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1221 1240
1222 1241 t = stats['added'] + stats['deleted']
1223 1242 unit = float(width) / (t or 1)
1224 1243
1225 1244 # needs > 9% of width to be visible or 0 to be hidden
1226 1245 a_p = max(9, unit * a) if a > 0 else 0
1227 1246 d_p = max(9, unit * d) if d > 0 else 0
1228 1247 p_sum = a_p + d_p
1229 1248
1230 1249 if p_sum > width:
1231 1250 #adjust the percentage to be == 100% since we adjusted to 9
1232 1251 if a_p > d_p:
1233 1252 a_p = a_p - (p_sum - width)
1234 1253 else:
1235 1254 d_p = d_p - (p_sum - width)
1236 1255
1237 1256 a_v = a if a > 0 else ''
1238 1257 d_v = d if d > 0 else ''
1239 1258
1240 1259 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1241 1260 cgen('a', a_v, d_v), a_p, a_v
1242 1261 )
1243 1262 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1244 1263 cgen('d', a_v, d_v), d_p, d_v
1245 1264 )
1246 1265 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1247 1266
1248 1267
1249 1268 def urlify_text(text_, safe=True):
1250 1269 """
1251 1270 Extract urls from text and make html links out of them
1252 1271
1253 1272 :param text_:
1254 1273 """
1255 1274
1256 1275 def url_func(match_obj):
1257 1276 url_full = match_obj.groups()[0]
1258 1277 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1259 1278 _newtext = url_re.sub(url_func, text_)
1260 1279 if safe:
1261 1280 return literal(_newtext)
1262 1281 return _newtext
1263 1282
1264 1283
1265 1284 def urlify_changesets(text_, repository):
1266 1285 """
1267 1286 Extract revision ids from changeset and make link from them
1268 1287
1269 1288 :param text_:
1270 1289 :param repository: repo name to build the URL with
1271 1290 """
1272 1291 from pylons import url # doh, we need to re-import url to mock it later
1273 1292
1274 1293 def url_func(match_obj):
1275 1294 rev = match_obj.group(0)
1276 1295 return '<a class="revision-link" href="%(url)s">%(rev)s</a>' % {
1277 1296 'url': url('changeset_home', repo_name=repository, revision=rev),
1278 1297 'rev': rev,
1279 1298 }
1280 1299
1281 1300 return re.sub(r'(?:^|(?<=[\s(),]))([0-9a-fA-F]{12,40})(?=$|\s|[.,:()])', url_func, text_)
1282 1301
1283 1302 def linkify_others(t, l):
1284 1303 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1285 1304 links = []
1286 1305 for e in urls.split(t):
1287 1306 if not urls.match(e):
1288 1307 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1289 1308 else:
1290 1309 links.append(e)
1291 1310
1292 1311 return ''.join(links)
1293 1312
1294 1313 def urlify_commit(text_, repository, link_=None):
1295 1314 """
1296 1315 Parses given text message and makes proper links.
1297 1316 issues are linked to given issue-server, and rest is a changeset link
1298 1317 if link_ is given, in other case it's a plain text
1299 1318
1300 1319 :param text_:
1301 1320 :param repository:
1302 1321 :param link_: changeset link
1303 1322 """
1304 1323 def escaper(string):
1305 1324 return string.replace('<', '&lt;').replace('>', '&gt;')
1306 1325
1307 1326 # urlify changesets - extract revisions and make link out of them
1308 1327 newtext = urlify_changesets(escaper(text_), repository)
1309 1328
1310 1329 # extract http/https links and make them real urls
1311 1330 newtext = urlify_text(newtext, safe=False)
1312 1331
1313 1332 newtext = urlify_issues(newtext, repository, link_)
1314 1333
1315 1334 return literal(newtext)
1316 1335
1317 1336 def urlify_issues(newtext, repository, link_=None):
1318 1337 from kallithea import CONFIG as conf
1319 1338
1320 1339 # allow multiple issue servers to be used
1321 1340 valid_indices = [
1322 1341 x.group(1)
1323 1342 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1324 1343 if x and 'issue_server_link%s' % x.group(1) in conf
1325 1344 and 'issue_prefix%s' % x.group(1) in conf
1326 1345 ]
1327 1346
1328 1347 if valid_indices:
1329 1348 log.debug('found issue server suffixes `%s` during valuation of: %s'
1330 1349 % (','.join(valid_indices), newtext))
1331 1350
1332 1351 for pattern_index in valid_indices:
1333 1352 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1334 1353 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1335 1354 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1336 1355
1337 1356 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1338 1357 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1339 1358 ISSUE_PREFIX))
1340 1359
1341 1360 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1342 1361
1343 1362 def url_func(match_obj):
1344 1363 pref = ''
1345 1364 if match_obj.group().startswith(' '):
1346 1365 pref = ' '
1347 1366
1348 1367 issue_id = ''.join(match_obj.groups())
1349 1368 issue_url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1350 1369 if repository:
1351 1370 issue_url = issue_url.replace('{repo}', repository)
1352 1371 repo_name = repository.split(URL_SEP)[-1]
1353 1372 issue_url = issue_url.replace('{repo_name}', repo_name)
1354 1373
1355 1374 return (
1356 1375 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1357 1376 '%(issue-prefix)s%(id-repr)s'
1358 1377 '</a>'
1359 1378 ) % {
1360 1379 'pref': pref,
1361 1380 'cls': 'issue-tracker-link',
1362 1381 'url': issue_url,
1363 1382 'id-repr': issue_id,
1364 1383 'issue-prefix': ISSUE_PREFIX,
1365 1384 'serv': ISSUE_SERVER_LNK,
1366 1385 }
1367 1386 newtext = URL_PAT.sub(url_func, newtext)
1368 1387 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1369 1388
1370 1389 # if we actually did something above
1371 1390 if link_:
1372 1391 # wrap not links into final link => link_
1373 1392 newtext = linkify_others(newtext, link_)
1374 1393 return newtext
1375 1394
1376 1395
1377 1396 def rst(source):
1378 1397 return literal('<div class="rst-block">%s</div>' %
1379 1398 MarkupRenderer.rst(source))
1380 1399
1381 1400
1382 1401 def rst_w_mentions(source):
1383 1402 """
1384 1403 Wrapped rst renderer with @mention highlighting
1385 1404
1386 1405 :param source:
1387 1406 """
1388 1407 return literal('<div class="rst-block">%s</div>' %
1389 1408 MarkupRenderer.rst_with_mentions(source))
1390 1409
1391 1410 def short_ref(ref_type, ref_name):
1392 1411 if ref_type == 'rev':
1393 1412 return short_id(ref_name)
1394 1413 return ref_name
1395 1414
1396 1415 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1397 1416 """
1398 1417 Return full markup for a href to changeset_home for a changeset.
1399 1418 If ref_type is branch it will link to changelog.
1400 1419 ref_name is shortened if ref_type is 'rev'.
1401 1420 if rev is specified show it too, explicitly linking to that revision.
1402 1421 """
1403 1422 txt = short_ref(ref_type, ref_name)
1404 1423 if ref_type == 'branch':
1405 1424 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1406 1425 else:
1407 1426 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1408 1427 l = link_to(repo_name + '#' + txt, u)
1409 1428 if rev and ref_type != 'rev':
1410 1429 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1411 1430 return l
1412 1431
1413 1432 def changeset_status(repo, revision):
1414 1433 return ChangesetStatusModel().get_status(repo, revision)
1415 1434
1416 1435
1417 1436 def changeset_status_lbl(changeset_status):
1418 1437 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1419 1438
1420 1439
1421 1440 def get_permission_name(key):
1422 1441 return dict(Permission.PERMS).get(key)
1423 1442
1424 1443
1425 1444 def journal_filter_help():
1426 1445 return _(textwrap.dedent('''
1427 1446 Example filter terms:
1428 1447 repository:vcs
1429 1448 username:developer
1430 1449 action:*push*
1431 1450 ip:127.0.0.1
1432 1451 date:20120101
1433 1452 date:[20120101100000 TO 20120102]
1434 1453
1435 1454 Generate wildcards using '*' character:
1436 1455 "repository:vcs*" - search everything starting with 'vcs'
1437 1456 "repository:*vcs*" - search for repository containing 'vcs'
1438 1457
1439 1458 Optional AND / OR operators in queries
1440 1459 "repository:vcs OR repository:test"
1441 1460 "username:test AND repository:test*"
1442 1461 '''))
1443 1462
1444 1463
1445 1464 def not_mapped_error(repo_name):
1446 1465 flash(_('%s repository is not mapped to db perhaps'
1447 1466 ' it was created or renamed from the filesystem'
1448 1467 ' please run the application again'
1449 1468 ' in order to rescan repositories') % repo_name, category='error')
1450 1469
1451 1470
1452 1471 def ip_range(ip_addr):
1453 1472 from kallithea.model.db import UserIpMap
1454 1473 s, e = UserIpMap._get_ip_range(ip_addr)
1455 1474 return '%s - %s' % (s, e)
General Comments 0
You need to be logged in to leave comments. Login now