aboutsummaryrefslogblamecommitdiffstats
path: root/becommands/html.py
blob: dd7ce6567bca05f7506599b082199cbf7090ea14 (plain) (tree)
1
2
3
4
5
6
7
8

                                                      
 



                                                                      
 



                                                                 
 


                                                                         
                                                                  
                                      
                                            
                                       


                  
                                             

                 
                                  
                         
                                               












                                                           


                                           
                                   
                                                   
 
                     
                                                       
 
                               

                                                                 
                      



                                                                              
 
                 












                                                                                                 


                 

                                                                      



                                             




                                                                             
 




                                                                       
                    
                              





                                            


                                     
                                                                         
                                      
 







































                                                                                               
 






                                                                                       
 




                                                                                            
 





























                                                                                             
 













                                                                        
 




                                                                                       
 

























                                                                              
 


                                                                                 
 






























                                                                             
             
 





                                        
             
 








                                  
             
 
                   



                                   
             
 
                               
 
                

                                  
             
 





                                   
             
 
                                       
 







                                        
             
 


                                    
             
 
                              
 


                                    
             
 


                                         
 
                                 
 


                                 
             
 



                                        
             
 
                                        
 


                                        
             
 





                                                      
 
                                  
 



                                                                            
 




                                
             
 



                                     
             
          
 
                             
                                                                    
                                                                  

                                                                               
                                    
                                                                                       


                                                                      
 
                              
                                     

                   
 
                

                                                                                               
                 
 
                    
                                    
                   
 
                           
 

                    
                  
 


                                                                     



                                                                                               
                  
 


                   
 







                                                                       
           
 





































                                                                                       
 
                 
 
                               
 

                                                            
 







                                                                                               
 


                   
 














                                                         
 





                                                                             
# Copyright (C) 2009 Gianluca Montecchi <gian@grys.it>
#                    W. Trevor King <wking@drexel.edu>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Generate a static HTML dump of the current repository status"""
from libbe import cmdutil, bugdir, bug
import codecs, os, os.path, re, string, time
import xml.sax.saxutils, htmlentitydefs

__desc__ = __doc__

def execute(args, manipulate_encodings=True):
    """
    >>> import os
    >>> bd = bugdir.SimpleBugDir()
    >>> os.chdir(bd.root)
    >>> execute([], manipulate_encodings=False)
    >>> os.path.exists("./html_export")
    True
    >>> os.path.exists("./html_export/index.html")
    True
    >>> os.path.exists("./html_export/index_inactive.html")
    True
    >>> os.path.exists("./html_export/bugs")
    True
    >>> os.path.exists("./html_export/bugs/a.html")
    True
    >>> os.path.exists("./html_export/bugs/b.html")
    True
    >>> bd.cleanup()
    """
    parser = get_parser()
    options, args = parser.parse_args(args)
    complete(options, args, parser)
    cmdutil.default_complete(options, args, parser)

    if len(args) > 0:
        raise cmdutil.UsageError, 'Too many arguments.'

    template = options.template
    bd = bugdir.BugDir(from_disk=True,
                       manipulate_encodings=manipulate_encodings)
    bd.load_all_bugs()

    html_gen = HTMLGen(bd, template=options.template, verbose=options.verbose,
                       title=options.title, )
    html_gen.run(options.out_dir)

def get_parser():
    parser = cmdutil.CmdOptionParser('be html [options]')
    parser.add_option('-o', '--output', metavar='DIR', dest='out_dir',
        help='Set the output path (%default)', default='./html_export')
    parser.add_option('-t', '--template-dir', metavar='DIR', dest='template',
        help='Use a different template, defaults to internal templates', default=None)
    parser.add_option('--title', metavar='STRING', dest='title',
        help='Set the bug repository title (%default)',
        default='BugsEverywhere Issue Tracker')
    parser.add_option('--index-header', metavar='STRING', dest='index_header',
        help='Set the index page headers (%default)',
        default='BugsEverywhere Bug List')
    parser.add_option('-v', '--verbose',  action='store_true', metavar='verbose', dest='verbose',
        help='Verbose output, default is no', default=False)
    return parser

longhelp="""
Generate a set of html pages representing the current state of the bug
directory.
"""

def help():
    return get_parser().help_str() + longhelp

def complete(options, args, parser):
    for option, value in cmdutil.option_value_pairs(options, parser):
        if "--complete" in args:
            raise cmdutil.GetCompletions() # no positional arguments for list

