1 # Copyright (C) 2009-2010 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.
25 import xml.sax.saxutils
29 import libbe.command.util
30 import libbe.util.encoding
33 class HTML (libbe.command.Command):
34 """Generate a static HTML dump of the current repository status
37 >>> import libbe.bugdir
38 >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
39 >>> io = libbe.command.StringInputOutput()
40 >>> io.stdout = sys.stdout
41 >>> ui = libbe.command.UserInterface(io=io)
42 >>> ui.storage_callbacks.set_storage(bd.storage)
45 >>> ret = ui.run(cmd, {'output':os.path.join(bd.storage.repo, 'html_export')})
46 >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export'))
48 >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'index.html'))
50 >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'index_inactive.html'))
52 >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs'))
54 >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs', 'a.html'))
56 >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs', 'b.html'))
63 def __init__(self, *args, **kwargs):
64 libbe.command.Command.__init__(self, *args, **kwargs)
66 libbe.command.Option(name='output', short_name='o',
67 help='Set the output path (%default)',
68 arg=libbe.command.Argument(
69 name='output', metavar='DIR', default='./html_export',
70 completion_callback=libbe.command.util.complete_path)),
71 libbe.command.Option(name='template-dir', short_name='t',
72 help='Use a different template. Defaults to internal templates',
73 arg=libbe.command.Argument(
74 name='template-dir', metavar='DIR',
75 completion_callback=libbe.command.util.complete_path)),
76 libbe.command.Option(name='title',
77 help='Set the bug repository title (%default)',
78 arg=libbe.command.Argument(
79 name='title', metavar='STRING',
80 default='BugsEverywhere Issue Tracker')),
81 libbe.command.Option(name='index-header',
82 help='Set the index page headers (%default)',
83 arg=libbe.command.Argument(
84 name='index-header', metavar='STRING',
85 default='BugsEverywhere Bug List')),
86 libbe.command.Option(name='export-template', short_name='e',
87 help='Export the default template and exit.'),
88 libbe.command.Option(name='export-template-dir', short_name='d',
89 help='Set the directory for the template export (%default)',
90 arg=libbe.command.Argument(
91 name='export-template-dir', metavar='DIR',
92 default='./default-templates/',
93 completion_callback=libbe.command.util.complete_path)),
94 libbe.command.Option(name='verbose', short_name='v',
95 help='Verbose output, default is %default'),
98 def _run(self, **params):
99 if params['export-template'] == True:
100 html_gen.write_default_template(params['export-template-dir'])
102 bugdir = self._get_bugdir()
103 bugdir.load_all_bugs()
104 html_gen = HTMLGen(bugdir,
105 template=params['template-dir'],
106 title=params['title'],
107 index_header=params['index-header'],
108 verbose=params['verbose'],
110 html_gen.run(params['output'])
113 def _long_help(self):
115 Generate a set of html pages representing the current state of the bug
119 Html = HTML # alias for libbe.command.base.get_command_class()
121 class HTMLGen (object):
122 def __init__(self, bd, template=None,
123 title="Site Title", index_header="Index Header",
124 verbose=False, encoding=None, stdout=None,
126 self.generation_time = time.ctime()
129 self.template = "default"
131 self.template = os.path.abspath(os.path.expanduser(template))
133 self.index_header = index_header
134 self.verbose = verbose
137 self.encoding = encoding
139 self.encoding = libbe.util.encoding.get_filesystem_encoding()
140 self._load_default_templates()
142 self._load_user_templates()
144 def run(self, out_dir):
145 if self.verbose == True:
146 print >> self.stdout, \
147 'Creating the html output in %s using templates in %s' \
148 % (out_dir, self.template)
152 bugs = [b for b in self.bd]
154 bugs_active = [b for b in bugs if b.active == True]
155 bugs_inactive = [b for b in bugs if b.active != True]
157 self._create_output_directories(out_dir)
158 self._write_css_file()
161 up_link = '../index.html'
163 up_link = '../index_inactive.html'
164 self._write_bug_file(b, up_link)
165 self._write_index_file(
166 bugs_active, title=self.title,
167 index_header=self.index_header, bug_type='active')
168 self._write_index_file(
169 bugs_inactive, title=self.title,
170 index_header=self.index_header, bug_type='inactive')
172 def _create_output_directories(self, out_dir):
174 print >> self.stdout, 'Creating output directories'
175 self.out_dir = self._make_dir(out_dir)
176 self.out_dir_bugs = self._make_dir(
177 os.path.join(self.out_dir, 'bugs'))
179 def _write_css_file(self):
181 print >> self.stdout, 'Writing css file'
182 assert hasattr(self, 'out_dir'), \
183 'Must run after ._create_output_directories()'
184 self._write_file(self.css_file,
185 [self.out_dir,'style.css'])
187 def _write_bug_file(self, bug, up_link):
189 print >> self.stdout, '\tCreating bug file for %s' % bug.id.user()
190 assert hasattr(self, 'out_dir_bugs'), \
191 'Must run after ._create_output_directories()'
193 bug.load_comments(load_full=True)
194 comment_entries = self._generate_bug_comment_entries(bug)
195 filename = '%s.html' % bug.uuid
196 fullpath = os.path.join(self.out_dir_bugs, filename)
197 template_info = {'title':self.title,
198 'charset':self.encoding,
200 'shortname':bug.id.user(),
201 'comment_entries':comment_entries,
202 'generation_time':self.generation_time}
203 for attr in ['uuid', 'severity', 'status', 'assigned',
204 'reporter', 'creator', 'time_string', 'summary']:
205 template_info[attr] = self._escape(getattr(bug, attr))
206 self._write_file(self.bug_file % template_info, [fullpath])
208 def _generate_bug_comment_entries(self, bug):
209 assert hasattr(self, 'out_dir_bugs'), \
210 'Must run after ._create_output_directories()'
214 for depth,comment in bug.comment_root.thread(flatten=False):
215 while len(stack) > depth:
216 # pop non-parents off the stack
218 # close non-parent <div class="comment...
219 comment_entries.append('</div>\n')
220 assert len(stack) == depth
221 stack.append(comment)
223 comment_entries.append('<div class="comment root">')
225 comment_entries.append('<div class="comment">')
226 template_info = {'shortname': comment.id.user()}
227 for attr in ['uuid', 'author', 'date', 'body']:
228 value = getattr(comment, attr)
231 if comment.content_type == 'text/html':
232 pass # no need to escape html...
233 elif comment.content_type.startswith('text/'):
234 value = '<pre>\n'+self._escape(value)+'\n</pre>'
235 elif comment.content_type.startswith('image/'):
237 value = '<img src="./%s/%s" />' \
238 % (bug.uuid, comment.uuid)
241 value = '<a href="./%s/%s">Link to %s file</a>.' \
242 % (bug.uuid, comment.uuid, comment.content_type)
243 if save_body == True:
244 per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid)
245 if not os.path.exists(per_bug_dir):
246 os.mkdir(per_bug_dir)
247 comment_path = os.path.join(per_bug_dir, comment.uuid)
249 '<Files %s>\n ForceType %s\n</Files>' \
250 % (comment.uuid, comment.content_type),
251 [per_bug_dir, '.htaccess'], mode='a')
252 self._write_file( # TODO: long_to_linked_user()
253 libbe.util.id.long_to_short_text(
254 [self.bd], comment.body),
255 [per_bug_dir, comment.uuid], mode='wb')
257 value = self._escape(value)
258 template_info[attr] = value
259 comment_entries.append(self.bug_comment_entry % template_info)
260 while len(stack) > 0:
262 comment_entries.append('</div>\n') # close every remaining <div class='comment...
263 return '\n'.join(comment_entries)
265 def _write_index_file(self, bugs, title, index_header, bug_type='active'):
267 print >> self.stdout, 'Writing %s index file for %d bugs' % (bug_type, len(bugs))
268 assert hasattr(self, 'out_dir'), 'Must run after ._create_output_directories()'
271 bug_entries = self._generate_index_bug_entries(bugs)
273 if bug_type == 'active':
274 filename = 'index.html'
275 elif bug_type == 'inactive':
276 filename = 'index_inactive.html'
278 raise Exception, 'Unrecognized bug_type: "%s"' % bug_type
279 template_info = {'title':title,
280 'index_header':index_header,
281 'charset':self.encoding,
282 'active_class':'tab sel',
283 'inactive_class':'tab nsel',
284 'bug_entries':bug_entries,
285 'generation_time':self.generation_time}
286 if bug_type == 'inactive':
287 template_info['active_class'] = 'tab nsel'
288 template_info['inactive_class'] = 'tab sel'
290 self._write_file(self.index_file % template_info,
291 [self.out_dir, filename])
293 def _generate_index_bug_entries(self, bugs):
297 print >> self.stdout, '\tCreating bug entry for %s' % bug.id.user()
298 template_info = {'shortname':bug.id.user()}
299 for attr in ['uuid', 'severity', 'status', 'assigned',
300 'reporter', 'creator', 'time_string', 'summary']:
301 template_info[attr] = self._escape(getattr(bug, attr))
302 bug_entries.append(self.index_bug_entry % template_info)
303 return '\n'.join(bug_entries)
305 def _escape(self, string):
310 codepoint = ord(char)
311 if codepoint in htmlentitydefs.codepoint2name:
312 char = '&%s;' % htmlentitydefs.codepoint2name[codepoint]
313 #else: xml.sax.saxutils.escape(char)
315 return ''.join(chars)
317 def _load_user_templates(self):
318 for filename,attr in [('style.css','css_file'),
319 ('index_file.tpl','index_file'),
320 ('index_bug_entry.tpl','index_bug_entry'),
321 ('bug_file.tpl','bug_file'),
322 ('bug_comment_entry.tpl','bug_comment_entry')]:
323 fullpath = os.path.join(self.template, filename)
324 if os.path.exists(fullpath):
325 setattr(self, attr, self._read_file([fullpath]))
327 def _make_dir(self, dir_path):
328 dir_path = os.path.abspath(os.path.expanduser(dir_path))
329 if not os.path.exists(dir_path):
331 os.makedirs(dir_path)
333 raise libbe.command.UserError(
334 'Cannot create output directory "%s".' % dir_path)
337 def _write_file(self, content, path_array, mode='w'):
338 return libbe.util.encoding.set_file_contents(
339 os.path.join(*path_array), content, mode, self.encoding)
341 def _read_file(self, path_array, mode='r'):
342 return libbe.util.encoding.get_file_contents(
343 os.path.join(*path_array), mode, self.encoding, decode=True)
345 def write_default_template(self, out_dir):
347 print >> self.stdout, 'Creating output directories'
348 self.out_dir = self._make_dir(out_dir)
350 print >> self.stdout, 'Creating css file'
351 self._write_css_file()
353 print >> self.stdout, 'Creating index_file.tpl file'
354 self._write_file(self.index_file,
355 [self.out_dir, 'index_file.tpl'])
357 print >> self.stdout, 'Creating index_bug_entry.tpl file'
358 self._write_file(self.index_bug_entry,
359 [self.out_dir, 'index_bug_entry.tpl'])
361 print >> self.stdout, 'Creating bug_file.tpl file'
362 self._write_file(self.bug_file,
363 [self.out_dir, 'bug_file.tpl'])
365 print >> self.stdout, 'Creating bug_comment_entry.tpl file'
366 self._write_file(self.bug_comment_entry,
367 [self.out_dir, 'bug_comment_entry.tpl'])
369 def _load_default_templates(self):
372 font-family: "lucida grande", "sans serif";
383 background-color: #fcfcfc;
399 border: 10px #313131;
414 padding-right: 0.5em;
419 img { border-style: none; }
423 background-color: #305275;
432 list-style-type: none;
440 text-decoration: none;
443 a { color: #003d41; }
444 a:visited { color: #553d41; }
445 .footer a { color: #508d91; }
447 /* bug index pages */
455 background-color: #afafaf;
456 border: 1px solid #afafaf;
460 td.nsel.tab { border: 0px; }
463 background-color: #afafaf;
464 border: 2px solid #afafaf;
467 .bug_list tr { width: auto; }
468 tr.wishlist { background-color: #B4FF9B; }
469 tr.minor { background-color: #FCFF98; }
470 tr.serious { background-color: #FFB648; }
471 tr.critical { background-color: #FF752A; }
472 tr.fatal { background-color: #FF3300; }
474 /* bug detail pages */
476 td.bug_detail_label { text-align: right; }
478 td.bug_comment_label { text-align: right; vertical-align: top; }
490 /* padding-top: 0px; */
491 padding-bottom: 20px;
495 self.index_file = """
496 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
497 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
498 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
500 <title>%(title)s</title>
501 <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
502 <link rel="stylesheet" href="style.css" type="text/css" />
507 <h1>%(index_header)s</h1>
512 <td class="%(active_class)s"><a href="index.html">Active Bugs</a></td>
513 <td class="%(inactive_class)s"><a href="index_inactive.html">Inactive Bugs</a></td>
517 <table class="bug_list">
527 <p>Generated by <a href="http://www.bugseverywhere.org/">
528 BugsEverywhere</a> on %(generation_time)s</p>
530 <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a> |
531 <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
539 self.index_bug_entry ="""
540 <tr class="%(severity)s">
541 <td><a href="bugs/%(uuid)s.html">%(shortname)s</a></td>
542 <td><a href="bugs/%(uuid)s.html">%(status)s</a></td>
543 <td><a href="bugs/%(uuid)s.html">%(severity)s</a></td>
544 <td><a href="bugs/%(uuid)s.html">%(summary)s</a></td>
545 <td><a href="bugs/%(uuid)s.html">%(time_string)s</a></td>
550 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
551 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
552 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
554 <title>%(title)s</title>
555 <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
556 <link rel="stylesheet" href="../style.css" type="text/css" />
561 <h1>BugsEverywhere Bug List</h1>
562 <h5><a href="%(up_link)s">Back to Index</a></h5>
563 <h2>Bug: %(shortname)s</h2>
567 <tr><td class="bug_detail_label">ID :</td>
568 <td class="bug_detail">%(uuid)s</td></tr>
569 <tr><td class="bug_detail_label">Short name :</td>
570 <td class="bug_detail">%(shortname)s</td></tr>
571 <tr><td class="bug_detail_label">Status :</td>
572 <td class="bug_detail">%(status)s</td></tr>
573 <tr><td class="bug_detail_label">Severity :</td>
574 <td class="bug_detail">%(severity)s</td></tr>
575 <tr><td class="bug_detail_label">Assigned :</td>
576 <td class="bug_detail">%(assigned)s</td></tr>
577 <tr><td class="bug_detail_label">Reporter :</td>
578 <td class="bug_detail">%(reporter)s</td></tr>
579 <tr><td class="bug_detail_label">Creator :</td>
580 <td class="bug_detail">%(creator)s</td></tr>
581 <tr><td class="bug_detail_label">Created :</td>
582 <td class="bug_detail">%(time_string)s</td></tr>
583 <tr><td class="bug_detail_label">Summary :</td>
584 <td class="bug_detail">%(summary)s</td></tr>
593 <h5><a href="%(up_link)s">Back to Index</a></h5>
596 <p>Generated by <a href="http://www.bugseverywhere.org/">
597 BugsEverywhere</a> on %(generation_time)s</p>
599 <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a> |
600 <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
608 self.bug_comment_entry ="""
611 <td class="bug_comment_label">Comment:</td>
612 <td class="bug_comment">
613 --------- Comment ---------<br/>
615 Short name: %(shortname)s<br/>
616 From: %(author)s<br/>
625 # strip leading whitespace
626 for attr in ['css_file', 'index_file', 'index_bug_entry', 'bug_file',
627 'bug_comment_entry']:
628 value = getattr(self, attr)
629 value = value.replace('\n'+' '*12, '\n')
630 setattr(self, attr, value.strip()+'\n')