Mercurial > p > roundup > code
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: |
