Code

Fixed some problems with installation.
[roundup.git] / roundup / roundupdb.py
1 # $Id: roundupdb.py,v 1.3 2001-07-23 07:14:41 richard Exp $
3 import re, os, smtplib, socket
5 import hyperdb, date
7 def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
8     ''' Take a foo123 and return ('foo', 123)
9     '''
10     m = dre.match(designator)
11     return m.group(1), m.group(2)
13 class Database:
14     def getuid(self):
15         """Return the id of the "user" node associated with the user
16         that owns this connection to the hyperdatabase."""
17         return self.user.lookup(self.journaltag)
19     def uidFromAddress(self, address):
20         ''' address is from the rfc822 module, and therefore is (name, addr)
22             user is created if they don't exist in the db already
23         '''
24         (realname, address) = address
25         users = self.user.stringFind(address=address)
26         if users: return users[0]
27         return self.user.create(username=address, address=address,
28             realname=realname)
30 # XXX: added the 'creator' faked attribute
31 class Class(hyperdb.Class):
32     # Overridden methods:
33     def __init__(self, db, classname, **properties):
34         hyperdb.Class.__init__(self, db, classname, **properties)
35         self.auditors = {'create': [], 'set': [], 'retire': []}
36         self.reactors = {'create': [], 'set': [], 'retire': []}
38     def create(self, **propvalues):
39         """These operations trigger detectors and can be vetoed.  Attempts
40         to modify the "creation" or "activity" properties cause a KeyError.
41         """
42         if propvalues.has_key('creation') or propvalues.has_key('activity'):
43             raise KeyError, '"creation" and "activity" are reserved'
44         for audit in self.auditors['create']:
45             audit(self.db, self, None, propvalues)
46         nodeid = hyperdb.Class.create(self, **propvalues)
47         for react in self.reactors['create']:
48             react(self.db, self, nodeid, None)
49         return nodeid
51     def set(self, nodeid, **propvalues):
52         """These operations trigger detectors and can be vetoed.  Attempts
53         to modify the "creation" or "activity" properties cause a KeyError.
54         """
55         if propvalues.has_key('creation') or propvalues.has_key('activity'):
56             raise KeyError, '"creation" and "activity" are reserved'
57         for audit in self.auditors['set']:
58             audit(self.db, self, nodeid, propvalues)
59         oldvalues = self.db.getnode(self.classname, nodeid)
60         hyperdb.Class.set(self, nodeid, **propvalues)
61         for react in self.reactors['set']:
62             react(self.db, self, nodeid, oldvalues)
64     def retire(self, nodeid):
65         """These operations trigger detectors and can be vetoed.  Attempts
66         to modify the "creation" or "activity" properties cause a KeyError.
67         """
68         for audit in self.auditors['retire']:
69             audit(self.db, self, nodeid, None)
70         hyperdb.Class.retire(self, nodeid)
71         for react in self.reactors['retire']:
72             react(self.db, self, nodeid, None)
74     def get(self, nodeid, propname):
75         """Attempts to get the "creation" or "activity" properties should
76         do the right thing
77         """
78         if propname == 'creation':
79             journal = self.db.getjournal(self.classname, nodeid)
80             if journal:
81                 return self.db.getjournal(self.classname, nodeid)[0][1]
82             else:
83                 # on the strange chance that there's no journal
84                 return date.Date()
85         if propname == 'activity':
86             journal = self.db.getjournal(self.classname, nodeid)
87             if journal:
88                 return self.db.getjournal(self.classname, nodeid)[-1][1]
89             else:
90                 # on the strange chance that there's no journal
91                 return date.Date()
92         if propname == 'creator':
93             journal = self.db.getjournal(self.classname, nodeid)
94             if journal:
95                 name = self.db.getjournal(self.classname, nodeid)[0][2]
96             else:
97                 return None
98             return self.db.user.lookup(name)
99         return hyperdb.Class.get(self, nodeid, propname)
101     def getprops(self):
102         """In addition to the actual properties on the node, these
103         methods provide the "creation" and "activity" properties."""
104         d = hyperdb.Class.getprops(self).copy()
105         d['creation'] = hyperdb.Date()
106         d['activity'] = hyperdb.Date()
107         d['creator'] = hyperdb.Link("user")
108         return d
110     # New methods:
112     def audit(self, event, detector):
113         """Register a detector
114         """
115         self.auditors[event].append(detector)
117     def react(self, event, detector):
118         """Register a detector
119         """
120         self.reactors[event].append(detector)
122 class FileClass(Class):
123     def create(self, **propvalues):
124         ''' snaffle the file propvalue and store in a file
125         '''
126         content = propvalues['content']
127         del propvalues['content']
128         newid = Class.create(self, **propvalues)
129         self.setcontent(self.classname, newid, content)
130         return newid
132     def filename(self, classname, nodeid):
133         # TODO: split into multiple files directories
134         return os.path.join(self.db.dir, 'files', '%s%s'%(classname, nodeid))
136     def setcontent(self, classname, nodeid, content):
137         ''' set the content file for this file
138         '''
139         open(self.filename(classname, nodeid), 'wb').write(content)
141     def getcontent(self, classname, nodeid):
142         ''' get the content file for this file
143         '''
144         return open(self.filename(classname, nodeid), 'rb').read()
146     def get(self, nodeid, propname):
147         ''' trap the content propname and get it from the file
148         '''
149         if propname == 'content':
150             return self.getcontent(self.classname, nodeid)
151         return Class.get(self, nodeid, propname)
153     def getprops(self):
154         ''' In addition to the actual properties on the node, these methods
155             provide the "content" property.
156         '''
157         d = Class.getprops(self).copy()
158         d['content'] = hyperdb.String()
159         return d
161 # XXX deviation from spec - was called ItemClass
162 class IssueClass(Class):
163     # Overridden methods:
165     def __init__(self, db, classname, **properties):
166         """The newly-created class automatically includes the "messages",
167         "files", "nosy", and "superseder" properties.  If the 'properties'
168         dictionary attempts to specify any of these properties or a
169         "creation" or "activity" property, a ValueError is raised."""
170         if not properties.has_key('title'):
171             properties['title'] = hyperdb.String()
172         if not properties.has_key('messages'):
173             properties['messages'] = hyperdb.Multilink("msg")
174         if not properties.has_key('files'):
175             properties['files'] = hyperdb.Multilink("file")
176         if not properties.has_key('nosy'):
177             properties['nosy'] = hyperdb.Multilink("user")
178         if not properties.has_key('superseder'):
179             properties['superseder'] = hyperdb.Multilink("issue")
180         if (properties.has_key('creation') or properties.has_key('activity')
181                 or properties.has_key('creator')):
182             raise ValueError, '"creation", "activity" and "creator" are reserved'
183         Class.__init__(self, db, classname, **properties)
185     # New methods:
187     def addmessage(self, nodeid, summary, text):
188         """Add a message to an issue's mail spool.
190         A new "msg" node is constructed using the current date, the user that
191         owns the database connection as the author, and the specified summary
192         text.
194         The "files" and "recipients" fields are left empty.
196         The given text is saved as the body of the message and the node is
197         appended to the "messages" field of the specified issue.
198         """
200     def sendmessage(self, nodeid, msgid):
201         """Send a message to the members of an issue's nosy list.
203         The message is sent only to users on the nosy list who are not
204         already on the "recipients" list for the message.
205         
206         These users are then added to the message's "recipients" list.
207         """
208         # figure the recipient ids
209         recipients = self.db.msg.get(msgid, 'recipients')
210         r = {}
211         for recipid in recipients:
212             r[recipid] = 1
213         authid = self.db.msg.get(msgid, 'author')
214         r[authid] = 1
216         # now figure the nosy people who weren't recipients
217         sendto = []
218         nosy = self.get(nodeid, 'nosy')
219         for nosyid in nosy:
220             if not r.has_key(nosyid):
221                 sendto.append(nosyid)
222                 recipients.append(nosyid)
224         if sendto:
225             # update the message's recipients list
226             self.db.msg.set(msgid, recipients=recipients)
228             # send an email to the people who missed out
229             sendto = [self.db.user.get(i, 'address') for i in recipients]
230             cn = self.classname
231             title = self.get(nodeid, 'title') or '%s message copy'%cn
232             m = ['Subject: [%s%s] %s'%(cn, nodeid, title)]
233             m.append('To: %s'%', '.join(sendto))
234             m.append('Reply-To: %s'%self.ISSUE_TRACKER_EMAIL)
235             m.append('')
236             m.append(self.db.msg.get(msgid, 'content'))
237             # TODO attachments
238             try:
239                 smtp = smtplib.SMTP(self.MAILHOST)
240                 smtp.sendmail(self.ISSUE_TRACKER_EMAIL, sendto, '\n'.join(m))
241             except socket.error, value:
242                 return "Couldn't send confirmation email: mailhost %s"%value
243             except smtplib.SMTPException, value:
244                 return "Couldn't send confirmation email: %s"%value
247 # $Log: not supported by cvs2svn $
248 # Revision 1.2  2001/07/22 12:09:32  richard
249 # Final commit of Grande Splite
251 # Revision 1.1  2001/07/22 11:58:35  richard
252 # More Grande Splite