1 # $Id: roundupdb.py,v 1.6 2001-07-30 00:05:54 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 # XXX: added the 'creator' faked attribute
31 class Class(hyperdb.Class):
32 # Overridden methods:
33 def __init__(self, db, classname, **properties):
34 hyperdb.Class.__init__(self, db, classname, **properties)
35 self.auditors = {'create': [], 'set': [], 'retire': []}
36 self.reactors = {'create': [], 'set': [], 'retire': []}
38 def create(self, **propvalues):
39 """These operations trigger detectors and can be vetoed. Attempts
40 to modify the "creation" or "activity" properties cause a KeyError.
41 """
42 if propvalues.has_key('creation') or propvalues.has_key('activity'):
43 raise KeyError, '"creation" and "activity" are reserved'
44 for audit in self.auditors['create']:
45 audit(self.db, self, None, propvalues)
46 nodeid = hyperdb.Class.create(self, **propvalues)
47 for react in self.reactors['create']:
48 react(self.db, self, nodeid, None)
49 return nodeid
51 def set(self, nodeid, **propvalues):
52 """These operations trigger detectors and can be vetoed. Attempts
53 to modify the "creation" or "activity" properties cause a KeyError.
54 """
55 if propvalues.has_key('creation') or propvalues.has_key('activity'):
56 raise KeyError, '"creation" and "activity" are reserved'
57 for audit in self.auditors['set']:
58 audit(self.db, self, nodeid, propvalues)
59 oldvalues = self.db.getnode(self.classname, nodeid)
60 hyperdb.Class.set(self, nodeid, **propvalues)
61 for react in self.reactors['set']:
62 react(self.db, self, nodeid, oldvalues)
64 def retire(self, nodeid):
65 """These operations trigger detectors and can be vetoed. Attempts
66 to modify the "creation" or "activity" properties cause a KeyError.
67 """
68 for audit in self.auditors['retire']:
69 audit(self.db, self, nodeid, None)
70 hyperdb.Class.retire(self, nodeid)
71 for react in self.reactors['retire']:
72 react(self.db, self, nodeid, None)
74 def get(self, nodeid, propname):
75 """Attempts to get the "creation" or "activity" properties should
76 do the right thing
77 """
78 if propname == 'creation':
79 journal = self.db.getjournal(self.classname, nodeid)
80 if journal:
81 return self.db.getjournal(self.classname, nodeid)[0][1]
82 else:
83 # on the strange chance that there's no journal
84 return date.Date()
85 if propname == 'activity':
86 journal = self.db.getjournal(self.classname, nodeid)
87 if journal:
88 return self.db.getjournal(self.classname, nodeid)[-1][1]
89 else:
90 # on the strange chance that there's no journal
91 return date.Date()
92 if propname == 'creator':
93 journal = self.db.getjournal(self.classname, nodeid)
94 if journal:
95 name = self.db.getjournal(self.classname, nodeid)[0][2]
96 else:
97 return None
98 return self.db.user.lookup(name)
99 return hyperdb.Class.get(self, nodeid, propname)
101 def getprops(self):
102 """In addition to the actual properties on the node, these
103 methods provide the "creation" and "activity" properties."""
104 d = hyperdb.Class.getprops(self).copy()
105 d['creation'] = hyperdb.Date()
106 d['activity'] = hyperdb.Date()
107 d['creator'] = hyperdb.Link("user")
108 return d
110 #
111 # Detector interface
112 #
113 def audit(self, event, detector):
114 """Register a detector
115 """
116 self.auditors[event].append(detector)
118 def react(self, event, detector):
119 """Register a detector
120 """
121 self.reactors[event].append(detector)
124 class FileClass(Class):
125 def create(self, **propvalues):
126 ''' snaffle the file propvalue and store in a file
127 '''
128 content = propvalues['content']
129 del propvalues['content']
130 newid = Class.create(self, **propvalues)
131 self.setcontent(self.classname, newid, content)
132 return newid
134 def filename(self, classname, nodeid):
135 # TODO: split into multiple files directories
136 return os.path.join(self.db.dir, 'files', '%s%s'%(classname, nodeid))
138 def setcontent(self, classname, nodeid, content):
139 ''' set the content file for this file
140 '''
141 open(self.filename(classname, nodeid), 'wb').write(content)
143 def getcontent(self, classname, nodeid):
144 ''' get the content file for this file
145 '''
146 return open(self.filename(classname, nodeid), 'rb').read()
148 def get(self, nodeid, propname):
149 ''' trap the content propname and get it from the file
150 '''
151 if propname == 'content':
152 return self.getcontent(self.classname, nodeid)
153 return Class.get(self, nodeid, propname)
155 def getprops(self):
156 ''' In addition to the actual properties on the node, these methods
157 provide the "content" property.
158 '''
159 d = Class.getprops(self).copy()
160 d['content'] = hyperdb.String()
161 return d
163 # XXX deviation from spec - was called ItemClass
164 class IssueClass(Class):
165 # Overridden methods:
167 def __init__(self, db, classname, **properties):
168 """The newly-created class automatically includes the "messages",
169 "files", "nosy", and "superseder" properties. If the 'properties'
170 dictionary attempts to specify any of these properties or a
171 "creation" or "activity" property, a ValueError is raised."""
172 if not properties.has_key('title'):
173 properties['title'] = hyperdb.String()
174 if not properties.has_key('messages'):
175 properties['messages'] = hyperdb.Multilink("msg")
176 if not properties.has_key('files'):
177 properties['files'] = hyperdb.Multilink("file")
178 if not properties.has_key('nosy'):
179 properties['nosy'] = hyperdb.Multilink("user")
180 if not properties.has_key('superseder'):
181 properties['superseder'] = hyperdb.Multilink(classname)
182 if (properties.has_key('creation') or properties.has_key('activity')
183 or properties.has_key('creator')):
184 raise ValueError, '"creation", "activity" and "creator" are reserved'
185 Class.__init__(self, db, classname, **properties)
187 # New methods:
189 def addmessage(self, nodeid, summary, text):
190 """Add a message to an issue's mail spool.
192 A new "msg" node is constructed using the current date, the user that
193 owns the database connection as the author, and the specified summary
194 text.
196 The "files" and "recipients" fields are left empty.
198 The given text is saved as the body of the message and the node is
199 appended to the "messages" field of the specified issue.
200 """
202 def sendmessage(self, nodeid, msgid):
203 """Send a message to the members of an issue's nosy list.
205 The message is sent only to users on the nosy list who are not
206 already on the "recipients" list for the message.
208 These users are then added to the message's "recipients" list.
209 """
210 # figure the recipient ids
211 recipients = self.db.msg.get(msgid, 'recipients')
212 r = {}
213 for recipid in recipients:
214 r[recipid] = 1
215 authid = self.db.msg.get(msgid, 'author')
216 r[authid] = 1
218 # now figure the nosy people who weren't recipients
219 sendto = []
220 nosy = self.get(nodeid, 'nosy')
221 for nosyid in nosy:
222 if not r.has_key(nosyid):
223 sendto.append(nosyid)
224 recipients.append(nosyid)
226 if sendto:
227 # update the message's recipients list
228 self.db.msg.set(msgid, recipients=recipients)
230 # send an email to the people who missed out
231 sendto = [self.db.user.get(i, 'address') for i in recipients]
232 cn = self.classname
233 title = self.get(nodeid, 'title') or '%s message copy'%cn
234 m = ['Subject: [%s%s] %s'%(cn, nodeid, title)]
235 m.append('To: %s'%', '.join(sendto))
236 m.append('Reply-To: %s'%self.ISSUE_TRACKER_EMAIL)
237 m.append('')
238 m.append(self.db.msg.get(msgid, 'content'))
239 # TODO attachments
240 try:
241 smtp = smtplib.SMTP(self.MAILHOST)
242 smtp.sendmail(self.ISSUE_TRACKER_EMAIL, sendto, '\n'.join(m))
243 except socket.error, value:
244 return "Couldn't send confirmation email: mailhost %s"%value
245 except smtplib.SMTPException, value:
246 return "Couldn't send confirmation email: %s"%value
248 #
249 # $Log: not supported by cvs2svn $
250 # Revision 1.5 2001/07/29 07:01:39 richard
251 # Added vim command to all source so that we don't get no steenkin' tabs :)
252 #
253 # Revision 1.4 2001/07/29 04:05:37 richard
254 # Added the fabricated property "id".
255 #
256 # Revision 1.3 2001/07/23 07:14:41 richard
257 # Moved the database backends off into backends.
258 #
259 # Revision 1.2 2001/07/22 12:09:32 richard
260 # Final commit of Grande Splite
261 #
262 # Revision 1.1 2001/07/22 11:58:35 richard
263 # More Grande Splite
264 #
265 #
266 # vim: set filetype=python ts=4 sw=4 et si