099c1c61532614eded43bda2ecdbe181191cc16b
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.15 2001-10-23 01:00:18 richard Exp $
20 import re, os, smtplib, socket
22 import hyperdb, date
24 class DesignatorError(ValueError):
25 pass
26 def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
27 ''' Take a foo123 and return ('foo', 123)
28 '''
29 m = dre.match(designator)
30 if m is None:
31 raise DesignatorError, '"%s" not a node designator'%designator
32 return m.group(1), m.group(2)
34 class Database:
35 def getuid(self):
36 """Return the id of the "user" node associated with the user
37 that owns this connection to the hyperdatabase."""
38 return self.user.lookup(self.journaltag)
40 def uidFromAddress(self, address, create=1):
41 ''' address is from the rfc822 module, and therefore is (name, addr)
43 user is created if they don't exist in the db already
44 '''
45 (realname, address) = address
46 users = self.user.stringFind(address=address)
47 if users: return users[0]
48 return self.user.create(username=address, address=address,
49 realname=realname)
51 _marker = []
52 # XXX: added the 'creator' faked attribute
53 class Class(hyperdb.Class):
54 # Overridden methods:
55 def __init__(self, db, classname, **properties):
56 if (properties.has_key('creation') or properties.has_key('activity')
57 or properties.has_key('creator')):
58 raise ValueError, '"creation", "activity" and "creator" are reserved'
59 hyperdb.Class.__init__(self, db, classname, **properties)
60 self.auditors = {'create': [], 'set': [], 'retire': []}
61 self.reactors = {'create': [], 'set': [], 'retire': []}
63 def create(self, **propvalues):
64 """These operations trigger detectors and can be vetoed. Attempts
65 to modify the "creation" or "activity" properties cause a KeyError.
66 """
67 if propvalues.has_key('creation') or propvalues.has_key('activity'):
68 raise KeyError, '"creation" and "activity" are reserved'
69 for audit in self.auditors['create']:
70 audit(self.db, self, None, propvalues)
71 nodeid = hyperdb.Class.create(self, **propvalues)
72 for react in self.reactors['create']:
73 react(self.db, self, nodeid, None)
74 return nodeid
76 def set(self, nodeid, **propvalues):
77 """These operations trigger detectors and can be vetoed. Attempts
78 to modify the "creation" or "activity" properties cause a KeyError.
79 """
80 if propvalues.has_key('creation') or propvalues.has_key('activity'):
81 raise KeyError, '"creation" and "activity" are reserved'
82 for audit in self.auditors['set']:
83 audit(self.db, self, nodeid, propvalues)
84 oldvalues = self.db.getnode(self.classname, nodeid)
85 hyperdb.Class.set(self, nodeid, **propvalues)
86 for react in self.reactors['set']:
87 react(self.db, self, nodeid, oldvalues)
89 def retire(self, nodeid):
90 """These operations trigger detectors and can be vetoed. Attempts
91 to modify the "creation" or "activity" properties cause a KeyError.
92 """
93 for audit in self.auditors['retire']:
94 audit(self.db, self, nodeid, None)
95 hyperdb.Class.retire(self, nodeid)
96 for react in self.reactors['retire']:
97 react(self.db, self, nodeid, None)
99 def get(self, nodeid, propname, default=_marker):
100 """Attempts to get the "creation" or "activity" properties should
101 do the right thing.
102 """
103 if propname == 'creation':
104 journal = self.db.getjournal(self.classname, nodeid)
105 if journal:
106 return self.db.getjournal(self.classname, nodeid)[0][1]
107 else:
108 # on the strange chance that there's no journal
109 return date.Date()
110 if propname == 'activity':
111 journal = self.db.getjournal(self.classname, nodeid)
112 if journal:
113 return self.db.getjournal(self.classname, nodeid)[-1][1]
114 else:
115 # on the strange chance that there's no journal
116 return date.Date()
117 if propname == 'creator':
118 journal = self.db.getjournal(self.classname, nodeid)
119 if journal:
120 name = self.db.getjournal(self.classname, nodeid)[0][2]
121 else:
122 return None
123 return self.db.user.lookup(name)
124 if default is not _marker:
125 return hyperdb.Class.get(self, nodeid, propname, default)
126 else:
127 return hyperdb.Class.get(self, nodeid, propname)
129 def getprops(self, protected=1):
130 """In addition to the actual properties on the node, these
131 methods provide the "creation" and "activity" properties. If the
132 "protected" flag is true, we include protected properties - those
133 which may not be modified.
134 """
135 d = hyperdb.Class.getprops(self, protected=protected).copy()
136 if protected:
137 d['creation'] = hyperdb.Date()
138 d['activity'] = hyperdb.Date()
139 d['creator'] = hyperdb.Link("user")
140 return d
142 #
143 # Detector interface
144 #
145 def audit(self, event, detector):
146 """Register a detector
147 """
148 self.auditors[event].append(detector)
150 def react(self, event, detector):
151 """Register a detector
152 """
153 self.reactors[event].append(detector)
156 class FileClass(Class):
157 def create(self, **propvalues):
158 ''' snaffle the file propvalue and store in a file
159 '''
160 content = propvalues['content']
161 del propvalues['content']
162 newid = Class.create(self, **propvalues)
163 self.setcontent(self.classname, newid, content)
164 return newid
166 def filename(self, classname, nodeid):
167 # TODO: split into multiple files directories
168 return os.path.join(self.db.dir, 'files', '%s%s'%(classname, nodeid))
170 def setcontent(self, classname, nodeid, content):
171 ''' set the content file for this file
172 '''
173 open(self.filename(classname, nodeid), 'wb').write(content)
175 def getcontent(self, classname, nodeid):
176 ''' get the content file for this file
177 '''
178 return open(self.filename(classname, nodeid), 'rb').read()
180 def get(self, nodeid, propname, default=_marker):
181 ''' trap the content propname and get it from the file
182 '''
183 if propname == 'content':
184 return self.getcontent(self.classname, nodeid)
185 if default is not _marker:
186 return Class.get(self, nodeid, propname, default)
187 else:
188 return Class.get(self, nodeid, propname)
190 def getprops(self, protected=1):
191 ''' In addition to the actual properties on the node, these methods
192 provide the "content" property. If the "protected" flag is true,
193 we include protected properties - those which may not be
194 modified.
195 '''
196 d = Class.getprops(self, protected=protected).copy()
197 if protected:
198 d['content'] = hyperdb.String()
199 return d
201 # XXX deviation from spec - was called ItemClass
202 class IssueClass(Class):
203 # Overridden methods:
205 def __init__(self, db, classname, **properties):
206 """The newly-created class automatically includes the "messages",
207 "files", "nosy", and "superseder" properties. If the 'properties'
208 dictionary attempts to specify any of these properties or a
209 "creation" or "activity" property, a ValueError is raised."""
210 if not properties.has_key('title'):
211 properties['title'] = hyperdb.String()
212 if not properties.has_key('messages'):
213 properties['messages'] = hyperdb.Multilink("msg")
214 if not properties.has_key('files'):
215 properties['files'] = hyperdb.Multilink("file")
216 if not properties.has_key('nosy'):
217 properties['nosy'] = hyperdb.Multilink("user")
218 if not properties.has_key('superseder'):
219 properties['superseder'] = hyperdb.Multilink(classname)
220 Class.__init__(self, db, classname, **properties)
222 # New methods:
224 def addmessage(self, nodeid, summary, text):
225 """Add a message to an issue's mail spool.
227 A new "msg" node is constructed using the current date, the user that
228 owns the database connection as the author, and the specified summary
229 text.
231 The "files" and "recipients" fields are left empty.
233 The given text is saved as the body of the message and the node is
234 appended to the "messages" field of the specified issue.
235 """
237 def sendmessage(self, nodeid, msgid):
238 """Send a message to the members of an issue's nosy list.
240 The message is sent only to users on the nosy list who are not
241 already on the "recipients" list for the message.
243 These users are then added to the message's "recipients" list.
244 """
245 # figure the recipient ids
246 recipients = self.db.msg.get(msgid, 'recipients')
247 r = {}
248 for recipid in recipients:
249 r[recipid] = 1
250 authid = self.db.msg.get(msgid, 'author')
251 r[authid] = 1
253 # now figure the nosy people who weren't recipients
254 sendto = []
255 nosy = self.get(nodeid, 'nosy')
256 for nosyid in nosy:
257 if not r.has_key(nosyid):
258 sendto.append(nosyid)
259 recipients.append(nosyid)
261 if sendto:
262 # update the message's recipients list
263 self.db.msg.set(msgid, recipients=recipients)
265 # send an email to the people who missed out
266 sendto = [self.db.user.get(i, 'address') for i in recipients]
267 cn = self.classname
268 title = self.get(nodeid, 'title') or '%s message copy'%cn
269 # figure author information
270 authname = self.db.user.get(authid, 'realname')
271 if not authname:
272 authname = self.db.user.get(authid, 'username')
273 authaddr = self.db.user.get(authid, 'address')
274 if authaddr:
275 authaddr = '<%s> '%authaddr
276 else:
277 authaddr = ''
278 # TODO attachments
279 m = ['Subject: [%s%s] %s'%(cn, nodeid, title)]
280 m.append('To: %s'%', '.join(sendto))
281 m.append('Reply-To: %s'%self.ISSUE_TRACKER_EMAIL)
282 m.append('')
283 # add author information
284 m.append("%s %sadded the comment:"%(authname, authaddr))
285 m.append('')
286 # add the content
287 m.append(self.db.msg.get(msgid, 'content'))
288 # "list information" footer
289 m.append(self.email_footer(nodeid, msgid))
290 try:
291 smtp = smtplib.SMTP(self.MAILHOST)
292 smtp.sendmail(self.ISSUE_TRACKER_EMAIL, sendto, '\n'.join(m))
293 except socket.error, value:
294 return "Couldn't send confirmation email: mailhost %s"%value
295 except smtplib.SMTPException, value:
296 return "Couldn't send confirmation email: %s"%value
298 def email_footer(self, nodeid, msgid):
299 ''' Add a footer to the e-mail with some useful information
300 '''
301 web = self.ISSUE_TRACKER_WEB
302 return '''%s
303 Roundup issue tracker
304 %s
305 %s
306 '''%('_'*len(web), self.ISSUE_TRACKER_EMAIL, web)
308 #
309 # $Log: not supported by cvs2svn $
310 # Revision 1.14 2001/10/21 07:26:35 richard
311 # feature #473127: Filenames. I modified the file.index and htmltemplate
312 # source so that the filename is used in the link and the creation
313 # information is displayed.
314 #
315 # Revision 1.13 2001/10/21 00:45:15 richard
316 # Added author identification to e-mail messages from roundup.
317 #
318 # Revision 1.12 2001/10/04 02:16:15 richard
319 # Forgot to pass the protected flag down *sigh*.
320 #
321 # Revision 1.11 2001/10/04 02:12:42 richard
322 # Added nicer command-line item adding: passing no arguments will enter an
323 # interactive more which asks for each property in turn. While I was at it, I
324 # fixed an implementation problem WRT the spec - I wasn't raising a
325 # ValueError if the key property was missing from a create(). Also added a
326 # protected=boolean argument to getprops() so we can list only the mutable
327 # properties (defaults to yes, which lists the immutables).
328 #
329 # Revision 1.10 2001/08/07 00:24:42 richard
330 # stupid typo
331 #
332 # Revision 1.9 2001/08/07 00:15:51 richard
333 # Added the copyright/license notice to (nearly) all files at request of
334 # Bizar Software.
335 #
336 # Revision 1.8 2001/08/02 06:38:17 richard
337 # Roundupdb now appends "mailing list" information to its messages which
338 # include the e-mail address and web interface address. Templates may
339 # override this in their db classes to include specific information (support
340 # instructions, etc).
341 #
342 # Revision 1.7 2001/07/30 02:38:31 richard
343 # get() now has a default arg - for migration only.
344 #
345 # Revision 1.6 2001/07/30 00:05:54 richard
346 # Fixed IssueClass so that superseders links to its classname rather than
347 # hard-coded to "issue".
348 #
349 # Revision 1.5 2001/07/29 07:01:39 richard
350 # Added vim command to all source so that we don't get no steenkin' tabs :)
351 #
352 # Revision 1.4 2001/07/29 04:05:37 richard
353 # Added the fabricated property "id".
354 #
355 # Revision 1.3 2001/07/23 07:14:41 richard
356 # Moved the database backends off into backends.
357 #
358 # Revision 1.2 2001/07/22 12:09:32 richard
359 # Final commit of Grande Splite
360 #
361 # Revision 1.1 2001/07/22 11:58:35 richard
362 # More Grande Splite
363 #
364 #
365 # vim: set filetype=python ts=4 sw=4 et si