Code

b27493a34a8026347c5d6f423fb49182ef4e6727
[roundup.git] / roundup / roundupdb.py
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.
187         
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
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
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.
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.
243 # Also broke the help string in two.
245 # Revision 1.3  2001/07/19 05:52:22  anthonybaxter
246 # Added CVS keywords Id and Log to all python files.