bea/c283cb: merge Praveen's gitorious-issues integration request.
[be.git] / libbe / command / html.py
1 # Copyright (C) 2009-2012 Chris Ball <cjb@laptop.org>
2 #                         Gianluca Montecchi <gian@grys.it>
3 #                         Mathieu Clabaut <mathieu.clabaut@gmail.com>
4 #                         W. Trevor King <wking@drexel.edu>
5 #
6 # This file is part of Bugs Everywhere.
7 #
8 # Bugs Everywhere is free software: you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by the Free
10 # Software Foundation, either version 2 of the License, or (at your option) any
11 # later version.
12 #
13 # Bugs Everywhere is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15 # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
16 # more details.
17 #
18 # You should have received a copy of the GNU General Public License along with
19 # Bugs Everywhere.  If not, see <http://www.gnu.org/licenses/>.
20
21 import codecs
22 import htmlentitydefs
23 import itertools
24 import os
25 import os.path
26 import re
27 import string
28 import time
29 import xml.sax.saxutils
30
31 from jinja2 import Environment, FileSystemLoader, DictLoader, ChoiceLoader
32
33 import libbe
34 import libbe.command
35 import libbe.command.util
36 import libbe.comment
37 import libbe.util.encoding
38 import libbe.util.id
39 import libbe.command.depend
40
41
42 class HTML (libbe.command.Command):
43     """Generate a static HTML dump of the current repository status
44
45     >>> import sys
46     >>> import libbe.bugdir
47     >>> bugdir = libbe.bugdir.SimpleBugDir(memory=False)
48     >>> io = libbe.command.StringInputOutput()
49     >>> io.stdout = sys.stdout
50     >>> ui = libbe.command.UserInterface(io=io)
51     >>> ui.storage_callbacks.set_storage(bugdir.storage)
52     >>> cmd = HTML(ui=ui)
53
54     >>> ret = ui.run(cmd, {
55     ...         'output':os.path.join(bugdir.storage.repo, 'html_export')})
56     >>> os.path.exists(os.path.join(bugdir.storage.repo, 'html_export'))
57     True
58     >>> os.path.exists(os.path.join(
59     ...         bugdir.storage.repo, 'html_export', 'index.html'))
60     True
61     >>> os.path.exists(os.path.join(
62     ...         bugdir.storage.repo, 'html_export', 'index_inactive.html'))
63     True
64     >>> os.path.exists(os.path.join(
65     ...         bugdir.storage.repo, 'html_export', 'bugs'))
66     True
67     >>> os.path.exists(os.path.join(
68     ...         bugdir.storage.repo, 'html_export', 'bugs', 'a', 'index.html'))
69     True
70     >>> os.path.exists(os.path.join(
71     ...         bugdir.storage.repo, 'html_export', 'bugs', 'b', 'index.html'))
72     True
73     >>> ui.cleanup()
74     >>> bugdir.cleanup()
75     """
76     name = 'html'
77
78     def __init__(self, *args, **kwargs):
79         libbe.command.Command.__init__(self, *args, **kwargs)
80         self.options.extend([
81                 libbe.command.Option(name='output', short_name='o',
82                     help='Set the output path (%default)',
83                     arg=libbe.command.Argument(
84                         name='output', metavar='DIR', default='./html_export',
85                         completion_callback=libbe.command.util.complete_path)),
86                 libbe.command.Option(name='template-dir', short_name='t',
87                     help='Use a different template.  Defaults to internal templates',
88                     arg=libbe.command.Argument(
89                         name='template-dir', metavar='DIR',
90                         completion_callback=libbe.command.util.complete_path)),
91                 libbe.command.Option(name='title',
92                     help='Set the bug repository title (%default)',
93                     arg=libbe.command.Argument(
94                         name='title', metavar='STRING',
95                         default='Bugs Everywhere Issue Tracker')),
96                 libbe.command.Option(name='index-header',
97                     help='Set the index page headers (%default)',
98                     arg=libbe.command.Argument(
99                         name='index-header', metavar='STRING',
100                         default='Bugs Everywhere Bug List')),
101                 libbe.command.Option(name='export-template', short_name='e',
102                     help='Export the default template and exit.'),
103                 libbe.command.Option(name='export-template-dir', short_name='d',
104                     help='Set the directory for the template export (%default)',
105                     arg=libbe.command.Argument(
106                         name='export-template-dir', metavar='DIR',
107                         default='./default-templates/',
108                         completion_callback=libbe.command.util.complete_path)),
109                 libbe.command.Option(name='min-id-length', short_name='l',
110                     help='Attempt to truncate bug and comment IDs to this length.  Set to -1 for non-truncated IDs (%default)',
111                     arg=libbe.command.Argument(
112                         name='min-id-length', metavar='INT',
113                         default=-1, type='int')),
114                 libbe.command.Option(name='verbose', short_name='v',
115                     help='Verbose output, default is %default'),
116                 ])
117
118     def _run(self, **params):
119         if params['export-template'] == True:
120             bugdirs = None
121         else:
122             bugdirs = self._get_bugdirs()
123             for bugdir in bugdirs.values():
124                 bugdir.load_all_bugs()
125         html_gen = HTMLGen(bugdirs,
126                            template_dir=params['template-dir'],
127                            title=params['title'],
128                            header=params['index-header'],
129                            min_id_length=params['min-id-length'],
130                            verbose=params['verbose'],
131                            stdout=self.stdout)
132         if params['export-template'] == True:
133             html_gen.write_default_template(params['export-template-dir'])
134         else:
135             html_gen.run(params['output'])
136
137     def _long_help(self):
138         return """
139 Generate a set of html pages representing the current state of the bug
140 directory.
141 """
142
143 Html = HTML # alias for libbe.command.base.get_command_class()
144
145 class HTMLGen (object):
146     def __init__(self, bugdirs, template_dir=None,
147                  title="Site Title", header="Header",
148                  min_id_length=-1,
149                  verbose=False, encoding=None, stdout=None,
150                  ):
151         self.generation_time = time.ctime()
152         self.bugdirs = bugdirs
153         self.title = title
154         self.header = header
155         self.verbose = verbose
156         self.stdout = stdout
157         if encoding != None:
158             self.encoding = encoding
159         else:
160             self.encoding = libbe.util.encoding.get_text_file_encoding()
161         self._load_templates(template_dir)
162         self.min_id_length = min_id_length
163
164     def run(self, out_dir):
165         if self.verbose == True:
166             print >> self.stdout, \
167                 'Creating the html output in %s using templates in %s' \
168                 % (out_dir, self.template)
169
170         bugs_active = []
171         bugs_inactive = []
172         bugs_target = []
173         bugs = list(itertools.chain(*list(
174                     [bug for bug in bugdir]
175                     for bugdir in self.bugdirs.values())))
176         bugs.sort()
177         
178         for b in bugs:
179             if  b.active == True and b.severity != 'target':
180                 bugs_active.append(b)
181             if b.active != True and b.severity != 'target':
182                 bugs_inactive.append(b)
183             if b.severity == 'target':
184                 bugs_target.append(b)
185         
186         self._create_output_directories(out_dir)
187         self._write_css_file()
188         for b in bugs:
189             if b.severity == 'target':
190                 up_link = '../../index_target.html'
191             elif b.active:
192                 up_link = '../../index.html'
193             else:
194                 up_link = '../../index_inactive.html'                
195             self._write_bug_file(
196                 b, title=self.title, header=self.header,
197                 up_link=up_link)
198         self._write_index_file(
199             bugs_active, title=self.title,
200             header=self.header, bug_type='active')
201         self._write_index_file(
202             bugs_inactive, title=self.title,
203             header=self.header, bug_type='inactive')
204         self._write_index_file(
205             bugs_target, title=self.title,
206             header=self.header, bug_type='target')
207
208     def _truncated_bug_id(self, bug):
209         return libbe.util.id._truncate(
210             bug.uuid, bug.sibling_uuids(),
211             min_length=self.min_id_length)
212
213     def _truncated_comment_id(self, comment):
214         return libbe.util.id._truncate(
215             comment.uuid, comment.sibling_uuids(),
216             min_length=self.min_id_length)
217
218     def _create_output_directories(self, out_dir):
219         if self.verbose:
220             print >> self.stdout, 'Creating output directories'
221         self.out_dir = self._make_dir(out_dir)
222         self.out_dir_bugs = self._make_dir(
223             os.path.join(self.out_dir, 'bugs'))
224
225     def _write_css_file(self):
226         if self.verbose:
227             print >> self.stdout, 'Writing css file'
228         assert hasattr(self, 'out_dir'), \
229             'Must run after ._create_output_directories()'
230         template = self.template.get_template('style.css')
231         self._write_file(template.render(), [self.out_dir, 'style.css'])
232
233     def _write_bug_file(self, bug, title, header, up_link):
234         if self.verbose:
235             print >> self.stdout, '\tCreating bug file for %s' % bug.id.user()
236         assert hasattr(self, 'out_dir_bugs'), \
237             'Must run after ._create_output_directories()'
238         index_type = ''
239             
240         if bug.active == True:
241             index_type = 'Active'
242         else:
243             index_type = 'Inactive'
244         if bug.severity == 'target':
245             index_type = 'Target'
246                 
247         bug.load_comments(load_full=True)
248         bug.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
249         dirname = self._truncated_bug_id(bug)
250         fullpath = os.path.join(self.out_dir_bugs, dirname, 'index.html')
251         template_info = {
252             'title': title,
253             'charset': self.encoding,
254             'stylesheet': '../../style.css',
255             'header': header,
256             'backlinks': self.template.get_template('bug_backlinks.html'),
257             'up_link': up_link,
258             'index_type': index_type,
259             'bug': bug,
260             'comment_entry': self.template.get_template(
261                 'bug_comment_entry.html'),
262             'comments': [(depth,comment) for depth,comment
263                          in bug.comment_root.thread(flatten=False)],
264             'comment_dir': self._truncated_comment_id,
265             'format_body': self._format_comment_body,
266             'div_close': _DivCloser(),
267             'generation_time': self.generation_time,
268             }
269         fulldir = os.path.join(self.out_dir_bugs, dirname)
270         if not os.path.exists(fulldir):
271             os.mkdir(fulldir)
272         template = self.template.get_template('bug.html')
273         self._write_file(template.render(template_info), [fullpath])
274
275     def _write_index_file(self, bugs, title, header, bug_type='active'):
276         if self.verbose:
277             print >> self.stdout, 'Writing %s index file for %d bugs' % (bug_type, len(bugs))
278         assert hasattr(self, 'out_dir'), 'Must run after ._create_output_directories()'
279
280         if bug_type == 'active':
281             filename = 'index.html'
282         elif bug_type == 'inactive':
283             filename = 'index_inactive.html'
284         elif bug_type == 'target':
285             filename = 'index_by_target.html'
286         else:
287             raise ValueError('unrecognized bug_type: "%s"' % bug_type)
288
289         template_info = {
290             'title': title,
291             'charset': self.encoding,
292             'stylesheet': 'style.css',
293             'header': header,
294             'active_class': 'tab nsel',
295             'inactive_class': 'tab nsel',
296             'target_class': 'tab nsel',
297             'bugs': bugs,
298             'bug_entry': self.template.get_template('index_bug_entry.html'),
299             'bug_dir': self._truncated_bug_id,
300             'generation_time': self.generation_time,
301             }
302         template_info['%s_class' % bug_type] = 'tab sel'
303         if bug_type == 'target':
304             template = self.template.get_template('target_index.html')
305             template_info['targets'] = [
306                 (target, sorted(libbe.command.depend.get_blocked_by(
307                             target.bugdir, target)))
308                 for target in bugs]
309         else:
310             template = self.template.get_template('standard_index.html')           
311         self._write_file(
312             template.render(template_info)+'\n', [self.out_dir,filename])
313
314     def _long_to_linked_user(self, text):
315         """
316         >>> import libbe.bugdir
317         >>> bugdir = libbe.bugdir.SimpleBugDir(memory=False)
318         >>> h = HTMLGen({bugdir.uuid: bugdir})
319         >>> h._long_to_linked_user('A link #abc123/a#, and a non-link #x#y#.')
320         'A link <a href="./a/">abc/a</a>, and a non-link #x#y#.'
321         >>> bugdir.cleanup()
322         """
323         replacer = libbe.util.id.IDreplacer(
324             self.bugdirs, self._long_to_linked_user_replacer, wrap=False)
325         return re.sub(
326             libbe.util.id.REGEXP, replacer, text)
327
328     def _long_to_linked_user_replacer(self, bugdirs, long_id):
329         """
330         >>> import libbe.bugdir
331         >>> import libbe.util.id
332         >>> bugdir = libbe.bugdir.SimpleBugDir(memory=False)
333         >>> bugdirs = {bugdir.uuid: bugdir}
334         >>> a = bugdir.bug_from_uuid('a')
335         >>> uuid_gen = libbe.util.id.uuid_gen
336         >>> libbe.util.id.uuid_gen = lambda : '0123'
337         >>> c = a.new_comment('comment for link testing')
338         >>> libbe.util.id.uuid_gen = uuid_gen
339         >>> c.uuid
340         '0123'
341         >>> h = HTMLGen(bugdirs)
342         >>> h._long_to_linked_user_replacer(bugdirs, 'abc123')
343         '#abc123#'
344         >>> h._long_to_linked_user_replacer(bugdirs, 'abc123/a')
345         '<a href="./a/">abc/a</a>'
346         >>> h._long_to_linked_user_replacer(bugdirs, 'abc123/a/0123')
347         '<a href="./a/#0123">abc/a/012</a>'
348         >>> h._long_to_linked_user_replacer(bugdirs, 'x')
349         '#x#'
350         >>> h._long_to_linked_user_replacer(bugdirs, '')
351         '##'
352         >>> bugdir.cleanup()
353         """
354         try:
355             p = libbe.util.id.parse_user(bugdirs, long_id)
356         except (libbe.util.id.MultipleIDMatches,
357                 libbe.util.id.NoIDMatches,
358                 libbe.util.id.InvalidIDStructure), e:
359             return '#%s#' % long_id # re-wrap failures
360         if p['type'] == 'bugdir':
361             return '#%s#' % long_id
362         elif p['type'] == 'bug':
363             bugdir,bug,comment = (
364                 libbe.command.util.bugdir_bug_comment_from_user_id(
365                     bugdirs, long_id))
366             return '<a href="./%s/">%s</a>' \
367                 % (self._truncated_bug_id(bug), bug.id.user())
368         elif p['type'] == 'comment':
369             bugdir,bug,comment = (
370                 libbe.command.util.bugdir_bug_comment_from_user_id(
371                     bugdirs, long_id))
372             return '<a href="./%s/#%s">%s</a>' \
373                 % (self._truncated_bug_id(bug),
374                    self._truncated_comment_id(comment),
375                    comment.id.user())
376         raise Exception('Invalid id type %s for "%s"'
377                         % (p['type'], long_id))
378
379     def _format_comment_body(self, bug, comment):
380         link_long_ids = False
381         save_body = False
382         value = comment.body
383         if comment.content_type == 'text/html':
384             link_long_ids = True
385         elif comment.content_type.startswith('text/'):
386             value = '<pre>\n'+self._escape(value)+'\n</pre>'
387             link_long_ids = True
388         elif comment.content_type.startswith('image/'):
389             save_body = True
390             value = '<img src="./%s/%s" />' % (
391                 self._truncated_bug_id(bug),
392                 self._truncated_comment_id(comment))
393         else:
394             save_body = True
395             value = '<a href="./%s/%s">Link to %s file</a>.' % (
396                 self._truncated_bug_id(bug),
397                 self._truncated_comment_id(comment),
398                 comment.content_type)
399         if link_long_ids == True:
400             value = self._long_to_linked_user(value)
401         if save_body == True:
402             per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid)
403             if not os.path.exists(per_bug_dir):
404                 os.mkdir(per_bug_dir)
405             comment_path = os.path.join(per_bug_dir, comment.uuid)
406             self._write_file(
407                 '<Files %s>\n  ForceType %s\n</Files>' \
408                     % (comment.uuid, comment.content_type),
409                 [per_bug_dir, '.htaccess'], mode='a')
410             self._write_file(comment.body,
411                              [per_bug_dir, comment.uuid], mode='wb')
412         return value
413
414     def _escape(self, string):
415         if string == None:
416             return ''
417         return xml.sax.saxutils.escape(string)
418
419     def _make_dir(self, dir_path):
420         dir_path = os.path.abspath(os.path.expanduser(dir_path))
421         if not os.path.exists(dir_path):
422             try:
423                 os.makedirs(dir_path)
424             except:
425                 raise libbe.command.UserError(
426                     'Cannot create output directory "%s".' % dir_path)
427         return dir_path
428
429     def _write_file(self, content, path_array, mode='w'):
430         return libbe.util.encoding.set_file_contents(
431             os.path.join(*path_array), content, mode, self.encoding)
432
433     def _read_file(self, path_array, mode='r'):
434         return libbe.util.encoding.get_file_contents(
435             os.path.join(*path_array), mode, self.encoding, decode=True)
436
437     def write_default_template(self, out_dir):
438         if self.verbose:
439             print >> self.stdout, 'Creating output directories'
440         self.out_dir = self._make_dir(out_dir)
441         for filename,text in self.template_dict.iteritems():
442             if self.verbose:
443                 print >> self.stdout, 'Creating %s file'
444             self._write_file(text, [self.out_dir, filename])
445
446     def _load_templates(self, template_dir=None):
447         if template_dir is not None:
448             template_dir = os.path.abspath(os.path.expanduser(template_dir))
449
450         self.template_dict = {
451 ##
452             'style.css':
453 """body {
454   font-family: "lucida grande", "sans serif";
455   font-size: 14px;
456   color: #333;
457   width: auto;
458   margin: auto;
459 }
460
461 div.main {
462   padding: 20px;
463   margin: auto;
464   padding-top: 0;
465   margin-top: 1em;
466   background-color: #fcfcfc;
467   -moz-border-radius: 10px;
468   
469 }
470
471 div.footer {
472   font-size: small;
473   padding-left: 20px;
474   padding-right: 20px;
475   padding-top: 5px;
476   padding-bottom: 5px;
477   margin: auto;
478   background: #305275;
479   color: #fffee7;
480   -moz-border-radius: 10px;
481 }
482
483 div.header {
484     font-size: xx-large;
485     padding-left: 20px;
486     padding-right: 20px;
487     padding-top: 10px;
488     font-weight:bold;
489     padding-bottom: 10px;
490     background: #305275;
491     color: #fffee7;
492     -moz-border-radius: 10px;
493 }
494
495 th.target_name {
496     text-align:left;
497     border: 1px solid;
498     border-color: #305275;
499     background-color: #305275;
500     color: #fff;
501     width: auto%;
502     -moz-border-radius-topleft: 8px;
503     -moz-border-radius-topright: 8px;
504     padding-left: 5px;
505     padding-right: 5px;
506 }
507
508 table {
509   border-style: solid;
510   border: 1px #c3d9ff;
511   border-spacing: 0px 0px;
512   width: auto;
513   padding: 0px;
514   
515   }
516
517 tb { border: 1px; }
518
519 tr {
520   vertical-align: top;
521   border: 1px #c3d9ff;
522   border-style: dotted;
523   width: auto;
524   padding: 0px;
525 }
526
527 th {
528     border-width: 1px;
529     border-style: solid;
530     border-color: #c3d9ff;
531     border-collapse: collapse;
532     padding-left: 5px;
533     padding-right: 5px;
534 }
535
536
537 td {
538     border-width: 1px;
539     border-color: #c3d9ff;
540     border-collapse: collapse;
541     padding-left: 5px;
542     padding-right: 5px;
543     width: auto%;
544 }
545
546 img { border-style: none; }
547
548 ul {
549   list-style-type: none;
550   padding: 0;
551 }
552
553 p { width: auto; }
554
555 p.backlink {
556   width: auto;
557   font-weight: bold;
558 }
559
560 a {
561   background: inherit;
562   text-decoration: none;
563 }
564
565 a { color: #553d41; }
566 a:hover { color: #003d41; }
567 a:visited { color: #305275; }
568 .footer a { color: #508d91; }
569
570 /* bug index pages */
571
572 td.tab {
573   padding-right: 1em;
574   padding-left: 1em;
575 }
576
577 td.sel.tab {
578     background-color: #c3d9ff ;
579     border: 1px solid #c3d9ff;
580     font-weight:bold;    
581     -moz-border-radius-topleft: 15px;
582     -moz-border-radius-topright: 15px;
583 }
584
585 td.nsel.tab { 
586     border: 1px solid #c3d9ff;
587     font-weight:bold;    
588     -moz-border-radius-topleft: 5px;
589     -moz-border-radius-topright: 5px;
590 }
591
592 table.bug_list {
593     border-width: 1px;
594     border-style: solid;
595     border-color: #c3d9ff;
596     padding: 0px;
597     width: 100%;            
598     border: 1px solid #c3d9ff;
599 }
600
601 table.target_list {
602     padding: 0px;
603     width: 100%;
604     margin-bottom: 10px;
605 }
606
607 table.target_list.td {
608     border-width: 1px;
609 }
610
611 tr.wishlist { background-color: #DCFAFF;}
612 tr.wishlist:hover { background-color: #C2DCE1; }
613
614 tr.minor { background-color: #FFFFA6; }
615 tr.minor:hover { background-color: #E6E696; }
616
617 tr.serious { background-color: #FF9077;}
618 tr.serious:hover { background-color: #E6826B; }
619
620 tr.critical { background-color: #FF752A; }
621 tr.critical:hover { background-color: #D63905;}
622
623 tr.fatal { background-color: #FF3300;}
624 tr.fatal:hover { background-color: #D60000;}
625
626 td.uuid { width: 5%; border-style: dotted;}
627 td.status { width: 5%; border-style: dotted;}
628 td.severity { width: 5%; border-style: dotted;}
629 td.summary { border-style: dotted;}
630 td.date { width: 25%; border-style: dotted;}
631
632 /* bug detail pages */
633
634 td.bug_detail_label { text-align: right; border: none;}
635 td.bug_detail { border: none;}
636 td.bug_comment_label { text-align: right; vertical-align: top; }
637 td.bug_comment { }
638
639 div.comment {
640   padding: 20px;
641   padding-top: 20px;
642   margin: auto;
643   margin-top: 0;
644 }
645
646 div.root.comment {
647   padding: 0px;
648   /* padding-top: 0px; */
649   padding-bottom: 20px;
650 }
651 """,
652 ##
653             'base.html':
654 """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
655   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
656 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
657   <head>
658     <title>{{ title }}</title>
659     <meta http-equiv="Content-Type" content="text/html; charset={{ charset }}" />
660     <link rel="stylesheet" href="{{ stylesheet }}" type="text/css" />
661   </head>
662   <body>
663     <div class="header">{{ header }}</div>
664     <div class="main">
665       {% block content %}{% endblock %}
666     </div>
667     <div class="footer">
668       <p>Generated by <a href="http://www.bugseverywhere.org/">
669       Bugs Everywhere</a> on {{ generation_time }}</p>
670       <p>
671         <a href="http://validator.w3.org/check?uri=referer">
672           Validate XHTML</a>&nbsp;|&nbsp;
673         <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">
674           Validate CSS</a>
675       </p>
676     </div>
677   </body>
678 </html>
679 """,
680             'index.html':
681 """{% extends "base.html" %}
682
683 {% block content %}
684 <table>
685   <tbody>
686     <tr>
687       <td class="{{ active_class }}"><a href="index.html">Active Bugs</a></td>
688       <td class="{{ inactive_class }}"><a href="index_inactive.html">Inactive Bugs</a></td>
689       <td class="{{ target_class }}"><a href="index_by_target.html">Divided by target</a></td>
690     </tr>
691   </tbody>
692 </table>
693 {% if bugs %}
694 {% block bug_table %}{% endblock %}
695 {% else %}
696 <p>No bugs.</p>
697 {% endif %}
698 {% endblock %}
699 """,
700 ##
701             'standard_index.html':
702 """{% extends "index.html" %}
703
704 {% block bug_table %}
705 <table class="bug_list">
706   <thead>
707     <tr>
708       <th>UUID</th>
709       <th>Status</th>
710       <th>Severity</th>
711       <th>Summary</th>
712       <th>Date</th>
713     </tr>
714   </thead>
715   <tbody>
716     {% for bug in bugs %}
717     {{ bug_entry.render({'bug':bug, 'dir':bug_dir(bug)}) }}
718     {% endfor %}
719   </tbody>
720 </table>
721 {% endblock %}
722 """,
723 ##
724         'target_index.html':
725 """{% extends "index.html" %}
726
727 {% block bug_table %}
728 {% for target,bugs in targets %}
729 <table class="target_list">
730   <thead>
731     <tr>
732       <th class="target_name" colspan="5">
733         Target: {{ target.summary|e }} ({{ target.status|e }})
734       </th>
735     </tr>
736     <tr>
737       <th>UUID</th>
738       <th>Status</th>
739       <th>Severity</th>
740       <th>Summary</th>
741       <th>Date</th>
742     </tr>
743   </thead>
744   <tbody>
745     {% for bug in bugs %}
746     {{ bug_entry.render({'bug':bug, 'dir':bug_dir(bug)}) }}
747     {% endfor %}
748   </tbody>
749 </table>
750 {% endfor %}
751 {% endblock %}
752 """,
753 ##
754             'index_bug_entry.html':
755 """<tr class="{{ bug.severity }}">
756   <td class="uuid"><a href="bugs/{{ dir }}/index.html">{{ bug.id.user()|e }}</a></td>
757   <td class="status"><a href="bugs/{{ dir }}/index.html">{{ bug.status|e }}</a></td>
758   <td class="severity"><a href="bugs/{{ dir }}/index.html">{{ bug.severity|e }}</a></td>
759   <td class="summary"><a href="bugs/{{ dir }}/index.html">{{ bug.summary|e }}</a></td>
760   <td class="date"><a href="bugs/{{ dir }}/index.html">{{ (bug.time_string or '')|e }}</a></td>
761 </tr>
762 """,
763 ##
764         'bug.html':
765 """{% extends "base.html" %}
766
767 {% block content %}
768 {{ backlinks.render({'up_link': up_link, 'index_type':index_type}) }}
769 <h1>Bug: {{ bug.id.user()|e }}</h1>
770
771 <table>
772   <tbody>
773     <tr><td class="bug_detail_label">ID :</td>
774         <td class="bug_detail">{{ bug.uuid|e }}</td></tr>
775     <tr><td class="bug_detail_label">Short name :</td>
776         <td class="bug_detail">{{ bug.id.user()|e }}</td></tr>
777     <tr><td class="bug_detail_label">Status :</td>
778         <td class="bug_detail">{{ bug.status|e }}</td></tr>
779     <tr><td class="bug_detail_label">Severity :</td>
780         <td class="bug_detail">{{ bug.severity|e }}</td></tr>
781     <tr><td class="bug_detail_label">Assigned :</td>
782         <td class="bug_detail">{{ (bug.assigned or '')|e }}</td></tr>
783     <tr><td class="bug_detail_label">Reporter :</td>
784         <td class="bug_detail">{{ (bug.reporter or '')|e }}</td></tr>
785     <tr><td class="bug_detail_label">Creator :</td>
786         <td class="bug_detail">{{ (bug.creator or '')|e }}</td></tr>
787     <tr><td class="bug_detail_label">Created :</td>
788         <td class="bug_detail">{{ (bug.time_string or '')|e }}</td></tr>
789     <tr><td class="bug_detail_label">Summary :</td>
790         <td class="bug_detail">{{ bug.summary|e }}</td></tr>
791   </tbody>
792 </table>
793
794 <hr/>
795
796 {% if comments %}
797 {% for depth,comment in comments %}
798 {% if depth == 0 %}
799 <div class="comment root" id="C{{ comment_dir(comment) }}">
800 {% else %}
801 <div class="comment" id="C{{ comment_dir(comment) }}">
802 {% endif %}
803 {{ comment_entry.render({
804        'depth':depth, 'bug': bug, 'comment':comment, 'comment_dir':comment_dir,
805        'format_body': format_body, 'div_close': div_close}) }}
806 {{ div_close(depth) }}
807 {% endfor %}
808 {% if comments[-1][0] > 0 %}
809 {{ div_close(0) }}
810 {% endif %}
811 {% else %}
812 <p>No comments.</p>
813 {% endif %}
814 {{ backlinks.render({'up_link': up_link, 'index_type': index_type}) }}
815 {% endblock %}
816 """,
817 ##
818             'bug_backlinks.html':
819 """<p class="backlink"><a href="{{ up_link }}">Back to {{ index_type }} Index</a></p>
820 <p class="backlink"><a href="../../index_by_target.html">Back to Target Index</a></p>
821 """,
822 ##
823             'bug_comment_entry.html':
824 """<table>
825   <tbody>
826     <tr>
827       <td class="bug_comment_label">Comment:</td>
828       <td class="bug_comment">
829         --------- Comment ---------<br/>
830         ID: {{ comment.uuid }}<br/>
831         Short name: {{ comment.id.user() }}<br/>
832         From: {{ (comment.author or '')|e }}<br/>
833         Date: {{ (comment.date or '')|e }}<br/>
834         <br/>
835         {{ format_body(bug, comment) }}
836       </td>
837     </tr>
838   </tbody>
839 </table>
840 """,
841             }
842
843         loader = DictLoader(self.template_dict)
844
845         if template_dir:
846             file_system_loader = FileSystemLoader(template_dir)
847             loader = ChoiceLoader([file_system_loader, loader])
848
849         self.template = Environment(loader=loader)
850
851
852 class _DivCloser (object):
853     def __init__(self, depth=0):
854         self.depth = depth
855
856     def __call__(self, depth):
857         ret = []
858         while self.depth >= depth:
859             self.depth -= 1
860             ret.append('</div>')
861         self.depth = depth
862         return '\n'.join(ret)