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