546e74652909ed5efbf72c61932b1084c25d1b6a
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.14 2001-10-21 07:26:35 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 if (properties.has_key('creation') or properties.has_key('activity')
53 or properties.has_key('creator')):
54 raise ValueError, '"creation", "activity" and "creator" are reserved'
55 hyperdb.Class.__init__(self, db, classname, **properties)
56 self.auditors = {'create': [], 'set': [], 'retire': []}
57 self.reactors = {'create': [], 'set': [], 'retire': []}
59 def create(self, **propvalues):
60 """These operations trigger detectors and can be vetoed. Attempts
61 to modify the "creation" or "activity" properties cause a KeyError.
62 """
63 if propvalues.has_key('creation') or propvalues.has_key('activity'):
64 raise KeyError, '"creation" and "activity" are reserved'
65 for audit in self.auditors['create']:
66 audit(self.db, self, None, propvalues)
67 nodeid = hyperdb.Class.create(self, **propvalues)
68 for react in self.reactors['create']:
69 react(self.db, self, nodeid, None)
70 return nodeid
72 def set(self, nodeid, **propvalues):
73 """These operations trigger detectors and can be vetoed. Attempts
74 to modify the "creation" or "activity" properties cause a KeyError.
75 """
76 if propvalues.has_key('creation') or propvalues.has_key('activity'):
77 raise KeyError, '"creation" and "activity" are reserved'
78 for audit in self.auditors['set']:
79 audit(self.db, self, nodeid, propvalues)
80 oldvalues = self.db.getnode(self.classname, nodeid)
81 hyperdb.Class.set(self, nodeid, **propvalues)
82 for react in self.reactors['set']:
83 react(self.db, self, nodeid, oldvalues)
85 def retire(self, nodeid):
86 """These operations trigger detectors and can be vetoed. Attempts
87 to modify the "creation" or "activity" properties cause a KeyError.
88 """
89 for audit in self.auditors['retire']:
90 audit(self.db, self, nodeid, None)
91 hyperdb.Class.retire(self, nodeid)
92 for react in self.reactors['retire']:
93 react(self.db, self, nodeid, None)
95 def get(self, nodeid, propname, default=_marker):
96 """Attempts to get the "creation" or "activity" properties should
97 do the right thing.
98 """
99 if propname == 'creation':
100 journal = self.db.getjournal(self.classname, nodeid)
101 if journal:
102 return self.db.getjournal(self.classname, nodeid)[0][1]
103 else:
104 # on the strange chance that there's no journal
105 return date.Date()
106 if propname == 'activity':
107 journal = self.db.getjournal(self.classname, nodeid)
108 if journal:
109 return self.db.getjournal(self.classname, nodeid)[-1][1]
110 else:
111 # on the strange chance that there's no journal
112 return date.Date()
113 if propname == 'creator':
114 journal = self.db.getjournal(self.classname, nodeid)
115 if journal:
116 name = self.db.getjournal(self.classname, nodeid)[0][2]
117 else:
118 return None
119 return self.db.user.lookup(name)
120 if default is not _marker:
121 return hyperdb.Class.get(self, nodeid, propname, default)
122 else:
123 return hyperdb.Class.get(self, nodeid, propname)
125 def getprops(self, protected=1):
126 """In addition to the actual properties on the node, these
127 methods provide the "creation" and "activity" properties. If the
128 "protected" flag is true, we include protected properties - those
129 which may not be modified.
130 """
131 d = hyperdb.Class.getprops(self, protected=protected).copy()
132 if protected:
133 d['creation'] = hyperdb.Date()
134 d['activity'] = hyperdb.Date()
135 d['creator'] = hyperdb.Link("user")
136 return d
138 #
139 # Detector interface
140 #
141 def audit(self, event, detector):
142 """Register a detector
143 """
144 self.auditors[event].append(detector)
146 def react(self, event, detector):
147 """Register a detector
148 """
149 self.reactors[event].append(detector)
152 class FileClass(Class):
153 def create(self, **propvalues):
154 ''' snaffle the file propvalue and store in a file
155 '''
156 content = propvalues['content']
157 del propvalues['content']
158 newid = Class.create(self, **propvalues)
159 self.setcontent(self.classname, newid, content)
160 return newid
162 def filename(self, classname, nodeid):
163 # TODO: split into multiple files directories
164 return os.path.join(self.db.dir, 'files', '%s%s'%(classname, nodeid))
166 def setcontent(self, classname, nodeid, content):
167 ''' set the content file for this file
168 '''
169 open(self.filename(classname, nodeid), 'wb').write(content)
171 def getcontent(self, classname, nodeid):
172 ''' get the content file for this file
173 '''
174 return open(self.filename(classname, nodeid), 'rb').read()
176 def get(self, nodeid, propname, default=_marker):
177 ''' trap the content propname and get it from the file
178 '''
179 if propname == 'content':
180 return self.getcontent(self.classname, nodeid)
181 if default is not _marker:
182 return Class.get(self, nodeid, propname, default)
183 else:
184 return Class.get(self, nodeid, propname)
186 def getprops(self, protected=1):
187 ''' In addition to the actual properties on the node, these methods
188 provide the "content" property. If the "protected" flag is true,
189 we include protected properties - those which may not be
190 modified.
191 '''
192 d = Class.getprops(self, protected=protected).copy()
193 if protected:
194 d['content'] = hyperdb.String()
195 return d
197 # XXX deviation from spec - was called ItemClass
198 class IssueClass(Class):
199 # Overridden methods:
201 def __init__(self, db, classname, **properties):
202 """The newly-created class automatically includes the "messages",
203 "files", "nosy", and "superseder" properties. If the 'properties'
204 dictionary attempts to specify any of these properties or a
205 "creation" or "activity" property, a ValueError is raised."""
206 if not properties.has_key('title'):
207 properties['title'] = hyperdb.String()
208 if not properties.has_key('messages'):
209 properties['messages'] = hyperdb.Multilink("msg")
210 if not properties.has_key('files'):
211 properties['files'] = hyperdb.Multilink("file")
212 if not properties.has_key('nosy'):
213 properties['nosy'] = hyperdb.Multilink("user")
214 if not properties.has_key('superseder'):
215 properties['superseder'] = hyperdb.Multilink(classname)
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 # figure author information
266 authname = self.db.user.get(authid, 'realname')
267 if not authname:
268 authname = self.db.user.get(authid, 'username')
269 authaddr = self.db.user.get(authid, 'address')
270 if authaddr:
271 authaddr = '<%s> '%authaddr
272 else:
273 authaddr = ''
274 # TODO attachments
275 m = ['Subject: [%s%s] %s'%(cn, nodeid, title)]
276 m.append('To: %s'%', '.join(sendto))
277 m.append('Reply-To: %s'%self.ISSUE_TRACKER_EMAIL)
278 m.append('')
279 # add author information
280 m.append("%s %sadded the comment:"%(authname, authaddr))
281 m.append('')
282 # add the content
283 m.append(self.db.msg.get(msgid, 'content'))
284 # "list information" footer
285 m.append(self.email_footer(nodeid, msgid))
286 try:
287 smtp = smtplib.SMTP(self.MAILHOST)
288 smtp.sendmail(self.ISSUE_TRACKER_EMAIL, sendto, '\n'.join(m))
289 except socket.error, value:
290 return "Couldn't send confirmation email: mailhost %s"%value
291 except smtplib.SMTPException, value:
292 return "Couldn't send confirmation email: %s"%value
294 def email_footer(self, nodeid, msgid):
295 ''' Add a footer to the e-mail with some useful information
296 '''
297 web = self.ISSUE_TRACKER_WEB
298 return '''%s
299 Roundup issue tracker
300 %s
301 %s
302 '''%('_'*len(web), self.ISSUE_TRACKER_EMAIL, web)
304 #
305 # $Log: not supported by cvs2svn $
306 # Revision 1.13 2001/10/21 00:45:15 richard
307 # Added author identification to e-mail messages from roundup.
308 #
309 # Revision 1.12 2001/10/04 02:16:15 richard
310 # Forgot to pass the protected flag down *sigh*.
311 #
312 # Revision 1.11 2001/10/04 02:12:42 richard
313 # Added nicer command-line item adding: passing no arguments will enter an
314 # interactive more which asks for each property in turn. While I was at it, I
315 # fixed an implementation problem WRT the spec - I wasn't raising a
316 # ValueError if the key property was missing from a create(). Also added a
317 # protected=boolean argument to getprops() so we can list only the mutable
318 # properties (defaults to yes, which lists the immutables).
319 #
320 # Revision 1.10 2001/08/07 00:24:42 richard
321 # stupid typo
322 #
323 # Revision 1.9 2001/08/07 00:15:51 richard
324 # Added the copyright/license notice to (nearly) all files at request of
325 # Bizar Software.
326 #
327 # Revision 1.8 2001/08/02 06:38:17 richard
328 # Roundupdb now appends "mailing list" information to its messages which
329 # include the e-mail address and web interface address. Templates may
330 # override this in their db classes to include specific information (support
331 # instructions, etc).
332 #
333 # Revision 1.7 2001/07/30 02:38:31 richard
334 # get() now has a default arg - for migration only.
335 #
336 # Revision 1.6 2001/07/30 00:05:54 richard
337 # Fixed IssueClass so that superseders links to its classname rather than
338 # hard-coded to "issue".
339 #
340 # Revision 1.5 2001/07/29 07:01:39 richard
341 # Added vim command to all source so that we don't get no steenkin' tabs :)
342 #
343 # Revision 1.4 2001/07/29 04:05:37 richard
344 # Added the fabricated property "id".
345 #
346 # Revision 1.3 2001/07/23 07:14:41 richard
347 # Moved the database backends off into backends.
348 #
349 # Revision 1.2 2001/07/22 12:09:32 richard
350 # Final commit of Grande Splite
351 #
352 # Revision 1.1 2001/07/22 11:58:35 richard
353 # More Grande Splite
354 #
355 #
356 # vim: set filetype=python ts=4 sw=4 et si