599d1886c587b10f5516a340e2dbcc41ea308847
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 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.12 2001-10-04 02:16:15 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, create=1):
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, protected=1):
123 """In addition to the actual properties on the node, these
124 methods provide the "creation" and "activity" properties. If the
125 "protected" flag is true, we include protected properties - those
126 which may not be modified.
127 """
128 d = hyperdb.Class.getprops(self, protected=protected).copy()
129 if protected:
130 d['creation'] = hyperdb.Date()
131 d['activity'] = hyperdb.Date()
132 d['creator'] = hyperdb.Link("user")
133 return d
135 #
136 # Detector interface
137 #
138 def audit(self, event, detector):
139 """Register a detector
140 """
141 self.auditors[event].append(detector)
143 def react(self, event, detector):
144 """Register a detector
145 """
146 self.reactors[event].append(detector)
149 class FileClass(Class):
150 def create(self, **propvalues):
151 ''' snaffle the file propvalue and store in a file
152 '''
153 content = propvalues['content']
154 del propvalues['content']
155 newid = Class.create(self, **propvalues)
156 self.setcontent(self.classname, newid, content)
157 return newid
159 def filename(self, classname, nodeid):
160 # TODO: split into multiple files directories
161 return os.path.join(self.db.dir, 'files', '%s%s'%(classname, nodeid))
163 def setcontent(self, classname, nodeid, content):
164 ''' set the content file for this file
165 '''
166 open(self.filename(classname, nodeid), 'wb').write(content)
168 def getcontent(self, classname, nodeid):
169 ''' get the content file for this file
170 '''
171 return open(self.filename(classname, nodeid), 'rb').read()
173 def get(self, nodeid, propname, default=_marker):
174 ''' trap the content propname and get it from the file
175 '''
176 if propname == 'content':
177 return self.getcontent(self.classname, nodeid)
178 if default is not _marker:
179 return Class.get(self, nodeid, propname, default)
180 else:
181 return Class.get(self, nodeid, propname)
183 def getprops(self, protected=1):
184 ''' In addition to the actual properties on the node, these methods
185 provide the "content" property. If the "protected" flag is true,
186 we include protected properties - those which may not be
187 modified.
188 '''
189 d = Class.getprops(self, protected=protected).copy()
190 if protected:
191 d['content'] = hyperdb.String()
192 return d
194 # XXX deviation from spec - was called ItemClass
195 class IssueClass(Class):
196 # Overridden methods:
198 def __init__(self, db, classname, **properties):
199 """The newly-created class automatically includes the "messages",
200 "files", "nosy", and "superseder" properties. If the 'properties'
201 dictionary attempts to specify any of these properties or a
202 "creation" or "activity" property, a ValueError is raised."""
203 if not properties.has_key('title'):
204 properties['title'] = hyperdb.String()
205 if not properties.has_key('messages'):
206 properties['messages'] = hyperdb.Multilink("msg")
207 if not properties.has_key('files'):
208 properties['files'] = hyperdb.Multilink("file")
209 if not properties.has_key('nosy'):
210 properties['nosy'] = hyperdb.Multilink("user")
211 if not properties.has_key('superseder'):
212 properties['superseder'] = hyperdb.Multilink(classname)
213 if (properties.has_key('creation') or properties.has_key('activity')
214 or properties.has_key('creator')):
215 raise ValueError, '"creation", "activity" and "creator" are reserved'
216 Class.__init__(self, db, classname, **properties)
218 # New methods:
220 def addmessage(self, nodeid, summary, text):
221 """Add a message to an issue's mail spool.
223 A new "msg" node is constructed using the current date, the user that
224 owns the database connection as the author, and the specified summary
225 text.
227 The "files" and "recipients" fields are left empty.
229 The given text is saved as the body of the message and the node is
230 appended to the "messages" field of the specified issue.
231 """
233 def sendmessage(self, nodeid, msgid):
234 """Send a message to the members of an issue's nosy list.
236 The message is sent only to users on the nosy list who are not
237 already on the "recipients" list for the message.
239 These users are then added to the message's "recipients" list.
240 """
241 # figure the recipient ids
242 recipients = self.db.msg.get(msgid, 'recipients')
243 r = {}
244 for recipid in recipients:
245 r[recipid] = 1
246 authid = self.db.msg.get(msgid, 'author')
247 r[authid] = 1
249 # now figure the nosy people who weren't recipients
250 sendto = []
251 nosy = self.get(nodeid, 'nosy')
252 for nosyid in nosy:
253 if not r.has_key(nosyid):
254 sendto.append(nosyid)
255 recipients.append(nosyid)
257 if sendto:
258 # update the message's recipients list
259 self.db.msg.set(msgid, recipients=recipients)
261 # send an email to the people who missed out
262 sendto = [self.db.user.get(i, 'address') for i in recipients]
263 cn = self.classname
264 title = self.get(nodeid, 'title') or '%s message copy'%cn
265 m = ['Subject: [%s%s] %s'%(cn, nodeid, title)]
266 m.append('To: %s'%', '.join(sendto))
267 m.append('Reply-To: %s'%self.ISSUE_TRACKER_EMAIL)
268 m.append('')
269 m.append(self.db.msg.get(msgid, 'content'))
270 m.append(self.email_footer(nodeid, msgid))
271 # TODO attachments
272 try:
273 smtp = smtplib.SMTP(self.MAILHOST)
274 smtp.sendmail(self.ISSUE_TRACKER_EMAIL, sendto, '\n'.join(m))
275 except socket.error, value:
276 return "Couldn't send confirmation email: mailhost %s"%value
277 except smtplib.SMTPException, value:
278 return "Couldn't send confirmation email: %s"%value
280 def email_footer(self, nodeid, msgid):
281 ''' Add a footer to the e-mail with some useful information
282 '''
283 web = self.ISSUE_TRACKER_WEB
284 return '''%s
285 Roundup issue tracker
286 %s
287 %s
288 '''%('_'*len(web), self.ISSUE_TRACKER_EMAIL, web)
290 #
291 # $Log: not supported by cvs2svn $
292 # Revision 1.11 2001/10/04 02:12:42 richard
293 # Added nicer command-line item adding: passing no arguments will enter an
294 # interactive more which asks for each property in turn. While I was at it, I
295 # fixed an implementation problem WRT the spec - I wasn't raising a
296 # ValueError if the key property was missing from a create(). Also added a
297 # protected=boolean argument to getprops() so we can list only the mutable
298 # properties (defaults to yes, which lists the immutables).
299 #
300 # Revision 1.10 2001/08/07 00:24:42 richard
301 # stupid typo
302 #
303 # Revision 1.9 2001/08/07 00:15:51 richard
304 # Added the copyright/license notice to (nearly) all files at request of
305 # Bizar Software.
306 #
307 # Revision 1.8 2001/08/02 06:38:17 richard
308 # Roundupdb now appends "mailing list" information to its messages which
309 # include the e-mail address and web interface address. Templates may
310 # override this in their db classes to include specific information (support
311 # instructions, etc).
312 #
313 # Revision 1.7 2001/07/30 02:38:31 richard
314 # get() now has a default arg - for migration only.
315 #
316 # Revision 1.6 2001/07/30 00:05:54 richard
317 # Fixed IssueClass so that superseders links to its classname rather than
318 # hard-coded to "issue".
319 #
320 # Revision 1.5 2001/07/29 07:01:39 richard
321 # Added vim command to all source so that we don't get no steenkin' tabs :)
322 #
323 # Revision 1.4 2001/07/29 04:05:37 richard
324 # Added the fabricated property "id".
325 #
326 # Revision 1.3 2001/07/23 07:14:41 richard
327 # Moved the database backends off into backends.
328 #
329 # Revision 1.2 2001/07/22 12:09:32 richard
330 # Final commit of Grande Splite
331 #
332 # Revision 1.1 2001/07/22 11:58:35 richard
333 # More Grande Splite
334 #
335 #
336 # vim: set filetype=python ts=4 sw=4 et si