class HTMLGen (object):
    def __init__(self, bd, template=None, verbose=False, encoding=None,
                 title="Site Title", index_header="Index Header",
                 ):
        self.generation_time = time.ctime()
        self.bd = bd
        self.verbose = verbose
        self.title = title
        self.index_header = index_header
        if encoding != None:
            self.encoding = encoding
        else:
            self.encoding = self.bd.encoding
        if template == None:
            self.template = "default"
        else:
            self.template = os.path.abspath(os.path.expanduser(template))
        self._load_default_templates()

        if template != None:
            self._load_user_templates()

    def run(self, out_dir):
        if self.verbose == True:
            print "Creating the html output in %s using templates in %s" \
                % (out_dir, self.template)

        bugs_active = []
        bugs_inactive = []
        bugs = [b for b in self.bd]
        bugs.sort()
        bugs_active = [b for b in bugs if b.active == True]
        bugs_inactive = [b for b in bugs if b.active != True]

        self._create_output_directories(out_dir)
        self._write_css_file()
        for b in bugs:
            if b.active:
                up_link = "../index.html"
            else:
                up_link = "../index_inactive.html"
            self._write_bug_file(b, up_link)
        self._write_index_file(bugs_active, title=self.title,
                               index_header=self.index_header, bug_type="active")
        self._write_index_file(bugs_inactive, title=self.title,
                               index_header=self.index_header, bug_type="inactive")

    def _create_output_directories(self, out_dir):
        if self.verbose:
            print "Creating output directories"
        self.out_dir = os.path.abspath(os.path.expanduser(out_dir))
        if not os.path.exists(self.out_dir):
            try:
                os.mkdir(self.out_dir)
            except:
                raise cmdutil.UsageError, "Cannot create output directory '%s'." % self.out_dir
        self.out_dir_bugs = os.path.join(self.out_dir, "bugs")
        if not os.path.exists(self.out_dir_bugs):
            os.mkdir(self.out_dir_bugs)

    def _write_css_file(self):
        if self.verbose:
            print "Writing css file"
        assert hasattr(self, "out_dir"), "Must run after ._create_output_directories()"
        f = codecs.open(os.path.join(self.out_dir,"style.css"), "w", self.encoding)
        f.write(self.css_file)
        f.close()

    def _write_bug_file(self, bug, up_link):
        if self.verbose:
            print "\tCreating bug file for %s" % self.bd.bug_shortname(bug)
        assert hasattr(self, "out_dir_bugs"), "Must run after ._create_output_directories()"
        esc = self._escape

        bug.load_comments(load_full=True)
        stack = []
        comment_entries = []
        for depth,comment in bug.comment_root.thread(flatten=False):
            while len(stack) > depth:
                stack.pop(-1)      # pop non-parents off the stack
                comment_entries.append("</div>\n") # close non-parent <div class="comment...
            assert len(stack) == depth
            stack.append(comment)
            if depth == 0:
                comment_entries.append('<div class="comment root">')
            else:
                comment_entries.append('<div class="comment">')
            template_info = {}
            for attr in ['uuid', 'author', 'date', 'body']:
                value = getattr(comment, attr)
                if attr == 'body':
                    if comment.content_type == 'text/html':
                        pass # no need to escape html...
                    elif comment.content_type.startswith('text/'):
                        value = '<pre>\n'+esc(value)+'\n</pre>'
                    else:
                        value = "TODO: linkout to %s" % comment.content_type
                else:
                    value = esc(value)
                template_info[attr] = value
            comment_entries.append(self.bug_comment_entry % template_info)
        while len(stack) > 0:
            stack.pop(-1)
            comment_entries.append("</div>\n") # close every remaining <div class="comment...

        filename = "%s.html" % bug.uuid
        fullpath = os.path.join(self.out_dir_bugs, filename)
        template_info = {'title':self.title,
                         'charset':self.encoding,
                         'up_link':up_link,
                         'shortname':self.bd.bug_shortname(bug),
                         'comment_entries':'\n'.join(comment_entries),
                         'generation_time':self.generation_time}
        for attr in ['uuid', 'severity', 'status', 'assigned', 'target',
                     'reporter', 'creator', 'time_string', 'summary']:
            template_info[attr] = esc(getattr(bug, attr))
        f = codecs.open(fullpath, "w", self.encoding)
        f.write(self.bug_file % template_info)
        f.close()

    def _write_index_file(self, bugs, title, index_header, bug_type="active"):
        if self.verbose:
            print "Writing %s index file for %d bugs" % (bug_type, len(bugs))
        assert hasattr(self, "out_dir"), "Must run after ._create_output_directories()"
        esc = self._escape

        bug_entries = []
        for b in bugs:
            if self.verbose:
                print "\tCreating bug entry for %s" % self.bd.bug_shortname(b)
            template_info = {'shortname':self.bd.bug_shortname(b)}
            for attr in ['uuid', 'severity', 'status', 'assigned', 'target',
                         'reporter', 'creator', 'time_string', 'summary']:
                template_info[attr] = esc(getattr(b, attr))
            bug_entries.append(self.index_bug_entry % template_info)

        if bug_type == "active":
            filename = "index.html"
        elif bug_type == "inactive":
            filename = "index_inactive.html"
        else:
            raise Exception, "Unrecognized bug_type: '%s'" % bug_type
        template_info = {'title':title,
                         'index_header':index_header,
                         'charset':self.encoding,
                         'active_class':'tab sel',
                         'inactive_class':'tab nsel',
                         'bug_entries':'\n'.join(bug_entries),
                         'generation_time':self.generation_time}
        if bug_type == "inactive":
            template_info['active_class'] = 'tab nsel'
            template_info['inactive_class'] = 'tab sel'

        f = codecs.open(os.path.join(self.out_dir, filename), "w", self.encoding)
        f.write(self.index_file % template_info)
        f.close()

    def _escape(self, string):
        if string == None:
            return ""
        chars = []
        for char in string:
            codepoint = ord(char)
            if codepoint in htmlentitydefs.codepoint2name:
                char = "&%s;" % htmlentitydefs.codepoint2name[codepoint]
            #else: xml.sax.saxutils.escape(char)
            chars.append(char)
        return "".join(chars)

    def _load_user_templates(self):
        for filename,attr in [('style.css','css_file'),
                              ('index_file.tpl','index_file'),
                              ('index_bug_entry.tpl','index_bug_entry'),
                              ('bug_file.tpl','bug_file'),
                              ('bug_comment_entry.tpl','bug_comment_entry')]:
            fullpath = os.path.join(self.template, filename)
            if os.path.exists(fullpath):
                f = codecs.open(fullpath, "r", self.encoding)
                setattr(self, attr, f.read())
                f.close()

    def _load_default_templates(self):
        self.css_file = """
            body {
              font-family: "lucida grande", "sans serif";
              color: #333;
              width: auto;
              margin: auto;
            }

            div.main {
              padding: 20px;
              margin: auto;
              padding-top: 0;
              margin-top: 1em;
              background-color: #fcfcfc;
            }

            div.footer {
              font-size: small;
              padding-left: 20px;
              padding-right: 20px;
              padding-top: 5px;
              padding-bottom: 5px;
              margin: auto;
              background: #305275;
              color: #fffee7;
            }

            table {
              border-style: solid;
              border: 10px #313131;
              border-spacing: 0;
              width: auto;
            }

            tb { border: 1px; }

            tr {
              vertical-align: top;
              width: auto;
            }

            td {
              border-width: 0;
              border-style: none;
              padding-right: 0.5em;
              padding-left: 0.5em;
              width: auto;
            }

            img { border-style: none; }

            h1 {
              padding: 0.5em;
              background-color: #305275;
              margin-top: 0;
              margin-bottom: 0;
              color: #fff;
              margin-left: -20px;
              margin-right: -20px;
            }

            ul {
              list-style-type: none;
              padding: 0;
            }

            p { width: auto; }

            a, a:visited {
              background: inherit;
              text-decoration: none;
            }

            a { color: #003d41; }
            a:visited { color: #553d41; }
            .footer a { color: #508d91; }

            /* bug index pages */

            td.tab {
              padding-right: 1em;
              padding-left: 1em;
            }

            td.sel.tab {
              background-color: #afafaf;
              border: 1px solid #afafaf;
              font-weight:bold;
            }

            td.nsel.tab { border: 0px; }

            table.bug_list {
              background-color: #afafaf;
              border: 2px solid #afafaf;
            }

            .bug_list tr { width: auto; }
            tr.wishlist { background-color: #B4FF9B; }
            tr.minor { background-color: #FCFF98; }
            tr.serious { background-color: #FFB648; }
            tr.critical { background-color: #FF752A; }
            tr.fatal { background-color: #FF3300; }

            /* bug detail pages */

            td.bug_detail_label { text-align: right; }
            td.bug_detail { } 
            td.bug_comment_label { text-align: right; vertical-align: top; }
            td.bug_comment { } 

            div.comment {
              padding: 20px;
              padding-top: 20px;
              margin: auto;
              margin-top: 0;
            }

            div.root.comment {
              padding: 0px;
              /* padding-top: 0px; */
              padding-bottom: 20px;
            }
       """

        self.index_file = """
            <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
              "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
            <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
            <head>
            <title>%(title)s</title>
            <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
            <link rel="stylesheet" href="style.css" type="text/css" />
            </head>
            <body>

            <div class="main">
            <h1>%(index_header)s</h1>
            <p></p>
            <table>

            <tr>
            <td class="%(active_class)s"><a href="index.html">Active Bugs</a></td>
            <td class="%(inactive_class)s"><a href="index_inactive.html">Inactive Bugs</a></td>
            </tr>

            </table>
            <table class="bug_list">
            <tbody>

            %(bug_entries)s

            </tbody>
            </table>
            </div>

            <div class="footer">
            <p>Generated by <a href="http://www.bugseverywhere.org/">
            BugsEverywhere</a> on %(generation_time)s</p>
            <p>
            <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a>&nbsp;|&nbsp;
            <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
            </p>
            </div>

            </body>
            </html>
        """

        self.index_bug_entry ="""
            <tr class="%(severity)s-row">
              <td><a href="bugs/%(uuid)s.html">%(shortname)s</a></td>
              <td><a href="bugs/%(uuid)s.html">%(status)s</a></td>
              <td><a href="bugs/%(uuid)s.html">%(severity)s</a></td>
              <td><a href="bugs/%(uuid)s.html">%(summary)s</a></td>
              <td><a href="bugs/%(uuid)s.html">%(time_string)s</a></td>
            </tr>
        """

        self.bug_file = """
            <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
              "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
            <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
            <head>
            <title>%(title)s</title>
            <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
            <link rel="stylesheet" href="../style.css" type="text/css" />
            </head>
            <body>
    
            <div class="main">
            <h1>BugsEverywhere Bug List</h1>
            <h5><a href="%(up_link)s">Back to Index</a></h5>
            <h2>Bug: %(shortname)s</h2>
            <table>
            <tbody>
    
            <tr><td class="bug_detail_label">ID :</td>
                <td class="bug_detail">%(uuid)s</td></tr>
            <tr><td class="bug_detail_label">Short name :</td>
                <td class="bug_detail">%(shortname)s</td></tr>
            <tr><td class="bug_detail_label">Status :</td>
                <td class="bug_detail">%(status)s</td></tr>
            <tr><td class="bug_detail_label">Severity :</td>
                <td class="bug_detail">%(severity)s</td></tr>
            <tr><td class="bug_detail_label">Assigned :</td>
                <td class="bug_detail">%(assigned)s</td></tr>
            <tr><td class="bug_detail_label">Reporter :</td>
                <td class="bug_detail">%(reporter)s</td></tr>
            <tr><td class="bug_detail_label">Creator :</td>
                <td class="bug_detail">%(creator)s</td></tr>
            <tr><td class="bug_detail_label">Created :</td>
                <td class="bug_detail">%(time_string)s</td></tr>
            <tr><td class="bug_detail_label">Summary :</td>
                <td class="bug_detail">%(summary)s</td></tr>
            </tbody>
            </table>

            <hr/>

            %(comment_entries)s

            </div>
            <h5><a href="%(up_link)s">Back to Index</a></h5>

            <div class="footer">
            <p>Generated by <a href="http://www.bugseverywhere.org/">
            BugsEverywhere</a> on %(generation_time)s</p>
            <p>
            <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a>&nbsp;|&nbsp;
            <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
            </p>
            </div>

            </body>
            </html>
        """

        self.bug_comment_entry ="""
            <table>
            <tr>
              <td class="bug_comment_label">Comment:</td>
              <td class="bug_comment">
            --------- Comment ---------<br/>
            Name: %(uuid)s<br/>
            From: %(author)s<br/>
            Date: %(date)s<br/>
            <br/>
            %(body)s
              </td>
            </tr>
            </table>
        """

        # strip leading whitespace
        for attr in ['css_file', 'index_file', 'index_bug_entry', 'bug_file',
                     'bug_comment_entry']:
            value = getattr(self, attr)
            value = '\n'.join(value.split('\n'+' '*12))
            setattr(self, attr, value.strip()+'\n')