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.
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)
40 >>> cmd._storage = bd.storage
41 >>> cmd._setup_io = lambda i_enc,o_enc : None
42 >>> cmd.stdout = sys.stdout
45 >>> os.chdir(bd.storage.repo)
47 >>> os.path.exists('./html_export')
49 >>> os.path.exists('./html_export/index.html')
51 >>> os.path.exists('./html_export/index_inactive.html')
53 >>> os.path.exists('./html_export/bugs')
55 >>> os.path.exists('./html_export/bugs/a.html')
57 >>> os.path.exists('./html_export/bugs/b.html')
64 def __init__(self, *args, **kwargs):
65 libbe.command.Command.__init__(self, *args, **kwargs)
67 libbe.command.Option(name='output', short_name='o',
68 help='Set the output path (%default)',
69 arg=libbe.command.Argument(
70 name='output', metavar='DIR', default='./html_export',
71 completion_callback=libbe.command.util.complete_path)),
72 libbe.command.Option(name='template-dir', short_name='t',
73 help='Use a different template. Defaults to internal templates',
74 arg=libbe.command.Argument(
75 name='template-dir', metavar='DIR',
76 completion_callback=libbe.command.util.complete_path)),
77 libbe.command.Option(name='title',
78 help='Set the bug repository title (%default)',
79 arg=libbe.command.Argument(
80 name='title', metavar='STRING',
81 default='BugsEverywhere Issue Tracker')),
82 libbe.command.Option(name='index-header',
83 help='Set the index page headers (%default)',
84 arg=libbe.command.Argument(
85 name='index-header', metavar='STRING',
86 default='BugsEverywhere Bug List')),
87 libbe.command.Option(name='export-template', short_name='e',
88 help='Export the default template and exit.'),
89 libbe.command.Option(name='export-template-dir', short_name='d',
90 help='Set the directory for the template export (%default)',
91 arg=libbe.command.Argument(
92 name='export-template-dir', metavar='DIR',
93 default='./default-templates/',
94 completion_callback=libbe.command.util.complete_path)),
95 libbe.command.Option(name='verbose', short_name='v',
96 help='Verbose output, default is %default'),
99 def _run(self, **params):
100 if params['export-template'] == True:
101 html_gen.write_default_template(params['export-template-dir'])
103 bugdir = self._get_bugdir()
104 bugdir.load_all_bugs()
105 html_gen = HTMLGen(bugdir,
106 template=params['template-dir'],
107 title=params['title'],
108 index_header=params['index-header'],
109 verbose=params['verbose'],
111 html_gen.run(params['output'])
114 def _long_help(self):
116 Generate a set of html pages representing the current state of the bug
120 Html = HTML # alias for libbe.command.base.get_command_class()
122 class HTMLGen (object):
123 def __init__(self, bd, template=None,
124 title="Site Title", index_header="Index Header",
125 verbose=False, encoding=None, stdout=None,
127 self.generation_time = time.ctime()
130 self.template = "default"
132 self.template = os.path.abspath(os.path.expanduser(template))
134 self.index_header = index_header
135 self.verbose = verbose
138 self.encoding = encoding
140 self.encoding = libbe.util.encoding.get_filesystem_encoding()
141 self._load_default_templates()
143 self._load_user_templates()
145 def run(self, out_dir):
146 if self.verbose == True:
147 print >> self.stdout, \
148 'Creating the html output in %s using templates in %s' \
149 % (out_dir, self.template)
153 bugs = [b for b in self.bd]
155 bugs_active = [b for b in bugs if b.active == True]
156 bugs_inactive = [b for b in bugs if b.active != True]
158 self._create_output_directories(out_dir)
159 self._write_css_file()
162 up_link = '../index.html'
164 up_link = '../index_inactive.html'
165 self._write_bug_file(b, up_link)
166 self._write_index_file(
167 bugs_active, title=self.title,
168 index_header=self.index_header, bug_type='active')
169 self._write_index_file(
170 bugs_inactive, title=self.title,
171 index_header=self.index_header, bug_type='inactive')
173 def _create_output_directories(self, out_dir):
175 print >> self.stdout, 'Creating output directories'
176 self.out_dir = self._make_dir(out_dir)
177 self.out_dir_bugs = self._make_dir(
178 os.path.join(self.out_dir, 'bugs'))
180 def _write_css_file(self):
182 print >> self.stdout, 'Writing css file'
183 assert hasattr(self, 'out_dir'), \
184 'Must run after ._create_output_directories()'
185 self._write_file(self.css_file,
186 [self.out_dir,'style.css'])
188 def _write_bug_file(self, bug, up_link):
190 print >> self.stdout, '\tCreating bug file for %s' % bug.id.user()
191 assert hasattr(self, 'out_dir_bugs'), \
192 'Must run after ._create_output_directories()'
194 bug.load_comments(load_full=True)
195 comment_entries = self._generate_bug_comment_entries(bug)
196 filename = '%s.html' % bug.uuid
197 fullpath = os.path.join(self.out_dir_bugs, filename)
198 template_info = {'title':self.title,
199 'charset':self.encoding,
201 'shortname':bug.id.user(),
202 'comment_entries':comment_entries,
203 'generation_time':self.generation_time}
204 for attr in ['uuid', 'severity', 'status', 'assigned',
205 'reporter', 'creator', 'time_string', 'summary']:
206 template_info[attr] = self._escape(getattr(bug, attr))
207 self._write_file(self.bug_file % template_info, [fullpath])
209 def _generate_bug_comment_entries(self, bug):
210 assert hasattr(self, 'out_dir_bugs'), \
211 'Must run after ._create_output_directories()'
215 for depth,comment in bug.comment_root.thread(flatten=False):
216 while len(stack) > depth:
217 # pop non-parents off the stack
219 # close non-parent <div class="comment...
220 comment_entries.append('</div>\n')
221 assert len(stack) == depth
222 stack.append(comment)
224 comment_entries.append('<div class="comment root">')
226 comment_entries.append('<div class="comment">')
228 for attr in ['uuid', 'author', 'date', 'body']:
229 value = getattr(comment, attr)
232 if comment.content_type == 'text/html':
233 pass # no need to escape html...
234 elif comment.content_type.startswith('text/'):
235 value = '<pre>\n'+self._escape(value)+'\n</pre>'
236 elif comment.content_type.startswith('image/'):
238 value = '<img src="./%s/%s" />' \
239 % (bug.uuid, comment.uuid)
242 value = '<a href="./%s/%s">Link to %s file</a>.' \
243 % (bug.uuid, comment.uuid, comment.content_type)
244 if save_body == True:
245 per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid)
246 if not os.path.exists(per_bug_dir):
247 os.mkdir(per_bug_dir)
248 comment_path = os.path.join(per_bug_dir, comment.uuid)
250 '<Files %s>\n ForceType %s\n</Files>' \
251 % (comment.uuid, comment.content_type),
252 [per_bug_dir, '.htaccess'], mode='a')
253 self._write_file( # TODO: long_to_linked_user()
254 libbe.util.id.long_to_short_text(
255 [self.bd], comment.body),
256 [per_bug_dir, comment.uuid], mode='wb')
258 value = self._escape(value)
259 template_info[attr] = value
260 comment_entries.append(self.bug_comment_entry % template_info)
261 while len(stack) > 0:
263 comment_entries.append('</div>\n') # close every remaining <div class='comment...
264 return '\n'.join(comment_entries)
266 def _write_index_file(self, bugs, title, index_header, bug_type='active'):
268 print >> self.stdout, 'Writing %s index file for %d bugs' % (bug_type, len(bugs))
269 assert hasattr(self, 'out_dir'), 'Must run after ._create_output_directories()'
272 bug_entries = self._generate_index_bug_entries(bugs)
274 if bug_type == 'active':
275 filename = 'index.html'
276 elif bug_type == 'inactive':
277 filename = 'index_inactive.html'
279 raise Exception, 'Unrecognized bug_type: "%s"' % bug_type
280 template_info = {'title':title,
281 'index_header':index_header,
282 'charset':self.encoding,
283 'active_class':'tab sel',
284 'inactive_class':'tab nsel',
285 'bug_entries':bug_entries,
286 'generation_time':self.generation_time}
287 if bug_type == 'inactive':
288 template_info['active_class'] = 'tab nsel'
289 template_info['inactive_class'] = 'tab sel'
291 self._write_file(self.index_file % template_info,
292 [self.out_dir, filename])
294 def _generate_index_bug_entries(self, bugs):
298 print >> self.stdout, '\tCreating bug entry for %s' % bug.id.user()
299 template_info = {'shortname':bug.id.user()}
300 for attr in ['uuid', 'severity', 'status', 'assigned',
301 'reporter', 'creator', 'time_string', 'summary']:
302 template_info[attr] = self._escape(getattr(bug, attr))
303 bug_entries.append(self.index_bug_entry % template_info)
304 return '\n'.join(bug_entries)
306 def _escape(self, string):
311 codepoint = ord(char)
312 if codepoint in htmlentitydefs.codepoint2name:
313 char = '&%s;' % htmlentitydefs.codepoint2name[codepoint]
314 #else: xml.sax.saxutils.escape(char)
316 return ''.join(chars)
318 def _load_user_templates(self):
319 for filename,attr in [('style.css','css_file'),
320 ('index_file.tpl','index_file'),
321 ('index_bug_entry.tpl','index_bug_entry'),
322 ('bug_file.tpl','bug_file'),
323 ('bug_comment_entry.tpl','bug_comment_entry')]:
324 fullpath = os.path.join(self.template, filename)
325 if os.path.exists(fullpath):
326 setattr(self, attr, self._read_file([fullpath]))
328 def _make_dir(self, dir_path):
329 dir_path = os.path.abspath(os.path.expanduser(dir_path))
330 if not os.path.exists(dir_path):
332 os.makedirs(dir_path)
334 raise libbe.command.UserError(
335 'Cannot create output directory "%s".' % dir_path)
338 def _write_file(self, content, path_array, mode='w'):
339 return libbe.util.encoding.set_file_contents(
340 os.path.join(*path_array), content, mode, self.encoding)
342 def _read_file(self, path_array, mode='r'):
343 return libbe.util.encoding.get_file_contents(
344 os.path.join(*path_array), mode, self.encoding, decode=True)
346 def write_default_template(self, out_dir):
348 print >> self.stdout, 'Creating output directories'
349 self.out_dir = self._make_dir(out_dir)
351 print >> self.stdout, 'Creating css file'
352 self._write_css_file()
354 print >> self.stdout, 'Creating index_file.tpl file'
355 self._write_file(self.index_file,
356 [self.out_dir, 'index_file.tpl'])
358 print >> self.stdout, 'Creating index_bug_entry.tpl file'
359 self._write_file(self.index_bug_entry,
360 [self.out_dir, 'index_bug_entry.tpl'])
362 print >> self.stdout, 'Creating bug_file.tpl file'
363 self._write_file(self.bug_file,
364 [self.out_dir, 'bug_file.tpl'])
366 print >> self.stdout, 'Creating bug_comment_entry.tpl file'
367 self._write_file(self.bug_comment_entry,
368 [self.out_dir, 'bug_comment_entry.tpl'])
370 def _load_default_templates(self):
373 font-family: "lucida grande", "sans serif";
384 background-color: #fcfcfc;
400 border: 10px #313131;
415 padding-right: 0.5em;
420 img { border-style: none; }
424 background-color: #305275;
433 list-style-type: none;
441 text-decoration: none;
444 a { color: #003d41; }
445 a:visited { color: #553d41; }
446 .footer a { color: #508d91; }
448 /* bug index pages */
456 background-color: #afafaf;
457 border: 1px solid #afafaf;
461 td.nsel.tab { border: 0px; }
464 background-color: #afafaf;
465 border: 2px solid #afafaf;
468 .bug_list tr { width: auto; }
469 tr.wishlist { background-color: #B4FF9B; }
470 tr.minor { background-color: #FCFF98; }
471 tr.serious { background-color: #FFB648; }
472 tr.critical { background-color: #FF752A; }
473 tr.fatal { background-color: #FF3300; }
475 /* bug detail pages */
477 td.bug_detail_label { text-align: right; }
479 td.bug_comment_label { text-align: right; vertical-align: top; }
491 /* padding-top: 0px; */
492 padding-bottom: 20px;
496 self.index_file = """
497 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
498 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
499 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
501 <title>%(title)s</title>
502 <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
503 <link rel="stylesheet" href="style.css" type="text/css" />
508 <h1>%(index_header)s</h1>
513 <td class="%(active_class)s"><a href="index.html">Active Bugs</a></td>
514 <td class="%(inactive_class)s"><a href="index_inactive.html">Inactive Bugs</a></td>
518 <table class="bug_list">
528 <p>Generated by <a href="http://www.bugseverywhere.org/">
529 BugsEverywhere</a> on %(generation_time)s</p>
531 <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a> |
532 <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
540 self.index_bug_entry ="""
541 <tr class="%(severity)s">
542 <td><a href="bugs/%(uuid)s.html">%(shortname)s</a></td>
543 <td><a href="bugs/%(uuid)s.html">%(status)s</a></td>
544 <td><a href="bugs/%(uuid)s.html">%(severity)s</a></td>
545 <td><a href="bugs/%(uuid)s.html">%(summary)s</a></td>
546 <td><a href="bugs/%(uuid)s.html">%(time_string)s</a></td>
551 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
552 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
553 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
555 <title>%(title)s</title>
556 <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
557 <link rel="stylesheet" href="../style.css" type="text/css" />
562 <h1>BugsEverywhere Bug List</h1>
563 <h5><a href="%(up_link)s">Back to Index</a></h5>
564 <h2>Bug: %(shortname)s</h2>
568 <tr><td class="bug_detail_label">ID :</td>
569 <td class="bug_detail">%(uuid)s</td></tr>
570 <tr><td class="bug_detail_label">Short name :</td>
571 <td class="bug_detail">%(shortname)s</td></tr>
572 <tr><td class="bug_detail_label">Status :</td>
573 <td class="bug_detail">%(status)s</td></tr>
574 <tr><td class="bug_detail_label">Severity :</td>
575 <td class="bug_detail">%(severity)s</td></tr>
576 <tr><td class="bug_detail_label">Assigned :</td>
577 <td class="bug_detail">%(assigned)s</td></tr>
578 <tr><td class="bug_detail_label">Reporter :</td>
579 <td class="bug_detail">%(reporter)s</td></tr>
580 <tr><td class="bug_detail_label">Creator :</td>
581 <td class="bug_detail">%(creator)s</td></tr>
582 <tr><td class="bug_detail_label">Created :</td>
583 <td class="bug_detail">%(time_string)s</td></tr>
584 <tr><td class="bug_detail_label">Summary :</td>
585 <td class="bug_detail">%(summary)s</td></tr>
594 <h5><a href="%(up_link)s">Back to Index</a></h5>
597 <p>Generated by <a href="http://www.bugseverywhere.org/">
598 BugsEverywhere</a> on %(generation_time)s</p>
600 <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a> |
601 <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
609 self.bug_comment_entry ="""
612 <td class="bug_comment_label">Comment:</td>
613 <td class="bug_comment">
614 --------- Comment ---------<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')