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