##// END OF EJS Templates
added wrapping into journal tooltips
marcink -
r911:435882d9 beta
parent child Browse files
Show More
@@ -1,525 +1,525 b''
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 from pygments.formatters import HtmlFormatter
9 9 from pygments import highlight as code_highlight
10 10 from pylons import url, app_globals as g
11 11 from pylons.i18n.translation import _, ungettext
12 12 from vcs.utils.annotate import annotate_highlight
13 13 from webhelpers.html import literal, HTML, escape
14 14 from webhelpers.html.tools import *
15 15 from webhelpers.html.builder import make_tag
16 16 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
17 17 end_form, file, form, hidden, image, javascript_link, link_to, link_to_if, \
18 18 link_to_unless, ol, required_legend, select, stylesheet_link, submit, text, \
19 19 password, textarea, title, ul, xml_declaration, radio
20 20 from webhelpers.html.tools import auto_link, button_to, highlight, js_obfuscate, \
21 21 mail_to, strip_links, strip_tags, tag_re
22 22 from webhelpers.number import format_byte_size, format_bit_size
23 23 from webhelpers.pylonslib import Flash as _Flash
24 24 from webhelpers.pylonslib.secure_form import secure_form
25 25 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
26 26 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
27 27 replace_whitespace, urlify, truncate, wrap_paragraphs
28 28 from webhelpers.date import time_ago_in_words
29 29
30 30 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
31 31 convert_boolean_attrs, NotGiven
32 32
33 33 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
34 34 """Reset button
35 35 """
36 36 _set_input_attrs(attrs, type, name, value)
37 37 _set_id_attr(attrs, id, name)
38 38 convert_boolean_attrs(attrs, ["disabled"])
39 39 return HTML.input(**attrs)
40 40
41 41 reset = _reset
42 42
43 43
44 44 def get_token():
45 45 """Return the current authentication token, creating one if one doesn't
46 46 already exist.
47 47 """
48 48 token_key = "_authentication_token"
49 49 from pylons import session
50 50 if not token_key in session:
51 51 try:
52 52 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
53 53 except AttributeError: # Python < 2.4
54 54 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
55 55 session[token_key] = token
56 56 if hasattr(session, 'save'):
57 57 session.save()
58 58 return session[token_key]
59 59
60 60 class _GetError(object):
61 61 """Get error from form_errors, and represent it as span wrapped error
62 62 message
63 63
64 64 :param field_name: field to fetch errors for
65 65 :param form_errors: form errors dict
66 66 """
67 67
68 68 def __call__(self, field_name, form_errors):
69 69 tmpl = """<span class="error_msg">%s</span>"""
70 70 if form_errors and form_errors.has_key(field_name):
71 71 return literal(tmpl % form_errors.get(field_name))
72 72
73 73 get_error = _GetError()
74 74
75 75 def recursive_replace(str, replace=' '):
76 76 """Recursive replace of given sign to just one instance
77 77
78 78 :param str: given string
79 79 :param replace: char to find and replace multiple instances
80 80
81 81 Examples::
82 82 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
83 83 'Mighty-Mighty-Bo-sstones'
84 84 """
85 85
86 86 if str.find(replace * 2) == -1:
87 87 return str
88 88 else:
89 89 str = str.replace(replace * 2, replace)
90 90 return recursive_replace(str, replace)
91 91
92 92 class _ToolTip(object):
93 93
94 94 def __call__(self, tooltip_title, trim_at=50):
95 95 """Special function just to wrap our text into nice formatted
96 96 autowrapped text
97 97
98 98 :param tooltip_title:
99 99 """
100 100
101 101 return wrap_paragraphs(escape(tooltip_title), trim_at)\
102 102 .replace('\n', '<br/>')
103 103
104 104 def activate(self):
105 105 """Adds tooltip mechanism to the given Html all tooltips have to have
106 106 set class `tooltip` and set attribute `tooltip_title`.
107 107 Then a tooltip will be generated based on that. All with yui js tooltip
108 108 """
109 109
110 110 js = '''
111 111 YAHOO.util.Event.onDOMReady(function(){
112 112 function toolTipsId(){
113 113 var ids = [];
114 114 var tts = YAHOO.util.Dom.getElementsByClassName('tooltip');
115 115
116 116 for (var i = 0; i < tts.length; i++) {
117 117 //if element doesn't not have and id autogenerate one for tooltip
118 118
119 119 if (!tts[i].id){
120 120 tts[i].id='tt'+i*100;
121 121 }
122 122 ids.push(tts[i].id);
123 123 }
124 124 return ids
125 125 };
126 126 var myToolTips = new YAHOO.widget.Tooltip("tooltip", {
127 127 context: toolTipsId(),
128 128 monitorresize:false,
129 129 xyoffset :[0,0],
130 130 autodismissdelay:300000,
131 131 hidedelay:5,
132 132 showdelay:20,
133 133 });
134 134
135 135 // Set the text for the tooltip just before we display it. Lazy method
136 136 myToolTips.contextTriggerEvent.subscribe(
137 137 function(type, args) {
138 138
139 139 var context = args[0];
140 140
141 141 //positioning of tooltip
142 142 var tt_w = this.element.clientWidth;//tooltip width
143 143 var tt_h = this.element.clientHeight;//tooltip height
144 144
145 145 var context_w = context.offsetWidth;
146 146 var context_h = context.offsetHeight;
147 147
148 148 var pos_x = YAHOO.util.Dom.getX(context);
149 149 var pos_y = YAHOO.util.Dom.getY(context);
150 150
151 151 var display_strategy = 'right';
152 152 var xy_pos = [0,0];
153 153 switch (display_strategy){
154 154
155 155 case 'top':
156 156 var cur_x = (pos_x+context_w/2)-(tt_w/2);
157 157 var cur_y = (pos_y-tt_h-4);
158 158 xy_pos = [cur_x,cur_y];
159 159 break;
160 160 case 'bottom':
161 161 var cur_x = (pos_x+context_w/2)-(tt_w/2);
162 162 var cur_y = pos_y+context_h+4;
163 163 xy_pos = [cur_x,cur_y];
164 164 break;
165 165 case 'left':
166 166 var cur_x = (pos_x-tt_w-4);
167 167 var cur_y = pos_y-((tt_h/2)-context_h/2);
168 168 xy_pos = [cur_x,cur_y];
169 169 break;
170 170 case 'right':
171 171 var cur_x = (pos_x+context_w+4);
172 172 var cur_y = pos_y-((tt_h/2)-context_h/2);
173 173 xy_pos = [cur_x,cur_y];
174 174 break;
175 175 default:
176 176 var cur_x = (pos_x+context_w/2)-(tt_w/2);
177 177 var cur_y = pos_y-tt_h-4;
178 178 xy_pos = [cur_x,cur_y];
179 179 break;
180 180
181 181 }
182 182
183 183 this.cfg.setProperty("xy",xy_pos);
184 184
185 185 });
186 186
187 187 //Mouse out
188 188 myToolTips.contextMouseOutEvent.subscribe(
189 189 function(type, args) {
190 190 var context = args[0];
191 191
192 192 });
193 193 });
194 194 '''
195 195 return literal(js)
196 196
197 197 tooltip = _ToolTip()
198 198
199 199 class _FilesBreadCrumbs(object):
200 200
201 201 def __call__(self, repo_name, rev, paths):
202 202 url_l = [link_to(repo_name, url('files_home',
203 203 repo_name=repo_name,
204 204 revision=rev, f_path=''))]
205 205 paths_l = paths.split('/')
206 206
207 207 for cnt, p in enumerate(paths_l):
208 208 if p != '':
209 209 url_l.append(link_to(p, url('files_home',
210 210 repo_name=repo_name,
211 211 revision=rev,
212 212 f_path='/'.join(paths_l[:cnt + 1]))))
213 213
214 214 return literal('/'.join(url_l))
215 215
216 216 files_breadcrumbs = _FilesBreadCrumbs()
217 217
218 218 class CodeHtmlFormatter(HtmlFormatter):
219 219
220 220 def wrap(self, source, outfile):
221 221 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
222 222
223 223 def _wrap_code(self, source):
224 224 for cnt, it in enumerate(source):
225 225 i, t = it
226 226 t = '<div id="#S-%s">%s</div>' % (cnt + 1, t)
227 227 yield i, t
228 228 def pygmentize(filenode, **kwargs):
229 229 """pygmentize function using pygments
230 230
231 231 :param filenode:
232 232 """
233 233
234 234 return literal(code_highlight(filenode.content,
235 235 filenode.lexer, CodeHtmlFormatter(**kwargs)))
236 236
237 237 def pygmentize_annotation(filenode, **kwargs):
238 238 """pygmentize function for annotation
239 239
240 240 :param filenode:
241 241 """
242 242
243 243 color_dict = {}
244 244 def gen_color():
245 245 """generator for getting 10k of evenly distibuted colors using hsv color
246 246 and golden ratio.
247 247 """
248 248 import colorsys
249 249 n = 10000
250 250 golden_ratio = 0.618033988749895
251 251 h = 0.22717784590367374
252 252 #generate 10k nice web friendly colors in the same order
253 253 for c in xrange(n):
254 254 h += golden_ratio
255 255 h %= 1
256 256 HSV_tuple = [h, 0.95, 0.95]
257 257 RGB_tuple = colorsys.hsv_to_rgb(*HSV_tuple)
258 258 yield map(lambda x:str(int(x * 256)), RGB_tuple)
259 259
260 260 cgenerator = gen_color()
261 261
262 262 def get_color_string(cs):
263 263 if color_dict.has_key(cs):
264 264 col = color_dict[cs]
265 265 else:
266 266 col = color_dict[cs] = cgenerator.next()
267 267 return "color: rgb(%s)! important;" % (', '.join(col))
268 268
269 269 def url_func(changeset):
270 270 tooltip_html = "<div style='font-size:0.8em'><b>Author:</b>" + \
271 271 " %s<br/><b>Date:</b> %s</b><br/><b>Message:</b> %s<br/></div>"
272 272
273 273 tooltip_html = tooltip_html % (changeset.author,
274 274 changeset.date,
275 275 tooltip(changeset.message))
276 276 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
277 277 short_id(changeset.raw_id))
278 278 uri = link_to(
279 279 lnk_format,
280 280 url('changeset_home', repo_name=changeset.repository.name,
281 281 revision=changeset.raw_id),
282 282 style=get_color_string(changeset.raw_id),
283 283 class_='tooltip',
284 284 title=tooltip_html
285 285 )
286 286
287 287 uri += '\n'
288 288 return uri
289 289 return literal(annotate_highlight(filenode, url_func, **kwargs))
290 290
291 291 def repo_name_slug(value):
292 292 """Return slug of name of repository
293 293 This function is called on each creation/modification
294 294 of repository to prevent bad names in repo
295 295 """
296 296
297 297 slug = remove_formatting(value)
298 298 slug = strip_tags(slug)
299 299
300 300 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
301 301 slug = slug.replace(c, '-')
302 302 slug = recursive_replace(slug, '-')
303 303 slug = collapse(slug, '-')
304 304 return slug
305 305
306 306 def get_changeset_safe(repo, rev):
307 307 from vcs.backends.base import BaseRepository
308 308 from vcs.exceptions import RepositoryError
309 309 if not isinstance(repo, BaseRepository):
310 310 raise Exception('You must pass an Repository '
311 311 'object as first argument got %s', type(repo))
312 312
313 313 try:
314 314 cs = repo.get_changeset(rev)
315 315 except RepositoryError:
316 316 from rhodecode.lib.utils import EmptyChangeset
317 317 cs = EmptyChangeset()
318 318 return cs
319 319
320 320
321 321 flash = _Flash()
322 322
323 323
324 324 #==============================================================================
325 325 # MERCURIAL FILTERS available via h.
326 326 #==============================================================================
327 327 from mercurial import util
328 328 from mercurial.templatefilters import person as _person
329 329
330 330 def _age(curdate):
331 331 """turns a datetime into an age string."""
332 332
333 333 if not curdate:
334 334 return ''
335 335
336 336 from datetime import timedelta, datetime
337 337
338 338 agescales = [("year", 3600 * 24 * 365),
339 339 ("month", 3600 * 24 * 30),
340 340 ("day", 3600 * 24),
341 341 ("hour", 3600),
342 342 ("minute", 60),
343 343 ("second", 1), ]
344 344
345 345 age = datetime.now() - curdate
346 346 age_seconds = (age.days * agescales[2][1]) + age.seconds
347 347 pos = 1
348 348 for scale in agescales:
349 349 if scale[1] <= age_seconds:
350 350 if pos == 6:pos = 5
351 351 return time_ago_in_words(curdate, agescales[pos][0]) + ' ' + _('ago')
352 352 pos += 1
353 353
354 354 return _('just now')
355 355
356 356 age = lambda x:_age(x)
357 357 capitalize = lambda x: x.capitalize()
358 358 email = util.email
359 359 email_or_none = lambda x: util.email(x) if util.email(x) != x else None
360 360 person = lambda x: _person(x)
361 361 short_id = lambda x: x[:12]
362 362
363 363
364 364 def bool2icon(value):
365 365 """Returns True/False values represented as small html image of true/false
366 366 icons
367 367
368 368 :param value: bool value
369 369 """
370 370
371 371 if value is True:
372 372 return HTML.tag('img', src="/images/icons/accept.png", alt=_('True'))
373 373
374 374 if value is False:
375 375 return HTML.tag('img', src="/images/icons/cancel.png", alt=_('False'))
376 376
377 377 return value
378 378
379 379
380 380 def action_parser(user_log):
381 381 """This helper will map the specified string action into translated
382 382 fancy names with icons and links
383 383
384 384 :param user_log: user log instance
385 385 """
386 386
387 387 action = user_log.action
388 388 action_params = ' '
389 389
390 390 x = action.split(':')
391 391
392 392 if len(x) > 1:
393 393 action, action_params = x
394 394
395 395 def get_cs_links():
396 396 if action == 'push':
397 397 revs_limit = 5 #display this amount always
398 398 revs_top_limit = 50 #show upto this amount of changesets hidden
399 399 revs = action_params.split(',')
400 400 repo_name = user_log.repository.repo_name
401 401 from rhodecode.model.scm import ScmModel
402 402
403 403 message = lambda rev: get_changeset_safe(ScmModel().get(repo_name),
404 404 rev).message
405 405
406 406 cs_links = " " + ', '.join ([link_to(rev,
407 407 url('changeset_home',
408 408 repo_name=repo_name,
409 revision=rev), title=message(rev),
409 revision=rev), title=tooltip(message(rev)),
410 410 class_='tooltip') for rev in revs[:revs_limit] ])
411 411 if len(revs) > revs_limit:
412 412 uniq_id = revs[0]
413 413 html_tmpl = ('<span> %s '
414 414 '<a class="show_more" id="_%s" href="#">%s</a> '
415 415 '%s</span>')
416 416 cs_links += html_tmpl % (_('and'), uniq_id, _('%s more') \
417 417 % (len(revs) - revs_limit),
418 418 _('revisions'))
419 419
420 420 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
421 421 cs_links += html_tmpl % (uniq_id, ', '.join([link_to(rev,
422 422 url('changeset_home',
423 423 repo_name=repo_name, revision=rev),
424 424 title=message(rev), class_='tooltip')
425 425 for rev in revs[revs_limit:revs_top_limit]]))
426 426
427 427 return cs_links
428 428 return ''
429 429
430 430 def get_fork_name():
431 431 if action == 'user_forked_repo':
432 432 from rhodecode.model.scm import ScmModel
433 433 repo_name = action_params
434 434 repo = ScmModel().get(repo_name)
435 435 if repo is None:
436 436 return repo_name
437 437 return link_to(action_params, url('summary_home',
438 438 repo_name=repo.name,),
439 439 title=repo.dbrepo.description)
440 440 return ''
441 441 map = {'user_deleted_repo':_('User [deleted] repository'),
442 442 'user_created_repo':_('User [created] repository'),
443 443 'user_forked_repo':_('User [forked] repository as: %s') % get_fork_name(),
444 444 'user_updated_repo':_('User [updated] repository'),
445 445 'admin_deleted_repo':_('Admin [delete] repository'),
446 446 'admin_created_repo':_('Admin [created] repository'),
447 447 'admin_forked_repo':_('Admin [forked] repository'),
448 448 'admin_updated_repo':_('Admin [updated] repository'),
449 449 'push':_('[Pushed] %s') % get_cs_links(),
450 450 'pull':_('[Pulled]'),
451 451 'started_following_repo':_('User [started following] repository'),
452 452 'stopped_following_repo':_('User [stopped following] repository'),
453 453 }
454 454
455 455 action_str = map.get(action, action)
456 456 return literal(action_str.replace('[', '<span class="journal_highlight">')\
457 457 .replace(']', '</span>'))
458 458
459 459 def action_parser_icon(user_log):
460 460 action = user_log.action
461 461 action_params = None
462 462 x = action.split(':')
463 463
464 464 if len(x) > 1:
465 465 action, action_params = x
466 466
467 467 tmpl = """<img src="/images/icons/%s" alt="%s"/>"""
468 468 map = {'user_deleted_repo':'database_delete.png',
469 469 'user_created_repo':'database_add.png',
470 470 'user_forked_repo':'arrow_divide.png',
471 471 'user_updated_repo':'database_edit.png',
472 472 'admin_deleted_repo':'database_delete.png',
473 473 'admin_created_repo':'database_add.png',
474 474 'admin_forked_repo':'arrow_divide.png',
475 475 'admin_updated_repo':'database_edit.png',
476 476 'push':'script_add.png',
477 477 'pull':'down_16.png',
478 478 'started_following_repo':'heart_add.png',
479 479 'stopped_following_repo':'heart_delete.png',
480 480 }
481 481 return literal(tmpl % (map.get(action, action), action))
482 482
483 483
484 484 #==============================================================================
485 485 # PERMS
486 486 #==============================================================================
487 487 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
488 488 HasRepoPermissionAny, HasRepoPermissionAll
489 489
490 490 #==============================================================================
491 491 # GRAVATAR URL
492 492 #==============================================================================
493 493 import hashlib
494 494 import urllib
495 495 from pylons import request
496 496
497 497 def gravatar_url(email_address, size=30):
498 498 ssl_enabled = 'https' == request.environ.get('HTTP_X_URL_SCHEME')
499 499 default = 'identicon'
500 500 baseurl_nossl = "http://www.gravatar.com/avatar/"
501 501 baseurl_ssl = "https://secure.gravatar.com/avatar/"
502 502 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
503 503
504 504
505 505 # construct the url
506 506 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
507 507 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
508 508
509 509 return gravatar_url
510 510
511 511 def safe_unicode(str):
512 512 """safe unicode function. In case of UnicodeDecode error we try to return
513 513 unicode with errors replace, if this failes we return unicode with
514 514 string_escape decoding """
515 515
516 516 try:
517 517 u_str = unicode(str)
518 518 except UnicodeDecodeError:
519 519 try:
520 520 u_str = unicode(str, 'utf-8', 'replace')
521 521 except UnicodeDecodeError:
522 522 #incase we have a decode error just represent as byte string
523 523 u_str = unicode(str(str).encode('string_escape'))
524 524
525 525 return u_str
General Comments 0
You need to be logged in to leave comments. Login now