Code

0355b8ba42380443aaaf7149cfd857bcbdf5d9d5
[roundup.git] / roundup / roundupdb.py
1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL THE BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17
18 # $Id: roundupdb.py,v 1.9 2001-08-07 00:15:51 richard Exp $
20 import re, os, smtplib, socket
22 import hyperdb, date
24 def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
25     ''' Take a foo123 and return ('foo', 123)
26     '''
27     m = dre.match(designator)
28     return m.group(1), m.group(2)
30 class Database:
31     def getuid(self):
32         """Return the id of the "user" node associated with the user
33         that owns this connection to the hyperdatabase."""
34         return self.user.lookup(self.journaltag)
36     def uidFromAddress(self, address):
37         ''' address is from the rfc822 module, and therefore is (name, addr)
39             user is created if they don't exist in the db already
40         '''
41         (realname, address) = address
42         users = self.user.stringFind(address=address)
43         if users: return users[0]
44         return self.user.create(username=address, address=address,
45             realname=realname)
47 _marker = []
48 # XXX: added the 'creator' faked attribute
49 class Class(hyperdb.Class):
50     # Overridden methods:
51     def __init__(self, db, classname, **properties):
52         hyperdb.Class.__init__(self, db, classname, **properties)
53         self.auditors = {'create': [], 'set': [], 'retire': []}
54         self.reactors = {'create': [], 'set': [], 'retire': []}
56     def create(self, **propvalues):
57         """These operations trigger detectors and can be vetoed.  Attempts
58         to modify the "creation" or "activity" properties cause a KeyError.
59         """
60         if propvalues.has_key('creation') or propvalues.has_key('activity'):
61             raise KeyError, '"creation" and "activity" are reserved'
62         for audit in self.auditors['create']:
63             audit(self.db, self, None, propvalues)
64         nodeid = hyperdb.Class.create(self, **propvalues)
65         for react in self.reactors['create']:
66             react(self.db, self, nodeid, None)
67         return nodeid
69     def set(self, nodeid, **propvalues):
70         """These operations trigger detectors and can be vetoed.  Attempts
71         to modify the "creation" or "activity" properties cause a KeyError.
72         """
73         if propvalues.has_key('creation') or propvalues.has_key('activity'):
74             raise KeyError, '"creation" and "activity" are reserved'
75         for audit in self.auditors['set']:
76             audit(self.db, self, nodeid, propvalues)
77         oldvalues = self.db.getnode(self.classname, nodeid)
78         hyperdb.Class.set(self, nodeid, **propvalues)
79         for react in self.reactors['set']:
80             react(self.db, self, nodeid, oldvalues)
82     def retire(self, nodeid):
83         """These operations trigger detectors and can be vetoed.  Attempts
84         to modify the "creation" or "activity" properties cause a KeyError.
85         """
86         for audit in self.auditors['retire']:
87             audit(self.db, self, nodeid, None)
88         hyperdb.Class.retire(self, nodeid)
89         for react in self.reactors['retire']:
90             react(self.db, self, nodeid, None)
92     def get(self, nodeid, propname, default=_marker):
93         """Attempts to get the "creation" or "activity" properties should
94         do the right thing.
95         """
96         if propname == 'creation':
97             journal = self.db.getjournal(self.classname, nodeid)
98             if journal:
99                 return self.db.getjournal(self.classname, nodeid)[0][1]
100             else:
101                 # on the strange chance that there's no journal
102                 return date.Date()
103         if propname == 'activity':
104             journal = self.db.getjournal(self.classname, nodeid)
105             if journal:
106                 return self.db.getjournal(self.classname, nodeid)[-1][1]
107             else:
108                 # on the strange chance that there's no journal
109                 return date.Date()
110         if propname == 'creator':
111             journal = self.db.getjournal(self.classname, nodeid)
112             if journal:
113                 name = self.db.getjournal(self.classname, nodeid)[0][2]
114             else:
115                 return None
116             return self.db.user.lookup(name)
117         if default is not _marker:
118             return hyperdb.Class.get(self, nodeid, propname, default)
119         else:
120             return hyperdb.Class.get(self, nodeid, propname)
122     def getprops(self):
123         """In addition to the actual properties on the node, these
124         methods provide the "creation" and "activity" properties."""
125         d = hyperdb.Class.getprops(self).copy()
126         d['creation'] = hyperdb.Date()
127         d['activity'] = hyperdb.Date()
128         d['creator'] = hyperdb.Link("user")
129         return d
131     #
132     # Detector interface
133     #
134     def audit(self, event, detector):
135         """Register a detector
136         """
137         self.auditors[event].append(detector)
139     def react(self, event, detector):
140         """Register a detector
141         """
142         self.reactors[event].append(detector)
145 class FileClass(Class):
146     def create(self, **propvalues):
147         ''' snaffle the file propvalue and store in a file
148         '''
149         content = propvalues['content']
150         del propvalues['content']
151         newid = Class.create(self, **propvalues)
152         self.setcontent(self.classname, newid, content)
153         return newid
155     def filename(self, classname, nodeid):
156         # TODO: split into multiple files directories
157         return os.path.join(self.db.dir, 'files', '%s%s'%(classname, nodeid))
159     def setcontent(self, classname, nodeid, content):
160         ''' set the content file for this file
161         '''
162         open(self.filename(classname, nodeid), 'wb').write(content)
164     def getcontent(self, classname, nodeid):
165         ''' get the content file for this file
166         '''
167         return open(self.filename(classname, nodeid), 'rb').read()
169     def get(self, nodeid, propname, default=_marker):
170         ''' trap the content propname and get it from the file
171         '''
172         if propname == 'content':
173             return self.getcontent(self.classname, nodeid)
174         if default is not _marker:
175             return Class.get(self, nodeid, propname, default)
176         else:
177             return Class.get(self, nodeid, propname)
179     def getprops(self):
180         ''' In addition to the actual properties on the node, these methods
181             provide the "content" property.
182         '''
183         d = Class.getprops(self).copy()
184         d['content'] = hyperdb.String()
185         return d
187 # XXX deviation from spec - was called ItemClass
188 class IssueClass(Class):
189     # Overridden methods:
191     def __init__(self, db, classname, **properties):
192         """The newly-created class automatically includes the "messages",
193         "files", "nosy", and "superseder" properties.  If the 'properties'
194         dictionary attempts to specify any of these properties or a
195         "creation" or "activity" property, a ValueError is raised."""
196         if not properties.has_key('title'):
197             properties['title'] = hyperdb.String()
198         if not properties.has_key('messages'):
199             properties['messages'] = hyperdb.Multilink("msg")
200         if not properties.has_key('files'):
201             properties['files'] = hyperdb.Multilink("file")
202         if not properties.has_key('nosy'):
203             properties['nosy'] = hyperdb.Multilink("user")
204         if not properties.has_key('superseder'):
205             properties['superseder'] = hyperdb.Multilink(classname)
206         if (properties.has_key('creation') or properties.has_key('activity')
207                 or properties.has_key('creator')):
208             raise ValueError, '"creation", "activity" and "creator" are reserved'
209         Class.__init__(self, db, classname, **properties)
211     # New methods:
213     def addmessage(self, nodeid, summary, text):
214         """Add a message to an issue's mail spool.
216         A new "msg" node is constructed using the current date, the user that
217         owns the database connection as the author, and the specified summary
218         text.
220         The "files" and "recipients" fields are left empty.
222         The given text is saved as the body of the message and the node is
223         appended to the "messages" field of the specified issue.
224         """
226     def sendmessage(self, nodeid, msgid):
227         """Send a message to the members of an issue's nosy list.
229         The message is sent only to users on the nosy list who are not
230         already on the "recipients" list for the message.
231         
232         These users are then added to the message's "recipients" list.
233         """
234         # figure the recipient ids
235         recipients = self.db.msg.get(msgid, 'recipients')
236         r = {}
237         for recipid in recipients:
238             r[recipid] = 1
239         authid = self.db.msg.get(msgid, 'author')
240         r[authid] = 1
242         # now figure the nosy people who weren't recipients
243         sendto = []
244         nosy = self.get(nodeid, 'nosy')
245         for nosyid in nosy:
246             if not r.has_key(nosyid):
247                 sendto.append(nosyid)
248                 recipients.append(nosyid)
250         if sendto:
251             # update the message's recipients list
252             self.db.msg.set(msgid, recipients=recipients)
254             # send an email to the people who missed out
255             sendto = [self.db.user.get(i, 'address') for i in recipients]
256             cn = self.classname
257             title = self.get(nodeid, 'title') or '%s message copy'%cn
258             m = ['Subject: [%s%s] %s'%(cn, nodeid, title)]
259             m.append('To: %s'%', '.join(sendto))
260             m.append('Reply-To: %s'%self.ISSUE_TRACKER_EMAIL)
261             m.append('')
262             m.append(self.db.msg.get(msgid, 'content'))
263             m.append(self.email_footer(nodeid, msgid))
264             # TODO attachments
265             try:
266                 smtp = smtplib.SMTP(self.MAILHOST)
267                 smtp.sendmail(self.ISSUE_TRACKER_EMAIL, sendto, '\n'.join(m))
268             except socket.error, value:
269                 return "Couldn't send confirmation email: mailhost %s"%value
270             except smtplib.SMTPException, value:
271                 return "Couldn't send confirmation email: %s"%value
273     def email_footer(self, nodeid, msgid):
274         ''' Add a footer to the e-mail with some useful information
275         '''
276         web = self.ISSUE_TRACKER_WEB
277         return '''%s
278 Roundup issue tracker
279 %s
280 %s
281 '''%('_'*len(web), self.ISSUE_TRACKER_EMAIL, web)
284 # $Log: not supported by cvs2svn $
285 # Revision 1.8  2001/08/02 06:38:17  richard
286 # Roundupdb now appends "mailing list" information to its messages which
287 # include the e-mail address and web interface address. Templates may
288 # override this in their db classes to include specific information (support
289 # instructions, etc).
291 # Revision 1.7  2001/07/30 02:38:31  richard
292 # get() now has a default arg - for migration only.
294 # Revision 1.6  2001/07/30 00:05:54  richard
295 # Fixed IssueClass so that superseders links to its classname rather than
296 # hard-coded to "issue".
298 # Revision 1.5  2001/07/29 07:01:39  richard
299 # Added vim command to all source so that we don't get no steenkin' tabs :)
301 # Revision 1.4  2001/07/29 04:05:37  richard
302 # Added the fabricated property "id".
304 # Revision 1.3  2001/07/23 07:14:41  richard
305 # Moved the database backends off into backends.
307 # Revision 1.2  2001/07/22 12:09:32  richard
308 # Final commit of Grande Splite
310 # Revision 1.1  2001/07/22 11:58:35  richard
311 # More Grande Splite
314 # vim: set filetype=python ts=4 sw=4 et si