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