Code

f874db8cbc6336b89e84608d66201f10de4eafe6
[roundup.git] / roundup / roundupdb.py
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.53 2002-05-25 07:16:24 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 MimeWriter, cStringIO
26 import base64, quopri, 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(userClass, 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 userClass.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,
77             self.user.stringFind(address=address))
78         if user is not None: return user
80         # try the user alternate addresses if possible
81         props = self.user.getprops()
82         if props.has_key('alternate_addresses'):
83             users = self.user.filter(None, {'alternate_addresses': address},
84                 [], [])
85             user = extractUserFromList(self.user, users)
86             if user is not None: return user
88         # try to match the username to the address (for local
89         # submissions where the address is empty)
90         user = extractUserFromList(self.user,
91             self.user.stringFind(username=address))
93         # couldn't match address or username, so create a new user
94         if create:
95             return self.user.create(username=address, address=address,
96                 realname=realname)
97         else:
98             return 0
100 _marker = []
101 # XXX: added the 'creator' faked attribute
102 class Class(hyperdb.Class):
103     # Overridden methods:
104     def __init__(self, db, classname, **properties):
105         if (properties.has_key('creation') or properties.has_key('activity')
106                 or properties.has_key('creator')):
107             raise ValueError, '"creation", "activity" and "creator" are reserved'
108         hyperdb.Class.__init__(self, db, classname, **properties)
109         self.auditors = {'create': [], 'set': [], 'retire': []}
110         self.reactors = {'create': [], 'set': [], 'retire': []}
112     def create(self, **propvalues):
113         """These operations trigger detectors and can be vetoed.  Attempts
114         to modify the "creation" or "activity" properties cause a KeyError.
115         """
116         if propvalues.has_key('creation') or propvalues.has_key('activity'):
117             raise KeyError, '"creation" and "activity" are reserved'
118         for audit in self.auditors['create']:
119             audit(self.db, self, None, propvalues)
120         nodeid = hyperdb.Class.create(self, **propvalues)
121         for react in self.reactors['create']:
122             react(self.db, self, nodeid, None)
123         return nodeid
125     def set(self, nodeid, **propvalues):
126         """These operations trigger detectors and can be vetoed.  Attempts
127         to modify the "creation" or "activity" properties cause a KeyError.
128         """
129         if propvalues.has_key('creation') or propvalues.has_key('activity'):
130             raise KeyError, '"creation" and "activity" are reserved'
131         for audit in self.auditors['set']:
132             audit(self.db, self, nodeid, propvalues)
133         # Take a copy of the node dict so that the subsequent set
134         # operation doesn't modify the oldvalues structure.
135         try:
136             # try not using the cache initially
137             oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid,
138                 cache=0))
139         except IndexError:
140             # this will be needed if somone does a create() and set()
141             # with no intervening commit()
142             oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
143         hyperdb.Class.set(self, nodeid, **propvalues)
144         for react in self.reactors['set']:
145             react(self.db, self, nodeid, oldvalues)
147     def retire(self, nodeid):
148         """These operations trigger detectors and can be vetoed.  Attempts
149         to modify the "creation" or "activity" properties cause a KeyError.
150         """
151         for audit in self.auditors['retire']:
152             audit(self.db, self, nodeid, None)
153         hyperdb.Class.retire(self, nodeid)
154         for react in self.reactors['retire']:
155             react(self.db, self, nodeid, None)
157     def get(self, nodeid, propname, default=_marker, cache=1):
158         """Attempts to get the "creation" or "activity" properties should
159         do the right thing.
160         """
161         if propname == 'creation':
162             journal = self.db.getjournal(self.classname, nodeid)
163             if journal:
164                 return self.db.getjournal(self.classname, nodeid)[0][1]
165             else:
166                 # on the strange chance that there's no journal
167                 return date.Date()
168         if propname == 'activity':
169             journal = self.db.getjournal(self.classname, nodeid)
170             if journal:
171                 return self.db.getjournal(self.classname, nodeid)[-1][1]
172             else:
173                 # on the strange chance that there's no journal
174                 return date.Date()
175         if propname == 'creator':
176             journal = self.db.getjournal(self.classname, nodeid)
177             if journal:
178                 name = self.db.getjournal(self.classname, nodeid)[0][2]
179             else:
180                 return None
181             return self.db.user.lookup(name)
182         if default is not _marker:
183             return hyperdb.Class.get(self, nodeid, propname, default,
184                 cache=cache)
185         else:
186             return hyperdb.Class.get(self, nodeid, propname, cache=cache)
188     def getprops(self, protected=1):
189         """In addition to the actual properties on the node, these
190         methods provide the "creation" and "activity" properties. If the
191         "protected" flag is true, we include protected properties - those
192         which may not be modified.
193         """
194         d = hyperdb.Class.getprops(self, protected=protected).copy()
195         if protected:
196             d['creation'] = hyperdb.Date()
197             d['activity'] = hyperdb.Date()
198             d['creator'] = hyperdb.Link("user")
199         return d
201     #
202     # Detector interface
203     #
204     def audit(self, event, detector):
205         """Register a detector
206         """
207         l = self.auditors[event]
208         if detector not in l:
209             self.auditors[event].append(detector)
211     def react(self, event, detector):
212         """Register a detector
213         """
214         l = self.reactors[event]
215         if detector not in l:
216             self.reactors[event].append(detector)
219 class FileClass(Class):
220     def create(self, **propvalues):
221         ''' snaffle the file propvalue and store in a file
222         '''
223         content = propvalues['content']
224         del propvalues['content']
225         newid = Class.create(self, **propvalues)
226         self.db.storefile(self.classname, newid, None, content)
227         return newid
229     def get(self, nodeid, propname, default=_marker, cache=1):
230         ''' trap the content propname and get it from the file
231         '''
233         poss_msg = 'Possibly a access right configuration problem.'
234         if propname == 'content':
235             try:
236                 return self.db.getfile(self.classname, nodeid, None)
237             except IOError, (strerror):
238                 # BUG: by catching this we donot see an error in the log.
239                 return 'ERROR reading file: %s%s\n%s\n%s'%(
240                         self.classname, nodeid, poss_msg, strerror)
241         if default is not _marker:
242             return Class.get(self, nodeid, propname, default, cache=cache)
243         else:
244             return Class.get(self, nodeid, propname, cache=cache)
246     def getprops(self, protected=1):
247         ''' In addition to the actual properties on the node, these methods
248             provide the "content" property. If the "protected" flag is true,
249             we include protected properties - those which may not be
250             modified.
251         '''
252         d = Class.getprops(self, protected=protected).copy()
253         if protected:
254             d['content'] = hyperdb.String()
255         return d
257 class MessageSendError(RuntimeError):
258     pass
260 class DetectorError(RuntimeError):
261     pass
263 # XXX deviation from spec - was called ItemClass
264 class IssueClass(Class):
266     # Overridden methods:
268     def __init__(self, db, classname, **properties):
269         """The newly-created class automatically includes the "messages",
270         "files", "nosy", and "superseder" properties.  If the 'properties'
271         dictionary attempts to specify any of these properties or a
272         "creation" or "activity" property, a ValueError is raised."""
273         if not properties.has_key('title'):
274             properties['title'] = hyperdb.String()
275         if not properties.has_key('messages'):
276             properties['messages'] = hyperdb.Multilink("msg")
277         if not properties.has_key('files'):
278             properties['files'] = hyperdb.Multilink("file")
279         if not properties.has_key('nosy'):
280             properties['nosy'] = hyperdb.Multilink("user")
281         if not properties.has_key('superseder'):
282             properties['superseder'] = hyperdb.Multilink(classname)
283         Class.__init__(self, db, classname, **properties)
285     # New methods:
287     def addmessage(self, nodeid, summary, text):
288         """Add a message to an issue's mail spool.
290         A new "msg" node is constructed using the current date, the user that
291         owns the database connection as the author, and the specified summary
292         text.
294         The "files" and "recipients" fields are left empty.
296         The given text is saved as the body of the message and the node is
297         appended to the "messages" field of the specified issue.
298         """
300     def nosymessage(self, nodeid, msgid, change_note):
301         """Send a message to the members of an issue's nosy list.
303         The message is sent only to users on the nosy list who are not
304         already on the "recipients" list for the message.
305         
306         These users are then added to the message's "recipients" list.
307         """
308         users = self.db.user
309         messages = self.db.msg
311         # figure the recipient ids
312         sendto = []
313         r = {}
314         recipients = messages.get(msgid, 'recipients')
315         for recipid in messages.get(msgid, 'recipients'):
316             r[recipid] = 1
318         # figure the author's id, and indicate they've received the message
319         authid = messages.get(msgid, 'author')
321         # get the current nosy list, we'll need it
322         nosy = self.get(nodeid, 'nosy')
324         # possibly send the message to the author, as long as they aren't
325         # anonymous
326         if (self.db.config.MESSAGES_TO_AUTHOR == 'yes' and
327                 users.get(authid, 'username') != 'anonymous'):
328             sendto.append(authid)
329         r[authid] = 1
331         # now figure the nosy people who weren't recipients
332         for nosyid in nosy:
333             # Don't send nosy mail to the anonymous user (that user
334             # shouldn't appear in the nosy list, but just in case they
335             # do...)
336             if users.get(nosyid, 'username') == 'anonymous':
337                 continue
338             # make sure they haven't seen the message already
339             if not r.has_key(nosyid):
340                 # send it to them
341                 sendto.append(nosyid)
342                 recipients.append(nosyid)
344         # we have new recipients
345         if sendto:
346             # map userids to addresses
347             sendto = [users.get(i, 'address') for i in sendto]
349             # update the message's recipients list
350             messages.set(msgid, recipients=recipients)
352             # send the message
353             self.send_message(nodeid, msgid, change_note, sendto)
355     # XXX backwards compatibility - don't remove
356     sendmessage = nosymessage
358     def send_message(self, nodeid, msgid, note, sendto):
359         '''Actually send the nominated message from this node to the sendto
360            recipients, with the note appended.
361         '''
362         users = self.db.user
363         messages = self.db.msg
364         files = self.db.file
366         # determine the messageid and inreplyto of the message
367         inreplyto = messages.get(msgid, 'inreplyto')
368         messageid = messages.get(msgid, 'messageid')
370         # make up a messageid if there isn't one (web edit)
371         if not messageid:
372             # this is an old message that didn't get a messageid, so
373             # create one
374             messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
375                 self.classname, nodeid, self.db.config.MAIL_DOMAIN)
376             messages.set(msgid, messageid=messageid)
378         # send an email to the people who missed out
379         cn = self.classname
380         title = self.get(nodeid, 'title') or '%s message copy'%cn
381         # figure author information
382         authid = messages.get(msgid, 'author')
383         authname = users.get(authid, 'realname')
384         if not authname:
385             authname = users.get(authid, 'username')
386         authaddr = users.get(authid, 'address')
387         if authaddr:
388             authaddr = ' <%s>'%authaddr
389         else:
390             authaddr = ''
392         # make the message body
393         m = ['']
395         # put in roundup's signature
396         if self.db.config.EMAIL_SIGNATURE_POSITION == 'top':
397             m.append(self.email_signature(nodeid, msgid))
399         # add author information
400         if len(self.get(nodeid,'messages')) == 1:
401             m.append("New submission from %s%s:"%(authname, authaddr))
402         else:
403             m.append("%s%s added the comment:"%(authname, authaddr))
404         m.append('')
406         # add the content
407         m.append(messages.get(msgid, 'content'))
409         # add the change note
410         if note:
411             m.append(note)
413         # put in roundup's signature
414         if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom':
415             m.append(self.email_signature(nodeid, msgid))
417         # encode the content as quoted-printable
418         content = cStringIO.StringIO('\n'.join(m))
419         content_encoded = cStringIO.StringIO()
420         quopri.encode(content, content_encoded, 0)
421         content_encoded = content_encoded.getvalue()
423         # get the files for this message
424         message_files = messages.get(msgid, 'files')
426         # make sure the To line is always the same (for testing mostly)
427         sendto.sort()
429         # create the message
430         message = cStringIO.StringIO()
431         writer = MimeWriter.MimeWriter(message)
432         writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, title))
433         writer.addheader('To', ', '.join(sendto))
434         writer.addheader('From', '%s <%s>'%(authname,
435             self.db.config.ISSUE_TRACKER_EMAIL))
436         writer.addheader('Reply-To', '%s <%s>'%(self.db.config.INSTANCE_NAME,
437             self.db.config.ISSUE_TRACKER_EMAIL))
438         writer.addheader('MIME-Version', '1.0')
439         if messageid:
440             writer.addheader('Message-Id', messageid)
441         if inreplyto:
442             writer.addheader('In-Reply-To', inreplyto)
444         # add a uniquely Roundup header to help filtering
445         writer.addheader('X-Roundup-Name', self.db.config.INSTANCE_NAME)
447         # attach files
448         if message_files:
449             part = writer.startmultipartbody('mixed')
450             part = writer.nextpart()
451             part.addheader('Content-Transfer-Encoding', 'quoted-printable')
452             body = part.startbody('text/plain')
453             body.write(content_encoded)
454             for fileid in message_files:
455                 name = files.get(fileid, 'name')
456                 mime_type = files.get(fileid, 'type')
457                 content = files.get(fileid, 'content')
458                 part = writer.nextpart()
459                 if mime_type == 'text/plain':
460                     part.addheader('Content-Disposition',
461                         'attachment;\n filename="%s"'%name)
462                     part.addheader('Content-Transfer-Encoding', '7bit')
463                     body = part.startbody('text/plain')
464                     body.write(content)
465                 else:
466                     # some other type, so encode it
467                     if not mime_type:
468                         # this should have been done when the file was saved
469                         mime_type = mimetypes.guess_type(name)[0]
470                     if mime_type is None:
471                         mime_type = 'application/octet-stream'
472                     part.addheader('Content-Disposition',
473                         'attachment;\n filename="%s"'%name)
474                     part.addheader('Content-Transfer-Encoding', 'base64')
475                     body = part.startbody(mime_type)
476                     body.write(base64.encodestring(content))
477             writer.lastpart()
478         else:
479             writer.addheader('Content-Transfer-Encoding', 'quoted-printable')
480             body = writer.startbody('text/plain')
481             body.write(content_encoded)
483         # now try to send the message
484         if SENDMAILDEBUG:
485             open(SENDMAILDEBUG, 'w').write('FROM: %s\nTO: %s\n%s\n'%(
486                 self.db.config.ADMIN_EMAIL,
487                 ', '.join(sendto),message.getvalue()))
488         else:
489             try:
490                 # send the message as admin so bounces are sent there
491                 # instead of to roundup
492                 smtp = smtplib.SMTP(self.db.config.MAILHOST)
493                 smtp.sendmail(self.db.config.ADMIN_EMAIL, sendto,
494                     message.getvalue())
495             except socket.error, value:
496                 raise MessageSendError, \
497                     "Couldn't send confirmation email: mailhost %s"%value
498             except smtplib.SMTPException, value:
499                 raise MessageSendError, \
500                     "Couldn't send confirmation email: %s"%value
502     def email_signature(self, nodeid, msgid):
503         ''' Add a signature to the e-mail with some useful information
504         '''
505         web = self.db.config.ISSUE_TRACKER_WEB + 'issue'+ nodeid
506         email = '"%s" <%s>'%(self.db.config.INSTANCE_NAME,
507             self.db.config.ISSUE_TRACKER_EMAIL)
508         line = '_' * max(len(web), len(email))
509         return '%s\n%s\n%s\n%s'%(line, email, web, line)
511     def generateCreateNote(self, nodeid):
512         """Generate a create note that lists initial property values
513         """
514         cn = self.classname
515         cl = self.db.classes[cn]
516         props = cl.getprops(protected=0)
518         # list the values
519         m = []
520         l = props.items()
521         l.sort()
522         for propname, prop in l:
523             value = cl.get(nodeid, propname, None)
524             # skip boring entries
525             if not value:
526                 continue
527             if isinstance(prop, hyperdb.Link):
528                 link = self.db.classes[prop.classname]
529                 if value:
530                     key = link.labelprop(default_to_id=1)
531                     if key:
532                         value = link.get(value, key)
533                 else:
534                     value = ''
535             elif isinstance(prop, hyperdb.Multilink):
536                 if value is None: value = []
537                 l = []
538                 link = self.db.classes[prop.classname]
539                 key = link.labelprop(default_to_id=1)
540                 if key:
541                     value = [link.get(entry, key) for entry in value]
542                 value.sort()
543                 value = ', '.join(value)
544             m.append('%s: %s'%(propname, value))
545         m.insert(0, '----------')
546         m.insert(0, '')
547         return '\n'.join(m)
549     def generateChangeNote(self, nodeid, oldvalues):
550         """Generate a change note that lists property changes
551         """
552         cn = self.classname
553         cl = self.db.classes[cn]
554         changed = {}
555         props = cl.getprops(protected=0)
557         # determine what changed
558         for key in oldvalues.keys():
559             if key in ['files','messages']: continue
560             new_value = cl.get(nodeid, key)
561             # the old value might be non existent
562             try:
563                 old_value = oldvalues[key]
564                 if type(new_value) is type([]):
565                     new_value.sort()
566                     old_value.sort()
567                 if new_value != old_value:
568                     changed[key] = old_value
569             except:
570                 changed[key] = new_value
572         # list the changes
573         m = []
574         l = changed.items()
575         l.sort()
576         for propname, oldvalue in l:
577             prop = props[propname]
578             value = cl.get(nodeid, propname, None)
579             if isinstance(prop, hyperdb.Link):
580                 link = self.db.classes[prop.classname]
581                 key = link.labelprop(default_to_id=1)
582                 if key:
583                     if value:
584                         value = link.get(value, key)
585                     else:
586                         value = ''
587                     if oldvalue:
588                         oldvalue = link.get(oldvalue, key)
589                     else:
590                         oldvalue = ''
591                 change = '%s -> %s'%(oldvalue, value)
592             elif isinstance(prop, hyperdb.Multilink):
593                 change = ''
594                 if value is None: value = []
595                 if oldvalue is None: oldvalue = []
596                 l = []
597                 link = self.db.classes[prop.classname]
598                 key = link.labelprop(default_to_id=1)
599                 # check for additions
600                 for entry in value:
601                     if entry in oldvalue: continue
602                     if key:
603                         l.append(link.get(entry, key))
604                     else:
605                         l.append(entry)
606                 if l:
607                     change = '+%s'%(', '.join(l))
608                     l = []
609                 # check for removals
610                 for entry in oldvalue:
611                     if entry in value: continue
612                     if key:
613                         l.append(link.get(entry, key))
614                     else:
615                         l.append(entry)
616                 if l:
617                     change += ' -%s'%(', '.join(l))
618             else:
619                 change = '%s -> %s'%(oldvalue, value)
620             m.append('%s: %s'%(propname, change))
621         if m:
622             m.insert(0, '----------')
623             m.insert(0, '')
624         return '\n'.join(m)
627 # $Log: not supported by cvs2svn $
628 # Revision 1.52  2002/05/15 03:27:16  richard
629 #  . fixed SCRIPT_NAME in ZRoundup for instances not at top level of Zope
630 #    (thanks dman)
631 #  . fixed some sorting issues that were breaking some unit tests under py2.2
632 #  . mailgw test output dir was confusing the init test (but only on 2.2 *shrug*)
634 # fixed bug in the init unit test that meant only the bsddb test ran if it
635 # could (it clobbered the anydbm test)
637 # Revision 1.51  2002/04/08 03:46:42  richard
638 # make it work
640 # Revision 1.50  2002/04/08 03:40:31  richard
641 #  . added a "detectors" directory for people to put their useful auditors and
642 #    reactors in. Note - the roundupdb.IssueClass.sendmessage method has been
643 #    split and renamed "nosymessage" specifically for things like the nosy
644 #    reactor, and "send_message" which just sends the message.
646 # The initial detector is one that we'll be using here at ekit - it bounces new
647 # issue messages to a team address.
649 # Revision 1.49.2.1  2002/04/19 19:54:42  rochecompaan
650 # cgi_client.py
651 #     removed search link for the time being
652 #     moved rendering of matches to htmltemplate
653 # hyperdb.py
654 #     filtering of nodes on full text search incorporated in filter method
655 # roundupdb.py
656 #     added paramater to call of filter method
657 # roundup_indexer.py
658 #     added search method to RoundupIndexer class
660 # Revision 1.49  2002/03/19 06:41:49  richard
661 # Faster, easier, less mess ;)
663 # Revision 1.48  2002/03/18 18:32:00  rochecompaan
664 # All messages sent to the nosy list are now encoded as quoted-printable.
666 # Revision 1.47  2002/02/27 03:16:02  richard
667 # Fixed a couple of dodgy bits found by pychekcer.
669 # Revision 1.46  2002/02/25 14:22:59  grubert
670 #  . roundup db: catch only IOError in getfile.
672 # Revision 1.44  2002/02/15 07:08:44  richard
673 #  . Alternate email addresses are now available for users. See the MIGRATION
674 #    file for info on how to activate the feature.
676 # Revision 1.43  2002/02/14 22:33:15  richard
677 #  . Added a uniquely Roundup header to email, "X-Roundup-Name"
679 # Revision 1.42  2002/01/21 09:55:14  rochecompaan
680 # Properties in change note are now sorted
682 # Revision 1.41  2002/01/15 00:12:40  richard
683 # #503340 ] creating issue with [asignedto=p.ohly]
685 # Revision 1.40  2002/01/14 22:21:38  richard
686 # #503353 ] setting properties in initial email
688 # Revision 1.39  2002/01/14 02:20:15  richard
689 #  . changed all config accesses so they access either the instance or the
690 #    config attriubute on the db. This means that all config is obtained from
691 #    instance_config instead of the mish-mash of classes. This will make
692 #    switching to a ConfigParser setup easier too, I hope.
694 # At a minimum, this makes migration a _little_ easier (a lot easier in the
695 # 0.5.0 switch, I hope!)
697 # Revision 1.38  2002/01/10 05:57:45  richard
698 # namespace clobberation
700 # Revision 1.37  2002/01/08 04:12:05  richard
701 # Changed message-id format to "<%s.%s.%s%s@%s>" so it complies with RFC822
703 # Revision 1.36  2002/01/02 02:31:38  richard
704 # Sorry for the huge checkin message - I was only intending to implement #496356
705 # but I found a number of places where things had been broken by transactions:
706 #  . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
707 #    for _all_ roundup-generated smtp messages to be sent to.
708 #  . the transaction cache had broken the roundupdb.Class set() reactors
709 #  . newly-created author users in the mailgw weren't being committed to the db
711 # Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
712 # on when I found that stuff :):
713 #  . #496356 ] Use threading in messages
714 #  . detectors were being registered multiple times
715 #  . added tests for mailgw
716 #  . much better attaching of erroneous messages in the mail gateway
718 # Revision 1.35  2001/12/20 15:43:01  rochecompaan
719 # Features added:
720 #  .  Multilink properties are now displayed as comma separated values in
721 #     a textbox
722 #  .  The add user link is now only visible to the admin user
723 #  .  Modified the mail gateway to reject submissions from unknown
724 #     addresses if ANONYMOUS_ACCESS is denied
726 # Revision 1.34  2001/12/17 03:52:48  richard
727 # Implemented file store rollback. As a bonus, the hyperdb is now capable of
728 # storing more than one file per node - if a property name is supplied,
729 # the file is called designator.property.
730 # I decided not to migrate the existing files stored over to the new naming
731 # scheme - the FileClass just doesn't specify the property name.
733 # Revision 1.33  2001/12/16 10:53:37  richard
734 # take a copy of the node dict so that the subsequent set
735 # operation doesn't modify the oldvalues structure
737 # Revision 1.32  2001/12/15 23:48:35  richard
738 # Added ROUNDUPDBSENDMAILDEBUG so one can test the sendmail method without
739 # actually sending mail :)
741 # Revision 1.31  2001/12/15 19:24:39  rochecompaan
742 #  . Modified cgi interface to change properties only once all changes are
743 #    collected, files created and messages generated.
744 #  . Moved generation of change note to nosyreactors.
745 #  . We now check for changes to "assignedto" to ensure it's added to the
746 #    nosy list.
748 # Revision 1.30  2001/12/12 21:47:45  richard
749 #  . Message author's name appears in From: instead of roundup instance name
750 #    (which still appears in the Reply-To:)
751 #  . envelope-from is now set to the roundup-admin and not roundup itself so
752 #    delivery reports aren't sent to roundup (thanks Patrick Ohly)
754 # Revision 1.29  2001/12/11 04:50:49  richard
755 # fixed the order of the blank line and '-------' line
757 # Revision 1.28  2001/12/10 22:20:01  richard
758 # Enabled transaction support in the bsddb backend. It uses the anydbm code
759 # where possible, only replacing methods where the db is opened (it uses the
760 # btree opener specifically.)
761 # Also cleaned up some change note generation.
762 # Made the backends package work with pydoc too.
764 # Revision 1.27  2001/12/10 21:02:53  richard
765 # only insert the -------- change note marker if there is a change note
767 # Revision 1.26  2001/12/05 14:26:44  rochecompaan
768 # Removed generation of change note from "sendmessage" in roundupdb.py.
769 # The change note is now generated when the message is created.
771 # Revision 1.25  2001/11/30 20:28:10  rochecompaan
772 # Property changes are now completely traceable, whether changes are
773 # made through the web or by email
775 # Revision 1.24  2001/11/30 11:29:04  rochecompaan
776 # Property changes are now listed in emails generated by Roundup
778 # Revision 1.23  2001/11/27 03:17:13  richard
779 # oops
781 # Revision 1.22  2001/11/27 03:00:50  richard
782 # couple of bugfixes from latest patch integration
784 # Revision 1.21  2001/11/26 22:55:56  richard
785 # Feature:
786 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
787 #    the instance.
788 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
789 #    signature info in e-mails.
790 #  . Some more flexibility in the mail gateway and more error handling.
791 #  . Login now takes you to the page you back to the were denied access to.
793 # Fixed:
794 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
796 # Revision 1.20  2001/11/25 10:11:14  jhermann
797 # Typo fix
799 # Revision 1.19  2001/11/22 15:46:42  jhermann
800 # Added module docstrings to all modules.
802 # Revision 1.18  2001/11/15 10:36:17  richard
803 #  . incorporated patch from Roch'e Compaan implementing attachments in nosy
804 #     e-mail
806 # Revision 1.17  2001/11/12 22:01:06  richard
807 # Fixed issues with nosy reaction and author copies.
809 # Revision 1.16  2001/10/30 00:54:45  richard
810 # Features:
811 #  . #467129 ] Lossage when username=e-mail-address
812 #  . #473123 ] Change message generation for author
813 #  . MailGW now moves 'resolved' to 'chatting' on receiving e-mail for an issue.
815 # Revision 1.15  2001/10/23 01:00:18  richard
816 # Re-enabled login and registration access after lopping them off via
817 # disabling access for anonymous users.
818 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
819 # a couple of bugs while I was there. Probably introduced a couple, but
820 # things seem to work OK at the moment.
822 # Revision 1.14  2001/10/21 07:26:35  richard
823 # feature #473127: Filenames. I modified the file.index and htmltemplate
824 #  source so that the filename is used in the link and the creation
825 #  information is displayed.
827 # Revision 1.13  2001/10/21 00:45:15  richard
828 # Added author identification to e-mail messages from roundup.
830 # Revision 1.12  2001/10/04 02:16:15  richard
831 # Forgot to pass the protected flag down *sigh*.
833 # Revision 1.11  2001/10/04 02:12:42  richard
834 # Added nicer command-line item adding: passing no arguments will enter an
835 # interactive more which asks for each property in turn. While I was at it, I
836 # fixed an implementation problem WRT the spec - I wasn't raising a
837 # ValueError if the key property was missing from a create(). Also added a
838 # protected=boolean argument to getprops() so we can list only the mutable
839 # properties (defaults to yes, which lists the immutables).
841 # Revision 1.10  2001/08/07 00:24:42  richard
842 # stupid typo
844 # Revision 1.9  2001/08/07 00:15:51  richard
845 # Added the copyright/license notice to (nearly) all files at request of
846 # Bizar Software.
848 # Revision 1.8  2001/08/02 06:38:17  richard
849 # Roundupdb now appends "mailing list" information to its messages which
850 # include the e-mail address and web interface address. Templates may
851 # override this in their db classes to include specific information (support
852 # instructions, etc).
854 # Revision 1.7  2001/07/30 02:38:31  richard
855 # get() now has a default arg - for migration only.
857 # Revision 1.6  2001/07/30 00:05:54  richard
858 # Fixed IssueClass so that superseders links to its classname rather than
859 # hard-coded to "issue".
861 # Revision 1.5  2001/07/29 07:01:39  richard
862 # Added vim command to all source so that we don't get no steenkin' tabs :)
864 # Revision 1.4  2001/07/29 04:05:37  richard
865 # Added the fabricated property "id".
867 # Revision 1.3  2001/07/23 07:14:41  richard
868 # Moved the database backends off into backends.
870 # Revision 1.2  2001/07/22 12:09:32  richard
871 # Final commit of Grande Splite
873 # Revision 1.1  2001/07/22 11:58:35  richard
874 # More Grande Splite
877 # vim: set filetype=python ts=4 sw=4 et si