Reported bug with utf-8 strings
[be.git] / becommands / html.py
1 # Copyright (C) 2009 Gianluca Montecchi <gian@grys.it>
2 #                    W. Trevor King <wking@drexel.edu>
3 #
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.
8 #
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.
13 #
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 #from html_data import *
20 import codecs, os, re, string, time
21 import xml.sax.saxutils, htmlentitydefs
22
23 __desc__ = __doc__
24
25 def execute(args, manipulate_encodings=True):
26     """
27     >>> import os
28     >>> bd = bugdir.SimpleBugDir()
29     >>> os.chdir(bd.root)
30     >>> execute([], manipulate_encodings=False)
31     Creating the html output in html_export
32     >>> os.path.exists("./html_export")
33     True
34     >>> os.path.exists("./html_export/index.html")
35     True
36     >>> os.path.exists("./html_export/index_inactive.html")
37     True
38     >>> os.path.exists("./html_export/bugs")
39     True
40     >>> os.path.exists("./html_export/bugs/a.html")
41     True
42     >>> os.path.exists("./html_export/bugs/b.html")
43     True
44     >>> bd.cleanup()
45     """
46     parser = get_parser()
47     options, args = parser.parse_args(args)
48     complete(options, args, parser)
49     cmdutil.default_complete(options, args, parser,
50                              bugid_args={0: lambda bug : bug.active==False})
51     
52     if len(args) == 0:
53         out_dir = options.outdir
54         print "Creating the html output in %s"%out_dir
55     else:
56         out_dir = args[0]
57     if len(args) > 0:
58         raise cmdutil.UsageError, "Too many arguments."
59     
60     bd = bugdir.BugDir(from_disk=True,
61                        manipulate_encodings=manipulate_encodings)
62     bd.load_all_bugs()
63     status_list = bug.status_values
64     severity_list = bug.severity_values
65     st = {}
66     se = {}
67     stime = {}
68     bugs_active = []
69     bugs_inactive = []
70     for s in status_list:
71         st[s] = 0
72     for b in sorted(bd, reverse=True):
73         stime[b.uuid]  = b.time
74         if b.active == True:
75             bugs_active.append(b)
76         else:
77             bugs_inactive.append(b)
78         st[b.status] += 1
79     ordered_bug_list = sorted([(value,key) for (key,value) in stime.items()])
80     ordered_bug_list_in = sorted([(value,key) for (key,value) in stime.items()])
81     #open_bug_list = sorted([(value,key) for (key,value) in bugs.items()])
82     
83     html_gen = BEHTMLGen(bd)
84     html_gen.create_index_file(out_dir,  st, bugs_active, ordered_bug_list, "active", bd.encoding)
85     html_gen.create_index_file(out_dir,  st, bugs_inactive, ordered_bug_list, "inactive", bd.encoding)
86     
87 def get_parser():
88     parser = cmdutil.CmdOptionParser("be open OUTPUT_DIR")
89     parser.add_option("-o", "--output", metavar="export_dir", dest="outdir",
90         help="Set the output path, default is ./html_export", default="html_export")    
91     return parser
92
93 longhelp="""
94 Generate a set of html pages representing the current state of the bug
95 directory.
96 """
97
98 def help():
99     return get_parser().help_str() + longhelp
100
101 def complete(options, args, parser):
102     for option, value in cmdutil.option_value_pairs(options, parser):
103         if "--complete" in args:
104             raise cmdutil.GetCompletions() # no positional arguments for list
105         
106
107 def escape(string):
108     if string == None:
109         return ""
110     chars = []
111     for char in xml.sax.saxutils.escape(string):
112         codepoint = ord(char)
113         if codepoint in htmlentitydefs.codepoint2name:
114             char = "&%s;" % htmlentitydefs.codepoint2name[codepoint]
115         chars.append(char)
116     return "".join(chars)
117
118 class BEHTMLGen():
119     def __init__(self, bd):
120         self.index_value = ""    
121         self.bd = bd
122         
123         self.css_file = """
124         body {
125         font-family: "lucida grande", "sans serif";
126         color: #333;
127         width: auto;
128         margin: auto;
129         }
130         
131         
132         div.main {
133         padding: 20px;
134         margin: auto;
135         padding-top: 0;
136         margin-top: 1em;
137         background-color: #fcfcfc;
138         }
139         
140         .comment {
141         padding: 20px;
142         margin: auto;
143         padding-top: 20px;
144         margin-top: 0;
145         }
146         
147         .commentF {
148         padding: 0px;
149         margin: auto;
150         padding-top: 0px;
151         paddin-bottom: 20px;
152         margin-top: 0;
153         }
154         
155         tb {
156         border = 1;
157         }
158         
159         .wishlist-row {
160         background-color: #B4FF9B;
161         width: auto;
162         }
163         
164         .minor-row {
165         background-color: #FCFF98;
166         width: auto;
167         }
168         
169         
170         .serious-row {
171         background-color: #FFB648;
172         width: auto;
173         }
174         
175         .critical-row {
176         background-color: #FF752A;
177         width: auto;
178         }
179         
180         .fatal-row {
181         background-color: #FF3300;
182         width: auto;
183         }
184                 
185         .person {
186         font-family: courier;
187         }
188         
189         a, a:visited {
190         background: inherit;
191         text-decoration: none;
192         }
193         
194         a {
195         color: #003d41;
196         }
197         
198         a:visited {
199         color: #553d41;
200         }
201         
202         ul {
203         list-style-type: none;
204         padding: 0;
205         }
206         
207         p {
208         width: auto;
209         }
210         
211         .inline-status-image {
212         position: relative;
213         top: 0.2em;
214         }
215         
216         .dimmed {
217         color: #bbb;
218         }
219         
220         table {
221         border-style: 10px solid #313131;
222         border-spacing: 0;
223         width: auto;
224         }
225         
226         table.log {
227         }
228         
229         td {
230         border-width: 0;
231         border-style: none;
232         padding-right: 0.5em;
233         padding-left: 0.5em;
234         width: auto;
235         }
236         
237         .td_sel {
238         background-color: #afafaf;
239         border: 1px solid #afafaf;
240         font-weight:bold;
241         padding-right: 1em;
242         padding-left: 1em;
243         
244         }
245         
246         .td_nsel {
247         border: 0px;
248         padding-right: 1em;
249         padding-left: 1em;
250         }
251         
252         tr {
253         vertical-align: top;
254         width: auto;
255         }
256         
257         h1 {
258         padding: 0.5em;
259         background-color: #305275;
260         margin-top: 0;
261         margin-bottom: 0;
262         color: #fff;
263         margin-left: -20px;
264         margin-right: -20px;  
265         }
266         
267         wid {
268         text-transform: uppercase;
269         font-size: smaller;
270         margin-top: 1em;
271         margin-left: -0.5em;  
272         /*background: #fffbce;*/
273         /*background: #628a0d;*/
274         padding: 5px;
275         color: #305275;
276         }
277         
278         .attrname {
279         text-align: right;
280         font-size: smaller;
281         }
282         
283         .attrval {
284         color: #222;
285         }
286         
287         .issue-closed-fixed {
288         background-image: "green-check.png";
289         }
290         
291         .issue-closed-wontfix {
292         background-image: "red-check.png";
293         }
294         
295         .issue-closed-reorg {
296         background-image: "blue-check.png";
297         }
298         
299         .inline-issue-link {
300         text-decoration: underline;
301         }
302         
303         img {
304         border: 0;
305         }
306         
307         
308         div.footer {
309         font-size: small;
310         padding-left: 20px;
311         padding-right: 20px;
312         padding-top: 5px;
313         padding-bottom: 5px;
314         margin: auto;
315         background: #305275;
316         color: #fffee7;
317         }
318         
319         .footer a {
320         color: #508d91;
321         }
322         
323         
324         .header {
325         font-family: "lucida grande", "sans serif";
326         font-size: smaller;
327         background-color: #a9a9a9;
328         text-align: left;
329         
330         padding-right: 0.5em;
331         padding-left: 0.5em;
332         
333         }
334         
335         
336         .selected-cell {
337         background-color: #e9e9e2;
338         }
339         
340         .plain-cell {
341         background-color: #f9f9f9;
342         }
343         
344         
345         .logcomment {
346         padding-left: 4em;
347         font-size: smaller;
348         }
349         
350         .id {
351         font-family: courier;
352         }
353         
354         .table_bug {
355         background-color: #afafaf;
356         border: 2px solid #afafaf;
357         }
358         
359         .message {
360         }
361         
362         .progress-meter-done {
363         background-color: #03af00;
364         }
365         
366         .progress-meter-undone {
367         background-color: #ddd;
368         }
369         
370         .progress-meter {
371         }
372         
373         """
374         
375         self.index_first = """
376         <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
377           "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
378         <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
379         <head>
380         <title>BugsEverywhere Issue Tracker</title>
381         <meta http-equiv="Content-Type" content="text/html; charset=%s" />
382         <link rel="stylesheet" href="style.css" type="text/css" />
383         </head>
384         <body>
385         
386         
387         <div class="main">
388         <h1>BugsEverywhere Bug List</h1>
389         <p></p>
390         <table>
391         
392         <tr>
393         <td class="%%s"><a href="index.html">Active Bugs</a></td>
394         <td class="%%s"><a href="index_inactive.html">Inactive Bugs</a></td>
395         </tr>
396         
397         </table>
398         <table class="table_bug">
399         <tbody>
400         """ % self.bd.encoding
401         
402         self.bug_line ="""
403         <tr class="%s-row">
404         <td ><a href="bugs/%s.html">%s</a></td>
405         <td ><a href="bugs/%s.html">%s</a></td>
406         <td><a href="bugs/%s.html">%s</a></td>
407         <td><a href="bugs/%s.html">%s</a></td>
408         <td><a href="bugs/%s.html">%s</a></td>
409         </tr>
410         """
411         
412         self.detail_first = """
413         <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
414           "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
415         <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
416         <head>
417         <title>BugsEverywhere Issue Tracker</title>
418         <meta http-equiv="Content-Type" content="text/html; charset=%s" />
419         <link rel="stylesheet" href="../style.css" type="text/css" />
420         </head>
421         <body>
422         
423         
424         <div class="main">
425         <h1>BugsEverywhere Bug List</h1>
426         <h5><a href="%%s">Back to Index</a></h5>
427         <h2>Bug: _bug_id_</h2>
428         <table >
429         <tbody>
430         """ % self.bd.encoding
431         
432         
433         
434         self.detail_line ="""
435         <tr>
436         <td align="right">%s</td><td>%s</td>
437         </tr>
438         """
439         
440         self.index_last = """
441         </tbody>
442         </table>
443         
444         </div>
445         
446         <div class="footer">Generated by <a href="http://www.bugseverywhere.org/">BugsEverywhere</a> on %s</div>
447         
448         </body>
449         </html>
450         """
451         
452         self.comment_section = """
453         """
454         
455         self.begin_comment_section ="""
456         <tr>
457         <td align="right">Comments:
458         </td>
459         <td>
460         """
461         
462         
463         self.end_comment_section ="""
464         </td>
465         </tr>
466         """
467         
468         self.detail_last = """
469         </tbody>
470         </table>
471         </div>
472         <h5><a href="%s">Back to Index</a></h5>
473         <div class="footer">Generated by <a href="http://www.bugseverywhere.org/">BugsEverywhere</a>.</div>
474         </body>
475         </html>
476         """   
477         
478         
479     def create_index_file(self, out_dir_path,  summary,  bugs, ordered_bug, fileid, encoding):
480         try:
481             os.stat(out_dir_path)
482         except:
483             try:
484                 os.mkdir(out_dir_path)
485             except:
486                 raise  cmdutil.UsageError, "Cannot create output directory."
487         try:
488             FO = codecs.open(out_dir_path+"/style.css", "w", encoding)
489             FO.write(self.css_file)
490             FO.close()
491         except:
492             raise  cmdutil.UsageError, "Cannot create the style.css file."
493         
494         try:
495             os.mkdir(out_dir_path+"/bugs")
496         except:
497             pass
498         
499         try:
500             if fileid == "active":
501                 FO = codecs.open(out_dir_path+"/index.html", "w", encoding)
502                 FO.write(self.index_first%('td_sel','td_nsel'))
503             if fileid == "inactive":
504                 FO = codecs.open(out_dir_path+"/index_inactive.html", "w", encoding)
505                 FO.write(self.index_first%('td_nsel','td_sel'))
506         except:
507             raise  cmdutil.UsageError, "Cannot create the index.html file."
508         
509         c = 0
510         t = len(bugs) - 1
511         for l in range(t,  -1,  -1):
512             line = self.bug_line%(escape(bugs[l].severity),
513                                   escape(bugs[l].uuid), escape(bugs[l].uuid[0:3]),
514                                   escape(bugs[l].uuid), escape(bugs[l].status),
515                                   escape(bugs[l].uuid), escape(bugs[l].severity),
516                                   escape(bugs[l].uuid), escape(bugs[l].summary),
517                                   escape(bugs[l].uuid), escape(bugs[l].time_string)
518                                   )
519             FO.write(line)
520             c += 1
521             self.create_detail_file(bugs[l], out_dir_path, fileid, encoding)
522         when = time.ctime()
523         FO.write(self.index_last%when)
524
525
526     def create_detail_file(self, bug, out_dir_path, fileid, encoding):
527         f = "%s.html"%bug.uuid
528         p = out_dir_path+"/bugs/"+f
529         try:
530             FD = codecs.open(p, "w", encoding)
531         except:
532             raise  cmdutil.UsageError, "Cannot create the detail html file."
533
534         detail_first_ = re.sub('_bug_id_', bug.uuid[0:3], self.detail_first)
535         if fileid == "active":
536             FD.write(detail_first_%"../index.html")
537         if fileid == "inactive":
538             FD.write(detail_first_%"../index_inactive.html")
539             
540         
541          
542         bug_ = self.bd.bug_from_shortname(bug.uuid)
543         bug_.load_comments(load_full=True)
544         
545         FD.write(self.detail_line%("ID : ", bug.uuid))
546         FD.write(self.detail_line%("Short name : ", escape(bug.uuid[0:3])))
547         FD.write(self.detail_line%("Severity : ", escape(bug.severity)))
548         FD.write(self.detail_line%("Status : ", escape(bug.status)))
549         FD.write(self.detail_line%("Assigned : ", escape(bug.assigned)))
550         FD.write(self.detail_line%("Target : ", escape(bug.target)))
551         FD.write(self.detail_line%("Reporter : ", escape(bug.reporter)))
552         FD.write(self.detail_line%("Creator : ", escape(bug.creator)))
553         FD.write(self.detail_line%("Created : ", escape(bug.time_string)))
554         FD.write(self.detail_line%("Summary : ", escape(bug.summary)))
555         FD.write("<tr><td colspan=\"2\"><hr /></td></tr>")
556         FD.write(self.begin_comment_section)
557         tr = []
558         b = ''
559         level = 0
560         stack = []
561         for depth,comment in bug_.comment_root.thread(flatten=False):
562             while len(stack) > depth:
563                 stack.pop(-1)      # pop non-parents off the stack
564                 FD.write("</div>\n") # close non-parent <div class="comment...
565             assert len(stack) == depth
566             stack.append(comment)
567             lines = ["--------- Comment ---------",
568                      "Name: %s" % comment.uuid,
569                      "From: %s" % escape(comment.author),
570                      "Date: %s" % escape(comment.date),
571                      ""]
572             lines.extend(escape(comment.body).splitlines())
573             if depth == 0:
574                 FD.write('<div class="commentF">')
575             else:
576                 FD.write('<div class="comment">')
577             FD.write("<br />\n".join(lines)+"<br />\n")
578         while len(stack) > 0:
579             stack.pop(-1)
580             FD.write("</div>\n") # close every remaining <div class="comment...
581         FD.write(self.end_comment_section)
582         if fileid == "active":
583             FD.write(self.detail_last%"../index.html")
584         if fileid == "inactive":
585             FD.write(self.detail_last%"../index_inactive.html")
586         FD.close()
587         
588