Code

Added vim command to all source so that we don't get no steenkin' tabs :)
[roundup.git] / roundup / roundupdb.py
1 # $Id: roundupdb.py,v 1.5 2001-07-29 07:01:39 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("issue")
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.
207         
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
249 # $Log: not supported by cvs2svn $
250 # Revision 1.4  2001/07/29 04:05:37  richard
251 # Added the fabricated property "id".
253 # Revision 1.3  2001/07/23 07:14:41  richard
254 # Moved the database backends off into backends.
256 # Revision 1.2  2001/07/22 12:09:32  richard
257 # Final commit of Grande Splite
259 # Revision 1.1  2001/07/22 11:58:35  richard
260 # More Grande Splite
263 # vim: set filetype=python ts=4 sw=4 et si