##// END OF EJS Templates
fixes #24, added generator that generates equally distrybuted colors. Thus skipping creating one large coloring history.
marcink -
r438:0d4fceb9 default
parent child Browse files
Show More
@@ -1,199 +1,199 b''
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 # files controller for pylons
4 4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5 5
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; version 2
9 9 # of the License or (at your opinion) any later version of the license.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 19 # MA 02110-1301, USA.
20 20 """
21 21 Created on April 21, 2010
22 22 files controller for pylons
23 23 @author: marcink
24 24 """
25 25 from mercurial import archival
26 26 from pylons import request, response, session, tmpl_context as c, url
27 27 from pylons.controllers.util import redirect
28 28 from pylons_app.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
29 29 from pylons_app.lib.base import BaseController, render
30 from pylons_app.lib.utils import EmptyChangeset, get_repo_slug
30 from pylons_app.lib.utils import EmptyChangeset
31 31 from pylons_app.model.hg_model import HgModel
32 32 from vcs.exceptions import RepositoryError, ChangesetError
33 33 from vcs.nodes import FileNode
34 34 from vcs.utils import diffs as differ
35 35 import logging
36 36 import pylons_app.lib.helpers as h
37 37 import tempfile
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41 class FilesController(BaseController):
42 42
43 43 @LoginRequired()
44 44 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
45 45 'repository.admin')
46 46 def __before__(self):
47 47 super(FilesController, self).__before__()
48 48
49 49 def index(self, repo_name, revision, f_path):
50 50 hg_model = HgModel()
51 51 c.repo = repo = hg_model.get_repo(c.repo_name)
52 52 revision = request.POST.get('at_rev', None) or revision
53 53
54 54 def get_next_rev(cur):
55 55 max_rev = len(c.repo.revisions) - 1
56 56 r = cur + 1
57 57 if r > max_rev:
58 58 r = max_rev
59 59 return r
60 60
61 61 def get_prev_rev(cur):
62 62 r = cur - 1
63 63 return r
64 64
65 65 c.f_path = f_path
66 66
67 67
68 68 try:
69 69 cur_rev = repo.get_changeset(revision).revision
70 70 prev_rev = repo.get_changeset(get_prev_rev(cur_rev)).raw_id
71 71 next_rev = repo.get_changeset(get_next_rev(cur_rev)).raw_id
72 72
73 73 c.url_prev = url('files_home', repo_name=c.repo_name,
74 74 revision=prev_rev, f_path=f_path)
75 75 c.url_next = url('files_home', repo_name=c.repo_name,
76 76 revision=next_rev, f_path=f_path)
77 77
78 78 c.changeset = repo.get_changeset(revision)
79 79
80 80
81 81 c.cur_rev = c.changeset.raw_id
82 82 c.rev_nr = c.changeset.revision
83 83 c.files_list = c.changeset.get_node(f_path)
84 84 c.file_history = self._get_history(repo, c.files_list, f_path)
85 85
86 86 except (RepositoryError, ChangesetError):
87 87 c.files_list = None
88 88
89 89 return render('files/files.html')
90 90
91 91 def rawfile(self, repo_name, revision, f_path):
92 92 hg_model = HgModel()
93 93 c.repo = hg_model.get_repo(c.repo_name)
94 94 file_node = c.repo.get_changeset(revision).get_node(f_path)
95 95 response.content_type = file_node.mimetype
96 96 response.content_disposition = 'attachment; filename=%s' \
97 97 % f_path.split('/')[-1]
98 98 return file_node.content
99 99
100 100 def annotate(self, repo_name, revision, f_path):
101 101 hg_model = HgModel()
102 102 c.repo = hg_model.get_repo(c.repo_name)
103 103 cs = c.repo.get_changeset(revision)
104 104 c.file = cs.get_node(f_path)
105 105 c.file_msg = cs.get_file_message(f_path)
106 106 c.cur_rev = cs.raw_id
107 107 c.rev_nr = cs.revision
108 108 c.f_path = f_path
109 109
110 110 return render('files/files_annotate.html')
111 111
112 112 def archivefile(self, repo_name, revision, fileformat):
113 113 archive_specs = {
114 114 '.tar.bz2': ('application/x-tar', 'tbz2'),
115 115 '.tar.gz': ('application/x-tar', 'tgz'),
116 116 '.zip': ('application/zip', 'zip'),
117 117 }
118 118 if not archive_specs.has_key(fileformat):
119 119 return 'Unknown archive type %s' % fileformat
120 120
121 121 def read_in_chunks(file_object, chunk_size=1024 * 40):
122 122 """Lazy function (generator) to read a file piece by piece.
123 123 Default chunk size: 40k."""
124 124 while True:
125 125 data = file_object.read(chunk_size)
126 126 if not data:
127 127 break
128 128 yield data
129 129
130 130 archive = tempfile.TemporaryFile()
131 131 repo = HgModel().get_repo(repo_name).repo
132 132 fname = '%s-%s%s' % (repo_name, revision, fileformat)
133 133 archival.archive(repo, archive, revision, archive_specs[fileformat][1],
134 134 prefix='%s-%s' % (repo_name, revision))
135 135 response.content_type = archive_specs[fileformat][0]
136 136 response.content_disposition = 'attachment; filename=%s' % fname
137 137 archive.seek(0)
138 138 return read_in_chunks(archive)
139 139
140 140 def diff(self, repo_name, f_path):
141 141 hg_model = HgModel()
142 142 diff1 = request.GET.get('diff1')
143 143 diff2 = request.GET.get('diff2')
144 144 c.action = request.GET.get('diff')
145 145 c.no_changes = diff1 == diff2
146 146 c.f_path = f_path
147 147 c.repo = hg_model.get_repo(c.repo_name)
148 148
149 149 try:
150 150 if diff1 not in ['', None, 'None', '0' * 12]:
151 151 c.changeset_1 = c.repo.get_changeset(diff1)
152 152 node1 = c.changeset_1.get_node(f_path)
153 153 else:
154 154 c.changeset_1 = EmptyChangeset()
155 155 node1 = FileNode('.', '')
156 156 if diff2 not in ['', None, 'None', '0' * 12]:
157 157 c.changeset_2 = c.repo.get_changeset(diff2)
158 158 node2 = c.changeset_2.get_node(f_path)
159 159 else:
160 160 c.changeset_2 = EmptyChangeset()
161 161 node2 = FileNode('.', '')
162 162 except RepositoryError:
163 163 return redirect(url('files_home',
164 164 repo_name=c.repo_name, f_path=f_path))
165 165
166 166 c.diff1 = 'r%s:%s' % (c.changeset_1.revision, c.changeset_1.raw_id)
167 167 c.diff2 = 'r%s:%s' % (c.changeset_2.revision, c.changeset_2.raw_id)
168 168 f_udiff = differ.get_udiff(node1, node2)
169 169
170 170 diff = differ.DiffProcessor(f_udiff)
171 171
172 172 if c.action == 'download':
173 173 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
174 174 response.content_type = 'text/plain'
175 175 response.content_disposition = 'attachment; filename=%s' \
176 176 % diff_name
177 177 return diff.raw_diff()
178 178
179 179 elif c.action == 'raw':
180 180 c.cur_diff = '<pre class="raw">%s</pre>' % h.escape(diff.raw_diff())
181 181 elif c.action == 'diff':
182 182 c.cur_diff = diff.as_html()
183 183 else:
184 184 #default option
185 185 c.cur_diff = diff.as_html()
186 186
187 187 if not c.cur_diff: c.no_changes = True
188 188 return render('files/file_diff.html')
189 189
190 190 def _get_history(self, repo, node, f_path):
191 191 from vcs.nodes import NodeKind
192 192 if not node.kind is NodeKind.FILE:
193 193 return []
194 194 changesets = node.history
195 195 hist_l = []
196 196 for chs in changesets:
197 197 n_desc = 'r%s:%s' % (chs.revision, chs._short)
198 198 hist_l.append((chs._short, n_desc,))
199 199 return hist_l
@@ -1,33 +1,31 b''
1 1 """The application's Globals object"""
2 2
3 3 from beaker.cache import CacheManager
4 4 from beaker.util import parse_cache_config_options
5 5 from vcs.utils.lazy import LazyProperty
6 6
7 7 class Globals(object):
8 8 """Globals acts as a container for objects available throughout the
9 9 life of the application
10 10
11 11 """
12 12
13 13 def __init__(self, config):
14 14 """One instance of Globals is created during application
15 15 initialization and is available during requests via the
16 16 'app_globals' variable
17 17
18 18 """
19 19 self.cache = CacheManager(**parse_cache_config_options(config))
20 self.changeset_annotation_colors = {}
21 20 self.available_permissions = None # propagated after init_model
22 self.app_title = None # propagated after init_model
23 21 self.baseui = None # propagated after init_model
24 22
25 23 @LazyProperty
26 24 def paths(self):
27 25 if self.baseui:
28 26 return self.baseui.configitems('paths')
29 27
30 28 @LazyProperty
31 29 def base_path(self):
32 30 if self.baseui:
33 31 return self.paths[0][1].replace('*', '')
@@ -1,325 +1,338 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 from pygments.formatters import HtmlFormatter
7 7 from pygments import highlight as code_highlight
8 8 from pylons import url, app_globals as g
9 9 from pylons.i18n.translation import _, ungettext
10 10 from vcs.utils.annotate import annotate_highlight
11 11 from webhelpers.html import literal, HTML, escape
12 12 from webhelpers.html.tools import *
13 13 from webhelpers.html.builder import make_tag
14 14 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
15 15 end_form, file, form, hidden, image, javascript_link, link_to, link_to_if, \
16 16 link_to_unless, ol, required_legend, select, stylesheet_link, submit, text, \
17 17 password, textarea, title, ul, xml_declaration, radio
18 18 from webhelpers.html.tools import auto_link, button_to, highlight, js_obfuscate, \
19 19 mail_to, strip_links, strip_tags, tag_re
20 20 from webhelpers.number import format_byte_size, format_bit_size
21 21 from webhelpers.pylonslib import Flash as _Flash
22 22 from webhelpers.pylonslib.secure_form import secure_form
23 23 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
24 24 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
25 25 replace_whitespace, urlify, truncate, wrap_paragraphs
26 26
27
28 27 #Custom helpers here :)
29 28 class _Link(object):
30 29 '''
31 30 Make a url based on label and url with help of url_for
32 31 @param label:name of link if not defined url is used
33 32 @param url: the url for link
34 33 '''
35 34
36 35 def __call__(self, label='', *url_, **urlargs):
37 36 if label is None or '':
38 37 label = url
39 38 link_fn = link_to(label, url(*url_, **urlargs))
40 39 return link_fn
41 40
42 41 link = _Link()
43 42
44 43 class _GetError(object):
45 44
46 45 def __call__(self, field_name, form_errors):
47 46 tmpl = """<span class="error_msg">%s</span>"""
48 47 if form_errors and form_errors.has_key(field_name):
49 48 return literal(tmpl % form_errors.get(field_name))
50 49
51 50 get_error = _GetError()
52 51
53 52 def recursive_replace(str, replace=' '):
54 53 """
55 54 Recursive replace of given sign to just one instance
56 55 @param str: given string
57 56 @param replace:char to find and replace multiple instances
58 57
59 58 Examples::
60 59 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
61 60 'Mighty-Mighty-Bo-sstones'
62 61 """
63 62
64 63 if str.find(replace * 2) == -1:
65 64 return str
66 65 else:
67 66 str = str.replace(replace * 2, replace)
68 67 return recursive_replace(str, replace)
69 68
70 69 class _ToolTip(object):
71 70
72 71 def __call__(self, tooltip_title, trim_at=50):
73 72 """
74 73 Special function just to wrap our text into nice formatted autowrapped
75 74 text
76 75 @param tooltip_title:
77 76 """
78 77
79 78 return literal(wrap_paragraphs(tooltip_title, trim_at)\
80 79 .replace('\n', '<br/>'))
81 80
82 81 def activate(self):
83 82 """
84 83 Adds tooltip mechanism to the given Html all tooltips have to have
85 84 set class tooltip and set attribute tooltip_title.
86 85 Then a tooltip will be generated based on that
87 86 All with yui js tooltip
88 87 """
89 88
90 89 js = '''
91 90 YAHOO.util.Event.onDOMReady(function(){
92 91 function toolTipsId(){
93 92 var ids = [];
94 93 var tts = YAHOO.util.Dom.getElementsByClassName('tooltip');
95 94
96 95 for (var i = 0; i < tts.length; i++) {
97 96 //if element doesn not have and id autgenerate one for tooltip
98 97
99 98 if (!tts[i].id){
100 99 tts[i].id='tt'+i*100;
101 100 }
102 101 ids.push(tts[i].id);
103 102 }
104 103 return ids
105 104 };
106 105 var myToolTips = new YAHOO.widget.Tooltip("tooltip", {
107 106 context: toolTipsId(),
108 107 monitorresize:false,
109 108 xyoffset :[0,0],
110 109 autodismissdelay:300000,
111 110 hidedelay:5,
112 111 showdelay:20,
113 112 });
114 113
115 114 //Mouse Over event disabled for new repositories since they dont
116 115 //have last commit message
117 116 myToolTips.contextMouseOverEvent.subscribe(
118 117 function(type, args) {
119 118 var context = args[0];
120 119 var txt = context.getAttribute('tooltip_title');
121 120 if(txt){
122 121 return true;
123 122 }
124 123 else{
125 124 return false;
126 125 }
127 126 });
128 127
129 128
130 129 // Set the text for the tooltip just before we display it. Lazy method
131 130 myToolTips.contextTriggerEvent.subscribe(
132 131 function(type, args) {
133 132
134 133
135 134 var context = args[0];
136 135
137 136 var txt = context.getAttribute('tooltip_title');
138 137 this.cfg.setProperty("text", txt);
139 138
140 139
141 140 // positioning of tooltip
142 141 var tt_w = this.element.clientWidth;
143 142 var tt_h = this.element.clientHeight;
144 143
145 144 var context_w = context.offsetWidth;
146 145 var context_h = context.offsetHeight;
147 146
148 147 var pos_x = YAHOO.util.Dom.getX(context);
149 148 var pos_y = YAHOO.util.Dom.getY(context);
150 149
151 150 var display_strategy = 'top';
152 151 var xy_pos = [0,0];
153 152 switch (display_strategy){
154 153
155 154 case 'top':
156 155 var cur_x = (pos_x+context_w/2)-(tt_w/2);
157 156 var cur_y = pos_y-tt_h-4;
158 157 xy_pos = [cur_x,cur_y];
159 158 break;
160 159 case 'bottom':
161 160 var cur_x = (pos_x+context_w/2)-(tt_w/2);
162 161 var cur_y = pos_y+context_h+4;
163 162 xy_pos = [cur_x,cur_y];
164 163 break;
165 164 case 'left':
166 165 var cur_x = (pos_x-tt_w-4);
167 166 var cur_y = pos_y-((tt_h/2)-context_h/2);
168 167 xy_pos = [cur_x,cur_y];
169 168 break;
170 169 case 'right':
171 170 var cur_x = (pos_x+context_w+4);
172 171 var cur_y = pos_y-((tt_h/2)-context_h/2);
173 172 xy_pos = [cur_x,cur_y];
174 173 break;
175 174 default:
176 175 var cur_x = (pos_x+context_w/2)-(tt_w/2);
177 176 var cur_y = pos_y-tt_h-4;
178 177 xy_pos = [cur_x,cur_y];
179 178 break;
180 179
181 180 }
182 181
183 182 this.cfg.setProperty("xy",xy_pos);
184 183
185 184 });
186 185
187 186 //Mouse out
188 187 myToolTips.contextMouseOutEvent.subscribe(
189 188 function(type, args) {
190 189 var context = args[0];
191 190
192 191 });
193 192 });
194 193 '''
195 194 return literal(js)
196 195
197 196 tooltip = _ToolTip()
198 197
199 198 class _FilesBreadCrumbs(object):
200 199
201 200 def __call__(self, repo_name, rev, paths):
202 201 url_l = [link_to(repo_name, url('files_home',
203 202 repo_name=repo_name,
204 203 revision=rev, f_path=''))]
205 204 paths_l = paths.split('/')
206 205
207 206 for cnt, p in enumerate(paths_l, 1):
208 207 if p != '':
209 208 url_l.append(link_to(p, url('files_home',
210 209 repo_name=repo_name,
211 210 revision=rev,
212 211 f_path='/'.join(paths_l[:cnt]))))
213 212
214 213 return literal(' / '.join(url_l))
215 214
216 215 files_breadcrumbs = _FilesBreadCrumbs()
217 216
218 217 def pygmentize(filenode, **kwargs):
219 218 """
220 219 pygmentize function using pygments
221 220 @param filenode:
222 221 """
223 222 return literal(code_highlight(filenode.content,
224 223 filenode.lexer, HtmlFormatter(**kwargs)))
225 224
226 225 def pygmentize_annotation(filenode, **kwargs):
227 226 """
228 227 pygmentize function for annotation
229 228 @param filenode:
230 229 """
231 230
232 color_dict = g.changeset_annotation_colors
231 color_dict = {}
233 232 def gen_color():
234 import random
235 return [str(random.randrange(10, 235)) for _ in xrange(3)]
233 """generator for getting 10k of evenly distibuted colors using hsv color
234 and golden ratio.
235 """
236 import colorsys
237 n = 10000
238 golden_ratio = 0.618033988749895
239 h = 0.22717784590367374
240 #generate 10k nice web friendly colors in the same order
241 for c in xrange(n):
242 h +=golden_ratio
243 h %= 1
244 HSV_tuple = [h, 0.95, 0.95]
245 RGB_tuple = colorsys.hsv_to_rgb(*HSV_tuple)
246 yield map(lambda x:str(int(x*256)),RGB_tuple)
247
248 cgenerator = gen_color()
249
236 250 def get_color_string(cs):
237 251 if color_dict.has_key(cs):
238 252 col = color_dict[cs]
239 253 else:
240 color_dict[cs] = gen_color()
241 col = color_dict[cs]
254 col = color_dict[cs] = cgenerator.next()
242 255 return "color: rgb(%s) ! important;" % (', '.join(col))
243 256
244 257 def url_func(changeset):
245 258 tooltip_html = "<div style='font-size:0.8em'><b>Author:</b>"+\
246 259 " %s<br/><b>Date:</b> %s</b><br/><b>Message:</b> %s<br/></div>"
247 260
248 261 tooltip_html = tooltip_html % (changeset.author,
249 262 changeset.date,
250 263 tooltip(changeset.message))
251 lnk_format = 'r%s:%s' % (changeset.revision,
264 lnk_format = 'r%-5s:%s' % (changeset.revision,
252 265 changeset.raw_id)
253 266 uri = link_to(
254 267 lnk_format,
255 268 url('changeset_home', repo_name=changeset.repository.name,
256 269 revision=changeset.raw_id),
257 270 style=get_color_string(changeset.raw_id),
258 271 class_='tooltip',
259 272 tooltip_title=tooltip_html
260 273 )
261 274
262 275 uri += '\n'
263 276 return uri
264 277 return literal(annotate_highlight(filenode, url_func, **kwargs))
265 278
266 279 def repo_name_slug(value):
267 280 """
268 281 Return slug of name of repository
269 282 """
270 283 slug = urlify(value)
271 284 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|:""":
272 285 slug = slug.replace(c, '-')
273 286 slug = recursive_replace(slug, '-')
274 287 return slug
275 288
276 289 flash = _Flash()
277 290
278 291
279 292 #===============================================================================
280 293 # MERCURIAL FILTERS available via h.
281 294 #===============================================================================
282 295 from mercurial import util
283 296 from mercurial.templatefilters import age as _age, person as _person
284 297
285 298 age = lambda x:_age(x)
286 299 capitalize = lambda x: x.capitalize()
287 300 date = lambda x: util.datestr(x)
288 301 email = util.email
289 302 email_or_none = lambda x: util.email(x) if util.email(x) != x else None
290 303 person = lambda x: _person(x)
291 304 hgdate = lambda x: "%d %d" % x
292 305 isodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M %1%2')
293 306 isodatesec = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2')
294 307 localdate = lambda x: (x[0], util.makedate()[1])
295 308 rfc822date = lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S %1%2")
296 309 rfc3339date = lambda x: util.datestr(x, "%Y-%m-%dT%H:%M:%S%1:%2")
297 310 time_ago = lambda x: util.datestr(_age(x), "%a, %d %b %Y %H:%M:%S %1%2")
298 311
299 312
300 313 #===============================================================================
301 314 # PERMS
302 315 #===============================================================================
303 316 from pylons_app.lib.auth import HasPermissionAny, HasPermissionAll, \
304 317 HasRepoPermissionAny, HasRepoPermissionAll
305 318
306 319 #===============================================================================
307 320 # GRAVATAR URL
308 321 #===============================================================================
309 322 import hashlib
310 323 import urllib
311 324 from pylons import request
312 325
313 326 def gravatar_url(email_address, size=30):
314 327 ssl_enabled = 'https' == request.environ.get('HTTP_X_URL_SCHEME')
315 328 default = 'identicon'
316 329 baseurl_nossl = "http://www.gravatar.com/avatar/"
317 330 baseurl_ssl = "https://secure.gravatar.com/avatar/"
318 331 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
319 332
320 333
321 334 # construct the url
322 335 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
323 336 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
324 337
325 338 return gravatar_url
General Comments 0
You need to be logged in to leave comments. Login now