diff options
Diffstat (limited to 'libbe/command')
-rw-r--r-- | libbe/command/__init__.py | 4 | ||||
-rw-r--r-- | libbe/command/assign.py | 17 | ||||
-rw-r--r-- | libbe/command/base.py | 45 | ||||
-rw-r--r-- | libbe/command/comment.py | 2 | ||||
-rw-r--r-- | libbe/command/commit.py | 30 | ||||
-rw-r--r-- | libbe/command/depend.py | 4 | ||||
-rw-r--r-- | libbe/command/diff.py | 1 | ||||
-rw-r--r-- | libbe/command/due.py | 3 | ||||
-rw-r--r-- | libbe/command/help.py | 1 | ||||
-rw-r--r-- | libbe/command/html.py | 1091 | ||||
-rw-r--r-- | libbe/command/import_xml.py | 12 | ||||
-rw-r--r-- | libbe/command/init.py | 1 | ||||
-rw-r--r-- | libbe/command/list.py | 2 | ||||
-rw-r--r-- | libbe/command/merge.py | 3 | ||||
-rw-r--r-- | libbe/command/new.py | 28 | ||||
-rw-r--r-- | libbe/command/remove.py | 1 | ||||
-rw-r--r-- | libbe/command/serve.py | 66 | ||||
-rw-r--r-- | libbe/command/set.py | 52 | ||||
-rw-r--r-- | libbe/command/severity.py | 29 | ||||
-rw-r--r-- | libbe/command/show.py | 1 | ||||
-rw-r--r-- | libbe/command/status.py | 10 | ||||
-rw-r--r-- | libbe/command/subscribe.py | 3 | ||||
-rw-r--r-- | libbe/command/tag.py | 3 | ||||
-rw-r--r-- | libbe/command/util.py | 3 |
24 files changed, 801 insertions, 611 deletions
diff --git a/libbe/command/__init__.py b/libbe/command/__init__.py index 1cad096..b520f40 100644 --- a/libbe/command/__init__.py +++ b/libbe/command/__init__.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com> +# Chris Ball <cjb@laptop.org> # W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. @@ -19,6 +20,7 @@ import base UserError = base.UserError +UsageError = base.UsageError UnknownCommand = base.UnknownCommand get_command = base.get_command get_command_class = base.get_command_class @@ -33,7 +35,7 @@ UnconnectedStorageGetter = base.UnconnectedStorageGetter StorageCallbacks = base.StorageCallbacks UserInterface = base.UserInterface -__all__ = [UserError, UnknownCommand, +__all__ = [UserError, UsageError, UnknownCommand, get_command, get_command_class, commands, Option, Argument, Command, InputOutput, StdInputOutput, StringInputOutput, diff --git a/libbe/command/assign.py b/libbe/command/assign.py index 0b3f407..99a657b 100644 --- a/libbe/command/assign.py +++ b/libbe/command/assign.py @@ -1,6 +1,8 @@ # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com> +# Chris Ball <cjb@laptop.org> # Gianluca Montecchi <gian@grys.it> # Marien Zwart <marien.zwart@gmail.com> +# Robert Lehmann <mail@robertlehmann.de> # Thomas Gerigk <tgerigk@gmx.de> # W. Trevor King <wking@drexel.edu> # @@ -73,11 +75,7 @@ class Assign (libbe.command.Command): ]) def _run(self, **params): - assigned = params['assigned'] - if assigned == 'none': - assigned = None - elif assigned == '-': - assigned = self._get_user_id() + assigned = parse_assigned(self, params['assigned']) bugdir = self._get_bugdir() for bug_id in params['bug-id']: bug,dummy_comment = \ @@ -99,3 +97,12 @@ Special assigned strings: "-" assign the bug to yourself "none" un-assigns the bug """ + +def parse_assigned(command, assigned): + """Standard processing for the 'assigned' Argument. + """ + if assigned == 'none': + assigned = None + elif assigned == '-': + assigned = command._get_user_id() + return assigned diff --git a/libbe/command/base.py b/libbe/command/base.py index b5f5a22..11835ee 100644 --- a/libbe/command/base.py +++ b/libbe/command/base.py @@ -1,4 +1,6 @@ -# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu> +# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org> +# Robert Lehmann <mail@robertlehmann.de> +# W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. # @@ -27,13 +29,41 @@ import libbe.ui.util.user import libbe.util.encoding import libbe.util.plugin -class UserError(Exception): + +class UserError (Exception): + "An error due to improper BE usage." pass -class UnknownCommand(UserError): - def __init__(self, cmd): - Exception.__init__(self, "Unknown command '%s'" % cmd) - self.cmd = cmd + +class UsageError (UserError): + """A serious parsing error due to invalid BE command construction. + + The distinction between `UserError`\s and the more specific + `UsageError`\s is that when displaying a `UsageError` to the user, + the user is pointed towards the command usage information. Use + the more general `UserError` if you feel that usage information + would not be particularly enlightening. + """ + def __init__(self, command=None, command_name=None, message=None): + super(UsageError, self).__init__(message) + self.command = command + if command_name is None and command is not None: + command_name = command.name + self.command_name = command_name + self.message = message + + +class UnknownCommand (UsageError): + def __init__(self, command_name, message=None): + uc_message = "Unknown command '%s'" % command_name + if message is None: + message = uc_message + else: + message = '%s\n(%s)' % (uc_message, message) + super(UnknownCommand, self).__init__( + command_name=command_name, + message=message) + def get_command(command_name): """Retrieves the module for a user command @@ -43,6 +73,7 @@ def get_command(command_name): ... except UnknownCommand, e: ... print e Unknown command 'asdf' + (No module named asdf) >>> repr(get_command('list')).startswith("<module 'libbe.command.list' from ") True """ @@ -50,7 +81,7 @@ def get_command(command_name): cmd = libbe.util.plugin.import_by_name( 'libbe.command.%s' % command_name.replace("-", "_")) except ImportError, e: - raise UnknownCommand(command_name) + raise UnknownCommand(command_name, message=unicode(e)) return cmd def get_command_class(module=None, command_name=None): diff --git a/libbe/command/comment.py b/libbe/command/comment.py index d182840..7fa6ec7 100644 --- a/libbe/command/comment.py +++ b/libbe/command/comment.py @@ -1,5 +1,7 @@ # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com> +# Chris Ball <cjb@laptop.org> # Gianluca Montecchi <gian@grys.it> +# Robert Lehmann <mail@robertlehmann.de> # W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. diff --git a/libbe/command/commit.py b/libbe/command/commit.py index a2ed051..8416107 100644 --- a/libbe/command/commit.py +++ b/libbe/command/commit.py @@ -1,4 +1,5 @@ -# Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it> +# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org> +# Gianluca Montecchi <gian@grys.it> # W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. @@ -59,16 +60,19 @@ class Commit (libbe.command.Command): ]) self.args.extend([ libbe.command.Argument( - name='comment', metavar='COMMENT', default=None), + name='summary', metavar='SUMMARY', default=None, + optional=True), ]) def _run(self, **params): - if params['comment'] == '-': # read summary from stdin + if params['summary'] == '-': # read summary from stdin assert params['body'] != 'EDITOR', \ 'Cannot spawn and editor when the summary is using stdin.' summary = sys.stdin.readline() else: - summary = params['comment'] + summary = params['summary'] + if summary == None and params['body'] == None: + params['body'] = 'EDITOR' storage = self._get_storage() if params['body'] == None: body = None @@ -79,6 +83,13 @@ class Commit (libbe.command.Command): self._check_restricted_access(storage, params['body']) body = libbe.util.encoding.get_file_contents( params['body'], decode=True) + if summary == None: # use the first body line as the summary + if body == None: + raise libbe.command.UserError( + 'cannot commit without a summary') + lines = body.splitlines() + summary = lines[0] + body = '\n'.join(lines[1:]).strip() + '\n' try: revision = storage.commit(summary, body=body, allow_empty=params['allow-empty']) @@ -89,7 +100,12 @@ class Commit (libbe.command.Command): def _long_help(self): return """ -Commit the current repository status. The summary specified on the -commandline is a string (only one line) that describes the commit -briefly or "-", in which case the string will be read from stdin. +Commit the current repository status. + +The summary specified on the commandline is a string (only one line) +that describes the commit briefly or "-", in which case the string +will be read from stdin. If no summary is given, the first line from +the body message is used instead. If no summary or body is given, we +spawn an editor without needing the special "EDITOR" value for the +"--body" option. """ diff --git a/libbe/command/depend.py b/libbe/command/depend.py index 9ae449a..1aa5053 100644 --- a/libbe/command/depend.py +++ b/libbe/command/depend.py @@ -1,4 +1,6 @@ -# Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it> +# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org> +# Gianluca Montecchi <gian@grys.it> +# Robert Lehmann <mail@robertlehmann.de> # W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. diff --git a/libbe/command/diff.py b/libbe/command/diff.py index 08a7efb..a9cdd50 100644 --- a/libbe/command/diff.py +++ b/libbe/command/diff.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com> +# Chris Ball <cjb@laptop.org> # Gianluca Montecchi <gian@grys.it> # W. Trevor King <wking@drexel.edu> # diff --git a/libbe/command/due.py b/libbe/command/due.py index e4fd0f1..cf1500d 100644 --- a/libbe/command/due.py +++ b/libbe/command/due.py @@ -1,4 +1,5 @@ -# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu> +# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org> +# W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. # diff --git a/libbe/command/help.py b/libbe/command/help.py index e4825f0..01eae5c 100644 --- a/libbe/command/help.py +++ b/libbe/command/help.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com> +# Chris Ball <cjb@laptop.org> # Gianluca Montecchi <gian@grys.it> # Marien Zwart <marien.zwart@gmail.com> # Thomas Gerigk <tgerigk@gmx.de> diff --git a/libbe/command/html.py b/libbe/command/html.py index bb5b554..7420ce8 100644 --- a/libbe/command/html.py +++ b/libbe/command/html.py @@ -1,4 +1,5 @@ -# Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it> +# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org> +# Gianluca Montecchi <gian@grys.it> # Mathieu Clabaut <mathieu.clabaut@gmail.com> # W. Trevor King <wking@drexel.edu> # @@ -26,6 +27,8 @@ import string import time import xml.sax.saxutils +from jinja2 import Environment, FileSystemLoader, DictLoader, ChoiceLoader + import libbe import libbe.command import libbe.command.util @@ -82,12 +85,12 @@ class HTML (libbe.command.Command): help='Set the bug repository title (%default)', arg=libbe.command.Argument( name='title', metavar='STRING', - default='BugsEverywhere Issue Tracker')), + 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='BugsEverywhere Bug List')), + default='Bugs Everywhere 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', @@ -112,9 +115,9 @@ class HTML (libbe.command.Command): bugdir = self._get_bugdir() bugdir.load_all_bugs() html_gen = HTMLGen(bugdir, - template=params['template-dir'], + template_dir=params['template-dir'], title=params['title'], - index_header=params['index-header'], + header=params['index-header'], min_id_length=params['min-id-length'], verbose=params['verbose'], stdout=self.stdout) @@ -132,28 +135,22 @@ 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", + def __init__(self, bd, template_dir=None, + title="Site Title", header="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.header = 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._load_templates(template_dir) self.min_id_length = min_id_length def run(self, out_dir): @@ -179,21 +176,24 @@ class HTMLGen (object): self._create_output_directories(out_dir) self._write_css_file() for b in bugs: - if b.active: + if b.severity == 'target': + up_link = '../../index_target.html' + elif b.active: up_link = '../../index.html' else: - up_link = '../../index_inactive.html' - - self._write_bug_file(b, up_link) + up_link = '../../index_inactive.html' + self._write_bug_file( + b, title=self.title, header=self.header, + up_link=up_link) self._write_index_file( bugs_active, title=self.title, - index_header=self.index_header, bug_type='active') + header=self.header, bug_type='active') self._write_index_file( bugs_inactive, title=self.title, - index_header=self.index_header, bug_type='inactive') + header=self.header, bug_type='inactive') self._write_index_file( bugs_target, title=self.title, - index_header=self.index_header, bug_type='target') + header=self.header, bug_type='target') def _truncated_bug_id(self, bug): return libbe.util.id._truncate( @@ -217,10 +217,10 @@ class HTMLGen (object): 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']) + template = self.template.get_template('style.css') + self._write_file(template.render(), [self.out_dir, 'style.css']) - def _write_bug_file(self, bug, up_link): + def _write_bug_file(self, bug, title, header, up_link): if self.verbose: print >> self.stdout, '\tCreating bug file for %s' % bug.id.user() assert hasattr(self, 'out_dir_bugs'), \ @@ -229,96 +229,77 @@ class HTMLGen (object): if bug.active == True: index_type = 'Active' - else : + else: index_type = 'Inactive' if bug.severity == 'target': index_type = 'Target' bug.load_comments(load_full=True) - comment_entries = self._generate_bug_comment_entries(bug) + bug.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True) 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, - 'index_type':index_type} - for attr in ['uuid', 'severity', 'status', 'assigned', - 'reporter', 'creator', 'time_string', 'summary']: - template_info[attr] = self._escape(getattr(bug, attr)) + template_info = { + 'title': title, + 'charset': self.encoding, + 'stylesheet': '../../style.css', + 'header': header, + 'backlinks': self.template.get_template('bug_backlinks.html'), + 'up_link': up_link, + 'index_type': index_type, + '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, + } 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]) + template = self.template.get_template('bug.html') + self._write_file(template.render(template_info), [fullpath]) - def _generate_bug_comment_entries(self, bug): - assert hasattr(self, 'out_dir_bugs'), \ - 'Must run after ._create_output_directories()' + def _write_index_file(self, bugs, title, 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()' - 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) + if bug_type == 'active': + filename = 'index.html' + elif bug_type == 'inactive': + filename = 'index_inactive.html' + elif bug_type == 'target': + filename = 'index_by_target.html' + else: + raise ValueError('unrecognized bug_type: "%s"' % bug_type) + + template_info = { + 'title': title, + 'charset': self.encoding, + 'stylesheet': 'style.css', + 'header': 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._truncated_bug_id, + 'generation_time': self.generation_time, + } + template_info['%s_class' % 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.bd, target))) + for target in bugs] + else: + template = self.template.get_template('standard_index.html') + self._write_file( + template.render(template_info)+'\n', [self.out_dir,filename]) def _long_to_linked_user(self, text): """ @@ -382,91 +363,46 @@ class HTMLGen (object): 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 - if (bug_type == 'target'): - bug_entries = self._generate_index_bug_entries_target(bugs) + 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: - bug_entries = self._generate_index_bug_entries(bugs) - - if bug_type == 'active': - filename = 'index.html' - elif bug_type == 'inactive': - filename = 'index_inactive.html' - elif bug_type == 'target': - filename = 'index_by_target.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', - 'target_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' - if bug_type == 'target': - template_info['active_class'] = 'tab nsel' - template_info['target_class'] = 'tab sel' - self._write_file(self.index_file % template_info, - [self.out_dir, filename]) - - def _generate_index_bug_entries_target(self, targets): - - target_entries = [] - for target in targets: - bug_entries = [] - template_info_list = {'target':target.summary, 'bug_entries': '', 'status': target.status} - blocker = libbe.command.depend.get_blocked_by(self.bd, target) - for bug in blocker: - 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) - template_info_list['bug_entries'] = '\n'.join(bug_entries) - target_entries.append(self.target_bug_list % template_info_list) - return '\n'.join(target_entries) - - def _generate_index_bug_entries(self, bugs): - bug_entries = [] - template_info_list = {'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) - template_info_list['bug_entries'] = '\n'.join(bug_entries) - return self.bug_list % template_info_list + 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 _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): @@ -489,388 +425,425 @@ class HTMLGen (object): 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"; - 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; - } - - div.target_name { - 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; } - - 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: #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 { - border-width: 1px; - border-style: solid; - border-collapse: collapse; - border-color: #c3d9ff; - 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; + for filename,text in self.template_dict.iteritems(): + if self.verbose: + print >> self.stdout, 'Creating %s file' + self._write_file(text, [self.out_dir, filename]) + + 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> +""", + 'index.html': +"""{% extends "base.html" %} + +{% block content %} +<table> + <tbody> + <tr> + <td class="{{ active_class }}"><a href="index.html">Active Bugs</a></td> + <td class="{{ inactive_class }}"><a href="index_inactive.html">Inactive Bugs</a></td> + <td class="{{ target_class }}"><a href="index_by_target.html">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)}) }} + {% 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)}) }} + {% endfor %} + </tbody> +</table> +{% endfor %} +{% endblock %} +""", +## + 'index_bug_entry.html': +"""<tr class="{{ bug.severity }}"> + <td class="uuid"><a href="bugs/{{ dir }}/index.html">{{ bug.id.user()|e }}</a></td> + <td class="status"><a href="bugs/{{ dir }}/index.html">{{ bug.status|e }}</a></td> + <td class="severity"><a href="bugs/{{ dir }}/index.html">{{ bug.severity|e }}</a></td> + <td class="summary"><a href="bugs/{{ dir }}/index.html">{{ bug.summary|e }}</a></td> + <td class="date"><a href="bugs/{{ dir }}/index.html">{{ (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}) }} +<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_by_target.html">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.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="header">%(index_header)s</div> - <div class="main"> - <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> - <td class="%(target_class)s"><a href="index_by_target.html">Divided by target</a></td> - </tr> - </table> + loader = DictLoader(self.template_dict) + if template_dir: + file_system_loader = FileSystemLoader(template_dir) + loader = ChoiceLoader([file_system_loader, loader]) - %(bug_entries)s + self.template = Environment(loader=loader) - </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 class="uuid"><a href="bugs/%(dir)s/index.html">%(shortname)s</a></td> - <td class="status"><a href="bugs/%(dir)s/index.html">%(status)s</a></td> - <td class="severity"><a href="bugs/%(dir)s/index.html">%(severity)s</a></td> - <td class="summary"><a href="bugs/%(dir)s/index.html">%(summary)s</a></td> - <td class="date"><a href="bugs/%(dir)s/index.html">%(time_string)s</a></td> - </tr> - """ - self.target_bug_list = """ - <tr> - <td> - <div class="target_name"> - Target: %(target)s (%(status)s) - </div> - <div> - <table class="target_list"> - - %(bug_entries)s - </table> - </div> - </td> - </tr> - """ - self.bug_list = """ - <table class="bug_list"> - - %(bug_entries)s - - </table> - - """ - 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="header">BugsEverywhere Bug List</div> - <div class="main"> - <h5><a href="%(up_link)s">Back to %(index_type)s Index</a></h5> - <h5><a href="../../index_by_target.html">Back to Target 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_type)s Index</a></h5> - <h5><a href="../../index_by_target.html">Back to Target 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) diff --git a/libbe/command/import_xml.py b/libbe/command/import_xml.py index bd25372..d53df8c 100644 --- a/libbe/command/import_xml.py +++ b/libbe/command/import_xml.py @@ -1,4 +1,6 @@ -# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu> +# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org> +# Valtteri Kokkoniemi <rvk@iki.fi> +# W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. # @@ -76,6 +78,8 @@ class Import_XML (libbe.command.Command): help="If any comment's <in-reply-to> refers to a non-existent comment, ignore it (instead of raising an exception)."), libbe.command.Option(name='add-only', short_name='a', help='If any bug or comment listed in the XML file already exists in the bug repository, do not alter the repository version.'), + libbe.command.Option(name='preserve-uuids', short_name='p', + help='Preserve UUIDs for trusted input (potential name collisions).'), libbe.command.Option(name='comment-root', short_name='c', help='Supply a bug or comment ID as the root of any <comment> elements that are direct children of the <be-xml> element. If any such <comment> elements exist, you are required to set this option.', arg=libbe.command.Argument( @@ -131,11 +135,11 @@ class Import_XML (libbe.command.Command): for child in be_xml.getchildren(): if child.tag == 'bug': new = libbe.bug.Bug(bugdir=bugdir) - new.from_xml(child) + new.from_xml(child, preserve_uuids=params['preserve-uuids']) root_bugs.append(new) elif child.tag == 'comment': new = libbe.comment.Comment(croot_bug) - new.from_xml(child) + new.from_xml(child, preserve_uuids=params['preserve-uuids']) root_comments.append(new) elif child.tag == 'version': for gchild in child.getchildren(): @@ -184,7 +188,7 @@ class Import_XML (libbe.command.Command): except KeyError: old = None if old == None: - bd.append(new) + bugdir.append(new) else: old.load_comments(load_full=True) old.merge(new, accept_changes=accept_changes, diff --git a/libbe/command/init.py b/libbe/command/init.py index 92e07f9..378e544 100644 --- a/libbe/command/init.py +++ b/libbe/command/init.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com> +# Chris Ball <cjb@laptop.org> # Gianluca Montecchi <gian@grys.it> # W. Trevor King <wking@drexel.edu> # diff --git a/libbe/command/list.py b/libbe/command/list.py index 9eda277..59254b2 100644 --- a/libbe/command/list.py +++ b/libbe/command/list.py @@ -1,6 +1,8 @@ # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com> +# Chris Ball <cjb@laptop.org> # Gianluca Montecchi <gian@grys.it> # Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com> +# Robert Lehmann <mail@robertlehmann.de> # W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. diff --git a/libbe/command/merge.py b/libbe/command/merge.py index 7fd62bb..0f49de9 100644 --- a/libbe/command/merge.py +++ b/libbe/command/merge.py @@ -1,4 +1,5 @@ -# Copyright (C) 2008-2011 Gianluca Montecchi <gian@grys.it> +# Copyright (C) 2008-2011 Chris Ball <cjb@laptop.org> +# Gianluca Montecchi <gian@grys.it> # W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. diff --git a/libbe/command/new.py b/libbe/command/new.py index 7dfbed8..deba8da 100644 --- a/libbe/command/new.py +++ b/libbe/command/new.py @@ -1,4 +1,6 @@ # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com> +# Andrew Cooper <andrew.cooper@hkcreations.org> +# Chris Ball <cjb@laptop.org> # Gianluca Montecchi <gian@grys.it> # W. Trevor King <wking@drexel.edu> # @@ -21,6 +23,8 @@ import libbe import libbe.command import libbe.command.util +from .assign import parse_assigned as _parse_assigned + class New (libbe.command.Command): """Create a new bug @@ -40,7 +44,8 @@ class New (libbe.command.Command): >>> uuid_gen = libbe.util.id.uuid_gen >>> libbe.util.id.uuid_gen = lambda: 'X' >>> ui._user_id = u'Fran\\xe7ois' - >>> ret = ui.run(cmd, args=['this is a test',]) + >>> options = {'assigned': 'none'} + >>> ret = ui.run(cmd, options=options, args=['this is a test',]) Created bug with ID abc/X >>> libbe.util.id.uuid_gen = uuid_gen >>> bd.flush_reload() @@ -57,6 +62,8 @@ class New (libbe.command.Command): minor >>> print bug.status open + >>> print bug.assigned + None >>> ui.cleanup() >>> bd.cleanup() """ @@ -78,6 +85,16 @@ class New (libbe.command.Command): arg=libbe.command.Argument( name='assigned', metavar='NAME', completion_callback=libbe.command.util.complete_assigned)), + libbe.command.Option(name='status', short_name='t', + help='The bug\'s status level', + arg=libbe.command.Argument( + name='status', metavar='STATUS', + completion_callback=libbe.command.util.complete_status)), + libbe.command.Option(name='severity', short_name='s', + help='The bug\'s severity', + arg=libbe.command.Argument( + name='severity', metavar='SEVERITY', + completion_callback=libbe.command.util.complete_severity)), ]) self.args.extend([ libbe.command.Argument(name='summary', metavar='SUMMARY') @@ -89,6 +106,7 @@ class New (libbe.command.Command): else: summary = params['summary'] bugdir = self._get_bugdir() + bugdir.storage.writeable = False bug = bugdir.new_bug(summary=summary.strip()) if params['creator'] != None: bug.creator = params['creator'] @@ -99,7 +117,13 @@ class New (libbe.command.Command): else: bug.reporter = bug.creator if params['assigned'] != None: - bug.assigned = params['assigned'] + bug.assigned = _parse_assigned(self, params['assigned']) + if params['status'] != None: + bug.status = params['status'] + if params['severity'] != None: + bug.severity = params['severity'] + bugdir.storage.writeable = True + bug.save() print >> self.stdout, 'Created bug with ID %s' % bug.id.user() return 0 diff --git a/libbe/command/remove.py b/libbe/command/remove.py index 1d4f265..095f1d3 100644 --- a/libbe/command/remove.py +++ b/libbe/command/remove.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com> +# Chris Ball <cjb@laptop.org> # Gianluca Montecchi <gian@grys.it> # Marien Zwart <marien.zwart@gmail.com> # Thomas Gerigk <tgerigk@gmx.de> diff --git a/libbe/command/serve.py b/libbe/command/serve.py index ba4b0d8..00591c0 100644 --- a/libbe/command/serve.py +++ b/libbe/command/serve.py @@ -1,4 +1,5 @@ -# Copyright (C) 2010-2011 W. Trevor King <wking@drexel.edu> +# Copyright (C) 2010-2011 Chris Ball <cjb@laptop.org> +# W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. # @@ -58,6 +59,7 @@ import libbe import libbe.command import libbe.command.util import libbe.util.encoding +import libbe.util.subproc import libbe.version if libbe.TESTING == True: @@ -507,9 +509,10 @@ class ServerApp (WSGI_AppObject): """ server_version = "BE-server/" + libbe.version.version() - def __init__(self, storage, *args, **kwargs): - WSGI_AppObject.__init__(self, *args, **kwargs) + def __init__(self, storage, notify=False, **kwargs): + WSGI_AppObject.__init__(self, **kwargs) self.storage = storage + self.notify = notify self.http_user_error = 418 self.urls = [ @@ -570,6 +573,9 @@ class ServerApp (WSGI_AppObject): directory = self.data_get_boolean( data, 'directory', default=False, source=source) self.storage.add(id, parent=parent, directory=directory) + if self.notify: + self._notify(environ, 'add', id, + [('parent', parent), ('directory', directory)]) return self.ok_response(environ, start_response, None) def exists(self, environ, start_response): @@ -593,6 +599,8 @@ class ServerApp (WSGI_AppObject): self.storage.recursive_remove(id) else: self.storage.remove(id) + if self.notify: + self._notify(environ, 'remove', id, [('recursive', recursive)]) return self.ok_response(environ, start_response, None) def ancestors(self, environ, start_response): @@ -641,6 +649,8 @@ class ServerApp (WSGI_AppObject): raise _HandlerError(406, 'Missing query key value') value = data['value'] self.storage.set(id, value) + if self.notify: + self._notify(environ, 'set', id, [('value', value)]) return self.ok_response(environ, start_response, None) def commit(self, environ, start_response): @@ -661,6 +671,10 @@ class ServerApp (WSGI_AppObject): revision = self.storage.commit(summary, body, allow_empty) except libbe.storage.EmptyCommit, e: raise _HandlerError(self.http_user_error, 'EmptyCommit') + if self.notify: + self._notify(environ, 'commit', id, + [('allow_empty', allow_empty), ('summary', summary), + ('body', body)]) return self.ok_response(environ, start_response, revision) def revision_id(self, environ, start_response): @@ -700,9 +714,43 @@ class ServerApp (WSGI_AppObject): raise _Unauthorized() # only non-guests allowed to write # allow read-only commands for all users + def _notify(self, environ, command, id, params): + message = self._format_notification(environ, command, id, params) + self._submit_notification(message) + + def _format_notification(self, environ, command, id, params): + key_length = len('command') + for key,value in params: + if len(key) > key_length and '\n' not in str(value): + key_length = len(key) + key_length += 1 + lines = [] + multi_line_params = [] + for key,value in [('address', environ.get('REMOTE_ADDR', '-')), + ('command', command), ('id', id)]+params: + v = str(value) + if '\n' in v: + multi_line_params.append((key,v)) + continue + lines.append('%*.*s %s' % (key_length, key_length, key+':', v)) + lines.append('') + for key,value in multi_line_params: + lines.extend(['=== START %s ===' % key, v, + '=== STOP %s ===' % key, '']) + lines.append('') + return '\n'.join(lines) + + def _submit_notification(self, message): + libbe.util.subproc.invoke(self.notify, stdin=message, shell=True) + class Serve (libbe.command.Command): - """:class:`~libbe.command.base.Command` wrapper around + """Serve bug directory storage over HTTP. + + This allows you to run local `be` commands interfacing with remote + data, transmitting file reads/writes/etc. over the network. + + :class:`~libbe.command.base.Command` wrapper around :class:`ServerApp`. """ @@ -721,6 +769,10 @@ class Serve (libbe.command.Command): name='host', metavar='HOST', default='')), libbe.command.Option(name='read-only', short_name='r', help='Dissable operations that require writing'), + libbe.command.Option(name='notify', short_name='n', + help='Send notification emails for changes.', + arg=libbe.command.Argument( + name='notify', metavar='EMAIL-COMMAND', default=None)), libbe.command.Option(name='ssl', short_name='s', help='Use CherryPy to serve HTTPS (HTTP over SSL/TLS)'), libbe.command.Option(name='auth', short_name='a', @@ -742,7 +794,8 @@ class Serve (libbe.command.Command): self._check_restricted_access(storage, params['auth']) users = Users(params['auth']) users.load() - app = ServerApp(storage=storage, logger=self.logger) + app = ServerApp( + storage=storage, notify=params['notify'], logger=self.logger) if params['auth'] != None: app = AdminApp(app, users=users, logger=self.logger) app = AuthenticationApp(app, realm=storage.repo, @@ -860,6 +913,7 @@ if libbe.TESTING == True: self.logger.setLevel(logging.INFO) self.default_environ = { # required by PEP 333 'REQUEST_METHOD': 'GET', # 'POST', 'HEAD' + 'REMOTE_ADDR': '192.168.0.123', 'SCRIPT_NAME':'', 'PATH_INFO': '', #'QUERY_STRING':'', # may be empty or absent @@ -920,7 +974,7 @@ if libbe.TESTING == True: self.app.log_request( environ=self.default_environ, status='-1 OK', bytes=123) log = self.logstream.getvalue() - self.failUnless(log.startswith('- -'), log) + self.failUnless(log.startswith('192.168.0.123 -'), log) class ExceptionAppTestCase (WSGITestCase): def setUp(self): diff --git a/libbe/command/set.py b/libbe/command/set.py index b3eb583..d70d33e 100644 --- a/libbe/command/set.py +++ b/libbe/command/set.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com> +# Chris Ball <cjb@laptop.org> # Gianluca Montecchi <gian@grys.it> # Marien Zwart <marien.zwart@gmail.com> # Thomas Gerigk <tgerigk@gmx.de> @@ -100,7 +101,56 @@ To unset a setting, set it to "none". Allowed settings are: -%s""" % ('\n'.join(get_bugdir_settings()),) +%s + +Note that this command does not provide a good interface for some of +these settings (yet!). You may need to edit the bugdir settings file +(`.be/<bugdir>/settings`) manually. Examples for each troublesome +setting are given below. + +Add the following lines to override the default severities and use +your own: + + severities: + - - target + - The issue is a target or milestone, not a bug. + - - wishlist + - A feature that could improve usefulness, but not a bug. + +You may add as many name/description pairs as you wish to have; they +are sorted in order from least important at the top, to most important +at the bottom. The target severity gets special handling by `be +target`. + +Note that the values here _override_ the defaults. That means that if +you like the defaults, and wish to keep them, you will have to copy +them here before adding any of your own. See `be severity --help` for +the current list. + +Add the following lines to override the default statuses and use your +own: + + active_status: + - - unconfirmed + - A possible bug which lacks independent existance confirmation. + - - open + - A working bug that has not been assigned to a developer. + + inactive_status: + - - closed + - The bug is no longer relevant. + - - fixed + - The bug should no longer occur. + +You may add as many name/description pairs as you wish to have; they +are sorted in order from most important at the top, to least important +at the bottom. + +Note that the values here _override_ the defaults. That means that if +you like the defaults, and wish to keep them, you will have to copy +them here before adding any of your own. See `be status --help` for +the current list. +""" % ('\n'.join(get_bugdir_settings()),) def get_bugdir_settings(): settings = [] diff --git a/libbe/command/severity.py b/libbe/command/severity.py index a84efe8..fa6007a 100644 --- a/libbe/command/severity.py +++ b/libbe/command/severity.py @@ -1,7 +1,9 @@ # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com> +# Chris Ball <cjb@laptop.org> # Gianluca Montecchi <gian@grys.it> # Marien Zwart <marien.zwart@gmail.com> # Thomas Gerigk <tgerigk@gmx.de> +# Tim Guirgies <lt.infiltrator@gmail.com> # W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. @@ -79,21 +81,26 @@ class Severity (libbe.command.Command): return 0 def _long_help(self): - ret = [""" -Show or change a bug's severity level. - -If no severity is specified, the current value is printed. If a severity level -is specified, it will be assigned to the bug. - -Severity levels are: -"""] try: # See if there are any per-tree severity configurations bd = self._get_bugdir() except NotImplementedError: pass # No tree, just show the defaults longest_severity_len = max([len(s) for s in libbe.bug.severity_values]) + severity_levels = [] for severity in libbe.bug.severity_values : description = libbe.bug.severity_description[severity] - ret.append('%*s : %s\n' \ - % (longest_severity_len, severity, description)) - return ''.join(ret) + s = '%*s : %s' % (longest_severity_len, severity, description) + severity_levels.append(s) + ret = """ +Show or change a bug's severity level. + +If no severity is specified, the current value is printed. If a severity level +is specified, it will be assigned to the bug. + +Severity levels are: + %s + +You can overide the list of allowed severities on a per-repository +basis. See `be set --help` for details. +""" % ('\n '.join(severity_levels)) + return ret diff --git a/libbe/command/show.py b/libbe/command/show.py index ea86191..4f85c69 100644 --- a/libbe/command/show.py +++ b/libbe/command/show.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com> +# Chris Ball <cjb@laptop.org> # Gianluca Montecchi <gian@grys.it> # Thomas Gerigk <tgerigk@gmx.de> # Thomas Habets <thomas@habets.pp.se> diff --git a/libbe/command/status.py b/libbe/command/status.py index 2e470e4..2eb0755 100644 --- a/libbe/command/status.py +++ b/libbe/command/status.py @@ -1,7 +1,9 @@ # Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com> +# Chris Ball <cjb@laptop.org> # Gianluca Montecchi <gian@grys.it> # Marien Zwart <marien.zwart@gmail.com> # Thomas Gerigk <tgerigk@gmx.de> +# Tim Guirgies <lt.infiltrator@gmail.com> # W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. @@ -80,6 +82,10 @@ class Status (libbe.command.Command): return 0 def _long_help(self): + try: # See if there are any per-tree status configurations + bd = self._get_bugdir() + except NotImplementedError: + pass # No tree, just show the defaults longest_status_len = max([len(s) for s in libbe.bug.status_values]) active_statuses = [] for status in libbe.bug.active_status_values : @@ -106,7 +112,7 @@ Active status levels are: Inactive status levels are: %s -You can overide the list of allowed statuses on a per-repository basis. -See "be set --help" for more details. +You can overide the list of allowed statuses on a per-repository +basis. See `be set --help` for details. """ % ('\n '.join(active_statuses), '\n '.join(inactive_statuses)) return ret diff --git a/libbe/command/subscribe.py b/libbe/command/subscribe.py index f49abb9..50f1e7e 100644 --- a/libbe/command/subscribe.py +++ b/libbe/command/subscribe.py @@ -1,4 +1,5 @@ -# Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it> +# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org> +# Gianluca Montecchi <gian@grys.it> # W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. diff --git a/libbe/command/tag.py b/libbe/command/tag.py index dea6e00..1da8fd9 100644 --- a/libbe/command/tag.py +++ b/libbe/command/tag.py @@ -1,4 +1,5 @@ -# Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it> +# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org> +# Gianluca Montecchi <gian@grys.it> # W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. diff --git a/libbe/command/util.py b/libbe/command/util.py index d8e049e..4e5471d 100644 --- a/libbe/command/util.py +++ b/libbe/command/util.py @@ -1,4 +1,5 @@ -# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu> +# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org> +# W. Trevor King <wking@drexel.edu> # # This file is part of Bugs Everywhere. # |