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