47ee587b565d1d3eca3d566aa3a57f9804417fdb
[be.git] / libbe / command / html.py
1 # Copyright (C) 2009-2010 Gianluca Montecchi <gian@grys.it>
2 #                         W. Trevor King <wking@drexel.edu>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18 import codecs
19 import htmlentitydefs
20 import os
21 import os.path
22 import re
23 import string
24 import time
25 import xml.sax.saxutils
26
27 import libbe
28 import libbe.command
29 import libbe.command.util
30 import libbe.util.encoding
31 import libbe.util.id
32
33
34 class HTML (libbe.command.Command):
35     """Generate a static HTML dump of the current repository status
36
37     >>> import sys
38     >>> import libbe.bugdir
39     >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
40     >>> io = libbe.command.StringInputOutput()
41     >>> io.stdout = sys.stdout
42     >>> ui = libbe.command.UserInterface(io=io)
43     >>> ui.storage_callbacks.set_storage(bd.storage)
44     >>> cmd = HTML(ui=ui)
45
46     >>> ret = ui.run(cmd, {'output':os.path.join(bd.storage.repo, 'html_export')})
47     >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export'))
48     True
49     >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'index.html'))
50     True
51     >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'index_inactive.html'))
52     True
53     >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs'))
54     True
55     >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs', 'a.html'))
56     True
57     >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs', 'b.html'))
58     True
59     >>> ui.cleanup()
60     >>> bd.cleanup()
61     """
62     name = 'html'
63
64     def __init__(self, *args, **kwargs):
65         libbe.command.Command.__init__(self, *args, **kwargs)
66         self.options.extend([
67                 libbe.command.Option(name='output', short_name='o',
68                     help='Set the output path (%default)',
69                     arg=libbe.command.Argument(
70                         name='output', metavar='DIR', default='./html_export',
71                         completion_callback=libbe.command.util.complete_path)),
72                 libbe.command.Option(name='template-dir', short_name='t',
73                     help='Use a different template.  Defaults to internal templates',
74                     arg=libbe.command.Argument(
75                         name='template-dir', metavar='DIR',
76                         completion_callback=libbe.command.util.complete_path)),
77                 libbe.command.Option(name='title',
78                     help='Set the bug repository title (%default)',
79                     arg=libbe.command.Argument(
80                         name='title', metavar='STRING',
81                         default='BugsEverywhere Issue Tracker')),
82                 libbe.command.Option(name='index-header',
83                     help='Set the index page headers (%default)',
84                     arg=libbe.command.Argument(
85                         name='index-header', metavar='STRING',
86                         default='BugsEverywhere Bug List')),
87                 libbe.command.Option(name='export-template', short_name='e',
88                     help='Export the default template and exit.'),
89                 libbe.command.Option(name='export-template-dir', short_name='d',
90                     help='Set the directory for the template export (%default)',
91                     arg=libbe.command.Argument(
92                         name='export-template-dir', metavar='DIR',
93                         default='./default-templates/',
94                         completion_callback=libbe.command.util.complete_path)),
95                 libbe.command.Option(name='verbose', short_name='v',
96                     help='Verbose output, default is %default'),
97                 ])
98
99     def _run(self, **params):
100         if params['export-template'] == True:
101             html_gen.write_default_template(params['export-template-dir'])
102             return 0
103         bugdir = self._get_bugdir()
104         bugdir.load_all_bugs()
105         html_gen = HTMLGen(bugdir,
106                            template=params['template-dir'],
107                            title=params['title'],
108                            index_header=params['index-header'],
109                            verbose=params['verbose'],
110                            stdout=self.stdout)
111         html_gen.run(params['output'])
112         return 0
113
114     def _long_help(self):
115         return """
116 Generate a set of html pages representing the current state of the bug
117 directory.
118 """
119
120 Html = HTML # alias for libbe.command.base.get_command_class()
121
122 class HTMLGen (object):
123     def __init__(self, bd, template=None,
124                  title="Site Title", index_header="Index Header",
125                  verbose=False, encoding=None, stdout=None,
126                  ):
127         self.generation_time = time.ctime()
128         self.bd = bd
129         if template == None:
130             self.template = "default"
131         else:
132             self.template = os.path.abspath(os.path.expanduser(template))
133         self.title = title
134         self.index_header = index_header
135         self.verbose = verbose
136         self.stdout = stdout
137         if encoding != None:
138             self.encoding = encoding
139         else:
140             self.encoding = libbe.util.encoding.get_filesystem_encoding()
141         self._load_default_templates()
142         if template != None:
143             self._load_user_templates()
144
145     def run(self, out_dir):
146         if self.verbose == True:
147             print >> self.stdout, \
148                 'Creating the html output in %s using templates in %s' \
149                 % (out_dir, self.template)
150
151         bugs_active = []
152         bugs_inactive = []
153         bugs = [b for b in self.bd]
154         bugs.sort()
155         bugs_active = [b for b in bugs if b.active == True]
156         bugs_inactive = [b for b in bugs if b.active != True]
157
158         self._create_output_directories(out_dir)
159         self._write_css_file()
160         for b in bugs:
161             if b.active:
162                 up_link = '../index.html'
163             else:
164                 up_link = '../index_inactive.html'
165             self._write_bug_file(b, up_link)
166         self._write_index_file(
167             bugs_active, title=self.title,
168             index_header=self.index_header, bug_type='active')
169         self._write_index_file(
170             bugs_inactive, title=self.title,
171             index_header=self.index_header, bug_type='inactive')
172
173     def _create_output_directories(self, out_dir):
174         if self.verbose:
175             print >> self.stdout, 'Creating output directories'
176         self.out_dir = self._make_dir(out_dir)
177         self.out_dir_bugs = self._make_dir(
178             os.path.join(self.out_dir, 'bugs'))
179
180     def _write_css_file(self):
181         if self.verbose:
182             print >> self.stdout, 'Writing css file'
183         assert hasattr(self, 'out_dir'), \
184             'Must run after ._create_output_directories()'
185         self._write_file(self.css_file,
186                          [self.out_dir,'style.css'])
187
188     def _write_bug_file(self, bug, up_link):
189         if self.verbose:
190             print >> self.stdout, '\tCreating bug file for %s' % bug.id.user()
191         assert hasattr(self, 'out_dir_bugs'), \
192             'Must run after ._create_output_directories()'
193
194         bug.load_comments(load_full=True)
195         comment_entries = self._generate_bug_comment_entries(bug)
196         filename = '%s.html' % bug.uuid
197         fullpath = os.path.join(self.out_dir_bugs, filename)
198         template_info = {'title':self.title,
199                          'charset':self.encoding,
200                          'up_link':up_link,
201                          'shortname':bug.id.user(),
202                          'comment_entries':comment_entries,
203                          'generation_time':self.generation_time}
204         for attr in ['uuid', 'severity', 'status', 'assigned',
205                      'reporter', 'creator', 'time_string', 'summary']:
206             template_info[attr] = self._escape(getattr(bug, attr))
207         self._write_file(self.bug_file % template_info, [fullpath])
208
209     def _generate_bug_comment_entries(self, bug):
210         assert hasattr(self, 'out_dir_bugs'), \
211             'Must run after ._create_output_directories()'
212
213         stack = []
214         comment_entries = []
215         for depth,comment in bug.comment_root.thread(flatten=False):
216             while len(stack) > depth:
217                 # pop non-parents off the stack
218                 stack.pop(-1)
219                 # close non-parent <div class="comment...
220                 comment_entries.append('</div>\n')
221             assert len(stack) == depth
222             stack.append(comment)
223             if depth == 0:
224                 comment_entries.append('<div class="comment root">')
225             else:
226                 comment_entries.append(
227                     '<div class="comment" id="%s">' % comment.uuid)
228             template_info = {'shortname': comment.id.user()}
229             for attr in ['uuid', 'author', 'date', 'body']:
230                 value = getattr(comment, attr)
231                 if attr == 'body':
232                     link_long_ids = False
233                     save_body = False
234                     if comment.content_type == 'text/html':
235                         link_long_ids = True
236                     elif comment.content_type.startswith('text/'):
237                         value = '<pre>\n'+self._escape(value)+'\n</pre>'
238                         link_long_ids = True
239                     elif comment.content_type.startswith('image/'):
240                         save_body = True
241                         value = '<img src="./%s/%s" />' \
242                             % (bug.uuid, comment.uuid)
243                     else:
244                         save_body = True
245                         value = '<a href="./%s/%s">Link to %s file</a>.' \
246                             % (bug.uuid, comment.uuid, comment.content_type)
247                     if link_long_ids == True:
248                         value = self._long_to_linked_user(value)
249                     if save_body == True:
250                         per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid)
251                         if not os.path.exists(per_bug_dir):
252                             os.mkdir(per_bug_dir)
253                         comment_path = os.path.join(per_bug_dir, comment.uuid)
254                         self._write_file(
255                             '<Files %s>\n  ForceType %s\n</Files>' \
256                                 % (comment.uuid, comment.content_type),
257                             [per_bug_dir, '.htaccess'], mode='a')
258                         self._write_file(comment.body,
259                             [per_bug_dir, comment.uuid], mode='wb')
260                 else:
261                     value = self._escape(value)
262                 template_info[attr] = value
263             comment_entries.append(self.bug_comment_entry % template_info)
264         while len(stack) > 0:
265             stack.pop(-1)
266             comment_entries.append('</div>\n') # close every remaining <div class='comment...
267         return '\n'.join(comment_entries)
268
269     def _long_to_linked_user(self, text):
270         """
271         >>> import libbe.bugdir
272         >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
273         >>> h = HTMLGen(bd)
274         >>> h._long_to_linked_user('A link #abc123/a#, and a non-link #x#y#.')
275         'A link <a href="./a.html">abc/a</a>, and a non-link #x#y#.'
276         >>> bd.cleanup()
277         """
278         replacer = libbe.util.id.IDreplacer(
279             [self.bd], self._long_to_linked_user_replacer, wrap=False)
280         return re.sub(
281             libbe.util.id.REGEXP, replacer, text)
282
283     def _long_to_linked_user_replacer(self, bugdirs, long_id):
284         """
285         >>> import libbe.bugdir
286         >>> import libbe.util.id
287         >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
288         >>> a = bd.bug_from_uuid('a')
289         >>> uuid_gen = libbe.util.id.uuid_gen
290         >>> libbe.util.id.uuid_gen = lambda : '0123'
291         >>> c = a.new_comment('comment for link testing')
292         >>> libbe.util.id.uuid_gen = uuid_gen
293         >>> c.uuid
294         '0123'
295         >>> h = HTMLGen(bd)
296         >>> h._long_to_linked_user_replacer([bd], 'abc123')
297         '#abc123#'
298         >>> h._long_to_linked_user_replacer([bd], 'abc123/a')
299         '<a href="./a.html">abc/a</a>'
300         >>> h._long_to_linked_user_replacer([bd], 'abc123/a/0123')
301         '<a href="./a.html#0123">abc/a/012</a>'
302         >>> h._long_to_linked_user_replacer([bd], 'x')
303         '#x#'
304         >>> h._long_to_linked_user_replacer([bd], '')
305         '##'
306         >>> bd.cleanup()
307         """
308         try:
309             p = libbe.util.id.parse_user(bugdirs[0], long_id)
310             short_id = libbe.util.id.long_to_short_user(bugdirs, long_id)
311         except (libbe.util.id.MultipleIDMatches,
312                 libbe.util.id.NoIDMatches,
313                 libbe.util.id.InvalidIDStructure), e:
314             return '#%s#' % long_id # re-wrap failures
315         if p['type'] == 'bugdir':
316             return '#%s#' % long_id
317         elif p['type'] == 'bug':
318             return '<a href="./%s.html">%s</a>' \
319                 % (p['bug'], short_id)
320         elif p['type'] == 'comment':
321             return '<a href="./%s.html#%s">%s</a>' \
322                 % (p['bug'], p['comment'], short_id)
323         raise Exception('Invalid id type %s for "%s"'
324                         % (p['type'], long_id))
325
326     def _write_index_file(self, bugs, title, index_header, bug_type='active'):
327         if self.verbose:
328             print >> self.stdout, 'Writing %s index file for %d bugs' % (bug_type, len(bugs))
329         assert hasattr(self, 'out_dir'), 'Must run after ._create_output_directories()'
330         esc = self._escape
331
332         bug_entries = self._generate_index_bug_entries(bugs)
333
334         if bug_type == 'active':
335             filename = 'index.html'
336         elif bug_type == 'inactive':
337             filename = 'index_inactive.html'
338         else:
339             raise Exception, 'Unrecognized bug_type: "%s"' % bug_type
340         template_info = {'title':title,
341                          'index_header':index_header,
342                          'charset':self.encoding,
343                          'active_class':'tab sel',
344                          'inactive_class':'tab nsel',
345                          'bug_entries':bug_entries,
346                          'generation_time':self.generation_time}
347         if bug_type == 'inactive':
348             template_info['active_class'] = 'tab nsel'
349             template_info['inactive_class'] = 'tab sel'
350
351         self._write_file(self.index_file % template_info,
352                          [self.out_dir, filename])
353
354     def _generate_index_bug_entries(self, bugs):
355         bug_entries = []
356         for bug in bugs:
357             if self.verbose:
358                 print >> self.stdout, '\tCreating bug entry for %s' % bug.id.user()
359             template_info = {'shortname':bug.id.user()}
360             for attr in ['uuid', 'severity', 'status', 'assigned',
361                          'reporter', 'creator', 'time_string', 'summary']:
362                 template_info[attr] = self._escape(getattr(bug, attr))
363             bug_entries.append(self.index_bug_entry % template_info)
364         return '\n'.join(bug_entries)
365
366     def _escape(self, string):
367         if string == None:
368             return ''
369         chars = []
370         for char in string:
371             codepoint = ord(char)
372             if codepoint in htmlentitydefs.codepoint2name:
373                 char = '&%s;' % htmlentitydefs.codepoint2name[codepoint]
374             #else: xml.sax.saxutils.escape(char)
375             chars.append(char)
376         return ''.join(chars)
377
378     def _load_user_templates(self):
379         for filename,attr in [('style.css','css_file'),
380                               ('index_file.tpl','index_file'),
381                               ('index_bug_entry.tpl','index_bug_entry'),
382                               ('bug_file.tpl','bug_file'),
383                               ('bug_comment_entry.tpl','bug_comment_entry')]:
384             fullpath = os.path.join(self.template, filename)
385             if os.path.exists(fullpath):
386                 setattr(self, attr, self._read_file([fullpath]))
387
388     def _make_dir(self, dir_path):
389         dir_path = os.path.abspath(os.path.expanduser(dir_path))
390         if not os.path.exists(dir_path):
391             try:
392                 os.makedirs(dir_path)
393             except:
394                 raise libbe.command.UserError(
395                     'Cannot create output directory "%s".' % dir_path)
396         return dir_path
397
398     def _write_file(self, content, path_array, mode='w'):
399         return libbe.util.encoding.set_file_contents(
400             os.path.join(*path_array), content, mode, self.encoding)
401
402     def _read_file(self, path_array, mode='r'):
403         return libbe.util.encoding.get_file_contents(
404             os.path.join(*path_array), mode, self.encoding, decode=True)
405
406     def write_default_template(self, out_dir):
407         if self.verbose:
408             print >> self.stdout, 'Creating output directories'
409         self.out_dir = self._make_dir(out_dir)
410         if self.verbose:
411             print >> self.stdout, 'Creating css file'
412         self._write_css_file()
413         if self.verbose:
414             print >> self.stdout, 'Creating index_file.tpl file'
415         self._write_file(self.index_file,
416                          [self.out_dir, 'index_file.tpl'])
417         if self.verbose:
418             print >> self.stdout, 'Creating index_bug_entry.tpl file'
419         self._write_file(self.index_bug_entry,
420                          [self.out_dir, 'index_bug_entry.tpl'])
421         if self.verbose:
422             print >> self.stdout, 'Creating bug_file.tpl file'
423         self._write_file(self.bug_file,
424                          [self.out_dir, 'bug_file.tpl'])
425         if self.verbose:
426             print >> self.stdout, 'Creating bug_comment_entry.tpl file'
427         self._write_file(self.bug_comment_entry,
428                          [self.out_dir, 'bug_comment_entry.tpl'])
429
430     def _load_default_templates(self):
431         self.css_file = """
432             body {
433               font-family: "lucida grande", "sans serif";
434               color: #333;
435               width: auto;
436               margin: auto;
437             }
438
439             div.main {
440               padding: 20px;
441               margin: auto;
442               padding-top: 0;
443               margin-top: 1em;
444               background-color: #fcfcfc;
445             }
446
447             div.footer {
448               font-size: small;
449               padding-left: 20px;
450               padding-right: 20px;
451               padding-top: 5px;
452               padding-bottom: 5px;
453               margin: auto;
454               background: #305275;
455               color: #fffee7;
456             }
457
458             table {
459               border-style: solid;
460               border: 10px #313131;
461               border-spacing: 0;
462               width: auto;
463             }
464
465             tb { border: 1px; }
466
467             tr {
468               vertical-align: top;
469               width: auto;
470             }
471
472             td {
473               border-width: 0;
474               border-style: none;
475               padding-right: 0.5em;
476               padding-left: 0.5em;
477               width: auto;
478             }
479
480             img { border-style: none; }
481
482             h1 {
483               padding: 0.5em;
484               background-color: #305275;
485               margin-top: 0;
486               margin-bottom: 0;
487               color: #fff;
488               margin-left: -20px;
489               margin-right: -20px;
490             }
491
492             ul {
493               list-style-type: none;
494               padding: 0;
495             }
496
497             p { width: auto; }
498
499             a, a:visited {
500               background: inherit;
501               text-decoration: none;
502             }
503
504             a { color: #003d41; }
505             a:visited { color: #553d41; }
506             .footer a { color: #508d91; }
507
508             /* bug index pages */
509
510             td.tab {
511               padding-right: 1em;
512               padding-left: 1em;
513             }
514
515             td.sel.tab {
516               background-color: #afafaf;
517               border: 1px solid #afafaf;
518               font-weight:bold;
519             }
520
521             td.nsel.tab { border: 0px; }
522
523             table.bug_list {
524               background-color: #afafaf;
525               border: 2px solid #afafaf;
526             }
527
528             .bug_list tr { width: auto; }
529             tr.wishlist { background-color: #B4FF9B; }
530             tr.minor { background-color: #FCFF98; }
531             tr.serious { background-color: #FFB648; }
532             tr.critical { background-color: #FF752A; }
533             tr.fatal { background-color: #FF3300; }
534
535             /* bug detail pages */
536
537             td.bug_detail_label { text-align: right; }
538             td.bug_detail { }
539             td.bug_comment_label { text-align: right; vertical-align: top; }
540             td.bug_comment { }
541
542             div.comment {
543               padding: 20px;
544               padding-top: 20px;
545               margin: auto;
546               margin-top: 0;
547             }
548
549             div.root.comment {
550               padding: 0px;
551               /* padding-top: 0px; */
552               padding-bottom: 20px;
553             }
554        """
555
556         self.index_file = """
557             <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
558               "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
559             <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
560             <head>
561             <title>%(title)s</title>
562             <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
563             <link rel="stylesheet" href="style.css" type="text/css" />
564             </head>
565             <body>
566
567             <div class="main">
568             <h1>%(index_header)s</h1>
569             <p></p>
570             <table>
571
572             <tr>
573             <td class="%(active_class)s"><a href="index.html">Active Bugs</a></td>
574             <td class="%(inactive_class)s"><a href="index_inactive.html">Inactive Bugs</a></td>
575             </tr>
576
577             </table>
578             <table class="bug_list">
579             <tbody>
580
581             %(bug_entries)s
582
583             </tbody>
584             </table>
585             </div>
586
587             <div class="footer">
588             <p>Generated by <a href="http://www.bugseverywhere.org/">
589             BugsEverywhere</a> on %(generation_time)s</p>
590             <p>
591             <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a>&nbsp;|&nbsp;
592             <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
593             </p>
594             </div>
595
596             </body>
597             </html>
598         """
599
600         self.index_bug_entry ="""
601             <tr class="%(severity)s">
602               <td><a href="bugs/%(uuid)s.html">%(shortname)s</a></td>
603               <td><a href="bugs/%(uuid)s.html">%(status)s</a></td>
604               <td><a href="bugs/%(uuid)s.html">%(severity)s</a></td>
605               <td><a href="bugs/%(uuid)s.html">%(summary)s</a></td>
606               <td><a href="bugs/%(uuid)s.html">%(time_string)s</a></td>
607             </tr>
608         """
609
610         self.bug_file = """
611             <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
612               "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
613             <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
614             <head>
615             <title>%(title)s</title>
616             <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
617             <link rel="stylesheet" href="../style.css" type="text/css" />
618             </head>
619             <body>
620
621             <div class="main">
622             <h1>BugsEverywhere Bug List</h1>
623             <h5><a href="%(up_link)s">Back to Index</a></h5>
624             <h2>Bug: %(shortname)s</h2>
625             <table>
626             <tbody>
627
628             <tr><td class="bug_detail_label">ID :</td>
629                 <td class="bug_detail">%(uuid)s</td></tr>
630             <tr><td class="bug_detail_label">Short name :</td>
631                 <td class="bug_detail">%(shortname)s</td></tr>
632             <tr><td class="bug_detail_label">Status :</td>
633                 <td class="bug_detail">%(status)s</td></tr>
634             <tr><td class="bug_detail_label">Severity :</td>
635                 <td class="bug_detail">%(severity)s</td></tr>
636             <tr><td class="bug_detail_label">Assigned :</td>
637                 <td class="bug_detail">%(assigned)s</td></tr>
638             <tr><td class="bug_detail_label">Reporter :</td>
639                 <td class="bug_detail">%(reporter)s</td></tr>
640             <tr><td class="bug_detail_label">Creator :</td>
641                 <td class="bug_detail">%(creator)s</td></tr>
642             <tr><td class="bug_detail_label">Created :</td>
643                 <td class="bug_detail">%(time_string)s</td></tr>
644             <tr><td class="bug_detail_label">Summary :</td>
645                 <td class="bug_detail">%(summary)s</td></tr>
646             </tbody>
647             </table>
648
649             <hr/>
650
651             %(comment_entries)s
652
653             </div>
654             <h5><a href="%(up_link)s">Back to Index</a></h5>
655
656             <div class="footer">
657             <p>Generated by <a href="http://www.bugseverywhere.org/">
658             BugsEverywhere</a> on %(generation_time)s</p>
659             <p>
660             <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a>&nbsp;|&nbsp;
661             <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
662             </p>
663             </div>
664
665             </body>
666             </html>
667         """
668
669         self.bug_comment_entry ="""
670             <table>
671             <tr>
672               <td class="bug_comment_label">Comment:</td>
673               <td class="bug_comment">
674             --------- Comment ---------<br/>
675             ID: %(uuid)s<br/>
676             Short name: %(shortname)s<br/>
677             From: %(author)s<br/>
678             Date: %(date)s<br/>
679             <br/>
680             %(body)s
681               </td>
682             </tr>
683             </table>
684         """
685
686         # strip leading whitespace
687         for attr in ['css_file', 'index_file', 'index_bug_entry', 'bug_file',
688                      'bug_comment_entry']:
689             value = getattr(self, attr)
690             value = value.replace('\n'+' '*12, '\n')
691             setattr(self, attr, value.strip()+'\n')