1 # $Id: roundupdb.py,v 1.8 2001-08-02 06:38:17 richard Exp $
3 import re, os, smtplib, socket
5 import hyperdb, date
7 def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
8 ''' Take a foo123 and return ('foo', 123)
9 '''
10 m = dre.match(designator)
11 return m.group(1), m.group(2)
13 class Database:
14 def getuid(self):
15 """Return the id of the "user" node associated with the user
16 that owns this connection to the hyperdatabase."""
17 return self.user.lookup(self.journaltag)
19 def uidFromAddress(self, address):
20 ''' address is from the rfc822 module, and therefore is (name, addr)
22 user is created if they don't exist in the db already
23 '''
24 (realname, address) = address
25 users = self.user.stringFind(address=address)
26 if users: return users[0]
27 return self.user.create(username=address, address=address,
28 realname=realname)
30 _marker = []
31 # XXX: added the 'creator' faked attribute
32 class Class(hyperdb.Class):
33 # Overridden methods:
34 def __init__(self, db, classname, **properties):
35 hyperdb.Class.__init__(self, db, classname, **properties)
36 self.auditors = {'create': [], 'set': [], 'retire': []}
37 self.reactors = {'create': [], 'set': [], 'retire': []}
39 def create(self, **propvalues):
40 """These operations trigger detectors and can be vetoed. Attempts
41 to modify the "creation" or "activity" properties cause a KeyError.
42 """
43 if propvalues.has_key('creation') or propvalues.has_key('activity'):
44 raise KeyError, '"creation" and "activity" are reserved'
45 for audit in self.auditors['create']:
46 audit(self.db, self, None, propvalues)
47 nodeid = hyperdb.Class.create(self, **propvalues)
48 for react in self.reactors['create']:
49 react(self.db, self, nodeid, None)
50 return nodeid
52 def set(self, nodeid, **propvalues):
53 """These operations trigger detectors and can be vetoed. Attempts
54 to modify the "creation" or "activity" properties cause a KeyError.
55 """
56 if propvalues.has_key('creation') or propvalues.has_key('activity'):
57 raise KeyError, '"creation" and "activity" are reserved'
58 for audit in self.auditors['set']:
59 audit(self.db, self, nodeid, propvalues)
60 oldvalues = self.db.getnode(self.classname, nodeid)
61 hyperdb.Class.set(self, nodeid, **propvalues)
62 for react in self.reactors['set']:
63 react(self.db, self, nodeid, oldvalues)
65 def retire(self, nodeid):
66 """These operations trigger detectors and can be vetoed. Attempts
67 to modify the "creation" or "activity" properties cause a KeyError.
68 """
69 for audit in self.auditors['retire']:
70 audit(self.db, self, nodeid, None)
71 hyperdb.Class.retire(self, nodeid)
72 for react in self.reactors['retire']:
73 react(self.db, self, nodeid, None)
75 def get(self, nodeid, propname, default=_marker):
76 """Attempts to get the "creation" or "activity" properties should
77 do the right thing.
78 """
79 if propname == 'creation':
80 journal = self.db.getjournal(self.classname, nodeid)
81 if journal:
82 return self.db.getjournal(self.classname, nodeid)[0][1]
83 else:
84 # on the strange chance that there's no journal
85 return date.Date()
86 if propname == 'activity':
87 journal = self.db.getjournal(self.classname, nodeid)
88 if journal:
89 return self.db.getjournal(self.classname, nodeid)[-1][1]
90 else:
91 # on the strange chance that there's no journal
92 return date.Date()
93 if propname == 'creator':
94 journal = self.db.getjournal(self.classname, nodeid)
95 if journal:
96 name = self.db.getjournal(self.classname, nodeid)[0][2]
97 else:
98 return None
99 return self.db.user.lookup(name)
100 if default is not _marker:
101 return hyperdb.Class.get(self, nodeid, propname, default)
102 else:
103 return hyperdb.Class.get(self, nodeid, propname)
105 def getprops(self):
106 """In addition to the actual properties on the node, these
107 methods provide the "creation" and "activity" properties."""
108 d = hyperdb.Class.getprops(self).copy()
109 d['creation'] = hyperdb.Date()
110 d['activity'] = hyperdb.Date()
111 d['creator'] = hyperdb.Link("user")
112 return d
114 #
115 # Detector interface
116 #
117 def audit(self, event, detector):
118 """Register a detector
119 """
120 self.auditors[event].append(detector)
122 def react(self, event, detector):
123 """Register a detector
124 """
125 self.reactors[event].append(detector)
128 class FileClass(Class):
129 def create(self, **propvalues):
130 ''' snaffle the file propvalue and store in a file
131 '''
132 content = propvalues['content']
133 del propvalues['content']
134 newid = Class.create(self, **propvalues)
135 self.setcontent(self.classname, newid, content)
136 return newid
138 def filename(self, classname, nodeid):
139 # TODO: split into multiple files directories
140 return os.path.join(self.db.dir, 'files', '%s%s'%(classname, nodeid))
142 def setcontent(self, classname, nodeid, content):
143 ''' set the content file for this file
144 '''
145 open(self.filename(classname, nodeid), 'wb').write(content)
147 def getcontent(self, classname, nodeid):
148 ''' get the content file for this file
149 '''
150 return open(self.filename(classname, nodeid), 'rb').read()
152 def get(self, nodeid, propname, default=_marker):
153 ''' trap the content propname and get it from the file
154 '''
155 if propname == 'content':
156 return self.getcontent(self.classname, nodeid)
157 if default is not _marker:
158 return Class.get(self, nodeid, propname, default)
159 else:
160 return Class.get(self, nodeid, propname)
162 def getprops(self):
163 ''' In addition to the actual properties on the node, these methods
164 provide the "content" property.
165 '''
166 d = Class.getprops(self).copy()
167 d['content'] = hyperdb.String()
168 return d
170 # XXX deviation from spec - was called ItemClass
171 class IssueClass(Class):
172 # Overridden methods:
174 def __init__(self, db, classname, **properties):
175 """The newly-created class automatically includes the "messages",
176 "files", "nosy", and "superseder" properties. If the 'properties'
177 dictionary attempts to specify any of these properties or a
178 "creation" or "activity" property, a ValueError is raised."""
179 if not properties.has_key('title'):
180 properties['title'] = hyperdb.String()
181 if not properties.has_key('messages'):
182 properties['messages'] = hyperdb.Multilink("msg")
183 if not properties.has_key('files'):
184 properties['files'] = hyperdb.Multilink("file")
185 if not properties.has_key('nosy'):
186 properties['nosy'] = hyperdb.Multilink("user")
187 if not properties.has_key('superseder'):
188 properties['superseder'] = hyperdb.Multilink(classname)
189 if (properties.has_key('creation') or properties.has_key('activity')
190 or properties.has_key('creator')):
191 raise ValueError, '"creation", "activity" and "creator" are reserved'
192 Class.__init__(self, db, classname, **properties)
194 # New methods:
196 def addmessage(self, nodeid, summary, text):
197 """Add a message to an issue's mail spool.
199 A new "msg" node is constructed using the current date, the user that
200 owns the database connection as the author, and the specified summary
201 text.
203 The "files" and "recipients" fields are left empty.
205 The given text is saved as the body of the message and the node is
206 appended to the "messages" field of the specified issue.
207 """
209 def sendmessage(self, nodeid, msgid):
210 """Send a message to the members of an issue's nosy list.
212 The message is sent only to users on the nosy list who are not
213 already on the "recipients" list for the message.
215 These users are then added to the message's "recipients" list.
216 """
217 # figure the recipient ids
218 recipients = self.db.msg.get(msgid, 'recipients')
219 r = {}
220 for recipid in recipients:
221 r[recipid] = 1
222 authid = self.db.msg.get(msgid, 'author')
223 r[authid] = 1
225 # now figure the nosy people who weren't recipients
226 sendto = []
227 nosy = self.get(nodeid, 'nosy')
228 for nosyid in nosy:
229 if not r.has_key(nosyid):
230 sendto.append(nosyid)
231 recipients.append(nosyid)
233 if sendto:
234 # update the message's recipients list
235 self.db.msg.set(msgid, recipients=recipients)
237 # send an email to the people who missed out
238 sendto = [self.db.user.get(i, 'address') for i in recipients]
239 cn = self.classname
240 title = self.get(nodeid, 'title') or '%s message copy'%cn
241 m = ['Subject: [%s%s] %s'%(cn, nodeid, title)]
242 m.append('To: %s'%', '.join(sendto))
243 m.append('Reply-To: %s'%self.ISSUE_TRACKER_EMAIL)
244 m.append('')
245 m.append(self.db.msg.get(msgid, 'content'))
246 m.append(self.email_footer(nodeid, msgid))
247 # TODO attachments
248 try:
249 smtp = smtplib.SMTP(self.MAILHOST)
250 smtp.sendmail(self.ISSUE_TRACKER_EMAIL, sendto, '\n'.join(m))
251 except socket.error, value:
252 return "Couldn't send confirmation email: mailhost %s"%value
253 except smtplib.SMTPException, value:
254 return "Couldn't send confirmation email: %s"%value
256 def email_footer(self, nodeid, msgid):
257 ''' Add a footer to the e-mail with some useful information
258 '''
259 web = self.ISSUE_TRACKER_WEB
260 return '''%s
261 Roundup issue tracker
262 %s
263 %s
264 '''%('_'*len(web), self.ISSUE_TRACKER_EMAIL, web)
266 #
267 # $Log: not supported by cvs2svn $
268 # Revision 1.7 2001/07/30 02:38:31 richard
269 # get() now has a default arg - for migration only.
270 #
271 # Revision 1.6 2001/07/30 00:05:54 richard
272 # Fixed IssueClass so that superseders links to its classname rather than
273 # hard-coded to "issue".
274 #
275 # Revision 1.5 2001/07/29 07:01:39 richard
276 # Added vim command to all source so that we don't get no steenkin' tabs :)
277 #
278 # Revision 1.4 2001/07/29 04:05:37 richard
279 # Added the fabricated property "id".
280 #
281 # Revision 1.3 2001/07/23 07:14:41 richard
282 # Moved the database backends off into backends.
283 #
284 # Revision 1.2 2001/07/22 12:09:32 richard
285 # Final commit of Grande Splite
286 #
287 # Revision 1.1 2001/07/22 11:58:35 richard
288 # More Grande Splite
289 #
290 #
291 # vim: set filetype=python ts=4 sw=4 et si