1 # Copyright (C) 2009 Gianluca Montecchi <gian@grys.it>
2 # W. Trevor King <wking@drexel.edu>
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.
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.
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
24 def execute(args, manipulate_encodings=True, restrict_file_access=False,
28 >>> bd = bugdir.SimpleBugDir()
30 >>> execute([], manipulate_encodings=False)
31 >>> os.path.exists("./html_export")
33 >>> os.path.exists("./html_export/index.html")
35 >>> os.path.exists("./html_export/index_inactive.html")
37 >>> os.path.exists("./html_export/bugs")
39 >>> os.path.exists("./html_export/bugs/a.html")
41 >>> os.path.exists("./html_export/bugs/b.html")
46 options, args = parser.parse_args(args)
47 complete(options, args, parser)
48 cmdutil.default_complete(options, args, parser)
51 raise cmdutil.UsageError, 'Too many arguments.'
53 bd = bugdir.BugDir(from_disk=True,
54 manipulate_encodings=manipulate_encodings,
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)
63 html_gen.run(options.out_dir)
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',
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',
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)')
90 Generate a set of html pages representing the current state of the bug
95 return get_parser().help_str() + longhelp
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
102 class HTMLGen (object):
103 def __init__(self, bd, template=None, verbose=False, encoding=None,
104 title="Site Title", index_header="Index Header",
106 self.generation_time = time.ctime()
108 self.verbose = verbose
110 self.index_header = index_header
112 self.encoding = encoding
114 self.encoding = self.bd.encoding
116 self.template = "default"
118 self.template = os.path.abspath(os.path.expanduser(template))
119 self._load_default_templates()
122 self._load_user_templates()
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)
131 bugs = [b for b in self.bd]
133 bugs_active = [b for b in bugs if b.active == True]
134 bugs_inactive = [b for b in bugs if b.active != True]
136 self._create_output_directories(out_dir)
137 self._write_css_file()
140 up_link = "../index.html"
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")
151 def _create_output_directories(self, out_dir):
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"))
158 def _write_css_file(self):
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"])
166 def _write_bug_file(self, bug, up_link):
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()"
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,
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])
187 def _generate_bug_comment_entries(self, bug):
188 assert hasattr(self, "out_dir_bugs"), \
189 "Must run after ._create_output_directories()"
193 for depth,comment in bug.comment_root.thread(flatten=False):
194 while len(stack) > depth:
195 # pop non-parents off the stack
197 # close non-parent <div class="comment...
198 comment_entries.append("</div>\n")
199 assert len(stack) == depth
200 stack.append(comment)
202 comment_entries.append('<div class="comment root">')
204 comment_entries.append('<div class="comment">')
206 for attr in ['uuid', 'author', 'date', 'body']:
207 value = getattr(comment, attr)
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/'):
216 value = '<img src="./%s/%s" />' \
217 % (bug.uuid, comment.uuid)
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)
228 '<Files %s>\n ForceType %s\n</Files>' \
229 % (comment.uuid, comment.content_type),
230 [per_bug_dir, '.htaccess'], mode='a')
233 [per_bug_dir, comment.uuid], mode='wb')
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:
240 comment_entries.append("</div>\n") # close every remaining <div class="comment...
241 return '\n'.join(comment_entries)
243 def _write_index_file(self, bugs, title, index_header, bug_type="active"):
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()"
249 bug_entries = self._generate_index_bug_entries(bugs)
251 if bug_type == "active":
252 filename = "index.html"
253 elif bug_type == "inactive":
254 filename = "index_inactive.html"
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'
268 self._write_file(self.index_file % template_info,
269 [self.out_dir, filename])
271 def _generate_index_bug_entries(self, bugs):
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)
283 def _escape(self, string):
288 codepoint = ord(char)
289 if codepoint in htmlentitydefs.codepoint2name:
290 char = "&%s;" % htmlentitydefs.codepoint2name[codepoint]
291 #else: xml.sax.saxutils.escape(char)
293 return "".join(chars)
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]))
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):
309 os.makedirs(dir_path)
311 raise cmdutil.UsageError, "Cannot create output directory '%s'." % dir_path
314 def _write_file(self, content, path_array, mode='w'):
315 f = codecs.open(os.path.join(*path_array), mode, self.encoding)
319 def _read_file(self, path_array, mode='r'):
320 f = codecs.open(os.path.join(*path_array), mode, self.encoding)
325 def write_default_template(self, out_dir):
327 print "Creating output directories"
328 self.out_dir = self._make_dir(out_dir)
330 print "Creating css file"
331 self._write_css_file()
333 print "Creating index_file.tpl file"
334 self._write_file(self.index_file,
335 [self.out_dir, "index_file.tpl"])
337 print "Creating index_bug_entry.tpl file"
338 self._write_file(self.index_bug_entry,
339 [self.out_dir, "index_bug_entry.tpl"])
341 print "Creating bug_file.tpl file"
342 self._write_file(self.bug_file,
343 [self.out_dir, "bug_file.tpl"])
345 print "Creating bug_comment_entry.tpl file"
346 self._write_file(self.bug_comment_entry,
347 [self.out_dir, "bug_comment_entry.tpl"])
349 def _load_default_templates(self):
352 font-family: "lucida grande", "sans serif";
363 background-color: #fcfcfc;
379 border: 10px #313131;
394 padding-right: 0.5em;
399 img { border-style: none; }
403 background-color: #305275;
412 list-style-type: none;
420 text-decoration: none;
423 a { color: #003d41; }
424 a:visited { color: #553d41; }
425 .footer a { color: #508d91; }
427 /* bug index pages */
435 background-color: #afafaf;
436 border: 1px solid #afafaf;
440 td.nsel.tab { border: 0px; }
443 background-color: #afafaf;
444 border: 2px solid #afafaf;
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; }
454 /* bug detail pages */
456 td.bug_detail_label { text-align: right; }
458 td.bug_comment_label { text-align: right; vertical-align: top; }
470 /* padding-top: 0px; */
471 padding-bottom: 20px;
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">
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" />
487 <h1>%(index_header)s</h1>
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>
497 <table class="bug_list">
507 <p>Generated by <a href="http://www.bugseverywhere.org/">
508 BugsEverywhere</a> on %(generation_time)s</p>
510 <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a> |
511 <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
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>
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">
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" />
541 <h1>BugsEverywhere Bug List</h1>
542 <h5><a href="%(up_link)s">Back to Index</a></h5>
543 <h2>Bug: %(shortname)s</h2>
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>
573 <h5><a href="%(up_link)s">Back to Index</a></h5>
576 <p>Generated by <a href="http://www.bugseverywhere.org/">
577 BugsEverywhere</a> on %(generation_time)s</p>
579 <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a> |
580 <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
588 self.bug_comment_entry ="""
591 <td class="bug_comment_label">Comment:</td>
592 <td class="bug_comment">
593 --------- Comment ---------<br/>
595 From: %(author)s<br/>
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')