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.63 2002-07-26 08:26:59 richard Exp $
20 __doc__ = """
21 Extending hyperdb with types specific to issue-tracking.
22 """
24 import re, os, smtplib, socket, 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
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 Database:
40 def getuid(self):
41 """Return the id of the "user" node associated with the user
42 that owns this connection to the hyperdatabase."""
43 return self.user.lookup(self.journaltag)
45 class MessageSendError(RuntimeError):
46 pass
48 class DetectorError(RuntimeError):
49 pass
51 # XXX deviation from spec - was called ItemClass
52 class IssueClass:
53 """ This class is intended to be mixed-in with a hyperdb backend
54 implementation. The backend should provide a mechanism that
55 enforces the title, messages, files, nosy and superseder
56 properties:
57 properties['title'] = hyperdb.String(indexme='yes')
58 properties['messages'] = hyperdb.Multilink("msg")
59 properties['files'] = hyperdb.Multilink("file")
60 properties['nosy'] = hyperdb.Multilink("user")
61 properties['superseder'] = hyperdb.Multilink(classname)
62 """
64 # New methods:
65 def addmessage(self, nodeid, summary, text):
66 """Add a message to an issue's mail spool.
68 A new "msg" node is constructed using the current date, the user that
69 owns the database connection as the author, and the specified summary
70 text.
72 The "files" and "recipients" fields are left empty.
74 The given text is saved as the body of the message and the node is
75 appended to the "messages" field of the specified issue.
76 """
78 def nosymessage(self, nodeid, msgid, oldvalues):
79 """Send a message to the members of an issue's nosy list.
81 The message is sent only to users on the nosy list who are not
82 already on the "recipients" list for the message.
84 These users are then added to the message's "recipients" list.
85 """
86 users = self.db.user
87 messages = self.db.msg
89 # figure the recipient ids
90 sendto = []
91 r = {}
92 recipients = messages.get(msgid, 'recipients')
93 for recipid in messages.get(msgid, 'recipients'):
94 r[recipid] = 1
96 # figure the author's id, and indicate they've received the message
97 authid = messages.get(msgid, 'author')
99 # possibly send the message to the author, as long as they aren't
100 # anonymous
101 if (self.db.config.MESSAGES_TO_AUTHOR == 'yes' and
102 users.get(authid, 'username') != 'anonymous'):
103 sendto.append(authid)
104 r[authid] = 1
106 # now figure the nosy people who weren't recipients
107 nosy = self.get(nodeid, 'nosy')
108 for nosyid in nosy:
109 # Don't send nosy mail to the anonymous user (that user
110 # shouldn't appear in the nosy list, but just in case they
111 # do...)
112 if users.get(nosyid, 'username') == 'anonymous':
113 continue
114 # make sure they haven't seen the message already
115 if not r.has_key(nosyid):
116 # send it to them
117 sendto.append(nosyid)
118 recipients.append(nosyid)
120 # generate a change note
121 if oldvalues:
122 note = self.generateChangeNote(nodeid, oldvalues)
123 else:
124 note = self.generateCreateNote(nodeid)
126 # we have new recipients
127 if sendto:
128 # map userids to addresses
129 sendto = [users.get(i, 'address') for i in sendto]
131 # update the message's recipients list
132 messages.set(msgid, recipients=recipients)
134 # send the message
135 self.send_message(nodeid, msgid, note, sendto)
137 # XXX backwards compatibility - don't remove
138 sendmessage = nosymessage
140 def send_message(self, nodeid, msgid, note, sendto):
141 '''Actually send the nominated message from this node to the sendto
142 recipients, with the note appended.
143 '''
144 users = self.db.user
145 messages = self.db.msg
146 files = self.db.file
148 # determine the messageid and inreplyto of the message
149 inreplyto = messages.get(msgid, 'inreplyto')
150 messageid = messages.get(msgid, 'messageid')
152 # make up a messageid if there isn't one (web edit)
153 if not messageid:
154 # this is an old message that didn't get a messageid, so
155 # create one
156 messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
157 self.classname, nodeid, self.db.config.MAIL_DOMAIN)
158 messages.set(msgid, messageid=messageid)
160 # send an email to the people who missed out
161 cn = self.classname
162 title = self.get(nodeid, 'title') or '%s message copy'%cn
163 # figure author information
164 authid = messages.get(msgid, 'author')
165 authname = users.get(authid, 'realname')
166 if not authname:
167 authname = users.get(authid, 'username')
168 authaddr = users.get(authid, 'address')
169 if authaddr:
170 authaddr = " <%s>" % straddr( ('',authaddr) )
171 else:
172 authaddr = ''
174 # make the message body
175 m = ['']
177 # put in roundup's signature
178 if self.db.config.EMAIL_SIGNATURE_POSITION == 'top':
179 m.append(self.email_signature(nodeid, msgid))
181 # add author information
182 if len(self.get(nodeid,'messages')) == 1:
183 m.append("New submission from %s%s:"%(authname, authaddr))
184 else:
185 m.append("%s%s added the comment:"%(authname, authaddr))
186 m.append('')
188 # add the content
189 m.append(messages.get(msgid, 'content'))
191 # add the change note
192 if note:
193 m.append(note)
195 # put in roundup's signature
196 if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom':
197 m.append(self.email_signature(nodeid, msgid))
199 # encode the content as quoted-printable
200 content = cStringIO.StringIO('\n'.join(m))
201 content_encoded = cStringIO.StringIO()
202 quopri.encode(content, content_encoded, 0)
203 content_encoded = content_encoded.getvalue()
205 # get the files for this message
206 message_files = messages.get(msgid, 'files')
208 # make sure the To line is always the same (for testing mostly)
209 sendto.sort()
211 # create the message
212 message = cStringIO.StringIO()
213 writer = MimeWriter.MimeWriter(message)
214 writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, title))
215 writer.addheader('To', ', '.join(sendto))
216 writer.addheader('From', straddr(
217 (authname, self.db.config.ISSUE_TRACKER_EMAIL) ) )
218 writer.addheader('Reply-To', straddr(
219 (self.db.config.INSTANCE_NAME,
220 self.db.config.ISSUE_TRACKER_EMAIL) ) )
221 writer.addheader('MIME-Version', '1.0')
222 if messageid:
223 writer.addheader('Message-Id', messageid)
224 if inreplyto:
225 writer.addheader('In-Reply-To', inreplyto)
227 # add a uniquely Roundup header to help filtering
228 writer.addheader('X-Roundup-Name', self.db.config.INSTANCE_NAME)
230 # attach files
231 if message_files:
232 part = writer.startmultipartbody('mixed')
233 part = writer.nextpart()
234 part.addheader('Content-Transfer-Encoding', 'quoted-printable')
235 body = part.startbody('text/plain')
236 body.write(content_encoded)
237 for fileid in message_files:
238 name = files.get(fileid, 'name')
239 mime_type = files.get(fileid, 'type')
240 content = files.get(fileid, 'content')
241 part = writer.nextpart()
242 if mime_type == 'text/plain':
243 part.addheader('Content-Disposition',
244 'attachment;\n filename="%s"'%name)
245 part.addheader('Content-Transfer-Encoding', '7bit')
246 body = part.startbody('text/plain')
247 body.write(content)
248 else:
249 # some other type, so encode it
250 if not mime_type:
251 # this should have been done when the file was saved
252 mime_type = mimetypes.guess_type(name)[0]
253 if mime_type is None:
254 mime_type = 'application/octet-stream'
255 part.addheader('Content-Disposition',
256 'attachment;\n filename="%s"'%name)
257 part.addheader('Content-Transfer-Encoding', 'base64')
258 body = part.startbody(mime_type)
259 body.write(base64.encodestring(content))
260 writer.lastpart()
261 else:
262 writer.addheader('Content-Transfer-Encoding', 'quoted-printable')
263 body = writer.startbody('text/plain')
264 body.write(content_encoded)
266 # now try to send the message
267 if SENDMAILDEBUG:
268 open(SENDMAILDEBUG, 'w').write('FROM: %s\nTO: %s\n%s\n'%(
269 self.db.config.ADMIN_EMAIL,
270 ', '.join(sendto),message.getvalue()))
271 else:
272 try:
273 # send the message as admin so bounces are sent there
274 # instead of to roundup
275 smtp = smtplib.SMTP(self.db.config.MAILHOST)
276 smtp.sendmail(self.db.config.ADMIN_EMAIL, sendto,
277 message.getvalue())
278 except socket.error, value:
279 raise MessageSendError, \
280 "Couldn't send confirmation email: mailhost %s"%value
281 except smtplib.SMTPException, value:
282 raise MessageSendError, \
283 "Couldn't send confirmation email: %s"%value
285 def email_signature(self, nodeid, msgid):
286 ''' Add a signature to the e-mail with some useful information
287 '''
289 # simplistic check to see if the url is valid,
290 # then append a trailing slash if it is missing
291 base = self.db.config.ISSUE_TRACKER_WEB
292 if not isinstance(base , type('')) or not base.startswith('http://'):
293 base = "Configuration Error: ISSUE_TRACKER_WEB isn't a " \
294 "fully-qualified URL"
295 elif base[-1] != '/' :
296 base += '/'
297 web = base + 'issue'+ nodeid
299 # ensure the email address is properly quoted
300 email = straddr((self.db.config.INSTANCE_NAME,
301 self.db.config.ISSUE_TRACKER_EMAIL))
303 line = '_' * max(len(web), len(email))
304 return '%s\n%s\n%s\n%s'%(line, email, web, line)
307 def generateCreateNote(self, nodeid):
308 """Generate a create note that lists initial property values
309 """
310 cn = self.classname
311 cl = self.db.classes[cn]
312 props = cl.getprops(protected=0)
314 # list the values
315 m = []
316 l = props.items()
317 l.sort()
318 for propname, prop in l:
319 value = cl.get(nodeid, propname, None)
320 # skip boring entries
321 if not value:
322 continue
323 if isinstance(prop, hyperdb.Link):
324 link = self.db.classes[prop.classname]
325 if value:
326 key = link.labelprop(default_to_id=1)
327 if key:
328 value = link.get(value, key)
329 else:
330 value = ''
331 elif isinstance(prop, hyperdb.Multilink):
332 if value is None: value = []
333 l = []
334 link = self.db.classes[prop.classname]
335 key = link.labelprop(default_to_id=1)
336 if key:
337 value = [link.get(entry, key) for entry in value]
338 value.sort()
339 value = ', '.join(value)
340 m.append('%s: %s'%(propname, value))
341 m.insert(0, '----------')
342 m.insert(0, '')
343 return '\n'.join(m)
345 def generateChangeNote(self, nodeid, oldvalues):
346 """Generate a change note that lists property changes
347 """
348 if __debug__ :
349 if not isinstance(oldvalues, type({})) :
350 raise TypeError("'oldvalues' must be dict-like, not %s."%
351 type(oldvalues))
353 cn = self.classname
354 cl = self.db.classes[cn]
355 changed = {}
356 props = cl.getprops(protected=0)
358 # determine what changed
359 for key in oldvalues.keys():
360 if key in ['files','messages']: continue
361 new_value = cl.get(nodeid, key)
362 # the old value might be non existent
363 try:
364 old_value = oldvalues[key]
365 if type(new_value) is type([]):
366 new_value.sort()
367 old_value.sort()
368 if new_value != old_value:
369 changed[key] = old_value
370 except:
371 changed[key] = new_value
373 # list the changes
374 m = []
375 l = changed.items()
376 l.sort()
377 for propname, oldvalue in l:
378 prop = props[propname]
379 value = cl.get(nodeid, propname, None)
380 if isinstance(prop, hyperdb.Link):
381 link = self.db.classes[prop.classname]
382 key = link.labelprop(default_to_id=1)
383 if key:
384 if value:
385 value = link.get(value, key)
386 else:
387 value = ''
388 if oldvalue:
389 oldvalue = link.get(oldvalue, key)
390 else:
391 oldvalue = ''
392 change = '%s -> %s'%(oldvalue, value)
393 elif isinstance(prop, hyperdb.Multilink):
394 change = ''
395 if value is None: value = []
396 if oldvalue is None: oldvalue = []
397 l = []
398 link = self.db.classes[prop.classname]
399 key = link.labelprop(default_to_id=1)
400 # check for additions
401 for entry in value:
402 if entry in oldvalue: continue
403 if key:
404 l.append(link.get(entry, key))
405 else:
406 l.append(entry)
407 if l:
408 change = '+%s'%(', '.join(l))
409 l = []
410 # check for removals
411 for entry in oldvalue:
412 if entry in value: continue
413 if key:
414 l.append(link.get(entry, key))
415 else:
416 l.append(entry)
417 if l:
418 change += ' -%s'%(', '.join(l))
419 else:
420 change = '%s -> %s'%(oldvalue, value)
421 m.append('%s: %s'%(propname, change))
422 if m:
423 m.insert(0, '----------')
424 m.insert(0, '')
425 return '\n'.join(m)
427 #
428 # $Log: not supported by cvs2svn $
429 # Revision 1.62 2002/07/14 02:05:53 richard
430 # . all storage-specific code (ie. backend) is now implemented by the backends
431 #
432 # Revision 1.61 2002/07/09 04:19:09 richard
433 # Added reindex command to roundup-admin.
434 # Fixed reindex on first access.
435 # Also fixed reindexing of entries that change.
436 #
437 # Revision 1.60 2002/07/09 03:02:52 richard
438 # More indexer work:
439 # - all String properties may now be indexed too. Currently there's a bit of
440 # "issue" specific code in the actual searching which needs to be
441 # addressed. In a nutshell:
442 # + pass 'indexme="yes"' as a String() property initialisation arg, eg:
443 # file = FileClass(db, "file", name=String(), type=String(),
444 # comment=String(indexme="yes"))
445 # + the comment will then be indexed and be searchable, with the results
446 # related back to the issue that the file is linked to
447 # - as a result of this work, the FileClass has a default MIME type that may
448 # be overridden in a subclass, or by the use of a "type" property as is
449 # done in the default templates.
450 # - the regeneration of the indexes (if necessary) is done once the schema is
451 # set up in the dbinit.
452 #
453 # Revision 1.59 2002/06/18 03:55:25 dman13
454 # Fixed name/address display problem introduced by an earlier change.
455 # (instead of "name<addr>" display "name <addr>")
456 #
457 # Revision 1.58 2002/06/16 01:05:15 dman13
458 # Removed temporary workaround -- it seems it was a bug in the
459 # nosyreaction detector in the 0.4.1 extended template and has already
460 # been fixed in CVS. We'll see.
461 #
462 # Revision 1.57 2002/06/15 15:49:29 dman13
463 # Use 'email' instead of 'rfc822', if available.
464 # Don't use isinstance() on a string (not allowed in python 2.1).
465 # Return an error message instead of crashing if 'oldvalues' isn't a
466 # dict (in generateChangeNote).
467 #
468 # Revision 1.56 2002/06/14 03:54:21 dman13
469 # #565992 ] if ISSUE_TRACKER_WEB doesn't have the trailing '/', add it
470 #
471 # use the rfc822 module to ensure that every (oddball) email address and
472 # real-name is properly quoted
473 #
474 # Revision 1.55 2002/06/11 04:58:07 richard
475 # detabbing
476 #
477 # Revision 1.54 2002/05/29 01:16:17 richard
478 # Sorry about this huge checkin! It's fixing a lot of related stuff in one go
479 # though.
480 #
481 # . #541941 ] changing multilink properties by mail
482 # . #526730 ] search for messages capability
483 # . #505180 ] split MailGW.handle_Message
484 # - also changed cgi client since it was duplicating the functionality
485 # . build htmlbase if tests are run using CVS checkout (removed note from
486 # installation.txt)
487 # . don't create an empty message on email issue creation if the email is empty
488 #
489 # Revision 1.53 2002/05/25 07:16:24 rochecompaan
490 # Merged search_indexing-branch with HEAD
491 #
492 # Revision 1.52 2002/05/15 03:27:16 richard
493 # . fixed SCRIPT_NAME in ZRoundup for instances not at top level of Zope
494 # (thanks dman)
495 # . fixed some sorting issues that were breaking some unit tests under py2.2
496 # . mailgw test output dir was confusing the init test (but only on 2.2 *shrug*)
497 #
498 # fixed bug in the init unit test that meant only the bsddb test ran if it
499 # could (it clobbered the anydbm test)
500 #
501 # Revision 1.51 2002/04/08 03:46:42 richard
502 # make it work
503 #
504 # Revision 1.50 2002/04/08 03:40:31 richard
505 # . added a "detectors" directory for people to put their useful auditors and
506 # reactors in. Note - the roundupdb.IssueClass.sendmessage method has been
507 # split and renamed "nosymessage" specifically for things like the nosy
508 # reactor, and "send_message" which just sends the message.
509 #
510 # The initial detector is one that we'll be using here at ekit - it bounces new
511 # issue messages to a team address.
512 #
513 # Revision 1.49.2.1 2002/04/19 19:54:42 rochecompaan
514 # cgi_client.py
515 # removed search link for the time being
516 # moved rendering of matches to htmltemplate
517 # hyperdb.py
518 # filtering of nodes on full text search incorporated in filter method
519 # roundupdb.py
520 # added paramater to call of filter method
521 # roundup_indexer.py
522 # added search method to RoundupIndexer class
523 #
524 # Revision 1.49 2002/03/19 06:41:49 richard
525 # Faster, easier, less mess ;)
526 #
527 # Revision 1.48 2002/03/18 18:32:00 rochecompaan
528 # All messages sent to the nosy list are now encoded as quoted-printable.
529 #
530 # Revision 1.47 2002/02/27 03:16:02 richard
531 # Fixed a couple of dodgy bits found by pychekcer.
532 #
533 # Revision 1.46 2002/02/25 14:22:59 grubert
534 # . roundup db: catch only IOError in getfile.
535 #
536 # Revision 1.44 2002/02/15 07:08:44 richard
537 # . Alternate email addresses are now available for users. See the MIGRATION
538 # file for info on how to activate the feature.
539 #
540 # Revision 1.43 2002/02/14 22:33:15 richard
541 # . Added a uniquely Roundup header to email, "X-Roundup-Name"
542 #
543 # Revision 1.42 2002/01/21 09:55:14 rochecompaan
544 # Properties in change note are now sorted
545 #
546 # Revision 1.41 2002/01/15 00:12:40 richard
547 # #503340 ] creating issue with [asignedto=p.ohly]
548 #
549 # Revision 1.40 2002/01/14 22:21:38 richard
550 # #503353 ] setting properties in initial email
551 #
552 # Revision 1.39 2002/01/14 02:20:15 richard
553 # . changed all config accesses so they access either the instance or the
554 # config attriubute on the db. This means that all config is obtained from
555 # instance_config instead of the mish-mash of classes. This will make
556 # switching to a ConfigParser setup easier too, I hope.
557 #
558 # At a minimum, this makes migration a _little_ easier (a lot easier in the
559 # 0.5.0 switch, I hope!)
560 #
561 # Revision 1.38 2002/01/10 05:57:45 richard
562 # namespace clobberation
563 #
564 # Revision 1.37 2002/01/08 04:12:05 richard
565 # Changed message-id format to "<%s.%s.%s%s@%s>" so it complies with RFC822
566 #
567 # Revision 1.36 2002/01/02 02:31:38 richard
568 # Sorry for the huge checkin message - I was only intending to implement #496356
569 # but I found a number of places where things had been broken by transactions:
570 # . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
571 # for _all_ roundup-generated smtp messages to be sent to.
572 # . the transaction cache had broken the roundupdb.Class set() reactors
573 # . newly-created author users in the mailgw weren't being committed to the db
574 #
575 # Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
576 # on when I found that stuff :):
577 # . #496356 ] Use threading in messages
578 # . detectors were being registered multiple times
579 # . added tests for mailgw
580 # . much better attaching of erroneous messages in the mail gateway
581 #
582 # Revision 1.35 2001/12/20 15:43:01 rochecompaan
583 # Features added:
584 # . Multilink properties are now displayed as comma separated values in
585 # a textbox
586 # . The add user link is now only visible to the admin user
587 # . Modified the mail gateway to reject submissions from unknown
588 # addresses if ANONYMOUS_ACCESS is denied
589 #
590 # Revision 1.34 2001/12/17 03:52:48 richard
591 # Implemented file store rollback. As a bonus, the hyperdb is now capable of
592 # storing more than one file per node - if a property name is supplied,
593 # the file is called designator.property.
594 # I decided not to migrate the existing files stored over to the new naming
595 # scheme - the FileClass just doesn't specify the property name.
596 #
597 # Revision 1.33 2001/12/16 10:53:37 richard
598 # take a copy of the node dict so that the subsequent set
599 # operation doesn't modify the oldvalues structure
600 #
601 # Revision 1.32 2001/12/15 23:48:35 richard
602 # Added ROUNDUPDBSENDMAILDEBUG so one can test the sendmail method without
603 # actually sending mail :)
604 #
605 # Revision 1.31 2001/12/15 19:24:39 rochecompaan
606 # . Modified cgi interface to change properties only once all changes are
607 # collected, files created and messages generated.
608 # . Moved generation of change note to nosyreactors.
609 # . We now check for changes to "assignedto" to ensure it's added to the
610 # nosy list.
611 #
612 # Revision 1.30 2001/12/12 21:47:45 richard
613 # . Message author's name appears in From: instead of roundup instance name
614 # (which still appears in the Reply-To:)
615 # . envelope-from is now set to the roundup-admin and not roundup itself so
616 # delivery reports aren't sent to roundup (thanks Patrick Ohly)
617 #
618 # Revision 1.29 2001/12/11 04:50:49 richard
619 # fixed the order of the blank line and '-------' line
620 #
621 # Revision 1.28 2001/12/10 22:20:01 richard
622 # Enabled transaction support in the bsddb backend. It uses the anydbm code
623 # where possible, only replacing methods where the db is opened (it uses the
624 # btree opener specifically.)
625 # Also cleaned up some change note generation.
626 # Made the backends package work with pydoc too.
627 #
628 # Revision 1.27 2001/12/10 21:02:53 richard
629 # only insert the -------- change note marker if there is a change note
630 #
631 # Revision 1.26 2001/12/05 14:26:44 rochecompaan
632 # Removed generation of change note from "sendmessage" in roundupdb.py.
633 # The change note is now generated when the message is created.
634 #
635 # Revision 1.25 2001/11/30 20:28:10 rochecompaan
636 # Property changes are now completely traceable, whether changes are
637 # made through the web or by email
638 #
639 # Revision 1.24 2001/11/30 11:29:04 rochecompaan
640 # Property changes are now listed in emails generated by Roundup
641 #
642 # Revision 1.23 2001/11/27 03:17:13 richard
643 # oops
644 #
645 # Revision 1.22 2001/11/27 03:00:50 richard
646 # couple of bugfixes from latest patch integration
647 #
648 # Revision 1.21 2001/11/26 22:55:56 richard
649 # Feature:
650 # . Added INSTANCE_NAME to configuration - used in web and email to identify
651 # the instance.
652 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
653 # signature info in e-mails.
654 # . Some more flexibility in the mail gateway and more error handling.
655 # . Login now takes you to the page you back to the were denied access to.
656 #
657 # Fixed:
658 # . Lots of bugs, thanks Roché and others on the devel mailing list!
659 #
660 # Revision 1.20 2001/11/25 10:11:14 jhermann
661 # Typo fix
662 #
663 # Revision 1.19 2001/11/22 15:46:42 jhermann
664 # Added module docstrings to all modules.
665 #
666 # Revision 1.18 2001/11/15 10:36:17 richard
667 # . incorporated patch from Roch'e Compaan implementing attachments in nosy
668 # e-mail
669 #
670 # Revision 1.17 2001/11/12 22:01:06 richard
671 # Fixed issues with nosy reaction and author copies.
672 #
673 # Revision 1.16 2001/10/30 00:54:45 richard
674 # Features:
675 # . #467129 ] Lossage when username=e-mail-address
676 # . #473123 ] Change message generation for author
677 # . MailGW now moves 'resolved' to 'chatting' on receiving e-mail for an issue.
678 #
679 # Revision 1.15 2001/10/23 01:00:18 richard
680 # Re-enabled login and registration access after lopping them off via
681 # disabling access for anonymous users.
682 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
683 # a couple of bugs while I was there. Probably introduced a couple, but
684 # things seem to work OK at the moment.
685 #
686 # Revision 1.14 2001/10/21 07:26:35 richard
687 # feature #473127: Filenames. I modified the file.index and htmltemplate
688 # source so that the filename is used in the link and the creation
689 # information is displayed.
690 #
691 # Revision 1.13 2001/10/21 00:45:15 richard
692 # Added author identification to e-mail messages from roundup.
693 #
694 # Revision 1.12 2001/10/04 02:16:15 richard
695 # Forgot to pass the protected flag down *sigh*.
696 #
697 # Revision 1.11 2001/10/04 02:12:42 richard
698 # Added nicer command-line item adding: passing no arguments will enter an
699 # interactive more which asks for each property in turn. While I was at it, I
700 # fixed an implementation problem WRT the spec - I wasn't raising a
701 # ValueError if the key property was missing from a create(). Also added a
702 # protected=boolean argument to getprops() so we can list only the mutable
703 # properties (defaults to yes, which lists the immutables).
704 #
705 # Revision 1.10 2001/08/07 00:24:42 richard
706 # stupid typo
707 #
708 # Revision 1.9 2001/08/07 00:15:51 richard
709 # Added the copyright/license notice to (nearly) all files at request of
710 # Bizar Software.
711 #
712 # Revision 1.8 2001/08/02 06:38:17 richard
713 # Roundupdb now appends "mailing list" information to its messages which
714 # include the e-mail address and web interface address. Templates may
715 # override this in their db classes to include specific information (support
716 # instructions, etc).
717 #
718 # Revision 1.7 2001/07/30 02:38:31 richard
719 # get() now has a default arg - for migration only.
720 #
721 # Revision 1.6 2001/07/30 00:05:54 richard
722 # Fixed IssueClass so that superseders links to its classname rather than
723 # hard-coded to "issue".
724 #
725 # Revision 1.5 2001/07/29 07:01:39 richard
726 # Added vim command to all source so that we don't get no steenkin' tabs :)
727 #
728 # Revision 1.4 2001/07/29 04:05:37 richard
729 # Added the fabricated property "id".
730 #
731 # Revision 1.3 2001/07/23 07:14:41 richard
732 # Moved the database backends off into backends.
733 #
734 # Revision 1.2 2001/07/22 12:09:32 richard
735 # Final commit of Grande Splite
736 #
737 # Revision 1.1 2001/07/22 11:58:35 richard
738 # More Grande Splite
739 #
740 #
741 # vim: set filetype=python ts=4 sw=4 et si