diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py
index 755594c7ebbdc7f63e76042a4593ef590a290611..1b2a385551b31cfaefac752acdca7b1e3bcb8456 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.57 2002-06-15 15:49:29 dman13 Exp $
+# $Id: roundupdb.py,v 1.63 2002-07-26 08:26:59 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'
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
- and return that user, otherwise return None
- '''
- if len(users) > 1:
- # make sure we don't match the anonymous or admin user
- for user in users:
- if user == '1': continue
- if userClass.get(user, 'username') == 'anonymous': continue
- # first valid match will do
- return user
- # well, I guess we have no choice
- return user[0]
- elif users:
- return users[0]
- return None
-
class Database:
def getuid(self):
"""Return the id of the "user" node associated with the user
that owns this connection to the hyperdatabase."""
return self.user.lookup(self.journaltag)
- def uidFromAddress(self, address, create=1):
- ''' address is from the rfc822 module, and therefore is (name, addr)
-
- user is created if they don't exist in the db already
- '''
- (realname, address) = address
-
- # try a straight match of the address
- user = extractUserFromList(self.user,
- self.user.stringFind(address=address))
- if user is not None: return user
-
- # try the user alternate addresses if possible
- props = self.user.getprops()
- if props.has_key('alternate_addresses'):
- users = self.user.filter(None, {'alternate_addresses': address},
- [], [])
- user = extractUserFromList(self.user, users)
- if user is not None: return user
-
- # try to match the username to the address (for local
- # submissions where the address is empty)
- user = extractUserFromList(self.user,
- self.user.stringFind(username=address))
-
- # couldn't match address or username, so create a new user
- if create:
- return self.user.create(username=address, address=address,
- realname=realname)
- 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'
- self.fireAuditors('create', None, propvalues)
- nodeid = hyperdb.Class.create(self, **propvalues)
- self.fireReactors('create', 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'
- self.fireAuditors('set', 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)
- self.fireReactors('set', 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.
- """
- self.fireAuditors('retire', nodeid, None)
- hyperdb.Class.retire(self, nodeid)
- self.fireReactors('retire', 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 fireAuditors(self, action, nodeid, newvalues):
- """Fire all registered auditors.
- """
- for audit in self.auditors[action]:
- audit(self.db, self, nodeid, newvalues)
-
- def react(self, event, detector):
- """Register a detector
- """
- l = self.reactors[event]
- if detector not in l:
- self.reactors[event].append(detector)
-
- def fireReactors(self, action, nodeid, oldvalues):
- """Fire all registered reactors.
- """
- for react in self.reactors[action]:
- react(self.db, self, nodeid, oldvalues)
-
-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
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.
authname = users.get(authid, 'username')
authaddr = users.get(authid, 'address')
if authaddr:
- authaddr = straddr( ('',authaddr) )
+ authaddr = " <%s>" % straddr( ('',authaddr) )
else:
authaddr = ''
# 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
- # Oops, can't do this in python2.1
- #if not isinstance( base , "" ) or not base.startswith( "http://" ) :
- if type(base) != type("") or not base.startswith( "http://" ) :
- base = "Configuration Error: ISSUE_TRACKER_WEB isn't a fully-qualified URL"
+ 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) )
+ 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 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 = {}
props = cl.getprops(protected=0)
- # XXX DSH
- # Temporary work-around to prevent crashes and allow the issue to be
- # submitted.
- try :
- oldvalues.keys
- except AttributeError :
- # The arg isn't a dict. Precondition/interface violation.
- return '\n'.join(
- ('', '-'*10,
- "Precondition/interface Error -- 'oldvalues' isn't a dict." ,
- '-'*10 , '' , str(oldvalues) ) )
-
# determine what changed
for key in oldvalues.keys():
if key in ['files','messages']: continue
#
# $Log: not supported by cvs2svn $
+# Revision 1.62 2002/07/14 02:05:53 richard
+# . all storage-specific code (ie. backend) is now implemented by the backends
+#
+# 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<addr>" display "name <addr>")
+#
+# 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
#
# . 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!
+# . Lots of bugs, thanks Roché and others on the devel mailing list!
#
# Revision 1.20 2001/11/25 10:11:14 jhermann
# Typo fix