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 libbe.util.encoding
20 import codecs, os, os.path, re, string, time
21 import xml.sax.saxutils, htmlentitydefs
25 def execute(args, manipulate_encodings=True, restrict_file_access=False,
29 >>> bd = bugdir.SimpleBugDir()
31 >>> execute([], manipulate_encodings=False)
32 >>> os.path.exists("./html_export")
34 >>> os.path.exists("./html_export/index.html")
36 >>> os.path.exists("./html_export/index_inactive.html")
38 >>> os.path.exists("./html_export/bugs")
40 >>> os.path.exists("./html_export/bugs/a.html")
42 >>> os.path.exists("./html_export/bugs/b.html")
47 options, args = parser.parse_args(args)
48 complete(options, args, parser)
49 cmdutil.default_complete(options, args, parser)
52 raise cmdutil.UsageError, 'Too many arguments.'
54 bd = bugdir.BugDir(from_disk=True,
55 manipulate_encodings=manipulate_encodings,
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)
64 html_gen.run(options.out_dir)
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',
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',
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)')
91 Generate a set of html pages representing the current state of the bug
96 return get_parser().help_str() + longhelp
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
103 class HTMLGen (object):
104 def __init__(self, bd, template=None, verbose=False, encoding=None,
105 title="Site Title", index_header="Index Header",
107 self.generation_time = time.ctime()
109 self.verbose = verbose
111 self.index_header = index_header
113 self.encoding = encoding
115 self.encoding = self.bd.encoding
117 self.template = "default"
119 self.template = os.path.abspath(os.path.expanduser(template))
120 self._load_default_templates()
123 self._load_user_templates()
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)
132 bugs = [b for b in self.bd]
134 bugs_active = [b for b in bugs if b.active == True]
135 bugs_inactive = [b for b in bugs if b.active != True]
137 self._create_output_directories(out_dir)
138 self._write_css_file()
141 up_link = "../index.html"
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")
152 def _create_output_directories(self, out_dir):
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"))
159 def _write_css_file(self):
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"])
167 def _write_bug_file(self, bug, up_link):
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()"
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,
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])
188 def _generate_bug_comment_entries(self, bug):
189 assert hasattr(self, "out_dir_bugs"), \
190 "Must run after ._create_output_directories()"
194 for depth,comment in bug.comment_root.thread(flatten=False):
195 while len(stack) > depth:
196 # pop non-parents off the stack
198 # close non-parent <div class="comment...
199 comment_entries.append("</div>\n")
200 assert len(stack) == depth
201 stack.append(comment)
203 comment_entries.append('<div class="comment root">')
205 comment_entries.append('<div class="comment">')
207 for attr in ['uuid', 'author', 'date', 'body']:
208 value = getattr(comment, attr)
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/'):
217 value = '<img src="./%s/%s" />' \
218 % (bug.uuid, comment.uuid)
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)
229 '<Files %s>\n ForceType %s\n</Files>' \
230 % (comment.uuid, comment.content_type),
231 [per_bug_dir, '.htaccess'], mode='a')
234 [per_bug_dir, comment.uuid], mode='wb')
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:
241 comment_entries.append("</div>\n") # close every remaining <div class="comment...
242 return '\n'.join(comment_entries)
244 def _write_index_file(self, bugs, title, index_header, bug_type="active"):
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()"
250 bug_entries = self._generate_index_bug_entries(bugs)
252 if bug_type == "active":
253 filename = "index.html"
254 elif bug_type == "inactive":
255 filename = "index_inactive.html"
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'
269 self._write_file(self.index_file % template_info,
270 [self.out_dir, filename])
272 def _generate_index_bug_entries(self, bugs):
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)
284 def _escape(self, string):
289 codepoint = ord(char)
290 if codepoint in htmlentitydefs.codepoint2name:
291 char = "&%s;" % htmlentitydefs.codepoint2name[codepoint]
292 #else: xml.sax.saxutils.escape(char)
294 return "".join(chars)
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]))
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):
310 os.makedirs(dir_path)
312 raise cmdutil.UsageError, "Cannot create output directory '%s'." % dir_path
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)
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)
323 def write_default_template(self, out_dir):
325 print "Creating output directories"
326 self.out_dir = self._make_dir(out_dir)
328 print "Creating css file"
329 self._write_css_file()
331 print "Creating index_file.tpl file"
332 self._write_file(self.index_file,
333 [self.out_dir, "index_file.tpl"])
335 print "Creating index_bug_entry.tpl file"
336 self._write_file(self.index_bug_entry,
337 [self.out_dir, "index_bug_entry.tpl"])
339 print "Creating bug_file.tpl file"
340 self._write_file(self.bug_file,
341 [self.out_dir, "bug_file.tpl"])
343 print "Creating bug_comment_entry.tpl file"
344 self._write_file(self.bug_comment_entry,
345 [self.out_dir, "bug_comment_entry.tpl"])
347 def _load_default_templates(self):
350 font-family: "lucida grande", "sans serif";
361 background-color: #fcfcfc;
377 border: 10px #313131;
392 padding-right: 0.5em;
397 img { border-style: none; }
401 background-color: #305275;
410 list-style-type: none;
418 text-decoration: none;
421 a { color: #003d41; }
422 a:visited { color: #553d41; }
423 .footer a { color: #508d91; }
425 /* bug index pages */
433 background-color: #afafaf;
434 border: 1px solid #afafaf;
438 td.nsel.tab { border: 0px; }
441 background-color: #afafaf;
442 border: 2px solid #afafaf;
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; }
452 /* bug detail pages */
454 td.bug_detail_label { text-align: right; }
456 td.bug_comment_label { text-align: right; vertical-align: top; }
468 /* padding-top: 0px; */
469 padding-bottom: 20px;
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">
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" />
485 <h1>%(index_header)s</h1>
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>
495 <table class="bug_list">
505 <p>Generated by <a href="http://www.bugseverywhere.org/">
506 BugsEverywhere</a> on %(generation_time)s</p>
508 <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a> |
509 <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
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>
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">
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" />
539 <h1>BugsEverywhere Bug List</h1>
540 <h5><a href="%(up_link)s">Back to Index</a></h5>
541 <h2>Bug: %(shortname)s</h2>
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>
571 <h5><a href="%(up_link)s">Back to Index</a></h5>
574 <p>Generated by <a href="http://www.bugseverywhere.org/">
575 BugsEverywhere</a> on %(generation_time)s</p>
577 <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a> |
578 <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
586 self.bug_comment_entry ="""
589 <td class="bug_comment_label">Comment:</td>
590 <td class="bug_comment">
591 --------- Comment ---------<br/>
593 From: %(author)s<br/>
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')