X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Froundupdb.py;h=57e678da6ed9cbc74b4a2d7b412913e93d0c21e8;hb=a71f2c4589d3fea807be59b00862b57c00afa055;hp=f870bb99a0e4b75e60accffc5d66b1dfdebaf2f5;hpb=38c65e9e3806b6813aa2a7f15c782731aaddb983;p=roundup.git diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py index f870bb9..57e678d 100644 --- a/roundup/roundupdb.py +++ b/roundup/roundupdb.py @@ -15,32 +15,27 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundupdb.py,v 1.52 2002-05-15 03:27:16 richard Exp $ +# $Id: roundupdb.py,v 1.62 2002-07-14 02:05:53 richard Exp $ __doc__ = """ Extending hyperdb with types specific to issue-tracking. """ -import re, os, smtplib, socket, copy, time, random +import re, os, smtplib, socket, time, random import MimeWriter, cStringIO import base64, quopri, mimetypes +# if available, use the 'email' module, otherwise fallback to 'rfc822' +try : + from email.Utils import dump_address_pair as straddr +except ImportError : + from rfc822 import dump_address_pair as straddr -import hyperdb, date +import hyperdb # set to indicate to roundup not to actually _send_ email # this var must contain a file to write the mail to SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '') -class DesignatorError(ValueError): - pass -def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')): - ''' Take a foo123 and return ('foo', 123) - ''' - m = dre.match(designator) - if m is None: - raise DesignatorError, '"%s" not a node designator'%designator - return m.group(1), m.group(2) - def extractUserFromList(userClass, users): '''Given a list of users, try to extract the first non-anonymous user @@ -80,7 +75,7 @@ class Database: # try the user alternate addresses if possible props = self.user.getprops() if props.has_key('alternate_addresses'): - users = self.user.filter({'alternate_addresses': address}, + users = self.user.filter(None, {'alternate_addresses': address}, [], []) user = extractUserFromList(self.user, users) if user is not None: return user @@ -97,163 +92,6 @@ class Database: else: return 0 -_marker = [] -# XXX: added the 'creator' faked attribute -class Class(hyperdb.Class): - # Overridden methods: - def __init__(self, db, classname, **properties): - if (properties.has_key('creation') or properties.has_key('activity') - or properties.has_key('creator')): - raise ValueError, '"creation", "activity" and "creator" are reserved' - hyperdb.Class.__init__(self, db, classname, **properties) - self.auditors = {'create': [], 'set': [], 'retire': []} - self.reactors = {'create': [], 'set': [], 'retire': []} - - def create(self, **propvalues): - """These operations trigger detectors and can be vetoed. Attempts - to modify the "creation" or "activity" properties cause a KeyError. - """ - if propvalues.has_key('creation') or propvalues.has_key('activity'): - raise KeyError, '"creation" and "activity" are reserved' - for audit in self.auditors['create']: - audit(self.db, self, None, propvalues) - nodeid = hyperdb.Class.create(self, **propvalues) - for react in self.reactors['create']: - react(self.db, self, nodeid, None) - return nodeid - - def set(self, nodeid, **propvalues): - """These operations trigger detectors and can be vetoed. Attempts - to modify the "creation" or "activity" properties cause a KeyError. - """ - if propvalues.has_key('creation') or propvalues.has_key('activity'): - 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. - 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) - - def retire(self, nodeid): - """These operations trigger detectors and can be vetoed. Attempts - to modify the "creation" or "activity" properties cause a KeyError. - """ - for audit in self.auditors['retire']: - audit(self.db, self, nodeid, None) - hyperdb.Class.retire(self, nodeid) - for react in self.reactors['retire']: - react(self.db, self, nodeid, None) - - def get(self, nodeid, propname, default=_marker, cache=1): - """Attempts to get the "creation" or "activity" properties should - do the right thing. - """ - if propname == 'creation': - journal = self.db.getjournal(self.classname, nodeid) - if journal: - return self.db.getjournal(self.classname, nodeid)[0][1] - else: - # on the strange chance that there's no journal - return date.Date() - if propname == 'activity': - journal = self.db.getjournal(self.classname, nodeid) - if journal: - return self.db.getjournal(self.classname, nodeid)[-1][1] - else: - # on the strange chance that there's no journal - return date.Date() - if propname == 'creator': - journal = self.db.getjournal(self.classname, nodeid) - if journal: - name = self.db.getjournal(self.classname, nodeid)[0][2] - else: - return None - return self.db.user.lookup(name) - if default is not _marker: - return hyperdb.Class.get(self, nodeid, propname, default, - cache=cache) - else: - return hyperdb.Class.get(self, nodeid, propname, cache=cache) - - def getprops(self, protected=1): - """In addition to the actual properties on the node, these - methods provide the "creation" and "activity" properties. If the - "protected" flag is true, we include protected properties - those - which may not be modified. - """ - d = hyperdb.Class.getprops(self, protected=protected).copy() - if protected: - d['creation'] = hyperdb.Date() - d['activity'] = hyperdb.Date() - d['creator'] = hyperdb.Link("user") - return d - - # - # Detector interface - # - def audit(self, event, detector): - """Register a detector - """ - l = self.auditors[event] - if detector not in l: - self.auditors[event].append(detector) - - def react(self, event, detector): - """Register a detector - """ - l = self.reactors[event] - if detector not in l: - self.reactors[event].append(detector) - - -class FileClass(Class): - def create(self, **propvalues): - ''' snaffle the file propvalue and store in a file - ''' - content = propvalues['content'] - del propvalues['content'] - newid = Class.create(self, **propvalues) - self.db.storefile(self.classname, newid, None, content) - return newid - - def get(self, nodeid, propname, default=_marker, cache=1): - ''' trap the content propname and get it from the file - ''' - - poss_msg = 'Possibly a access right configuration problem.' - if propname == 'content': - try: - return self.db.getfile(self.classname, nodeid, None) - except IOError, (strerror): - # BUG: by catching this we donot see an error in the log. - return 'ERROR reading file: %s%s\n%s\n%s'%( - self.classname, nodeid, poss_msg, strerror) - if default is not _marker: - return Class.get(self, nodeid, propname, default, cache=cache) - else: - return Class.get(self, nodeid, propname, cache=cache) - - def getprops(self, protected=1): - ''' In addition to the actual properties on the node, these methods - provide the "content" property. If the "protected" flag is true, - we include protected properties - those which may not be - modified. - ''' - d = Class.getprops(self, protected=protected).copy() - if protected: - d['content'] = hyperdb.String() - return d - class MessageSendError(RuntimeError): pass @@ -261,29 +99,19 @@ class DetectorError(RuntimeError): pass # XXX deviation from spec - was called ItemClass -class IssueClass(Class): - - # Overridden methods: - - def __init__(self, db, classname, **properties): - """The newly-created class automatically includes the "messages", - "files", "nosy", and "superseder" properties. If the 'properties' - dictionary attempts to specify any of these properties or a - "creation" or "activity" property, a ValueError is raised.""" - if not properties.has_key('title'): - properties['title'] = hyperdb.String() - if not properties.has_key('messages'): +class IssueClass: + """ This class is intended to be mixed-in with a hyperdb backend + implementation. The backend should provide a mechanism that + enforces the title, messages, files, nosy and superseder + properties: + properties['title'] = hyperdb.String(indexme='yes') properties['messages'] = hyperdb.Multilink("msg") - if not properties.has_key('files'): properties['files'] = hyperdb.Multilink("file") - if not properties.has_key('nosy'): properties['nosy'] = hyperdb.Multilink("user") - if not properties.has_key('superseder'): properties['superseder'] = hyperdb.Multilink(classname) - Class.__init__(self, db, classname, **properties) + """ # New methods: - def addmessage(self, nodeid, summary, text): """Add a message to an issue's mail spool. @@ -297,7 +125,7 @@ class IssueClass(Class): appended to the "messages" field of the specified issue. """ - def nosymessage(self, nodeid, msgid, change_note): + def nosymessage(self, nodeid, msgid, oldvalues): """Send a message to the members of an issue's nosy list. The message is sent only to users on the nosy list who are not @@ -318,9 +146,6 @@ class IssueClass(Class): # figure the author's id, and indicate they've received the message authid = messages.get(msgid, 'author') - # get the current nosy list, we'll need it - nosy = self.get(nodeid, 'nosy') - # possibly send the message to the author, as long as they aren't # anonymous if (self.db.config.MESSAGES_TO_AUTHOR == 'yes' and @@ -329,6 +154,7 @@ class IssueClass(Class): 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 @@ -341,16 +167,22 @@ class IssueClass(Class): sendto.append(nosyid) recipients.append(nosyid) + # generate a change note + if oldvalues: + note = self.generateChangeNote(nodeid, oldvalues) + else: + note = self.generateCreateNote(nodeid) + # we have new recipients if sendto: - # map userids to addresses + # map userids to addresses sendto = [users.get(i, 'address') for i in sendto] # update the message's recipients list messages.set(msgid, recipients=recipients) # send the message - self.send_message(nodeid, msgid, change_note, sendto) + self.send_message(nodeid, msgid, note, sendto) # XXX backwards compatibility - don't remove sendmessage = nosymessage @@ -385,7 +217,7 @@ class IssueClass(Class): authname = users.get(authid, 'username') authaddr = users.get(authid, 'address') if authaddr: - authaddr = ' <%s>'%authaddr + authaddr = " <%s>" % straddr( ('',authaddr) ) else: authaddr = '' @@ -431,10 +263,11 @@ class IssueClass(Class): 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.db.config.ISSUE_TRACKER_EMAIL)) - writer.addheader('Reply-To', '%s <%s>'%(self.db.config.INSTANCE_NAME, - self.db.config.ISSUE_TRACKER_EMAIL)) + writer.addheader('From', straddr( + (authname, self.db.config.ISSUE_TRACKER_EMAIL) ) ) + writer.addheader('Reply-To', straddr( + (self.db.config.INSTANCE_NAME, + self.db.config.ISSUE_TRACKER_EMAIL) ) ) writer.addheader('MIME-Version', '1.0') if messageid: writer.addheader('Message-Id', messageid) @@ -502,12 +335,25 @@ class IssueClass(Class): def email_signature(self, nodeid, msgid): ''' Add a signature to the e-mail with some useful information ''' - web = self.db.config.ISSUE_TRACKER_WEB + 'issue'+ nodeid - email = '"%s" <%s>'%(self.db.config.INSTANCE_NAME, - self.db.config.ISSUE_TRACKER_EMAIL) + + # simplistic check to see if the url is valid, + # then append a trailing slash if it is missing + base = self.db.config.ISSUE_TRACKER_WEB + if not isinstance(base , type('')) or not base.startswith('http://'): + base = "Configuration Error: ISSUE_TRACKER_WEB isn't a " \ + "fully-qualified URL" + elif base[-1] != '/' : + base += '/' + web = base + 'issue'+ nodeid + + # ensure the email address is properly quoted + email = straddr((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 """ @@ -549,6 +395,11 @@ class IssueClass(Class): def generateChangeNote(self, nodeid, oldvalues): """Generate a change note that lists property changes """ + if __debug__ : + if not isinstance(oldvalues, type({})) : + raise TypeError("'oldvalues' must be dict-like, not %s."% + type(oldvalues)) + cn = self.classname cl = self.db.classes[cn] changed = {} @@ -625,6 +476,75 @@ class IssueClass(Class): # # $Log: not supported by cvs2svn $ +# Revision 1.61 2002/07/09 04:19:09 richard +# Added reindex command to roundup-admin. +# Fixed reindex on first access. +# Also fixed reindexing of entries that change. +# +# Revision 1.60 2002/07/09 03:02:52 richard +# More indexer work: +# - all String properties may now be indexed too. Currently there's a bit of +# "issue" specific code in the actual searching which needs to be +# addressed. In a nutshell: +# + pass 'indexme="yes"' as a String() property initialisation arg, eg: +# file = FileClass(db, "file", name=String(), type=String(), +# comment=String(indexme="yes")) +# + the comment will then be indexed and be searchable, with the results +# related back to the issue that the file is linked to +# - as a result of this work, the FileClass has a default MIME type that may +# be overridden in a subclass, or by the use of a "type" property as is +# done in the default templates. +# - the regeneration of the indexes (if necessary) is done once the schema is +# set up in the dbinit. +# +# Revision 1.59 2002/06/18 03:55:25 dman13 +# Fixed name/address display problem introduced by an earlier change. +# (instead of "name" display "name ") +# +# Revision 1.58 2002/06/16 01:05:15 dman13 +# Removed temporary workaround -- it seems it was a bug in the +# nosyreaction detector in the 0.4.1 extended template and has already +# been fixed in CVS. We'll see. +# +# Revision 1.57 2002/06/15 15:49:29 dman13 +# Use 'email' instead of 'rfc822', if available. +# Don't use isinstance() on a string (not allowed in python 2.1). +# Return an error message instead of crashing if 'oldvalues' isn't a +# dict (in generateChangeNote). +# +# Revision 1.56 2002/06/14 03:54:21 dman13 +# #565992 ] if ISSUE_TRACKER_WEB doesn't have the trailing '/', add it +# +# use the rfc822 module to ensure that every (oddball) email address and +# real-name is properly quoted +# +# Revision 1.55 2002/06/11 04:58:07 richard +# detabbing +# +# Revision 1.54 2002/05/29 01:16:17 richard +# Sorry about this huge checkin! It's fixing a lot of related stuff in one go +# though. +# +# . #541941 ] changing multilink properties by mail +# . #526730 ] search for messages capability +# . #505180 ] split MailGW.handle_Message +# - also changed cgi client since it was duplicating the functionality +# . build htmlbase if tests are run using CVS checkout (removed note from +# installation.txt) +# . don't create an empty message on email issue creation if the email is empty +# +# Revision 1.53 2002/05/25 07:16:24 rochecompaan +# Merged search_indexing-branch with HEAD +# +# Revision 1.52 2002/05/15 03:27:16 richard +# . fixed SCRIPT_NAME in ZRoundup for instances not at top level of Zope +# (thanks dman) +# . fixed some sorting issues that were breaking some unit tests under py2.2 +# . mailgw test output dir was confusing the init test (but only on 2.2 *shrug*) +# +# fixed bug in the init unit test that meant only the bsddb test ran if it +# could (it clobbered the anydbm test) +# # Revision 1.51 2002/04/08 03:46:42 richard # make it work # @@ -637,6 +557,17 @@ class IssueClass(Class): # The initial detector is one that we'll be using here at ekit - it bounces new # issue messages to a team address. # +# Revision 1.49.2.1 2002/04/19 19:54:42 rochecompaan +# cgi_client.py +# removed search link for the time being +# moved rendering of matches to htmltemplate +# hyperdb.py +# filtering of nodes on full text search incorporated in filter method +# roundupdb.py +# added paramater to call of filter method +# roundup_indexer.py +# added search method to RoundupIndexer class +# # Revision 1.49 2002/03/19 06:41:49 richard # Faster, easier, less mess ;) #