aboutsummaryrefslogblamecommitdiffstats
path: root/libbe/command/html.py
blob: 0c5cad178b47fffeb008622e3c3f6df064a5bb21 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                                                           
                                                                     
                                                           
 
                                       
 



                                                                            
 






                                                                          












                         
                    
                          
                    
 
 





                                                                   




                                                    
 
                                                                                  
                                                                    
        
                                                                                  
        
                                                                                           
        
                                                                            
        
                                                                                               
        
                                                                                               
        
                    
                    
       
                 
 

                                                             




























                                                                                     




                                                                                                                               



                                                                    
                             




                                             



                                                               
                                                                 

                                                     



                                                                          


                         

                                                                      

   

                                                              
                       
                                         
                                                                 
                                  
                                                           

                                           
                    



                                                                         

                                        

                              


                                    
                                                                         
                                      

                                       
                                          


                                

                                                                        
                                          
 










                                                             
                                            
                 
                                                     
                                            

                                          
                                                              

                                            
                                                                
 








                                                  
 

                                                  
                                                               

                                              
                                               


                              


                                                          
                                       
                                                    


                                            


                                                                              


                                                                 

                                                                         


                                                 
                                                   

                                                                
                                                              

                                                                      


                                                          
                                                                   

                                                 

                                                          


                            
                                                                       

                                                                    


                                                         
                                                  

                                      


                                                                    


                                                                    
                                       

                                                    


                                                           
                                         

                                                           
                                            

                                                                        
                                            


                                                                   

                                                                   


                                                                          


                                                                   

                                                                




                                                                               



                                                                       
                                                      
                                                                   





                                                                          
                                                                                             

                                         
                                         




                                                                              
                                                                

                        





                                                                      














                                                             
                                  
                                                                  
                                           





                                                      

                                                             


                                                     
                                                      
                                 
                                   
                                



                                                                      
                                    





                                                                      


                                                     
                                                                              
                        

                                                                                             



                                                            



                                            
             
                                                                     






                                                                
                                  


                                                       

                                                         




                                                

                                                                                   
                                                                  

                                                                          
                                                              

                                                                    
 

                              
                     
                                              








                                                                             





                                                                
                                     
                   

                                                                      


                                                         

                                                                    

                                               

                                                                        


                                              
                                                               

                                              
                                                     

                              
                                                                
                                         
                                                          
                        
                                                                     
                                              
                                                               
                        
                                                              
                                       
                                                        
                        
                                                                       
                                                
                                                                 
 
                                      

                           



                                                         
             
 
                      




                                        
             









                                  
             
 
                   



                                   
             





                                  
             
 
                




                                   
             


                                       
                






                                        
             



                                    
             





                                    
             









                                         
             




                                        
             





                                        
             










                                                      
                             
                                                                            
                              





                                
             




                                     
             

          
                             
                                                                    
                                                                  

                                                                               

                                                                                       


                                                                      
 
                              
                                     

                   
 
                

                                                                                               
                 
 
                    
                                    
                   


                           

                    
                  









                                                                                               


                   

                                 
                                     




                                                                  
                 
           
 






                                                                                       
                                                                            

                   
 





                                                            
 






































                                                                                               
           
 





                                                         

                                          







                                 
 



                                                                             
                                                    
                                                   
# Copyright (C) 2009-2010 Gianluca Montecchi <gian@grys.it>
#                         Mathieu Clabaut <mathieu.clabaut@gmail.com>
#                         W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
# Bugs Everywhere 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.
#
# Bugs Everywhere 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 Bugs Everywhere.  If not, see <http://www.gnu.org/licenses/>.

import codecs
import htmlentitydefs
import os
import os.path
import re
import string
import time
import xml.sax.saxutils

import libbe
import libbe.command
import libbe.command.util
import libbe.comment
import libbe.util.encoding
import libbe.util.id


