##// END OF EJS Templates
Fixed differ to properly extract filenames, and dates from diff file. and swaped order of columns with lines nr in diff html
marcink -
r152:0c00fbaf default
parent child Browse files
Show More
@@ -1,108 +1,112 b''
1 import logging
1 import logging
2
2
3 from pylons import request, response, session, tmpl_context as c, url, config, app_globals as g
3 from pylons import request, response, session, tmpl_context as c, url, config, app_globals as g
4 from pylons.controllers.util import abort, redirect
4 from pylons.controllers.util import abort, redirect
5
5
6 from pylons_app.lib.base import BaseController, render
6 from pylons_app.lib.base import BaseController, render
7 from pylons_app.lib.utils import get_repo_slug
7 from pylons_app.lib.utils import get_repo_slug
8 from pylons_app.model.hg_model import HgModel
8 from pylons_app.model.hg_model import HgModel
9 from difflib import unified_diff
9 from difflib import unified_diff
10 from pylons_app.lib.differ import render_udiff
10 from pylons_app.lib.differ import render_udiff
11 from vcs.exceptions import RepositoryError, ChangesetError
11 from vcs.exceptions import RepositoryError, ChangesetError
12
12
13 log = logging.getLogger(__name__)
13 log = logging.getLogger(__name__)
14
14
15 class FilesController(BaseController):
15 class FilesController(BaseController):
16 def __before__(self):
16 def __before__(self):
17 c.repos_prefix = config['repos_name']
17 c.repos_prefix = config['repos_name']
18 c.repo_name = get_repo_slug(request)
18 c.repo_name = get_repo_slug(request)
19
19
20 def index(self, repo_name, revision, f_path):
20 def index(self, repo_name, revision, f_path):
21 hg_model = HgModel()
21 hg_model = HgModel()
22 c.repo = repo = hg_model.get_repo(c.repo_name)
22 c.repo = repo = hg_model.get_repo(c.repo_name)
23 revision = request.POST.get('at_rev', None) or revision
23 revision = request.POST.get('at_rev', None) or revision
24
24
25 def get_next_rev(cur):
25 def get_next_rev(cur):
26 max_rev = len(c.repo.revisions) - 1
26 max_rev = len(c.repo.revisions) - 1
27 r = cur + 1
27 r = cur + 1
28 if r > max_rev:
28 if r > max_rev:
29 r = max_rev
29 r = max_rev
30 return r
30 return r
31
31
32 def get_prev_rev(cur):
32 def get_prev_rev(cur):
33 r = cur - 1
33 r = cur - 1
34 return r
34 return r
35
35
36 c.f_path = f_path
36 c.f_path = f_path
37
37
38
38
39 try:
39 try:
40 cur_rev = repo.get_changeset(revision).revision
40 cur_rev = repo.get_changeset(revision).revision
41 prev_rev = repo.get_changeset(get_prev_rev(cur_rev)).raw_id
41 prev_rev = repo.get_changeset(get_prev_rev(cur_rev)).raw_id
42 next_rev = repo.get_changeset(get_next_rev(cur_rev)).raw_id
42 next_rev = repo.get_changeset(get_next_rev(cur_rev)).raw_id
43
43
44 c.url_prev = url('files_home', repo_name=c.repo_name,
44 c.url_prev = url('files_home', repo_name=c.repo_name,
45 revision=prev_rev, f_path=f_path)
45 revision=prev_rev, f_path=f_path)
46 c.url_next = url('files_home', repo_name=c.repo_name,
46 c.url_next = url('files_home', repo_name=c.repo_name,
47 revision=next_rev, f_path=f_path)
47 revision=next_rev, f_path=f_path)
48
48
49 c.changeset = repo.get_changeset(revision)
49 c.changeset = repo.get_changeset(revision)
50 try:
50 try:
51 c.file_msg = c.changeset.get_file_message(f_path)
51 c.file_msg = c.changeset.get_file_message(f_path)
52 except:
52 except:
53 c.file_msg = None
53 c.file_msg = None
54
54
55 c.cur_rev = c.changeset.raw_id
55 c.cur_rev = c.changeset.raw_id
56 c.rev_nr = c.changeset.revision
56 c.rev_nr = c.changeset.revision
57 c.files_list = c.changeset.get_node(f_path)
57 c.files_list = c.changeset.get_node(f_path)
58 c.file_history = self._get_history(repo, c.files_list, f_path)
58 c.file_history = self._get_history(repo, c.files_list, f_path)
59
59
60 except (RepositoryError, ChangesetError):
60 except (RepositoryError, ChangesetError):
61 c.files_list = None
61 c.files_list = None
62
62
63 return render('files/files.html')
63 return render('files/files.html')
64
64
65 def rawfile(self, repo_name, revision, f_path):
65 def rawfile(self, repo_name, revision, f_path):
66 hg_model = HgModel()
66 hg_model = HgModel()
67 c.repo = hg_model.get_repo(c.repo_name)
67 c.repo = hg_model.get_repo(c.repo_name)
68 file_node = c.repo.get_changeset(revision).get_node(f_path)
68 file_node = c.repo.get_changeset(revision).get_node(f_path)
69 response.headers['Content-type'] = file_node.mimetype
69 response.headers['Content-type'] = file_node.mimetype
70 response.headers['Content-disposition'] = 'attachment; filename=%s' \
70 response.headers['Content-disposition'] = 'attachment; filename=%s' \
71 % f_path.split('/')[-1]
71 % f_path.split('/')[-1]
72 return file_node.content
72 return file_node.content
73
73
74 def archivefile(self, repo_name, revision, fileformat):
74 def archivefile(self, repo_name, revision, fileformat):
75 return '%s %s %s' % (repo_name, revision, fileformat)
75 return '%s %s %s' % (repo_name, revision, fileformat)
76
76
77 def diff(self, repo_name, f_path):
77 def diff(self, repo_name, f_path):
78 hg_model = HgModel()
78 hg_model = HgModel()
79 diff1 = request.GET.get('diff1')
79 diff1 = request.GET.get('diff1')
80 diff2 = request.GET.get('diff2')
80 diff2 = request.GET.get('diff2')
81 c.no_changes = diff1 == diff2
81 c.no_changes = diff1 == diff2
82 c.f_path = f_path
82 c.f_path = f_path
83 c.repo = hg_model.get_repo(c.repo_name)
83 c.repo = hg_model.get_repo(c.repo_name)
84 c.changeset_1 = c.repo.get_changeset(diff1)
84 c.changeset_1 = c.repo.get_changeset(diff1)
85 c.changeset_2 = c.repo.get_changeset(diff2)
85 c.changeset_2 = c.repo.get_changeset(diff2)
86
86 f1 = c.changeset_1.get_node(f_path)
87 c.file_1 = c.changeset_1.get_file_content(f_path)
87 f2 = c.changeset_2.get_node(f_path)
88 c.file_2 = c.changeset_2.get_file_content(f_path)
88
89 c.diff1 = 'r%s:%s' % (c.changeset_1.revision, c.changeset_1._short)
89 c.diff1 = 'r%s:%s' % (c.changeset_1.revision, c.changeset_1._short)
90 c.diff2 = 'r%s:%s' % (c.changeset_2.revision, c.changeset_2._short)
90 c.diff2 = 'r%s:%s' % (c.changeset_2.revision, c.changeset_2._short)
91
91
92 d2 = unified_diff(c.file_1.splitlines(1), c.file_2.splitlines(1))
92 f_udiff = unified_diff(f1.content.splitlines(True),
93 c.diff_files = render_udiff(udiff=d2)
93 f2.content.splitlines(True),
94 f1.name,
95 f2.name)
94
96
97 c.diff_files = render_udiff(udiff=f_udiff, differ='difflib')
98 print c.diff_files
95 if len(c.diff_files) < 1:
99 if len(c.diff_files) < 1:
96 c.no_changes = True
100 c.no_changes = True
97 return render('files/file_diff.html')
101 return render('files/file_diff.html')
98
102
99 def _get_history(self, repo, node, f_path):
103 def _get_history(self, repo, node, f_path):
100 from vcs.nodes import NodeKind
104 from vcs.nodes import NodeKind
101 if not node.kind is NodeKind.FILE:
105 if not node.kind is NodeKind.FILE:
102 return []
106 return []
103 changesets = node.history
107 changesets = node.history
104 hist_l = []
108 hist_l = []
105 for chs in changesets:
109 for chs in changesets:
106 n_desc = 'r%s:%s' % (chs.revision, chs._short)
110 n_desc = 'r%s:%s' % (chs.revision, chs._short)
107 hist_l.append((chs._short, n_desc,))
111 hist_l.append((chs._short, n_desc,))
108 return hist_l
112 return hist_l
@@ -1,209 +1,216 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # original copyright: 2007-2008 by Armin Ronacher
2 # original copyright: 2007-2008 by Armin Ronacher
3 # licensed under the BSD license.
3 # licensed under the BSD license.
4
4
5 import re, difflib
5 import re, difflib
6
6
7 def render_udiff(udiff, differ='udiff'):
7 def render_udiff(udiff, differ='udiff'):
8 """Renders the udiff into multiple chunks of nice looking tables.
8 """Renders the udiff into multiple chunks of nice looking tables.
9 The return value is a list of those tables.
9 The return value is a list of those tables.
10 """
10 """
11 return DiffProcessor(udiff, differ).prepare()
11 return DiffProcessor(udiff, differ).prepare()
12
12
13 class DiffProcessor(object):
13 class DiffProcessor(object):
14 """Give it a unified diff and it returns a list of the files that were
14 """Give it a unified diff and it returns a list of the files that were
15 mentioned in the diff together with a dict of meta information that
15 mentioned in the diff together with a dict of meta information that
16 can be used to render it in a HTML template.
16 can be used to render it in a HTML template.
17 """
17 """
18 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
18 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
19
19
20 def __init__(self, udiff, differ):
20 def __init__(self, udiff, differ):
21 """
21 """
22 :param udiff: a text in udiff format
22 :param udiff: a text in udiff format
23 """
23 """
24 if isinstance(udiff, basestring):
24 if isinstance(udiff, basestring):
25 udiff = udiff.splitlines(1)
25 udiff = udiff.splitlines(1)
26
26
27 self.lines = map(self.escaper, udiff)
27 self.lines = map(self.escaper, udiff)
28
28
29 # Select a differ.
29 # Select a differ.
30 if differ == 'difflib':
30 if differ == 'difflib':
31 self.differ = self._highlight_line_difflib
31 self.differ = self._highlight_line_difflib
32 else:
32 else:
33 self.differ = self._highlight_line_udiff
33 self.differ = self._highlight_line_udiff
34
34
35
35
36 def escaper(self, string):
36 def escaper(self, string):
37 return string.replace('<', '&lt;').replace('>', '&gt;')
37 return string.replace('<', '&lt;').replace('>', '&gt;')
38
38
39 def _extract_rev(self, line1, line2):
39 def _extract_rev(self, line1, line2):
40 """Extract the filename and revision hint from a line."""
40 """Extract the filename and revision hint from a line."""
41 try:
41 try:
42 if line1.startswith('--- ') and line2.startswith('+++ '):
42 if line1.startswith('--- ') and line2.startswith('+++ '):
43 filename, old_rev = line1[4:].split(None, 1)
43 l1 = line1[4:].split(None, 1)
44 new_rev = line2[4:].split(None, 1)[1]
44 old_filename = l1[0] if len(l1) >= 1 else None
45 return filename, 'old', 'new'
45 old_rev = l1[1] if len(l1) == 2 else 'old'
46
47 l2 = line1[4:].split(None, 1)
48 new_filename = l2[0] if len(l2) >= 1 else None
49 new_rev = l2[1] if len(l2) == 2 else 'new'
50
51 return old_filename, new_rev, old_rev
46 except (ValueError, IndexError):
52 except (ValueError, IndexError):
47 pass
53 pass
54
48 return None, None, None
55 return None, None, None
49
56
50 def _highlight_line_difflib(self, line, next):
57 def _highlight_line_difflib(self, line, next):
51 """Highlight inline changes in both lines."""
58 """Highlight inline changes in both lines."""
52
59
53 if line['action'] == 'del':
60 if line['action'] == 'del':
54 old, new = line, next
61 old, new = line, next
55 else:
62 else:
56 old, new = next, line
63 old, new = next, line
57
64
58 oldwords = re.split(r'(\W)', old['line'])
65 oldwords = re.split(r'(\W)', old['line'])
59 newwords = re.split(r'(\W)', new['line'])
66 newwords = re.split(r'(\W)', new['line'])
60
67
61 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
68 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
62
69
63 oldfragments, newfragments = [], []
70 oldfragments, newfragments = [], []
64 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
71 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
65 oldfrag = ''.join(oldwords[i1:i2])
72 oldfrag = ''.join(oldwords[i1:i2])
66 newfrag = ''.join(newwords[j1:j2])
73 newfrag = ''.join(newwords[j1:j2])
67 if tag != 'equal':
74 if tag != 'equal':
68 if oldfrag:
75 if oldfrag:
69 oldfrag = '<del>%s</del>' % oldfrag
76 oldfrag = '<del>%s</del>' % oldfrag
70 if newfrag:
77 if newfrag:
71 newfrag = '<ins>%s</ins>' % newfrag
78 newfrag = '<ins>%s</ins>' % newfrag
72 oldfragments.append(oldfrag)
79 oldfragments.append(oldfrag)
73 newfragments.append(newfrag)
80 newfragments.append(newfrag)
74
81
75 old['line'] = "".join(oldfragments)
82 old['line'] = "".join(oldfragments)
76 new['line'] = "".join(newfragments)
83 new['line'] = "".join(newfragments)
77
84
78 def _highlight_line_udiff(self, line, next):
85 def _highlight_line_udiff(self, line, next):
79 """Highlight inline changes in both lines."""
86 """Highlight inline changes in both lines."""
80 start = 0
87 start = 0
81 limit = min(len(line['line']), len(next['line']))
88 limit = min(len(line['line']), len(next['line']))
82 while start < limit and line['line'][start] == next['line'][start]:
89 while start < limit and line['line'][start] == next['line'][start]:
83 start += 1
90 start += 1
84 end = -1
91 end = -1
85 limit -= start
92 limit -= start
86 while - end <= limit and line['line'][end] == next['line'][end]:
93 while - end <= limit and line['line'][end] == next['line'][end]:
87 end -= 1
94 end -= 1
88 end += 1
95 end += 1
89 if start or end:
96 if start or end:
90 def do(l):
97 def do(l):
91 last = end + len(l['line'])
98 last = end + len(l['line'])
92 if l['action'] == 'add':
99 if l['action'] == 'add':
93 tag = 'ins'
100 tag = 'ins'
94 else:
101 else:
95 tag = 'del'
102 tag = 'del'
96 l['line'] = '%s<%s>%s</%s>%s' % (
103 l['line'] = '%s<%s>%s</%s>%s' % (
97 l['line'][:start],
104 l['line'][:start],
98 tag,
105 tag,
99 l['line'][start:last],
106 l['line'][start:last],
100 tag,
107 tag,
101 l['line'][last:]
108 l['line'][last:]
102 )
109 )
103 do(line)
110 do(line)
104 do(next)
111 do(next)
105
112
106 def _parse_udiff(self):
113 def _parse_udiff(self):
107 """Parse the diff an return data for the template."""
114 """Parse the diff an return data for the template."""
108 lineiter = iter(self.lines)
115 lineiter = iter(self.lines)
109 files = []
116 files = []
110 try:
117 try:
111 line = lineiter.next()
118 line = lineiter.next()
112 while 1:
119 while 1:
113 # continue until we found the old file
120 # continue until we found the old file
114 if not line.startswith('--- '):
121 if not line.startswith('--- '):
115 line = lineiter.next()
122 line = lineiter.next()
116 continue
123 continue
117
124
118 chunks = []
125 chunks = []
119 filename, old_rev, new_rev = \
126 filename, old_rev, new_rev = \
120 self._extract_rev(line, lineiter.next())
127 self._extract_rev(line, lineiter.next())
121 files.append({
128 files.append({
122 'filename': filename,
129 'filename': filename,
123 'old_revision': old_rev,
130 'old_revision': old_rev,
124 'new_revision': new_rev,
131 'new_revision': new_rev,
125 'chunks': chunks
132 'chunks': chunks
126 })
133 })
127
134
128 line = lineiter.next()
135 line = lineiter.next()
129 while line:
136 while line:
130 match = self._chunk_re.match(line)
137 match = self._chunk_re.match(line)
131 if not match:
138 if not match:
132 break
139 break
133
140
134 lines = []
141 lines = []
135 chunks.append(lines)
142 chunks.append(lines)
136
143
137 old_line, old_end, new_line, new_end = \
144 old_line, old_end, new_line, new_end = \
138 [int(x or 1) for x in match.groups()[:-1]]
145 [int(x or 1) for x in match.groups()[:-1]]
139 old_line -= 1
146 old_line -= 1
140 new_line -= 1
147 new_line -= 1
141 context = match.groups()[-1]
148 context = match.groups()[-1]
142 old_end += old_line
149 old_end += old_line
143 new_end += new_line
150 new_end += new_line
144
151
145 if context:
152 if context:
146 lines.append({
153 lines.append({
147 'old_lineno': None,
154 'old_lineno': None,
148 'new_lineno': None,
155 'new_lineno': None,
149 'action': 'context',
156 'action': 'context',
150 'line': line,
157 'line': line,
151 })
158 })
152
159
153 line = lineiter.next()
160 line = lineiter.next()
154
161
155 while old_line < old_end or new_line < new_end:
162 while old_line < old_end or new_line < new_end:
156 if line:
163 if line:
157 command, line = line[0], line[1:]
164 command, line = line[0], line[1:]
158 else:
165 else:
159 command = ' '
166 command = ' '
160 affects_old = affects_new = False
167 affects_old = affects_new = False
161
168
162 # ignore those if we don't expect them
169 # ignore those if we don't expect them
163 if command in '#@':
170 if command in '#@':
164 continue
171 continue
165 elif command == '+':
172 elif command == '+':
166 affects_new = True
173 affects_new = True
167 action = 'add'
174 action = 'add'
168 elif command == '-':
175 elif command == '-':
169 affects_old = True
176 affects_old = True
170 action = 'del'
177 action = 'del'
171 else:
178 else:
172 affects_old = affects_new = True
179 affects_old = affects_new = True
173 action = 'unmod'
180 action = 'unmod'
174
181
175 old_line += affects_old
182 old_line += affects_old
176 new_line += affects_new
183 new_line += affects_new
177 lines.append({
184 lines.append({
178 'old_lineno': affects_old and old_line or '',
185 'old_lineno': affects_old and old_line or '',
179 'new_lineno': affects_new and new_line or '',
186 'new_lineno': affects_new and new_line or '',
180 'action': action,
187 'action': action,
181 'line': line
188 'line': line
182 })
189 })
183 line = lineiter.next()
190 line = lineiter.next()
184
191
185 except StopIteration:
192 except StopIteration:
186 pass
193 pass
187
194
188 # highlight inline changes
195 # highlight inline changes
189 for file in files:
196 for file in files:
190 for chunk in chunks:
197 for chunk in chunks:
191 lineiter = iter(chunk)
198 lineiter = iter(chunk)
192 first = True
199 first = True
193 try:
200 try:
194 while 1:
201 while 1:
195 line = lineiter.next()
202 line = lineiter.next()
196 if line['action'] != 'unmod':
203 if line['action'] != 'unmod':
197 nextline = lineiter.next()
204 nextline = lineiter.next()
198 if nextline['action'] == 'unmod' or \
205 if nextline['action'] == 'unmod' or \
199 nextline['action'] == line['action']:
206 nextline['action'] == line['action']:
200 continue
207 continue
201 self.differ(line, nextline)
208 self.differ(line, nextline)
202 except StopIteration:
209 except StopIteration:
203 pass
210 pass
204
211
205 return files
212 return files
206
213
207 def prepare(self):
214 def prepare(self):
208 """Prepare the passed udiff for HTML rendering."""
215 """Prepare the passed udiff for HTML rendering."""
209 return self._parse_udiff()
216 return self._parse_udiff()
@@ -1,59 +1,59 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('Repository managment')}
4 ${_('Repository managment')}
5 </%def>
5 </%def>
6 <%def name="breadcrumbs()">
6 <%def name="breadcrumbs()">
7 ${h.link_to(u'Home',h.url('/'))}
7 ${h.link_to(u'Home',h.url('/'))}
8 /
8 /
9 ${h.link_to(c.repo_name,h.url('files_home',repo_name=c.repo_name))}
9 ${h.link_to(c.repo_name,h.url('files_home',repo_name=c.repo_name))}
10 /
10 /
11 ${_('files')}
11 ${_('files')}
12 </%def>
12 </%def>
13 <%def name="page_nav()">
13 <%def name="page_nav()">
14 <form action="log">
14 <form action="log">
15 <dl class="search">
15 <dl class="search">
16 <dt><label>Search: </label></dt>
16 <dt><label>Search: </label></dt>
17 <dd><input type="text" name="rev" /></dd>
17 <dd><input type="text" name="rev" /></dd>
18 </dl>
18 </dl>
19 </form>
19 </form>
20
20
21 ${self.menu('files')}
21 ${self.menu('files')}
22 </%def>
22 </%def>
23 <%def name="css()">
23 <%def name="css()">
24 <link rel="stylesheet" href="/css/monoblue_custom.css" type="text/css" />
24 <link rel="stylesheet" href="/css/monoblue_custom.css" type="text/css" />
25 <link rel="stylesheet" href="/css/diff.css" type="text/css" />
25 <link rel="stylesheet" href="/css/diff.css" type="text/css" />
26 </%def>
26 </%def>
27 <%def name="main()">
27 <%def name="main()">
28 <h2 class="no-link no-border">${'%s: %s %s %s' % (_('File diff'),c.diff2,'&rarr;',c.diff1)|n}</h2>
28 <h2 class="no-link no-border">${'%s: %s %s %s' % (_('File diff'),c.diff2,'&rarr;',c.diff1)|n}</h2>
29 <div id="body" class="diffblock">
29 <div id="body" class="diffblock">
30 <div class="code-header">
30 <div class="code-header">
31 <span>${h.link_to(c.f_path,h.url('files_home',repo_name=c.repo_name,revision=c.diff2.split(':')[1],f_path=c.f_path))}</span>
31 <span>${h.link_to(c.f_path,h.url('files_home',repo_name=c.repo_name,revision=c.diff2.split(':')[1],f_path=c.f_path))}</span>
32 </div>
32 </div>
33 <div class="code-body">
33 <div class="code-body">
34 %if c.no_changes:
34 %if c.no_changes:
35 ${_('No changes')}
35 ${_('No changes')}
36 %else:
36 %else:
37 <table class='code-difftable'>
37 <table class='code-difftable'>
38 %for diff in c.diff_files:
38 %for diff in c.diff_files:
39 %for x in diff['chunks']:
39 %for x in diff['chunks']:
40 %for y in x:
40 %for y in x:
41 <tr class="line ${y['action']}">
41 <tr class="line ${y['action']}">
42 <td id="#${diff['filename']}_O${y['old_lineno']}" class="lineno old">
43 <pre><a href="#${diff['filename']}_O${y['old_lineno']}">${y['old_lineno']}</a></pre>
44 </td>
42 <td id="#${diff['filename']}_N${y['new_lineno']}"class="lineno new">
45 <td id="#${diff['filename']}_N${y['new_lineno']}"class="lineno new">
43 <pre><a href="#${diff['filename']}_N${y['new_lineno']}">${y['new_lineno']}</a></pre>
46 <pre><a href="#${diff['filename']}_N${y['new_lineno']}">${y['new_lineno']}</a></pre>
44 </td>
45 <td id="#${diff['filename']}_O${y['old_lineno']}" class="lineno old">
46 <pre><a href="#${diff['filename']}_O${y['old_lineno']}">${y['old_lineno']}</a></pre>
47 </td>
47 </td>
48 <td class="code">
48 <td class="code">
49 <pre>${y['line']|n}</pre>
49 <pre>${y['line']|n}</pre>
50 </td>
50 </td>
51 </tr>
51 </tr>
52 %endfor$
52 %endfor$
53 %endfor
53 %endfor
54 %endfor
54 %endfor
55 </table>
55 </table>
56 %endif
56 %endif
57 </div>
57 </div>
58 </div>
58 </div>
59 </%def> No newline at end of file
59 </%def>
General Comments 0
You need to be logged in to leave comments. Login now