Code

Roundupdb now appends "mailing list" information to its messages which
[roundup.git] / roundup / roundupdb.py
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.
214         
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)
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.
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".
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 :)
278 # Revision 1.4  2001/07/29 04:05:37  richard
279 # Added the fabricated property "id".
281 # Revision 1.3  2001/07/23 07:14:41  richard
282 # Moved the database backends off into backends.
284 # Revision 1.2  2001/07/22 12:09:32  richard
285 # Final commit of Grande Splite
287 # Revision 1.1  2001/07/22 11:58:35  richard
288 # More Grande Splite
291 # vim: set filetype=python ts=4 sw=4 et si