comparison roundup/roundupdb.py @ 858:2dd862af72ee

all storage-specific code (ie. backend) is now implemented by the backends
author Richard Jones <richard@users.sourceforge.net>
date Sun, 14 Jul 2002 02:05:54 +0000
parents 6d7a45c8464a
children 502a5ae11cc5
comparison
equal deleted inserted replaced
857:6dd691e37aa8 858:2dd862af72ee
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" 14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, 15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 # 17 #
18 # $Id: roundupdb.py,v 1.61 2002-07-09 04:19:09 richard Exp $ 18 # $Id: roundupdb.py,v 1.62 2002-07-14 02:05:53 richard Exp $
19 19
20 __doc__ = """ 20 __doc__ = """
21 Extending hyperdb with types specific to issue-tracking. 21 Extending hyperdb with types specific to issue-tracking.
22 """ 22 """
23 23
24 import re, os, smtplib, socket, copy, time, random 24 import re, os, smtplib, socket, time, random
25 import MimeWriter, cStringIO 25 import MimeWriter, cStringIO
26 import base64, quopri, mimetypes 26 import base64, quopri, mimetypes
27 # if available, use the 'email' module, otherwise fallback to 'rfc822' 27 # if available, use the 'email' module, otherwise fallback to 'rfc822'
28 try : 28 try :
29 from email.Utils import dump_address_pair as straddr 29 from email.Utils import dump_address_pair as straddr
30 except ImportError : 30 except ImportError :
31 from rfc822 import dump_address_pair as straddr 31 from rfc822 import dump_address_pair as straddr
32 32
33 import hyperdb, date 33 import hyperdb
34 34
35 # set to indicate to roundup not to actually _send_ email 35 # set to indicate to roundup not to actually _send_ email
36 # this var must contain a file to write the mail to 36 # this var must contain a file to write the mail to
37 SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '') 37 SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
38
39 class DesignatorError(ValueError):
40 pass
41 def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
42 ''' Take a foo123 and return ('foo', 123)
43 '''
44 m = dre.match(designator)
45 if m is None:
46 raise DesignatorError, '"%s" not a node designator'%designator
47 return m.group(1), m.group(2)
48 38
49 39
50 def extractUserFromList(userClass, users): 40 def extractUserFromList(userClass, users):
51 '''Given a list of users, try to extract the first non-anonymous user 41 '''Given a list of users, try to extract the first non-anonymous user
52 and return that user, otherwise return None 42 and return that user, otherwise return None
100 return self.user.create(username=address, address=address, 90 return self.user.create(username=address, address=address,
101 realname=realname) 91 realname=realname)
102 else: 92 else:
103 return 0 93 return 0
104 94
105 _marker = []
106 # XXX: added the 'creator' faked attribute
107 class Class(hyperdb.Class):
108 # Overridden methods:
109 def __init__(self, db, classname, **properties):
110 if (properties.has_key('creation') or properties.has_key('activity')
111 or properties.has_key('creator')):
112 raise ValueError, '"creation", "activity" and "creator" are reserved'
113 hyperdb.Class.__init__(self, db, classname, **properties)
114 self.auditors = {'create': [], 'set': [], 'retire': []}
115 self.reactors = {'create': [], 'set': [], 'retire': []}
116
117 def create(self, **propvalues):
118 """These operations trigger detectors and can be vetoed. Attempts
119 to modify the "creation" or "activity" properties cause a KeyError.
120 """
121 if propvalues.has_key('creation') or propvalues.has_key('activity'):
122 raise KeyError, '"creation" and "activity" are reserved'
123 self.fireAuditors('create', None, propvalues)
124 nodeid = hyperdb.Class.create(self, **propvalues)
125 self.fireReactors('create', nodeid, None)
126 return nodeid
127
128 def set(self, nodeid, **propvalues):
129 """These operations trigger detectors and can be vetoed. Attempts
130 to modify the "creation" or "activity" properties cause a KeyError.
131 """
132 if propvalues.has_key('creation') or propvalues.has_key('activity'):
133 raise KeyError, '"creation" and "activity" are reserved'
134 self.fireAuditors('set', nodeid, propvalues)
135 # Take a copy of the node dict so that the subsequent set
136 # operation doesn't modify the oldvalues structure.
137 try:
138 # try not using the cache initially
139 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid,
140 cache=0))
141 except IndexError:
142 # this will be needed if somone does a create() and set()
143 # with no intervening commit()
144 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
145 hyperdb.Class.set(self, nodeid, **propvalues)
146 self.fireReactors('set', nodeid, oldvalues)
147
148 def retire(self, nodeid):
149 """These operations trigger detectors and can be vetoed. Attempts
150 to modify the "creation" or "activity" properties cause a KeyError.
151 """
152 self.fireAuditors('retire', nodeid, None)
153 hyperdb.Class.retire(self, nodeid)
154 self.fireReactors('retire', nodeid, None)
155
156 def get(self, nodeid, propname, default=_marker, cache=1):
157 """Attempts to get the "creation" or "activity" properties should
158 do the right thing.
159 """
160 if propname == 'creation':
161 journal = self.db.getjournal(self.classname, nodeid)
162 if journal:
163 return self.db.getjournal(self.classname, nodeid)[0][1]
164 else:
165 # on the strange chance that there's no journal
166 return date.Date()
167 if propname == 'activity':
168 journal = self.db.getjournal(self.classname, nodeid)
169 if journal:
170 return self.db.getjournal(self.classname, nodeid)[-1][1]
171 else:
172 # on the strange chance that there's no journal
173 return date.Date()
174 if propname == 'creator':
175 journal = self.db.getjournal(self.classname, nodeid)
176 if journal:
177 name = self.db.getjournal(self.classname, nodeid)[0][2]
178 else:
179 return None
180 return self.db.user.lookup(name)
181 if default is not _marker:
182 return hyperdb.Class.get(self, nodeid, propname, default,
183 cache=cache)
184 else:
185 return hyperdb.Class.get(self, nodeid, propname, cache=cache)
186
187 def getprops(self, protected=1):
188 """In addition to the actual properties on the node, these
189 methods provide the "creation" and "activity" properties. If the
190 "protected" flag is true, we include protected properties - those
191 which may not be modified.
192 """
193 d = hyperdb.Class.getprops(self, protected=protected).copy()
194 if protected:
195 d['creation'] = hyperdb.Date()
196 d['activity'] = hyperdb.Date()
197 d['creator'] = hyperdb.Link("user")
198 return d
199
200 #
201 # Detector interface
202 #
203 def audit(self, event, detector):
204 """Register a detector
205 """
206 l = self.auditors[event]
207 if detector not in l:
208 self.auditors[event].append(detector)
209
210 def fireAuditors(self, action, nodeid, newvalues):
211 """Fire all registered auditors.
212 """
213 for audit in self.auditors[action]:
214 audit(self.db, self, nodeid, newvalues)
215
216 def react(self, event, detector):
217 """Register a detector
218 """
219 l = self.reactors[event]
220 if detector not in l:
221 self.reactors[event].append(detector)
222
223 def fireReactors(self, action, nodeid, oldvalues):
224 """Fire all registered reactors.
225 """
226 for react in self.reactors[action]:
227 react(self.db, self, nodeid, oldvalues)
228
229 class FileClass(Class):
230 '''This class defines a large chunk of data. To support this, it has a
231 mandatory String property "content" which is typically saved off
232 externally to the hyperdb.
233
234 The default MIME type of this data is defined by the
235 "default_mime_type" class attribute, which may be overridden by each
236 node if the class defines a "type" String property.
237 '''
238 default_mime_type = 'text/plain'
239
240 def create(self, **propvalues):
241 ''' snaffle the file propvalue and store in a file
242 '''
243 content = propvalues['content']
244 del propvalues['content']
245 newid = Class.create(self, **propvalues)
246 self.db.storefile(self.classname, newid, None, content)
247 return newid
248
249 def get(self, nodeid, propname, default=_marker, cache=1):
250 ''' trap the content propname and get it from the file
251 '''
252
253 poss_msg = 'Possibly a access right configuration problem.'
254 if propname == 'content':
255 try:
256 return self.db.getfile(self.classname, nodeid, None)
257 except IOError, (strerror):
258 # BUG: by catching this we donot see an error in the log.
259 return 'ERROR reading file: %s%s\n%s\n%s'%(
260 self.classname, nodeid, poss_msg, strerror)
261 if default is not _marker:
262 return Class.get(self, nodeid, propname, default, cache=cache)
263 else:
264 return Class.get(self, nodeid, propname, cache=cache)
265
266 def getprops(self, protected=1):
267 ''' In addition to the actual properties on the node, these methods
268 provide the "content" property. If the "protected" flag is true,
269 we include protected properties - those which may not be
270 modified.
271 '''
272 d = Class.getprops(self, protected=protected).copy()
273 if protected:
274 d['content'] = hyperdb.String()
275 return d
276
277 def index(self, nodeid):
278 ''' Index the node in the search index.
279
280 We want to index the content in addition to the normal String
281 property indexing.
282 '''
283 # perform normal indexing
284 Class.index(self, nodeid)
285
286 # get the content to index
287 content = self.get(nodeid, 'content')
288
289 # figure the mime type
290 if self.properties.has_key('type'):
291 mime_type = self.get(nodeid, 'type')
292 else:
293 mime_type = self.default_mime_type
294
295 # and index!
296 self.db.indexer.add_text((self.classname, nodeid, 'content'), content,
297 mime_type)
298
299 class MessageSendError(RuntimeError): 95 class MessageSendError(RuntimeError):
300 pass 96 pass
301 97
302 class DetectorError(RuntimeError): 98 class DetectorError(RuntimeError):
303 pass 99 pass
304 100
305 # XXX deviation from spec - was called ItemClass 101 # XXX deviation from spec - was called ItemClass
306 class IssueClass(Class): 102 class IssueClass:
307 103 """ This class is intended to be mixed-in with a hyperdb backend
308 # Overridden methods: 104 implementation. The backend should provide a mechanism that
309 105 enforces the title, messages, files, nosy and superseder
310 def __init__(self, db, classname, **properties): 106 properties:
311 """The newly-created class automatically includes the "messages",
312 "files", "nosy", and "superseder" properties. If the 'properties'
313 dictionary attempts to specify any of these properties or a
314 "creation" or "activity" property, a ValueError is raised."""
315 if not properties.has_key('title'):
316 properties['title'] = hyperdb.String(indexme='yes') 107 properties['title'] = hyperdb.String(indexme='yes')
317 if not properties.has_key('messages'):
318 properties['messages'] = hyperdb.Multilink("msg") 108 properties['messages'] = hyperdb.Multilink("msg")
319 if not properties.has_key('files'):
320 properties['files'] = hyperdb.Multilink("file") 109 properties['files'] = hyperdb.Multilink("file")
321 if not properties.has_key('nosy'):
322 properties['nosy'] = hyperdb.Multilink("user") 110 properties['nosy'] = hyperdb.Multilink("user")
323 if not properties.has_key('superseder'):
324 properties['superseder'] = hyperdb.Multilink(classname) 111 properties['superseder'] = hyperdb.Multilink(classname)
325 Class.__init__(self, db, classname, **properties) 112 """
326 113
327 # New methods: 114 # New methods:
328
329 def addmessage(self, nodeid, summary, text): 115 def addmessage(self, nodeid, summary, text):
330 """Add a message to an issue's mail spool. 116 """Add a message to an issue's mail spool.
331 117
332 A new "msg" node is constructed using the current date, the user that 118 A new "msg" node is constructed using the current date, the user that
333 owns the database connection as the author, and the specified summary 119 owns the database connection as the author, and the specified summary
551 ''' 337 '''
552 338
553 # simplistic check to see if the url is valid, 339 # simplistic check to see if the url is valid,
554 # then append a trailing slash if it is missing 340 # then append a trailing slash if it is missing
555 base = self.db.config.ISSUE_TRACKER_WEB 341 base = self.db.config.ISSUE_TRACKER_WEB
556 if not isinstance( base , type('') ) or not base.startswith( "http://" ) : 342 if not isinstance(base , type('')) or not base.startswith('http://'):
557 base = "Configuration Error: ISSUE_TRACKER_WEB isn't a fully-qualified URL" 343 base = "Configuration Error: ISSUE_TRACKER_WEB isn't a " \
344 "fully-qualified URL"
558 elif base[-1] != '/' : 345 elif base[-1] != '/' :
559 base += '/' 346 base += '/'
560 web = base + 'issue'+ nodeid 347 web = base + 'issue'+ nodeid
561 348
562 # ensure the email address is properly quoted 349 # ensure the email address is properly quoted
563 email = straddr( (self.db.config.INSTANCE_NAME , 350 email = straddr((self.db.config.INSTANCE_NAME,
564 self.db.config.ISSUE_TRACKER_EMAIL) ) 351 self.db.config.ISSUE_TRACKER_EMAIL))
565 352
566 line = '_' * max(len(web), len(email)) 353 line = '_' * max(len(web), len(email))
567 return '%s\n%s\n%s\n%s'%(line, email, web, line) 354 return '%s\n%s\n%s\n%s'%(line, email, web, line)
568 355
569 356
606 return '\n'.join(m) 393 return '\n'.join(m)
607 394
608 def generateChangeNote(self, nodeid, oldvalues): 395 def generateChangeNote(self, nodeid, oldvalues):
609 """Generate a change note that lists property changes 396 """Generate a change note that lists property changes
610 """ 397 """
611
612 if __debug__ : 398 if __debug__ :
613 if not isinstance( oldvalues , type({}) ) : 399 if not isinstance(oldvalues, type({})) :
614 raise TypeError( 400 raise TypeError("'oldvalues' must be dict-like, not %s."%
615 "'oldvalues' must be dict-like, not %s." 401 type(oldvalues))
616 % str(type(oldvalues)) )
617 402
618 cn = self.classname 403 cn = self.classname
619 cl = self.db.classes[cn] 404 cl = self.db.classes[cn]
620 changed = {} 405 changed = {}
621 props = cl.getprops(protected=0) 406 props = cl.getprops(protected=0)
689 m.insert(0, '') 474 m.insert(0, '')
690 return '\n'.join(m) 475 return '\n'.join(m)
691 476
692 # 477 #
693 # $Log: not supported by cvs2svn $ 478 # $Log: not supported by cvs2svn $
479 # Revision 1.61 2002/07/09 04:19:09 richard
480 # Added reindex command to roundup-admin.
481 # Fixed reindex on first access.
482 # Also fixed reindexing of entries that change.
483 #
694 # Revision 1.60 2002/07/09 03:02:52 richard 484 # Revision 1.60 2002/07/09 03:02:52 richard
695 # More indexer work: 485 # More indexer work:
696 # - all String properties may now be indexed too. Currently there's a bit of 486 # - all String properties may now be indexed too. Currently there's a bit of
697 # "issue" specific code in the actual searching which needs to be 487 # "issue" specific code in the actual searching which needs to be
698 # addressed. In a nutshell: 488 # addressed. In a nutshell:

Roundup Issue Tracker: http://roundup-tracker.org/