0e30f71bb0452950ef1c5782b2e58c2044a626d9
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.10 2001-08-07 00:24:42 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.
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)
283 #
284 # $Log: not supported by cvs2svn $
285 # Revision 1.9 2001/08/07 00:15:51 richard
286 # Added the copyright/license notice to (nearly) all files at request of
287 # Bizar Software.
288 #
289 # Revision 1.8 2001/08/02 06:38:17 richard
290 # Roundupdb now appends "mailing list" information to its messages which
291 # include the e-mail address and web interface address. Templates may
292 # override this in their db classes to include specific information (support
293 # instructions, etc).
294 #
295 # Revision 1.7 2001/07/30 02:38:31 richard
296 # get() now has a default arg - for migration only.
297 #
298 # Revision 1.6 2001/07/30 00:05:54 richard
299 # Fixed IssueClass so that superseders links to its classname rather than
300 # hard-coded to "issue".
301 #
302 # Revision 1.5 2001/07/29 07:01:39 richard
303 # Added vim command to all source so that we don't get no steenkin' tabs :)
304 #
305 # Revision 1.4 2001/07/29 04:05:37 richard
306 # Added the fabricated property "id".
307 #
308 # Revision 1.3 2001/07/23 07:14:41 richard
309 # Moved the database backends off into backends.
310 #
311 # Revision 1.2 2001/07/22 12:09:32 richard
312 # Final commit of Grande Splite
313 #
314 # Revision 1.1 2001/07/22 11:58:35 richard
315 # More Grande Splite
316 #
317 #
318 # vim: set filetype=python ts=4 sw=4 et si