class HTML (libbe.command.Command):
    """Generate a static HTML dump of the current repository status

    >>> import sys
    >>> import libbe.bugdir
    >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
    >>> io = libbe.command.StringInputOutput()
    >>> io.stdout = sys.stdout
    >>> ui = libbe.command.UserInterface(io=io)
    >>> ui.storage_callbacks.set_storage(bd.storage)
    >>> cmd = HTML(ui=ui)

    >>> ret = ui.run(cmd, {'output':os.path.join(bd.storage.repo, 'html_export')})
    >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export'))
    True
    >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'index.html'))
    True
    >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'index_inactive.html'))
    True
    >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs'))
    True
    >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs', 'a', 'index.html'))
    True
    >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs', 'b', 'index.html'))
    True
    >>> ui.cleanup()
    >>> bd.cleanup()
    """
    name = 'html'

    def __init__(self, *args, **kwargs):
        libbe.command.Command.__init__(self, *args, **kwargs)
        self.options.extend([
                libbe.command.Option(name='output', short_name='o',
                    help='Set the output path (%default)',
                    arg=libbe.command.Argument(
                        name='output', metavar='DIR', default='./html_export',
                        completion_callback=libbe.command.util.complete_path)),
                libbe.command.Option(name='template-dir', short_name='t',
                    help='Use a different template.  Defaults to internal templates',
                    arg=libbe.command.Argument(
                        name='template-dir', metavar='DIR',
                        completion_callback=libbe.command.util.complete_path)),
                libbe.command.Option(name='title',
                    help='Set the bug repository title (%default)',
                    arg=libbe.command.Argument(
                        name='title', metavar='STRING',
                        default='BugsEverywhere Issue Tracker')),
                libbe.command.Option(name='index-header',
                    help='Set the index page headers (%default)',
                    arg=libbe.command.Argument(
                        name='index-header', metavar='STRING',
                        default='BugsEverywhere Bug List')),
                libbe.command.Option(name='export-template', short_name='e',
                    help='Export the default template and exit.'),
                libbe.command.Option(name='export-template-dir', short_name='d',
                    help='Set the directory for the template export (%default)',
                    arg=libbe.command.Argument(
                        name='export-template-dir', metavar='DIR',
                        default='./default-templates/',
                        completion_callback=libbe.command.util.complete_path)),
                libbe.command.Option(name='min-id-length', short_name='l',
                    help='Attempt to truncate bug and comment IDs to this length.  Set to -1 for non-truncated IDs (%default)',
                    arg=libbe.command.Argument(
                        name='min-id-length', metavar='INT',
                        default=-1, type='int')),
                libbe.command.Option(name='verbose', short_name='v',
                    help='Verbose output, default is %default'),
                ])

    def _run(self, **params):
        if params['export-template'] == True:
            bugdir = None
        else:
            bugdir = self._get_bugdir()
            bugdir.load_all_bugs()
        html_gen = HTMLGen(bugdir,
                           template=params['template-dir'],
                           title=params['title'],
                           index_header=params['index-header'],
                           min_id_length=params['min-id-length'],
                           verbose=params['verbose'],
                           stdout=self.stdout)
        if params['export-template'] == True:
            html_gen.write_default_template(params['export-template-dir'])
        else:
            html_gen.run(params['output'])

    def _long_help(self):
        return """
Generate a set of html pages representing the current state of the bug
directory.
"""

