diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py
index 50cd90c4dcca11d9a69076be33364dba63e56d3a..c2aeafef6fec1129d52186001cbc43c88f815b3a 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.34 2001-12-17 03:52:48 richard Exp $
+# $Id: roundupdb.py,v 1.41 2002-01-15 00:12:40 richard Exp $
__doc__ = """
Extending hyperdb with types specific to issue-tracking.
"""
-import re, os, smtplib, socket, copy
+import re, os, smtplib, socket, copy, time, random
import mimetools, MimeWriter, cStringIO
import base64, mimetypes
import hyperdb, date
# set to indicate to roundup not to actually _send_ email
-ROUNDUPDBSENDMAILDEBUG = os.environ.get('ROUNDUPDBSENDMAILDEBUG', '')
+# this var must contain a file to write the mail to
+SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
class DesignatorError(ValueError):
pass
users = self.user.stringFind(username=address)
# couldn't match address or username, so create a new user
- return self.user.create(username=address, address=address,
- realname=realname)
+ if create:
+ print 'CREATING USER', address
+ return self.user.create(username=address, address=address,
+ realname=realname)
+ else:
+ return 0
_marker = []
# XXX: added the 'creator' faked attribute
raise KeyError, '"creation" and "activity" are reserved'
for audit in self.auditors['set']:
audit(self.db, self, nodeid, propvalues)
- # take a copy of the node dict so that the subsequent set
- # operation doesn't modify the oldvalues structure
- oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
+ # Take a copy of the node dict so that the subsequent set
+ # operation doesn't modify the oldvalues structure.
+ try:
+ # try not using the cache initially
+ oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid,
+ cache=0))
+ except IndexError:
+ # this will be needed if somone does a create() and set()
+ # with no intervening commit()
+ oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
hyperdb.Class.set(self, nodeid, **propvalues)
for react in self.reactors['set']:
react(self.db, self, nodeid, oldvalues)
for react in self.reactors['retire']:
react(self.db, self, nodeid, None)
- def get(self, nodeid, propname, default=_marker):
+ def get(self, nodeid, propname, default=_marker, cache=1):
"""Attempts to get the "creation" or "activity" properties should
do the right thing.
"""
return None
return self.db.user.lookup(name)
if default is not _marker:
- return hyperdb.Class.get(self, nodeid, propname, default)
+ return hyperdb.Class.get(self, nodeid, propname, default,
+ cache=cache)
else:
- return hyperdb.Class.get(self, nodeid, propname)
+ return hyperdb.Class.get(self, nodeid, propname, cache=cache)
def getprops(self, protected=1):
"""In addition to the actual properties on the node, these
def audit(self, event, detector):
"""Register a detector
"""
- self.auditors[event].append(detector)
+ l = self.auditors[event]
+ if detector not in l:
+ self.auditors[event].append(detector)
def react(self, event, detector):
"""Register a detector
"""
- self.reactors[event].append(detector)
+ l = self.reactors[event]
+ if detector not in l:
+ self.reactors[event].append(detector)
class FileClass(Class):
self.db.storefile(self.classname, newid, None, content)
return newid
- def get(self, nodeid, propname, default=_marker):
+ def get(self, nodeid, propname, default=_marker, cache=1):
''' trap the content propname and get it from the file
'''
if propname == 'content':
return self.db.getfile(self.classname, nodeid, None)
if default is not _marker:
- return Class.get(self, nodeid, propname, default)
+ return Class.get(self, nodeid, propname, default, cache=cache)
else:
- return Class.get(self, nodeid, propname)
+ return Class.get(self, nodeid, propname, cache=cache)
def getprops(self, protected=1):
''' In addition to the actual properties on the node, these methods
# XXX deviation from spec - was called ItemClass
class IssueClass(Class):
- # configuration
- MESSAGES_TO_AUTHOR = 'no'
- INSTANCE_NAME = 'Roundup issue tracker'
- EMAIL_SIGNATURE_POSITION = 'bottom'
# Overridden methods:
These users are then added to the message's "recipients" list.
"""
+ users = self.db.user
+ messages = self.db.msg
+ files = self.db.file
+
# figure the recipient ids
- recipients = self.db.msg.get(msgid, 'recipients')
+ sendto = []
r = {}
- for recipid in recipients:
+ recipients = messages.get(msgid, 'recipients')
+ for recipid in messages.get(msgid, 'recipients'):
r[recipid] = 1
- rlen = len(recipients)
# figure the author's id, and indicate they've received the message
- authid = self.db.msg.get(msgid, 'author')
+ authid = messages.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
- self.db.user.get(authid, 'username') != 'anonymous'):
- if not r.has_key(authid):
- recipients.append(authid)
+ # possibly send the message to the author, as long as they aren't
+ # anonymous
+ if (self.db.config.MESSAGES_TO_AUTHOR == 'yes' and
+ users.get(authid, 'username') != 'anonymous'):
+ sendto.append(authid)
r[authid] = 1
# now figure the nosy people who weren't recipients
# Don't send nosy mail to the anonymous user (that user
# shouldn't appear in the nosy list, but just in case they
# do...)
- if self.db.user.get(nosyid, 'username') == 'anonymous': continue
+ if users.get(nosyid, 'username') == 'anonymous':
+ continue
+ # make sure they haven't seen the message already
if not r.has_key(nosyid):
+ # send it to them
+ sendto.append(nosyid)
recipients.append(nosyid)
# no new recipients
- if rlen == len(recipients):
+ if not sendto:
return
+ # determine the messageid and inreplyto of the message
+ inreplyto = messages.get(msgid, 'inreplyto')
+ messageid = messages.get(msgid, 'messageid')
+ if not messageid:
+ # this is an old message that didn't get a messageid, so
+ # create one
+ messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
+ self.classname, nodeid, self.db.config.MAIL_DOMAIN)
+ messages.set(msgid, messageid=messageid)
+
# update the message's recipients list
- self.db.msg.set(msgid, recipients=recipients)
+ messages.set(msgid, recipients=recipients)
# send an email to the people who missed out
- sendto = [self.db.user.get(i, 'address') for i in recipients]
+ sendto = [users.get(i, 'address') for i in sendto]
cn = self.classname
title = self.get(nodeid, 'title') or '%s message copy'%cn
# figure author information
- authname = self.db.user.get(authid, 'realname')
+ authname = users.get(authid, 'realname')
if not authname:
- authname = self.db.user.get(authid, 'username')
- authaddr = self.db.user.get(authid, 'address')
+ authname = users.get(authid, 'username')
+ authaddr = users.get(authid, 'address')
if authaddr:
authaddr = ' <%s>'%authaddr
else:
m = ['']
# put in roundup's signature
- if self.EMAIL_SIGNATURE_POSITION == 'top':
+ if self.db.config.EMAIL_SIGNATURE_POSITION == 'top':
m.append(self.email_signature(nodeid, msgid))
# add author information
m.append('')
# add the content
- m.append(self.db.msg.get(msgid, 'content'))
+ m.append(messages.get(msgid, 'content'))
# add the change note
if change_note:
m.append(change_note)
# put in roundup's signature
- if self.EMAIL_SIGNATURE_POSITION == 'bottom':
+ if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom':
m.append(self.email_signature(nodeid, msgid))
# get the files for this message
- files = self.db.msg.get(msgid, 'files')
+ message_files = messages.get(msgid, 'files')
# create the message
message = cStringIO.StringIO()
writer = MimeWriter.MimeWriter(message)
writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, title))
writer.addheader('To', ', '.join(sendto))
- writer.addheader('From', '%s <%s>'%(authname, self.ISSUE_TRACKER_EMAIL))
- writer.addheader('Reply-To', '%s <%s>'%(self.INSTANCE_NAME,
- self.ISSUE_TRACKER_EMAIL))
+ writer.addheader('From', '%s <%s>'%(authname,
+ self.db.config.ISSUE_TRACKER_EMAIL))
+ writer.addheader('Reply-To', '%s <%s>'%(self.db.config.INSTANCE_NAME,
+ self.db.config.ISSUE_TRACKER_EMAIL))
writer.addheader('MIME-Version', '1.0')
+ if messageid:
+ writer.addheader('Message-Id', messageid)
+ if inreplyto:
+ writer.addheader('In-Reply-To', inreplyto)
# attach files
- if files:
+ if message_files:
part = writer.startmultipartbody('mixed')
part = writer.nextpart()
body = part.startbody('text/plain')
body.write('\n'.join(m))
- for fileid in files:
- name = self.db.file.get(fileid, 'name')
- mime_type = self.db.file.get(fileid, 'type')
- content = self.db.file.get(fileid, 'content')
+ for fileid in message_files:
+ name = files.get(fileid, 'name')
+ mime_type = files.get(fileid, 'type')
+ content = files.get(fileid, 'content')
part = writer.nextpart()
if mime_type == 'text/plain':
part.addheader('Content-Disposition',
body.write('\n'.join(m))
# now try to send the message
- try:
- if ROUNDUPDBSENDMAILDEBUG:
- print 'From: %s\nTo: %s\n%s\n=-=-=-=-=-=-=-='%(
- self.ADMIN_EMAIL, sendto, message.getvalue())
- else:
- smtp = smtplib.SMTP(self.MAILHOST)
- # send the message as admin so bounces are sent there instead
- # of to roundup
- smtp.sendmail(self.ADMIN_EMAIL, sendto, message.getvalue())
- except socket.error, value:
- raise MessageSendError, \
- "Couldn't send confirmation email: mailhost %s"%value
- except smtplib.SMTPException, value:
- raise MessageSendError, \
- "Couldn't send confirmation email: %s"%value
+ if SENDMAILDEBUG:
+ open(SENDMAILDEBUG, 'w').write('FROM: %s\nTO: %s\n%s\n'%(
+ self.db.config.ADMIN_EMAIL,', '.join(sendto),message.getvalue()))
+ else:
+ try:
+ # send the message as admin so bounces are sent there
+ # instead of to roundup
+ smtp = smtplib.SMTP(self.db.config.MAILHOST)
+ smtp.sendmail(self.db.config.ADMIN_EMAIL, sendto,
+ message.getvalue())
+ except socket.error, value:
+ raise MessageSendError, \
+ "Couldn't send confirmation email: mailhost %s"%value
+ except smtplib.SMTPException, value:
+ raise MessageSendError, \
+ "Couldn't send confirmation email: %s"%value
def email_signature(self, nodeid, msgid):
''' Add a signature to the e-mail with some useful information
'''
- web = self.ISSUE_TRACKER_WEB + 'issue'+ nodeid
- email = '"%s" <%s>'%(self.INSTANCE_NAME, self.ISSUE_TRACKER_EMAIL)
+ web = self.db.config.ISSUE_TRACKER_WEB + 'issue'+ nodeid
+ email = '"%s" <%s>'%(self.db.config.INSTANCE_NAME,
+ self.db.config.ISSUE_TRACKER_EMAIL)
line = '_' * max(len(web), len(email))
return '%s\n%s\n%s\n%s'%(line, email, web, line)
+ def generateCreateNote(self, nodeid):
+ """Generate a create note that lists initial property values
+ """
+ cn = self.classname
+ cl = self.db.classes[cn]
+ props = cl.getprops(protected=0)
+
+ # list the values
+ m = []
+ l = props.items()
+ l.sort()
+ for propname, prop in l:
+ value = cl.get(nodeid, propname, None)
+ # skip boring entries
+ if not value:
+ continue
+ if isinstance(prop, hyperdb.Link):
+ link = self.db.classes[prop.classname]
+ if value:
+ key = link.labelprop(default_to_id=1)
+ if key:
+ value = link.get(value, key)
+ else:
+ value = ''
+ elif isinstance(prop, hyperdb.Multilink):
+ if value is None: value = []
+ l = []
+ link = self.db.classes[prop.classname]
+ key = link.labelprop(default_to_id=1)
+ if key:
+ value = [link.get(entry, key) for entry in value]
+ value = ', '.join(value)
+ m.append('%s: %s'%(propname, value))
+ m.insert(0, '----------')
+ m.insert(0, '')
+ return '\n'.join(m)
+
def generateChangeNote(self, nodeid, oldvalues):
"""Generate a change note that lists property changes
"""
#
# $Log: not supported by cvs2svn $
+# Revision 1.40 2002/01/14 22:21:38 richard
+# #503353 ] setting properties in initial email
+#
+# Revision 1.39 2002/01/14 02:20:15 richard
+# . changed all config accesses so they access either the instance or the
+# config attriubute on the db. This means that all config is obtained from
+# instance_config instead of the mish-mash of classes. This will make
+# switching to a ConfigParser setup easier too, I hope.
+#
+# At a minimum, this makes migration a _little_ easier (a lot easier in the
+# 0.5.0 switch, I hope!)
+#
+# Revision 1.38 2002/01/10 05:57:45 richard
+# namespace clobberation
+#
+# Revision 1.37 2002/01/08 04:12:05 richard
+# Changed message-id format to "<%s.%s.%s%s@%s>" so it complies with RFC822
+#
+# Revision 1.36 2002/01/02 02:31:38 richard
+# Sorry for the huge checkin message - I was only intending to implement #496356
+# but I found a number of places where things had been broken by transactions:
+# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
+# for _all_ roundup-generated smtp messages to be sent to.
+# . the transaction cache had broken the roundupdb.Class set() reactors
+# . newly-created author users in the mailgw weren't being committed to the db
+#
+# Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
+# on when I found that stuff :):
+# . #496356 ] Use threading in messages
+# . detectors were being registered multiple times
+# . added tests for mailgw
+# . much better attaching of erroneous messages in the mail gateway
+#
+# Revision 1.35 2001/12/20 15:43:01 rochecompaan
+# Features added:
+# . Multilink properties are now displayed as comma separated values in
+# a textbox
+# . The add user link is now only visible to the admin user
+# . Modified the mail gateway to reject submissions from unknown
+# addresses if ANONYMOUS_ACCESS is denied
+#
+# Revision 1.34 2001/12/17 03:52:48 richard
+# Implemented file store rollback. As a bonus, the hyperdb is now capable of
+# storing more than one file per node - if a property name is supplied,
+# the file is called designator.property.
+# I decided not to migrate the existing files stored over to the new naming
+# scheme - the FileClass just doesn't specify the property name.
+#
# Revision 1.33 2001/12/16 10:53:37 richard
# take a copy of the node dict so that the subsequent set
# operation doesn't modify the oldvalues structure