-# Copyright (C) 2009-2010 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2009-2012 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
+# Mathieu Clabaut <mathieu.clabaut@gmail.com>
# 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 file is part of Bugs Everywhere.
#
-# 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.
+# 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.
#
-# 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.
+# 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 itertools
import os
import os.path
import re
import time
import xml.sax.saxutils
+from jinja2 import Environment, FileSystemLoader, DictLoader, ChoiceLoader
+
import libbe
import libbe.command
+import libbe.command.depend
import libbe.command.util
import libbe.comment
import libbe.util.encoding
import libbe.util.id
+import libbe.util.wsgi
+import libbe.version
-class HTML (libbe.command.Command):
- """Generate a static HTML dump of the current repository status
+class ServerApp (libbe.util.wsgi.WSGI_AppObject,
+ libbe.util.wsgi.WSGI_DataObject):
+ """WSGI server for a BE Storage instance over HTML.
- >>> 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()
+ Serve browsable HTML for public consumption. Currently everything
+ is read-only.
"""
- 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:
- html_gen.write_default_template(params['export-template-dir'])
- return 0
- 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)
- html_gen.run(params['output'])
- return 0
-
- 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))
+ server_version = 'BE-html-server/' + libbe.version.version()
+
+ def __init__(self, bugdirs={}, template_dir=None, title='Site Title',
+ header='Header', index_file='', min_id_length=-1,
+ generation_time=None, **kwargs):
+ super(ServerApp, self).__init__(
+ urls=[
+ (r'^{}$'.format(index_file), self.index),
+ (r'^style.css$', self.style),
+ (r'^([^/]+)/([^/]+)/{}'.format(index_file), self.bug),
+ ],
+ **kwargs)
+ self.bugdirs = bugdirs
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.header = header
+ self._index_file = index_file
self.min_id_length = min_id_length
+ self.generation_time = generation_time
+ self._refresh = 0
+ self.http_user_error = 418
+ self._load_templates(template_dir=template_dir)
+ self._filters = {
+ 'active': lambda bug: bug.active and bug.severity != 'target',
+ 'inactive': lambda bug: not bug.active and bug.severity !='target',
+ 'target': lambda bug: bug.severity == 'target'
+ }
- 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]
+ # handlers
+ def style(self, environ, start_response):
+ template = self.template.get_template('style.css')
+ content = template.render()
+ return self.ok_response(
+ environ, start_response, content, content_type='text/css')
+
+ def index(self, environ, start_response):
+ data = self.query_data(environ)
+ source = 'query'
+ bug_type = self.data_get_string(
+ data, 'type', default='active', source=source)
+ assert bug_type in ['active', 'inactive', 'target'], bug_type
+ self.refresh()
+ filter_ = self._filters.get(bug_type, self._filters['active'])
+ bugs = list(itertools.chain(*list(
+ [bug for bug in bugdir if filter_(bug)]
+ for bugdir in self.bugdirs.values())))
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')
+ if self.logger:
+ self.logger.log(
+ self.log_level, 'generate {} index file for {} bugs'.format(
+ bug_type, len(bugs)))
+ template_info = {
+ 'title': self.title,
+ 'charset': 'UTF-8',
+ 'stylesheet': 'style.css',
+ 'header': self.header,
+ 'active_class': 'tab nsel',
+ 'inactive_class': 'tab nsel',
+ 'target_class': 'tab nsel',
+ 'bugs': bugs,
+ 'bug_entry': self.template.get_template('index_bug_entry.html'),
+ 'bug_dir': self.bug_dir,
+ 'index_file': self._index_file,
+ 'generation_time': self._generation_time(),
+ }
+ template_info['{}_class'.format(bug_type)] = 'tab sel'
+ if bug_type == 'target':
+ template = self.template.get_template('target_index.html')
+ template_info['targets'] = [
+ (target, sorted(libbe.command.depend.get_blocked_by(
+ self.bugdirs, target)))
+ for target in bugs]
+ else:
+ template = self.template.get_template('standard_index.html')
+ content = template.render(template_info)+'\n'
+ return self.ok_response(
+ environ, start_response, content, content_type='text/html')
+
+ def bug(self, environ, start_response):
+ try:
+ bugdir_id,bug_id = environ['be-server.url_args']
+ except:
+ raise libbe.util.wsgi.HandlerError(404, 'Not Found')
+ user_id = '{}/{}'.format(bugdir_id, bug_id)
+ bugdir,bug,comment = (
+ libbe.command.util.bugdir_bug_comment_from_user_id(
+ self.bugdirs, user_id))
+ if self.logger:
+ self.logger.log(
+ self.log_level, 'generate bug file for {}/{}'.format(
+ bugdir.uuid, bug.uuid))
+ if bug.severity == 'target':
+ index_type = 'target'
+ elif bug.active:
+ index_type = 'active'
+ else:
+ index_type = 'inactive'
+ up_link = '../../{}?type={}'.format(self._index_file, index_type)
+ bug.load_comments(load_full=True)
+ bug.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
+ template_info = {
+ 'title': self.title,
+ 'charset': 'UTF-8',
+ 'stylesheet': '../../style.css',
+ 'header': self.header,
+ 'backlinks': self.template.get_template('bug_backlinks.html'),
+ 'up_link': up_link,
+ 'index_type': index_type.capitalize(),
+ 'index_file': self._index_file,
+ 'bug': bug,
+ 'comment_entry': self.template.get_template(
+ 'bug_comment_entry.html'),
+ 'comments': [(depth,comment) for depth,comment
+ in bug.comment_root.thread(flatten=False)],
+ 'comment_dir': self._truncated_comment_id,
+ 'format_body': self._format_comment_body,
+ 'div_close': _DivCloser(),
+ 'generation_time': self._generation_time(),
+ }
+ template = self.template.get_template('bug.html')
+ content = template.render(template_info)
+ return self.ok_response(
+ environ, start_response, content, content_type='text/html')
+
+ # helper functions
+ def refresh(self):
+ if time.time() > self._refresh:
+ if self.logger:
+ self.logger.log(self.log_level, 'refresh bugdirs')
+ for bugdir in self.bugdirs.values():
+ bugdir.load_all_bugs()
+ self._refresh = time.time() + 60
+
+ def _truncated_bugdir_id(self, bugdir):
+ return libbe.util.id._truncate(
+ bugdir.uuid, self.bugdirs.keys(),
+ min_length=self.min_id_length)
def _truncated_bug_id(self, bug):
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 bug_dir(self, bug):
+ return '{}/{}'.format(
+ self._truncated_bugdir_id(bug.bugdir),
+ self._truncated_bug_id(bug))
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#.')
+ >>> bugdir = libbe.bugdir.SimpleBugDir(memory=False)
+ >>> a = ServerApp(bugdirs={bugdir.uuid: bugdir})
+ >>> a._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()
+ >>> bugdir.cleanup()
"""
replacer = libbe.util.id.IDreplacer(
- [self.bd], self._long_to_linked_user_replacer, wrap=False)
+ self.bugdirs, self._long_to_linked_user_replacer, wrap=False)
return re.sub(
libbe.util.id.REGEXP, replacer, text)
"""
>>> import libbe.bugdir
>>> import libbe.util.id
- >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
- >>> a = bd.bug_from_uuid('a')
+ >>> bugdir = libbe.bugdir.SimpleBugDir(memory=False)
+ >>> bugdirs = {bugdir.uuid: bugdir}
+ >>> a = bugdir.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')
+ >>> a = ServerApp(bugdirs=bugdirs)
+ >>> a._long_to_linked_user_replacer(bugdirs, 'abc123')
'#abc123#'
- >>> h._long_to_linked_user_replacer([bd], 'abc123/a')
+ >>> a._long_to_linked_user_replacer(bugdirs, 'abc123/a')
'<a href="./a/">abc/a</a>'
- >>> h._long_to_linked_user_replacer([bd], 'abc123/a/0123')
+ >>> a._long_to_linked_user_replacer(bugdirs, 'abc123/a/0123')
'<a href="./a/#0123">abc/a/012</a>'
- >>> h._long_to_linked_user_replacer([bd], 'x')
+ >>> a._long_to_linked_user_replacer(bugdirs, 'x')
'#x#'
- >>> h._long_to_linked_user_replacer([bd], '')
+ >>> a._long_to_linked_user_replacer(bugdirs, '')
'##'
- >>> bd.cleanup()
+ >>> bugdir.cleanup()
"""
try:
- p = libbe.util.id.parse_user(bugdirs[0], long_id)
+ p = libbe.util.id.parse_user(bugdirs, long_id)
except (libbe.util.id.MultipleIDMatches,
libbe.util.id.NoIDMatches,
libbe.util.id.InvalidIDStructure), e:
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)
+ bugdir,bug,comment = (
+ libbe.command.util.bugdir_bug_comment_from_user_id(
+ bugdirs, 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)
+ bugdir,bug,comment = (
+ libbe.command.util.bugdir_bug_comment_from_user_id(
+ bugdirs, long_id))
return '<a href="./%s/#%s">%s</a>' \
% (self._truncated_bug_id(bug),
self._truncated_comment_id(comment),
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'
+ def _format_comment_body(self, bug, comment):
+ link_long_ids = False
+ save_body = False
+ value = comment.body
+ 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:
- 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)
+ 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')
+ return value
+
+ def _generation_time(self):
+ if self.generation_time:
+ return self.generation_time
+ return time.ctime()
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;
+ def _load_templates(self, template_dir=None):
+ if template_dir is not None:
+ template_dir = os.path.abspath(os.path.expanduser(template_dir))
+
+ self.template_dict = {
+##
+ 'style.css':
+"""body {
+ font-family: "lucida grande", "sans serif";
+ font-size: 14px;
+ color: #333;
+ width: auto;
+ margin: auto;
+}
+
+div.main {
+ padding: 20px;
+ margin: auto;
+ padding-top: 0;
+ margin-top: 1em;
+ background-color: #fcfcfc;
+ -moz-border-radius: 10px;
+
+}
+
+div.footer {
+ font-size: small;
+ padding-left: 20px;
+ padding-right: 20px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ margin: auto;
+ background: #305275;
+ color: #fffee7;
+ -moz-border-radius: 10px;
+}
+
+div.header {
+ font-size: xx-large;
+ padding-left: 20px;
+ padding-right: 20px;
+ padding-top: 10px;
+ font-weight:bold;
+ padding-bottom: 10px;
+ background: #305275;
+ color: #fffee7;
+ -moz-border-radius: 10px;
+}
+
+th.target_name {
+ text-align:left;
+ border: 1px solid;
+ border-color: #305275;
+ background-color: #305275;
+ color: #fff;
+ width: auto%;
+ -moz-border-radius-topleft: 8px;
+ -moz-border-radius-topright: 8px;
+ padding-left: 5px;
+ padding-right: 5px;
+}
+
+table {
+ border-style: solid;
+ border: 1px #c3d9ff;
+ border-spacing: 0px 0px;
+ width: auto;
+ padding: 0px;
+
+ }
+
+tb { border: 1px; }
+
+tr {
+ vertical-align: top;
+ border: 1px #c3d9ff;
+ border-style: dotted;
+ width: auto;
+ padding: 0px;
+}
+
+th {
+ border-width: 1px;
+ border-style: solid;
+ border-color: #c3d9ff;
+ border-collapse: collapse;
+ padding-left: 5px;
+ padding-right: 5px;
+}
+
+
+td {
+ border-width: 1px;
+ border-color: #c3d9ff;
+ border-collapse: collapse;
+ padding-left: 5px;
+ padding-right: 5px;
+ width: auto%;
+}
+
+img { border-style: none; }
+
+ul {
+ list-style-type: none;
+ padding: 0;
+}
+
+p { width: auto; }
+
+p.backlink {
+ width: auto;
+ font-weight: bold;
+}
+
+a {
+ background: inherit;
+ text-decoration: none;
+}
+
+a { color: #553d41; }
+a:hover { color: #003d41; }
+a:visited { color: #305275; }
+.footer a { color: #508d91; }
+
+/* bug index pages */
+
+td.tab {
+ padding-right: 1em;
+ padding-left: 1em;
+}
+
+td.sel.tab {
+ background-color: #c3d9ff ;
+ border: 1px solid #c3d9ff;
+ font-weight:bold;
+ -moz-border-radius-topleft: 15px;
+ -moz-border-radius-topright: 15px;
+}
+
+td.nsel.tab {
+ border: 1px solid #c3d9ff;
+ font-weight:bold;
+ -moz-border-radius-topleft: 5px;
+ -moz-border-radius-topright: 5px;
+}
+
+table.bug_list {
+ border-width: 1px;
+ border-style: solid;
+ border-color: #c3d9ff;
+ padding: 0px;
+ width: 100%;
+ border: 1px solid #c3d9ff;
+}
+
+table.target_list {
+ padding: 0px;
+ width: 100%;
+ margin-bottom: 10px;
+}
+
+table.target_list.td {
+ border-width: 1px;
+}
+
+tr.wishlist { background-color: #DCFAFF;}
+tr.wishlist:hover { background-color: #C2DCE1; }
+
+tr.minor { background-color: #FFFFA6; }
+tr.minor:hover { background-color: #E6E696; }
+
+tr.serious { background-color: #FF9077;}
+tr.serious:hover { background-color: #E6826B; }
+
+tr.critical { background-color: #FF752A; }
+tr.critical:hover { background-color: #D63905;}
+
+tr.fatal { background-color: #FF3300;}
+tr.fatal:hover { background-color: #D60000;}
+
+td.uuid { width: 5%; border-style: dotted;}
+td.status { width: 5%; border-style: dotted;}
+td.severity { width: 5%; border-style: dotted;}
+td.summary { border-style: dotted;}
+td.date { width: 25%; border-style: dotted;}
+
+/* bug detail pages */
+
+td.bug_detail_label { text-align: right; border: none;}
+td.bug_detail { border: none;}
+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;
+}
+""",
+##
+ 'base.html':
+"""<!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 }}</title>
+ <meta http-equiv="Content-Type" content="text/html; charset={{ charset }}" />
+ <link rel="stylesheet" href="{{ stylesheet }}" type="text/css" />
+ </head>
+ <body>
+ <div class="header">{{ header }}</div>
+ <div class="main">
+ {% block content %}{% endblock %}
+ </div>
+ <div class="footer">
+ <p>Generated by <a href="http://www.bugseverywhere.org/">
+ Bugs Everywhere</a> on {{ generation_time }}</p>
+ <p>
+ <a href="http://validator.w3.org/check?uri=referer">
+ Validate XHTML</a> |
+ <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">
+ Validate CSS</a>
+ </p>
+ </div>
+ </body>
+</html>
+""",
+##
+ 'bugdirs.html':
+"""{% extends "base.html" %}
+
+{% block content %}
+{% if bugdirss %}
+{% block bugdir_table %}{% endblock %}
+{% else %}
+<p>No bugdirs.</p>
+{% endif %}
+{% endblock %}
+""",
+##
+ 'index.html':
+"""{% extends "base.html" %}
+
+{% block content %}
+<table>
+ <tbody>
+ <tr>
+ <td class="{{ active_class }}"><a href="{% if index_file %}{{ index_file }}{% else %}.{% endif %}">Active Bugs</a></td>
+ <td class="{{ inactive_class }}"><a href="{{ index_file }}?type=inactive">Inactive Bugs</a></td>
+ <td class="{{ target_class }}"><a href="{{ index_file }}?type=target">Divided by target</a></td>
+ </tr>
+ </tbody>
+</table>
+{% if bugs %}
+{% block bug_table %}{% endblock %}
+{% else %}
+<p>No bugs.</p>
+{% endif %}
+{% endblock %}
+""",
+##
+ 'standard_index.html':
+"""{% extends "index.html" %}
+
+{% block bug_table %}
+<table class="bug_list">
+ <thead>
+ <tr>
+ <th>UUID</th>
+ <th>Status</th>
+ <th>Severity</th>
+ <th>Summary</th>
+ <th>Date</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for bug in bugs %}
+ {{ bug_entry.render({'bug':bug, 'dir':bug_dir(bug), 'index_file':index_file}) }}
+ {% endfor %}
+ </tbody>
+</table>
+{% endblock %}
+""",
+##
+ 'target_index.html':
+"""{% extends "index.html" %}
+
+{% block bug_table %}
+{% for target,bugs in targets %}
+<table class="target_list">
+ <thead>
+ <tr>
+ <th class="target_name" colspan="5">
+ Target: {{ target.summary|e }} ({{ target.status|e }})
+ </th>
+ </tr>
+ <tr>
+ <th>UUID</th>
+ <th>Status</th>
+ <th>Severity</th>
+ <th>Summary</th>
+ <th>Date</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for bug in bugs %}
+ {{ bug_entry.render({'bug':bug, 'dir':bug_dir(bug), 'index_file':index_file}) }}
+ {% endfor %}
+ </tbody>
+</table>
+{% endfor %}
+{% endblock %}
+""",
+##
+ 'index_bug_entry.html':
+"""<tr class="{{ bug.severity }}">
+ <td class="uuid"><a href="{{ dir }}/{{ index_file }}">{{ bug.id.user()|e }}</a></td>
+ <td class="status"><a href="{{ dir }}/{{ index_file }}">{{ bug.status|e }}</a></td>
+ <td class="severity"><a href="{{ dir }}/{{ index_file }}">{{ bug.severity|e }}</a></td>
+ <td class="summary"><a href="{{ dir }}/{{ index_file }}">{{ bug.summary|e }}</a></td>
+ <td class="date"><a href="{{ dir }}/{{ index_file }}">{{ (bug.time_string or '')|e }}</a></td>
+</tr>
+""",
+##
+ 'bug.html':
+"""{% extends "base.html" %}
+
+{% block content %}
+{{ backlinks.render({'up_link': up_link, 'index_type':index_type, 'index_file':index_file}) }}
+<h1>Bug: {{ bug.id.user()|e }}</h1>
+
+<table>
+ <tbody>
+ <tr><td class="bug_detail_label">ID :</td>
+ <td class="bug_detail">{{ bug.uuid|e }}</td></tr>
+ <tr><td class="bug_detail_label">Short name :</td>
+ <td class="bug_detail">{{ bug.id.user()|e }}</td></tr>
+ <tr><td class="bug_detail_label">Status :</td>
+ <td class="bug_detail">{{ bug.status|e }}</td></tr>
+ <tr><td class="bug_detail_label">Severity :</td>
+ <td class="bug_detail">{{ bug.severity|e }}</td></tr>
+ <tr><td class="bug_detail_label">Assigned :</td>
+ <td class="bug_detail">{{ (bug.assigned or '')|e }}</td></tr>
+ <tr><td class="bug_detail_label">Reporter :</td>
+ <td class="bug_detail">{{ (bug.reporter or '')|e }}</td></tr>
+ <tr><td class="bug_detail_label">Creator :</td>
+ <td class="bug_detail">{{ (bug.creator or '')|e }}</td></tr>
+ <tr><td class="bug_detail_label">Created :</td>
+ <td class="bug_detail">{{ (bug.time_string or '')|e }}</td></tr>
+ <tr><td class="bug_detail_label">Summary :</td>
+ <td class="bug_detail">{{ bug.summary|e }}</td></tr>
+ </tbody>
+</table>
+
+<hr/>
+
+{% if comments %}
+{% for depth,comment in comments %}
+{% if depth == 0 %}
+<div class="comment root" id="C{{ comment_dir(comment) }}">
+{% else %}
+<div class="comment" id="C{{ comment_dir(comment) }}">
+{% endif %}
+{{ comment_entry.render({
+ 'depth':depth, 'bug': bug, 'comment':comment, 'comment_dir':comment_dir,
+ 'format_body': format_body, 'div_close': div_close}) }}
+{{ div_close(depth) }}
+{% endfor %}
+{% if comments[-1][0] > 0 %}
+{{ div_close(0) }}
+{% endif %}
+{% else %}
+<p>No comments.</p>
+{% endif %}
+{{ backlinks.render({'up_link': up_link, 'index_type': index_type}) }}
+{% endblock %}
+""",
+##
+ 'bug_backlinks.html':
+"""<p class="backlink"><a href="{{ up_link }}">Back to {{ index_type }} Index</a></p>
+<p class="backlink"><a href="../../{{ index_file }}?type=target">Back to Target Index</a></p>
+""",
+##
+ 'bug_comment_entry.html':
+"""<table>
+ <tbody>
+ <tr>
+ <td class="bug_comment_label">Comment:</td>
+ <td class="bug_comment">
+ --------- Comment ---------<br/>
+ ID: {{ comment.uuid }}<br/>
+ Short name: {{ comment.id.user() }}<br/>
+ From: {{ (comment.author or '')|e }}<br/>
+ Date: {{ (comment.date or '')|e }}<br/>
+ <br/>
+ {{ format_body(bug, comment) }}
+ </td>
+ </tr>
+ </tbody>
+</table>
+""",
}
- div.main {
- padding: 20px;
- margin: auto;
- padding-top: 0;
- margin-top: 1em;
- background-color: #fcfcfc;
- }
+ loader = DictLoader(self.template_dict)
- div.footer {
- font-size: small;
- padding-left: 20px;
- padding-right: 20px;
- padding-top: 5px;
- padding-bottom: 5px;
- margin: auto;
- background: #305275;
- color: #fffee7;
- }
+ if template_dir:
+ file_system_loader = FileSystemLoader(template_dir)
+ loader = ChoiceLoader([file_system_loader, loader])
+ self.template = Environment(loader=loader)
- table {
- border-style: solid;
- border: 10px #313131;
- border-spacing: 0;
- width: auto;
- }
- tb { border: 1px; }
-
- tr {
- vertical-align: top;
- width: auto;
- }
+class HTML (libbe.util.wsgi.ServerCommand):
+ """Serve or dump browsable HTML for the current repository
- td {
- border-width: 0;
- border-style: none;
- padding-right: 0.5em;
- padding-left: 0.5em;
- width: auto;
- }
+ >>> import sys
+ >>> import libbe.bugdir
+ >>> bugdir = libbe.bugdir.SimpleBugDir(memory=False)
+ >>> io = libbe.command.StringInputOutput()
+ >>> io.stdout = sys.stdout
+ >>> ui = libbe.command.UserInterface(io=io)
+ >>> ui.storage_callbacks.set_storage(bugdir.storage)
+ >>> cmd = HTML(ui=ui)
- img { border-style: none; }
+ >>> export_path = os.path.join(bugdir.storage.repo, 'html_export')
+ >>> ret = ui.run(cmd, {'output': export_path, 'export-html': True})
+ >>> os.path.exists(export_path)
+ True
+ >>> os.path.exists(os.path.join(export_path, 'index.html'))
+ True
+ >>> os.path.exists(os.path.join(export_path, 'index_inactive.html'))
+ True
+ >>> os.path.exists(os.path.join(export_path, bugdir.uuid))
+ True
+ >>> for bug in sorted(bugdir):
+ ... if os.path.exists(os.path.join(
+ ... export_path, bugdir.uuid, bug.uuid, 'index.html')):
+ ... print('got {}'.format(bug.uuid))
+ ... else:
+ ... print('missing {}'.format(bug.uuid))
+ got a
+ got b
- h1 {
- padding: 0.5em;
- background-color: #305275;
- margin-top: 0;
- margin-bottom: 0;
- color: #fff;
- margin-left: -20px;
- margin-right: -20px;
- }
+ >>> ui.cleanup()
+ >>> bugdir.cleanup()
+ """
+ name = 'html'
- ul {
- list-style-type: none;
- padding: 0;
- }
+ def __init__(self, *args, **kwargs):
+ super(HTML, self).__init__(*args, **kwargs)
+ # ServerApp cannot write, so drop some security options
+ self.options = [
+ option for option in self.options
+ if option.name not in [
+ 'read-only',
+ 'notify',
+ 'auth',
+ ]]
- p { width: auto; }
+ self.options.extend([
+ libbe.command.Option(name='template-dir', short_name='t',
+ help=('Use different templates. 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='Bugs Everywhere 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='Bugs Everywhere Bug List')),
+ 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='export-html', short_name='e',
+ help='Export all HTML pages and exit.'),
+ libbe.command.Option(name='output', short_name='o',
+ help='Set the output path for HTML export (%default)',
+ arg=libbe.command.Argument(
+ name='output', metavar='DIR', default='./html_export',
+ completion_callback=libbe.command.util.complete_path)),
+ 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)),
+ ])
- a, a:visited {
- background: inherit;
- text-decoration: none;
- }
+ def _run(self, **params):
+ if True in [params['export-template'], params['export-html']]:
+ app = self._get_app(
+ logger=None, storage=None, index_file='index.html',
+ generation_time=time.ctime(), **params)
+ if params['export-template']:
+ self._write_default_template(
+ template_dict=app.template_dict,
+ out_dir=params['export-template-dir'])
+ elif params['export-html']:
+ self._write_static_pages(app=app, out_dir=params['output'])
+ return 0
+ # provide defaults for the dropped options
+ params['read-only'] = True
+ params['notify'] = False
+ params['auth'] = False
+ return super(HTML, self)._run(**params)
+
+ def _get_app(self, logger, storage, index_file='', generation_time=None,
+ **kwargs):
+ return ServerApp(
+ logger=logger, bugdirs=self._get_bugdirs(),
+ template_dir=kwargs['template-dir'],
+ title=kwargs['title'],
+ header=kwargs['index-header'],
+ index_file=index_file,
+ min_id_length=kwargs['min-id-length'],
+ generation_time=generation_time)
- a { color: #003d41; }
- a:visited { color: #553d41; }
- .footer a { color: #508d91; }
+ def _long_help(self):
+ return """
+Example usage::
- /* bug index pages */
+ $ be html
- td.tab {
- padding-right: 1em;
- padding-left: 1em;
- }
+Then point your browser at ``http://localhost:8000/``.
- td.sel.tab {
- background-color: #afafaf;
- border: 1px solid #afafaf;
- font-weight:bold;
- }
+If either ``--export-html`` or ``export-template`` is set, the command
+will exit after the dump without serving anything over the wire.
+"""
- td.nsel.tab { border: 0px; }
+ def _write_default_template(self, template_dict, out_dir):
+ out_dir = self._make_dir(out_dir)
+ for filename,text in template_dict.iteritems():
+ self._write_file(text, [out_dir, filename])
+
+ def _write_static_pages(self, app, out_dir):
+ url_mappings = [
+ ('index.html?type=active', 'index.html'),
+ ('index.html?type=inactive', 'index_inactive.html'),
+ ('index.html?type=target', 'index_by_target.html'),
+ ]
+ out_dir = self._make_dir(out_dir)
+ caller = libbe.util.wsgi.WSGICaller()
+ self._write_file(
+ content=self._get_content(caller, app, 'style.css'),
+ path_array=[out_dir, 'style.css'])
+ for url,data_dict,path in [
+ ('index.html', {'type': 'active'}, 'index.html'),
+ ('index.html', {'type': 'inactive'}, 'index_inactive.html'),
+ ('index.html', {'type': 'target'}, 'index_by_target.html'),
+ ]:
+ content = self._get_content(caller, app, url, data_dict)
+ for url_,path_ in url_mappings:
+ content = content.replace(url_, path_)
+ self._write_file(content=content, path_array=[out_dir, path])
+ for bugdir in app.bugdirs.values():
+ for bug in bugdir:
+ bug_dir_url = app.bug_dir(bug=bug)
+ segments = bug_dir_url.split('/')
+ path_array = [out_dir]
+ path_array.extend(segments)
+ bug_dir_path = os.path.join(*path_array)
+ path_array.append(app._index_file)
+ url = '{}/{}'.format(bug_dir_url, app._index_file)
+ content = self._get_content(caller, app, url)
+ for url_,path_ in url_mappings:
+ content = content.replace(url_, path_)
+ if not os.path.isdir(bug_dir_path):
+ self._make_dir(bug_dir_path)
+ self._write_file(content=content, path_array=path_array)
+
+ def _get_content(self, caller, app, path, data_dict=None):
+ try:
+ return caller.getURL(app=app, path=path, data_dict=data_dict)
+ except libbe.util.wsgi.HandlerError:
+ self.stdout.write(
+ 'error retrieving {} with {}\n'.format(path, data_dict))
+ raise
- table.bug_list {
- background-color: #afafaf;
- border: 2px solid #afafaf;
- }
+ 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 "{}".'.format(dir_path))
+ return dir_path
- .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;
- }
+ def _write_file(self, content, path_array, mode='w'):
+ if not hasattr(self, 'encoding'):
+ self.encoding = libbe.util.encoding.get_text_file_encoding()
+ return libbe.util.encoding.set_file_contents(
+ os.path.join(*path_array), content, mode, self.encoding)
- 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> |
- <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>
- """
+Html = HTML # alias for libbe.command.base.get_command_class()
- 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> |
- <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>
- """
+class _DivCloser (object):
+ def __init__(self, depth=0):
+ self.depth = depth
- # 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')
+ def __call__(self, depth):
+ ret = []
+ while self.depth >= depth:
+ self.depth -= 1
+ ret.append('</div>')
+ self.depth = depth
+ return '\n'.join(ret)