Html = HTML # alias for libbe.command.base.get_command_class()

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

    def run(self, out_dir):
        if self.verbose == True:
            print >> self.stdout, \
                '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 _truncated_bug_id(self, bug):
        return libbe.util.id._truncate(
            bug.uuid, bug.sibling_uuids(),
            min_length=self.min_id_length)

    def _truncated_comment_id(self, comment):
        return libbe.util.id._truncate(
            comment.uuid, comment.sibling_uuids(),
            min_length=self.min_id_length)

    def _create_output_directories(self, out_dir):
        if self.verbose:
            print >> self.stdout, 'Creating output directories'
        self.out_dir = self._make_dir(out_dir)
        self.out_dir_bugs = self._make_dir(
            os.path.join(self.out_dir, 'bugs'))

    def _write_css_file(self):
        if self.verbose:
            print >> self.stdout, 'Writing css file'
        assert hasattr(self, 'out_dir'), \
            'Must run after ._create_output_directories()'
        self._write_file(self.css_file,
                         [self.out_dir,'style.css'])

    def _write_bug_file(self, bug, up_link):
        if self.verbose:
            print >> self.stdout, '\tCreating bug file for %s' % bug.id.user()
        assert hasattr(self, 'out_dir_bugs'), \
            'Must run after ._create_output_directories()'

        bug.load_comments(load_full=True)
        comment_entries = self._generate_bug_comment_entries(bug)
        dirname = self._truncated_bug_id(bug)
        fullpath = os.path.join(self.out_dir_bugs, dirname, 'index.html')
        template_info = {'title':self.title,
                         'charset':self.encoding,
                         'up_link':up_link,
                         'shortname':bug.id.user(),
                         'comment_entries':comment_entries,
                         'generation_time':self.generation_time}
        for attr in ['uuid', 'severity', 'status', 'assigned',
                     'reporter', 'creator', 'time_string', 'summary']:
            template_info[attr] = self._escape(getattr(bug, attr))
        fulldir = os.path.join(self.out_dir_bugs, dirname)
        if not os.path.exists(fulldir):
            os.mkdir(fulldir)
        self._write_file(self.bug_file % template_info, [fullpath])

    def _generate_bug_comment_entries(self, bug):
        assert hasattr(self, 'out_dir_bugs'), \
            'Must run after ._create_output_directories()'

        stack = []
        comment_entries = []
        bug.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
        for depth,comment in bug.comment_root.thread(flatten=False):
            while len(stack) > depth:
                # pop non-parents off the stack
                stack.pop(-1)
                # close non-parent <div class="comment...
                comment_entries.append('</div>\n')
            assert len(stack) == depth
            stack.append(comment)
            template_info = {
                'shortname': comment.id.user(),
                'truncated_id': self._truncated_comment_id(comment)}
            if depth == 0:
                comment_entries.append('<div class="comment root">')
            else:
                comment_entries.append(
                    '<div class="comment" id="%s">'
                    % template_info['truncated_id'])
            for attr in ['uuid', 'author', 'date', 'body']:
                value = getattr(comment, attr)
                if attr == 'body':
                    link_long_ids = False
                    save_body = False
                    if comment.content_type == 'text/html':
                        link_long_ids = True
                    elif comment.content_type.startswith('text/'):
                        value = '<pre>\n'+self._escape(value)+'\n</pre>'
                        link_long_ids = True
                    elif comment.content_type.startswith('image/'):
                        save_body = True
                        value = '<img src="./%s/%s" />' \
                            % (self._truncated_bug_id(bug),
                               self._truncated_comment_id(comment))
                    else:
                        save_body = True
                        value = '<a href="./%s/%s">Link to %s file</a>.' \
                            % (self._truncated_bug_id(bug),
                               self._truncated_comment_id(comment),
                               comment.content_type)
                    if link_long_ids == True:
                        value = self._long_to_linked_user(value)
                    if save_body == True:
                        per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid)
                        if not os.path.exists(per_bug_dir):
                            os.mkdir(per_bug_dir)
                        comment_path = os.path.join(per_bug_dir, comment.uuid)
                        self._write_file(
                            '<Files %s>\n  ForceType %s\n</Files>' \
                                % (comment.uuid, comment.content_type),
                            [per_bug_dir, '.htaccess'], mode='a')
                        self._write_file(comment.body,
                            [per_bug_dir, comment.uuid], mode='wb')
                else:
                    value = self._escape(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...
        return '\n'.join(comment_entries)

    def _long_to_linked_user(self, text):
        """
        >>> import libbe.bugdir
        >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
        >>> h = HTMLGen(bd)
        >>> h._long_to_linked_user('A link #abc123/a#, and a non-link #x#y#.')
        'A link <a href="./a/">abc/a</a>, and a non-link #x#y#.'
        >>> bd.cleanup()
        """
        replacer = libbe.util.id.IDreplacer(
            [self.bd], self._long_to_linked_user_replacer, wrap=False)
        return re.sub(
            libbe.util.id.REGEXP, replacer, text)

    def _long_to_linked_user_replacer(self, bugdirs, long_id):
        """
        >>> import libbe.bugdir
        >>> import libbe.util.id
        >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
        >>> a = bd.bug_from_uuid('a')
        >>> uuid_gen = libbe.util.id.uuid_gen
        >>> libbe.util.id.uuid_gen = lambda : '0123'
        >>> c = a.new_comment('comment for link testing')
        >>> libbe.util.id.uuid_gen = uuid_gen
        >>> c.uuid
        '0123'
        >>> h = HTMLGen(bd)
        >>> h._long_to_linked_user_replacer([bd], 'abc123')
        '#abc123#'
        >>> h._long_to_linked_user_replacer([bd], 'abc123/a')
        '<a href="./a/">abc/a</a>'
        >>> h._long_to_linked_user_replacer([bd], 'abc123/a/0123')
        '<a href="./a/#0123">abc/a/012</a>'
        >>> h._long_to_linked_user_replacer([bd], 'x')
        '#x#'
        >>> h._long_to_linked_user_replacer([bd], '')
        '##'
        >>> bd.cleanup()
        """
        try:
            p = libbe.util.id.parse_user(bugdirs[0], long_id)
        except (libbe.util.id.MultipleIDMatches,
                libbe.util.id.NoIDMatches,
                libbe.util.id.InvalidIDStructure), e:
            return '#%s#' % long_id # re-wrap failures
        if p['type'] == 'bugdir':
            return '#%s#' % long_id
        elif p['type'] == 'bug':
            bug,comment = libbe.command.util.bug_comment_from_user_id(
                bugdirs[0], long_id)
            return '<a href="./%s/">%s</a>' \
                % (self._truncated_bug_id(bug), bug.id.user())
        elif p['type'] == 'comment':
            bug,comment = libbe.command.util.bug_comment_from_user_id(
                bugdirs[0], long_id)
            return '<a href="./%s/#%s">%s</a>' \
                % (self._truncated_bug_id(bug),
                   self._truncated_comment_id(comment),
                   comment.id.user())
        raise Exception('Invalid id type %s for "%s"'
                        % (p['type'], long_id))

    def _write_index_file(self, bugs, title, index_header, bug_type='active'):
        if self.verbose:
            print >> self.stdout, '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 = self._generate_index_bug_entries(bugs)

        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':bug_entries,
                         'generation_time':self.generation_time}
        if bug_type == 'inactive':
            template_info['active_class'] = 'tab nsel'
            template_info['inactive_class'] = 'tab sel'

        self._write_file(self.index_file % template_info,
                         [self.out_dir, filename])

    def _generate_index_bug_entries(self, bugs):
        bug_entries = []
        for bug in bugs:
            if self.verbose:
                print >> self.stdout, '\tCreating bug entry for %s' % bug.id.user()
            template_info = {'shortname':bug.id.user()}
            for attr in ['uuid', 'severity', 'status', 'assigned',
                         'reporter', 'creator', 'time_string', 'summary']:
                template_info[attr] = self._escape(getattr(bug, attr))
            template_info['dir'] = self._truncated_bug_id(bug)
            bug_entries.append(self.index_bug_entry % template_info)
        return '\n'.join(bug_entries)

    def _escape(self, string):
        if string == None:
            return ''
        return xml.sax.saxutils.escape(string)

    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):
                setattr(self, attr, self._read_file([fullpath]))

    def _make_dir(self, dir_path):
        dir_path = os.path.abspath(os.path.expanduser(dir_path))
        if not os.path.exists(dir_path):
            try:
                os.makedirs(dir_path)
            except:
                raise libbe.command.UserError(
                    'Cannot create output directory "%s".' % dir_path)
        return dir_path

    def _write_file(self, content, path_array, mode='w'):
        return libbe.util.encoding.set_file_contents(
            os.path.join(*path_array), content, mode, self.encoding)

    def _read_file(self, path_array, mode='r'):
        return libbe.util.encoding.get_file_contents(
            os.path.join(*path_array), mode, self.encoding, decode=True)

    def write_default_template(self, out_dir):
        if self.verbose:
            print >> self.stdout, 'Creating output directories'
        self.out_dir = self._make_dir(out_dir)
        if self.verbose:
            print >> self.stdout, 'Creating css file'
        self._write_css_file()
        if self.verbose:
            print >> self.stdout, 'Creating index_file.tpl file'
        self._write_file(self.index_file,
                         [self.out_dir, 'index_file.tpl'])
        if self.verbose:
            print >> self.stdout, 'Creating index_bug_entry.tpl file'
        self._write_file(self.index_bug_entry,
                         [self.out_dir, 'index_bug_entry.tpl'])
        if self.verbose:
            print >> self.stdout, 'Creating bug_file.tpl file'
        self._write_file(self.bug_file,
                         [self.out_dir, 'bug_file.tpl'])
        if self.verbose:
            print >> self.stdout, 'Creating bug_comment_entry.tpl file'
        self._write_file(self.bug_comment_entry,
                         [self.out_dir, 'bug_comment_entry.tpl'])

    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">
              <td><a href="bugs/%(dir)s/">%(shortname)s</a></td>
              <td><a href="bugs/%(dir)s/">%(status)s</a></td>
              <td><a href="bugs/%(dir)s/">%(severity)s</a></td>
              <td><a href="bugs/%(dir)s/">%(summary)s</a></td>
              <td><a href="bugs/%(dir)s/">%(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/>
            ID: %(uuid)s<br/>
            Short name: %(shortname)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 = value.replace('\n'+' '*12, '\n')
            setattr(self, attr, value.strip()+'\n')