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