Transitioned comment to Command format
[be.git] / libbe / command / html.py
1 # Copyright (C) 2009 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 """Generate a static HTML dump of the current repository status"""
18 from libbe import cmdutil, bugdir, bug
19 import libbe.util.encoding
20 import codecs, os, os.path, re, string, time
21 import xml.sax.saxutils, htmlentitydefs
22
23 __desc__ = __doc__
24
25 def execute(args, manipulate_encodings=True, restrict_file_access=False,
26             dir="."):
27     """
28     >>> import os
29     >>> bd = bugdir.SimpleBugDir()
30     >>> os.chdir(bd.root)
31     >>> execute([], manipulate_encodings=False)
32     >>> os.path.exists("./html_export")
33     True
34     >>> os.path.exists("./html_export/index.html")
35     True
36     >>> os.path.exists("./html_export/index_inactive.html")
37     True
38     >>> os.path.exists("./html_export/bugs")
39     True
40     >>> os.path.exists("./html_export/bugs/a.html")
41     True
42     >>> os.path.exists("./html_export/bugs/b.html")
43     True
44     >>> bd.cleanup()
45     """
46     parser = get_parser()
47     options, args = parser.parse_args(args)
48     complete(options, args, parser)
49     cmdutil.default_complete(options, args, parser)
50
51     if len(args) > 0:
52         raise cmdutil.UsageError, 'Too many arguments.'
53
54     bd = bugdir.BugDir(from_disk=True,
55                        manipulate_encodings=manipulate_encodings,
56                        root=dir)
57     bd.load_all_bugs()
58
59     html_gen = HTMLGen(bd, template=options.template, verbose=options.verbose,
60                        title=options.title, index_header=options.index_header)
61     if options.exp_template == True:
62         html_gen.write_default_template(options.exp_template_dir)
63         return
64     html_gen.run(options.out_dir)
65
66 def get_parser():
67     parser = cmdutil.CmdOptionParser('be html [options]')
68     parser.add_option('-o', '--output', metavar='DIR', dest='out_dir',
69         help='Set the output path (%default)', default='./html_export')
70     parser.add_option('-t', '--template-dir', metavar='DIR', dest='template',
71         help='Use a different template, defaults to internal templates',
72         default=None)
73     parser.add_option('--title', metavar='STRING', dest='title',
74         help='Set the bug repository title (%default)',
75         default='BugsEverywhere Issue Tracker')
76     parser.add_option('--index-header', metavar='STRING', dest='index_header',
77         help='Set the index page headers (%default)',
78         default='BugsEverywhere Bug List')
79     parser.add_option('-v', '--verbose',  action='store_true',
80         metavar='verbose', dest='verbose',
81         help='Verbose output, default is %default', default=False)
82     parser.add_option('-e', '--export-template',  action='store_true',
83         dest='exp_template',
84         help='Export the default template and exit.', default=False)
85     parser.add_option('-d', '--export-template-dir', metavar='DIR',
86         dest='exp_template_dir', default='./default-templates/',
87         help='Set the directory for the template export (%default)')
88     return parser
89
90 longhelp="""
91 Generate a set of html pages representing the current state of the bug
92 directory.
93 """
94
95 def help():
96     return get_parser().help_str() + longhelp
97
98 def complete(options, args, parser):
99     for option, value in cmdutil.option_value_pairs(options, parser):
100         if "--complete" in args:
101             raise cmdutil.GetCompletions() # no positional arguments for list
102
103 class HTMLGen (object):
104     def __init__(self, bd, template=None, verbose=False, encoding=None,
105                  title="Site Title", index_header="Index Header",
106                  ):
107         self.generation_time = time.ctime()
108         self.bd = bd
109         self.verbose = verbose
110         self.title = title
111         self.index_header = index_header
112         if encoding != None:
113             self.encoding = encoding
114         else:
115             self.encoding = self.bd.encoding
116         if template == None:
117             self.template = "default"
118         else:
119             self.template = os.path.abspath(os.path.expanduser(template))
120         self._load_default_templates()
121
122         if template != None:
123             self._load_user_templates()
124
125     def run(self, out_dir):
126         if self.verbose == True:
127             print "Creating the html output in %s using templates in %s" \
128                 % (out_dir, self.template)
129
130         bugs_active = []
131         bugs_inactive = []
132         bugs = [b for b in self.bd]
133         bugs.sort()
134         bugs_active = [b for b in bugs if b.active == True]
135         bugs_inactive = [b for b in bugs if b.active != True]
136
137         self._create_output_directories(out_dir)
138         self._write_css_file()
139         for b in bugs:
140             if b.active:
141                 up_link = "../index.html"
142             else:
143                 up_link = "../index_inactive.html"
144             self._write_bug_file(b, up_link)
145         self._write_index_file(
146             bugs_active, title=self.title,
147             index_header=self.index_header, bug_type="active")
148         self._write_index_file(
149             bugs_inactive, title=self.title,
150             index_header=self.index_header, bug_type="inactive")
151
152     def _create_output_directories(self, out_dir):
153         if self.verbose:
154             print "Creating output directories"
155         self.out_dir = self._make_dir(out_dir)
156         self.out_dir_bugs = self._make_dir(
157             os.path.join(self.out_dir, "bugs"))
158
159     def _write_css_file(self):
160         if self.verbose:
161             print "Writing css file"
162         assert hasattr(self, "out_dir"), \
163             "Must run after ._create_output_directories()"
164         self._write_file(self.css_file,
165                          [self.out_dir,"style.css"])
166
167     def _write_bug_file(self, bug, up_link):
168         if self.verbose:
169             print "\tCreating bug file for %s" % self.bd.bug_shortname(bug)
170         assert hasattr(self, "out_dir_bugs"), \
171             "Must run after ._create_output_directories()"
172
173         bug.load_comments(load_full=True)
174         comment_entries = self._generate_bug_comment_entries(bug)
175         filename = "%s.html" % bug.uuid
176         fullpath = os.path.join(self.out_dir_bugs, filename)
177         template_info = {'title':self.title,
178                          'charset':self.encoding,
179                          'up_link':up_link,
180                          'shortname':self.bd.bug_shortname(bug),
181                          'comment_entries':comment_entries,
182                          'generation_time':self.generation_time}
183         for attr in ['uuid', 'severity', 'status', 'assigned',
184                      'reporter', 'creator', 'time_string', 'summary']:
185             template_info[attr] = self._escape(getattr(bug, attr))
186         self._write_file(self.bug_file % template_info, [fullpath])
187
188     def _generate_bug_comment_entries(self, bug):
189         assert hasattr(self, "out_dir_bugs"), \
190             "Must run after ._create_output_directories()"
191
192         stack = []
193         comment_entries = []
194         for depth,comment in bug.comment_root.thread(flatten=False):
195             while len(stack) > depth:
196                 # pop non-parents off the stack
197                 stack.pop(-1)
198                 # close non-parent <div class="comment...
199                 comment_entries.append("</div>\n")
200             assert len(stack) == depth
201             stack.append(comment)
202             if depth == 0:
203                 comment_entries.append('<div class="comment root">')
204             else:
205                 comment_entries.append('<div class="comment">')
206             template_info = {}
207             for attr in ['uuid', 'author', 'date', 'body']:
208                 value = getattr(comment, attr)
209                 if attr == 'body':
210                     save_body = False
211                     if comment.content_type == 'text/html':
212                         pass # no need to escape html...
213                     elif comment.content_type.startswith('text/'):
214                         value = '<pre>\n'+self._escape(value)+'\n</pre>'
215                     elif comment.content_type.startswith('image/'):
216                         save_body = True
217                         value = '<img src="./%s/%s" />' \
218                             % (bug.uuid, comment.uuid)
219                     else:
220                         save_body = True
221                         value = '<a href="./%s/%s">Link to %s file</a>.' \
222                             % (bug.uuid, comment.uuid, comment.content_type)
223                     if save_body == True:
224                         per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid)
225                         if not os.path.exists(per_bug_dir):
226                             os.mkdir(per_bug_dir)
227                         comment_path = os.path.join(per_bug_dir, comment.uuid)
228                         self._write_file(
229                             '<Files %s>\n  ForceType %s\n</Files>' \
230                                 % (comment.uuid, comment.content_type),
231                             [per_bug_dir, '.htaccess'], mode='a')
232                         self._write_file(
233                             comment.body,
234                             [per_bug_dir, comment.uuid], mode='wb')
235                 else:
236                     value = self._escape(value)
237                 template_info[attr] = value
238             comment_entries.append(self.bug_comment_entry % template_info)
239         while len(stack) > 0:
240             stack.pop(-1)
241             comment_entries.append("</div>\n") # close every remaining <div class="comment...
242         return '\n'.join(comment_entries)
243
244     def _write_index_file(self, bugs, title, index_header, bug_type="active"):
245         if self.verbose:
246             print "Writing %s index file for %d bugs" % (bug_type, len(bugs))
247         assert hasattr(self, "out_dir"), "Must run after ._create_output_directories()"
248         esc = self._escape
249
250         bug_entries = self._generate_index_bug_entries(bugs)
251
252         if bug_type == "active":
253             filename = "index.html"
254         elif bug_type == "inactive":
255             filename = "index_inactive.html"
256         else:
257             raise Exception, "Unrecognized bug_type: '%s'" % bug_type
258         template_info = {'title':title,
259                          'index_header':index_header,
260                          'charset':self.encoding,
261                          'active_class':'tab sel',
262                          'inactive_class':'tab nsel',
263                          'bug_entries':bug_entries,
264                          'generation_time':self.generation_time}
265         if bug_type == "inactive":
266             template_info['active_class'] = 'tab nsel'
267             template_info['inactive_class'] = 'tab sel'
268
269         self._write_file(self.index_file % template_info,
270                          [self.out_dir, filename])
271
272     def _generate_index_bug_entries(self, bugs):
273         bug_entries = []
274         for bug in bugs:
275             if self.verbose:
276                 print "\tCreating bug entry for %s" % self.bd.bug_shortname(bug)
277             template_info = {'shortname':self.bd.bug_shortname(bug)}
278             for attr in ['uuid', 'severity', 'status', 'assigned',
279                          'reporter', 'creator', 'time_string', 'summary']:
280                 template_info[attr] = self._escape(getattr(bug, attr))
281             bug_entries.append(self.index_bug_entry % template_info)
282         return '\n'.join(bug_entries)
283
284     def _escape(self, string):
285         if string == None:
286             return ""
287         chars = []
288         for char in string:
289             codepoint = ord(char)
290             if codepoint in htmlentitydefs.codepoint2name:
291                 char = "&%s;" % htmlentitydefs.codepoint2name[codepoint]
292             #else: xml.sax.saxutils.escape(char)
293             chars.append(char)
294         return "".join(chars)
295
296     def _load_user_templates(self):
297         for filename,attr in [('style.css','css_file'),
298                               ('index_file.tpl','index_file'),
299                               ('index_bug_entry.tpl','index_bug_entry'),
300                               ('bug_file.tpl','bug_file'),
301                               ('bug_comment_entry.tpl','bug_comment_entry')]:
302             fullpath = os.path.join(self.template, filename)
303             if os.path.exists(fullpath):
304                 setattr(self, attr, self._read_file([fullpath]))
305
306     def _make_dir(self, dir_path):
307         dir_path = os.path.abspath(os.path.expanduser(dir_path))
308         if not os.path.exists(dir_path):
309             try:
310                 os.makedirs(dir_path)
311             except:
312                 raise cmdutil.UsageError, "Cannot create output directory '%s'." % dir_path
313         return dir_path
314
315     def _write_file(self, content, path_array, mode='w'):
316         return libbe.util.encoding.set_file_contents(
317             os.path.join(*path_array), content, mode, self.encoding)
318
319     def _read_file(self, path_array, mode='r'):
320         return libbe.util.encoding.get_file_contents(
321             os.path.join(*path_array), mode, self.encoding, decode=True)
322
323     def write_default_template(self, out_dir):
324         if self.verbose:
325             print "Creating output directories"
326         self.out_dir = self._make_dir(out_dir)
327         if self.verbose:
328             print "Creating css file"
329         self._write_css_file()
330         if self.verbose:
331             print "Creating index_file.tpl file"
332         self._write_file(self.index_file,
333                          [self.out_dir, "index_file.tpl"])
334         if self.verbose:
335             print "Creating index_bug_entry.tpl file"
336         self._write_file(self.index_bug_entry,
337                          [self.out_dir, "index_bug_entry.tpl"])
338         if self.verbose:
339             print "Creating bug_file.tpl file"
340         self._write_file(self.bug_file,
341                          [self.out_dir, "bug_file.tpl"])
342         if self.verbose:
343             print "Creating bug_comment_entry.tpl file"
344         self._write_file(self.bug_comment_entry,
345                          [self.out_dir, "bug_comment_entry.tpl"])
346
347     def _load_default_templates(self):
348         self.css_file = """
349             body {
350               font-family: "lucida grande", "sans serif";
351               color: #333;
352               width: auto;
353               margin: auto;
354             }
355
356             div.main {
357               padding: 20px;
358               margin: auto;
359               padding-top: 0;
360               margin-top: 1em;
361               background-color: #fcfcfc;
362             }
363
364             div.footer {
365               font-size: small;
366               padding-left: 20px;
367               padding-right: 20px;
368               padding-top: 5px;
369               padding-bottom: 5px;
370               margin: auto;
371               background: #305275;
372               color: #fffee7;
373             }
374
375             table {
376               border-style: solid;
377               border: 10px #313131;
378               border-spacing: 0;
379               width: auto;
380             }
381
382             tb { border: 1px; }
383
384             tr {
385               vertical-align: top;
386               width: auto;
387             }
388
389             td {
390               border-width: 0;
391               border-style: none;
392               padding-right: 0.5em;
393               padding-left: 0.5em;
394               width: auto;
395             }
396
397             img { border-style: none; }
398
399             h1 {
400               padding: 0.5em;
401               background-color: #305275;
402               margin-top: 0;
403               margin-bottom: 0;
404               color: #fff;
405               margin-left: -20px;
406               margin-right: -20px;
407             }
408
409             ul {
410               list-style-type: none;
411               padding: 0;
412             }
413
414             p { width: auto; }
415
416             a, a:visited {
417               background: inherit;
418               text-decoration: none;
419             }
420
421             a { color: #003d41; }
422             a:visited { color: #553d41; }
423             .footer a { color: #508d91; }
424
425             /* bug index pages */
426
427             td.tab {
428               padding-right: 1em;
429               padding-left: 1em;
430             }
431
432             td.sel.tab {
433               background-color: #afafaf;
434               border: 1px solid #afafaf;
435               font-weight:bold;
436             }
437
438             td.nsel.tab { border: 0px; }
439
440             table.bug_list {
441               background-color: #afafaf;
442               border: 2px solid #afafaf;
443             }
444
445             .bug_list tr { width: auto; }
446             tr.wishlist { background-color: #B4FF9B; }
447             tr.minor { background-color: #FCFF98; }
448             tr.serious { background-color: #FFB648; }
449             tr.critical { background-color: #FF752A; }
450             tr.fatal { background-color: #FF3300; }
451
452             /* bug detail pages */
453
454             td.bug_detail_label { text-align: right; }
455             td.bug_detail { }
456             td.bug_comment_label { text-align: right; vertical-align: top; }
457             td.bug_comment { }
458
459             div.comment {
460               padding: 20px;
461               padding-top: 20px;
462               margin: auto;
463               margin-top: 0;
464             }
465
466             div.root.comment {
467               padding: 0px;
468               /* padding-top: 0px; */
469               padding-bottom: 20px;
470             }
471        """
472
473         self.index_file = """
474             <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
475               "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
476             <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
477             <head>
478             <title>%(title)s</title>
479             <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
480             <link rel="stylesheet" href="style.css" type="text/css" />
481             </head>
482             <body>
483
484             <div class="main">
485             <h1>%(index_header)s</h1>
486             <p></p>
487             <table>
488
489             <tr>
490             <td class="%(active_class)s"><a href="index.html">Active Bugs</a></td>
491             <td class="%(inactive_class)s"><a href="index_inactive.html">Inactive Bugs</a></td>
492             </tr>
493
494             </table>
495             <table class="bug_list">
496             <tbody>
497
498             %(bug_entries)s
499
500             </tbody>
501             </table>
502             </div>
503
504             <div class="footer">
505             <p>Generated by <a href="http://www.bugseverywhere.org/">
506             BugsEverywhere</a> on %(generation_time)s</p>
507             <p>
508             <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a>&nbsp;|&nbsp;
509             <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
510             </p>
511             </div>
512
513             </body>
514             </html>
515         """
516
517         self.index_bug_entry ="""
518             <tr class="%(severity)s">
519               <td><a href="bugs/%(uuid)s.html">%(shortname)s</a></td>
520               <td><a href="bugs/%(uuid)s.html">%(status)s</a></td>
521               <td><a href="bugs/%(uuid)s.html">%(severity)s</a></td>
522               <td><a href="bugs/%(uuid)s.html">%(summary)s</a></td>
523               <td><a href="bugs/%(uuid)s.html">%(time_string)s</a></td>
524             </tr>
525         """
526
527         self.bug_file = """
528             <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
529               "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
530             <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
531             <head>
532             <title>%(title)s</title>
533             <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
534             <link rel="stylesheet" href="../style.css" type="text/css" />
535             </head>
536             <body>
537
538             <div class="main">
539             <h1>BugsEverywhere Bug List</h1>
540             <h5><a href="%(up_link)s">Back to Index</a></h5>
541             <h2>Bug: %(shortname)s</h2>
542             <table>
543             <tbody>
544
545             <tr><td class="bug_detail_label">ID :</td>
546                 <td class="bug_detail">%(uuid)s</td></tr>
547             <tr><td class="bug_detail_label">Short name :</td>
548                 <td class="bug_detail">%(shortname)s</td></tr>
549             <tr><td class="bug_detail_label">Status :</td>
550                 <td class="bug_detail">%(status)s</td></tr>
551             <tr><td class="bug_detail_label">Severity :</td>
552                 <td class="bug_detail">%(severity)s</td></tr>
553             <tr><td class="bug_detail_label">Assigned :</td>
554                 <td class="bug_detail">%(assigned)s</td></tr>
555             <tr><td class="bug_detail_label">Reporter :</td>
556                 <td class="bug_detail">%(reporter)s</td></tr>
557             <tr><td class="bug_detail_label">Creator :</td>
558                 <td class="bug_detail">%(creator)s</td></tr>
559             <tr><td class="bug_detail_label">Created :</td>
560                 <td class="bug_detail">%(time_string)s</td></tr>
561             <tr><td class="bug_detail_label">Summary :</td>
562                 <td class="bug_detail">%(summary)s</td></tr>
563             </tbody>
564             </table>
565
566             <hr/>
567
568             %(comment_entries)s
569
570             </div>
571             <h5><a href="%(up_link)s">Back to Index</a></h5>
572
573             <div class="footer">
574             <p>Generated by <a href="http://www.bugseverywhere.org/">
575             BugsEverywhere</a> on %(generation_time)s</p>
576             <p>
577             <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a>&nbsp;|&nbsp;
578             <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
579             </p>
580             </div>
581
582             </body>
583             </html>
584         """
585
586         self.bug_comment_entry ="""
587             <table>
588             <tr>
589               <td class="bug_comment_label">Comment:</td>
590               <td class="bug_comment">
591             --------- Comment ---------<br/>
592             Name: %(uuid)s<br/>
593             From: %(author)s<br/>
594             Date: %(date)s<br/>
595             <br/>
596             %(body)s
597               </td>
598             </tr>
599             </table>
600         """
601
602         # strip leading whitespace
603         for attr in ['css_file', 'index_file', 'index_bug_entry', 'bug_file',
604                      'bug_comment_entry']:
605             value = getattr(self, attr)
606             value = value.replace('\n'+' '*12, '\n')
607             setattr(self, attr, value.strip()+'\n')