summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: f1e17dc)
raw | patch | inline | side by side (parent: f1e17dc)
author | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Mon, 26 Nov 2001 22:55:56 +0000 (22:55 +0000) | ||
committer | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Mon, 26 Nov 2001 22:55:56 +0000 (22:55 +0000) |
. Added INSTANCE_NAME to configuration - used in web and email to identify
the instance.
. Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
signature info in e-mails.
. Some more flexibility in the mail gateway and more error handling.
. Login now takes you to the page you back to the were denied access to.
Fixed:
. Lots of bugs, thanks Roch�nd others on the devel mailing list!
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@423 57a73879-2fb5-44c3-a270-3262357dd7e2
the instance.
. Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
signature info in e-mails.
. Some more flexibility in the mail gateway and more error handling.
. Login now takes you to the page you back to the were denied access to.
Fixed:
. Lots of bugs, thanks Roch�nd others on the devel mailing list!
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@423 57a73879-2fb5-44c3-a270-3262357dd7e2
14 files changed:
diff --git a/CHANGES.txt b/CHANGES.txt
index 8686a36858ede10816a8e4a41848a8e8075d9b07..33c8566683fada9fafc570324214b79f83b16dcf 100644 (file)
--- a/CHANGES.txt
+++ b/CHANGES.txt
This file contains the changes to the Roundup system over time. The entries
are given with the most recent entry first.
-2001-10-?? - 0.3.0
+2001-11-?? - 0.3.1
+Feature:
+ . Added INSTANCE_NAME to configuration - used in web and email to identify
+ the instance.
+ . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
+ signature info in e-mails.
+ . Some more flexibility in the mail gateway and more error handling.
+ . Login now takes you to the page you back to the were denied access to.
+
+Fixed:
+ . Lots of bugs, thanks Roché and others on the devel mailing list!
+
+
+2001-11-23 - 0.3.0
Feature:
. #467129 ] Lossage when username=e-mail-address
. #473123 ] Change message generation for author
diff --git a/cgi-bin/roundup.cgi b/cgi-bin/roundup.cgi
index e755c08bab4854d88e1495a0a8977a88a5feebde..d327ee3b8d4298e1f8009ccb370536a38a818e32 100755 (executable)
--- a/cgi-bin/roundup.cgi
+++ b/cgi-bin/roundup.cgi
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: roundup.cgi,v 1.19 2001-11-22 00:25:10 richard Exp $
+# $Id: roundup.cgi,v 1.20 2001-11-26 22:55:56 richard Exp $
# python version check
import sys
os.environ['PATH_INFO'] = string.join(path[2:], '/')
request = RequestWrapper(out)
if ROUNDUP_INSTANCE_HOMES.has_key(instance):
- instance_home = ROUNDUP_INSTANCE_HOMES[instance]
- instance = roundup.instance.open(instance_home)
- from roundup import cgi_client
- client = instance.Client(instance, request, os.environ)
- try:
- client.main()
- except cgi_client.Unauthorised:
- request.send_response(403)
- request.send_header('Content-Type', 'text/html')
+ # redirect if we need a trailing '/'
+ if len(path) == 2:
+ request.send_response(301)
+ absolute_url = 'http://%s%s/'%(os.environ['HTTP_HOST'],
+ os.environ['REQUEST_URI'])
+ request.send_header('Location', absolute_url)
request.end_headers()
- out.write('Unauthorised')
- except cgi_client.NotFound:
- request.send_response(404)
- request.send_header('Content-Type', 'text/html')
- request.end_headers()
- out.write('Not found: %s'%client.path)
+ out.write('Moved Permanently')
+ else:
+ instance_home = ROUNDUP_INSTANCE_HOMES[instance]
+ instance = roundup.instance.open(instance_home)
+ from roundup import cgi_client
+ client = instance.Client(instance, request, os.environ)
+ try:
+ client.main()
+ except cgi_client.Unauthorised:
+ request.send_response(403)
+ request.send_header('Content-Type', 'text/html')
+ request.end_headers()
+ out.write('Unauthorised')
+ except cgi_client.NotFound:
+ request.send_response(404)
+ request.send_header('Content-Type', 'text/html')
+ request.end_headers()
+ out.write('Not found: %s'%client.path)
+
else:
import urllib
request.send_response(200)
#
# $Log: not supported by cvs2svn $
+# Revision 1.19 2001/11/22 00:25:10 richard
+# quick fix for file uploads on windows in roundup.cgi
+#
# Revision 1.18 2001/11/06 22:10:11 jhermann
# Added env config; fixed request wrapper & index list; sort list by key
#
diff --git a/roundup-admin b/roundup-admin
index bca9aaf9b214cb87f2d6c7be72eaed20fa9f3229..cde8cee934a84d330485a17bfb30c0d5780e291a 100755 (executable)
--- a/roundup-admin
+++ b/roundup-admin
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: roundup-admin,v 1.46 2001-11-21 03:40:54 richard Exp $
+# $Id: roundup-admin,v 1.47 2001-11-26 22:55:56 richard Exp $
import sys
if int(sys.version[0]) < 2:
properties = cl.getprops()
for key, value in props.items():
- type = properties[key]
- if isinstance(type, hyperdb.String):
+ proptype = properties[key]
+ if isinstance(proptype, hyperdb.String):
continue
- elif isinstance(type, hyperdb.Password):
+ elif isinstance(proptype, hyperdb.Password):
props[key] = password.Password(value)
- elif isinstance(type, hyperdb.Date):
+ elif isinstance(proptype, hyperdb.Date):
try:
props[key] = date.Date(value)
except ValueError, message:
raise UsageError, '"%s": %s'%(value, message)
- elif isinstance(type, hyperdb.Interval):
+ elif isinstance(proptype, hyperdb.Interval):
try:
props[key] = date.Interval(value)
except ValueError, message:
raise UsageError, '"%s": %s'%(value, message)
- elif isinstance(type, hyperdb.Link):
+ elif isinstance(proptype, hyperdb.Link):
props[key] = value
- elif isinstance(type, hyperdb.Multilink):
+ elif isinstance(proptype, hyperdb.Multilink):
props[key] = value.split(',')
# try the set
# make sure it's a link
if (not isinstance(property, hyperdb.Link) and not
- isinstance(type, hyperdb.Multilink)):
+ isinstance(proptype, hyperdb.Multilink)):
raise UsageError, 'You may only "find" link properties'
# get the linked-to class and look up the key property
for key in props.keys():
# get the property
try:
- type = properties[key]
+ proptype = properties[key]
except KeyError:
raise UsageError, '%s has no property "%s"'%(classname, key)
- if isinstance(type, hyperdb.Date):
+ if isinstance(proptype, hyperdb.Date):
try:
props[key] = date.Date(value)
except ValueError, message:
raise UsageError, '"%s": %s'%(value, message)
- elif isinstance(type, hyperdb.Interval):
+ elif isinstance(proptype, hyperdb.Interval):
try:
props[key] = date.Interval(value)
except ValueError, message:
raise UsageError, '"%s": %s'%(value, message)
- elif isinstance(type, hyperdb.Password):
+ elif isinstance(proptype, hyperdb.Password):
props[key] = password.Password(value)
- elif isinstance(type, hyperdb.Multilink):
+ elif isinstance(proptype, hyperdb.Multilink):
props[key] = value.split(',')
# check for the key property
properties = cl.properties.items()
for nodeid in cl.list():
l = []
- for prop, type in properties:
+ for prop, proptype in properties:
value = cl.get(nodeid, prop)
# convert data where needed
- if isinstance(type, hyperdb.Date):
+ if isinstance(proptype, hyperdb.Date):
value = value.get_tuple()
- elif isinstance(type, hyperdb.Interval):
+ elif isinstance(proptype, hyperdb.Interval):
value = value.get_tuple()
- elif isinstance(type, hyperdb.Password):
+ elif isinstance(proptype, hyperdb.Password):
value = str(value)
l.append(repr(value))
from roundup import hyperdb
# ensure that the properties and the CSV file headings match
+ classname = args[0]
try:
cl = self.db.getclass(classname)
except KeyError:
value = eval(l[i])
# Figure the property for this column
key = file_props[i]
- type = cl.properties[key]
+ proptype = cl.properties[key]
# Convert for property type
- if isinstance(type, hyperdb.Date):
+ if isinstance(proptype, hyperdb.Date):
value = date.Date(value)
- elif isinstance(type, hyperdb.Interval):
+ elif isinstance(proptype, hyperdb.Interval):
value = date.Interval(value)
- elif isinstance(type, hyperdb.Password):
+ elif isinstance(proptype, hyperdb.Password):
pwd = password.Password()
pwd.unpack(value)
value = pwd
#
# $Log: not supported by cvs2svn $
+# Revision 1.46 2001/11/21 03:40:54 richard
+# more new property handling
+#
# Revision 1.45 2001/11/12 22:51:59 jhermann
# Fixed option & associated error handling
#
diff --git a/roundup-server b/roundup-server
index a71ce4783814a448c682604d0234a7bf4f559993..4ff953a266105fcfeae3bcc021cddac67db33fef 100755 (executable)
--- a/roundup-server
+++ b/roundup-server
Based on CGIHTTPServer in the Python library.
-$Id: roundup-server,v 1.19 2001-11-12 22:51:04 jhermann Exp $
+$Id: roundup-server,v 1.20 2001-11-26 22:55:56 richard Exp $
"""
import sys
except SystemExit:
raise
except:
- type, value = sys.exc_info()[:2]
- usage('%s: %s'%(type, value))
+ exc_type, exc_value = sys.exc_info()[:2]
+ usage('%s: %s'%(exc_type, exc_value))
# we don't want the cgi module interpreting the command-line args ;)
sys.argv = sys.argv[:1]
#
# $Log: not supported by cvs2svn $
+# Revision 1.19 2001/11/12 22:51:04 jhermann
+# Fixed option & associated error handling
+#
# Revision 1.18 2001/11/01 22:04:37 richard
# Started work on supporting a pop3-fetching server
# Fixed bugs:
diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py
index a4c88e0080b19ba8f9f9112d68028493b759e830..8e80355b082726303b322ff8dfffcac1edfacd0f 100644 (file)
--- a/roundup/cgi_client.py
+++ b/roundup/cgi_client.py
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: cgi_client.py,v 1.62 2001-11-24 00:45:42 jhermann Exp $
+# $Id: cgi_client.py,v 1.63 2001-11-26 22:55:56 richard Exp $
__doc__ = """
WWW request handler (also used in the stand-alone server).
"""
import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
-import binascii, Cookie, time, __builtin__
+import binascii, Cookie, time
import roundupdb, htmltemplate, date, hyperdb, password
from roundup.i18n import _
-# avoid clash with database field "type"
-typeof = __builtin__.type
-
class Unauthorised(ValueError):
pass
ANONYMOUS_ACCESS - one of 'deny', 'allow'
ANONYMOUS_REGISTER - one of 'deny', 'allow'
+ from the roundup class:
+ INSTANCE_NAME - defaults to 'Roundup issue tracker'
+
'''
FILTER_POSITION = 'bottom' # one of 'top', 'bottom', 'top and bottom'
ANONYMOUS_ACCESS = 'deny' # one of 'deny', 'allow'
self.write('<dt><b>Form entries</b></dt>')
for k in self.form.keys():
v = self.form.getvalue(k, "<empty>")
- if typeof(v) is typeof([]):
+ if type(v) is type([]):
# Multiple username fields specified
v = "|".join(v)
self.write('<dd><em>%s</em>=%s</dd>'%(k, cgi.escape(v)))
'''
if self.form.has_key(arg):
arg = self.form[arg]
- if typeof(arg) == typeof([]):
+ if type(arg) == type([]):
return [arg.value for arg in arg]
return arg.value.split(',')
return []
value = self.form[key]
if (isinstance(prop, hyperdb.Link) or
isinstance(prop, hyperdb.Multilink)):
- if typeof(value) == typeof([]):
+ if type(value) == type([]):
value = [arg.value for arg in value]
else:
value = value.value.split(',')
'''
cn = self.classname
- self.pagehead(_('Index of %(classname)s')%{'classname': cn})
+ cl = self.db.classes[cn]
+ self.pagehead(_('%(instancename)s: Index of %(classname)s')%{
+ 'classname': cn, 'instancename': cl.INSTANCE_NAME})
if sort is None: sort = self.index_arg(':sort')
if group is None: group = self.index_arg(':group')
if filter is None: filter = self.index_arg(':filter')
# possibly perform an edit
keys = self.form.keys()
num_re = re.compile('^\d+$')
- if keys:
+ # don't try to set properties if the user has just logged in
+ if keys and not self.form.has_key('__login_name')::
try:
props, changed = parsePropsFromForm(self.db, cl, self.form,
self.nodeid)
- cl.set(self.nodeid, **props)
- self._post_editnode(self.nodeid, changed)
- # and some nice feedback for the user
- message = '%s edited ok'%', '.join(changed)
+
+ # set status to chatting if 'unread' or 'resolved'
+ if 'status' not in changed:
+ try:
+ # determine the id of 'unread','resolved' and 'chatting'
+ unread_id = self.db.status.lookup('unread')
+ resolved_id = self.db.status.lookup('resolved')
+ chatting_id = self.db.status.lookup('chatting')
+ except KeyError:
+ pass
+ else:
+ if (not props.has_key('status') or
+ props['status'] == unread_id or
+ props['status'] == resolved_id):
+ props['status'] = chatting_id
+ changed.append('status')
+ note = None
+ if self.form.has_key('__note'):
+ note = self.form['__note']
+ note = note.value
+ if changed or note:
+ cl.set(self.nodeid, **props)
+ self._post_editnode(self.nodeid, changed)
+ # and some nice feedback for the user
+ message = '%s edited ok'%', '.join(changed)
+ else:
+ message = 'nothing changed'
except:
s = StringIO.StringIO()
traceback.print_exc(None, s)
try:
props, changed = parsePropsFromForm(self.db, user, self.form,
self.nodeid)
+ set_cookie = 0
if self.nodeid == self.getuid() and 'password' in changed:
- set_cookie = self.form['password'].value.strip()
- else:
- set_cookie = 0
+ password = self.form['password'].value.strip()
+ if password:
+ set_cookie = password
+ else:
+ del props['password']
+ del changed[changed.index('password')]
user.set(self.nodeid, **props)
self._post_editnode(self.nodeid, changed)
# and some feedback for the user
'''
nodeid = self.nodeid
cl = self.db.file
- mimetype = cl.get(nodeid, 'type')
- if mimetype == 'message/rfc822':
- mimetype = 'text/plain'
- self.header(headers={'Content-Type': mimetype})
+ mime_type = cl.get(nodeid, 'type')
+ if mime_type == 'message/rfc822':
+ mime_type = 'text/plain'
+ self.header(headers={'Content-Type': mime_type})
self.write(cl.get(nodeid, 'content'))
def _createnode(self):
for key in keys:
if key == ':multilink':
value = self.form[key].value
- if typeof(value) != typeof([]): value = [value]
+ if type(value) != type([]): value = [value]
for value in value:
designator, property = value.split(':')
link, nodeid = roundupdb.splitDesignator(designator)
link.set(nodeid, **{property: value})
elif key == ':link':
value = self.form[key].value
- if typeof(value) != typeof([]): value = [value]
+ if type(value) != type([]): value = [value]
for value in value:
designator, property = value.split(':')
link, nodeid = roundupdb.splitDesignator(designator)
if self.form.has_key('__file'):
file = self.form['__file']
if file.filename:
- type = mimetypes.guess_type(file.filename)[0]
- if not type:
- type = "application/octet-stream"
+ mime_type = mimetypes.guess_type(file.filename)[0]
+ if not mime_type:
+ mime_type = "application/octet-stream"
# create the new file entry
- files.append(self.db.file.create(type=type, name=file.filename,
- content=file.file.read()))
+ files.append(self.db.file.create(type=mime_type,
+ name=file.filename, content=file.file.read()))
# generate an edit message
# don't bother if there's no messages or nosy list
props['messages'].classname == 'msg'):
# handle the note
+ edit_msg = 'This %s has been edited through the web.\n'%cn
if note:
if '\n' in note:
summary = re.split(r'\n\r?', note)[0]
else:
summary = note
- m = ['%s\n'%note]
+ m = [edit_msg + '%s\n'%note]
else:
- summary = 'This %s has been edited through the web.\n'%cn
- m = [summary]
+ m = [edit_msg]
first = 1
for name, prop in props.items():
content = '\n'.join(m)
message_id = self.db.msg.create(author=self.getuid(),
recipients=[], date=date.Date('.'), summary=summary,
- content=content)
+ content=content, files=files)
messages = cl.get(nid, 'messages')
messages.append(message_id)
props = {'messages': messages, 'files': files}
if [i for i in keys if i[0] != ':']:
try:
file = self.form['content']
- type = mimetypes.guess_type(file.filename)[0]
- if not type:
- type = "application/octet-stream"
+ mime_type = mimetypes.guess_type(file.filename)[0]
+ if not mime_type:
+ mime_type = "application/octet-stream"
self._post_editnode(cl.create(content=file.file.read(),
- type=type, name=file.filename))
+ type=mime_type, name=file.filename))
# and some nice feedback for the user
message = '%s created ok'%cn
except:
else:
raise Unauthorised
- def login(self, message=None, newuser_form=None):
+ def login(self, message=None, newuser_form=None, action='index'):
self.pagehead(_('Login to roundup'), message)
self.write('''
<table>
<tr><td colspan=2 class="strong-header">Existing User Login</td></tr>
<form action="login_action" method=POST>
+<input type="hidden" name="__destination_url" value="%s">
<tr><td align=right>Login name: </td>
<td><input name="__login_name"></td></tr>
<tr><td align=right>Password: </td>
<tr><td></td>
<td><input type="submit" value="Log In"></td></tr>
</form>
-''')
+'''%action)
if self.user is None and self.ANONYMOUS_REGISTER == 'deny':
self.write('</table>')
self.pagefoot()
return self.login(message=_('Incorrect password'))
self.set_cookie(self.user, password)
- return self.index()
def set_cookie(self, user, password):
# construct the cookie
self.set_cookie(self.user, self.form['password'].value)
return self.index()
- def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
- nre=re.compile(r'new(\w+)')):
-
+ def main(self):
# determine the uid to use
self.db = self.instance.open('admin')
cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
# now figure which function to call
path = self.split_path
+
+ # default action to index if the path has no information in it
if not path or path[0] in ('', 'index'):
action = 'index'
else:
# everyone is allowed to try to log in
if action == 'login_action':
- return self.login_action()
+ # do the login
+ self.login_action()
+ # figure the resulting page
+ action = self.form['__destination_url'].value
+ if not action:
+ action = 'index'
+ return self.do_action(action)
# allow anonymous people to register
if action == 'newuser_action':
# if we don't have a login and anonymous people aren't allowed to
# register, then spit up the login form
if self.ANONYMOUS_REGISTER == 'deny' and self.user is None:
- return self.login()
- return self.newuser_action()
-
- # make sure totally anonymous access is OK
+ if action == 'login':
+ return self.login() # go to the index after login
+ else:
+ return self.login(action=action)
+ # add the user
+ self.newuser_action()
+ # figure the resulting page
+ action = self.form['__destination_url'].value
+ if not action:
+ action = 'index'
+ return self.do_action(action)
+
+ # no login or registration, make sure totally anonymous access is OK
if self.ANONYMOUS_ACCESS == 'deny' and self.user is None:
- return self.login()
+ if action == 'login':
+ return self.login() # go to the index after login
+ else:
+ return self.login(action=action)
+
+ # just a regular action
+ return self.do_action(action)
+ def do_action(self, action, dre=re.compile(r'([^\d]+)(\d+)'),
+ nre=re.compile(r'new(\w+)')):
# here be the "normal" functionality
if action == 'index':
return self.index()
self.db.getclass(self.classname)
except KeyError:
raise NotFound
- self.list()
+ return self.list()
def __del__(self):
self.db.close()
key, value, link)
elif isinstance(proptype, hyperdb.Multilink):
value = form[key]
- if typeof(value) != typeof([]):
+ if type(value) != type([]):
value = [i.strip() for i in value.value.split(',')]
else:
value = [i.value.strip() for i in value]
#
# $Log: not supported by cvs2svn $
+# Revision 1.62 2001/11/24 00:45:42 jhermann
+# typeof() instead of type(): avoid clash with database field(?) "type"
+#
+# Fixes this traceback:
+#
+# Traceback (most recent call last):
+# File "roundup\cgi_client.py", line 535, in newnode
+# self._post_editnode(nid)
+# File "roundup\cgi_client.py", line 415, in _post_editnode
+# if type(value) != type([]): value = [value]
+# UnboundLocalError: local variable 'type' referenced before assignment
+#
# Revision 1.61 2001/11/22 15:46:42 jhermann
# Added module docstrings to all modules.
#
index a10f9e765630e5c56d1e2d069722e8789a95e05c..042e7e53ea63da840887cb9f7ba906c71390e1d6 100644 (file)
--- a/roundup/htmltemplate.py
+++ b/roundup/htmltemplate.py
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: htmltemplate.py,v 1.46 2001-11-24 00:53:12 jhermann Exp $
+# $Id: htmltemplate.py,v 1.47 2001-11-26 22:55:56 richard Exp $
__doc__ = """
Template engine.
w = self.client.write
+ # wrap the template in a single table to ensure the whole widget
+ # is displayed at once
+ w('<table><tr><td>')
+
if template and filter:
# display the filter section
w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
w('</tr>\n')
w('</table>\n')
+ # and the outer table
+ w('</td></tr></table>')
+
def sortby(self, sort_name, filterspec, columns, filter, group, sort):
l = []
# designators...
w = self.client.write
- w('<form action="%s%s">'%(self.classname, nodeid))
+ w('<form action="%s%s" method="POST" enctype="multipart/form-data">'%(
+ self.classname, nodeid))
s = open(os.path.join(self.templates, self.classname+'.item')).read()
replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
w(replace.go(s))
#
# $Log: not supported by cvs2svn $
+# Revision 1.46 2001/11/24 00:53:12 jhermann
+# "except:" is bad, bad , bad!
+#
# Revision 1.45 2001/11/22 15:46:42 jhermann
# Added module docstrings to all modules.
#
diff --git a/roundup/mailgw.py b/roundup/mailgw.py
index 6274173e836511b409bb31f111892c3badbfad26..8a15ad0890ffbc49df3d8c5cbb6b3848c69fa214 100644 (file)
--- a/roundup/mailgw.py
+++ b/roundup/mailgw.py
an exception, the original message is bounced back to the sender with the
explanatory message given in the exception.
-$Id: mailgw.py,v 1.35 2001-11-22 15:46:42 jhermann Exp $
+$Id: mailgw.py,v 1.36 2001-11-26 22:55:56 richard Exp $
'''
import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
-import traceback
+import traceback, MimeWriter
import hyperdb, date, password
class MailGWError(ValueError):
return Message(s)
subject_re = re.compile(r'(?P<refwd>\s*\W?\s*(fwd|re)\s*\W?\s*)*'
- r'\s*(\[(?P<classname>[^\d]+)(?P<nodeid>\d+)?\])'
- r'\s*(?P<title>[^\[]+)(\[(?P<args>.+?)\])?', re.I)
+ r'\s*(\[(?P<classname>[^\d\s]+)(?P<nodeid>\d+)?\])'
+ r'\s*(?P<title>[^[]+)?(\[(?P<args>.+?)\])?', re.I)
class MailGW:
def __init__(self, instance, db):
errors in a sane manner. It should be replaced if you wish to
handle errors in a different manner.
'''
- m = []
# in some rare cases, a particularly stuffed-up e-mail will make
# its way into here... try to handle it gracefully
sendto = message.getaddrlist('from')
if sendto:
try:
- self.handle_message(message)
- return
+ return self.handle_message(message)
except MailUsageError, value:
# bounce the message back to the sender with the usage message
fulldoc = '\n'.join(string.split(__doc__, '\n')[2:])
sendto = [sendto[0][1]]
- m = ['Subject: Failed issue tracker submission', '']
+ m = ['']
m.append(str(value))
m.append('\n\nMail Gateway Help\n=================')
m.append(fulldoc)
+ m = self.bounce_message(message, sendto, m)
except:
# bounce the message back to the sender with the error message
sendto = [sendto[0][1]]
- m = ['Subject: failed issue tracker submission', '']
- # TODO as attachments?
+ m = ['']
m.append('---- traceback of failure ----')
s = cStringIO.StringIO()
import traceback
traceback.print_exc(None, s)
m.append(s.getvalue())
- m.append('---- failed message follows ----')
- try:
- message.fp.seek(0)
- except:
- pass
- m.append(message.fp.read())
+ m = self.bounce_message(message, sendto, m)
else:
# very bad-looking message - we don't even know who sent it
sendto = [self.ADMIN_EMAIL]
m.append('')
m.append('The mail gateway retrieved a message which has no From:')
m.append('line, indicating that it is corrupt. Please check your')
- m.append('mail gateway source.')
+ m.append('mail gateway source. Failed message is attached.')
m.append('')
- m.append('---- failed message follows ----')
- try:
- message.fp.seek(0)
- except:
- pass
- m.append(message.fp.read())
+ m = self.bounce_message(message, sendto, m,
+ subject='Badly formed message from mail gateway')
# now send the message
try:
smtp = smtplib.SMTP(self.MAILHOST)
- smtp.sendmail(self.ADMIN_EMAIL, sendto, '\n'.join(m))
+ smtp.sendmail(self.ADMIN_EMAIL, sendto, m.getvalue())
except socket.error, value:
raise MailGWError, "Couldn't send confirmation email: "\
"mailhost %s"%value
except smtplib.SMTPException, value:
raise MailGWError, "Couldn't send confirmation email: %s"%value
+ def bounce_message(self, message, sendto, error,
+ subject='Failed issue tracker submission'):
+ ''' create a message that explains the reason for the failed
+ issue submission to the author and attach the original
+ message.
+ '''
+ msg = cStringIO.StringIO()
+ writer = MimeWriter.MimeWriter(msg)
+ writer.addheader('Subject', subject)
+ writer.addheader('From', '%s <%s>'% (self.instance.INSTANCE_NAME,
+ self.ISSUE_TRACKER_EMAIL))
+ writer.addheader('To', ','.join(sendto))
+ writer.addheader('MIME-Version', '1.0')
+ part = writer.startmultipartbody('mixed')
+ part = writer.nextpart()
+ body = part.startbody('text/plain')
+ body.write('\n'.join(error))
+
+ # reconstruct the original message
+ m = cStringIO.StringIO()
+ w = MimeWriter.MimeWriter(m)
+ for header in message.headers:
+ header_name = header.split(':')[0]
+ if message.getheader(header_name):
+ w.addheader(header_name,message.getheader(header_name))
+ body = w.startbody('text/plain')
+ try:
+ message.fp.seek(0)
+ except:
+ pass
+ body.write(message.fp.read())
+
+ # attach the original message to the returned message
+ part = writer.nextpart()
+ part.addheader('Content-Disposition','attachment')
+ part.addheader('Content-Transfer-Encoding', '7bit')
+ body = part.startbody('message/rfc822')
+ body.write(m.getvalue())
+
+ writer.lastpart()
+ return msg
+
def handle_message(self, message):
''' message - a Message instance
Subject was: "%s"
'''%subject
+
+ # get the classname
classname = m.group('classname')
- nodeid = m.group('nodeid')
- title = m.group('title').strip()
- subject_args = m.group('args')
try:
cl = self.db.getclass(classname)
except KeyError:
Subject was: "%s"
'''%(classname, ', '.join(self.db.getclasses()), subject)
+ # get the optional nodeid
+ nodeid = m.group('nodeid')
+
+ # title is optional too
+ title = m.group('title')
+ if title:
+ title = title.strip()
+ else:
+ title = ''
+
+ # but we do need either a title or a nodeid...
+ if not nodeid and not title:
+ raise MailUsageError, '''
+I cannot match your message to a node in the database - you need to either
+supply a full node identifier (with number, eg "[issue123]" or keep the
+previous subject title intact so I can match that.
+
+Subject was: "%s"
+'''%(classname, subject)
+
+ # extract the args
+ subject_args = m.group('args')
+
# If there's no nodeid, check to see if this is a followup and
# maybe someone's responded to the initial mail that created an
# entry. Try to find the matching nodes with the same title, and
Subject was: "%s"
'''%(message, subject)
+ key = key.strip()
try:
- type = properties[key]
+ proptype = properties[key]
except KeyError:
raise MailUsageError, '''
Subject argument list refers to an invalid property: "%s"
Subject was: "%s"
'''%(key, subject)
- if isinstance(type, hyperdb.String):
- props[key] = value
- if isinstance(type, hyperdb.Password):
- props[key] = password.Password(value)
- elif isinstance(type, hyperdb.Date):
+ if isinstance(proptype, hyperdb.String):
+ props[key] = value.strip()
+ if isinstance(proptype, hyperdb.Password):
+ props[key] = password.Password(value.strip())
+ elif isinstance(proptype, hyperdb.Date):
try:
- props[key] = date.Date(value)
+ props[key] = date.Date(value.strip())
except ValueError, message:
raise UsageError, '''
Subject argument list contains an invalid date for %s.
Error was: %s
Subject was: "%s"
'''%(key, message, subject)
- elif isinstance(type, hyperdb.Interval):
+ elif isinstance(proptype, hyperdb.Interval):
try:
- props[key] = date.Interval(value)
+ props[key] = date.Interval(value) # no strip needed
except ValueError, message:
raise UsageError, '''
Subject argument list contains an invalid date interval for %s.
Error was: %s
Subject was: "%s"
'''%(key, message, subject)
- elif isinstance(type, hyperdb.Link):
- props[key] = value
- elif isinstance(type, hyperdb.Multilink):
- props[key] = value.split(',')
+ elif isinstance(proptype, hyperdb.Link):
+ props[key] = value.strip()
+ elif isinstance(proptype, hyperdb.Multilink):
+ props[key] = [x.strip() for x in value.split(',')]
#
# handle the users
# handle the files
files = []
- for (name, type, data) in attachments:
- files.append(self.db.file.create(type=type, name=name,
+ for (name, mime_type, data) in attachments:
+ files.append(self.db.file.create(type=mime_type, name=name,
content=data))
# now handle the db stuff
props['status'] == resolved_id):
props['status'] = chatting_id
+ # add nosy in arguments to issue's nosy list, don't replace
+ if props.has_key('nosy'):
+ n = {}
+ for nid in cl.get(nodeid, 'nosy'):
+ n[nid] = 1
+ for value in props['nosy']:
+ if self.db.hasnode('user', value):
+ nid = value
+ else:
+ try:
+ nid = self.db.user.lookup(value)
+ except:
+ continue
+ if n.has_key(nid): continue
+ n[nid] = 1
+ props['nosy'] = n.keys()
+
+ # now apply the changes
try:
cl.set(nodeid, **props)
except (TypeError, IndexError, ValueError), message:
message_id = self.db.msg.create(author=author,
recipients=recipients, date=date.Date('.'), summary=summary,
content=content, files=files)
- # fill out the properties with defaults where required
- if properties.has_key('assignedto') and \
- not props.has_key('assignedto'):
- props['assignedto'] = '1' # "admin"
# pre-set the issue to unread
if properties.has_key('status') and not props.has_key('status'):
#
# $Log: not supported by cvs2svn $
+# Revision 1.35 2001/11/22 15:46:42 jhermann
+# Added module docstrings to all modules.
+#
# Revision 1.34 2001/11/15 10:24:27 richard
# handle the case where there is no file attached
#
diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py
index 7650cc8e78768f6f3c2a6b63991bef55a5223429..dde58dfec043c673d48d276ed1095d4f87903b17 100644 (file)
--- a/roundup/roundupdb.py
+++ b/roundup/roundupdb.py
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: roundupdb.py,v 1.20 2001-11-25 10:11:14 jhermann Exp $
+# $Id: roundupdb.py,v 1.21 2001-11-26 22:55:56 richard Exp $
__doc__ = """
Extending hyperdb with types specific to issue-tracking.
import re, os, smtplib, socket
import mimetools, MimeWriter, cStringIO
-import binascii, mimetypes
+import base64, mimetypes
import hyperdb, date
class IssueClass(Class):
# configuration
MESSAGES_TO_AUTHOR = 'no'
+ INSTANCE_NAME = 'Roundup issue tracker'
+ EMAIL_SIGNATURE_POSITION = 'bottom'
# Overridden methods:
# figure the author's id, and indicate they've received the message
authid = self.db.msg.get(msgid, 'author')
+ # get the current nosy list, we'll need it
+ nosy = self.get(nodeid, 'nosy')
+
# ... but duplicate the message to the author as long as it's not
# the anonymous user
if (self.MESSAGES_TO_AUTHOR == 'yes' and
r[authid] = 1
# now figure the nosy people who weren't recipients
- nosy = self.get(nodeid, 'nosy')
for nosyid in nosy:
# Don't send nosy mail to the anonymous user (that user
# shouldn't appear in the nosy list, but just in case they
else:
authaddr = ''
# make the message body
- m = []
+ m = ['']
+
+ # put in roundup's signature
+ if self.EMAIL_SIGNATURE_POSITION == 'top':
+ m.append(self.email_signature(nodeid, msgid))
+
# add author information
- m.append("%s %sadded the comment:"%(authname, authaddr))
+ if len(self.db.issue.get(nodeid, 'messages')) == 1:
+ m.append("New submission from %s <%s>:"%(authname, authaddr))
+ else:
+ m.append("%s <%s> added the comment:"%(authname, authaddr))
m.append('')
+
# add the content
m.append(self.db.msg.get(msgid, 'content'))
+
# "list information" footer
- m.append(self.email_footer(nodeid, msgid))
+ if self.EMAIL_SIGNATURE_POSITION == 'bottom':
+ m.append(self.email_signature(nodeid, msgid))
# get the files for this message
files = self.db.msg.get(msgid, 'files')
writer = MimeWriter.MimeWriter(message)
writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, title))
writer.addheader('To', ', '.join(sendto))
- writer.addheader('From', self.ISSUE_TRACKER_EMAIL)
- writer.addheader('Reply-To:', self.ISSUE_TRACKER_EMAIL)
+ writer.addheader('From', '%s <%s>'%(self.INSTANCE_NAME,
+ self.ISSUE_TRACKER_EMAIL))
+ writer.addheader('Reply-To:', '%s <%s>'%(self.INSTANCE_NAME,
+ self.ISSUE_TRACKER_EMAIL))
+ writer.addheader('MIME-Version', '1.0')
+
+ # attach files
if files:
part = writer.startmultipartbody('mixed')
part = writer.nextpart()
body.write('\n'.join(m))
for fileid in files:
name = self.db.file.get(fileid, 'name')
- type = self.db.file.get(fileid, 'type')
+ mime_type = self.db.file.get(fileid, 'type')
content = self.db.file.get(fileid, 'content')
part = writer.nextpart()
- if type == 'text/plain':
+ if mime_type == 'text/plain':
part.addheader('Content-Disposition',
'attachment;\n filename="%s"'%name)
part.addheader('Content-Transfer-Encoding', '7bit')
body = part.startbody('text/plain')
body.write(content)
else:
- type = mimetypes.guess_type(name)[0]
+ # some other type, so encode it
+ if not mime_type:
+ # this should have been done when the file was saved
+ mime_type = mimetypes.guess_type(name)[0]
+ if mime_type is None:
+ mime_type = 'application/octet-stream'
part.addheader('Content-Disposition',
'attachment;\n filename="%s"'%name)
part.addheader('Content-Transfer-Encoding', 'base64')
- body = part.startbody(type)
- body.write(binascii.b2a_base64(content))
+ body = part.startbody(mime_type)
+ body.write(base64.encodestring(content))
writer.lastpart()
else:
body = writer.startbody('text/plain')
raise MessageSendError, \
"Couldn't send confirmation email: %s"%value
- def email_footer(self, nodeid, msgid):
- ''' Add a footer to the e-mail with some useful information
+ def email_signature(self, nodeid, msgid):
+ ''' Add a signature to the e-mail with some useful information
'''
web = self.ISSUE_TRACKER_WEB + 'issue'+ nodeid
return '''%s
-Roundup issue tracker
%s
%s
-'''%('_'*len(web), self.ISSUE_TRACKER_EMAIL, web)
+%s
+'''%('_'*len(web), self.INSTANCE_NAME, self.ISSUE_TRACKER_EMAIL, web,
+ '_'*len(web))
#
# $Log: not supported by cvs2svn $
+# Revision 1.20 2001/11/25 10:11:14 jhermann
+# Typo fix
+#
# Revision 1.19 2001/11/22 15:46:42 jhermann
# Added module docstrings to all modules.
#
index a48553c9050a7bb66f6a8f5442e475cf0b8f3050..bb389d7ae08e250ac1e599cfa6b7aff90a844e15 100644 (file)
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: dbinit.py,v 1.9 2001-10-30 00:54:45 richard Exp $
+# $Id: dbinit.py,v 1.10 2001-11-26 22:55:56 richard Exp $
import os
class IssueClass(roundupdb.IssueClass):
''' issues need the email information
'''
+ INSTANCE_NAME = instance_config.INSTANCE_NAME
ISSUE_TRACKER_WEB = instance_config.ISSUE_TRACKER_WEB
ISSUE_TRACKER_EMAIL = instance_config.ISSUE_TRACKER_EMAIL
ADMIN_EMAIL = instance_config.ADMIN_EMAIL
MAILHOST = instance_config.MAILHOST
MESSAGES_TO_AUTHOR = instance_config.MESSAGES_TO_AUTHOR
+ EMAIL_SIGNATURE_POSITION = instance_config.EMAIL_SIGNATURE_POSITION
def open(name=None):
#
# $Log: not supported by cvs2svn $
+# Revision 1.9 2001/10/30 00:54:45 richard
+# Features:
+# . #467129 ] Lossage when username=e-mail-address
+# . #473123 ] Change message generation for author
+# . MailGW now moves 'resolved' to 'chatting' on receiving e-mail for an issue.
+#
# Revision 1.8 2001/10/09 07:25:59 richard
# Added the Password property type. See "pydoc roundup.password" for
# implementation details. Have updated some of the documentation too.
diff --git a/roundup/templates/classic/detectors/nosyreaction.py b/roundup/templates/classic/detectors/nosyreaction.py
index 876219784d4545e17d4bcc6c10178ed5d763223e..6f84a39498a8ef257b0587f88401d41d6ae6c39e 100644 (file)
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-#$Id: nosyreaction.py,v 1.5 2001-11-12 22:01:07 richard Exp $
+#$Id: nosyreaction.py,v 1.6 2001-11-26 22:55:56 richard Exp $
from roundup import roundupdb
if n.has_key(authid): continue
if db.user.get(authid, 'username') == 'anonymous': continue
change = 1
- nosy.append(authid)
+ # append the author only after issue creation
+ if oldvalues is None:
+ nosy.append(authid)
if change:
cl.set(nodeid, nosy=nosy)
#
#$Log: not supported by cvs2svn $
+#Revision 1.5 2001/11/12 22:01:07 richard
+#Fixed issues with nosy reaction and author copies.
+#
#Revision 1.4 2001/10/30 00:54:45 richard
#Features:
# . #467129 ] Lossage when username=e-mail-address
diff --git a/roundup/templates/classic/instance_config.py b/roundup/templates/classic/instance_config.py
index de3759f3ea23b5de0266e1243c17feb350b47c91..ac2cffea7acc07b7be0730f35a030c9d6e4009cb 100644 (file)
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: instance_config.py,v 1.9 2001-10-30 00:54:45 richard Exp $
+# $Id: instance_config.py,v 1.10 2001-11-26 22:55:56 richard Exp $
MAIL_DOMAIN=MAILHOST=HTTP_HOST=None
HTTP_PORT=0
# This is the directory that the HTML templates reside in
TEMPLATES = os.path.join(INSTANCE_HOME, 'html')
+# A descriptive name for your roundup instance
+INSTANCE_NAME = 'Roundup issue tracker'
+
# The email address that mail to roundup should go to
ISSUE_TRACKER_EMAIL = 'issue_tracker@%s'%MAIL_DOMAIN
# Send nosy messages to the author of the message
MESSAGES_TO_AUTHOR = 'no' # either 'yes' or 'no'
+# Where to place the email signature
+EMAIL_SIGNATURE_POSITION = 'bottom'
+
#
# $Log: not supported by cvs2svn $
+# Revision 1.9 2001/10/30 00:54:45 richard
+# Features:
+# . #467129 ] Lossage when username=e-mail-address
+# . #473123 ] Change message generation for author
+# . MailGW now moves 'resolved' to 'chatting' on receiving e-mail for an issue.
+#
# Revision 1.8 2001/10/23 01:00:18 richard
# Re-enabled login and registration access after lopping them off via
# disabling access for anonymous users.
index 45754cb3984167d1204a2657fc6b7fa1c5ae0c1a..2a92ea7ff15f89b611d332b82aeb87588148251c 100644 (file)
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: dbinit.py,v 1.14 2001-11-21 02:34:18 richard Exp $
+# $Id: dbinit.py,v 1.15 2001-11-26 22:55:56 richard Exp $
import os
class IssueClass(roundupdb.IssueClass):
''' issues need the email information
'''
+ INSTANCE_NAME = instance_config.INSTANCE_NAME
ISSUE_TRACKER_WEB = instance_config.ISSUE_TRACKER_WEB
ISSUE_TRACKER_EMAIL = instance_config.ISSUE_TRACKER_EMAIL
ADMIN_EMAIL = instance_config.ADMIN_EMAIL
MAILHOST = instance_config.MAILHOST
MESSAGES_TO_AUTHOR = instance_config.MESSAGES_TO_AUTHOR
+ EMAIL_SIGNATURE_POSITION = instance_config.EMAIL_SIGNATURE_POSITION
def open(name=None):
#
# $Log: not supported by cvs2svn $
+# Revision 1.14 2001/11/21 02:34:18 richard
+# Added a target version field to the extended issue schema
+#
# Revision 1.13 2001/10/30 00:54:45 richard
# Features:
# . #467129 ] Lossage when username=e-mail-address
diff --git a/roundup/templates/extended/detectors/nosyreaction.py b/roundup/templates/extended/detectors/nosyreaction.py
index ea8f69576c7187347c3b8f302034e5f834ee2f2f..dc7ff83449121099bdc34e541be67a4f349ec533 100644 (file)
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-#$Id: nosyreaction.py,v 1.5 2001-11-12 22:01:07 richard Exp $
+#$Id: nosyreaction.py,v 1.6 2001-11-26 22:55:56 richard Exp $
from roundup import roundupdb
if n.has_key(authid): continue
if db.user.get(authid, 'username') == 'anonymous': continue
change = 1
- nosy.append(authid)
+ # append the author only after issue creation
+ if oldvalues is None:
+ nosy.append(authid)
if change:
cl.set(nodeid, nosy=nosy)
#
#$Log: not supported by cvs2svn $
+#Revision 1.5 2001/11/12 22:01:07 richard
+#Fixed issues with nosy reaction and author copies.
+#
#Revision 1.4 2001/10/30 00:54:45 richard
#Features:
# . #467129 ] Lossage when username=e-mail-address
diff --git a/roundup/templates/extended/instance_config.py b/roundup/templates/extended/instance_config.py
index 78bff5b726be84e8c2f56f7d1bd45bd0f2904be7..97cf108ee4a9f794172bb879e8f857e2392e649e 100644 (file)
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: instance_config.py,v 1.9 2001-10-30 00:54:45 richard Exp $
+# $Id: instance_config.py,v 1.10 2001-11-26 22:55:56 richard Exp $
MAIL_DOMAIN=MAILHOST=HTTP_HOST=None
HTTP_PORT=0
# This is the directory that the HTML templates reside in
TEMPLATES = os.path.join(INSTANCE_HOME, 'html')
+# A descriptive name for your roundup instance
+INSTANCE_NAME = 'Roundup issue tracker'
+
# The email address that mail to roundup should go to
ISSUE_TRACKER_EMAIL = 'issue_tracker@%s'%MAIL_DOMAIN
# Send nosy messages to the author of the message
MESSAGES_TO_AUTHOR = 'no' # either 'yes' or 'no'
+# Where to place the email signature
+EMAIL_SIGNATURE_POSITION = 'bottom'
+
#
# $Log: not supported by cvs2svn $
+# Revision 1.9 2001/10/30 00:54:45 richard
+# Features:
+# . #467129 ] Lossage when username=e-mail-address
+# . #473123 ] Change message generation for author
+# . MailGW now moves 'resolved' to 'chatting' on receiving e-mail for an issue.
+#
# Revision 1.8 2001/10/23 01:00:18 richard
# Re-enabled login and registration access after lopping them off via
# disabling access for anonymous users.