##// END OF EJS Templates
simplified str2bool, and moved safe_unicode out of helpers since it was not html specific function
marcink -
r1154:36fe593d beta
parent child Browse files
Show More
@@ -1,47 +1,67 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.__init__
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Some simple helper functions
7 7
8 8 :created_on: Jan 5, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2009-2010 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
14 14 # modify it under the terms of the GNU General Public License
15 15 # as published by the Free Software Foundation; version 2
16 16 # of the License or (at your opinion) any later version of the license.
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, write to the Free Software
25 25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 26 # MA 02110-1301, USA.
27 27
28 def str2bool(v):
29 if isinstance(v, (str, unicode)):
30 obj = v.strip().lower()
31 if obj in ['true', 'yes', 'on', 'y', 't', '1']:
32 return True
33 elif obj in ['false', 'no', 'off', 'n', 'f', '0']:
34 return False
35 else:
36 if not safe:
37 raise ValueError("String is not true/false: %r" % obj)
38 return bool(obj)
28 def str2bool(s):
29 if s is None:
30 return False
31 if s in (True, False):
32 return s
33 s = str(s).strip().lower()
34 return s in ('t', 'true', 'y', 'yes', 'on', '1')
39 35
40 36 def generate_api_key(username, salt=None):
37 """
38 Generates uniq API key for given username
39
40 :param username: username as string
41 :param salt: salt to hash generate KEY
42 """
41 43 from tempfile import _RandomNameSequence
42 44 import hashlib
43 45
44 46 if salt is None:
45 47 salt = _RandomNameSequence().next()
46 48
47 49 return hashlib.sha1(username + salt).hexdigest()
50
51 def safe_unicode(str):
52 """
53 safe unicode function. In case of UnicodeDecode error we try to return
54 unicode with errors replace, if this fails we return unicode with
55 string_escape decoding
56 """
57
58 try:
59 u_str = unicode(str)
60 except UnicodeDecodeError:
61 try:
62 u_str = unicode(str, 'utf-8', 'replace')
63 except UnicodeDecodeError:
64 #incase we have a decode error just represent as byte string
65 u_str = unicode(str(str).encode('string_escape'))
66
67 return u_str
@@ -1,698 +1,682 b''
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10
11 from datetime import datetime
11 12 from pygments.formatters import HtmlFormatter
12 13 from pygments import highlight as code_highlight
13 14 from pylons import url, request, config
14 15 from pylons.i18n.translation import _, ungettext
15 16
16 17 from webhelpers.html import literal, HTML, escape
17 18 from webhelpers.html.tools import *
18 19 from webhelpers.html.builder import make_tag
19 20 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
20 21 end_form, file, form, hidden, image, javascript_link, link_to, link_to_if, \
21 22 link_to_unless, ol, required_legend, select, stylesheet_link, submit, text, \
22 23 password, textarea, title, ul, xml_declaration, radio
23 24 from webhelpers.html.tools import auto_link, button_to, highlight, js_obfuscate, \
24 25 mail_to, strip_links, strip_tags, tag_re
25 26 from webhelpers.number import format_byte_size, format_bit_size
26 27 from webhelpers.pylonslib import Flash as _Flash
27 28 from webhelpers.pylonslib.secure_form import secure_form
28 29 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
29 30 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
30 31 replace_whitespace, urlify, truncate, wrap_paragraphs
31 32 from webhelpers.date import time_ago_in_words
32 33 from webhelpers.paginate import Page
33 34 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
34 35 convert_boolean_attrs, NotGiven
35 36
36 37 from vcs.utils.annotate import annotate_highlight
37 38 from rhodecode.lib.utils import repo_name_slug
38 39 from rhodecode.lib import str2bool
39 40
40 41 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
41 """Reset button
42 """
43 Reset button
42 44 """
43 45 _set_input_attrs(attrs, type, name, value)
44 46 _set_id_attr(attrs, id, name)
45 47 convert_boolean_attrs(attrs, ["disabled"])
46 48 return HTML.input(**attrs)
47 49
48 50 reset = _reset
49 51
50 52
51 53 def get_token():
52 54 """Return the current authentication token, creating one if one doesn't
53 55 already exist.
54 56 """
55 57 token_key = "_authentication_token"
56 58 from pylons import session
57 59 if not token_key in session:
58 60 try:
59 61 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
60 62 except AttributeError: # Python < 2.4
61 63 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
62 64 session[token_key] = token
63 65 if hasattr(session, 'save'):
64 66 session.save()
65 67 return session[token_key]
66 68
67 69 class _GetError(object):
68 70 """Get error from form_errors, and represent it as span wrapped error
69 71 message
70 72
71 73 :param field_name: field to fetch errors for
72 74 :param form_errors: form errors dict
73 75 """
74 76
75 77 def __call__(self, field_name, form_errors):
76 78 tmpl = """<span class="error_msg">%s</span>"""
77 79 if form_errors and form_errors.has_key(field_name):
78 80 return literal(tmpl % form_errors.get(field_name))
79 81
80 82 get_error = _GetError()
81 83
82 84 class _ToolTip(object):
83 85
84 86 def __call__(self, tooltip_title, trim_at=50):
85 87 """Special function just to wrap our text into nice formatted
86 88 autowrapped text
87 89
88 90 :param tooltip_title:
89 91 """
90 92
91 93 return wrap_paragraphs(escape(tooltip_title), trim_at)\
92 94 .replace('\n', '<br/>')
93 95
94 96 def activate(self):
95 97 """Adds tooltip mechanism to the given Html all tooltips have to have
96 98 set class `tooltip` and set attribute `tooltip_title`.
97 99 Then a tooltip will be generated based on that. All with yui js tooltip
98 100 """
99 101
100 102 js = '''
101 103 YAHOO.util.Event.onDOMReady(function(){
102 104 function toolTipsId(){
103 105 var ids = [];
104 106 var tts = YAHOO.util.Dom.getElementsByClassName('tooltip');
105 107
106 108 for (var i = 0; i < tts.length; i++) {
107 109 //if element doesn't not have and id autogenerate one for tooltip
108 110
109 111 if (!tts[i].id){
110 112 tts[i].id='tt'+i*100;
111 113 }
112 114 ids.push(tts[i].id);
113 115 }
114 116 return ids
115 117 };
116 118 var myToolTips = new YAHOO.widget.Tooltip("tooltip", {
117 119 context: toolTipsId(),
118 120 monitorresize:false,
119 121 xyoffset :[0,0],
120 122 autodismissdelay:300000,
121 123 hidedelay:5,
122 124 showdelay:20,
123 125 });
124 126
125 127 // Set the text for the tooltip just before we display it. Lazy method
126 128 myToolTips.contextTriggerEvent.subscribe(
127 129 function(type, args) {
128 130
129 131 var context = args[0];
130 132
131 133 //positioning of tooltip
132 134 var tt_w = this.element.clientWidth;//tooltip width
133 135 var tt_h = this.element.clientHeight;//tooltip height
134 136
135 137 var context_w = context.offsetWidth;
136 138 var context_h = context.offsetHeight;
137 139
138 140 var pos_x = YAHOO.util.Dom.getX(context);
139 141 var pos_y = YAHOO.util.Dom.getY(context);
140 142
141 143 var display_strategy = 'right';
142 144 var xy_pos = [0,0];
143 145 switch (display_strategy){
144 146
145 147 case 'top':
146 148 var cur_x = (pos_x+context_w/2)-(tt_w/2);
147 149 var cur_y = (pos_y-tt_h-4);
148 150 xy_pos = [cur_x,cur_y];
149 151 break;
150 152 case 'bottom':
151 153 var cur_x = (pos_x+context_w/2)-(tt_w/2);
152 154 var cur_y = pos_y+context_h+4;
153 155 xy_pos = [cur_x,cur_y];
154 156 break;
155 157 case 'left':
156 158 var cur_x = (pos_x-tt_w-4);
157 159 var cur_y = pos_y-((tt_h/2)-context_h/2);
158 160 xy_pos = [cur_x,cur_y];
159 161 break;
160 162 case 'right':
161 163 var cur_x = (pos_x+context_w+4);
162 164 var cur_y = pos_y-((tt_h/2)-context_h/2);
163 165 xy_pos = [cur_x,cur_y];
164 166 break;
165 167 default:
166 168 var cur_x = (pos_x+context_w/2)-(tt_w/2);
167 169 var cur_y = pos_y-tt_h-4;
168 170 xy_pos = [cur_x,cur_y];
169 171 break;
170 172
171 173 }
172 174
173 175 this.cfg.setProperty("xy",xy_pos);
174 176
175 177 });
176 178
177 179 //Mouse out
178 180 myToolTips.contextMouseOutEvent.subscribe(
179 181 function(type, args) {
180 182 var context = args[0];
181 183
182 184 });
183 185 });
184 186 '''
185 187 return literal(js)
186 188
187 189 tooltip = _ToolTip()
188 190
189 191 class _FilesBreadCrumbs(object):
190 192
191 193 def __call__(self, repo_name, rev, paths):
192 194 if isinstance(paths, str):
193 195 paths = paths.decode('utf-8', 'replace')
194 196 url_l = [link_to(repo_name, url('files_home',
195 197 repo_name=repo_name,
196 198 revision=rev, f_path=''))]
197 199 paths_l = paths.split('/')
198 200 for cnt, p in enumerate(paths_l):
199 201 if p != '':
200 202 url_l.append(link_to(p, url('files_home',
201 203 repo_name=repo_name,
202 204 revision=rev,
203 205 f_path='/'.join(paths_l[:cnt + 1]))))
204 206
205 207 return literal('/'.join(url_l))
206 208
207 209 files_breadcrumbs = _FilesBreadCrumbs()
208 210
209 211 class CodeHtmlFormatter(HtmlFormatter):
210 212 """My code Html Formatter for source codes
211 213 """
212 214
213 215 def wrap(self, source, outfile):
214 216 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
215 217
216 218 def _wrap_code(self, source):
217 219 for cnt, it in enumerate(source):
218 220 i, t = it
219 221 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
220 222 yield i, t
221 223
222 224 def _wrap_tablelinenos(self, inner):
223 225 dummyoutfile = StringIO.StringIO()
224 226 lncount = 0
225 227 for t, line in inner:
226 228 if t:
227 229 lncount += 1
228 230 dummyoutfile.write(line)
229 231
230 232 fl = self.linenostart
231 233 mw = len(str(lncount + fl - 1))
232 234 sp = self.linenospecial
233 235 st = self.linenostep
234 236 la = self.lineanchors
235 237 aln = self.anchorlinenos
236 238 nocls = self.noclasses
237 239 if sp:
238 240 lines = []
239 241
240 242 for i in range(fl, fl + lncount):
241 243 if i % st == 0:
242 244 if i % sp == 0:
243 245 if aln:
244 246 lines.append('<a href="#%s%d" class="special">%*d</a>' %
245 247 (la, i, mw, i))
246 248 else:
247 249 lines.append('<span class="special">%*d</span>' % (mw, i))
248 250 else:
249 251 if aln:
250 252 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
251 253 else:
252 254 lines.append('%*d' % (mw, i))
253 255 else:
254 256 lines.append('')
255 257 ls = '\n'.join(lines)
256 258 else:
257 259 lines = []
258 260 for i in range(fl, fl + lncount):
259 261 if i % st == 0:
260 262 if aln:
261 263 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
262 264 else:
263 265 lines.append('%*d' % (mw, i))
264 266 else:
265 267 lines.append('')
266 268 ls = '\n'.join(lines)
267 269
268 270 # in case you wonder about the seemingly redundant <div> here: since the
269 271 # content in the other cell also is wrapped in a div, some browsers in
270 272 # some configurations seem to mess up the formatting...
271 273 if nocls:
272 274 yield 0, ('<table class="%stable">' % self.cssclass +
273 275 '<tr><td><div class="linenodiv" '
274 276 'style="background-color: #f0f0f0; padding-right: 10px">'
275 277 '<pre style="line-height: 125%">' +
276 278 ls + '</pre></div></td><td class="code">')
277 279 else:
278 280 yield 0, ('<table class="%stable">' % self.cssclass +
279 281 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
280 282 ls + '</pre></div></td><td class="code">')
281 283 yield 0, dummyoutfile.getvalue()
282 284 yield 0, '</td></tr></table>'
283 285
284 286
285 287 def pygmentize(filenode, **kwargs):
286 288 """pygmentize function using pygments
287 289
288 290 :param filenode:
289 291 """
290 292
291 293 return literal(code_highlight(filenode.content,
292 294 filenode.lexer, CodeHtmlFormatter(**kwargs)))
293 295
294 296 def pygmentize_annotation(filenode, **kwargs):
295 297 """pygmentize function for annotation
296 298
297 299 :param filenode:
298 300 """
299 301
300 302 color_dict = {}
301 303 def gen_color(n=10000):
302 304 """generator for getting n of evenly distributed colors using
303 305 hsv color and golden ratio. It always return same order of colors
304 306
305 307 :returns: RGB tuple
306 308 """
307 309 import colorsys
308 310 golden_ratio = 0.618033988749895
309 311 h = 0.22717784590367374
310 312
311 313 for c in xrange(n):
312 314 h += golden_ratio
313 315 h %= 1
314 316 HSV_tuple = [h, 0.95, 0.95]
315 317 RGB_tuple = colorsys.hsv_to_rgb(*HSV_tuple)
316 318 yield map(lambda x:str(int(x * 256)), RGB_tuple)
317 319
318 320 cgenerator = gen_color()
319 321
320 322 def get_color_string(cs):
321 323 if color_dict.has_key(cs):
322 324 col = color_dict[cs]
323 325 else:
324 326 col = color_dict[cs] = cgenerator.next()
325 327 return "color: rgb(%s)! important;" % (', '.join(col))
326 328
327 329 def url_func(changeset):
328 330 tooltip_html = "<div style='font-size:0.8em'><b>Author:</b>" + \
329 331 " %s<br/><b>Date:</b> %s</b><br/><b>Message:</b> %s<br/></div>"
330 332
331 333 tooltip_html = tooltip_html % (changeset.author,
332 334 changeset.date,
333 335 tooltip(changeset.message))
334 336 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
335 337 short_id(changeset.raw_id))
336 338 uri = link_to(
337 339 lnk_format,
338 340 url('changeset_home', repo_name=changeset.repository.name,
339 341 revision=changeset.raw_id),
340 342 style=get_color_string(changeset.raw_id),
341 343 class_='tooltip',
342 344 title=tooltip_html
343 345 )
344 346
345 347 uri += '\n'
346 348 return uri
347 349 return literal(annotate_highlight(filenode, url_func, **kwargs))
348 350
349 351 def get_changeset_safe(repo, rev):
350 352 from vcs.backends.base import BaseRepository
351 353 from vcs.exceptions import RepositoryError
352 354 if not isinstance(repo, BaseRepository):
353 355 raise Exception('You must pass an Repository '
354 356 'object as first argument got %s', type(repo))
355 357
356 358 try:
357 359 cs = repo.get_changeset(rev)
358 360 except RepositoryError:
359 361 from rhodecode.lib.utils import EmptyChangeset
360 362 cs = EmptyChangeset()
361 363 return cs
362 364
363 365
364 366 def is_following_repo(repo_name, user_id):
365 367 from rhodecode.model.scm import ScmModel
366 368 return ScmModel().is_following_repo(repo_name, user_id)
367 369
368 370 flash = _Flash()
369 371
370 372
371 373 #==============================================================================
372 374 # MERCURIAL FILTERS available via h.
373 375 #==============================================================================
374 376 from mercurial import util
375 377 from mercurial.templatefilters import person as _person
376 378
377 379 def _age(curdate):
378 380 """turns a datetime into an age string."""
379 381
380 382 if not curdate:
381 383 return ''
382 384
383 from datetime import timedelta, datetime
384
385 385 agescales = [("year", 3600 * 24 * 365),
386 386 ("month", 3600 * 24 * 30),
387 387 ("day", 3600 * 24),
388 388 ("hour", 3600),
389 389 ("minute", 60),
390 390 ("second", 1), ]
391 391
392 392 age = datetime.now() - curdate
393 393 age_seconds = (age.days * agescales[2][1]) + age.seconds
394 394 pos = 1
395 395 for scale in agescales:
396 396 if scale[1] <= age_seconds:
397 397 if pos == 6:pos = 5
398 398 return time_ago_in_words(curdate, agescales[pos][0]) + ' ' + _('ago')
399 399 pos += 1
400 400
401 401 return _('just now')
402 402
403 403 age = lambda x:_age(x)
404 404 capitalize = lambda x: x.capitalize()
405 405 email = util.email
406 406 email_or_none = lambda x: util.email(x) if util.email(x) != x else None
407 407 person = lambda x: _person(x)
408 408 short_id = lambda x: x[:12]
409 409
410 410
411 411 def bool2icon(value):
412 412 """Returns True/False values represented as small html image of true/false
413 413 icons
414 414
415 415 :param value: bool value
416 416 """
417 417
418 418 if value is True:
419 419 return HTML.tag('img', src=url("/images/icons/accept.png"),
420 420 alt=_('True'))
421 421
422 422 if value is False:
423 423 return HTML.tag('img', src=url("/images/icons/cancel.png"),
424 424 alt=_('False'))
425 425
426 426 return value
427 427
428 428
429 429 def action_parser(user_log, feed=False):
430 430 """This helper will action_map the specified string action into translated
431 431 fancy names with icons and links
432 432
433 433 :param user_log: user log instance
434 434 :param feed: use output for feeds (no html and fancy icons)
435 435 """
436 436
437 437 action = user_log.action
438 438 action_params = ' '
439 439
440 440 x = action.split(':')
441 441
442 442 if len(x) > 1:
443 443 action, action_params = x
444 444
445 445 def get_cs_links():
446 446 revs_limit = 5 #display this amount always
447 447 revs_top_limit = 50 #show upto this amount of changesets hidden
448 448 revs = action_params.split(',')
449 449 repo_name = user_log.repository.repo_name
450 450
451 451 from rhodecode.model.scm import ScmModel
452 452 repo, dbrepo = ScmModel().get(repo_name, retval='repo',
453 453 invalidation_list=[])
454 454
455 455 message = lambda rev: get_changeset_safe(repo, rev).message
456 456
457 457 cs_links = " " + ', '.join ([link_to(rev,
458 458 url('changeset_home',
459 459 repo_name=repo_name,
460 460 revision=rev), title=tooltip(message(rev)),
461 461 class_='tooltip') for rev in revs[:revs_limit] ])
462 462
463 463 compare_view = (' <div class="compare_view tooltip" title="%s">'
464 464 '<a href="%s">%s</a> '
465 465 '</div>' % (_('Show all combined changesets %s->%s' \
466 466 % (revs[0], revs[-1])),
467 467 url('changeset_home', repo_name=repo_name,
468 468 revision='%s...%s' % (revs[0], revs[-1])
469 469 ),
470 470 _('compare view'))
471 471 )
472 472
473 473 if len(revs) > revs_limit:
474 474 uniq_id = revs[0]
475 475 html_tmpl = ('<span> %s '
476 476 '<a class="show_more" id="_%s" href="#more">%s</a> '
477 477 '%s</span>')
478 478 if not feed:
479 479 cs_links += html_tmpl % (_('and'), uniq_id, _('%s more') \
480 480 % (len(revs) - revs_limit),
481 481 _('revisions'))
482 482
483 483 if not feed:
484 484 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
485 485 else:
486 486 html_tmpl = '<span id="%s"> %s </span>'
487 487
488 488 cs_links += html_tmpl % (uniq_id, ', '.join([link_to(rev,
489 489 url('changeset_home',
490 490 repo_name=repo_name, revision=rev),
491 491 title=message(rev), class_='tooltip')
492 492 for rev in revs[revs_limit:revs_top_limit]]))
493 493 if len(revs) > 1:
494 494 cs_links += compare_view
495 495 return cs_links
496 496
497 497 def get_fork_name():
498 498 repo_name = action_params
499 499 return _('fork name ') + str(link_to(action_params, url('summary_home',
500 500 repo_name=repo_name,)))
501 501
502 502 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
503 503 'user_created_repo':(_('[created] repository'), None),
504 504 'user_forked_repo':(_('[forked] repository'), get_fork_name),
505 505 'user_updated_repo':(_('[updated] repository'), None),
506 506 'admin_deleted_repo':(_('[delete] repository'), None),
507 507 'admin_created_repo':(_('[created] repository'), None),
508 508 'admin_forked_repo':(_('[forked] repository'), None),
509 509 'admin_updated_repo':(_('[updated] repository'), None),
510 510 'push':(_('[pushed] into'), get_cs_links),
511 511 'push_remote':(_('[pulled from remote] into'), get_cs_links),
512 512 'pull':(_('[pulled] from'), None),
513 513 'started_following_repo':(_('[started following] repository'), None),
514 514 'stopped_following_repo':(_('[stopped following] repository'), None),
515 515 }
516 516
517 517 action_str = action_map.get(action, action)
518 518 if feed:
519 519 action = action_str[0].replace('[', '').replace(']', '')
520 520 else:
521 521 action = action_str[0].replace('[', '<span class="journal_highlight">')\
522 522 .replace(']', '</span>')
523 523
524 524 action_params_func = lambda :""
525 525
526 526 if callable(action_str[1]):
527 527 action_params_func = action_str[1]
528 528
529 529 return [literal(action), action_params_func]
530 530
531 531 def action_parser_icon(user_log):
532 532 action = user_log.action
533 533 action_params = None
534 534 x = action.split(':')
535 535
536 536 if len(x) > 1:
537 537 action, action_params = x
538 538
539 539 tmpl = """<img src="%s%s" alt="%s"/>"""
540 540 map = {'user_deleted_repo':'database_delete.png',
541 541 'user_created_repo':'database_add.png',
542 542 'user_forked_repo':'arrow_divide.png',
543 543 'user_updated_repo':'database_edit.png',
544 544 'admin_deleted_repo':'database_delete.png',
545 545 'admin_created_repo':'database_add.png',
546 546 'admin_forked_repo':'arrow_divide.png',
547 547 'admin_updated_repo':'database_edit.png',
548 548 'push':'script_add.png',
549 549 'push_remote':'connect.png',
550 550 'pull':'down_16.png',
551 551 'started_following_repo':'heart_add.png',
552 552 'stopped_following_repo':'heart_delete.png',
553 553 }
554 554 return literal(tmpl % ((url('/images/icons/')),
555 555 map.get(action, action), action))
556 556
557 557
558 558 #==============================================================================
559 559 # PERMS
560 560 #==============================================================================
561 561 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
562 562 HasRepoPermissionAny, HasRepoPermissionAll
563 563
564 564 #==============================================================================
565 565 # GRAVATAR URL
566 566 #==============================================================================
567 567
568 568 def gravatar_url(email_address, size=30):
569 569 if not str2bool(config['app_conf'].get('use_gravatar')):
570 570 return "/images/user%s.png" % size
571 571
572 572 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
573 573 default = 'identicon'
574 574 baseurl_nossl = "http://www.gravatar.com/avatar/"
575 575 baseurl_ssl = "https://secure.gravatar.com/avatar/"
576 576 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
577 577
578 578 if isinstance(email_address, unicode):
579 579 #hashlib crashes on unicode items
580 580 email_address = email_address.encode('utf8', 'replace')
581 581 # construct the url
582 582 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
583 583 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
584 584
585 585 return gravatar_url
586 586
587 587
588 588 #==============================================================================
589 589 # REPO PAGER
590 590 #==============================================================================
591 591 class RepoPage(Page):
592 592
593 593 def __init__(self, collection, page=1, items_per_page=20,
594 594 item_count=None, url=None, branch_name=None, **kwargs):
595 595
596 596 """Create a "RepoPage" instance. special pager for paging
597 597 repository
598 598 """
599 599 self._url_generator = url
600 600
601 601 # Safe the kwargs class-wide so they can be used in the pager() method
602 602 self.kwargs = kwargs
603 603
604 604 # Save a reference to the collection
605 605 self.original_collection = collection
606 606
607 607 self.collection = collection
608 608
609 609 # The self.page is the number of the current page.
610 610 # The first page has the number 1!
611 611 try:
612 612 self.page = int(page) # make it int() if we get it as a string
613 613 except (ValueError, TypeError):
614 614 self.page = 1
615 615
616 616 self.items_per_page = items_per_page
617 617
618 618 # Unless the user tells us how many items the collections has
619 619 # we calculate that ourselves.
620 620 if item_count is not None:
621 621 self.item_count = item_count
622 622 else:
623 623 self.item_count = len(self.collection)
624 624
625 625 # Compute the number of the first and last available page
626 626 if self.item_count > 0:
627 627 self.first_page = 1
628 628 self.page_count = ((self.item_count - 1) / self.items_per_page) + 1
629 629 self.last_page = self.first_page + self.page_count - 1
630 630
631 631 # Make sure that the requested page number is the range of valid pages
632 632 if self.page > self.last_page:
633 633 self.page = self.last_page
634 634 elif self.page < self.first_page:
635 635 self.page = self.first_page
636 636
637 637 # Note: the number of items on this page can be less than
638 638 # items_per_page if the last page is not full
639 639 self.first_item = max(0, (self.item_count) - (self.page * items_per_page))
640 640 self.last_item = ((self.item_count - 1) - items_per_page * (self.page - 1))
641 641
642 642 iterator = self.collection.get_changesets(start=self.first_item,
643 643 end=self.last_item,
644 644 reverse=True,
645 645 branch_name=branch_name)
646 646 self.items = list(iterator)
647 647
648 648 # Links to previous and next page
649 649 if self.page > self.first_page:
650 650 self.previous_page = self.page - 1
651 651 else:
652 652 self.previous_page = None
653 653
654 654 if self.page < self.last_page:
655 655 self.next_page = self.page + 1
656 656 else:
657 657 self.next_page = None
658 658
659 659 # No items available
660 660 else:
661 661 self.first_page = None
662 662 self.page_count = 0
663 663 self.last_page = None
664 664 self.first_item = None
665 665 self.last_item = None
666 666 self.previous_page = None
667 667 self.next_page = None
668 668 self.items = []
669 669
670 670 # This is a subclass of the 'list' type. Initialise the list now.
671 671 list.__init__(self, self.items)
672 672
673 673
674 def safe_unicode(str):
675 """safe unicode function. In case of UnicodeDecode error we try to return
676 unicode with errors replace, if this failes we return unicode with
677 string_escape decoding """
678
679 try:
680 u_str = unicode(str)
681 except UnicodeDecodeError:
682 try:
683 u_str = unicode(str, 'utf-8', 'replace')
684 except UnicodeDecodeError:
685 #incase we have a decode error just represent as byte string
686 u_str = unicode(str(str).encode('string_escape'))
687
688 return u_str
689
690 674 def changed_tooltip(nodes):
691 675 if nodes:
692 676 pref = ': <br/> '
693 677 suf = ''
694 678 if len(nodes) > 30:
695 679 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
696 680 return literal(pref + '<br/> '.join([x.path.decode('utf-8', 'replace') for x in nodes[:30]]) + suf)
697 681 else:
698 682 return ': ' + _('No Files')
@@ -1,237 +1,241 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.indexers.daemon
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 A deamon will read from task table and run tasks
7 7
8 8 :created_on: Jan 26, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 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
14 14 # modify it under the terms of the GNU General Public License
15 15 # as published by the Free Software Foundation; version 2
16 16 # of the License or (at your opinion) any later version of the license.
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, write to the Free Software
25 25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 26 # MA 02110-1301, USA.
27 27
28 import os
28 29 import sys
29 import os
30 import logging
30 31 import traceback
32
33 from shutil import rmtree
34 from time import mktime
35
31 36 from os.path import dirname as dn
32 37 from os.path import join as jn
33 38
34 39 #to get the rhodecode import
35 40 project_path = dn(dn(dn(dn(os.path.realpath(__file__)))))
36 41 sys.path.append(project_path)
37 42
38 43
39 44 from rhodecode.model.scm import ScmModel
40 from rhodecode.lib.helpers import safe_unicode
41 from whoosh.index import create_in, open_dir
42 from shutil import rmtree
45 from rhodecode.lib import safe_unicode
43 46 from rhodecode.lib.indexers import INDEX_EXTENSIONS, SCHEMA, IDX_NAME
44 47
45 from time import mktime
46 48 from vcs.exceptions import ChangesetError, RepositoryError
47 49
48 import logging
50 from whoosh.index import create_in, open_dir
51
52
49 53
50 54 log = logging.getLogger('whooshIndexer')
51 55 # create logger
52 56 log.setLevel(logging.DEBUG)
53 57 log.propagate = False
54 58 # create console handler and set level to debug
55 59 ch = logging.StreamHandler()
56 60 ch.setLevel(logging.DEBUG)
57 61
58 62 # create formatter
59 63 formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
60 64
61 65 # add formatter to ch
62 66 ch.setFormatter(formatter)
63 67
64 68 # add ch to logger
65 69 log.addHandler(ch)
66 70
67 71 class WhooshIndexingDaemon(object):
68 72 """
69 73 Deamon for atomic jobs
70 74 """
71 75
72 76 def __init__(self, indexname='HG_INDEX', index_location=None,
73 77 repo_location=None, sa=None, repo_list=None):
74 78 self.indexname = indexname
75 79
76 80 self.index_location = index_location
77 81 if not index_location:
78 82 raise Exception('You have to provide index location')
79 83
80 84 self.repo_location = repo_location
81 85 if not repo_location:
82 86 raise Exception('You have to provide repositories location')
83 87
84 88 self.repo_paths = ScmModel(sa).repo_scan(self.repo_location)
85 89
86 90 if repo_list:
87 91 filtered_repo_paths = {}
88 92 for repo_name, repo in self.repo_paths.items():
89 93 if repo_name in repo_list:
90 94 filtered_repo_paths[repo.name] = repo
91 95
92 96 self.repo_paths = filtered_repo_paths
93 97
94 98
95 99 self.initial = False
96 100 if not os.path.isdir(self.index_location):
97 101 os.makedirs(self.index_location)
98 102 log.info('Cannot run incremental index since it does not'
99 103 ' yet exist running full build')
100 104 self.initial = True
101 105
102 106 def get_paths(self, repo):
103 107 """recursive walk in root dir and return a set of all path in that dir
104 108 based on repository walk function
105 109 """
106 110 index_paths_ = set()
107 111 try:
108 112 tip = repo.get_changeset('tip')
109 113 for topnode, dirs, files in tip.walk('/'):
110 114 for f in files:
111 115 index_paths_.add(jn(repo.path, f.path))
112 116 for dir in dirs:
113 117 for f in files:
114 118 index_paths_.add(jn(repo.path, f.path))
115 119
116 120 except RepositoryError, e:
117 121 log.debug(traceback.format_exc())
118 122 pass
119 123 return index_paths_
120 124
121 125 def get_node(self, repo, path):
122 126 n_path = path[len(repo.path) + 1:]
123 127 node = repo.get_changeset().get_node(n_path)
124 128 return node
125 129
126 130 def get_node_mtime(self, node):
127 131 return mktime(node.last_changeset.date.timetuple())
128 132
129 133 def add_doc(self, writer, path, repo):
130 134 """Adding doc to writer this function itself fetches data from
131 135 the instance of vcs backend"""
132 136 node = self.get_node(repo, path)
133 137
134 138 #we just index the content of chosen files, and skip binary files
135 139 if node.extension in INDEX_EXTENSIONS and not node.is_binary:
136 140
137 141 u_content = node.content
138 142 if not isinstance(u_content, unicode):
139 143 log.warning(' >> %s Could not get this content as unicode '
140 144 'replacing with empty content', path)
141 145 u_content = u''
142 146 else:
143 147 log.debug(' >> %s [WITH CONTENT]' % path)
144 148
145 149 else:
146 150 log.debug(' >> %s' % path)
147 151 #just index file name without it's content
148 152 u_content = u''
149 153
150 154 writer.add_document(owner=unicode(repo.contact),
151 155 repository=safe_unicode(repo.name),
152 156 path=safe_unicode(path),
153 157 content=u_content,
154 158 modtime=self.get_node_mtime(node),
155 159 extension=node.extension)
156 160
157 161
158 162 def build_index(self):
159 163 if os.path.exists(self.index_location):
160 164 log.debug('removing previous index')
161 165 rmtree(self.index_location)
162 166
163 167 if not os.path.exists(self.index_location):
164 168 os.mkdir(self.index_location)
165 169
166 170 idx = create_in(self.index_location, SCHEMA, indexname=IDX_NAME)
167 171 writer = idx.writer()
168 172
169 173 for repo in self.repo_paths.values():
170 174 log.debug('building index @ %s' % repo.path)
171 175
172 176 for idx_path in self.get_paths(repo):
173 177 self.add_doc(writer, idx_path, repo)
174 178
175 179 log.debug('>> COMMITING CHANGES <<')
176 180 writer.commit(merge=True)
177 181 log.debug('>>> FINISHED BUILDING INDEX <<<')
178 182
179 183
180 184 def update_index(self):
181 185 log.debug('STARTING INCREMENTAL INDEXING UPDATE')
182 186
183 187 idx = open_dir(self.index_location, indexname=self.indexname)
184 188 # The set of all paths in the index
185 189 indexed_paths = set()
186 190 # The set of all paths we need to re-index
187 191 to_index = set()
188 192
189 193 reader = idx.reader()
190 194 writer = idx.writer()
191 195
192 196 # Loop over the stored fields in the index
193 197 for fields in reader.all_stored_fields():
194 198 indexed_path = fields['path']
195 199 indexed_paths.add(indexed_path)
196 200
197 201 repo = self.repo_paths[fields['repository']]
198 202
199 203 try:
200 204 node = self.get_node(repo, indexed_path)
201 205 except ChangesetError:
202 206 # This file was deleted since it was indexed
203 207 log.debug('removing from index %s' % indexed_path)
204 208 writer.delete_by_term('path', indexed_path)
205 209
206 210 else:
207 211 # Check if this file was changed since it was indexed
208 212 indexed_time = fields['modtime']
209 213 mtime = self.get_node_mtime(node)
210 214 if mtime > indexed_time:
211 215 # The file has changed, delete it and add it to the list of
212 216 # files to reindex
213 217 log.debug('adding to reindex list %s' % indexed_path)
214 218 writer.delete_by_term('path', indexed_path)
215 219 to_index.add(indexed_path)
216 220
217 221 # Loop over the files in the filesystem
218 222 # Assume we have a function that gathers the filenames of the
219 223 # documents to be indexed
220 224 for repo in self.repo_paths.values():
221 225 for path in self.get_paths(repo):
222 226 if path in to_index or path not in indexed_paths:
223 227 # This is either a file that's changed, or a new file
224 228 # that wasn't indexed before. So index it!
225 229 self.add_doc(writer, path, repo)
226 230 log.debug('re indexing %s' % path)
227 231
228 232 log.debug('>> COMMITING CHANGES <<')
229 233 writer.commit(merge=True)
230 234 log.debug('>>> FINISHED REBUILDING INDEX <<<')
231 235
232 236 def run(self, full_index=False):
233 237 """Run daemon"""
234 238 if full_index or self.initial:
235 239 self.build_index()
236 240 else:
237 241 self.update_index()
General Comments 0
You need to be logged in to leave comments. Login now