##// END OF EJS Templates
helpers: Fix displayed author on changelog_summary_data when no email is set in commit author (Issue #137)
Marc Villetard -
r5170:3e1afbd6 stable
parent child Browse files
Show More
@@ -1,1452 +1,1452 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 409 def pop_messages(self):
410 410 """Return all accumulated messages and delete them from the session.
411 411
412 412 The return value is a list of ``Message`` objects.
413 413 """
414 414 from pylons import session
415 415 messages = session.pop(self.session_key, [])
416 416 session.save()
417 417 return [_Message(*m) for m in messages]
418 418
419 419 flash = Flash()
420 420
421 421 #==============================================================================
422 422 # SCM FILTERS available via h.
423 423 #==============================================================================
424 424 from kallithea.lib.vcs.utils import author_name, author_email
425 425 from kallithea.lib.utils2 import credentials_filter, age as _age
426 426 from kallithea.model.db import User, ChangesetStatus
427 427
428 428 age = lambda x, y=False: _age(x, y)
429 429 capitalize = lambda x: x.capitalize()
430 430 email = author_email
431 431 short_id = lambda x: x[:12]
432 432 hide_credentials = lambda x: ''.join(credentials_filter(x))
433 433
434 434
435 435 def show_id(cs):
436 436 """
437 437 Configurable function that shows ID
438 438 by default it's r123:fffeeefffeee
439 439
440 440 :param cs: changeset instance
441 441 """
442 442 from kallithea import CONFIG
443 443 def_len = safe_int(CONFIG.get('show_sha_length', 12))
444 444 show_rev = str2bool(CONFIG.get('show_revision_number', False))
445 445
446 446 raw_id = cs.raw_id[:def_len]
447 447 if show_rev:
448 448 return 'r%s:%s' % (cs.revision, raw_id)
449 449 else:
450 450 return '%s' % (raw_id)
451 451
452 452
453 453 def fmt_date(date):
454 454 if date:
455 455 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
456 456
457 457 return ""
458 458
459 459
460 460 def is_git(repository):
461 461 if hasattr(repository, 'alias'):
462 462 _type = repository.alias
463 463 elif hasattr(repository, 'repo_type'):
464 464 _type = repository.repo_type
465 465 else:
466 466 _type = repository
467 467 return _type == 'git'
468 468
469 469
470 470 def is_hg(repository):
471 471 if hasattr(repository, 'alias'):
472 472 _type = repository.alias
473 473 elif hasattr(repository, 'repo_type'):
474 474 _type = repository.repo_type
475 475 else:
476 476 _type = repository
477 477 return _type == 'hg'
478 478
479 479
480 480 def user_or_none(author):
481 481 email = author_email(author)
482 if email is not None:
482 if email:
483 483 user = User.get_by_email(email, case_insensitive=True, cache=True)
484 484 if user is not None:
485 485 return user
486 486
487 487 user = User.get_by_username(author_name(author), case_insensitive=True, cache=True)
488 488 if user is not None:
489 489 return user
490 490
491 491 return None
492 492
493 493 def email_or_none(author):
494 494 if not author:
495 495 return None
496 496 user = user_or_none(author)
497 497 if user is not None:
498 498 return user.email # always use main email address - not necessarily the one used to find user
499 499
500 500 # extract email from the commit string
501 501 email = author_email(author)
502 502 if email:
503 503 return email
504 504
505 505 # No valid email, not a valid user in the system, none!
506 506 return None
507 507
508 508 def person(author, show_attr="username"):
509 509 """Find the user identified by 'author', return one of the users attributes,
510 510 default to the username attribute, None if there is no user"""
511 511 # attr to return from fetched user
512 512 person_getter = lambda usr: getattr(usr, show_attr)
513 513
514 514 # if author is already an instance use it for extraction
515 515 if isinstance(author, User):
516 516 return person_getter(author)
517 517
518 518 user = user_or_none(author)
519 519 if user is not None:
520 520 return person_getter(user)
521 521
522 522 # Still nothing? Just pass back the author name if any, else the email
523 523 return author_name(author) or email(author)
524 524
525 525
526 526 def person_by_id(id_, show_attr="username"):
527 527 # attr to return from fetched user
528 528 person_getter = lambda usr: getattr(usr, show_attr)
529 529
530 530 #maybe it's an ID ?
531 531 if str(id_).isdigit() or isinstance(id_, int):
532 532 id_ = int(id_)
533 533 user = User.get(id_)
534 534 if user is not None:
535 535 return person_getter(user)
536 536 return id_
537 537
538 538
539 539 def desc_stylize(value):
540 540 """
541 541 converts tags from value into html equivalent
542 542
543 543 :param value:
544 544 """
545 545 if not value:
546 546 return ''
547 547
548 548 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
549 549 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
550 550 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
551 551 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
552 552 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
553 553 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
554 554 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
555 555 '<div class="metatag" tag="lang">\\2</div>', value)
556 556 value = re.sub(r'\[([a-z]+)\]',
557 557 '<div class="metatag" tag="\\1">\\1</div>', value)
558 558
559 559 return value
560 560
561 561
562 562 def boolicon(value):
563 563 """Returns boolean value of a value, represented as small html image of true/false
564 564 icons
565 565
566 566 :param value: value
567 567 """
568 568
569 569 if value:
570 570 return HTML.tag('i', class_="icon-ok")
571 571 else:
572 572 return HTML.tag('i', class_="icon-minus-circled")
573 573
574 574
575 575 def action_parser(user_log, feed=False, parse_cs=False):
576 576 """
577 577 This helper will action_map the specified string action into translated
578 578 fancy names with icons and links
579 579
580 580 :param user_log: user log instance
581 581 :param feed: use output for feeds (no html and fancy icons)
582 582 :param parse_cs: parse Changesets into VCS instances
583 583 """
584 584
585 585 action = user_log.action
586 586 action_params = ' '
587 587
588 588 x = action.split(':')
589 589
590 590 if len(x) > 1:
591 591 action, action_params = x
592 592
593 593 def get_cs_links():
594 594 revs_limit = 3 # display this amount always
595 595 revs_top_limit = 50 # show upto this amount of changesets hidden
596 596 revs_ids = action_params.split(',')
597 597 deleted = user_log.repository is None
598 598 if deleted:
599 599 return ','.join(revs_ids)
600 600
601 601 repo_name = user_log.repository.repo_name
602 602
603 603 def lnk(rev, repo_name):
604 604 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
605 605 lazy_cs = True
606 606 if getattr(rev, 'op', None) and getattr(rev, 'ref_name', None):
607 607 lazy_cs = False
608 608 lbl = '?'
609 609 if rev.op == 'delete_branch':
610 610 lbl = '%s' % _('Deleted branch: %s') % rev.ref_name
611 611 title = ''
612 612 elif rev.op == 'tag':
613 613 lbl = '%s' % _('Created tag: %s') % rev.ref_name
614 614 title = ''
615 615 _url = '#'
616 616
617 617 else:
618 618 lbl = '%s' % (rev.short_id[:8])
619 619 _url = url('changeset_home', repo_name=repo_name,
620 620 revision=rev.raw_id)
621 621 title = tooltip(rev.message)
622 622 else:
623 623 ## changeset cannot be found/striped/removed etc.
624 624 lbl = ('%s' % rev)[:12]
625 625 _url = '#'
626 626 title = _('Changeset not found')
627 627 if parse_cs:
628 628 return link_to(lbl, _url, title=title, class_='tooltip')
629 629 return link_to(lbl, _url, raw_id=rev.raw_id, repo_name=repo_name,
630 630 class_='lazy-cs' if lazy_cs else '')
631 631
632 632 def _get_op(rev_txt):
633 633 _op = None
634 634 _name = rev_txt
635 635 if len(rev_txt.split('=>')) == 2:
636 636 _op, _name = rev_txt.split('=>')
637 637 return _op, _name
638 638
639 639 revs = []
640 640 if len(filter(lambda v: v != '', revs_ids)) > 0:
641 641 repo = None
642 642 for rev in revs_ids[:revs_top_limit]:
643 643 _op, _name = _get_op(rev)
644 644
645 645 # we want parsed changesets, or new log store format is bad
646 646 if parse_cs:
647 647 try:
648 648 if repo is None:
649 649 repo = user_log.repository.scm_instance
650 650 _rev = repo.get_changeset(rev)
651 651 revs.append(_rev)
652 652 except ChangesetDoesNotExistError:
653 653 log.error('cannot find revision %s in this repo' % rev)
654 654 revs.append(rev)
655 655 continue
656 656 else:
657 657 _rev = AttributeDict({
658 658 'short_id': rev[:12],
659 659 'raw_id': rev,
660 660 'message': '',
661 661 'op': _op,
662 662 'ref_name': _name
663 663 })
664 664 revs.append(_rev)
665 665 cs_links = [" " + ', '.join(
666 666 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
667 667 )]
668 668 _op1, _name1 = _get_op(revs_ids[0])
669 669 _op2, _name2 = _get_op(revs_ids[-1])
670 670
671 671 _rev = '%s...%s' % (_name1, _name2)
672 672
673 673 compare_view = (
674 674 ' <div class="compare_view tooltip" title="%s">'
675 675 '<a href="%s">%s</a> </div>' % (
676 676 _('Show all combined changesets %s->%s') % (
677 677 revs_ids[0][:12], revs_ids[-1][:12]
678 678 ),
679 679 url('changeset_home', repo_name=repo_name,
680 680 revision=_rev
681 681 ),
682 682 _('compare view')
683 683 )
684 684 )
685 685
686 686 # if we have exactly one more than normally displayed
687 687 # just display it, takes less space than displaying
688 688 # "and 1 more revisions"
689 689 if len(revs_ids) == revs_limit + 1:
690 690 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
691 691
692 692 # hidden-by-default ones
693 693 if len(revs_ids) > revs_limit + 1:
694 694 uniq_id = revs_ids[0]
695 695 html_tmpl = (
696 696 '<span> %s <a class="show_more" id="_%s" '
697 697 'href="#more">%s</a> %s</span>'
698 698 )
699 699 if not feed:
700 700 cs_links.append(html_tmpl % (
701 701 _('and'),
702 702 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
703 703 _('revisions')
704 704 )
705 705 )
706 706
707 707 if not feed:
708 708 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
709 709 else:
710 710 html_tmpl = '<span id="%s"> %s </span>'
711 711
712 712 morelinks = ', '.join(
713 713 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
714 714 )
715 715
716 716 if len(revs_ids) > revs_top_limit:
717 717 morelinks += ', ...'
718 718
719 719 cs_links.append(html_tmpl % (uniq_id, morelinks))
720 720 if len(revs) > 1:
721 721 cs_links.append(compare_view)
722 722 return ''.join(cs_links)
723 723
724 724 def get_fork_name():
725 725 repo_name = action_params
726 726 _url = url('summary_home', repo_name=repo_name)
727 727 return _('fork name %s') % link_to(action_params, _url)
728 728
729 729 def get_user_name():
730 730 user_name = action_params
731 731 return user_name
732 732
733 733 def get_users_group():
734 734 group_name = action_params
735 735 return group_name
736 736
737 737 def get_pull_request():
738 738 pull_request_id = action_params
739 739 deleted = user_log.repository is None
740 740 if deleted:
741 741 repo_name = user_log.repository_name
742 742 else:
743 743 repo_name = user_log.repository.repo_name
744 744 return link_to(_('Pull request #%s') % pull_request_id,
745 745 url('pullrequest_show', repo_name=repo_name,
746 746 pull_request_id=pull_request_id))
747 747
748 748 def get_archive_name():
749 749 archive_name = action_params
750 750 return archive_name
751 751
752 752 # action : translated str, callback(extractor), icon
753 753 action_map = {
754 754 'user_deleted_repo': (_('[deleted] repository'),
755 755 None, 'icon-trashcan'),
756 756 'user_created_repo': (_('[created] repository'),
757 757 None, 'icon-plus'),
758 758 'user_created_fork': (_('[created] repository as fork'),
759 759 None, 'icon-fork'),
760 760 'user_forked_repo': (_('[forked] repository'),
761 761 get_fork_name, 'icon-fork'),
762 762 'user_updated_repo': (_('[updated] repository'),
763 763 None, 'icon-pencil'),
764 764 'user_downloaded_archive': (_('[downloaded] archive from repository'),
765 765 get_archive_name, 'icon-download-cloud'),
766 766 'admin_deleted_repo': (_('[delete] repository'),
767 767 None, 'icon-trashcan'),
768 768 'admin_created_repo': (_('[created] repository'),
769 769 None, 'icon-plus'),
770 770 'admin_forked_repo': (_('[forked] repository'),
771 771 None, 'icon-fork'),
772 772 'admin_updated_repo': (_('[updated] repository'),
773 773 None, 'icon-pencil'),
774 774 'admin_created_user': (_('[created] user'),
775 775 get_user_name, 'icon-user'),
776 776 'admin_updated_user': (_('[updated] user'),
777 777 get_user_name, 'icon-user'),
778 778 'admin_created_users_group': (_('[created] user group'),
779 779 get_users_group, 'icon-pencil'),
780 780 'admin_updated_users_group': (_('[updated] user group'),
781 781 get_users_group, 'icon-pencil'),
782 782 'user_commented_revision': (_('[commented] on revision in repository'),
783 783 get_cs_links, 'icon-comment'),
784 784 'user_commented_pull_request': (_('[commented] on pull request for'),
785 785 get_pull_request, 'icon-comment'),
786 786 'user_closed_pull_request': (_('[closed] pull request for'),
787 787 get_pull_request, 'icon-ok'),
788 788 'push': (_('[pushed] into'),
789 789 get_cs_links, 'icon-move-up'),
790 790 'push_local': (_('[committed via Kallithea] into repository'),
791 791 get_cs_links, 'icon-pencil'),
792 792 'push_remote': (_('[pulled from remote] into repository'),
793 793 get_cs_links, 'icon-move-up'),
794 794 'pull': (_('[pulled] from'),
795 795 None, 'icon-move-down'),
796 796 'started_following_repo': (_('[started following] repository'),
797 797 None, 'icon-heart'),
798 798 'stopped_following_repo': (_('[stopped following] repository'),
799 799 None, 'icon-heart-empty'),
800 800 }
801 801
802 802 action_str = action_map.get(action, action)
803 803 if feed:
804 804 action = action_str[0].replace('[', '').replace(']', '')
805 805 else:
806 806 action = action_str[0]\
807 807 .replace('[', '<span class="journal_highlight">')\
808 808 .replace(']', '</span>')
809 809
810 810 action_params_func = lambda: ""
811 811
812 812 if callable(action_str[1]):
813 813 action_params_func = action_str[1]
814 814
815 815 def action_parser_icon():
816 816 action = user_log.action
817 817 action_params = None
818 818 x = action.split(':')
819 819
820 820 if len(x) > 1:
821 821 action, action_params = x
822 822
823 823 tmpl = """<i class="%s" alt="%s"></i>"""
824 824 ico = action_map.get(action, ['', '', ''])[2]
825 825 return literal(tmpl % (ico, action))
826 826
827 827 # returned callbacks we need to call to get
828 828 return [lambda: literal(action), action_params_func, action_parser_icon]
829 829
830 830
831 831
832 832 #==============================================================================
833 833 # PERMS
834 834 #==============================================================================
835 835 from kallithea.lib.auth import HasPermissionAny, HasPermissionAll, \
836 836 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
837 837 HasRepoGroupPermissionAny
838 838
839 839
840 840 #==============================================================================
841 841 # GRAVATAR URL
842 842 #==============================================================================
843 843 def gravatar(email_address, cls='', size=30, ssl_enabled=True):
844 844 """return html element of the gravatar
845 845
846 846 This method will return an <img> with the resolution double the size (for
847 847 retina screens) of the image. If the url returned from gravatar_url is
848 848 empty then we fallback to using an icon.
849 849
850 850 """
851 851 src = gravatar_url(email_address, size*2, ssl_enabled)
852 852
853 853 # here it makes sense to use style="width: ..." (instead of, say, a
854 854 # stylesheet) because we using this to generate a high-res (retina) size
855 855 tmpl = """<img alt="gravatar" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>"""
856 856
857 857 # if src is empty then there was no gravatar, so we use a font icon
858 858 if not src:
859 859 tmpl = """<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
860 860
861 861 tmpl = tmpl.format(cls=cls, size=size, src=src)
862 862 return literal(tmpl)
863 863
864 864 def gravatar_url(email_address, size=30, ssl_enabled=True):
865 865 # doh, we need to re-import those to mock it later
866 866 from pylons import url
867 867 from pylons import tmpl_context as c
868 868
869 869 _def = 'anonymous@kallithea-scm.org' # default gravatar
870 870 _use_gravatar = c.visual.use_gravatar
871 871 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
872 872
873 873 email_address = email_address or _def
874 874
875 875 if not _use_gravatar or not email_address or email_address == _def:
876 876 return ""
877 877
878 878 if _use_gravatar:
879 879 _md5 = lambda s: hashlib.md5(s).hexdigest()
880 880
881 881 tmpl = _gravatar_url
882 882 parsed_url = urlparse.urlparse(url.current(qualified=True))
883 883 tmpl = tmpl.replace('{email}', email_address)\
884 884 .replace('{md5email}', _md5(safe_str(email_address).lower())) \
885 885 .replace('{netloc}', parsed_url.netloc)\
886 886 .replace('{scheme}', parsed_url.scheme)\
887 887 .replace('{size}', safe_str(size))
888 888 return tmpl
889 889
890 890 class Page(_Page):
891 891 """
892 892 Custom pager to match rendering style with YUI paginator
893 893 """
894 894
895 895 def _get_pos(self, cur_page, max_page, items):
896 896 edge = (items / 2) + 1
897 897 if (cur_page <= edge):
898 898 radius = max(items / 2, items - cur_page)
899 899 elif (max_page - cur_page) < edge:
900 900 radius = (items - 1) - (max_page - cur_page)
901 901 else:
902 902 radius = items / 2
903 903
904 904 left = max(1, (cur_page - (radius)))
905 905 right = min(max_page, cur_page + (radius))
906 906 return left, cur_page, right
907 907
908 908 def _range(self, regexp_match):
909 909 """
910 910 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
911 911
912 912 Arguments:
913 913
914 914 regexp_match
915 915 A "re" (regular expressions) match object containing the
916 916 radius of linked pages around the current page in
917 917 regexp_match.group(1) as a string
918 918
919 919 This function is supposed to be called as a callable in
920 920 re.sub.
921 921
922 922 """
923 923 radius = int(regexp_match.group(1))
924 924
925 925 # Compute the first and last page number within the radius
926 926 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
927 927 # -> leftmost_page = 5
928 928 # -> rightmost_page = 9
929 929 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
930 930 self.last_page,
931 931 (radius * 2) + 1)
932 932 nav_items = []
933 933
934 934 # Create a link to the first page (unless we are on the first page
935 935 # or there would be no need to insert '..' spacers)
936 936 if self.page != self.first_page and self.first_page < leftmost_page:
937 937 nav_items.append(self._pagerlink(self.first_page, self.first_page))
938 938
939 939 # Insert dots if there are pages between the first page
940 940 # and the currently displayed page range
941 941 if leftmost_page - self.first_page > 1:
942 942 # Wrap in a SPAN tag if nolink_attr is set
943 943 text = '..'
944 944 if self.dotdot_attr:
945 945 text = HTML.span(c=text, **self.dotdot_attr)
946 946 nav_items.append(text)
947 947
948 948 for thispage in xrange(leftmost_page, rightmost_page + 1):
949 949 # Highlight the current page number and do not use a link
950 950 if thispage == self.page:
951 951 text = '%s' % (thispage,)
952 952 # Wrap in a SPAN tag if nolink_attr is set
953 953 if self.curpage_attr:
954 954 text = HTML.span(c=text, **self.curpage_attr)
955 955 nav_items.append(text)
956 956 # Otherwise create just a link to that page
957 957 else:
958 958 text = '%s' % (thispage,)
959 959 nav_items.append(self._pagerlink(thispage, text))
960 960
961 961 # Insert dots if there are pages between the displayed
962 962 # page numbers and the end of the page range
963 963 if self.last_page - rightmost_page > 1:
964 964 text = '..'
965 965 # Wrap in a SPAN tag if nolink_attr is set
966 966 if self.dotdot_attr:
967 967 text = HTML.span(c=text, **self.dotdot_attr)
968 968 nav_items.append(text)
969 969
970 970 # Create a link to the very last page (unless we are on the last
971 971 # page or there would be no need to insert '..' spacers)
972 972 if self.page != self.last_page and rightmost_page < self.last_page:
973 973 nav_items.append(self._pagerlink(self.last_page, self.last_page))
974 974
975 975 #_page_link = url.current()
976 976 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
977 977 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
978 978 return self.separator.join(nav_items)
979 979
980 980 def pager(self, format='~2~', page_param='page', partial_param='partial',
981 981 show_if_single_page=False, separator=' ', onclick=None,
982 982 symbol_first='<<', symbol_last='>>',
983 983 symbol_previous='<', symbol_next='>',
984 984 link_attr={'class': 'pager_link', 'rel': 'prerender'},
985 985 curpage_attr={'class': 'pager_curpage'},
986 986 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
987 987
988 988 self.curpage_attr = curpage_attr
989 989 self.separator = separator
990 990 self.pager_kwargs = kwargs
991 991 self.page_param = page_param
992 992 self.partial_param = partial_param
993 993 self.onclick = onclick
994 994 self.link_attr = link_attr
995 995 self.dotdot_attr = dotdot_attr
996 996
997 997 # Don't show navigator if there is no more than one page
998 998 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
999 999 return ''
1000 1000
1001 1001 from string import Template
1002 1002 # Replace ~...~ in token format by range of pages
1003 1003 result = re.sub(r'~(\d+)~', self._range, format)
1004 1004
1005 1005 # Interpolate '%' variables
1006 1006 result = Template(result).safe_substitute({
1007 1007 'first_page': self.first_page,
1008 1008 'last_page': self.last_page,
1009 1009 'page': self.page,
1010 1010 'page_count': self.page_count,
1011 1011 'items_per_page': self.items_per_page,
1012 1012 'first_item': self.first_item,
1013 1013 'last_item': self.last_item,
1014 1014 'item_count': self.item_count,
1015 1015 'link_first': self.page > self.first_page and \
1016 1016 self._pagerlink(self.first_page, symbol_first) or '',
1017 1017 'link_last': self.page < self.last_page and \
1018 1018 self._pagerlink(self.last_page, symbol_last) or '',
1019 1019 'link_previous': self.previous_page and \
1020 1020 self._pagerlink(self.previous_page, symbol_previous) \
1021 1021 or HTML.span(symbol_previous, class_="yui-pg-previous"),
1022 1022 'link_next': self.next_page and \
1023 1023 self._pagerlink(self.next_page, symbol_next) \
1024 1024 or HTML.span(symbol_next, class_="yui-pg-next")
1025 1025 })
1026 1026
1027 1027 return literal(result)
1028 1028
1029 1029
1030 1030 #==============================================================================
1031 1031 # REPO PAGER, PAGER FOR REPOSITORY
1032 1032 #==============================================================================
1033 1033 class RepoPage(Page):
1034 1034
1035 1035 def __init__(self, collection, page=1, items_per_page=20,
1036 1036 item_count=None, url=None, **kwargs):
1037 1037
1038 1038 """Create a "RepoPage" instance. special pager for paging
1039 1039 repository
1040 1040 """
1041 1041 self._url_generator = url
1042 1042
1043 1043 # Safe the kwargs class-wide so they can be used in the pager() method
1044 1044 self.kwargs = kwargs
1045 1045
1046 1046 # Save a reference to the collection
1047 1047 self.original_collection = collection
1048 1048
1049 1049 self.collection = collection
1050 1050
1051 1051 # The self.page is the number of the current page.
1052 1052 # The first page has the number 1!
1053 1053 try:
1054 1054 self.page = int(page) # make it int() if we get it as a string
1055 1055 except (ValueError, TypeError):
1056 1056 self.page = 1
1057 1057
1058 1058 self.items_per_page = items_per_page
1059 1059
1060 1060 # Unless the user tells us how many items the collections has
1061 1061 # we calculate that ourselves.
1062 1062 if item_count is not None:
1063 1063 self.item_count = item_count
1064 1064 else:
1065 1065 self.item_count = len(self.collection)
1066 1066
1067 1067 # Compute the number of the first and last available page
1068 1068 if self.item_count > 0:
1069 1069 self.first_page = 1
1070 1070 self.page_count = int(math.ceil(float(self.item_count) /
1071 1071 self.items_per_page))
1072 1072 self.last_page = self.first_page + self.page_count - 1
1073 1073
1074 1074 # Make sure that the requested page number is the range of
1075 1075 # valid pages
1076 1076 if self.page > self.last_page:
1077 1077 self.page = self.last_page
1078 1078 elif self.page < self.first_page:
1079 1079 self.page = self.first_page
1080 1080
1081 1081 # Note: the number of items on this page can be less than
1082 1082 # items_per_page if the last page is not full
1083 1083 self.first_item = max(0, (self.item_count) - (self.page *
1084 1084 items_per_page))
1085 1085 self.last_item = ((self.item_count - 1) - items_per_page *
1086 1086 (self.page - 1))
1087 1087
1088 1088 self.items = list(self.collection[self.first_item:self.last_item + 1])
1089 1089
1090 1090 # Links to previous and next page
1091 1091 if self.page > self.first_page:
1092 1092 self.previous_page = self.page - 1
1093 1093 else:
1094 1094 self.previous_page = None
1095 1095
1096 1096 if self.page < self.last_page:
1097 1097 self.next_page = self.page + 1
1098 1098 else:
1099 1099 self.next_page = None
1100 1100
1101 1101 # No items available
1102 1102 else:
1103 1103 self.first_page = None
1104 1104 self.page_count = 0
1105 1105 self.last_page = None
1106 1106 self.first_item = None
1107 1107 self.last_item = None
1108 1108 self.previous_page = None
1109 1109 self.next_page = None
1110 1110 self.items = []
1111 1111
1112 1112 # This is a subclass of the 'list' type. Initialise the list now.
1113 1113 list.__init__(self, reversed(self.items))
1114 1114
1115 1115
1116 1116 def changed_tooltip(nodes):
1117 1117 """
1118 1118 Generates a html string for changed nodes in changeset page.
1119 1119 It limits the output to 30 entries
1120 1120
1121 1121 :param nodes: LazyNodesGenerator
1122 1122 """
1123 1123 if nodes:
1124 1124 pref = ': <br/> '
1125 1125 suf = ''
1126 1126 if len(nodes) > 30:
1127 1127 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1128 1128 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1129 1129 for x in nodes[:30]]) + suf)
1130 1130 else:
1131 1131 return ': ' + _('No Files')
1132 1132
1133 1133
1134 1134 def repo_link(groups_and_repos):
1135 1135 """
1136 1136 Makes a breadcrumbs link to repo within a group
1137 1137 joins &raquo; on each group to create a fancy link
1138 1138
1139 1139 ex::
1140 1140 group >> subgroup >> repo
1141 1141
1142 1142 :param groups_and_repos:
1143 1143 :param last_url:
1144 1144 """
1145 1145 groups, just_name, repo_name = groups_and_repos
1146 1146 last_url = url('summary_home', repo_name=repo_name)
1147 1147 last_link = link_to(just_name, last_url)
1148 1148
1149 1149 def make_link(group):
1150 1150 return link_to(group.name,
1151 1151 url('repos_group_home', group_name=group.group_name))
1152 1152 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1153 1153
1154 1154
1155 1155 def fancy_file_stats(stats):
1156 1156 """
1157 1157 Displays a fancy two colored bar for number of added/deleted
1158 1158 lines of code on file
1159 1159
1160 1160 :param stats: two element list of added/deleted lines of code
1161 1161 """
1162 1162 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1163 1163 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1164 1164
1165 1165 def cgen(l_type, a_v, d_v):
1166 1166 mapping = {'tr': 'top-right-rounded-corner-mid',
1167 1167 'tl': 'top-left-rounded-corner-mid',
1168 1168 'br': 'bottom-right-rounded-corner-mid',
1169 1169 'bl': 'bottom-left-rounded-corner-mid'}
1170 1170 map_getter = lambda x: mapping[x]
1171 1171
1172 1172 if l_type == 'a' and d_v:
1173 1173 #case when added and deleted are present
1174 1174 return ' '.join(map(map_getter, ['tl', 'bl']))
1175 1175
1176 1176 if l_type == 'a' and not d_v:
1177 1177 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1178 1178
1179 1179 if l_type == 'd' and a_v:
1180 1180 return ' '.join(map(map_getter, ['tr', 'br']))
1181 1181
1182 1182 if l_type == 'd' and not a_v:
1183 1183 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1184 1184
1185 1185 a, d = stats['added'], stats['deleted']
1186 1186 width = 100
1187 1187
1188 1188 if stats['binary']:
1189 1189 #binary mode
1190 1190 lbl = ''
1191 1191 bin_op = 1
1192 1192
1193 1193 if BIN_FILENODE in stats['ops']:
1194 1194 lbl = 'bin+'
1195 1195
1196 1196 if NEW_FILENODE in stats['ops']:
1197 1197 lbl += _('new file')
1198 1198 bin_op = NEW_FILENODE
1199 1199 elif MOD_FILENODE in stats['ops']:
1200 1200 lbl += _('mod')
1201 1201 bin_op = MOD_FILENODE
1202 1202 elif DEL_FILENODE in stats['ops']:
1203 1203 lbl += _('del')
1204 1204 bin_op = DEL_FILENODE
1205 1205 elif RENAMED_FILENODE in stats['ops']:
1206 1206 lbl += _('rename')
1207 1207 bin_op = RENAMED_FILENODE
1208 1208
1209 1209 #chmod can go with other operations
1210 1210 if CHMOD_FILENODE in stats['ops']:
1211 1211 _org_lbl = _('chmod')
1212 1212 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1213 1213
1214 1214 #import ipdb;ipdb.set_trace()
1215 1215 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1216 1216 b_a = '<div class="bin bin1" style="width:0%%"></div>'
1217 1217 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1218 1218
1219 1219 t = stats['added'] + stats['deleted']
1220 1220 unit = float(width) / (t or 1)
1221 1221
1222 1222 # needs > 9% of width to be visible or 0 to be hidden
1223 1223 a_p = max(9, unit * a) if a > 0 else 0
1224 1224 d_p = max(9, unit * d) if d > 0 else 0
1225 1225 p_sum = a_p + d_p
1226 1226
1227 1227 if p_sum > width:
1228 1228 #adjust the percentage to be == 100% since we adjusted to 9
1229 1229 if a_p > d_p:
1230 1230 a_p = a_p - (p_sum - width)
1231 1231 else:
1232 1232 d_p = d_p - (p_sum - width)
1233 1233
1234 1234 a_v = a if a > 0 else ''
1235 1235 d_v = d if d > 0 else ''
1236 1236
1237 1237 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1238 1238 cgen('a', a_v, d_v), a_p, a_v
1239 1239 )
1240 1240 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1241 1241 cgen('d', a_v, d_v), d_p, d_v
1242 1242 )
1243 1243 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1244 1244
1245 1245
1246 1246 def urlify_text(text_, safe=True):
1247 1247 """
1248 1248 Extract urls from text and make html links out of them
1249 1249
1250 1250 :param text_:
1251 1251 """
1252 1252
1253 1253 def url_func(match_obj):
1254 1254 url_full = match_obj.groups()[0]
1255 1255 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1256 1256 _newtext = url_re.sub(url_func, text_)
1257 1257 if safe:
1258 1258 return literal(_newtext)
1259 1259 return _newtext
1260 1260
1261 1261
1262 1262 def urlify_changesets(text_, repository):
1263 1263 """
1264 1264 Extract revision ids from changeset and make link from them
1265 1265
1266 1266 :param text_:
1267 1267 :param repository: repo name to build the URL with
1268 1268 """
1269 1269 from pylons import url # doh, we need to re-import url to mock it later
1270 1270
1271 1271 def url_func(match_obj):
1272 1272 rev = match_obj.group(0)
1273 1273 return '<a class="revision-link" href="%(url)s">%(rev)s</a>' % {
1274 1274 'url': url('changeset_home', repo_name=repository, revision=rev),
1275 1275 'rev': rev,
1276 1276 }
1277 1277
1278 1278 return re.sub(r'(?:^|(?<=[\s(),]))([0-9a-fA-F]{12,40})(?=$|\s|[.,:()])', url_func, text_)
1279 1279
1280 1280 def linkify_others(t, l):
1281 1281 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1282 1282 links = []
1283 1283 for e in urls.split(t):
1284 1284 if not urls.match(e):
1285 1285 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1286 1286 else:
1287 1287 links.append(e)
1288 1288
1289 1289 return ''.join(links)
1290 1290
1291 1291 def urlify_commit(text_, repository, link_=None):
1292 1292 """
1293 1293 Parses given text message and makes proper links.
1294 1294 issues are linked to given issue-server, and rest is a changeset link
1295 1295 if link_ is given, in other case it's a plain text
1296 1296
1297 1297 :param text_:
1298 1298 :param repository:
1299 1299 :param link_: changeset link
1300 1300 """
1301 1301 def escaper(string):
1302 1302 return string.replace('<', '&lt;').replace('>', '&gt;')
1303 1303
1304 1304 # urlify changesets - extract revisions and make link out of them
1305 1305 newtext = urlify_changesets(escaper(text_), repository)
1306 1306
1307 1307 # extract http/https links and make them real urls
1308 1308 newtext = urlify_text(newtext, safe=False)
1309 1309
1310 1310 newtext = urlify_issues(newtext, repository, link_)
1311 1311
1312 1312 return literal(newtext)
1313 1313
1314 1314 def urlify_issues(newtext, repository, link_=None):
1315 1315 from kallithea import CONFIG as conf
1316 1316
1317 1317 # allow multiple issue servers to be used
1318 1318 valid_indices = [
1319 1319 x.group(1)
1320 1320 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1321 1321 if x and 'issue_server_link%s' % x.group(1) in conf
1322 1322 and 'issue_prefix%s' % x.group(1) in conf
1323 1323 ]
1324 1324
1325 1325 if valid_indices:
1326 1326 log.debug('found issue server suffixes `%s` during valuation of: %s'
1327 1327 % (','.join(valid_indices), newtext))
1328 1328
1329 1329 for pattern_index in valid_indices:
1330 1330 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1331 1331 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1332 1332 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1333 1333
1334 1334 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1335 1335 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1336 1336 ISSUE_PREFIX))
1337 1337
1338 1338 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1339 1339
1340 1340 def url_func(match_obj):
1341 1341 pref = ''
1342 1342 if match_obj.group().startswith(' '):
1343 1343 pref = ' '
1344 1344
1345 1345 issue_id = ''.join(match_obj.groups())
1346 1346 issue_url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1347 1347 if repository:
1348 1348 issue_url = issue_url.replace('{repo}', repository)
1349 1349 repo_name = repository.split(URL_SEP)[-1]
1350 1350 issue_url = issue_url.replace('{repo_name}', repo_name)
1351 1351
1352 1352 return (
1353 1353 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1354 1354 '%(issue-prefix)s%(id-repr)s'
1355 1355 '</a>'
1356 1356 ) % {
1357 1357 'pref': pref,
1358 1358 'cls': 'issue-tracker-link',
1359 1359 'url': issue_url,
1360 1360 'id-repr': issue_id,
1361 1361 'issue-prefix': ISSUE_PREFIX,
1362 1362 'serv': ISSUE_SERVER_LNK,
1363 1363 }
1364 1364 newtext = URL_PAT.sub(url_func, newtext)
1365 1365 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1366 1366
1367 1367 # if we actually did something above
1368 1368 if link_:
1369 1369 # wrap not links into final link => link_
1370 1370 newtext = linkify_others(newtext, link_)
1371 1371 return newtext
1372 1372
1373 1373
1374 1374 def rst(source):
1375 1375 return literal('<div class="rst-block">%s</div>' %
1376 1376 MarkupRenderer.rst(source))
1377 1377
1378 1378
1379 1379 def rst_w_mentions(source):
1380 1380 """
1381 1381 Wrapped rst renderer with @mention highlighting
1382 1382
1383 1383 :param source:
1384 1384 """
1385 1385 return literal('<div class="rst-block">%s</div>' %
1386 1386 MarkupRenderer.rst_with_mentions(source))
1387 1387
1388 1388 def short_ref(ref_type, ref_name):
1389 1389 if ref_type == 'rev':
1390 1390 return short_id(ref_name)
1391 1391 return ref_name
1392 1392
1393 1393 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1394 1394 """
1395 1395 Return full markup for a href to changeset_home for a changeset.
1396 1396 If ref_type is branch it will link to changelog.
1397 1397 ref_name is shortened if ref_type is 'rev'.
1398 1398 if rev is specified show it too, explicitly linking to that revision.
1399 1399 """
1400 1400 txt = short_ref(ref_type, ref_name)
1401 1401 if ref_type == 'branch':
1402 1402 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1403 1403 else:
1404 1404 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1405 1405 l = link_to(repo_name + '#' + txt, u)
1406 1406 if rev and ref_type != 'rev':
1407 1407 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1408 1408 return l
1409 1409
1410 1410 def changeset_status(repo, revision):
1411 1411 return ChangesetStatusModel().get_status(repo, revision)
1412 1412
1413 1413
1414 1414 def changeset_status_lbl(changeset_status):
1415 1415 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1416 1416
1417 1417
1418 1418 def get_permission_name(key):
1419 1419 return dict(Permission.PERMS).get(key)
1420 1420
1421 1421
1422 1422 def journal_filter_help():
1423 1423 return _(textwrap.dedent('''
1424 1424 Example filter terms:
1425 1425 repository:vcs
1426 1426 username:developer
1427 1427 action:*push*
1428 1428 ip:127.0.0.1
1429 1429 date:20120101
1430 1430 date:[20120101100000 TO 20120102]
1431 1431
1432 1432 Generate wildcards using '*' character:
1433 1433 "repository:vcs*" - search everything starting with 'vcs'
1434 1434 "repository:*vcs*" - search for repository containing 'vcs'
1435 1435
1436 1436 Optional AND / OR operators in queries
1437 1437 "repository:vcs OR repository:test"
1438 1438 "username:test AND repository:test*"
1439 1439 '''))
1440 1440
1441 1441
1442 1442 def not_mapped_error(repo_name):
1443 1443 flash(_('%s repository is not mapped to db perhaps'
1444 1444 ' it was created or renamed from the filesystem'
1445 1445 ' please run the application again'
1446 1446 ' in order to rescan repositories') % repo_name, category='error')
1447 1447
1448 1448
1449 1449 def ip_range(ip_addr):
1450 1450 from kallithea.model.db import UserIpMap
1451 1451 s, e = UserIpMap._get_ip_range(ip_addr)
1452 1452 return '%s - %s' % (s, e)
General Comments 0
You need to be logged in to leave comments. Login now