b27493a34a8026347c5d6f423fb49182ef4e6727
1 # $Id: roundupdb.py,v 1.1 2001-07-22 11:58:35 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 class Class(hyperdb.Class):
31 # Overridden methods:
32 def __init__(self, db, classname, **properties):
33 hyperdb.Class.__init__(self, db, classname, **properties)
34 self.auditors = {'create': [], 'set': [], 'retire': []}
35 self.reactors = {'create': [], 'set': [], 'retire': []}
37 def create(self, **propvalues):
38 """These operations trigger detectors and can be vetoed. Attempts
39 to modify the "creation" or "activity" properties cause a KeyError.
40 """
41 if propvalues.has_key('creation') or propvalues.has_key('activity'):
42 raise KeyError, '"creation" and "activity" are reserved'
43 for audit in self.auditors['create']:
44 audit(self.db, self, None, propvalues)
45 nodeid = hyperdb.Class.create(self, **propvalues)
46 for react in self.reactors['create']:
47 react(self.db, self, nodeid, None)
48 return nodeid
50 def set(self, nodeid, **propvalues):
51 """These operations trigger detectors and can be vetoed. Attempts
52 to modify the "creation" or "activity" properties cause a KeyError.
53 """
54 if propvalues.has_key('creation') or propvalues.has_key('activity'):
55 raise KeyError, '"creation" and "activity" are reserved'
56 for audit in self.auditors['set']:
57 audit(self.db, self, nodeid, propvalues)
58 oldvalues = self.db.getnode(self.classname, nodeid)
59 hyperdb.Class.set(self, nodeid, **propvalues)
60 for react in self.reactors['set']:
61 react(self.db, self, nodeid, oldvalues)
63 def retire(self, nodeid):
64 """These operations trigger detectors and can be vetoed. Attempts
65 to modify the "creation" or "activity" properties cause a KeyError.
66 """
67 for audit in self.auditors['retire']:
68 audit(self.db, self, nodeid, None)
69 hyperdb.Class.retire(self, nodeid)
70 for react in self.reactors['retire']:
71 react(self.db, self, nodeid, None)
73 # New methods:
75 def audit(self, event, detector):
76 """Register a detector
77 """
78 self.auditors[event].append(detector)
80 def react(self, event, detector):
81 """Register a detector
82 """
83 self.reactors[event].append(detector)
85 class FileClass(Class):
86 def create(self, **propvalues):
87 ''' snaffle the file propvalue and store in a file
88 '''
89 content = propvalues['content']
90 del propvalues['content']
91 newid = Class.create(self, **propvalues)
92 self.setcontent(self.classname, newid, content)
93 return newid
95 def filename(self, classname, nodeid):
96 # TODO: split into multiple files directories
97 return os.path.join(self.db.dir, 'files', '%s%s'%(classname, nodeid))
99 def setcontent(self, classname, nodeid, content):
100 ''' set the content file for this file
101 '''
102 open(self.filename(classname, nodeid), 'wb').write(content)
104 def getcontent(self, classname, nodeid):
105 ''' get the content file for this file
106 '''
107 return open(self.filename(classname, nodeid), 'rb').read()
109 def get(self, nodeid, propname):
110 ''' trap the content propname and get it from the file
111 '''
112 if propname == 'content':
113 return self.getcontent(self.classname, nodeid)
114 return Class.get(self, nodeid, propname)
116 def getprops(self):
117 ''' In addition to the actual properties on the node, these methods
118 provide the "content" property.
119 '''
120 d = Class.getprops(self).copy()
121 d['content'] = hyperdb.String()
122 return d
124 # XXX deviation from spec - was called ItemClass
125 class IssueClass(Class):
126 # Overridden methods:
128 def __init__(self, db, classname, **properties):
129 """The newly-created class automatically includes the "messages",
130 "files", "nosy", and "superseder" properties. If the 'properties'
131 dictionary attempts to specify any of these properties or a
132 "creation" or "activity" property, a ValueError is raised."""
133 if not properties.has_key('title'):
134 properties['title'] = hyperdb.String()
135 if not properties.has_key('messages'):
136 properties['messages'] = hyperdb.Multilink("msg")
137 if not properties.has_key('files'):
138 properties['files'] = hyperdb.Multilink("file")
139 if not properties.has_key('nosy'):
140 properties['nosy'] = hyperdb.Multilink("user")
141 if not properties.has_key('superseder'):
142 properties['superseder'] = hyperdb.Multilink("issue")
143 if (properties.has_key('creation') or properties.has_key('activity')
144 or properties.has_key('creator')):
145 raise ValueError, '"creation", "activity" and "creator" are reserved'
146 Class.__init__(self, db, classname, **properties)
148 def get(self, nodeid, propname):
149 if propname == 'creation':
150 return self.db.getjournal(self.classname, nodeid)[0][1]
151 if propname == 'activity':
152 return self.db.getjournal(self.classname, nodeid)[-1][1]
153 if propname == 'creator':
154 name = self.db.getjournal(self.classname, nodeid)[0][2]
155 return self.db.user.lookup(name)
156 return Class.get(self, nodeid, propname)
158 def getprops(self):
159 """In addition to the actual properties on the node, these
160 methods provide the "creation" and "activity" properties."""
161 d = Class.getprops(self).copy()
162 d['creation'] = hyperdb.Date()
163 d['activity'] = hyperdb.Date()
164 d['creator'] = hyperdb.Link("user")
165 return d
167 # New methods:
169 def addmessage(self, nodeid, summary, text):
170 """Add a message to an issue's mail spool.
172 A new "msg" node is constructed using the current date, the user that
173 owns the database connection as the author, and the specified summary
174 text.
176 The "files" and "recipients" fields are left empty.
178 The given text is saved as the body of the message and the node is
179 appended to the "messages" field of the specified issue.
180 """
182 def sendmessage(self, nodeid, msgid):
183 """Send a message to the members of an issue's nosy list.
185 The message is sent only to users on the nosy list who are not
186 already on the "recipients" list for the message.
188 These users are then added to the message's "recipients" list.
189 """
190 # figure the recipient ids
191 recipients = self.db.msg.get(msgid, 'recipients')
192 r = {}
193 for recipid in recipients:
194 r[recipid] = 1
195 authid = self.db.msg.get(msgid, 'author')
196 r[authid] = 1
198 # now figure the nosy people who weren't recipients
199 sendto = []
200 nosy = self.get(nodeid, 'nosy')
201 for nosyid in nosy:
202 if not r.has_key(nosyid):
203 sendto.append(nosyid)
204 recipients.append(nosyid)
206 if sendto:
207 # update the message's recipients list
208 self.db.msg.set(msgid, recipients=recipients)
210 # send an email to the people who missed out
211 sendto = [self.db.user.get(i, 'address') for i in recipients]
212 cn = self.classname
213 title = self.get(nodeid, 'title') or '%s message copy'%cn
214 m = ['Subject: [%s%s] %s'%(cn, nodeid, title)]
215 m.append('To: %s'%', '.join(sendto))
216 m.append('Reply-To: %s'%self.ISSUE_TRACKER_EMAIL)
217 m.append('')
218 m.append(self.db.msg.get(msgid, 'content'))
219 # TODO attachments
220 try:
221 smtp = smtplib.SMTP(self.MAILHOST)
222 smtp.sendmail(self.ISSUE_TRACKER_EMAIL, sendto, '\n'.join(m))
223 except socket.error, value:
224 return "Couldn't send confirmation email: mailhost %s"%value
225 except smtplib.SMTPException, value:
226 return "Couldn't send confirmation email: %s"%value
228 #
229 # $Log: not supported by cvs2svn $
230 # Revision 1.6 2001/07/20 07:35:55 richard
231 # largish changes as a start of splitting off bits and pieces to allow more
232 # flexible installation / database back-ends
233 #
234 # Revision 1.5 2001/07/20 00:22:50 richard
235 # Priority list changes - removed the redundant TODO and added support. See
236 # roundup-devel for details.
237 #
238 # Revision 1.4 2001/07/19 06:27:07 anthonybaxter
239 # fixing (manually) the (dollarsign)Log(dollarsign) entries caused by
240 # my using the magic (dollarsign)Id(dollarsign) and (dollarsign)Log(dollarsign)
241 # strings in a commit message. I'm a twonk.
242 #
243 # Also broke the help string in two.
244 #
245 # Revision 1.3 2001/07/19 05:52:22 anthonybaxter
246 # Added CVS keywords Id and Log to all python files.
247 #
248 #