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