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