diff options
Diffstat (limited to 'interfaces')
79 files changed, 4337 insertions, 0 deletions
diff --git a/interfaces/email/interactive/README b/interfaces/email/interactive/README new file mode 100644 index 0000000..48bccdd --- /dev/null +++ b/interfaces/email/interactive/README @@ -0,0 +1,160 @@ +*************** +Email Interface +*************** + +Overview +======== + +The interactive email interface to Bugs Everywhere (BE) attempts to +provide a `Debian-bug-tracking-system-style`_ interface to a BE +repository. Users can mail in bug reports, comments, or control +requests, which will be committed to the served repository. +Developers can then pull the changes they approve of from the served +repository into their other repositories and push updates back onto +the served repository. + +.. _Debian-bug-tracking-system-style: http://www.debian.org/Bugs + +Architecture +============ + +In order to reduce setup costs, the entire interface can piggyback on +an existing email address, although from a security standpoint it's +probably best to create a dedicated user. Incoming email is filtered +by procmail, with matching emails being piped into ``be-handle-mail`` +for execution. + +Once ``be-handle-mail`` receives the email, the parsing method is +selected according to the subject tag that procmail used grab the +email in the first place. There are four parsing styles: + + +--------------------+----------------------------------+ + | Style | Subject | + +====================+==================================+ + | creating bugs | [be-bug:submit] new bug summary | + +--------------------+----------------------------------+ + | commenting on bugs | [be-bug:<bug-id>] commit message | + +--------------------+----------------------------------+ + | control | [be-bug] commit message | + +--------------------+----------------------------------+ + +These are analogous to ``submit@bugs.debian.org``, +``nnn@bugs.debian.org``, and ``control@bugs.debian.org`` respectively. + +Creating bugs +============= + +This interface creates a bug whose summary is given by the email's +post-tag subject. The body of the email must begin with a +pseudo-header containing at least the ``Version`` field. Anything after +the pseudo-header and before a line starting with ``--`` is, if present, +attached as the bug's first comment.:: + + From jdoe@example.com Fri Apr 18 12:00:00 2008 + From: John Doe <jdoe@example.com> + Date: Fri, 18 Apr 2008 12:00:00 +0000 + Content-Type: text/plain; charset=UTF-8 + Content-Transfer-Encoding: 8bit + Subject: [be-bug:submit] Need tests for the email interface. + + Version: XYZ + Severity: minor + + Someone should write up a series of test emails to send into + be-handle-mail so we can test changes quickly without having to + use procmail. + + -- + Goofy tagline not included. + +Available pseudo-headers are ``Version``, ``Reporter``, ``Assign``, +``Depend``, ``Severity``, ``Status``, ``Tag``, and ``Target``. + +Commenting on bugs +================== + +This interface appends a comment to the bug specified in the subject +tag. The the first non-multipart body is attached with the +appropriate content-type. In the case of ``text/plain`` contents, +anything following a line starting with ``--`` is stripped.:: + + From jdoe@example.com Fri Apr 18 12:00:00 2008 + From: John Doe <jdoe@example.com> + Date: Fri, 18 Apr 2008 12:00:00 +0000 + Content-Type: text/plain; charset=UTF-8 + Content-Transfer-Encoding: 8bit + Subject: [be-bug:XYZ] Isolated problem in baz() + + Finally tracked it down to the bar() call. Some sort of + string<->unicode conversion problem. Solution ideas? + + -- + Goofy tagline not included. + +Controlling bugs +================ + +This interface consists of a list of allowed be commands, with one +command per line. Blank lines and lines beginning with ``#`` are +ignored, as well anything following a line starting with ``--``. All +the listed commands are executed in order and their output returned. +The commands are split into arguments with the POSIX-compliant +shlex.split().:: + + From jdoe@example.com Fri Apr 18 12:00:00 2008 + From: John Doe <jdoe@example.com> + Date: Fri, 18 Apr 2008 12:00:00 +0000 + Content-Type: text/plain; charset=UTF-8 + Content-Transfer-Encoding: 8bit + Subject: [be-bug] I'll handle XYZ by release 1.2.3 + + assign XYZ "John Doe <jdoe@example.com>" + status XYZ assigned + severity XYZ critical + target XYZ 1.2.3 + + -- + Goofy tagline ignored. + +Example emails +============== + +Take a look at ``interfaces/email/interactive/examples`` for some +more examples. + +Procmail rules +============== + +The file ``_procmailrc`` as it stands is fairly appropriate for as a +dedicated user's ``~/.procmailrc``. It forwards matching mail to +``be-handle-mail``, which should be installed somewhere in the user's +path. All non-matching mail is dumped into ``/dev/null``. Everything +procmail does will be logged to ``~/be-mail/procmail.log``. + +If you're piggybacking the interface on top of an existing account, +you probably only need to add the ``be-handle-mail`` stanza to your +existing ``~/.procmailrc``, since you will still want to receive +non-bug emails. + +Note that you will probably have to add a:: + + --repo /path/to/served/repository + +option to the ``be-handle-mail`` invocation so it knows what repository to +serve. + +Multiple repositories may be served by the same email address by adding +multiple ``be-handle-mail`` stanzas, each matching a different tag, for +example the ``[be-bug`` portion of the stanza could be ``[projectX-bug``, +``[projectY-bug``, etc. If you change the base tag, be sure to add a:: + + --tag-base "projectX-bug" + +or equivalent to your ``be-handle-mail`` invocation. + +Testing +======= + +Send test emails in to ``be-handle-mail`` with something like:: + + cat examples/blank | ./be-handle-mail -o -l - -a diff --git a/interfaces/email/interactive/_procmailrc b/interfaces/email/interactive/_procmailrc new file mode 100644 index 0000000..d42c0cf --- /dev/null +++ b/interfaces/email/interactive/_procmailrc @@ -0,0 +1,22 @@ +# .procmailrc +# +# see man procmail, procmailrc, and procmailex +# +# If you already have a ~/.procmailrc file, you probably only need to +# insert the bug-email grabbing stanza in your ~/.procmailrc. +# +# This file is released to the Public Domain. + +MAILDIR=$HOME/be-mail +LOGFILE=$MAILDIR/procmail.log + +# Grab all incoming bug emails (but not replies). This rule eats +# matching emails (i.e. no further procmail processing). +:0 +* ^Subject: \[be-bug +* !^Subject:.*\[be-bug].*Re: +| be-handle-mail + +# Drop everything else +:0 +/dev/null diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail new file mode 100755 index 0000000..c8343fc --- /dev/null +++ b/interfaces/email/interactive/be-handle-mail @@ -0,0 +1,909 @@ +#!/usr/bin/env python +# +# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Provide and email interface to the distributed bugtracker Bugs +Everywhere. Recieves incoming email via procmail. Provides an +interface similar to the Debian Bug Tracker. There are currently +three distinct email types: submits, comments, and controls. The +email types are differentiated by tags in the email subject. See +SUBJECT_TAG* for the current values. + +Submit emails create a bug (and optionally add some intitial +comments). The post-tag subject is used as the bug summary, and the +email body is parsed for a pseudo-header. Any text after the +psuedo-header but before a possible line starting with BREAK is added +as the initial bug comment. + +Comment emails add comments to a bug. The first non-multipart portion +of the email is used as the comment body. If that portion has a +"text/plain" type, any text after and including a possible line +starting with BREAK is stripped to avoid lots of taglines cluttering +up the repository. + +Control emails preform any allowed BE commands. The first +non-multipart portion of the email is used as the comment body. If +that portion has a "text/plain" type, any text after and including a +possible line starting with BREAK is stripped. Each pre-BREAK line of +the portion should be a valid BE command, with the initial "be" +omitted, e.g. "be status XYZ fixed" --> "status XYZ fixed". + +Any changes made to the repository are commited after the email is +executed, with the email's post-tag subject as the commit message. +""" + +import codecs +import StringIO as StringIO +import email +from email.mime.multipart import MIMEMultipart +import email.utils +import os +import os.path +import re +import shlex +import sys +import time +import traceback +import types +import doctest +import unittest + +import libbe.bugdir +import libbe.bug +import libbe.comment +import libbe.diff +import libbe.command +import libbe.command.subscribe as subscribe +import libbe.storage +import libbe.ui.command_line +import libbe.util.encoding +import libbe.util.utility +import send_pgp_mime + +THIS_SERVER = u'thor.physics.drexel.edu' +THIS_ADDRESS = u'BE Bugs <wking@thor.physics.drexel.edu>' +UI = None +_THIS_DIR = os.path.abspath(os.path.dirname(__file__)) +LOGPATH = os.path.join(_THIS_DIR, u'be-handle-mail.log') +LOGFILE = None + +# Tag strings generated by generate_global_tags() +SUBJECT_TAG_BASE = u'be-bug' +SUBJECT_TAG_RESPONSE = None +SUBJECT_TAG_START = None +SUBJECT_TAG_NEW = None +SUBJECT_TAG_COMMENT = None +SUBJECT_TAG_CONTROL = None + +BREAK = u'--' +NEW_REQUIRED_PSEUDOHEADERS = [u'Version'] +NEW_OPTIONAL_PSEUDOHEADERS = [u'Reporter', u'Assign', u'Depend', u'Severity', + u'Status', u'Tag', u'Target', + u'Confirm', u'Subscribe'] +CONTROL_COMMENT = u'#' +ALLOWED_COMMANDS = [u'assign', u'comment', u'commit', u'depend', u'diff', + u'due', u'help', u'list', u'merge', u'new', u'severity', + u'show', u'status', u'subscribe', u'tag', u'target'] + +AUTOCOMMIT = True + +ENCODING = u'utf-8' +libbe.util.encoding.ENCODING = ENCODING # force default encoding + +class InvalidEmail (ValueError): + def __init__(self, msg, message): + ValueError.__init__(self, message) + self.msg = msg + def response(self): + header = self.msg.response_header + body = [u'Error processing email:\n', + self.response_body(), u''] + response_generator = \ + send_pgp_mime.PGPMimeMessageFactory(u'\n'.join(body)) + response = MIMEMultipart() + response.attach(response_generator.plain()) + response.attach(self.msg.msg) + ret = send_pgp_mime.attach_root(header, response) + return ret + def response_body(self): + err_text = [unicode(self)] + return u'\n'.join(err_text) + +class InvalidSubject (InvalidEmail): + def __init__(self, msg, message=None): + if message == None: + message = u'Invalid subject' + InvalidEmail.__init__(self, msg, message) + def response_body(self): + err_text = u'\n'.join([unicode(self), u'', + u'full subject was:', + self.msg.subject()]) + return err_text + +class InvalidPseudoHeader (InvalidEmail): + def response_body(self): + err_text = [u'Invalid pseudo-header:\n', + unicode(self)] + return u'\n'.join(err_text) + +class InvalidCommand (InvalidEmail): + def __init__(self, msg, command, message=None): + bigmessage = u'Invalid execution command "%s"' % command + if message != None: + bigmessage += u'\n%s' % message + InvalidEmail.__init__(self, msg, bigmessage) + self.command = command + +class InvalidOption (InvalidCommand): + def __init__(self, msg, option, message=None): + bigmessage = u'Invalid option "%s"' % (option) + if message != None: + bigmessage += u'\n%s' % message + InvalidCommand.__init__(self, msg, info, command, bigmessage) + self.option = option + +class NotificationFailed (Exception): + def __init__(self, msg): + bigmessage = 'Notification failed: %s' % msg + Exception.__init__(self, bigmessage) + self.short_msg = msg + +class ID (object): + """ + Sometimes you want to reference the output of a command that + hasn't been executed yet. ID is there for situations like + > a = Command(msg, "new", ["create a bug"]) + > b = Command(msg, "comment", [ID(a), "and comment on it"]) + """ + def __init__(self, command): + self.command = command + def extract_id(self): + if hasattr(self, 'cached_id'): + return self._cached_id + assert self.command.ret == 0, self.command.ret + if self.command.command.name == u'new': + regexp = re.compile(u'Created bug with ID (.*)') + else: + raise NotImplementedError, self.command.command + match = regexp.match(self.command.stdout) + assert len(match.groups()) == 1, str(match.groups()) + self._cached_id = match.group(1) + return self._cached_id + def __str__(self): + if self.command.ret != 0: + return '<id for %s>' % repr(self.command) + return '<id %s>' % self.extract_id() + +class Command (object): + """ + A libbe.command.Command handler. + + Initialize with + Command(msg, command, args=None, stdin=None) + where + msg: the Message instance prompting this command + command: name of becommand to execute, e.g. "new" + args: list of arguments to pass to the command + stdin: if non-null, a string to pipe into the command's stdin + """ + def __init__(self, msg, command, args=None, stdin=None): + self.msg = msg + if args == None: + self.args = [] + else: + self.args = args + self.command = libbe.command.get_command_class(command_name=command)() + self.command._setup_io = lambda i_enc,o_enc : None + self.ret = None + self.stdin = stdin + self.stdout = None + def __str__(self): + return '<command: %s %s>' % (self.command, ' '.join([str(s) for s in self.args])) + def normalize_args(self): + """ + Expand any ID placeholders in self.args. + """ + for i,arg in enumerate(self.args): + if isinstance(arg, ID): + self.args[i] = arg.extract_id() + def run(self): + """ + Attempt to execute the command whose info is given in the dictionary + info. Returns the exit code, stdout, and stderr produced by the + command. + """ + if self.command.name in [None, u'']: # don't accept blank commands + raise InvalidCommand(self.msg, self, 'Blank') + elif self.command.name not in ALLOWED_COMMANDS: + raise InvalidCommand(self.msg, self, 'Not allowed') + assert self.ret == None, u'running %s twice!' % unicode(self) + self.normalize_args() + UI.io.set_stdin(self.stdin) + self.ret = libbe.ui.command_line.dispatch(UI, self.command, self.args) + self.stdout = UI.io.get_stdout() + return (self.ret, self.stdout) + def response_msg(self): + if self.ret == None: self.ret = -1 + response_body = [u'Results of running: (exit code %d)' % self.ret, + u' %s %s' % (self.command.name,u' '.join(self.args))] + if self.stdout != None and len(self.stdout) > 0: + response_body.extend([u'', u'output:', u'', self.stdout]) + response_body.append(u'') # trailing endline + response_generator = \ + send_pgp_mime.PGPMimeMessageFactory(u'\n'.join(response_body)) + return response_generator.plain() + +class DiffTree (libbe.diff.DiffTree): + """ + In order to avoid tons of tiny MIMEText attachments, bug-level + nodes set .add_child_text=True (in .join()), which is propogated + on to their descendents. Instead of creating their own + attachement, each of these descendents appends his data_part to + the end of the bug-level MIMEText attachment. + + For the example tree in the libbe.diff.Diff unittests: + bugdir + bugdir/settings + bugdir/bugs + bugdir/bugs/new + bugdir/bugs/new/c <- sets .add_child_text + bugdir/bugs/rem + bugdir/bugs/rem/b <- sets .add_child_text + bugdir/bugs/mod + bugdir/bugs/mod/a <- sets .add_child_text + bugdir/bugs/mod/a/settings + bugdir/bugs/mod/a/comments + bugdir/bugs/mod/a/comments/new + bugdir/bugs/mod/a/comments/new/acom + bugdir/bugs/mod/a/comments/rem + bugdir/bugs/mod/a/comments/mod + """ + def report_or_none(self): + report = self.report() + if report == None: + return None + payload = report.get_payload() + if payload == None or len(payload) == 0: + return None + return report + def report_string(self): + report = self.report_or_none() + if report == None: + return 'No changes' + else: + return send_pgp_mime.flatten(report, to_unicode=True) + def make_root(self): + return MIMEMultipart() + def join(self, root, parent, data_part): + if hasattr(parent, 'attach_child_text'): + self.attach_child_text = True + if data_part != None: + send_pgp_mime.append_text(parent.data_mime_part, u'\n\n%s' % (data_part)) + self.data_mime_part = parent.data_mime_part + else: + self.data_mime_part = None + if data_part != None: + self.data_mime_part = send_pgp_mime.encodedMIMEText(data_part) + if parent != None and parent.name in [u'new', u'rem', u'mod']: + self.attach_child_text = True + if data_part == None: # make blank data_mime_part for children's appends + self.data_mime_part = send_pgp_mime.encodedMIMEText(u'') + if self.data_mime_part != None: + self.data_mime_part[u'Content-Description'] = self.name + root.attach(self.data_mime_part) + def data_part(self, depth, indent=False): + return libbe.diff.DiffTree.data_part(self, depth, indent=indent) + +class Diff (libbe.diff.Diff): + def bug_add_string(self, bug): + return bug.string(show_comments=True) + def _comment_summary_string(self, comment): + return comment.string() + def comment_add_string(self, comment): + return self._comment_summary_string(comment) + def comment_rem_string(self, comment): + return self._comment_summary_string(comment) + +class Message (object): + def __init__(self, email_text=None, disable_parsing=False): + if disable_parsing == False: + self.text = email_text + p=email.Parser.Parser() + self.msg=p.parsestr(self.text) + if LOGFILE != None: + LOGFILE.write(u'handling %s\n' % self.author_addr()) + LOGFILE.write(u'\n%s\n\n' % self.text) + self.confirm = True # enable/disable confirmation email + def _yes_no(self, boolean): + if boolean == True: + return 'yes' + return 'no' + def author_tuple(self): + """ + Extract and normalize the sender's email address. Returns a + (name, email) tuple. + """ + if not hasattr(self, 'author_tuple_cache'): + self._author_tuple_cache = \ + send_pgp_mime.source_email(self.msg, return_realname=True) + return self._author_tuple_cache + def author_addr(self): + return email.utils.formataddr(self.author_tuple()) + def author_name(self): + return self.author_tuple()[0] + def author_email(self): + return self.author_tuple()[1] + def default_msg_attribute_access(self, attr_name, default=None): + if attr_name in self.msg: + return self.msg[attr_name] + return default + def message_id(self, default=None): + return self.default_msg_attribute_access('message-id', default=default) + def subject(self): + if 'subject' not in self.msg: + raise InvalidSubject(self, u'Email must contain a subject') + return self.msg['subject'] + def _split_subject(self): + """ + Returns (tag, subject), with missing values replaced by None. + """ + if hasattr(self, '_split_subject_cache'): + return self._split_subject_cache + args = self.subject().split(u']',1) + if len(args) < 1: + self._split_subject_cache = (None, None) + elif len(args) < 2: + self._split_subject_cache = (args[0]+u']', None) + else: + self._split_subject_cache = (args[0]+u']', args[1].strip()) + return self._split_subject_cache + def _subject_tag_type(self): + """ + Parse subject tag, return (type, value), where type is one of + None, "new", "comment", or "control"; and value is None except + in the case of "comment", in which case it's the bug + ID/shortname. + """ + tag,subject = self._split_subject() + type = None + value = None + if tag == SUBJECT_TAG_NEW: + type = u'new' + elif tag == SUBJECT_TAG_CONTROL: + type = u'control' + else: + match = SUBJECT_TAG_COMMENT.match(tag) + if len(match.groups()) == 1: + type = u'comment' + value = match.group(1) + return (type, value) + def validate_subject(self): + """ + Validate the subject line. + """ + tag,subject = self._split_subject() + if not tag.startswith(SUBJECT_TAG_START): + raise InvalidSubject( + self, u'Subject must start with "%s"' % SUBJECT_TAG_START) + tag_type,value = self._subject_tag_type() + if tag_type == None: + raise InvalidSubject(self, u'Invalid tag "%s"' % tag) + elif tag_type == u'new' and len(subject) == 0: + raise InvalidSubject(self, u'Cannot create a bug with blank title') + elif tag_type == u'comment' and len(value) == 0: + raise InvalidSubject(self, u'Must specify a bug ID to comment') + def _get_bodies_and_mime_types(self): + """ + Traverse the email message returning (body, mime_type) for + each non-mulitpart portion of the message. + """ + msg_charset = self.msg.get_content_charset(ENCODING).lower() + for part in self.msg.walk(): + if part.is_multipart(): + continue + body,mime_type=(part.get_payload(decode=True),part.get_content_type()) + charset = part.get_content_charset(msg_charset).lower() + if mime_type.startswith('text/'): + body = unicode(body, charset) # convert text types to unicode + yield (body, mime_type) + def _parse_body_pseudoheaders(self, body, required, optional, + dictionary=None): + """ + Grab any pseudo-headers from the beginning of body. Raise + InvalidPseudoHeader on errors. Returns the body text after + the pseudo-header and a dictionary of set options. If you + like, you can initialize the dictionary with some defaults + and pass your initialized dict in as dictionary. + """ + if dictionary == None: + dictionary = {} + body_lines = body.splitlines() + all = required+optional + for i,line in enumerate(body_lines): + line = line.strip() + if len(line) == 0: + break + if ':' not in line: + raise InvalidPseudoheader(self, line) + key,value = line.split(':', 1) + value = value.strip() + if key not in all: + raise InvalidPseudoHeader(self, key) + if len(value) == 0: + raise InvalidEmail( + self, u'Blank value for: %s' % key) + dictionary[key] = value + missing = [] + for key in required: + if key not in dictionary: + missing.append(key) + if len(missing) > 0: + raise InvalidPseudoHeader(self, + u'Missing required pseudo-headers:\n%s' + % u', '.join(missing)) + remaining_body = u'\n'.join(body_lines[i:]).strip() + return (remaining_body, dictionary) + def _strip_footer(self, body): + body_lines = body.splitlines() + for i,line in enumerate(body_lines): + if line.startswith(BREAK): + break + i += 1 # increment past the current valid line. + return u'\n'.join(body_lines[:i]).strip() + def parse(self): + """ + Parse the commands given in the email. Raises assorted + subclasses of InvalidEmail in the case of invalid messages, + otherwise returns a list of suggested commands to run. + """ + self.validate_subject() + tag_type,value = self._subject_tag_type() + if tag_type == u'new': + commands = self.parse_new() + elif tag_type == u'comment': + commands = self.parse_comment(value) + elif tag_type == u'control': + commands = self.parse_control() + else: + raise Exception, u'Unrecognized tag type "%s"' % tag_type + return commands + def parse_new(self): + command = u'new' + tag,subject = self._split_subject() + summary = subject + options = {u'Reporter': self.author_addr(), + u'Confirm': self._yes_no(self.confirm), + u'Subscribe': 'no', + } + body,mime_type = list(self._get_bodies_and_mime_types())[0] + comment_body,options = \ + self._parse_body_pseudoheaders(body, + NEW_REQUIRED_PSEUDOHEADERS, + NEW_OPTIONAL_PSEUDOHEADERS, + options) + if options[u'Confirm'].lower() == 'no': + self.confirm = False + if options[u'Subscribe'].lower() == 'yes' and self.confirm == True: + # respond with the subscription format rather than the + # normal command-output format, because the subscription + # format is more user-friendly. + self.confirm = False + args = [u'--reporter', options[u'Reporter']] + args.append(summary) + commands = [Command(self, command, args)] + id = ID(commands[0]) + comment_body = self._strip_footer(comment_body) + if len(comment_body) > 0: + command = u'comment' + comment = u'Version: %s\n\n'%options[u'Version'] + comment_body + args = [u'--author', self.author_addr(), + u'--alt-id', self.message_id(), + u'--content-type', mime_type] + args.append(id) + args.append(u'-') + commands.append(Command(self, u'comment', args, stdin=comment)) + for key,value in options.items(): + if key in [u'Version', u'Reporter', u'Confirm']: + continue # we've already handled these options + command = key.lower() + if key in [u'Depend', u'Tag', u'Target', u'Subscribe']: + args = [id, value] + else: + args = [value, id] + if key == u'Subscribe': + if value.lower() != 'yes': + continue + args = ['--subscriber', self.author_addr(), id] + commands.append(Command(self, command, args)) + return commands + def parse_comment(self, bug_uuid): + command = u'comment' + bug_id = bug_uuid + author = self.author_addr() + alt_id = self.message_id() + body,mime_type = list(self._get_bodies_and_mime_types())[0] + if mime_type == 'text/plain': + body = self._strip_footer(body) + content_type = mime_type + args = [u'--author', author] + if alt_id != None: + args.extend([u'--alt-id', alt_id]) + args.extend([u'--content-type', content_type, bug_id, u'-']) + commands = [Command(self, command, args, stdin=body)] + return commands + def parse_control(self): + body,mime_type = list(self._get_bodies_and_mime_types())[0] + commands = [] + for line in body.splitlines(): + line = line.strip() + if line.startswith(CONTROL_COMMENT) or len(line) == 0: + continue + if line.startswith(BREAK): + break + if type(line) == types.UnicodeType: + # work around http://bugs.python.org/issue1170 + line = line.encode('unicode escape') + fields = shlex.split(line) + if type(line) == types.UnicodeType: + # work around http://bugs.python.org/issue1170 + for field in fields: + field = unicode(field, 'unicode escape') + command,args = (fields[0], fields[1:]) + commands.append(Command(self, command, args)) + if len(commands) == 0: + raise InvalidEmail(self, u'No commands in control email.') + return commands + def run(self, repo='.'): + self._begin_response() + commands = self.parse() + try: + for i,command in enumerate(commands): + command.run() + self._add_response(command.response_msg()) + finally: + if AUTOCOMMIT == True: + tag,subject = self._split_subject() + self.commit_command = Command(self, 'commit', [subject]) + self.commit_command.run() + if LOGFILE != None: + LOGFILE.write(u'Autocommit:\n%s\n\n' % + send_pgp_mime.flatten(self.commit_command.response_msg(), + to_unicode=True)) + def _begin_response(self): + tag,subject = self._split_subject() + response_header = [u'From: %s' % THIS_ADDRESS, + u'To: %s' % self.author_addr(), + u'Date: %s' % libbe.util.utility.time_to_str(time.time()), + u'Subject: %s Re: %s'%(SUBJECT_TAG_RESPONSE,subject) + ] + if self.message_id() != None: + response_header.append(u'In-reply-to: %s' % self.message_id()) + self.response_header = \ + send_pgp_mime.header_from_text(text=u'\n'.join(response_header)) + self._response_messages = [] + def _add_response(self, response_message): + self._response_messages.append(response_message) + def response_email(self): + assert len(self._response_messages) > 0 + if len(self._response_messages) == 1: + response_body = self._response_messages[0] + else: + response_body = MIMEMultipart() + for message in self._response_messages: + response_body.attach(message) + return send_pgp_mime.attach_root(self.response_header, response_body) + def subscriber_emails(self, previous_revision=None): + if previous_revision == None: + if AUTOCOMMIT != True: # no way to tell what's changed + raise NotificationFailed('Autocommit dissabled') + if len(self._response_messages) == 0: + raise NotificationFailed('Initial email failed.') + if self.commit_command.ret != 0: + # commit failed. Error already logged. + raise NotificationFailed('Commit failed') + + bd = UI.storage_callbacks.get_bugdir() + writeable = bd.storage.writeable + bd.storage.writeable = False + if bd.storage.versioned == False: # no way to tell what's changed + bd.storage.writeable = writeable + raise NotificationFailed('Not versioned') + + bd.load_all_bugs() + subscribers = subscribe.get_bugdir_subscribers(bd, THIS_SERVER) + if len(subscribers) == 0: + bd.storage.writeable = writeable + return [] + for subscriber,subscriptions in subscribers.items(): + subscribers[subscriber] = [] + for id,types in subscriptions.items(): + for type in types: + subscribers[subscriber].append( + libbe.diff.Subscription(id,type)) + + before_bd, after_bd = self._get_before_and_after_bugdirs(bd, previous_revision) + diff = Diff(before_bd, after_bd) + diff.full_report(diff_tree=DiffTree) + header = self._subscriber_header(bd, previous_revision) + + emails = [] + for subscriber,subscriptions in subscribers.items(): + header.replace_header('to', subscriber) + report = diff.report_tree(subscriptions, diff_tree=DiffTree) + root = report.report_or_none() + if root != None: + emails.append(send_pgp_mime.attach_root(header, root)) + if LOGFILE != None: + LOGFILE.write(u'Preparing to notify %s of changes\n' % subscriber) + bd.storage.writeable = writeable + return emails + def _get_before_and_after_bugdirs(self, bd, previous_revision=None): + if previous_revision == None: + commit_msg = self.commit_command.stdout + assert commit_msg.startswith('Committed '), commit_msg + after_revision = commit_msg[len('Committed '):] + before_revision = bd.storage.revision_id(-2) + else: + before_revision = previous_revision + if before_revision == None: + # this commit was the initial commit + before_bd = libbe.bugdir.BugDir(from_disk=False, + manipulate_encodings=False) + else: + before_bd = libbe.bugdir.RevisionedBugDir(bd, before_revision) + #after_bd = bd.duplicate_bugdir(after_revision) + after_bd = bd # assume no changes since commit a few cycles ago + return (before_bd, after_bd) + def _subscriber_header(self, bd, previous_revision=None): + root_dir = os.path.basename(bd.storage.repo) + if previous_revision == None: + subject = 'Changes to %s on %s by %s' \ + % (root_dir, THIS_SERVER, self.author_addr()) + else: + subject = 'Changes to %s on %s since revision %s' \ + % (root_dir, THIS_SERVER, previous_revision) + header = [u'From: %s' % THIS_ADDRESS, + u'To: %s' % u'DUMMY-AUTHOR', + u'Date: %s' % libbe.util.utility.time_to_str(time.time()), + u'Subject: %s Re: %s' % (SUBJECT_TAG_RESPONSE, subject) + ] + return send_pgp_mime.header_from_text(text=u'\n'.join(header)) + +def generate_global_tags(tag_base=u'be-bug'): + """ + Generate a series of tags from a base tag string. + """ + global SUBJECT_TAG_BASE, SUBJECT_TAG_START, SUBJECT_TAG_RESPONSE, \ + SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL + SUBJECT_TAG_BASE = tag_base + SUBJECT_TAG_START = u'[%s' % tag_base + SUBJECT_TAG_RESPONSE = u'[%s]' % tag_base + SUBJECT_TAG_NEW = u'[%s:submit]' % tag_base + SUBJECT_TAG_COMMENT = re.compile(u'\[%s:([\-0-9a-z/]*)]' % tag_base) + SUBJECT_TAG_CONTROL = SUBJECT_TAG_RESPONSE + +def open_logfile(logpath=None): + """ + If logpath=None, default to global LOGPATH. + Special logpath strings: + "-" set LOGFILE to sys.stderr + "none" disable logging + Relative logpaths are expanded relative to _THIS_DIR + """ + global LOGPATH, LOGFILE + if logpath != None: + if logpath == u'-': + LOGPATH = u'stderr' + LOGFILE = sys.stderr + elif logpath == u'none': + LOGPATH = u'none' + LOGFILE = None + elif os.path.isabs(logpath): + LOGPATH = logpath + else: + LOGPATH = os.path.join(_THIS_DIR, logpath) + if LOGFILE == None and LOGPATH != u'none': + LOGFILE = codecs.open(LOGPATH, u'a+', + libbe.util.encoding.get_filesystem_encoding()) + +def close_logfile(): + if LOGFILE != None and LOGPATH not in [u'stderr', u'none']: + LOGFILE.close() + +def test(): + result = unittest.TextTestRunner(verbosity=2).run(suite) + num_errors = len(result.errors) + num_failures = len(result.failures) + num_bad = num_errors + num_failures + return num_bad + +def main(args): + from optparse import OptionParser + global AUTOCOMMIT, UI + + usage='be-handle-mail [options]\n\n%s' % (__doc__) + parser = OptionParser(usage=usage) + parser.add_option('-r', '--repo', dest='repo', default=_THIS_DIR, + metavar='REPO', + help='Select the BE repository to serve (%default).') + parser.add_option('-t', '--tag-base', dest='tag_base', + default=SUBJECT_TAG_BASE, metavar='TAG', + help='Set the subject tag base (%default).') + parser.add_option('-o', '--output', dest='output', action='store_true', + help="Don't mail the generated message, print it to stdout instead. Useful for testing be-handle-mail functionality without the whole mail transfer agent and procmail setup.") + parser.add_option('-l', '--logfile', dest='logfile', metavar='LOGFILE', + help='Set the logfile to LOGFILE. Relative paths are relative to the location of this be-handle-mail file (%s). The special value of "-" directs the log output to stderr, and "none" disables logging.' % _THIS_DIR) + parser.add_option('-a', '--disable-autocommit', dest='autocommit', + default=True, action='store_false', + help='Disable the autocommit after parsing the email.') + parser.add_option('-s', '--disable-subscribers', dest='subscribers', + default=True, action='store_false', + help='Disable subscriber notification emails.') + parser.add_option('--notify-since', dest='notify_since', metavar='REVISION', + help='Notify subscribers of all changes since REVISION. When this option is set, no input email parsing is done.') + parser.add_option('--test', dest='test', action='store_true', + help='Run internal unit-tests and exit.') + + pargs = args + options,args = parser.parse_args(args[1:]) + + if options.test == True: + num_bad = test() + if num_bad > 126: + num_bad = 1 + sys.exit(num_bad) + + AUTOCOMMIT = options.autocommit + + if options.notify_since == None: + msg_text = sys.stdin.read() + + open_logfile(options.logfile) + generate_global_tags(options.tag_base) + + io = libbe.command.StringInputOutput() + UI = libbe.command.UserInterface(io, location=options.repo) + + if options.notify_since != None: + if options.subscribers == True: + if LOGFILE != None: + LOGFILE.write(u'Checking for subscribers to notify since revision %s\n' + % options.notify_since) + try: + m = Message(disable_parsing=True) + emails = m.subscriber_emails(options.notify_since) + except NotificationFailed, e: + if LOGFILE != None: + LOGFILE.write(unicode(e) + u'\n') + else: + for msg in emails: + if options.output == True: + print send_pgp_mime.flatten(msg, to_unicode=True) + else: + send_pgp_mime.mail(msg, send_pgp_mime.sendmail) + close_logfile() + UI.cleanup() + sys.exit(0) + + if len(msg_text.strip()) == 0: # blank email!? + if LOGFILE != None: + LOGFILE.write(u'Blank email!\n') + close_logfile() + UI.cleanup() + sys.exit(1) + try: + m = Message(msg_text) + m.run() + except InvalidEmail, e: + response = e.response() + except Exception, e: + if LOGFILE != None: + LOGFILE.write(u'Uncaught exception:\n%s\n' % (e,)) + traceback.print_tb(sys.exc_traceback, file=LOGFILE) + close_logfile() + m.commit_command.cleanup() + UI.cleanup() + sys.exit(1) + else: + response = m.response_email() + if options.output == True: + print send_pgp_mime.flatten(response, to_unicode=True) + elif m.confirm == True: + if LOGFILE != None: + LOGFILE.write(u'Sending response to %s\n' % m.author_addr()) + LOGFILE.write(u'\n%s\n\n' % send_pgp_mime.flatten(response, + to_unicode=True)) + send_pgp_mime.mail(response, send_pgp_mime.sendmail) + else: + if LOGFILE != None: + LOGFILE.write(u'Response declined by %s\n' % m.author_addr()) + if options.subscribers == True: + if LOGFILE != None: + LOGFILE.write(u'Checking for subscribers\n') + try: + emails = m.subscriber_emails() + except NotificationFailed, e: + if LOGFILE != None: + LOGFILE.write(unicode(e) + u'\n') + else: + for msg in emails: + if options.output == True: + print send_pgp_mime.flatten(msg, to_unicode=True) + else: + send_pgp_mime.mail(msg, send_pgp_mime.sendmail) + + close_logfile() + m.commit_command.cleanup() + UI.cleanup() + +class GenerateGlobalTagsTestCase (unittest.TestCase): + def setUp(self): + super(GenerateGlobalTagsTestCase, self).setUp() + self.save_global_tags() + def tearDown(self): + self.restore_global_tags() + super(GenerateGlobalTagsTestCase, self).tearDown() + def save_global_tags(self): + self.saved_globals = [SUBJECT_TAG_BASE, SUBJECT_TAG_START, + SUBJECT_TAG_RESPONSE, SUBJECT_TAG_NEW, + SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL] + def restore_global_tags(self): + global SUBJECT_TAG_BASE, SUBJECT_TAG_START, SUBJECT_TAG_RESPONSE, \ + SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL + SUBJECT_TAG_BASE, SUBJECT_TAG_START, SUBJECT_TAG_RESPONSE, \ + SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL = \ + self.saved_globals + def test_restore_global_tags(self): + "Test global tag restoration by teardown function." + global SUBJECT_TAG_BASE + self.failUnlessEqual(SUBJECT_TAG_BASE, u'be-bug') + SUBJECT_TAG_BASE = 'projectX-bug' + self.failUnlessEqual(SUBJECT_TAG_BASE, u'projectX-bug') + self.restore_global_tags() + self.failUnlessEqual(SUBJECT_TAG_BASE, u'be-bug') + def test_subject_tag_base(self): + "Should set SUBJECT_TAG_BASE global correctly" + generate_global_tags(u'projectX-bug') + self.failUnlessEqual(SUBJECT_TAG_BASE, u'projectX-bug') + def test_subject_tag_start(self): + "Should set SUBJECT_TAG_START global correctly" + generate_global_tags(u'projectX-bug') + self.failUnlessEqual(SUBJECT_TAG_START, u'[projectX-bug') + def test_subject_tag_response(self): + "Should set SUBJECT_TAG_RESPONSE global correctly" + generate_global_tags(u'projectX-bug') + self.failUnlessEqual(SUBJECT_TAG_RESPONSE, u'[projectX-bug]') + def test_subject_tag_new(self): + "Should set SUBJECT_TAG_NEW global correctly" + generate_global_tags(u'projectX-bug') + self.failUnlessEqual(SUBJECT_TAG_NEW, u'[projectX-bug:submit]') + def test_subject_tag_control(self): + "Should set SUBJECT_TAG_CONTROL global correctly" + generate_global_tags(u'projectX-bug') + self.failUnlessEqual(SUBJECT_TAG_CONTROL, u'[projectX-bug]') + def test_subject_tag_comment(self): + "Should set SUBJECT_TAG_COMMENT global correctly" + generate_global_tags(u'projectX-bug') + m = SUBJECT_TAG_COMMENT.match('[projectX-bug:abc/xyz-123]') + self.failUnlessEqual(len(m.groups()), 1) + self.failUnlessEqual(m.group(1), u'abc/xyz-123') + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) + +if __name__ == "__main__": + main(sys.argv) diff --git a/interfaces/email/interactive/examples/blank b/interfaces/email/interactive/examples/blank new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/interfaces/email/interactive/examples/blank diff --git a/interfaces/email/interactive/examples/comment b/interfaces/email/interactive/examples/comment new file mode 100644 index 0000000..f22e4b2 --- /dev/null +++ b/interfaces/email/interactive/examples/comment @@ -0,0 +1,11 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <xyz@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug:a1d] Subject ignored + +We sure do. +-- +Goofy tagline ignored diff --git a/interfaces/email/interactive/examples/email_bugs b/interfaces/email/interactive/examples/email_bugs new file mode 100644 index 0000000..949e1c1 --- /dev/null +++ b/interfaces/email/interactive/examples/email_bugs @@ -0,0 +1,37 @@ +From jdoe@example.com Fri Apr 18 12:00:00 2008 +Content-Type: text/xml; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: quoted-printable +From: jdoe@example.com +To: a@b.com +Date: Fri, 18 Apr 2008 12:00:00 +0000 +Subject: [be-bug:xml] Updates to a, b + +<?xml version="1.0" encoding="utf-8" ?> +<be-xml> + <version> + <tag>1.0.0</tag> + <branch-nick>be</branch-nick> + <revno>446</revno> + <revision-id>wking@drexel.edu-20091119214553-iqyw2cpqluww3zna</revision-id> + </version> + <bug> + <uuid>a</uuid> + <short-name>a</short-name> + <severity>minor</severity> + <status>open</status> + <creator>John Doe <jdoe@example.com></creator> + <created>Thu, 01 Jan 1970 00:00:00 +0000</created> + <summary>Bug A</summary> + </bug> + <bug> + <uuid>b</uuid> + <short-name>b</short-name> + <severity>minor</severity> + <status>closed</status> + <creator>Jane Doe <jdoe@example.com></creator> + <created>Thu, 01 Jan 1970 00:00:00 +0000</created> + <summary>Bug B</summary> + </bug> +</be-xml> + diff --git a/interfaces/email/interactive/examples/failing_multiples b/interfaces/email/interactive/examples/failing_multiples new file mode 100644 index 0000000..cf50211 --- /dev/null +++ b/interfaces/email/interactive/examples/failing_multiples @@ -0,0 +1,16 @@ +From jdoe@example.com Fri Apr 18 12:00:00 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] Commit message... + +new "test bug" +new "test bug 2" +failing-command +new "test bug 3" + +-- +This message fails partway through, but the partial changes should be +recorded in a commit... diff --git a/interfaces/email/interactive/examples/invalid_command b/interfaces/email/interactive/examples/invalid_command new file mode 100644 index 0000000..f2963c7 --- /dev/null +++ b/interfaces/email/interactive/examples/invalid_command @@ -0,0 +1,11 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] + +close +-- +Close is currently disabled for the email interface. diff --git a/interfaces/email/interactive/examples/invalid_subject b/interfaces/email/interactive/examples/invalid_subject new file mode 100644 index 0000000..1e2eb88 --- /dev/null +++ b/interfaces/email/interactive/examples/invalid_subject @@ -0,0 +1,9 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: Spam! + +This should elicit an "invalid subject" response email. diff --git a/interfaces/email/interactive/examples/list b/interfaces/email/interactive/examples/list new file mode 100644 index 0000000..acba424 --- /dev/null +++ b/interfaces/email/interactive/examples/list @@ -0,0 +1,11 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] Subject ignored + +list --status all +-- +Dummy content diff --git a/interfaces/email/interactive/examples/missing_command b/interfaces/email/interactive/examples/missing_command new file mode 100644 index 0000000..bb390fc --- /dev/null +++ b/interfaces/email/interactive/examples/missing_command @@ -0,0 +1,11 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] Subject ignored + +abcde +-- +This should elicit a "invalid command 'abcde'" response email. diff --git a/interfaces/email/interactive/examples/multiple_commands b/interfaces/email/interactive/examples/multiple_commands new file mode 100644 index 0000000..41ef730 --- /dev/null +++ b/interfaces/email/interactive/examples/multiple_commands @@ -0,0 +1,14 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] Subject ignored + +help +list --status=all +list --status=fixed +show --xml 361 +-- +Goofy tagline ignored. diff --git a/interfaces/email/interactive/examples/new b/interfaces/email/interactive/examples/new new file mode 100644 index 0000000..c64db93 --- /dev/null +++ b/interfaces/email/interactive/examples/new @@ -0,0 +1,19 @@ +From jdoe@example.com Fri Apr 18 12:00:00 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug:submit] Need tests for the email interface. + +Version: XYZ +Reporter: Jane Doe +Assign: Dick Tracy +Depend: 00f +Severity: critical +Status: assigned +Tag: topsecret +Target: Law&Order + +-- +Goofy tagline not included, and no comment added. diff --git a/interfaces/email/interactive/examples/new_with_comment b/interfaces/email/interactive/examples/new_with_comment new file mode 100644 index 0000000..1077f0f --- /dev/null +++ b/interfaces/email/interactive/examples/new_with_comment @@ -0,0 +1,13 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug:submit] Need tests for the email interface. + +Version: XYZ + +I think so anyway. +-- +Goofy tagline not included. diff --git a/interfaces/email/interactive/examples/show b/interfaces/email/interactive/examples/show new file mode 100644 index 0000000..c5f8a4d --- /dev/null +++ b/interfaces/email/interactive/examples/show @@ -0,0 +1,11 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] Subject ignored + +show --xml 361 +-- +Can we show a bug? diff --git a/interfaces/email/interactive/examples/unicode b/interfaces/email/interactive/examples/unicode new file mode 100644 index 0000000..f0e8001 --- /dev/null +++ b/interfaces/email/interactive/examples/unicode @@ -0,0 +1,11 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] Subject ignored + +show --xml f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a +-- +Can we handle unicode output? diff --git a/interfaces/email/interactive/libbe b/interfaces/email/interactive/libbe new file mode 120000 index 0000000..7d18612 --- /dev/null +++ b/interfaces/email/interactive/libbe @@ -0,0 +1 @@ +../../../libbe
\ No newline at end of file diff --git a/interfaces/email/interactive/send_pgp_mime.py b/interfaces/email/interactive/send_pgp_mime.py new file mode 100644 index 0000000..517b1f0 --- /dev/null +++ b/interfaces/email/interactive/send_pgp_mime.py @@ -0,0 +1,611 @@ +#!/usr/bin/python +# +# Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Python module and command line tool for sending pgp/mime email. + +Mostly uses subprocess to call gpg and a sendmail-compatible mailer. +If you lack gpg, either don't use the encryption functions or adjust +the pgp_* commands. You may need to adjust the sendmail command to +point to whichever sendmail-compatible mailer you have on your system. +""" + +from cStringIO import StringIO +import os +import re +#import GnuPGInterface # Maybe should use this instead of subprocess +import smtplib +import subprocess +import sys +import tempfile +import types + +try: + from email import Message + from email.mime.text import MIMEText + from email.mime.multipart import MIMEMultipart + from email.mime.application import MIMEApplication + from email.encoders import encode_7or8bit + from email.generator import Generator + from email.parser import Parser + from email.utils import getaddress +except ImportError: + # adjust to old python 2.4 + from email import Message + from email.MIMEText import MIMEText + from email.MIMEMultipart import MIMEMultipart + from email.MIMENonMultipart import MIMENonMultipart + from email.Encoders import encode_7or8bit + from email.Generator import Generator + from email.Parser import Parser + from email.Utils import getaddresses + + getaddress = getaddresses + class MIMEApplication (MIMENonMultipart): + def __init__(self, _data, _subtype, _encoder, **params): + MIMENonMultipart.__init__(self, 'application', _subtype, **params) + self.set_payload(_data) + _encoder(self) + +usage="""usage: %prog [options] + +Scriptable PGP MIME email using gpg. + +You can use gpg-agent for passphrase caching if your key requires a +passphrase (it better!). Example usage would be to install gpg-agent, +and then run + export GPG_TTY=`tty` + eval $(gpg-agent --daemon) +in your shell before invoking this script. See gpg-agent(1) for more +details. Alternatively, you can send your passphrase in on stdin + echo 'passphrase' | %prog [options] +or use the --passphrase-file option + %prog [options] --passphrase-file FILE [more options] +Both of these alternatives are much less secure than gpg-agent. You +have been warned. +""" + +verboseInvoke = False +PGP_SIGN_AS = None +PASSPHRASE = None + +# The following commands are adapted from my .mutt/pgp configuration +# +# Printf-like sequences: +# %a The value of PGP_SIGN_AS. +# %f Expands to the name of a file with text to be signed/encrypted. +# %p Expands to the passphrase argument. +# %R A string with some number (0 on up) of pgp_reciepient_arg +# strings. +# %r One key ID (e.g. recipient email address) to build a +# pgp_reciepient_arg string. +# +# The above sequences can be used to optionally print a string if +# their length is nonzero. For example, you may only want to pass the +# -u/--local-user argument to gpg if PGP_SIGN_AS is defined. To +# optionally print a string based upon one of the above sequences, the +# following construct is used +# %?<sequence_char>?<optional_string>? +# where sequence_char is a character from the table above, and +# optional_string is the string you would like printed if status_char +# is nonzero. optional_string may contain other sequence as well as +# normal text, but it may not contain any question marks. +# +# see http://codesorcery.net/old/mutt/mutt-gnupg-howto +# http://www.mutt.org/doc/manual/manual-6.html#pgp_autosign +# http://tldp.org/HOWTO/Mutt-GnuPG-PGP-HOWTO-8.html +# for more details + +pgp_recipient_arg='-r "%r"' +pgp_stdin_passphrase_arg='--passphrase-fd 0' +pgp_sign_command='/usr/bin/gpg --no-verbose --quiet --batch %p --output - --detach-sign --armor --textmode %?a?-u "%a"? %f' +pgp_encrypt_only_command='/usr/bin/gpg --no-verbose --quiet --batch --output - --encrypt --armor --textmode --always-trust --encrypt-to "%a" %R -- %f' +pgp_encrypt_sign_command='/usr/bin/gpg --no-verbose --quiet --batch %p --output - --encrypt --sign %?a?-u "%a"? --armor --textmode --always-trust --encrypt-to "%a" %R -- %f' +sendmail='/usr/sbin/sendmail -t' + +def mail(msg, sendmail=None): + """ + Send an email Message instance on its merry way. + + We can shell out to the user specified sendmail in case + the local host doesn't have an SMTP server set up + for easy smtplib usage. + """ + if sendmail != None: + execute(sendmail, stdin=flatten(msg)) + return None + s = smtplib.SMTP() + s.connect() + s.sendmail(from_addr=source_email(msg), + to_addrs=target_emails(msg), + msg=flatten(msg)) + s.close() + +def header_from_text(text, encoding="us-ascii"): + """ + Simple wrapper for instantiating an email.Message from text. + >>> header = header_from_text('\\n'.join(['From: me@big.edu','To: you@big.edu','Subject: testing'])) + >>> print flatten(header) + From: me@big.edu + To: you@big.edu + Subject: testing + <BLANKLINE> + <BLANKLINE> + """ + text = text.strip() + if type(text) == types.UnicodeType: + text = text.encode(encoding) + # assume StringType arguments are already encoded + p = Parser() + return p.parsestr(text, headersonly=True) + +def guess_encoding(text): + if type(text) == types.StringType: + encoding = "us-ascii" + elif type(text) == types.UnicodeType: + for encoding in ["us-ascii", "iso-8859-1", "utf-8"]: + try: + text.encode(encoding) + except UnicodeError: + pass + else: + break + assert encoding != None + return encoding + +def encodedMIMEText(body, encoding=None): + if encoding == None: + encoding = guess_encoding(body) + if encoding == "us-ascii": + return MIMEText(body) + else: + # Create the message ('plain' stands for Content-Type: text/plain) + return MIMEText(body.encode(encoding), 'plain', encoding) + +def append_text(text_part, new_text): + original_payload = text_part.get_payload(decode=True) + new_payload = u"%s%s" % (original_payload, new_text) + new_encoding = guess_encoding(new_payload) + text_part.set_payload(new_payload.encode(new_encoding), new_encoding) + +def attach_root(header, root_part): + """ + Attach the email.Message root_part to the email.Message header + without generating a multi-part message. + """ + for k,v in header.items(): + root_part[k] = v + return root_part + +def execute(args, stdin=None, expect=(0,)): + """ + Execute a command (allows us to drive gpg). + """ + if verboseInvoke == True: + print >> sys.stderr, '$ '+args + try: + p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, close_fds=True) + except OSError, e: + strerror = '%s\nwhile executing %s' % (e.args[1], args) + raise Exception, strerror + output, error = p.communicate(input=stdin) + status = p.wait() + if verboseInvoke == True: + print >> sys.stderr, '(status: %d)\n%s%s' % (status, output, error) + if status not in expect: + strerror = '%s\nwhile executing %s\n%s\n%d' % (args[1], args, error, status) + raise Exception, strerror + return status, output, error + +def replace(template, format_char, replacement_text): + """ + >>> replace('--textmode %?a?-u %a? %f', 'f', 'file.in') + '--textmode %?a?-u %a? file.in' + >>> replace('--textmode %?a?-u %a? %f', 'a', '0xHEXKEY') + '--textmode -u 0xHEXKEY %f' + >>> replace('--textmode %?a?-u %a? %f', 'a', '') + '--textmode %f' + """ + if replacement_text == None: + replacement_text = "" + regexp = re.compile('%[?]'+format_char+'[?]([^?]*)[?]') + if len(replacement_text) > 0: + str = regexp.sub('\g<1>', template) + else: + str = regexp.sub('', template) + regexp = re.compile('%'+format_char) + str = regexp.sub(replacement_text, str) + return str + +def flatten(msg, to_unicode=False): + """ + Produce flat text output from an email Message instance. + """ + assert msg != None + fp = StringIO() + g = Generator(fp, mangle_from_=False) + g.flatten(msg) + text = fp.getvalue() + if to_unicode == True: + encoding = msg.get_content_charset() or "utf-8" + text = unicode(text, encoding=encoding) + return text + +def source_email(msg, return_realname=False): + """ + Search the header of an email Message instance to find the + sender's email address. + """ + froms = msg.get_all('from', []) + from_tuples = getaddresses(froms) # [(realname, email_address), ...] + assert len(from_tuples) == 1 + if return_realname == True: + return from_tuples[0] # (realname, email_address) + return from_tuples[0][1] # email_address + +def target_emails(msg): + """ + Search the header of an email Message instance to find a + list of recipient's email addresses. + """ + tos = msg.get_all('to', []) + ccs = msg.get_all('cc', []) + bccs = msg.get_all('bcc', []) + resent_tos = msg.get_all('resent-to', []) + resent_ccs = msg.get_all('resent-cc', []) + resent_bccs = msg.get_all('resent-bcc', []) + all_recipients = getaddresses(tos + ccs + bccs + resent_tos + + resent_ccs + resent_bccs) + return [addr[1] for addr in all_recipients] + +class PGPMimeMessageFactory (object): + """ + See http://www.ietf.org/rfc/rfc3156.txt for specification details. + >>> from_addr = "me@big.edu" + >>> to_addr = "you@you.edu" + >>> header = header_from_text('\\n'.join(['From: %s'%from_addr,'To: %s'%to_addr,'Subject: testing'])) + >>> source_email(header) == from_addr + True + >>> target_emails(header) == [to_addr] + True + >>> m = PGPMimeMessageFactory('check 1 2\\ncheck 1 2\\n') + >>> print flatten(m.clearBodyPart()) + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Disposition: inline + <BLANKLINE> + check 1 2 + check 1 2 + <BLANKLINE> + >>> print flatten(m.plain()) + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + <BLANKLINE> + check 1 2 + check 1 2 + <BLANKLINE> + >>> signed = m.sign(header) + >>> signed.set_boundary('boundsep') + >>> print flatten(signed).replace('\\t', ' '*4) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + Content-Type: multipart/signed; protocol="application/pgp-signature"; + micalg="pgp-sha1"; boundary="boundsep" + MIME-Version: 1.0 + Content-Disposition: inline + <BLANKLINE> + --boundsep + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Disposition: inline + <BLANKLINE> + check 1 2 + check 1 2 + <BLANKLINE> + --boundsep + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Description: signature + Content-Type: application/pgp-signature; name="signature.asc"; + charset="us-ascii" + <BLANKLINE> + -----BEGIN PGP SIGNATURE----- + ... + -----END PGP SIGNATURE----- + <BLANKLINE> + --boundsep-- + >>> encrypted = m.encrypt(header) + >>> encrypted.set_boundary('boundsep') + >>> print flatten(encrypted).replace('\\t', ' '*4) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + Content-Type: multipart/encrypted; + protocol="application/pgp-encrypted"; + micalg="pgp-sha1"; boundary="boundsep" + MIME-Version: 1.0 + Content-Disposition: inline + <BLANKLINE> + --boundsep + Content-Type: application/pgp-encrypted + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + <BLANKLINE> + Version: 1 + <BLANKLINE> + --boundsep + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Type: application/octet-stream; charset="us-ascii" + <BLANKLINE> + -----BEGIN PGP MESSAGE----- + ... + -----END PGP MESSAGE----- + <BLANKLINE> + --boundsep-- + >>> signedAndEncrypted = m.signAndEncrypt(header) + >>> signedAndEncrypted.set_boundary('boundsep') + >>> print flatten(signedAndEncrypted).replace('\\t', ' '*4) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + Content-Type: multipart/encrypted; + protocol="application/pgp-encrypted"; + micalg="pgp-sha1"; boundary="boundsep" + MIME-Version: 1.0 + Content-Disposition: inline + <BLANKLINE> + --boundsep + Content-Type: application/pgp-encrypted + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + <BLANKLINE> + Version: 1 + <BLANKLINE> + --boundsep + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Type: application/octet-stream; charset="us-ascii" + <BLANKLINE> + -----BEGIN PGP MESSAGE----- + ... + -----END PGP MESSAGE----- + <BLANKLINE> + --boundsep-- + """ + def __init__(self, body): + self.body = body + def clearBodyPart(self): + body = encodedMIMEText(self.body) + body.add_header('Content-Disposition', 'inline') + return body + def passphrase_arg(self, passphrase=None): + if passphrase == None and PASSPHRASE != None: + passphrase = PASSPHRASE + if passphrase == None: + return (None,'') + return (passphrase, pgp_stdin_passphrase_arg) + def plain(self): + """ + text/plain + """ + return encodedMIMEText(self.body) + def sign(self, header, passphrase=None): + """ + multipart/signed + +-> text/plain (body) + +-> application/pgp-signature (signature) + """ + passphrase,pass_arg = self.passphrase_arg(passphrase) + body = self.clearBodyPart() + bfile = tempfile.NamedTemporaryFile() + bfile.write(flatten(body)) + bfile.flush() + + args = replace(pgp_sign_command, 'f', bfile.name) + if PGP_SIGN_AS == None: + pgp_sign_as = '<%s>' % source_email(header) + else: + pgp_sign_as = PGP_SIGN_AS + args = replace(args, 'a', pgp_sign_as) + args = replace(args, 'p', pass_arg) + status,output,error = execute(args, stdin=passphrase) + signature = output + + sig = MIMEApplication(_data=signature, + _subtype='pgp-signature; name="signature.asc"', + _encoder=encode_7or8bit) + sig['Content-Description'] = 'signature' + sig.set_charset('us-ascii') + + msg = MIMEMultipart('signed', micalg='pgp-sha1', + protocol='application/pgp-signature') + msg.attach(body) + msg.attach(sig) + + msg['Content-Disposition'] = 'inline' + return msg + def encrypt(self, header, passphrase=None): + """ + multipart/encrypted + +-> application/pgp-encrypted (control information) + +-> application/octet-stream (body) + """ + body = self.clearBodyPart() + bfile = tempfile.NamedTemporaryFile() + bfile.write(flatten(body)) + bfile.flush() + + recipients = [replace(pgp_recipient_arg, 'r', recipient) + for recipient in target_emails(header)] + recipient_string = ' '.join(recipients) + args = replace(pgp_encrypt_only_command, 'R', recipient_string) + args = replace(args, 'f', bfile.name) + if PGP_SIGN_AS == None: + pgp_sign_as = '<%s>' % source_email(header) + else: + pgp_sign_as = PGP_SIGN_AS + args = replace(args, 'a', pgp_sign_as) + status,output,error = execute(args) + encrypted = output + + enc = MIMEApplication(_data=encrypted, _subtype='octet-stream', + _encoder=encode_7or8bit) + enc.set_charset('us-ascii') + + control = MIMEApplication(_data='Version: 1\n', _subtype='pgp-encrypted', + _encoder=encode_7or8bit) + + msg = MIMEMultipart('encrypted', micalg='pgp-sha1', + protocol='application/pgp-encrypted') + msg.attach(control) + msg.attach(enc) + + msg['Content-Disposition'] = 'inline' + return msg + def signAndEncrypt(self, header, passphrase=None): + """ + multipart/encrypted + +-> application/pgp-encrypted (control information) + +-> application/octet-stream (body) + """ + passphrase,pass_arg = self.passphrase_arg(passphrase) + body = self.sign(header, passphrase) + body.__delitem__('Bcc') + bfile = tempfile.NamedTemporaryFile() + bfile.write(flatten(body)) + bfile.flush() + + recipients = [replace(pgp_recipient_arg, 'r', recipient) + for recipient in target_emails(header)] + recipient_string = ' '.join(recipients) + args = replace(pgp_encrypt_only_command, 'R', recipient_string) + args = replace(args, 'f', bfile.name) + if PGP_SIGN_AS == None: + pgp_sign_as = '<%s>' % source_email(header) + else: + pgp_sign_as = PGP_SIGN_AS + args = replace(args, 'a', pgp_sign_as) + args = replace(args, 'p', pass_arg) + status,output,error = execute(args, stdin=passphrase) + encrypted = output + + enc = MIMEApplication(_data=encrypted, _subtype='octet-stream', + _encoder=encode_7or8bit) + enc.set_charset('us-ascii') + + control = MIMEApplication(_data='Version: 1\n', + _subtype='pgp-encrypted', + _encoder=encode_7or8bit) + + msg = MIMEMultipart('encrypted', micalg='pgp-sha1', + protocol='application/pgp-encrypted') + msg.attach(control) + msg.attach(enc) + + msg['Content-Disposition'] = 'inline' + return msg + +def test(): + import doctest + doctest.testmod() + + +if __name__ == '__main__': + from optparse import OptionParser + + parser = OptionParser(usage=usage) + parser.add_option('-t', '--test', dest='test', action='store_true', + help='Run doctests and exit') + + parser.add_option('-H', '--header-file', dest='header_filename', + help='file containing email header', metavar='FILE') + parser.add_option('-B', '--body-file', dest='body_filename', + help='file containing email body', metavar='FILE') + + parser.add_option('-P', '--passphrase-file', dest='passphrase_file', + help='file containing gpg passphrase', metavar='FILE') + parser.add_option('-p', '--passphrase-fd', dest='passphrase_fd', + help='file descriptor from which to read gpg passphrase (0 for stdin)', + type="int", metavar='DESCRIPTOR') + + parser.add_option('--mode', dest='mode', default='sign', + help="One of 'sign', 'encrypt', 'sign-encrypt', or 'plain'. Defaults to %default.", + metavar='MODE') + + parser.add_option('-a', '--sign-as', dest='sign_as', + help="The gpg key to sign with (gpg's -u/--local-user)", + metavar='KEY') + + parser.add_option('--output', dest='output', action='store_true', + help="Don't mail the generated message, print it to stdout instead.") + + (options, args) = parser.parse_args() + + stdin_used = False + + if options.passphrase_file != None: + PASSPHRASE = file(options.passphrase_file, 'r').read() + elif options.passphrase_fd != None: + if options.passphrase_fd == 0: + stdin_used = True + PASSPHRASE = sys.stdin.read() + else: + PASSPHRASE = os.read(options.passphrase_fd) + + if options.sign_as: + PGP_SIGN_AS = options.sign_as + + if options.test == True: + test() + sys.exit(0) + + header = None + if options.header_filename != None: + if options.header_filename == '-': + assert stdin_used == False + stdin_used = True + header = sys.stdin.read() + else: + header = file(options.header_filename, 'r').read() + if header == None: + raise Exception, "missing header" + headermsg = header_from_text(header) + body = None + if options.body_filename != None: + if options.body_filename == '-': + assert stdin_used == False + stdin_used = True + body = sys.stdin.read() + else: + body = file(options.body_filename, 'r').read() + if body == None: + raise Exception, "missing body" + + m = PGPMimeMessageFactory(body) + if options.mode == "sign": + bodymsg = m.sign(header) + elif options.mode == "encrypt": + bodymsg = m.encrypt(header) + elif options.mode == "sign-encrypt": + bodymsg = m.signAndEncrypt(header) + elif options.mode == "plain": + bodymsg = m.plain() + else: + print "Unrecognized mode '%s'" % options.mode + + message = attach_root(headermsg, bodymsg) + if options.output == True: + message = flatten(message) + print message + else: + mail(message, sendmail) diff --git a/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/body b/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/body new file mode 100644 index 0000000..49a1e50 --- /dev/null +++ b/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/body @@ -0,0 +1 @@ +The problem is the jQuery selector... I need to escape something special but I'm not sure what.
\ No newline at end of file diff --git a/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/values b/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/values new file mode 100644 index 0000000..bf58725 --- /dev/null +++ b/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/comments/e3389187-1e84-43d5-b40b-26f53090edff/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Mon, 02 Feb 2009 00:39:43 +0000 + + + + + + +From=Steve Losh <steve@stevelosh.com> + + + diff --git a/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/values b/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/values new file mode 100644 index 0000000..256574b --- /dev/null +++ b/interfaces/web/.be/bugs/04edb940-06dd-4ded-8697-156d54a1d875/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=open + + + + + + +summary=Assignee default selection is broken if two people have the same name but different emails. + + + + + + +target=beta + + + + + + +time=Mon, 02 Feb 2009 00:38:49 +0000 + + + diff --git a/interfaces/web/.be/bugs/0a234f51-2fdf-4001-a04f-b7e02c2fa47b/values b/interfaces/web/.be/bugs/0a234f51-2fdf-4001-a04f-b7e02c2fa47b/values new file mode 100644 index 0000000..b911874 --- /dev/null +++ b/interfaces/web/.be/bugs/0a234f51-2fdf-4001-a04f-b7e02c2fa47b/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=open + + + + + + +summary=Humanize empty result pages. + + + + + + +target=beta + + + + + + +time=Sat, 31 Jan 2009 03:03:52 +0000 + + + diff --git a/interfaces/web/.be/bugs/0be47243-c172-4de9-b71b-d5dea60f91d5/values b/interfaces/web/.be/bugs/0be47243-c172-4de9-b71b-d5dea60f91d5/values new file mode 100644 index 0000000..0626932 --- /dev/null +++ b/interfaces/web/.be/bugs/0be47243-c172-4de9-b71b-d5dea60f91d5/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=open + + + + + + +summary=Fix the null creation date bug. See bug ee6 in the BE repo for an example that breaks things. + + + + + + +target=beta + + + + + + +time=Sun, 01 Feb 2009 21:26:49 +0000 + + + diff --git a/interfaces/web/.be/bugs/171819aa-c092-4ddf-ace3-797635fa2572/values b/interfaces/web/.be/bugs/171819aa-c092-4ddf-ace3-797635fa2572/values new file mode 100644 index 0000000..361cfa7 --- /dev/null +++ b/interfaces/web/.be/bugs/171819aa-c092-4ddf-ace3-797635fa2572/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=fatal + + + + + + +status=closed + + + + + + +summary=Get a basic template mocked up for the list page. Go further from there. + + + + + + +target=alpha + + + + + + +time=Fri, 30 Jan 2009 03:16:26 +0000 + + + diff --git a/interfaces/web/.be/bugs/24555ea1-76b5-40a8-918f-115a28f5f36a/values b/interfaces/web/.be/bugs/24555ea1-76b5-40a8-918f-115a28f5f36a/values new file mode 100644 index 0000000..72629bc --- /dev/null +++ b/interfaces/web/.be/bugs/24555ea1-76b5-40a8-918f-115a28f5f36a/values @@ -0,0 +1,20 @@ +assigned: Steve Losh <steve@stevelosh.com> + + +creator: Steve Losh <steve@stevelosh.com> + + +severity: critical + + +status: wontfix + + +summary: Fix the extra severity problem. + + +target: beta + + +time: Thu, 25 Jun 2009 21:39:38 +0000 + diff --git a/interfaces/web/.be/bugs/312fb152-0155-45c1-9d4d-f49dd5816fbb/values b/interfaces/web/.be/bugs/312fb152-0155-45c1-9d4d-f49dd5816fbb/values new file mode 100644 index 0000000..2795a3a --- /dev/null +++ b/interfaces/web/.be/bugs/312fb152-0155-45c1-9d4d-f49dd5816fbb/values @@ -0,0 +1,20 @@ +assigned: Steve Losh <steve@stevelosh.com> + + +creator: Steve Losh <steve@stevelosh.com> + + +severity: serious + + +status: fixed + + +summary: Revamp the layout/design. + + +target: beta + + +time: Thu, 25 Jun 2009 21:38:38 +0000 + diff --git a/interfaces/web/.be/bugs/35b962a0-a64a-4b5c-82c5-ea740e8a6322/values b/interfaces/web/.be/bugs/35b962a0-a64a-4b5c-82c5-ea740e8a6322/values new file mode 100644 index 0000000..134df9b --- /dev/null +++ b/interfaces/web/.be/bugs/35b962a0-a64a-4b5c-82c5-ea740e8a6322/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Document the code for the alpha release. + + + + + + +target=alpha + + + + + + +time=Sat, 31 Jan 2009 05:17:34 +0000 + + + diff --git a/interfaces/web/.be/bugs/42716dc2-6201-4537-b5fd-e1280812a53d/values b/interfaces/web/.be/bugs/42716dc2-6201-4537-b5fd-e1280812a53d/values new file mode 100644 index 0000000..c193f8f --- /dev/null +++ b/interfaces/web/.be/bugs/42716dc2-6201-4537-b5fd-e1280812a53d/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Document the packaging and install. + + + + + + +target=alpha + + + + + + +time=Sat, 31 Jan 2009 05:17:45 +0000 + + + diff --git a/interfaces/web/.be/bugs/4286c0f8-5703-4bc1-b256-414dc408f067/values b/interfaces/web/.be/bugs/4286c0f8-5703-4bc1-b256-414dc408f067/values new file mode 100644 index 0000000..bc901f9 --- /dev/null +++ b/interfaces/web/.be/bugs/4286c0f8-5703-4bc1-b256-414dc408f067/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Get the layout rhythm right. + + + + + + +target=alpha + + + + + + +time=Sat, 31 Jan 2009 00:14:34 +0000 + + + diff --git a/interfaces/web/.be/bugs/528b2e84-a944-4628-a18f-cc1def1c7e16/values b/interfaces/web/.be/bugs/528b2e84-a944-4628-a18f-cc1def1c7e16/values new file mode 100644 index 0000000..19aafd2 --- /dev/null +++ b/interfaces/web/.be/bugs/528b2e84-a944-4628-a18f-cc1def1c7e16/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Implement viewing of a single bug (with comments). + + + + + + +target=alpha + + + + + + +time=Sat, 31 Jan 2009 02:59:28 +0000 + + + diff --git a/interfaces/web/.be/bugs/52a15454-196c-4990-b55d-be2e37d575c3/values b/interfaces/web/.be/bugs/52a15454-196c-4990-b55d-be2e37d575c3/values new file mode 100644 index 0000000..a3cb0aa --- /dev/null +++ b/interfaces/web/.be/bugs/52a15454-196c-4990-b55d-be2e37d575c3/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=open + + + + + + +summary=Fix the overflow problem in the comments. + + + + + + +target=beta + + + + + + +time=Sat, 07 Feb 2009 21:32:51 +0000 + + + diff --git a/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/body b/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/body new file mode 100644 index 0000000..5becd48 --- /dev/null +++ b/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/body @@ -0,0 +1 @@ +Apparently the summary can only be one line. That makes the whitespace issue less relevant.
\ No newline at end of file diff --git a/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/values b/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/values new file mode 100644 index 0000000..41f53c6 --- /dev/null +++ b/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/comments/88d54d29-7312-4bb3-bc50-1970bdb2bb0e/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Sun, 01 Feb 2009 22:49:29 +0000 + + + + + + +From=Steve Losh <steve@stevelosh.com> + + + diff --git a/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/values b/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/values new file mode 100644 index 0000000..851021e --- /dev/null +++ b/interfaces/web/.be/bugs/545311df-8c88-4504-9f83-11d7c5d8aa50/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Implement bug updating (not comments). Check on the whitespace of the summary field while you're at it. + + + + + + +target=alpha + + + + + + +time=Sat, 31 Jan 2009 02:59:54 +0000 + + + diff --git a/interfaces/web/.be/bugs/55e76f74-37fb-4254-8498-54b703ba54f6/values b/interfaces/web/.be/bugs/55e76f74-37fb-4254-8498-54b703ba54f6/values new file mode 100644 index 0000000..cded1dc --- /dev/null +++ b/interfaces/web/.be/bugs/55e76f74-37fb-4254-8498-54b703ba54f6/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Fix the footer width. + + + + + + +target=alpha + + + + + + +time=Sat, 31 Jan 2009 03:01:09 +0000 + + + diff --git a/interfaces/web/.be/bugs/615ad650-9fb9-4026-9779-58d42b4e528e/values b/interfaces/web/.be/bugs/615ad650-9fb9-4026-9779-58d42b4e528e/values new file mode 100644 index 0000000..56ae9a1 --- /dev/null +++ b/interfaces/web/.be/bugs/615ad650-9fb9-4026-9779-58d42b4e528e/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=open + + + + + + +summary=Figure out how to best fix the column widths. + + + + + + +target=beta + + + + + + +time=Sat, 31 Jan 2009 03:07:32 +0000 + + + diff --git a/interfaces/web/.be/bugs/63619cf7-89eb-4e64-91e9-b8a73d2a6c72/values b/interfaces/web/.be/bugs/63619cf7-89eb-4e64-91e9-b8a73d2a6c72/values new file mode 100644 index 0000000..cb7a38e --- /dev/null +++ b/interfaces/web/.be/bugs/63619cf7-89eb-4e64-91e9-b8a73d2a6c72/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=open + + + + + + +summary=Implement sorting. + + + + + + +target=beta + + + + + + +time=Sat, 31 Jan 2009 02:59:11 +0000 + + + diff --git a/interfaces/web/.be/bugs/700cd3f1-70b6-4887-89a2-c1d039732add/values b/interfaces/web/.be/bugs/700cd3f1-70b6-4887-89a2-c1d039732add/values new file mode 100644 index 0000000..71ab0a3 --- /dev/null +++ b/interfaces/web/.be/bugs/700cd3f1-70b6-4887-89a2-c1d039732add/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=open + + + + + + +summary=Implement pagination. + + + + + + +target=beta + + + + + + +time=Sat, 31 Jan 2009 03:00:35 +0000 + + + diff --git a/interfaces/web/.be/bugs/81f69fbd-1ca5-4f89-a6e1-79ea1e6bf4d9/values b/interfaces/web/.be/bugs/81f69fbd-1ca5-4f89-a6e1-79ea1e6bf4d9/values new file mode 100644 index 0000000..dcaa6b3 --- /dev/null +++ b/interfaces/web/.be/bugs/81f69fbd-1ca5-4f89-a6e1-79ea1e6bf4d9/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=open + + + + + + +summary=The CherryPy server seems to drop connections randomly. + + + + + + +target=beta + + + + + + +time=Mon, 02 Feb 2009 01:12:37 +0000 + + + diff --git a/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/body b/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/body new file mode 100644 index 0000000..f75f8b7 --- /dev/null +++ b/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/body @@ -0,0 +1 @@ +Right now you can only select assignees or targets that have already been specified in another bug. There should be a way to add new ones from the bug edit screen.
\ No newline at end of file diff --git a/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/values b/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/values new file mode 100644 index 0000000..7927b05 --- /dev/null +++ b/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/comments/738f9826-57b6-43d6-a0cb-0dfeeb185b96/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Sat, 07 Feb 2009 21:32:19 +0000 + + + + + + +From=Steve Losh <steve@stevelosh.com> + + + diff --git a/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/values b/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/values new file mode 100644 index 0000000..f9e0a64 --- /dev/null +++ b/interfaces/web/.be/bugs/866cba32-4347-4f51-9b1d-69454638ca78/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=serious + + + + + + +status=open + + + + + + +summary=Implement adding new assignees/targets. + + + + + + +target=beta + + + + + + +time=Sat, 07 Feb 2009 21:31:26 +0000 + + + diff --git a/interfaces/web/.be/bugs/870d5dbe-6449-4ec4-ae6f-e84bebadbce0/values b/interfaces/web/.be/bugs/870d5dbe-6449-4ec4-ae6f-e84bebadbce0/values new file mode 100644 index 0000000..e91a4cf --- /dev/null +++ b/interfaces/web/.be/bugs/870d5dbe-6449-4ec4-ae6f-e84bebadbce0/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Refine graphic design for the alpha version. + + + + + + +target=alpha + + + + + + +time=Sat, 31 Jan 2009 05:17:08 +0000 + + + diff --git a/interfaces/web/.be/bugs/8cb9045c-7266-4c40-9a76-65f3c5d5bb60/values b/interfaces/web/.be/bugs/8cb9045c-7266-4c40-9a76-65f3c5d5bb60/values new file mode 100644 index 0000000..b8403eb --- /dev/null +++ b/interfaces/web/.be/bugs/8cb9045c-7266-4c40-9a76-65f3c5d5bb60/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Refactor the web interface into its own file. + + + + + + +target=alpha + + + + + + +time=Sat, 07 Feb 2009 17:27:48 +0000 + + + diff --git a/interfaces/web/.be/bugs/984472f6-98f5-48fc-b521-70a1e5f60614/values b/interfaces/web/.be/bugs/984472f6-98f5-48fc-b521-70a1e5f60614/values new file mode 100644 index 0000000..21d3cef --- /dev/null +++ b/interfaces/web/.be/bugs/984472f6-98f5-48fc-b521-70a1e5f60614/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Implement the status filters. + + + + + + +target=alpha + + + + + + +time=Sat, 31 Jan 2009 00:22:40 +0000 + + + diff --git a/interfaces/web/.be/bugs/9bc14860-b2bb-4442-85ea-0b8e7083457b/values b/interfaces/web/.be/bugs/9bc14860-b2bb-4442-85ea-0b8e7083457b/values new file mode 100644 index 0000000..b01cd70 --- /dev/null +++ b/interfaces/web/.be/bugs/9bc14860-b2bb-4442-85ea-0b8e7083457b/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=open + + + + + + +summary=Create a project page. + + + + + + +target=beta + + + + + + +time=Sat, 31 Jan 2009 05:18:56 +0000 + + + diff --git a/interfaces/web/.be/bugs/ac72991a-72e5-4b14-b53c-0fa38d0f31bb/values b/interfaces/web/.be/bugs/ac72991a-72e5-4b14-b53c-0fa38d0f31bb/values new file mode 100644 index 0000000..b4de064 --- /dev/null +++ b/interfaces/web/.be/bugs/ac72991a-72e5-4b14-b53c-0fa38d0f31bb/values @@ -0,0 +1,42 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=open + + + + + + +summary=The bug editing/comment forms break the rhythm. + + + + + + +time=Sun, 01 Feb 2009 23:59:17 +0000 + + + diff --git a/interfaces/web/.be/bugs/bef126a0-27be-402f-84fa-85f6342c97c0/values b/interfaces/web/.be/bugs/bef126a0-27be-402f-84fa-85f6342c97c0/values new file mode 100644 index 0000000..94d96d7 --- /dev/null +++ b/interfaces/web/.be/bugs/bef126a0-27be-402f-84fa-85f6342c97c0/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Implement bug creation. + + + + + + +target=alpha + + + + + + +time=Sat, 31 Jan 2009 02:59:35 +0000 + + + diff --git a/interfaces/web/.be/bugs/c7251ff9-24e4-402d-8d4e-605a78b9a91d/values b/interfaces/web/.be/bugs/c7251ff9-24e4-402d-8d4e-605a78b9a91d/values new file mode 100644 index 0000000..4c1c8cb --- /dev/null +++ b/interfaces/web/.be/bugs/c7251ff9-24e4-402d-8d4e-605a78b9a91d/values @@ -0,0 +1,20 @@ +assigned: Steve Losh <steve@stevelosh.com> + + +creator: Steve Losh <steve@stevelosh.com> + + +severity: critical + + +status: assigned + + +summary: Document the installation. + + +target: alpha + + +time: Thu, 25 Jun 2009 21:41:02 +0000 + diff --git a/interfaces/web/.be/bugs/cfb52b6c-d1a6-4018-a255-27cc1c878193/values b/interfaces/web/.be/bugs/cfb52b6c-d1a6-4018-a255-27cc1c878193/values new file mode 100644 index 0000000..49fa830 --- /dev/null +++ b/interfaces/web/.be/bugs/cfb52b6c-d1a6-4018-a255-27cc1c878193/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=open + + + + + + +summary=Change the write operations to be inline/AJAJ operations. + + + + + + +target=beta + + + + + + +time=Sun, 01 Feb 2009 21:15:35 +0000 + + + diff --git a/interfaces/web/.be/bugs/d63d0bdd-e025-4f7c-9fcf-47a71de6d4d4/values b/interfaces/web/.be/bugs/d63d0bdd-e025-4f7c-9fcf-47a71de6d4d4/values new file mode 100644 index 0000000..c100da5 --- /dev/null +++ b/interfaces/web/.be/bugs/d63d0bdd-e025-4f7c-9fcf-47a71de6d4d4/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Reset the state of the values when choosing "Discard Changes." + + + + + + +target=alpha + + + + + + +time=Sun, 01 Feb 2009 22:55:13 +0000 + + + diff --git a/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/body b/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/body new file mode 100644 index 0000000..6447d18 --- /dev/null +++ b/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/body @@ -0,0 +1 @@ +I think I just need to adjust the wrapper width.
\ No newline at end of file diff --git a/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/values b/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/values new file mode 100644 index 0000000..c68151d --- /dev/null +++ b/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/comments/24aab4bf-b525-48d6-9666-626e3ddcecf7/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Sat, 07 Feb 2009 18:36:56 +0000 + + + + + + +From=Steve Losh <steve@stevelosh.com> + + + diff --git a/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/values b/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/values new file mode 100644 index 0000000..18dd9a3 --- /dev/null +++ b/interfaces/web/.be/bugs/dd7aa57c-f184-495a-8520-2676c1066fb4/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=open + + + + + + +summary=The external pane sometimes loads in the wrong place. + + + + + + +target=beta + + + + + + +time=Mon, 02 Feb 2009 01:11:47 +0000 + + + diff --git a/interfaces/web/.be/bugs/decc6e78-a3db-4cd3-ad23-2bf8ed77cb0d/values b/interfaces/web/.be/bugs/decc6e78-a3db-4cd3-ad23-2bf8ed77cb0d/values new file mode 100644 index 0000000..96f52d3 --- /dev/null +++ b/interfaces/web/.be/bugs/decc6e78-a3db-4cd3-ad23-2bf8ed77cb0d/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Implement the target filters. + + + + + + +target=alpha + + + + + + +time=Sat, 31 Jan 2009 02:58:44 +0000 + + + diff --git a/interfaces/web/.be/bugs/e22a9048-9a97-41b1-91a2-d4178c674b37/values b/interfaces/web/.be/bugs/e22a9048-9a97-41b1-91a2-d4178c674b37/values new file mode 100644 index 0000000..63e9e8c --- /dev/null +++ b/interfaces/web/.be/bugs/e22a9048-9a97-41b1-91a2-d4178c674b37/values @@ -0,0 +1,42 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=unconfirmed + + + + + + +summary=Think about authentication. + + + + + + +time=Sat, 31 Jan 2009 03:02:19 +0000 + + + diff --git a/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/body b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/body new file mode 100644 index 0000000..d13b1b7 --- /dev/null +++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/body @@ -0,0 +1 @@ +I agree. (Test message). diff --git a/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/values b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/values new file mode 100644 index 0000000..4f055dd --- /dev/null +++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/aea21508-69c2-4d6b-ada1-4fbadac14c56/values @@ -0,0 +1,28 @@ + + + +Content-type=text/plain + + + + + + +Date=Sat, 31 Jan 2009 06:31:12 +0000 + + + + + + +From=Steve Losh <steve@stevelosh.com> + + + + + + +In-reply-to=d5ffa1c4-f435-4a9a-99f3-2a7bc3072051 + + + diff --git a/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/body b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/body new file mode 100644 index 0000000..8598e67 --- /dev/null +++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/body @@ -0,0 +1 @@ +This will not be incredibly easy. It will require reworking of the repository roots. diff --git a/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/values b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/values new file mode 100644 index 0000000..f541cad --- /dev/null +++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/d5ffa1c4-f435-4a9a-99f3-2a7bc3072051/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Sat, 31 Jan 2009 06:00:40 +0000 + + + + + + +From=Steve Losh <steve@stevelosh.com> + + + diff --git a/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/body b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/body new file mode 100644 index 0000000..20dbfd2 --- /dev/null +++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/body @@ -0,0 +1,3 @@ +This is a comment. + +With several lines. diff --git a/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/values b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/values new file mode 100644 index 0000000..759a973 --- /dev/null +++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/comments/f1fd8249-ded3-4e3c-a6ef-967d0a0edcd9/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Sat, 31 Jan 2009 06:48:21 +0000 + + + + + + +From=Steve Losh <steve@stevelosh.com> + + + diff --git a/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/values b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/values new file mode 100644 index 0000000..017229d --- /dev/null +++ b/interfaces/web/.be/bugs/e645d562-6f84-4df2-b8ee-86ef42546c16/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Package everything into something easy to download and use. + + + + + + +target=alpha + + + + + + +time=Fri, 30 Jan 2009 03:19:19 +0000 + + + diff --git a/interfaces/web/.be/bugs/fd96c69d-6f78-4c0c-af6e-e01e9b8516d3/values b/interfaces/web/.be/bugs/fd96c69d-6f78-4c0c-af6e-e01e9b8516d3/values new file mode 100644 index 0000000..837213e --- /dev/null +++ b/interfaces/web/.be/bugs/fd96c69d-6f78-4c0c-af6e-e01e9b8516d3/values @@ -0,0 +1,49 @@ + + + +assigned=Steve Losh <steve@stevelosh.com> + + + + + + +creator=Steve Losh <steve@stevelosh.com> + + + + + + +severity=minor + + + + + + +status=closed + + + + + + +summary=Implement adding comments. + + + + + + +target=alpha + + + + + + +time=Sat, 31 Jan 2009 03:00:08 +0000 + + + diff --git a/interfaces/web/.be/settings b/interfaces/web/.be/settings new file mode 100644 index 0000000..8ef3e76 --- /dev/null +++ b/interfaces/web/.be/settings @@ -0,0 +1,7 @@ + + + +rcs_name=hg + + + diff --git a/interfaces/web/.be/version b/interfaces/web/.be/version new file mode 100644 index 0000000..990837e --- /dev/null +++ b/interfaces/web/.be/version @@ -0,0 +1 @@ +Bugs Everywhere Tree 1 0 diff --git a/interfaces/web/.hgignore b/interfaces/web/.hgignore new file mode 100644 index 0000000..a0e81b7 --- /dev/null +++ b/interfaces/web/.hgignore @@ -0,0 +1,6 @@ +syntax: glob +*.pyc +.DS_Store +*.log +*.tmproj + diff --git a/interfaces/web/.hgtags b/interfaces/web/.hgtags new file mode 100644 index 0000000..eeea432 --- /dev/null +++ b/interfaces/web/.hgtags @@ -0,0 +1,2 @@ +8d8c7f52f3afb6026dd47d7303a7f6a734b3177d alpha +abfe7aa4bdf3cd019ad1d51278c293a4e008b397 alpha diff --git a/interfaces/web/LICENSE b/interfaces/web/LICENSE new file mode 100644 index 0000000..44f0935 --- /dev/null +++ b/interfaces/web/LICENSE @@ -0,0 +1,24 @@ + +copyrev: 566007698e1bb8a4f0bc4929a68ecc068ab28890 +copy: LICENSE.txt + +Copyright (c) 2009 Steve Losh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/interfaces/web/README b/interfaces/web/README new file mode 100644 index 0000000..6bd04e5 --- /dev/null +++ b/interfaces/web/README @@ -0,0 +1,20 @@ +-*- markdown -*- + +Cherry Flavored Bugs Everywhere +=============================== + +CFBE is a quick web interface to [BugsEverywhere](http://bugseverywhere.org/). It's still very much a work-in-progress. + +Installing +---------- + +I intend to streamline the installation once I'm satisfied with the interface itself. For now, the install process goes something like this: + +* Install [CherryPy](http://cherrypy.org/) if you don't have it. +* Install [Jinja2](http://jinja.pocoo.org/2/) if you don't have it. +* Install [BugsEverywhere](http://bugseverywhere.org/) if you don't have it. +* Download a zip/tar of CFBE (or hg clone) from the [Mercurial repository](http://bitbucket.org/sjl/cherryflavoredbugseverywhere/). +* Unzip (if you grabbed a zip) and put the folder in your Python site-packages directory (or put it anywhere and symlink it to site-packages). +* Symlink `site-packages/cherryflavoredbugseverywhere/cfbe.py` to `/usr/local/bin/cfbe` +* Use `cfbe [project_root]` to start up the web interface for that project. +* Visit http://localhost:8080/ in a browser. diff --git a/interfaces/web/__init__.py b/interfaces/web/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/interfaces/web/__init__.py diff --git a/interfaces/web/cfbe.py b/interfaces/web/cfbe.py new file mode 100755 index 0000000..63fbc7e --- /dev/null +++ b/interfaces/web/cfbe.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python + +import cherrypy +from cherryflavoredbugseverywhere import web +from optparse import OptionParser +from os import path + +module_dir = path.dirname(path.abspath(web.__file__)) +template_dir = path.join(module_dir, 'templates') + +def build_parser(): + """Builds and returns the command line option parser.""" + + usage = 'usage: %prog bug_directory' + parser = OptionParser(usage) + return parser + +def parse_arguments(): + """Parse the command line arguments.""" + + parser = build_parser() + (options, args) = parser.parse_args() + + if len(args) != 1: + parser.error('You need to specify a bug directory.') + + return { 'bug_root': args[0], } + + +config = path.join(module_dir, 'cfbe.config') +options = parse_arguments() + +WebInterface = web.WebInterface(path.abspath(options['bug_root']), template_dir) + +cherrypy.config.update({'tools.staticdir.root': path.join(module_dir, 'static')}) +app_config = { '/static': { 'tools.staticdir.on': True, + 'tools.staticdir.dir': '', } } +cherrypy.quickstart(WebInterface, '/', app_config) diff --git a/interfaces/web/static/scripts/jquery.corners.min.js b/interfaces/web/static/scripts/jquery.corners.min.js new file mode 100644 index 0000000..0b2f979 --- /dev/null +++ b/interfaces/web/static/scripts/jquery.corners.min.js @@ -0,0 +1,7 @@ +/* + * jQuery Corners 0.3 + * Copyright (c) 2008 David Turnbull, Steven Wittens + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + */ +jQuery.fn.corners=function(C){var N="rounded_by_jQuery_corners";var V=B(C);var F=false;try{F=(document.body.style.WebkitBorderRadius!==undefined);var Y=navigator.userAgent.indexOf("Chrome");if(Y>=0){F=false}}catch(E){}var W=false;try{W=(document.body.style.MozBorderRadius!==undefined);var Y=navigator.userAgent.indexOf("Firefox");if(Y>=0&&parseInt(navigator.userAgent.substring(Y+8))<3){W=false}}catch(E){}return this.each(function(b,h){$e=jQuery(h);if($e.hasClass(N)){return }$e.addClass(N);var a=/{(.*)}/.exec(h.className);var c=a?B(a[1],V):V;var j=h.nodeName.toLowerCase();if(j=="input"){h=O(h)}if(F&&c.webkit){K(h,c)}else{if(W&&c.mozilla&&(c.sizex==c.sizey)){M(h,c)}else{var d=D(h.parentNode);var f=D(h);switch(j){case"a":case"input":Z(h,c,d,f);break;default:R(h,c,d,f);break}}}});function K(d,c){var a=""+c.sizex+"px "+c.sizey+"px";var b=jQuery(d);if(c.tl){b.css("WebkitBorderTopLeftRadius",a)}if(c.tr){b.css("WebkitBorderTopRightRadius",a)}if(c.bl){b.css("WebkitBorderBottomLeftRadius",a)}if(c.br){b.css("WebkitBorderBottomRightRadius",a)}}function M(d,c){var a=""+c.sizex+"px";var b=jQuery(d);if(c.tl){b.css("-moz-border-radius-topleft",a)}if(c.tr){b.css("-moz-border-radius-topright",a)}if(c.bl){b.css("-moz-border-radius-bottomleft",a)}if(c.br){b.css("-moz-border-radius-bottomright",a)}}function Z(k,n,l,a){var m=S("table");var i=S("tbody");m.appendChild(i);var j=S("tr");var d=S("td","top");j.appendChild(d);var h=S("tr");var c=T(k,n,S("td"));h.appendChild(c);var f=S("tr");var b=S("td","bottom");f.appendChild(b);if(n.tl||n.tr){i.appendChild(j);X(d,n,l,a,true)}i.appendChild(h);if(n.bl||n.br){i.appendChild(f);X(b,n,l,a,false)}k.appendChild(m);if(jQuery.browser.msie){m.onclick=Q}k.style.overflow="hidden"}function Q(){if(!this.parentNode.onclick){this.parentNode.click()}}function O(c){var b=document.createElement("a");b.id=c.id;b.className=c.className;if(c.onclick){b.href="javascript:";b.onclick=c.onclick}else{jQuery(c).parent("form").each(function(){b.href=this.action});b.onclick=I}var a=document.createTextNode(c.value);b.appendChild(a);c.parentNode.replaceChild(b,c);return b}function I(){jQuery(this).parent("form").each(function(){this.submit()});return false}function R(d,a,b,c){var f=T(d,a,document.createElement("div"));d.appendChild(f);if(a.tl||a.tr){X(d,a,b,c,true)}if(a.bl||a.br){X(d,a,b,c,false)}}function T(j,i,k){var b=jQuery(j);var l;while(l=j.firstChild){k.appendChild(l)}if(j.style.height){var f=parseInt(b.css("height"));k.style.height=f+"px";f+=parseInt(b.css("padding-top"))+parseInt(b.css("padding-bottom"));j.style.height=f+"px"}if(j.style.width){var a=parseInt(b.css("width"));k.style.width=a+"px";a+=parseInt(b.css("padding-left"))+parseInt(b.css("padding-right"));j.style.width=a+"px"}k.style.paddingLeft=b.css("padding-left");k.style.paddingRight=b.css("padding-right");if(i.tl||i.tr){k.style.paddingTop=U(j,i,b.css("padding-top"),true)}else{k.style.paddingTop=b.css("padding-top")}if(i.bl||i.br){k.style.paddingBottom=U(j,i,b.css("padding-bottom"),false)}else{k.style.paddingBottom=b.css("padding-bottom")}j.style.padding=0;return k}function U(f,a,d,c){if(d.indexOf("px")<0){try{console.error("%s padding not in pixels",(c?"top":"bottom"),f)}catch(b){}d=a.sizey+"px"}d=parseInt(d);if(d-a.sizey<0){try{console.error("%s padding is %ipx for %ipx corner:",(c?"top":"bottom"),d,a.sizey,f)}catch(b){}d=a.sizey}return d-a.sizey+"px"}function S(b,a){var c=document.createElement(b);c.style.border="none";c.style.borderCollapse="collapse";c.style.borderSpacing=0;c.style.padding=0;c.style.margin=0;if(a){c.style.verticalAlign=a}return c}function D(b){try{var d=jQuery.css(b,"background-color");if(d.match(/^(transparent|rgba\(0,\s*0,\s*0,\s*0\))$/i)&&b.parentNode){return D(b.parentNode)}if(d==null){return"#ffffff"}if(d.indexOf("rgb")>-1){d=A(d)}if(d.length==4){d=L(d)}return d}catch(a){return"#ffffff"}}function L(a){return"#"+a.substring(1,2)+a.substring(1,2)+a.substring(2,3)+a.substring(2,3)+a.substring(3,4)+a.substring(3,4)}function A(h){var a=255;var d="";var b;var e=/([0-9]+)[, ]+([0-9]+)[, ]+([0-9]+)/;var f=e.exec(h);for(b=1;b<4;b++){d+=("0"+parseInt(f[b]).toString(16)).slice(-2)}return"#"+d}function B(b,d){var b=b||"";var c={sizex:5,sizey:5,tl:false,tr:false,bl:false,br:false,webkit:true,mozilla:true,transparent:false};if(d){c.sizex=d.sizex;c.sizey=d.sizey;c.webkit=d.webkit;c.transparent=d.transparent;c.mozilla=d.mozilla}var a=false;var e=false;jQuery.each(b.split(" "),function(f,j){j=j.toLowerCase();var h=parseInt(j);if(h>0&&j==h+"px"){c.sizey=h;if(!a){c.sizex=h}a=true}else{switch(j){case"no-native":c.webkit=c.mozilla=false;break;case"webkit":c.webkit=true;break;case"no-webkit":c.webkit=false;break;case"mozilla":c.mozilla=true;break;case"no-mozilla":c.mozilla=false;break;case"anti-alias":c.transparent=false;break;case"transparent":c.transparent=true;break;case"top":e=c.tl=c.tr=true;break;case"right":e=c.tr=c.br=true;break;case"bottom":e=c.bl=c.br=true;break;case"left":e=c.tl=c.bl=true;break;case"top-left":e=c.tl=true;break;case"top-right":e=c.tr=true;break;case"bottom-left":e=c.bl=true;break;case"bottom-right":e=c.br=true;break}}});if(!e){if(!d){c.tl=c.tr=c.bl=c.br=true}else{c.tl=d.tl;c.tr=d.tr;c.bl=d.bl;c.br=d.br}}return c}function P(f,d,h){var e=Array(parseInt("0x"+f.substring(1,3)),parseInt("0x"+f.substring(3,5)),parseInt("0x"+f.substring(5,7)));var c=Array(parseInt("0x"+d.substring(1,3)),parseInt("0x"+d.substring(3,5)),parseInt("0x"+d.substring(5,7)));r="0"+Math.round(e[0]+(c[0]-e[0])*h).toString(16);g="0"+Math.round(e[1]+(c[1]-e[1])*h).toString(16);d="0"+Math.round(e[2]+(c[2]-e[2])*h).toString(16);return"#"+r.substring(r.length-2)+g.substring(g.length-2)+d.substring(d.length-2)}function X(f,a,b,d,c){if(a.transparent){G(f,a,b,c)}else{J(f,a,b,d,c)}}function J(k,z,p,a,n){var h,f;var l=document.createElement("div");l.style.fontSize="1px";l.style.backgroundColor=p;var b=0;for(h=1;h<=z.sizey;h++){var u,t,q;arc=Math.sqrt(1-Math.pow(1-h/z.sizey,2))*z.sizex;var c=z.sizex-Math.ceil(arc);var w=Math.floor(b);var v=z.sizex-c-w;var o=document.createElement("div");var m=l;o.style.margin="0px "+c+"px";o.style.height="1px";o.style.overflow="hidden";for(f=1;f<=v;f++){if(f==1){if(f==v){u=((arc+b)*0.5)-w}else{t=Math.sqrt(1-Math.pow(1-(c+1)/z.sizex,2))*z.sizey;u=(t-(z.sizey-h))*(arc-w-v+1)*0.5}}else{if(f==v){t=Math.sqrt(1-Math.pow((z.sizex-c-f+1)/z.sizex,2))*z.sizey;u=1-(1-(t-(z.sizey-h)))*(1-(b-w))*0.5}else{q=Math.sqrt(1-Math.pow((z.sizex-c-f)/z.sizex,2))*z.sizey;t=Math.sqrt(1-Math.pow((z.sizex-c-f+1)/z.sizex,2))*z.sizey;u=((t+q)*0.5)-(z.sizey-h)}}H(z,o,m,n,P(p,a,u));m=o;var o=m.cloneNode(false);o.style.margin="0px 1px"}H(z,o,m,n,a);b=arc}if(n){k.insertBefore(l,k.firstChild)}else{k.appendChild(l)}}function H(c,a,e,d,b){if(d&&!c.tl){a.style.marginLeft=0}if(d&&!c.tr){a.style.marginRight=0}if(!d&&!c.bl){a.style.marginLeft=0}if(!d&&!c.br){a.style.marginRight=0}a.style.backgroundColor=b;if(d){e.appendChild(a)}else{e.insertBefore(a,e.firstChild)}}function G(c,o,l,h){var f=document.createElement("div");f.style.fontSize="1px";var a=document.createElement("div");a.style.overflow="hidden";a.style.height="1px";a.style.borderColor=l;a.style.borderStyle="none solid";var m=o.sizex-1;var j=o.sizey-1;if(!j){j=1}for(var b=0;b<o.sizey;b++){var n=m-Math.floor(Math.sqrt(1-Math.pow(1-b/j,2))*m);if(b==2&&o.sizex==6&&o.sizey==6){n=2}var k=a.cloneNode(false);k.style.borderWidth="0 "+n+"px";if(h){k.style.borderWidth="0 "+(o.tr?n:0)+"px 0 "+(o.tl?n:0)+"px"}else{k.style.borderWidth="0 "+(o.br?n:0)+"px 0 "+(o.bl?n:0)+"px"}h?f.appendChild(k):f.insertBefore(k,f.firstChild)}if(h){c.insertBefore(f,c.firstChild)}else{c.appendChild(f)}}};
\ No newline at end of file diff --git a/interfaces/web/static/style/aal.css b/interfaces/web/static/style/aal.css new file mode 100644 index 0000000..9bad98f --- /dev/null +++ b/interfaces/web/static/style/aal.css @@ -0,0 +1,99 @@ +/* + aardvark.legs by Anatoli Papirovski - http://fecklessmind.com/ + Licensed under the MIT license. http://www.opensource.org/licenses/mit-license.php +*/ + +/* + Reset first. Modified version of Eric Meyer and Paul Chaplin reset + from http://meyerweb.com/eric/tools/css/reset/ +*/ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, font, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +header, nav, section, article, aside, footer +{border: 0; margin: 0; outline: 0; padding: 0; background: transparent; vertical-align: baseline;} + +blockquote, q {quotes: none;} +blockquote:before,blockquote:after,q:before,q:after {content: ''; content: none;} + +header, nav, section, article, aside, footer {display: block;} + +/* Basic styles */ +body {background: #fff; color: #000; font: 0.875em/1.5em "Helvetica Neue", Helvetica, Arial, "Liberation Sans", "Bitstream Vera Sans", sans-serif;} +html>body {font-size: 14px;} + +img {display: inline-block; vertical-align: bottom;} + +h1,h2,h3,h4,h5,h6,strong,b,dt,th {font-weight: 700;} +address,cite,em,i,caption,dfn,var {font-style: italic;} + +h1 {margin: 0 0 0.75em; font-size: 2em;} +h2 {margin: 0 0 1em; font-size: 1.5em;} +h3 {margin: 0 0 1.286em; font-size: 1.167em;} +h4 {margin: 0 0 1.5em; font-size: 1em;} +h5 {margin: 0 0 1.8em; font-size: .834em;} +h6 {margin: 0 0 2em; font-size: .75em;} + +p,ul,ol,dl,blockquote,pre {margin: 0 0 1.5em;} + +li ul,li ol {margin: 0;} +ul {list-style: outside disc;} +ol {list-style: outside decimal;} +li {margin: 0 0 0 2em;} +dd {padding-left: 1.5em;} +blockquote {padding: 0 1.5em;} + +a {text-decoration: underline;} +a:hover {text-decoration: none;} +abbr,acronym {border-bottom: 1px dotted; cursor: help;} +del {text-decoration: line-through;} +ins {text-decoration: overline;} +sub {font-size: .834em; line-height: 1em; vertical-align: sub;} +sup {font-size: .834em; line-height: 1em; vertical-align: super;} + +tt,code,kbd,samp,pre {font-size: 1em; font-family: "Courier New", Courier, monospace;} + +/* Table styles */ +table {border-collapse: collapse; border-spacing: 0; margin: 0 0 1.5em;} +caption {text-align: left;} +th, td {padding: .25em .5em;} +tbody td, tbody th {border: 1px solid #000;} +tfoot {font-style: italic;} + +/* Form styles */ +fieldset {clear: both;} +legend {padding: 0 0 1.286em; font-size: 1.167em; font-weight: 700;} +fieldset fieldset legend {padding: 0 0 1.5em; font-size: 1em;} +* html legend {margin-left: -7px;} +*+html legend {margin-left: -7px;} + +form .field, form .buttons {clear: both; margin: 0 0 1.5em;} +form .field label {display: block;} +form ul.fields li {list-style-type: none; margin: 0;} +form ul.inline li, form ul.inline label {display: inline;} +form ul.inline li {padding: 0 .75em 0 0;} + +input.radio, input.checkbox {vertical-align: top;} +label, button, input.submit, input.image {cursor: pointer;} +* html input.radio, * html input.checkbox {vertical-align: middle;} +*+html input.radio, *+html input.checkbox {vertical-align: middle;} + +textarea {overflow: auto;} +input.text, input.password, textarea, select {margin: 0; font: 1em/1.3 Helvetica, Arial, "Liberation Sans", "Bitstream Vera Sans", sans-serif; vertical-align: bottom;} +input.text, input.password, textarea {border: 1px solid #444; border-bottom-color: #666; border-right-color: #666; padding: 2px;} + +* html button {margin: 0 .34em 0 0;} +*+html button {margin: 0 .34em 0 0;} + +form.horizontal .field {padding-left: 150px;} +form.horizontal .field label {display: inline; float: left; width: 140px; margin-left: -150px;} + +/* Useful classes */ +img.left {display: inline; float: left; margin: 0 1.5em .75em 0;} +img.right {display: inline; float: right; margin: 0 0 .75em .75em;}
\ No newline at end of file diff --git a/interfaces/web/static/style/cfbe.css b/interfaces/web/static/style/cfbe.css new file mode 100644 index 0000000..c5f726e --- /dev/null +++ b/interfaces/web/static/style/cfbe.css @@ -0,0 +1,180 @@ +/* @override http://localhost:8080/static/style/cfbe.css */ + +body { + background-color: #eee; +} + +div#main-pane { + width: 960px; + margin: 3em auto; + border: 1px solid #888; + background-color: #fcfcfc; +} +.inside-main-pane { + padding: 0em 3em; +} + +div#header { + background-color: #D8004A; + height: 6em; +} +div#header h1 { + font-size: 4em; + line-height: 1.5em; + margin-bottom: 0em; + color: #fff; + font-weight: normal; + font-family: "Helvetica Neue Ultra Light", "HelveticaNeue-UltraLight", "Helvetica", "Arial", sans-serif; + letter-spacing: 1px; +} + +div#navigation { + height: 3em; + line-height: 3em; + border-bottom: 1px solid #888; +} +div#content-pane { + margin: 1.5em 0em 3em; +} + +div#filter-pane { + display: none; + border-bottom: 1px solid #888; + line-height: 3em; + text-align: right; +} +ul.filter-items { + list-style-type: none; + margin: 0em; + padding: 0em; +} +ul.filter-items li { + display: inline; + margin-left: 1.5em; +} + +div#footer { + text-align: center; + height: 3em; + border-top: 1px solid #888; +} +div#footer p { + font-size: 0.9em; + line-height: 3.333em; +} + +span#filters { + float: right; +} +span#filters a { + margin-left: 1.5em; +} + +a:link, a:visited, a:active { + color: #d03; text-decoration: none; font-weight: bold; +} +a:hover { + color: #60b305; +} + +.header-with-link { + display: inline-block; +} +.header-link { + margin-left: 1em; +} + +table#bug-list { + width: 100%; border-collapse: collapse; border: 0.084em solid #ccc; +} +table#bug-list td, table#bug-list th { + border: 0em; border-bottom: 0.084em solid #ccc; text-align: left; +} +table tr td, table tr th { + padding: 0px 5px; +} +table tr td { + line-height: 2.832em; padding-bottom: 0.084em; +} +table tr th { + line-height: 2.916em; +} +table { + margin-bottom: 1.417em; +} +tr.stripe { + background-color: #fcecf8; +} + +div#assignees, div#targets { + display: none; +} + +p.creation-info { + color: #888; +} +span.detail-field-header { + font-weight: 700; + width: 7.5em; + padding-right: 1em; + display: inline-block; + text-align: right; +} + +div.bug-comment { + margin-left: 2em; +} +p.bug-comment-body { + white-space: pre; + margin: 0em 0em 0em 0em; +} +p.bug-comment-footer { + margin: 0em 0em; color: #888; +} +h4.bug-comment-header { + margin: 1.5em 0em 0em; +} + +#create-form { + display: none; +} +#create-form fieldset { + clear: none; +} +#create-form input#create-summary { + width: 20em; + border: 1px solid #888; + margin-right: 1.5em; +} +#create-button { + margin: 0em; +} + +form#add-comment-form { + display: none; + margin-top: 1.5em; +} +p#add-comment-link { + margin-top: 1.5em; +} + +form#bug-details-edit-form { + display: none; +} +form#bug-details-edit-form label { + font-weight: 700; + width: 7.5em; + margin-left: 0em; + margin-right: 1em; + text-align: right; +} +form#bug-details-edit-form .field { + padding-left: 0em; +} + +form#bug-summary-edit-form { + display: none; +} +input#bug-summary-edit-body { + width: 95%; +} diff --git a/interfaces/web/templates/base.html b/interfaces/web/templates/base.html new file mode 100644 index 0000000..8f22d73 --- /dev/null +++ b/interfaces/web/templates/base.html @@ -0,0 +1,106 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + +<html> + <head> + <title>Cherry Flavored Bugs Everywhere!</title> + + <link rel="stylesheet" type="text/css" media="screen" + href="/static/style/aal.css" /> + <link rel="stylesheet" type="text/css" media="screen" + href="/static/style/cfbe.css" /> + + <script type="text/javascript" + src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"> + </script> + + <script type="text/javascript" + src="/static/scripts/jquery.corners.min.js"> + </script> + + <script type="text/javascript"> + $(function() { + $('#filter-assignee').click(function(e) { + $('#filter-pane').html($('#assignees').html()); + $('#filter-pane').fadeIn('fast'); + e.preventDefault(); + }); + + $('#filter-target').click(function(e) { + $('#filter-pane').html($('#targets').html()); + $('#filter-pane').fadeIn('fast'); + e.preventDefault(); + }); + + $('#create-bug').click(function(e) { + $('#create-bug').hide(); + $('#create-form').fadeIn('fast'); + e.preventDefault(); + }); + + $('table tr:odd').addClass('stripe'); + }); + </script> + + {% block script %}{% endblock %} + </head> + + <body> + <div id="main-pane"> + <div id="header" class="inside-main-pane"> + <h1>{{ repository_name }}</h1> + </div> + <div id="navigation" class="inside-main-pane"> + <span id="filters"> + <a href="/">Open</a> + <a href="/?status=closed">Closed</a> + <a href="" id="filter-assignee">Assigned to...</a> + <a href="" id="filter-target">Scheduled for...</a> + </span> + <span id="create"> + <a href="" id="create-bug">+ Create a new bug</a> + </span> + <span id="create-form"> + <form action="/create" method="post"> + <fieldset> + <input type="text" + id="create-summary" name="summary" /> + <button id="create-button" + type="submit">Create</button> + </fieldset> + </form> + </span> + </div> + <div id="filter-pane" class="inside-main-pane"></div> + <div id="content-pane" class="inside-main-pane"> + <h2>{% block page_title %} {% endblock %}</h2> + {% block content %}{% endblock %} + </div> + <div id="footer" class="inside-main-pane"> + <p> + <a href="">Cherry Flavored Bugs Everywhere</a> + was created by <a href="http://stevelosh.com">Steve Losh</a> and a very nice <a href="http://fecklessmind.com/2009/01/20/aardvark-css-framework/">aardvark</a> + using <a href="http://cherrypy.org">CherryPy</a>, + <a href="http://jinja.pocoo.org/2/">Jinja2</a>, + and <a href="http://jquery.com">jQuery</a>. + </p> + </div> + </div> + <div id="assignees"> + <ul class="filter-items"> + <li><a href="/?assignee=None">Unassigned</a></li> + {% for assignee in assignees %} + <li><a href="/?assignee={{ assignee|e }}">{{ assignee|e }}</a></li> + {% endfor %} + </ul> + </div> + <div id="targets"> + <ul class="filter-items"> + <li><a href="/?target=None">Unscheduled</a></li> + {% for target in targets %} + <li><a href="/?target={{ target }}">{{ target }}</a></li> + {% endfor %} + </ul> + </div> + </body> +</html> diff --git a/interfaces/web/templates/bug.html b/interfaces/web/templates/bug.html new file mode 100644 index 0000000..4d15536 --- /dev/null +++ b/interfaces/web/templates/bug.html @@ -0,0 +1,160 @@ +{% extends "base.html" %} + +{% block page_title %} + Bug {{ bd.bug_shortname(bug) }} – {{ bug.summary|truncate(70) }} +{% endblock %} + +{% block script %} + <script type="text/javascript"> + $(function() { + function set_current_detail_default_values() { + $('#bug-details-edit-status option[value="{{ bug.status }}"]').attr('selected', 'yes'); + $('#bug-details-edit-target option[value="{{ bug.target|e }}"]').attr('selected', 'yes'); + $('#bug-details-edit-assignee option[value^="{{ bug.assigned|striptags }}"]').attr('selected', 'yes'); + $('#bug-details-edit-severity option[value="{{ bug.severity }}"]').attr('selected', 'yes'); + } + + $('#add-comment').click(function(e) { + $('#add-comment-link').hide(); + $('#add-comment-form').fadeIn('fast'); + e.preventDefault(); + }); + + $('#edit-bug-details').click(function(e) { + $('#bug-details').hide(); + $('#bug-details-edit-form').fadeIn('fast'); + e.preventDefault(); + }); + + $('#bug-details-edit-form button[type="reset"]').click(function(e) { + $('#bug-details-edit-form').hide(); + $('#bug-details').fadeIn('fast', function() { set_current_detail_default_values(); } ); + }); + + $('#edit-bug-summary').click(function(e) { + $('#bug-summary').hide(); + $('#bug-summary-edit-form').fadeIn('fast'); + e.preventDefault(); + }); + + $('#bug-summary-edit-form button[type="reset"]').click(function(e) { + $('#bug-summary-edit-form').hide(); + $('#bug-summary').fadeIn('fast', function() { set_current_detail_default_values(); } ); + }); + + set_current_detail_default_values(); + }); + </script> +{% endblock %} + +{% block content %} + <p class="creation-info">Created on {{ bug.time|datetimeformat }} by {{ bug.creator|e }}</p> + + <h3 class="header-with-link">Bug Details</h3> + <span class="header-link"> + <a href="" id="edit-bug-details">edit</a> + </span> + + <p id="bug-details"> + <span class="detail-field-header">Status:</span> + <span class="detail-field-contents">{{ bug.status }}</span><br /> + + <span class="detail-field-header">Severity:</span> + <span class="detail-field-contents">{{ bug.severity }}</span><br /> + + <span class="detail-field-header">Scheduled for:</span> + <span class="detail-field-contents">{{ target }}</span><br /> + + <span class="detail-field-header">Assigned to:</span> + <span class="detail-field-contents">{{ assignee|e }}</span><br /> + + <span class="detail-field-header">Permanent ID:</span> + <span class="detail-field-contents">{{ bug.uuid }}</span><br /> + </p> + + <form id="bug-details-edit-form" class="horizontal" action="/edit" method="post"> + <fieldset> + <input type="hidden" name="id" value="{{ bug.uuid }}" /> + <div class="field"> + <label for="bug-details-edit-status">Status:</label> + <select id="bug-details-edit-status" name="status"> + {% for status in statuses %} + <option value="{{ status }}">{{ status }}</option> + {% endfor %} + </select> + </div> + <div class="field"> + <label for="bug-details-edit-severity">Severity:</label> + <select id="bug-details-edit-severity" name="severity"> + {% for severity in severities %} + <option value="{{ severity }}">{{ severity }}</option> + {% endfor %} + </select> + </div> + <div class="field"> + <label for="bug-details-edit-target">Scheduled for:</label> + <select id="bug-details-edit-target" name="target"> + <option value="None">Unscheduled</option> + {% for target in targets %} + <option value="{{ target|e }}">{{ target }}</option> + {% endfor %} + </select> + </div> + <div class="field"> + <label for="bug-details-edit-assignee">Assigned to:</label> + <select id="bug-details-edit-assignee" name="assignee"> + <option value="None">Unassigned</option> + {% for assignee in assignees %} + <option value="{{ assignee|e }}">{{ assignee|e }}</option> + {% endfor %} + </select> + </div> + <div class="buttons"> + <button type="submit">Save Changes</button> + <button type="reset">Discard Changes</button> + </div> + </fieldset> + </form> + + <h3 class="header-with-link">Summary</h3> + <span class="header-link"> + <a href="" id="edit-bug-summary">edit</a> + </span> + <p id="bug-summary"> + {{ bug.summary }} + </p> + + <form id="bug-summary-edit-form" class="vertical" action="/edit" method="post"> + <fieldset> + <input type="hidden" name="id" value="{{ bug.uuid }}" /> + <div class="field"> + <input type="text" class="text" id="bug-summary-edit-body" name="summary" value="{{ bug.summary }}" /> + </div> + <div class="buttons"> + <button type="submit">Save Changes</button> + <button type="reset">Discard Changes</button> + </div> + </fieldset> + </form> + + <h3>Comments</h3> + {% for comment in bug.comments() %} + <div class="bug-comment"> + <h4 class="bug-comment-header">{{ comment.From|striptags|e }} said:</h4> + <p class="bug-comment-body">{{ comment.body|trim|e }}</p> + <p class="bug-comment-footer">on {{ comment.time|datetimeformat }}</p> + </div> + {% endfor %} + <form id="add-comment-form" class="vertical" action="/comment" method="post"> + <fieldset> + <input type="hidden" name="id" value="{{ bug.uuid }}" /> + <div class="field"> + <textarea cols="60" rows="6" id="add-comment-body" name="body"></textarea> + </div> + <div class="buttons"> + <button type="submit">Submit</button> + </div> + </fieldset> + </form> + <p id="add-comment-link"><a href="" id="add-comment">+ Add a comment</a></p> +{% endblock %}
\ No newline at end of file diff --git a/interfaces/web/templates/list.html b/interfaces/web/templates/list.html new file mode 100644 index 0000000..1d409f7 --- /dev/null +++ b/interfaces/web/templates/list.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block page_title %} + {{ label }} +{% endblock %} + +{% block content %} + <table id="bug-list"> + <tr> + <th>ID</th> + <th>Summary</th> + <th>Status</th> + <th>Target</th> + <th>Assigned To</th> + </tr> + {% for bug in bugs %} + <tr> + <td>{{ bd.bug_shortname(bug) }}</td> + <td><a href="/bug?id={{ bd.bug_shortname(bug)}}"> + {{ bug.summary|e|truncate(70) }}</a></td> + <td>{{ bug.status }}</td> + <td>{{ bug.target }}</td> + <td>{{ bug.assigned|striptags }}</td> + </tr> + {% endfor %} + </table> +{% endblock %}
\ No newline at end of file diff --git a/interfaces/web/web.py b/interfaces/web/web.py new file mode 100644 index 0000000..9155c97 --- /dev/null +++ b/interfaces/web/web.py @@ -0,0 +1,153 @@ +import cherrypy +from libbe import bugdir, settings_object +from jinja2 import Environment, FileSystemLoader +from datetime import datetime + +EMPTY = settings_object.EMPTY + +def datetimeformat(value, format='%B %d, %Y at %I:%M %p'): + """Takes a timestamp and revormats it into a human-readable string.""" + return datetime.fromtimestamp(value).strftime(format) + + +class WebInterface: + """The web interface to CFBE.""" + + def __init__(self, bug_root, template_root): + """Initialize the bug repository for this web interface.""" + self.bug_root = bug_root + self.bd = bugdir.BugDir(root=self.bug_root) + self.repository_name = self.bd.root.split('/')[-1] + self.env = Environment(loader=FileSystemLoader(template_root)) + self.env.filters['datetimeformat'] = datetimeformat + + def get_common_information(self): + """Returns a dict of common information that most pages will need.""" + possible_assignees = list(set( + [unicode(bug.assigned) for bug in self.bd if bug.assigned != EMPTY])) + possible_assignees.sort(key=unicode.lower) + + possible_targets = list(set( + [unicode(bug.target) for bug in self.bd if bug.target != EMPTY])) + possible_targets.sort(key=unicode.lower) + + possible_statuses = [u'open', u'assigned', u'test', u'unconfirmed', + u'closed', u'disabled', u'fixed', u'wontfix'] + + possible_severities = [u'minor', u'serious', u'critical', u'fatal', + u'wishlist'] + + return {'possible_assignees': possible_assignees, + 'possible_targets': possible_targets, + 'possible_statuses': possible_statuses, + 'possible_severities': possible_severities, + 'repository_name': self.repository_name,} + + def filter_bugs(self, status, assignee, target): + """Filter the list of bugs to return only those desired.""" + bugs = [bug for bug in self.bd if bug.status in status] + + if assignee != '': + assignee = EMPTY if assignee == 'None' else assignee + bugs = [bug for bug in bugs if bug.assigned == assignee] + + if target != '': + target = None if target == 'None' else target + bugs = [bug for bug in bugs if bug.target == target] + + return bugs + + + @cherrypy.expose + def index(self, status='open', assignee='', target=''): + """The main bug page. + Bugs can be filtered by assignee or target. + The bug database will be reloaded on each visit.""" + + self.bd.load_all_bugs() + + if status == 'open': + status = ['open', 'assigned', 'test', 'unconfirmed', 'wishlist'] + label = 'All Open Bugs' + elif status == 'closed': + status = ['closed', 'disabled', 'fixed', 'wontfix'] + label = 'All Closed Bugs' + + if assignee != '': + label += ' Currently Unassigned' if assignee == 'None' \ + else ' Assigned to %s' % (assignee,) + if target != '': + label += ' Currently Unschdeuled' if target == 'None' \ + else ' Scheduled for %s' % (target,) + + template = self.env.get_template('list.html') + bugs = self.filter_bugs(status, assignee, target) + + common_info = self.get_common_information() + return template.render(bugs=bugs, bd=self.bd, label=label, + assignees=common_info['possible_assignees'], + targets=common_info['possible_targets'], + statuses=common_info['possible_statuses'], + severities=common_info['possible_severities'], + repository_name=common_info['repository_name']) + + + @cherrypy.expose + def bug(self, id=''): + """The page for viewing a single bug.""" + + self.bd.load_all_bugs() + + bug = self.bd.bug_from_shortname(id) + + template = self.env.get_template('bug.html') + common_info = self.get_common_information() + return template.render(bug=bug, bd=self.bd, + assignee='' if bug.assigned == EMPTY else bug.assigned, + target='' if bug.target == EMPTY else bug.target, + assignees=common_info['possible_assignees'], + targets=common_info['possible_targets'], + statuses=common_info['possible_statuses'], + severities=common_info['possible_severities'], + repository_name=common_info['repository_name']) + + + @cherrypy.expose + def create(self, summary): + """The view that handles the creation of a new bug.""" + if summary.strip() != '': + self.bd.new_bug(summary=summary).save() + raise cherrypy.HTTPRedirect('/', status=302) + + + @cherrypy.expose + def comment(self, id, body): + """The view that handles adding a comment.""" + bug = self.bd.bug_from_uuid(id) + shortname = self.bd.bug_shortname(bug) + + if body.strip() != '': + bug.comment_root.new_reply(body=body) + bug.save() + + raise cherrypy.HTTPRedirect('/bug?id=%s' % (shortname,), status=302) + + + @cherrypy.expose + def edit(self, id, status=None, target=None, assignee=None, severity=None, summary=None): + """The view that handles editing bug details.""" + bug = self.bd.bug_from_uuid(id) + shortname = self.bd.bug_shortname(bug) + + if summary != None: + bug.summary = summary + else: + bug.status = status if status != 'None' else None + bug.target = target if target != 'None' else None + bug.assigned = assignee if assignee != 'None' else None + bug.severity = severity if severity != 'None' else None + + bug.save() + + raise cherrypy.HTTPRedirect('/bug?id=%s' % (shortname,), status=302) + |