diff options
author | W. Trevor King <wking@drexel.edu> | 2009-12-07 20:07:55 -0500 |
---|---|---|
committer | W. Trevor King <wking@drexel.edu> | 2009-12-07 20:07:55 -0500 |
commit | 49a7771336ce09f6d42c7699ef32aecea0e83182 (patch) | |
tree | f237c7413fb68e72b1d87b0ccc4c788944168f10 /libbe/command/html.py | |
parent | c3bcafe12034d35f5c46f76a7dab97ab08b84dfd (diff) | |
download | bugseverywhere-49a7771336ce09f6d42c7699ef32aecea0e83182.tar.gz |
Initial directory restructuring to clarify dependencies
Diffstat (limited to 'libbe/command/html.py')
-rw-r--r-- | libbe/command/html.py | 609 |
1 files changed, 609 insertions, 0 deletions
diff --git a/libbe/command/html.py b/libbe/command/html.py new file mode 100644 index 0000000..d9e0d73 --- /dev/null +++ b/libbe/command/html.py @@ -0,0 +1,609 @@ +# Copyright (C) 2009 Gianluca Montecchi <gian@grys.it> +# W. Trevor King <wking@drexel.edu> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Generate a static HTML dump of the current repository status""" +from libbe import cmdutil, bugdir, bug +import codecs, os, os.path, re, string, time +import xml.sax.saxutils, htmlentitydefs + +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute([], manipulate_encodings=False) + >>> os.path.exists("./html_export") + True + >>> os.path.exists("./html_export/index.html") + True + >>> os.path.exists("./html_export/index_inactive.html") + True + >>> os.path.exists("./html_export/bugs") + True + >>> os.path.exists("./html_export/bugs/a.html") + True + >>> os.path.exists("./html_export/bugs/b.html") + True + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + cmdutil.default_complete(options, args, parser) + + if len(args) > 0: + raise cmdutil.UsageError, 'Too many arguments.' + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bd.load_all_bugs() + + html_gen = HTMLGen(bd, template=options.template, verbose=options.verbose, + title=options.title, index_header=options.index_header) + if options.exp_template == True: + html_gen.write_default_template(options.exp_template_dir) + return + html_gen.run(options.out_dir) + +def get_parser(): + parser = cmdutil.CmdOptionParser('be html [options]') + parser.add_option('-o', '--output', metavar='DIR', dest='out_dir', + help='Set the output path (%default)', default='./html_export') + parser.add_option('-t', '--template-dir', metavar='DIR', dest='template', + help='Use a different template, defaults to internal templates', + default=None) + parser.add_option('--title', metavar='STRING', dest='title', + help='Set the bug repository title (%default)', + default='BugsEverywhere Issue Tracker') + parser.add_option('--index-header', metavar='STRING', dest='index_header', + help='Set the index page headers (%default)', + default='BugsEverywhere Bug List') + parser.add_option('-v', '--verbose', action='store_true', + metavar='verbose', dest='verbose', + help='Verbose output, default is %default', default=False) + parser.add_option('-e', '--export-template', action='store_true', + dest='exp_template', + help='Export the default template and exit.', default=False) + parser.add_option('-d', '--export-template-dir', metavar='DIR', + dest='exp_template_dir', default='./default-templates/', + help='Set the directory for the template export (%default)') + return parser + +longhelp=""" +Generate a set of html pages representing the current state of the bug +directory. +""" + +def help(): + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if "--complete" in args: + raise cmdutil.GetCompletions() # no positional arguments for list + +class HTMLGen (object): + def __init__(self, bd, template=None, verbose=False, encoding=None, + title="Site Title", index_header="Index Header", + ): + self.generation_time = time.ctime() + self.bd = bd + self.verbose = verbose + self.title = title + self.index_header = index_header + if encoding != None: + self.encoding = encoding + else: + self.encoding = self.bd.encoding + if template == None: + self.template = "default" + else: + self.template = os.path.abspath(os.path.expanduser(template)) + self._load_default_templates() + + if template != None: + self._load_user_templates() + + def run(self, out_dir): + if self.verbose == True: + print "Creating the html output in %s using templates in %s" \ + % (out_dir, self.template) + + bugs_active = [] + bugs_inactive = [] + bugs = [b for b in self.bd] + bugs.sort() + bugs_active = [b for b in bugs if b.active == True] + bugs_inactive = [b for b in bugs if b.active != True] + + self._create_output_directories(out_dir) + self._write_css_file() + for b in bugs: + if b.active: + up_link = "../index.html" + else: + up_link = "../index_inactive.html" + self._write_bug_file(b, up_link) + self._write_index_file( + bugs_active, title=self.title, + index_header=self.index_header, bug_type="active") + self._write_index_file( + bugs_inactive, title=self.title, + index_header=self.index_header, bug_type="inactive") + + def _create_output_directories(self, out_dir): + if self.verbose: + print "Creating output directories" + self.out_dir = self._make_dir(out_dir) + self.out_dir_bugs = self._make_dir( + os.path.join(self.out_dir, "bugs")) + + def _write_css_file(self): + if self.verbose: + print "Writing css file" + assert hasattr(self, "out_dir"), \ + "Must run after ._create_output_directories()" + self._write_file(self.css_file, + [self.out_dir,"style.css"]) + + def _write_bug_file(self, bug, up_link): + if self.verbose: + print "\tCreating bug file for %s" % self.bd.bug_shortname(bug) + assert hasattr(self, "out_dir_bugs"), \ + "Must run after ._create_output_directories()" + + bug.load_comments(load_full=True) + comment_entries = self._generate_bug_comment_entries(bug) + filename = "%s.html" % bug.uuid + fullpath = os.path.join(self.out_dir_bugs, filename) + template_info = {'title':self.title, + 'charset':self.encoding, + 'up_link':up_link, + 'shortname':self.bd.bug_shortname(bug), + 'comment_entries':comment_entries, + 'generation_time':self.generation_time} + for attr in ['uuid', 'severity', 'status', 'assigned', + 'reporter', 'creator', 'time_string', 'summary']: + template_info[attr] = self._escape(getattr(bug, attr)) + self._write_file(self.bug_file % template_info, [fullpath]) + + def _generate_bug_comment_entries(self, bug): + assert hasattr(self, "out_dir_bugs"), \ + "Must run after ._create_output_directories()" + + stack = [] + comment_entries = [] + for depth,comment in bug.comment_root.thread(flatten=False): + while len(stack) > depth: + # pop non-parents off the stack + stack.pop(-1) + # close non-parent <div class="comment... + comment_entries.append("</div>\n") + assert len(stack) == depth + stack.append(comment) + if depth == 0: + comment_entries.append('<div class="comment root">') + else: + comment_entries.append('<div class="comment">') + template_info = {} + for attr in ['uuid', 'author', 'date', 'body']: + value = getattr(comment, attr) + if attr == 'body': + save_body = False + if comment.content_type == 'text/html': + pass # no need to escape html... + elif comment.content_type.startswith('text/'): + value = '<pre>\n'+self._escape(value)+'\n</pre>' + elif comment.content_type.startswith('image/'): + save_body = True + value = '<img src="./%s/%s" />' \ + % (bug.uuid, comment.uuid) + else: + save_body = True + value = '<a href="./%s/%s">Link to %s file</a>.' \ + % (bug.uuid, comment.uuid, comment.content_type) + if save_body == True: + per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid) + if not os.path.exists(per_bug_dir): + os.mkdir(per_bug_dir) + comment_path = os.path.join(per_bug_dir, comment.uuid) + self._write_file( + '<Files %s>\n ForceType %s\n</Files>' \ + % (comment.uuid, comment.content_type), + [per_bug_dir, '.htaccess'], mode='a') + self._write_file( + comment.body, + [per_bug_dir, comment.uuid], mode='wb') + else: + value = self._escape(value) + template_info[attr] = value + comment_entries.append(self.bug_comment_entry % template_info) + while len(stack) > 0: + stack.pop(-1) + comment_entries.append("</div>\n") # close every remaining <div class="comment... + return '\n'.join(comment_entries) + + def _write_index_file(self, bugs, title, index_header, bug_type="active"): + if self.verbose: + print "Writing %s index file for %d bugs" % (bug_type, len(bugs)) + assert hasattr(self, "out_dir"), "Must run after ._create_output_directories()" + esc = self._escape + + bug_entries = self._generate_index_bug_entries(bugs) + + if bug_type == "active": + filename = "index.html" + elif bug_type == "inactive": + filename = "index_inactive.html" + else: + raise Exception, "Unrecognized bug_type: '%s'" % bug_type + template_info = {'title':title, + 'index_header':index_header, + 'charset':self.encoding, + 'active_class':'tab sel', + 'inactive_class':'tab nsel', + 'bug_entries':bug_entries, + 'generation_time':self.generation_time} + if bug_type == "inactive": + template_info['active_class'] = 'tab nsel' + template_info['inactive_class'] = 'tab sel' + + self._write_file(self.index_file % template_info, + [self.out_dir, filename]) + + def _generate_index_bug_entries(self, bugs): + bug_entries = [] + for bug in bugs: + if self.verbose: + print "\tCreating bug entry for %s" % self.bd.bug_shortname(bug) + template_info = {'shortname':self.bd.bug_shortname(bug)} + for attr in ['uuid', 'severity', 'status', 'assigned', + 'reporter', 'creator', 'time_string', 'summary']: + template_info[attr] = self._escape(getattr(bug, attr)) + bug_entries.append(self.index_bug_entry % template_info) + return '\n'.join(bug_entries) + + def _escape(self, string): + if string == None: + return "" + chars = [] + for char in string: + codepoint = ord(char) + if codepoint in htmlentitydefs.codepoint2name: + char = "&%s;" % htmlentitydefs.codepoint2name[codepoint] + #else: xml.sax.saxutils.escape(char) + chars.append(char) + return "".join(chars) + + def _load_user_templates(self): + for filename,attr in [('style.css','css_file'), + ('index_file.tpl','index_file'), + ('index_bug_entry.tpl','index_bug_entry'), + ('bug_file.tpl','bug_file'), + ('bug_comment_entry.tpl','bug_comment_entry')]: + fullpath = os.path.join(self.template, filename) + if os.path.exists(fullpath): + setattr(self, attr, self._read_file([fullpath])) + + def _make_dir(self, dir_path): + dir_path = os.path.abspath(os.path.expanduser(dir_path)) + if not os.path.exists(dir_path): + try: + os.makedirs(dir_path) + except: + raise cmdutil.UsageError, "Cannot create output directory '%s'." % dir_path + return dir_path + + def _write_file(self, content, path_array, mode='w'): + f = codecs.open(os.path.join(*path_array), mode, self.encoding) + f.write(content) + f.close() + + def _read_file(self, path_array, mode='r'): + f = codecs.open(os.path.join(*path_array), mode, self.encoding) + content = f.read() + f.close() + return content + + def write_default_template(self, out_dir): + if self.verbose: + print "Creating output directories" + self.out_dir = self._make_dir(out_dir) + if self.verbose: + print "Creating css file" + self._write_css_file() + if self.verbose: + print "Creating index_file.tpl file" + self._write_file(self.index_file, + [self.out_dir, "index_file.tpl"]) + if self.verbose: + print "Creating index_bug_entry.tpl file" + self._write_file(self.index_bug_entry, + [self.out_dir, "index_bug_entry.tpl"]) + if self.verbose: + print "Creating bug_file.tpl file" + self._write_file(self.bug_file, + [self.out_dir, "bug_file.tpl"]) + if self.verbose: + print "Creating bug_comment_entry.tpl file" + self._write_file(self.bug_comment_entry, + [self.out_dir, "bug_comment_entry.tpl"]) + + def _load_default_templates(self): + self.css_file = """ + body { + font-family: "lucida grande", "sans serif"; + color: #333; + width: auto; + margin: auto; + } + + div.main { + padding: 20px; + margin: auto; + padding-top: 0; + margin-top: 1em; + background-color: #fcfcfc; + } + + div.footer { + font-size: small; + padding-left: 20px; + padding-right: 20px; + padding-top: 5px; + padding-bottom: 5px; + margin: auto; + background: #305275; + color: #fffee7; + } + + table { + border-style: solid; + border: 10px #313131; + border-spacing: 0; + width: auto; + } + + tb { border: 1px; } + + tr { + vertical-align: top; + width: auto; + } + + td { + border-width: 0; + border-style: none; + padding-right: 0.5em; + padding-left: 0.5em; + width: auto; + } + + img { border-style: none; } + + h1 { + padding: 0.5em; + background-color: #305275; + margin-top: 0; + margin-bottom: 0; + color: #fff; + margin-left: -20px; + margin-right: -20px; + } + + ul { + list-style-type: none; + padding: 0; + } + + p { width: auto; } + + a, a:visited { + background: inherit; + text-decoration: none; + } + + a { color: #003d41; } + a:visited { color: #553d41; } + .footer a { color: #508d91; } + + /* bug index pages */ + + td.tab { + padding-right: 1em; + padding-left: 1em; + } + + td.sel.tab { + background-color: #afafaf; + border: 1px solid #afafaf; + font-weight:bold; + } + + td.nsel.tab { border: 0px; } + + table.bug_list { + background-color: #afafaf; + border: 2px solid #afafaf; + } + + .bug_list tr { width: auto; } + tr.wishlist { background-color: #B4FF9B; } + tr.minor { background-color: #FCFF98; } + tr.serious { background-color: #FFB648; } + tr.critical { background-color: #FF752A; } + tr.fatal { background-color: #FF3300; } + + /* bug detail pages */ + + td.bug_detail_label { text-align: right; } + td.bug_detail { } + td.bug_comment_label { text-align: right; vertical-align: top; } + td.bug_comment { } + + div.comment { + padding: 20px; + padding-top: 20px; + margin: auto; + margin-top: 0; + } + + div.root.comment { + padding: 0px; + /* padding-top: 0px; */ + padding-bottom: 20px; + } + """ + + self.index_file = """ + <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <title>%(title)s</title> + <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" /> + <link rel="stylesheet" href="style.css" type="text/css" /> + </head> + <body> + + <div class="main"> + <h1>%(index_header)s</h1> + <p></p> + <table> + + <tr> + <td class="%(active_class)s"><a href="index.html">Active Bugs</a></td> + <td class="%(inactive_class)s"><a href="index_inactive.html">Inactive Bugs</a></td> + </tr> + + </table> + <table class="bug_list"> + <tbody> + + %(bug_entries)s + + </tbody> + </table> + </div> + + <div class="footer"> + <p>Generated by <a href="http://www.bugseverywhere.org/"> + BugsEverywhere</a> on %(generation_time)s</p> + <p> + <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a> | + <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a> + </p> + </div> + + </body> + </html> + """ + + self.index_bug_entry =""" + <tr class="%(severity)s"> + <td><a href="bugs/%(uuid)s.html">%(shortname)s</a></td> + <td><a href="bugs/%(uuid)s.html">%(status)s</a></td> + <td><a href="bugs/%(uuid)s.html">%(severity)s</a></td> + <td><a href="bugs/%(uuid)s.html">%(summary)s</a></td> + <td><a href="bugs/%(uuid)s.html">%(time_string)s</a></td> + </tr> + """ + + self.bug_file = """ + <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <title>%(title)s</title> + <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" /> + <link rel="stylesheet" href="../style.css" type="text/css" /> + </head> + <body> + + <div class="main"> + <h1>BugsEverywhere Bug List</h1> + <h5><a href="%(up_link)s">Back to Index</a></h5> + <h2>Bug: %(shortname)s</h2> + <table> + <tbody> + + <tr><td class="bug_detail_label">ID :</td> + <td class="bug_detail">%(uuid)s</td></tr> + <tr><td class="bug_detail_label">Short name :</td> + <td class="bug_detail">%(shortname)s</td></tr> + <tr><td class="bug_detail_label">Status :</td> + <td class="bug_detail">%(status)s</td></tr> + <tr><td class="bug_detail_label">Severity :</td> + <td class="bug_detail">%(severity)s</td></tr> + <tr><td class="bug_detail_label">Assigned :</td> + <td class="bug_detail">%(assigned)s</td></tr> + <tr><td class="bug_detail_label">Reporter :</td> + <td class="bug_detail">%(reporter)s</td></tr> + <tr><td class="bug_detail_label">Creator :</td> + <td class="bug_detail">%(creator)s</td></tr> + <tr><td class="bug_detail_label">Created :</td> + <td class="bug_detail">%(time_string)s</td></tr> + <tr><td class="bug_detail_label">Summary :</td> + <td class="bug_detail">%(summary)s</td></tr> + </tbody> + </table> + + <hr/> + + %(comment_entries)s + + </div> + <h5><a href="%(up_link)s">Back to Index</a></h5> + + <div class="footer"> + <p>Generated by <a href="http://www.bugseverywhere.org/"> + BugsEverywhere</a> on %(generation_time)s</p> + <p> + <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a> | + <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a> + </p> + </div> + + </body> + </html> + """ + + self.bug_comment_entry =""" + <table> + <tr> + <td class="bug_comment_label">Comment:</td> + <td class="bug_comment"> + --------- Comment ---------<br/> + Name: %(uuid)s<br/> + From: %(author)s<br/> + Date: %(date)s<br/> + <br/> + %(body)s + </td> + </tr> + </table> + """ + + # strip leading whitespace + for attr in ['css_file', 'index_file', 'index_bug_entry', 'bug_file', + 'bug_comment_entry']: + value = getattr(self, attr) + value = value.replace('\n'+' '*12, '\n') + setattr(self, attr, value.strip()+'\n') |