##// END OF EJS Templates
removed decodes, thus it should be implemented on vcs side...
*** failed to import extension hggit: No module named hggit -
r428:dee0e7eb default
parent child Browse files
Show More
@@ -1,142 +1,142 b''
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 # summary 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 18, 2010
22 22 summary controller for pylons
23 23 @author: marcink
24 24 """
25 25 from datetime import datetime, timedelta
26 26 from pylons import tmpl_context as c, request
27 27 from pylons_app.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 28 from pylons_app.lib.base import BaseController, render
29 29 from pylons_app.lib.helpers import person
30 30 from pylons_app.lib.utils import OrderedDict
31 31 from pylons_app.model.hg_model import HgModel
32 32 from time import mktime
33 33 from webhelpers.paginate import Page
34 34 import calendar
35 35 import logging
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39 class SummaryController(BaseController):
40 40
41 41 @LoginRequired()
42 42 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
43 43 'repository.admin')
44 44 def __before__(self):
45 45 super(SummaryController, self).__before__()
46 46
47 47 def index(self):
48 48 hg_model = HgModel()
49 49 c.repo_info = hg_model.get_repo(c.repo_name)
50 50 c.repo_changesets = Page(list(c.repo_info[:10]), page=1, items_per_page=20)
51 51 e = request.environ
52 52 uri = u'%(protocol)s://%(user)s@%(host)s/%(repo_name)s' % {
53 53 'protocol': e.get('wsgi.url_scheme'),
54 54 'user':str(c.hg_app_user.username),
55 55 'host':e.get('HTTP_HOST'),
56 56 'repo_name':c.repo_name, }
57 57 c.clone_repo_url = uri
58 58 c.repo_tags = OrderedDict()
59 59 for name, hash in c.repo_info.tags.items()[:10]:
60 60 c.repo_tags[name] = c.repo_info.get_changeset(hash)
61 61
62 62 c.repo_branches = OrderedDict()
63 63 for name, hash in c.repo_info.branches.items()[:10]:
64 64 c.repo_branches[name] = c.repo_info.get_changeset(hash)
65 65
66 66 c.commit_data = self.__get_commit_stats(c.repo_info)
67 67
68 68 return render('summary/summary.html')
69 69
70 70
71 71
72 72 def __get_commit_stats(self, repo):
73 73 aggregate = OrderedDict()
74 74
75 75 #graph range
76 76 td = datetime.today() + timedelta(days=1)
77 77 y = td.year
78 78 m = td.month
79 79 d = td.day
80 80 c.ts_min = mktime((y, (td - timedelta(days=calendar.mdays[m] - 1)).month,
81 81 d, 0, 0, 0, 0, 0, 0,))
82 82 c.ts_max = mktime((y, m, d, 0, 0, 0, 0, 0, 0,))
83 83
84 84
85 85 def author_key_cleaner(k):
86 86 k = person(k)
87 87 k = k.replace('"', "'") #for js data compatibilty
88 88 return k
89 89
90 90 for cs in repo:
91 91 k = '%s-%s-%s' % (cs.date.timetuple()[0], cs.date.timetuple()[1],
92 92 cs.date.timetuple()[2])
93 93 timetupple = [int(x) for x in k.split('-')]
94 94 timetupple.extend([0 for _ in xrange(6)])
95 95 k = mktime(timetupple)
96 96 if aggregate.has_key(author_key_cleaner(cs.author)):
97 97 if aggregate[author_key_cleaner(cs.author)].has_key(k):
98 98 aggregate[author_key_cleaner(cs.author)][k]["commits"] += 1
99 99 aggregate[author_key_cleaner(cs.author)][k]["added"] += len(cs.added)
100 100 aggregate[author_key_cleaner(cs.author)][k]["changed"] += len(cs.changed)
101 101 aggregate[author_key_cleaner(cs.author)][k]["removed"] += len(cs.removed)
102 102
103 103 else:
104 104 #aggregate[author_key_cleaner(cs.author)].update(dates_range)
105 105 if k >= c.ts_min and k <= c.ts_max:
106 106 aggregate[author_key_cleaner(cs.author)][k] = {}
107 107 aggregate[author_key_cleaner(cs.author)][k]["commits"] = 1
108 108 aggregate[author_key_cleaner(cs.author)][k]["added"] = len(cs.added)
109 109 aggregate[author_key_cleaner(cs.author)][k]["changed"] = len(cs.changed)
110 110 aggregate[author_key_cleaner(cs.author)][k]["removed"] = len(cs.removed)
111 111
112 112 else:
113 113 if k >= c.ts_min and k <= c.ts_max:
114 114 aggregate[author_key_cleaner(cs.author)] = OrderedDict()
115 115 #aggregate[author_key_cleaner(cs.author)].update(dates_range)
116 116 aggregate[author_key_cleaner(cs.author)][k] = {}
117 117 aggregate[author_key_cleaner(cs.author)][k]["commits"] = 1
118 118 aggregate[author_key_cleaner(cs.author)][k]["added"] = len(cs.added)
119 119 aggregate[author_key_cleaner(cs.author)][k]["changed"] = len(cs.changed)
120 120 aggregate[author_key_cleaner(cs.author)][k]["removed"] = len(cs.removed)
121 121
122 122 d = ''
123 123 tmpl0 = u""""%s":%s"""
124 124 tmpl1 = u"""{label:"%s",data:%s,schema:["commits"]},"""
125 125 for author in aggregate:
126 126
127 d += tmpl0 % (author.decode('utf8'),
127 d += tmpl0 % (author,
128 128 tmpl1 \
129 % (author.decode('utf8'),
129 % (author,
130 130 [{"time":x,
131 131 "commits":aggregate[author][x]['commits'],
132 132 "added":aggregate[author][x]['added'],
133 133 "changed":aggregate[author][x]['changed'],
134 134 "removed":aggregate[author][x]['removed'],
135 135 } for x in aggregate[author]]))
136 136 if d == '':
137 137 d = '"%s":{label:"%s",data:[[0,1],]}' \
138 138 % (author_key_cleaner(repo.contact),
139 139 author_key_cleaner(repo.contact))
140 140 return d
141 141
142 142
@@ -1,323 +1,325 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 27
28 28 #Custom helpers here :)
29 29 class _Link(object):
30 30 '''
31 31 Make a url based on label and url with help of url_for
32 32 @param label:name of link if not defined url is used
33 33 @param url: the url for link
34 34 '''
35 35
36 36 def __call__(self, label='', *url_, **urlargs):
37 37 if label is None or '':
38 38 label = url
39 39 link_fn = link_to(label, url(*url_, **urlargs))
40 40 return link_fn
41 41
42 42 link = _Link()
43 43
44 44 class _GetError(object):
45 45
46 46 def __call__(self, field_name, form_errors):
47 47 tmpl = """<span class="error_msg">%s</span>"""
48 48 if form_errors and form_errors.has_key(field_name):
49 49 return literal(tmpl % form_errors.get(field_name))
50 50
51 51 get_error = _GetError()
52 52
53 53 def recursive_replace(str, replace=' '):
54 54 """
55 55 Recursive replace of given sign to just one instance
56 56 @param str: given string
57 57 @param replace:char to find and replace multiple instances
58 58
59 59 Examples::
60 60 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
61 61 'Mighty-Mighty-Bo-sstones'
62 62 """
63 63
64 64 if str.find(replace * 2) == -1:
65 65 return str
66 66 else:
67 67 str = str.replace(replace * 2, replace)
68 68 return recursive_replace(str, replace)
69 69
70 70 class _ToolTip(object):
71 71
72 72 def __call__(self, tooltip_title, trim_at=50):
73 73 """
74 74 Special function just to wrap our text into nice formatted autowrapped
75 75 text
76 76 @param tooltip_title:
77 77 """
78 78
79 79 return literal(wrap_paragraphs(tooltip_title, trim_at)\
80 80 .replace('\n', '<br/>'))
81 81
82 82 def activate(self):
83 83 """
84 84 Adds tooltip mechanism to the given Html all tooltips have to have
85 85 set class tooltip and set attribute tooltip_title.
86 86 Then a tooltip will be generated based on that
87 87 All with yui js tooltip
88 88 """
89 89
90 90 js = '''
91 91 YAHOO.util.Event.onDOMReady(function(){
92 92 function toolTipsId(){
93 93 var ids = [];
94 94 var tts = YAHOO.util.Dom.getElementsByClassName('tooltip');
95 95
96 96 for (var i = 0; i < tts.length; i++) {
97 97 //if element doesn not have and id autgenerate one for tooltip
98 98
99 99 if (!tts[i].id){
100 100 tts[i].id='tt'+i*100;
101 101 }
102 102 ids.push(tts[i].id);
103 103 }
104 104 return ids
105 105 };
106 106 var myToolTips = new YAHOO.widget.Tooltip("tooltip", {
107 107 context: toolTipsId(),
108 108 monitorresize:false,
109 109 xyoffset :[0,0],
110 110 autodismissdelay:300000,
111 111 hidedelay:5,
112 112 showdelay:20,
113 113 });
114 114
115 115 //Mouse Over event disabled for new repositories since they dont
116 116 //have last commit message
117 117 myToolTips.contextMouseOverEvent.subscribe(
118 118 function(type, args) {
119 119 var context = args[0];
120 120 var txt = context.getAttribute('tooltip_title');
121 121 if(txt){
122 122 return true;
123 123 }
124 124 else{
125 125 return false;
126 126 }
127 127 });
128 128
129 129
130 130 // Set the text for the tooltip just before we display it. Lazy method
131 131 myToolTips.contextTriggerEvent.subscribe(
132 132 function(type, args) {
133 133
134 134
135 135 var context = args[0];
136 136
137 137 var txt = context.getAttribute('tooltip_title');
138 138 this.cfg.setProperty("text", txt);
139 139
140 140
141 141 // positioning of tooltip
142 142 var tt_w = this.element.clientWidth;
143 143 var tt_h = this.element.clientHeight;
144 144
145 145 var context_w = context.offsetWidth;
146 146 var context_h = context.offsetHeight;
147 147
148 148 var pos_x = YAHOO.util.Dom.getX(context);
149 149 var pos_y = YAHOO.util.Dom.getY(context);
150 150
151 151 var display_strategy = 'top';
152 152 var xy_pos = [0,0];
153 153 switch (display_strategy){
154 154
155 155 case 'top':
156 156 var cur_x = (pos_x+context_w/2)-(tt_w/2);
157 157 var cur_y = pos_y-tt_h-4;
158 158 xy_pos = [cur_x,cur_y];
159 159 break;
160 160 case 'bottom':
161 161 var cur_x = (pos_x+context_w/2)-(tt_w/2);
162 162 var cur_y = pos_y+context_h+4;
163 163 xy_pos = [cur_x,cur_y];
164 164 break;
165 165 case 'left':
166 166 var cur_x = (pos_x-tt_w-4);
167 167 var cur_y = pos_y-((tt_h/2)-context_h/2);
168 168 xy_pos = [cur_x,cur_y];
169 169 break;
170 170 case 'right':
171 171 var cur_x = (pos_x+context_w+4);
172 172 var cur_y = pos_y-((tt_h/2)-context_h/2);
173 173 xy_pos = [cur_x,cur_y];
174 174 break;
175 175 default:
176 176 var cur_x = (pos_x+context_w/2)-(tt_w/2);
177 177 var cur_y = pos_y-tt_h-4;
178 178 xy_pos = [cur_x,cur_y];
179 179 break;
180 180
181 181 }
182 182
183 183 this.cfg.setProperty("xy",xy_pos);
184 184
185 185 });
186 186
187 187 //Mouse out
188 188 myToolTips.contextMouseOutEvent.subscribe(
189 189 function(type, args) {
190 190 var context = args[0];
191 191
192 192 });
193 193 });
194 194 '''
195 195 return literal(js)
196 196
197 197 tooltip = _ToolTip()
198 198
199 199 class _FilesBreadCrumbs(object):
200 200
201 201 def __call__(self, repo_name, rev, paths):
202 202 url_l = [link_to(repo_name, url('files_home',
203 203 repo_name=repo_name,
204 204 revision=rev, f_path=''))]
205 205 paths_l = paths.split('/')
206 206
207 207 for cnt, p in enumerate(paths_l, 1):
208 208 if p != '':
209 209 url_l.append(link_to(p, url('files_home',
210 210 repo_name=repo_name,
211 211 revision=rev,
212 212 f_path='/'.join(paths_l[:cnt]))))
213 213
214 214 return literal(' / '.join(url_l))
215 215
216 216 files_breadcrumbs = _FilesBreadCrumbs()
217 217
218 218 def pygmentize(filenode, **kwargs):
219 219 """
220 220 pygmentize function using pygments
221 221 @param filenode:
222 222 """
223 return literal(code_highlight(filenode.content, filenode.lexer, HtmlFormatter(**kwargs)))
223 return literal(code_highlight(filenode.content,
224 filenode.lexer, HtmlFormatter(**kwargs)))
224 225
225 226 def pygmentize_annotation(filenode, **kwargs):
226 227 """
227 228 pygmentize function for annotation
228 229 @param filenode:
229 230 """
230 231
231 232 color_dict = g.changeset_annotation_colors
232 233 def gen_color():
233 234 import random
234 235 return [str(random.randrange(10, 235)) for _ in xrange(3)]
235 236 def get_color_string(cs):
236 237 if color_dict.has_key(cs):
237 238 col = color_dict[cs]
238 239 else:
239 240 color_dict[cs] = gen_color()
240 241 col = color_dict[cs]
241 242 return "color: rgb(%s) ! important;" % (', '.join(col))
242 243
243 244 def url_func(changeset):
244 tooltip_html = "<div style='font-size:0.8em'><b>Author:</b> %s<br/><b>Date:</b> %s</b><br/><b>Message:</b> %s<br/></div>"
245 tooltip_html = "<div style='font-size:0.8em'><b>Author:</b>"+\
246 " %s<br/><b>Date:</b> %s</b><br/><b>Message:</b> %s<br/></div>"
245 247
246 248 tooltip_html = tooltip_html % (changeset.author,
247 249 changeset.date,
248 250 tooltip(changeset.message))
249 251 lnk_format = 'r%s:%s' % (changeset.revision,
250 252 changeset.raw_id)
251 253 uri = link_to(
252 254 lnk_format,
253 url('changeset_home', repo_name='test',
255 url('changeset_home', repo_name=changeset.repository.name,
254 256 revision=changeset.raw_id),
255 257 style=get_color_string(changeset.raw_id),
256 258 class_='tooltip',
257 259 tooltip_title=tooltip_html
258 260 )
259 261
260 262 uri += '\n'
261 263 return uri
262 264 return literal(annotate_highlight(filenode, url_func, **kwargs))
263 265
264 266 def repo_name_slug(value):
265 267 """
266 268 Return slug of name of repository
267 269 """
268 270 slug = urlify(value)
269 271 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|:""":
270 272 slug = slug.replace(c, '-')
271 273 slug = recursive_replace(slug, '-')
272 274 return slug
273 275
274 276 flash = _Flash()
275 277
276 278
277 279 #===============================================================================
278 280 # MERCURIAL FILTERS available via h.
279 281 #===============================================================================
280 282 from mercurial import util
281 283 from mercurial.templatefilters import age as _age, person as _person
282 284
283 285 age = lambda x:_age(x)
284 286 capitalize = lambda x: x.capitalize()
285 287 date = lambda x: util.datestr(x)
286 288 email = util.email
287 289 email_or_none = lambda x: util.email(x) if util.email(x) != x else None
288 290 person = lambda x: _person(x)
289 291 hgdate = lambda x: "%d %d" % x
290 292 isodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M %1%2')
291 293 isodatesec = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2')
292 294 localdate = lambda x: (x[0], util.makedate()[1])
293 295 rfc822date = lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S %1%2")
294 296 rfc3339date = lambda x: util.datestr(x, "%Y-%m-%dT%H:%M:%S%1:%2")
295 297 time_ago = lambda x: util.datestr(_age(x), "%a, %d %b %Y %H:%M:%S %1%2")
296 298
297 299
298 300 #===============================================================================
299 301 # PERMS
300 302 #===============================================================================
301 303 from pylons_app.lib.auth import HasPermissionAny, HasPermissionAll, \
302 304 HasRepoPermissionAny, HasRepoPermissionAll
303 305
304 306 #===============================================================================
305 307 # GRAVATAR URL
306 308 #===============================================================================
307 309 import hashlib
308 310 import urllib
309 311 from pylons import request
310 312
311 313 def gravatar_url(email_address, size=30):
312 314 ssl_enabled = 'https' == request.environ.get('HTTP_X_URL_SCHEME')
313 315 default = 'identicon'
314 316 baseurl_nossl = "http://www.gravatar.com/avatar/"
315 317 baseurl_ssl = "https://secure.gravatar.com/avatar/"
316 318 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
317 319
318 320
319 321 # construct the url
320 322 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
321 323 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
322 324
323 325 return gravatar_url
@@ -1,117 +1,117 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.html"/>
4 4
5 5 <%def name="title()">
6 6 ${_('Changelog - %s') % c.repo_name}
7 7 </%def>
8 8
9 9 <%def name="breadcrumbs_links()">
10 10 ${h.link_to(u'Home',h.url('/'))}
11 11 &raquo;
12 12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 13 &raquo;
14 14 ${_('Changelog')} - ${_('showing ')} ${c.size if c.size <= c.total_cs else c.total_cs} ${_('out of')} ${c.total_cs} ${_('revisions')}
15 15 </%def>
16 16
17 17 <%def name="page_nav()">
18 18 ${self.menu('changelog')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <div class="box">
23 23 <!-- box / title -->
24 24 <div class="title">
25 25 ${self.breadcrumbs()}
26 26 </div>
27 27 <div class="table">
28 28 % if c.pagination:
29 29 <div id="graph">
30 30 <div id="graph_nodes">
31 31 <canvas id="graph_canvas"></canvas>
32 32 </div>
33 33 <div id="graph_content">
34 34 <div class="container_header">
35 35
36 36 ${h.form(h.url.current(),method='get')}
37 37 <div class="info_box">
38 38 <span>${_('Show')}:</span>
39 39 ${h.text('size',size=1,value=c.size)}
40 40 <span>${_('revisions')}</span>
41 41 ${h.submit('set',_('set'))}
42 42 </div>
43 43 ${h.end_form()}
44 44
45 45 </div>
46 46 %for cnt,cs in enumerate(c.pagination):
47 47 <div id="chg_${cnt+1}" class="container">
48 48 <div class="left">
49 49 <div class="date">${_('commit')} ${cs.revision}: ${cs.raw_id}@${cs.date}</div>
50 50 <span class="logtags">
51 51 <span class="branchtag">${cs.branch}</span>
52 52 %for tag in cs.tags:
53 53 <span class="tagtag">${tag}</span>
54 54 %endfor
55 55 </span>
56 56 <div class="author">
57 57 <div class="gravatar">
58 58 <img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),20)}"/>
59 59 </div>
60 60 <span>${h.person(cs.author)}</span><br/>
61 61 <span><a href="mailto:${h.email_or_none(cs.author)}">${h.email_or_none(cs.author)}</a></span><br/>
62 62 </div>
63 63 <div class="message">
64 ${h.link_to(h.wrap_paragraphs(cs.message.decode('utf-8','replace')),
64 ${h.link_to(h.wrap_paragraphs(cs.message),
65 65 h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
66 66 </div>
67 67 </div>
68 68 <div class="right">
69 69 <div class="changes">
70 70 <span class="removed" title="${_('removed')}">${len(cs.removed)}</span>
71 71 <span class="changed" title="${_('changed')}">${len(cs.changed)}</span>
72 72 <span class="added" title="${_('added')}">${len(cs.added)}</span>
73 73 </div>
74 74 %if len(cs.parents)>1:
75 75 <div class="merge">
76 76 ${_('merge')}<img alt="merge" src="/images/icons/arrow_join.png"/>
77 77 </div>
78 78 %endif
79 79 %for p_cs in reversed(cs.parents):
80 80 <div class="parent">${_('Parent')} ${p_cs.revision}: ${h.link_to(p_cs.raw_id,
81 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message.decode('utf-8','replace'))}
81 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}
82 82 </div>
83 83 %endfor
84 84 </div>
85 85 </div>
86 86
87 87 %endfor
88 88 <div class="pagination-wh pagination-left">
89 89 ${c.pagination.pager('$link_previous ~2~ $link_next')}
90 90 </div>
91 91 </div>
92 92 </div>
93 93
94 94 <script type="text/javascript" src="/js/graph.js"></script>
95 95 <script type="text/javascript">
96 96 YAHOO.util.Event.onDOMReady(function(){
97 97 function set_canvas() {
98 98 var c = document.getElementById('graph_nodes');
99 99 var t = document.getElementById('graph_content');
100 100 canvas = document.getElementById('graph_canvas');
101 101 var div_h = t.clientHeight;
102 102 c.style.height=div_h+'px';
103 103 canvas.setAttribute('height',div_h);
104 104 canvas.setAttribute('width',160);
105 105 };
106 106 set_canvas();
107 107 var jsdata = ${c.jsdata|n};
108 108 var r = new BranchRenderer();
109 109 r.render(jsdata);
110 110 });
111 111 </script>
112 112 %else:
113 113 ${_('There are no changes yet')}
114 114 %endif
115 115 </div>
116 116 </div>
117 117 </%def> No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now