# Copyright (C) 2009-2010 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.
import codecs
import htmlentitydefs
import os
import os.path
import re
import string
import time
import xml.sax.saxutils
import libbe
import libbe.command
import libbe.command.util
import libbe.util.encoding
class HTML (libbe.command.Command):
"""Generate a static HTML dump of the current repository status
>>> import sys
>>> import libbe.bugdir
>>> bd = libbe.bugdir.SimpleBugDir(memory=False)
>>> io = libbe.command.StringInputOutput()
>>> io.stdout = sys.stdout
>>> ui = libbe.command.UserInterface(io=io)
>>> ui.storage_callbacks.set_storage(bd.storage)
>>> cmd = HTML(ui=ui)
>>> ret = ui.run(cmd, {'output':os.path.join(bd.storage.repo, 'html_export')})
>>> os.path.exists(os.path.join(bd.storage.repo, 'html_export'))
True
>>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'index.html'))
True
>>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'index_inactive.html'))
True
>>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs'))
True
>>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs', 'a.html'))
True
>>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs', 'b.html'))
True
>>> ui.cleanup()
>>> bd.cleanup()
"""
name = 'html'
def __init__(self, *args, **kwargs):
libbe.command.Command.__init__(self, *args, **kwargs)
self.options.extend([
libbe.command.Option(name='output', short_name='o',
help='Set the output path (%default)',
arg=libbe.command.Argument(
name='output', metavar='DIR', default='./html_export',
completion_callback=libbe.command.util.complete_path)),
libbe.command.Option(name='template-dir', short_name='t',
help='Use a different template. Defaults to internal templates',
arg=libbe.command.Argument(
name='template-dir', metavar='DIR',
completion_callback=libbe.command.util.complete_path)),
libbe.command.Option(name='title',
help='Set the bug repository title (%default)',
arg=libbe.command.Argument(
name='title', metavar='STRING',
default='BugsEverywhere Issue Tracker')),
libbe.command.Option(name='index-header',
help='Set the index page headers (%default)',
arg=libbe.command.Argument(
name='index-header', metavar='STRING',
default='BugsEverywhere Bug List')),
libbe.command.Option(name='export-template', short_name='e',
help='Export the default template and exit.'),
libbe.command.Option(name='export-template-dir', short_name='d',
help='Set the directory for the template export (%default)',
arg=libbe.command.Argument(
name='export-template-dir', metavar='DIR',
default='./default-templates/',
completion_callback=libbe.command.util.complete_path)),
libbe.command.Option(name='verbose', short_name='v',
help='Verbose output, default is %default'),
])
def _run(self, **params):
if params['export-template'] == True:
html_gen.write_default_template(params['export-template-dir'])
return 0
bugdir = self._get_bugdir()
bugdir.load_all_bugs()
html_gen = HTMLGen(bugdir,
template=params['template-dir'],
title=params['title'],
index_header=params['index-header'],
verbose=params['verbose'],
stdout=self.stdout)
html_gen.run(params['output'])
return 0
def _long_help(self):
return """
Generate a set of html pages representing the current state of the bug
directory.
"""
Html = HTML # alias for libbe.command.base.get_command_class()
class HTMLGen (object):
def __init__(self, bd, template=None,
title="Site Title", index_header="Index Header",
verbose=False, encoding=None, stdout=None,
):
self.generation_time = time.ctime()
self.bd = bd
if template == None:
self.template = "default"
else:
self.template = os.path.abspath(os.path.expanduser(template))
self.title = title
self.index_header = index_header
self.verbose = verbose
self.stdout = stdout
if encoding != None:
self.encoding = encoding
else:
self.encoding = libbe.util.encoding.get_filesystem_encoding()
self._load_default_templates()
if template != None:
self._load_user_templates()
def run(self, out_dir):
if self.verbose == True:
print >> self.stdout, \
'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 >> self.stdout, '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 >> self.stdout, '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 >> self.stdout, '\tCreating bug file for %s' % bug.id.user()
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':bug.id.user(),
'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 = {'shortname': comment.id.user()}
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( # TODO: long_to_linked_user()
libbe.util.id.long_to_short_text(
[self.bd], 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 >> self.stdout, '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 >> self.stdout, '\tCreating bug entry for %s' % bug.id.user()
template_info = {'shortname':bug.id.user()}
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 libbe.command.UserError(
'Cannot create output directory "%s".' % dir_path)
return dir_path
def _write_file(self, content, path_array, mode='w'):
return libbe.util.encoding.set_file_contents(
os.path.join(*path_array), content, mode, self.encoding)
def _read_file(self, path_array, mode='r'):
return libbe.util.encoding.get_file_contents(
os.path.join(*path_array), mode, self.encoding, decode=True)
def write_default_template(self, out_dir):
if self.verbose:
print >> self.stdout, 'Creating output directories'
self.out_dir = self._make_dir(out_dir)
if self.verbose:
print >> self.stdout, 'Creating css file'
self._write_css_file()
if self.verbose:
print >> self.stdout, 'Creating index_file.tpl file'
self._write_file(self.index_file,
[self.out_dir, 'index_file.tpl'])
if self.verbose:
print >> self.stdout, 'Creating index_bug_entry.tpl file'
self._write_file(self.index_bug_entry,
[self.out_dir, 'index_bug_entry.tpl'])
if self.verbose:
print >> self.stdout, 'Creating bug_file.tpl file'
self._write_file(self.bug_file,
[self.out_dir, 'bug_file.tpl'])
if self.verbose:
print >> self.stdout, '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/>
ID: %(uuid)s<br/>
Short name: %(shortname)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')