3821421a5d0fa32194bfe3247164d392d3e04580
1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
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"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 #
18 # $Id: roundupdb.py,v 1.46 2002-02-25 14:22:59 grubert Exp $
20 __doc__ = """
21 Extending hyperdb with types specific to issue-tracking.
22 """
24 import re, os, smtplib, socket, copy, time, random
25 import mimetools, MimeWriter, cStringIO
26 import base64, mimetypes
28 import hyperdb, date
30 # set to indicate to roundup not to actually _send_ email
31 # this var must contain a file to write the mail to
32 SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
34 class DesignatorError(ValueError):
35 pass
36 def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
37 ''' Take a foo123 and return ('foo', 123)
38 '''
39 m = dre.match(designator)
40 if m is None:
41 raise DesignatorError, '"%s" not a node designator'%designator
42 return m.group(1), m.group(2)
45 def extractUserFromList(users):
46 '''Given a list of users, try to extract the first non-anonymous user
47 and return that user, otherwise return None
48 '''
49 if len(users) > 1:
50 # make sure we don't match the anonymous or admin user
51 for user in users:
52 if user == '1': continue
53 if self.user.get(user, 'username') == 'anonymous': continue
54 # first valid match will do
55 return user
56 # well, I guess we have no choice
57 return user[0]
58 elif users:
59 return users[0]
60 return None
62 class Database:
63 def getuid(self):
64 """Return the id of the "user" node associated with the user
65 that owns this connection to the hyperdatabase."""
66 return self.user.lookup(self.journaltag)
68 def uidFromAddress(self, address, create=1):
69 ''' address is from the rfc822 module, and therefore is (name, addr)
71 user is created if they don't exist in the db already
72 '''
73 (realname, address) = address
75 # try a straight match of the address
76 user = extractUserFromList(self.user.stringFind(address=address))
77 if user is not None: return user
79 # try the user alternate addresses if possible
80 props = self.user.getprops()
81 if props.has_key('alternate_addresses'):
82 users = self.user.filter({'alternate_addresses': address},
83 [], [])
84 user = extractUserFromList(users)
85 if user is not None: return user
87 # try to match the username to the address (for local
88 # submissions where the address is empty)
89 user = extractUserFromList(self.user.stringFind(username=address))
91 # couldn't match address or username, so create a new user
92 if create:
93 return self.user.create(username=address, address=address,
94 realname=realname)
95 else:
96 return 0
98 _marker = []
99 # XXX: added the 'creator' faked attribute
100 class Class(hyperdb.Class):
101 # Overridden methods:
102 def __init__(self, db, classname, **properties):
103 if (properties.has_key('creation') or properties.has_key('activity')
104 or properties.has_key('creator')):
105 raise ValueError, '"creation", "activity" and "creator" are reserved'
106 hyperdb.Class.__init__(self, db, classname, **properties)
107 self.auditors = {'create': [], 'set': [], 'retire': []}
108 self.reactors = {'create': [], 'set': [], 'retire': []}
110 def create(self, **propvalues):
111 """These operations trigger detectors and can be vetoed. Attempts
112 to modify the "creation" or "activity" properties cause a KeyError.
113 """
114 if propvalues.has_key('creation') or propvalues.has_key('activity'):
115 raise KeyError, '"creation" and "activity" are reserved'
116 for audit in self.auditors['create']:
117 audit(self.db, self, None, propvalues)
118 nodeid = hyperdb.Class.create(self, **propvalues)
119 for react in self.reactors['create']:
120 react(self.db, self, nodeid, None)
121 return nodeid
123 def set(self, nodeid, **propvalues):
124 """These operations trigger detectors and can be vetoed. Attempts
125 to modify the "creation" or "activity" properties cause a KeyError.
126 """
127 if propvalues.has_key('creation') or propvalues.has_key('activity'):
128 raise KeyError, '"creation" and "activity" are reserved'
129 for audit in self.auditors['set']:
130 audit(self.db, self, nodeid, propvalues)
131 # Take a copy of the node dict so that the subsequent set
132 # operation doesn't modify the oldvalues structure.
133 try:
134 # try not using the cache initially
135 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid,
136 cache=0))
137 except IndexError:
138 # this will be needed if somone does a create() and set()
139 # with no intervening commit()
140 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
141 hyperdb.Class.set(self, nodeid, **propvalues)
142 for react in self.reactors['set']:
143 react(self.db, self, nodeid, oldvalues)
145 def retire(self, nodeid):
146 """These operations trigger detectors and can be vetoed. Attempts
147 to modify the "creation" or "activity" properties cause a KeyError.
148 """
149 for audit in self.auditors['retire']:
150 audit(self.db, self, nodeid, None)
151 hyperdb.Class.retire(self, nodeid)
152 for react in self.reactors['retire']:
153 react(self.db, self, nodeid, None)
155 def get(self, nodeid, propname, default=_marker, cache=1):
156 """Attempts to get the "creation" or "activity" properties should
157 do the right thing.
158 """
159 if propname == 'creation':
160 journal = self.db.getjournal(self.classname, nodeid)
161 if journal:
162 return self.db.getjournal(self.classname, nodeid)[0][1]
163 else:
164 # on the strange chance that there's no journal
165 return date.Date()
166 if propname == 'activity':
167 journal = self.db.getjournal(self.classname, nodeid)
168 if journal:
169 return self.db.getjournal(self.classname, nodeid)[-1][1]
170 else:
171 # on the strange chance that there's no journal
172 return date.Date()
173 if propname == 'creator':
174 journal = self.db.getjournal(self.classname, nodeid)
175 if journal:
176 name = self.db.getjournal(self.classname, nodeid)[0][2]
177 else:
178 return None
179 return self.db.user.lookup(name)
180 if default is not _marker:
181 return hyperdb.Class.get(self, nodeid, propname, default,
182 cache=cache)
183 else:
184 return hyperdb.Class.get(self, nodeid, propname, cache=cache)
186 def getprops(self, protected=1):
187 """In addition to the actual properties on the node, these
188 methods provide the "creation" and "activity" properties. If the
189 "protected" flag is true, we include protected properties - those
190 which may not be modified.
191 """
192 d = hyperdb.Class.getprops(self, protected=protected).copy()
193 if protected:
194 d['creation'] = hyperdb.Date()
195 d['activity'] = hyperdb.Date()
196 d['creator'] = hyperdb.Link("user")
197 return d
199 #
200 # Detector interface
201 #
202 def audit(self, event, detector):
203 """Register a detector
204 """
205 l = self.auditors[event]
206 if detector not in l:
207 self.auditors[event].append(detector)
209 def react(self, event, detector):
210 """Register a detector
211 """
212 l = self.reactors[event]
213 if detector not in l:
214 self.reactors[event].append(detector)
217 class FileClass(Class):
218 def create(self, **propvalues):
219 ''' snaffle the file propvalue and store in a file
220 '''
221 content = propvalues['content']
222 del propvalues['content']
223 newid = Class.create(self, **propvalues)
224 self.db.storefile(self.classname, newid, None, content)
225 return newid
227 def get(self, nodeid, propname, default=_marker, cache=1):
228 ''' trap the content propname and get it from the file
229 '''
231 poss_msg = 'Possibly a access right configuration problem.'
232 if propname == 'content':
233 try:
234 return self.db.getfile(self.classname, nodeid, None)
235 except IOError, (strerror):
236 # BUG: by catching this we donot see an error in the log.
237 return 'ERROR reading file: %s%s\n%s\n%s'%(
238 self.classname, nodeid, poss_msg, strerror)
239 if default is not _marker:
240 return Class.get(self, nodeid, propname, default, cache=cache)
241 else:
242 return Class.get(self, nodeid, propname, cache=cache)
244 def getprops(self, protected=1):
245 ''' In addition to the actual properties on the node, these methods
246 provide the "content" property. If the "protected" flag is true,
247 we include protected properties - those which may not be
248 modified.
249 '''
250 d = Class.getprops(self, protected=protected).copy()
251 if protected:
252 d['content'] = hyperdb.String()
253 return d
255 class MessageSendError(RuntimeError):
256 pass
258 class DetectorError(RuntimeError):
259 pass
261 # XXX deviation from spec - was called ItemClass
262 class IssueClass(Class):
264 # Overridden methods:
266 def __init__(self, db, classname, **properties):
267 """The newly-created class automatically includes the "messages",
268 "files", "nosy", and "superseder" properties. If the 'properties'
269 dictionary attempts to specify any of these properties or a
270 "creation" or "activity" property, a ValueError is raised."""
271 if not properties.has_key('title'):
272 properties['title'] = hyperdb.String()
273 if not properties.has_key('messages'):
274 properties['messages'] = hyperdb.Multilink("msg")
275 if not properties.has_key('files'):
276 properties['files'] = hyperdb.Multilink("file")
277 if not properties.has_key('nosy'):
278 properties['nosy'] = hyperdb.Multilink("user")
279 if not properties.has_key('superseder'):
280 properties['superseder'] = hyperdb.Multilink(classname)
281 Class.__init__(self, db, classname, **properties)
283 # New methods:
285 def addmessage(self, nodeid, summary, text):
286 """Add a message to an issue's mail spool.
288 A new "msg" node is constructed using the current date, the user that
289 owns the database connection as the author, and the specified summary
290 text.
292 The "files" and "recipients" fields are left empty.
294 The given text is saved as the body of the message and the node is
295 appended to the "messages" field of the specified issue.
296 """
298 def sendmessage(self, nodeid, msgid, change_note):
299 """Send a message to the members of an issue's nosy list.
301 The message is sent only to users on the nosy list who are not
302 already on the "recipients" list for the message.
304 These users are then added to the message's "recipients" list.
305 """
306 users = self.db.user
307 messages = self.db.msg
308 files = self.db.file
310 # figure the recipient ids
311 sendto = []
312 r = {}
313 recipients = messages.get(msgid, 'recipients')
314 for recipid in messages.get(msgid, 'recipients'):
315 r[recipid] = 1
317 # figure the author's id, and indicate they've received the message
318 authid = messages.get(msgid, 'author')
320 # get the current nosy list, we'll need it
321 nosy = self.get(nodeid, 'nosy')
323 # possibly send the message to the author, as long as they aren't
324 # anonymous
325 if (self.db.config.MESSAGES_TO_AUTHOR == 'yes' and
326 users.get(authid, 'username') != 'anonymous'):
327 sendto.append(authid)
328 r[authid] = 1
330 # now figure the nosy people who weren't recipients
331 for nosyid in nosy:
332 # Don't send nosy mail to the anonymous user (that user
333 # shouldn't appear in the nosy list, but just in case they
334 # do...)
335 if users.get(nosyid, 'username') == 'anonymous':
336 continue
337 # make sure they haven't seen the message already
338 if not r.has_key(nosyid):
339 # send it to them
340 sendto.append(nosyid)
341 recipients.append(nosyid)
343 # no new recipients
344 if not sendto:
345 return
347 # determine the messageid and inreplyto of the message
348 inreplyto = messages.get(msgid, 'inreplyto')
349 messageid = messages.get(msgid, 'messageid')
350 if not messageid:
351 # this is an old message that didn't get a messageid, so
352 # create one
353 messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
354 self.classname, nodeid, self.db.config.MAIL_DOMAIN)
355 messages.set(msgid, messageid=messageid)
357 # update the message's recipients list
358 messages.set(msgid, recipients=recipients)
360 # send an email to the people who missed out
361 sendto = [users.get(i, 'address') for i in sendto]
362 cn = self.classname
363 title = self.get(nodeid, 'title') or '%s message copy'%cn
364 # figure author information
365 authname = users.get(authid, 'realname')
366 if not authname:
367 authname = users.get(authid, 'username')
368 authaddr = users.get(authid, 'address')
369 if authaddr:
370 authaddr = ' <%s>'%authaddr
371 else:
372 authaddr = ''
374 # make the message body
375 m = ['']
377 # put in roundup's signature
378 if self.db.config.EMAIL_SIGNATURE_POSITION == 'top':
379 m.append(self.email_signature(nodeid, msgid))
381 # add author information
382 if len(self.get(nodeid,'messages')) == 1:
383 m.append("New submission from %s%s:"%(authname, authaddr))
384 else:
385 m.append("%s%s added the comment:"%(authname, authaddr))
386 m.append('')
388 # add the content
389 m.append(messages.get(msgid, 'content'))
391 # add the change note
392 if change_note:
393 m.append(change_note)
395 # put in roundup's signature
396 if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom':
397 m.append(self.email_signature(nodeid, msgid))
399 # get the files for this message
400 message_files = messages.get(msgid, 'files')
402 # create the message
403 message = cStringIO.StringIO()
404 writer = MimeWriter.MimeWriter(message)
405 writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, title))
406 writer.addheader('To', ', '.join(sendto))
407 writer.addheader('From', '%s <%s>'%(authname,
408 self.db.config.ISSUE_TRACKER_EMAIL))
409 writer.addheader('Reply-To', '%s <%s>'%(self.db.config.INSTANCE_NAME,
410 self.db.config.ISSUE_TRACKER_EMAIL))
411 writer.addheader('MIME-Version', '1.0')
412 if messageid:
413 writer.addheader('Message-Id', messageid)
414 if inreplyto:
415 writer.addheader('In-Reply-To', inreplyto)
417 # add a uniquely Roundup header to help filtering
418 writer.addheader('X-Roundup-Name', self.db.config.INSTANCE_NAME)
420 # attach files
421 if message_files:
422 part = writer.startmultipartbody('mixed')
423 part = writer.nextpart()
424 body = part.startbody('text/plain')
425 body.write('\n'.join(m))
426 for fileid in message_files:
427 name = files.get(fileid, 'name')
428 mime_type = files.get(fileid, 'type')
429 content = files.get(fileid, 'content')
430 part = writer.nextpart()
431 if mime_type == 'text/plain':
432 part.addheader('Content-Disposition',
433 'attachment;\n filename="%s"'%name)
434 part.addheader('Content-Transfer-Encoding', '7bit')
435 body = part.startbody('text/plain')
436 body.write(content)
437 else:
438 # some other type, so encode it
439 if not mime_type:
440 # this should have been done when the file was saved
441 mime_type = mimetypes.guess_type(name)[0]
442 if mime_type is None:
443 mime_type = 'application/octet-stream'
444 part.addheader('Content-Disposition',
445 'attachment;\n filename="%s"'%name)
446 part.addheader('Content-Transfer-Encoding', 'base64')
447 body = part.startbody(mime_type)
448 body.write(base64.encodestring(content))
449 writer.lastpart()
450 else:
451 body = writer.startbody('text/plain')
452 body.write('\n'.join(m))
454 # now try to send the message
455 if SENDMAILDEBUG:
456 open(SENDMAILDEBUG, 'w').write('FROM: %s\nTO: %s\n%s\n'%(
457 self.db.config.ADMIN_EMAIL,', '.join(sendto),message.getvalue()))
458 else:
459 try:
460 # send the message as admin so bounces are sent there
461 # instead of to roundup
462 smtp = smtplib.SMTP(self.db.config.MAILHOST)
463 smtp.sendmail(self.db.config.ADMIN_EMAIL, sendto,
464 message.getvalue())
465 except socket.error, value:
466 raise MessageSendError, \
467 "Couldn't send confirmation email: mailhost %s"%value
468 except smtplib.SMTPException, value:
469 raise MessageSendError, \
470 "Couldn't send confirmation email: %s"%value
472 def email_signature(self, nodeid, msgid):
473 ''' Add a signature to the e-mail with some useful information
474 '''
475 web = self.db.config.ISSUE_TRACKER_WEB + 'issue'+ nodeid
476 email = '"%s" <%s>'%(self.db.config.INSTANCE_NAME,
477 self.db.config.ISSUE_TRACKER_EMAIL)
478 line = '_' * max(len(web), len(email))
479 return '%s\n%s\n%s\n%s'%(line, email, web, line)
481 def generateCreateNote(self, nodeid):
482 """Generate a create note that lists initial property values
483 """
484 cn = self.classname
485 cl = self.db.classes[cn]
486 props = cl.getprops(protected=0)
488 # list the values
489 m = []
490 l = props.items()
491 l.sort()
492 for propname, prop in l:
493 value = cl.get(nodeid, propname, None)
494 # skip boring entries
495 if not value:
496 continue
497 if isinstance(prop, hyperdb.Link):
498 link = self.db.classes[prop.classname]
499 if value:
500 key = link.labelprop(default_to_id=1)
501 if key:
502 value = link.get(value, key)
503 else:
504 value = ''
505 elif isinstance(prop, hyperdb.Multilink):
506 if value is None: value = []
507 l = []
508 link = self.db.classes[prop.classname]
509 key = link.labelprop(default_to_id=1)
510 if key:
511 value = [link.get(entry, key) for entry in value]
512 value = ', '.join(value)
513 m.append('%s: %s'%(propname, value))
514 m.insert(0, '----------')
515 m.insert(0, '')
516 return '\n'.join(m)
518 def generateChangeNote(self, nodeid, oldvalues):
519 """Generate a change note that lists property changes
520 """
521 cn = self.classname
522 cl = self.db.classes[cn]
523 changed = {}
524 props = cl.getprops(protected=0)
526 # determine what changed
527 for key in oldvalues.keys():
528 if key in ['files','messages']: continue
529 new_value = cl.get(nodeid, key)
530 # the old value might be non existent
531 try:
532 old_value = oldvalues[key]
533 if type(new_value) is type([]):
534 new_value.sort()
535 old_value.sort()
536 if new_value != old_value:
537 changed[key] = old_value
538 except:
539 changed[key] = new_value
541 # list the changes
542 m = []
543 l = changed.items()
544 l.sort()
545 for propname, oldvalue in l:
546 prop = cl.properties[propname]
547 value = cl.get(nodeid, propname, None)
548 if isinstance(prop, hyperdb.Link):
549 link = self.db.classes[prop.classname]
550 key = link.labelprop(default_to_id=1)
551 if key:
552 if value:
553 value = link.get(value, key)
554 else:
555 value = ''
556 if oldvalue:
557 oldvalue = link.get(oldvalue, key)
558 else:
559 oldvalue = ''
560 change = '%s -> %s'%(oldvalue, value)
561 elif isinstance(prop, hyperdb.Multilink):
562 change = ''
563 if value is None: value = []
564 if oldvalue is None: oldvalue = []
565 l = []
566 link = self.db.classes[prop.classname]
567 key = link.labelprop(default_to_id=1)
568 # check for additions
569 for entry in value:
570 if entry in oldvalue: continue
571 if key:
572 l.append(link.get(entry, key))
573 else:
574 l.append(entry)
575 if l:
576 change = '+%s'%(', '.join(l))
577 l = []
578 # check for removals
579 for entry in oldvalue:
580 if entry in value: continue
581 if key:
582 l.append(link.get(entry, key))
583 else:
584 l.append(entry)
585 if l:
586 change += ' -%s'%(', '.join(l))
587 else:
588 change = '%s -> %s'%(oldvalue, value)
589 m.append('%s: %s'%(propname, change))
590 if m:
591 m.insert(0, '----------')
592 m.insert(0, '')
593 return '\n'.join(m)
595 #
596 # $Log: not supported by cvs2svn $
597 # Revision 1.44 2002/02/15 07:08:44 richard
598 # . Alternate email addresses are now available for users. See the MIGRATION
599 # file for info on how to activate the feature.
600 #
601 # Revision 1.43 2002/02/14 22:33:15 richard
602 # . Added a uniquely Roundup header to email, "X-Roundup-Name"
603 #
604 # Revision 1.42 2002/01/21 09:55:14 rochecompaan
605 # Properties in change note are now sorted
606 #
607 # Revision 1.41 2002/01/15 00:12:40 richard
608 # #503340 ] creating issue with [asignedto=p.ohly]
609 #
610 # Revision 1.40 2002/01/14 22:21:38 richard
611 # #503353 ] setting properties in initial email
612 #
613 # Revision 1.39 2002/01/14 02:20:15 richard
614 # . changed all config accesses so they access either the instance or the
615 # config attriubute on the db. This means that all config is obtained from
616 # instance_config instead of the mish-mash of classes. This will make
617 # switching to a ConfigParser setup easier too, I hope.
618 #
619 # At a minimum, this makes migration a _little_ easier (a lot easier in the
620 # 0.5.0 switch, I hope!)
621 #
622 # Revision 1.38 2002/01/10 05:57:45 richard
623 # namespace clobberation
624 #
625 # Revision 1.37 2002/01/08 04:12:05 richard
626 # Changed message-id format to "<%s.%s.%s%s@%s>" so it complies with RFC822
627 #
628 # Revision 1.36 2002/01/02 02:31:38 richard
629 # Sorry for the huge checkin message - I was only intending to implement #496356
630 # but I found a number of places where things had been broken by transactions:
631 # . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
632 # for _all_ roundup-generated smtp messages to be sent to.
633 # . the transaction cache had broken the roundupdb.Class set() reactors
634 # . newly-created author users in the mailgw weren't being committed to the db
635 #
636 # Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
637 # on when I found that stuff :):
638 # . #496356 ] Use threading in messages
639 # . detectors were being registered multiple times
640 # . added tests for mailgw
641 # . much better attaching of erroneous messages in the mail gateway
642 #
643 # Revision 1.35 2001/12/20 15:43:01 rochecompaan
644 # Features added:
645 # . Multilink properties are now displayed as comma separated values in
646 # a textbox
647 # . The add user link is now only visible to the admin user
648 # . Modified the mail gateway to reject submissions from unknown
649 # addresses if ANONYMOUS_ACCESS is denied
650 #
651 # Revision 1.34 2001/12/17 03:52:48 richard
652 # Implemented file store rollback. As a bonus, the hyperdb is now capable of
653 # storing more than one file per node - if a property name is supplied,
654 # the file is called designator.property.
655 # I decided not to migrate the existing files stored over to the new naming
656 # scheme - the FileClass just doesn't specify the property name.
657 #
658 # Revision 1.33 2001/12/16 10:53:37 richard
659 # take a copy of the node dict so that the subsequent set
660 # operation doesn't modify the oldvalues structure
661 #
662 # Revision 1.32 2001/12/15 23:48:35 richard
663 # Added ROUNDUPDBSENDMAILDEBUG so one can test the sendmail method without
664 # actually sending mail :)
665 #
666 # Revision 1.31 2001/12/15 19:24:39 rochecompaan
667 # . Modified cgi interface to change properties only once all changes are
668 # collected, files created and messages generated.
669 # . Moved generation of change note to nosyreactors.
670 # . We now check for changes to "assignedto" to ensure it's added to the
671 # nosy list.
672 #
673 # Revision 1.30 2001/12/12 21:47:45 richard
674 # . Message author's name appears in From: instead of roundup instance name
675 # (which still appears in the Reply-To:)
676 # . envelope-from is now set to the roundup-admin and not roundup itself so
677 # delivery reports aren't sent to roundup (thanks Patrick Ohly)
678 #
679 # Revision 1.29 2001/12/11 04:50:49 richard
680 # fixed the order of the blank line and '-------' line
681 #
682 # Revision 1.28 2001/12/10 22:20:01 richard
683 # Enabled transaction support in the bsddb backend. It uses the anydbm code
684 # where possible, only replacing methods where the db is opened (it uses the
685 # btree opener specifically.)
686 # Also cleaned up some change note generation.
687 # Made the backends package work with pydoc too.
688 #
689 # Revision 1.27 2001/12/10 21:02:53 richard
690 # only insert the -------- change note marker if there is a change note
691 #
692 # Revision 1.26 2001/12/05 14:26:44 rochecompaan
693 # Removed generation of change note from "sendmessage" in roundupdb.py.
694 # The change note is now generated when the message is created.
695 #
696 # Revision 1.25 2001/11/30 20:28:10 rochecompaan
697 # Property changes are now completely traceable, whether changes are
698 # made through the web or by email
699 #
700 # Revision 1.24 2001/11/30 11:29:04 rochecompaan
701 # Property changes are now listed in emails generated by Roundup
702 #
703 # Revision 1.23 2001/11/27 03:17:13 richard
704 # oops
705 #
706 # Revision 1.22 2001/11/27 03:00:50 richard
707 # couple of bugfixes from latest patch integration
708 #
709 # Revision 1.21 2001/11/26 22:55:56 richard
710 # Feature:
711 # . Added INSTANCE_NAME to configuration - used in web and email to identify
712 # the instance.
713 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
714 # signature info in e-mails.
715 # . Some more flexibility in the mail gateway and more error handling.
716 # . Login now takes you to the page you back to the were denied access to.
717 #
718 # Fixed:
719 # . Lots of bugs, thanks Roché and others on the devel mailing list!
720 #
721 # Revision 1.20 2001/11/25 10:11:14 jhermann
722 # Typo fix
723 #
724 # Revision 1.19 2001/11/22 15:46:42 jhermann
725 # Added module docstrings to all modules.
726 #
727 # Revision 1.18 2001/11/15 10:36:17 richard
728 # . incorporated patch from Roch'e Compaan implementing attachments in nosy
729 # e-mail
730 #
731 # Revision 1.17 2001/11/12 22:01:06 richard
732 # Fixed issues with nosy reaction and author copies.
733 #
734 # Revision 1.16 2001/10/30 00:54:45 richard
735 # Features:
736 # . #467129 ] Lossage when username=e-mail-address
737 # . #473123 ] Change message generation for author
738 # . MailGW now moves 'resolved' to 'chatting' on receiving e-mail for an issue.
739 #
740 # Revision 1.15 2001/10/23 01:00:18 richard
741 # Re-enabled login and registration access after lopping them off via
742 # disabling access for anonymous users.
743 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
744 # a couple of bugs while I was there. Probably introduced a couple, but
745 # things seem to work OK at the moment.
746 #
747 # Revision 1.14 2001/10/21 07:26:35 richard
748 # feature #473127: Filenames. I modified the file.index and htmltemplate
749 # source so that the filename is used in the link and the creation
750 # information is displayed.
751 #
752 # Revision 1.13 2001/10/21 00:45:15 richard
753 # Added author identification to e-mail messages from roundup.
754 #
755 # Revision 1.12 2001/10/04 02:16:15 richard
756 # Forgot to pass the protected flag down *sigh*.
757 #
758 # Revision 1.11 2001/10/04 02:12:42 richard
759 # Added nicer command-line item adding: passing no arguments will enter an
760 # interactive more which asks for each property in turn. While I was at it, I
761 # fixed an implementation problem WRT the spec - I wasn't raising a
762 # ValueError if the key property was missing from a create(). Also added a
763 # protected=boolean argument to getprops() so we can list only the mutable
764 # properties (defaults to yes, which lists the immutables).
765 #
766 # Revision 1.10 2001/08/07 00:24:42 richard
767 # stupid typo
768 #
769 # Revision 1.9 2001/08/07 00:15:51 richard
770 # Added the copyright/license notice to (nearly) all files at request of
771 # Bizar Software.
772 #
773 # Revision 1.8 2001/08/02 06:38:17 richard
774 # Roundupdb now appends "mailing list" information to its messages which
775 # include the e-mail address and web interface address. Templates may
776 # override this in their db classes to include specific information (support
777 # instructions, etc).
778 #
779 # Revision 1.7 2001/07/30 02:38:31 richard
780 # get() now has a default arg - for migration only.
781 #
782 # Revision 1.6 2001/07/30 00:05:54 richard
783 # Fixed IssueClass so that superseders links to its classname rather than
784 # hard-coded to "issue".
785 #
786 # Revision 1.5 2001/07/29 07:01:39 richard
787 # Added vim command to all source so that we don't get no steenkin' tabs :)
788 #
789 # Revision 1.4 2001/07/29 04:05:37 richard
790 # Added the fabricated property "id".
791 #
792 # Revision 1.3 2001/07/23 07:14:41 richard
793 # Moved the database backends off into backends.
794 #
795 # Revision 1.2 2001/07/22 12:09:32 richard
796 # Final commit of Grande Splite
797 #
798 # Revision 1.1 2001/07/22 11:58:35 richard
799 # More Grande Splite
800 #
801 #
802 # vim: set filetype=python ts=4 sw=4 et si