From 023725ca22cc80484cd636aaea5b5a5006c189d4 Mon Sep 17 00:00:00 2001 From: richard Date: Fri, 15 Feb 2002 07:08:45 +0000 Subject: [PATCH] . Alternate email addresses are now available for users. See the MIGRATION file for info on how to activate the feature. git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@629 57a73879-2fb5-44c3-a270-3262357dd7e2 --- CHANGES.txt | 3 ++ MIGRATION.txt | 38 ++++++++++++++++ roundup/cgi_client.py | 16 ++++--- roundup/htmltemplate.py | 29 +++++++++++- roundup/hyperdb.py | 12 ++++- roundup/roundupdb.py | 55 +++++++++++++++-------- roundup/templates/classic/dbinit.py | 14 +++++- roundup/templates/classic/html/user.item | 8 +++- roundup/templates/extended/dbinit.py | 14 +++++- roundup/templates/extended/html/user.item | 6 ++- test/test_htmltemplate.py | 37 ++++++++++++--- test/test_mailgw.py | 45 ++++++++++++++----- 12 files changed, 228 insertions(+), 49 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8f5716e..01b42ac 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,6 +6,9 @@ Feature: . #503204 ] mailgw needs a default class - partially done - the setting of additional properties can wait for a better configuration system. + . Alternate email addresses are now available for users. See the MIGRATION + file for info on how to activate the feature. + Fixed: . Clean up mail handling, multipart handling. diff --git a/MIGRATION.txt b/MIGRATION.txt index b40b0e8..5de7618 100644 --- a/MIGRATION.txt +++ b/MIGRATION.txt @@ -22,6 +22,44 @@ ANONYMOUS_REGISTER_MAIL rather than overloading ANONYMOUS_REGISTER. If the variable doesn't exist, then ANONYMOUS_REGISTER is tested as before. +Alternate E-Mail Addresses +-------------------------- + +If you add the property "alternate_addresses" to your user class, your users +will be able to register alternate email addresses that they may use to +communicate with roundup as. All email from roundup will continue to be sent +to their primary address. + +If you have not edited the dbinit.py file in your instance home directory, +you may simply copy the new dbinit.py file from the core code. If you used +the classic schema, the interfaces file is in: + + /roundup/templates/classic/dbinit.py + +If you used the extended schema, the file is in: + + /roundup/templates/extended/dbinit.py + +If you have modified your dbinit.py file, you need to edit the dbinit.py +file in your instance home directory. Find the lines which define the user +class: + + user = Class(db, "msg", + username=String(), password=Password(), + address=String(), realname=String(), + phone=String(), organisation=String(), + alternate_addresses=String()) + +You will also want to add the property to the user's details page. The +template for this is the "user.item" file in your instance home "html" +directory. Similar to above, you may copy the file from the roundup source if +you haven't modified it. Otherwise, add the following to the template: + + + +with appropriate labelling etc. See the standard template for an idea. + + Migrating from 0.3.x to 0.4.x ============================= diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py index ad521c4..661dbb0 100644 --- a/roundup/cgi_client.py +++ b/roundup/cgi_client.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: cgi_client.py,v 1.101 2002-02-14 23:39:18 richard Exp $ +# $Id: cgi_client.py,v 1.102 2002-02-15 07:08:44 richard Exp $ __doc__ = """ WWW request handler (also used in the stand-alone server). @@ -778,7 +778,7 @@ function submit_once() { return values = {'realname': '', 'organisation': '', 'address': '', 'phone': '', 'username': '', 'password': '', 'confirm': '', - 'action': action} + 'action': action, 'alternate_addresses': ''} if newuser_form is not None: for key in newuser_form.keys(): values[key] = newuser_form[key].value @@ -789,11 +789,13 @@ function submit_once() {
Name: - + Organisation: - + E-Mail Address: - + +Alternate E-mail Addresses: + Phone: Preferred Login name: @@ -1200,6 +1202,10 @@ def parsePropsFromForm(db, cl, form, nodeid=0): # # $Log: not supported by cvs2svn $ +# Revision 1.101 2002/02/14 23:39:18 richard +# . All forms now have "double-submit" protection when Javascript is enabled +# on the client-side. +# # Revision 1.100 2002/01/16 07:02:57 richard # . lots of date/interval related changes: # - more relaxed date format for input diff --git a/roundup/htmltemplate.py b/roundup/htmltemplate.py index e913dc2..75af618 100644 --- a/roundup/htmltemplate.py +++ b/roundup/htmltemplate.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: htmltemplate.py,v 1.72 2002-02-14 23:39:18 richard Exp $ +# $Id: htmltemplate.py,v 1.73 2002-02-15 07:08:44 richard Exp $ __doc__ = """ Template engine. @@ -216,6 +216,27 @@ class TemplateFunctions: s = _('Plain: bad propclass "%(propclass)s"')%locals() return s + def do_multiline(self, property, rows=5, cols=40): + ''' display a string property in a multiline text edit field + ''' + if not self.nodeid and self.form is None and self.filterspec is None: + return _('[Multiline: not called from item]') + + propclass = self.properties[property] + + # make sure this is a link property + if not isinstance(propclass, hyperdb.String): + return _('[Multiline: not a string]') + + # get the value + value = self.determine_value(property) + if value is None: + value = '' + + # display + return ''%( + property, rows, cols, value) + def do_menu(self, property, size=None, height=None, showid=0): ''' for a Link property, display a menu of the available choices ''' @@ -389,7 +410,7 @@ class TemplateFunctions: ''' if not self.nodeid: return _('[Download: not called from item]') - return self.do_link(property, is_download=1) + return self.do_link(property, is_download=1) def do_checklist(self, property, **args): @@ -1043,6 +1064,10 @@ class NewItemTemplate(TemplateFunctions): # # $Log: not supported by cvs2svn $ +# Revision 1.72 2002/02/14 23:39:18 richard +# . All forms now have "double-submit" protection when Javascript is enabled +# on the client-side. +# # Revision 1.71 2002/01/23 06:15:24 richard # real (non-string, duh) sorting of lists by node id # diff --git a/roundup/hyperdb.py b/roundup/hyperdb.py index e47c85a..fba1e5c 100644 --- a/roundup/hyperdb.py +++ b/roundup/hyperdb.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: hyperdb.py,v 1.53 2002-01-22 07:21:13 richard Exp $ +# $Id: hyperdb.py,v 1.54 2002-02-15 07:08:44 richard Exp $ __doc__ = """ Hyperdatabase implementation, especially field types. @@ -846,7 +846,7 @@ class Class: else: continue break - elif t == 2 and not v.search(node[k]): + elif t == 2 and node[k] is None or not v.search(node[k]): # RE search break elif t == 6 and node[k] != v: @@ -1066,6 +1066,14 @@ def Choice(name, *options): # # $Log: not supported by cvs2svn $ +# Revision 1.53 2002/01/22 07:21:13 richard +# . fixed back_bsddb so it passed the journal tests +# +# ... it didn't seem happy using the back_anydbm _open method, which is odd. +# Yet another occurrance of whichdb not being able to recognise older bsddb +# databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the +# process. +# # Revision 1.52 2002/01/21 16:33:19 rochecompaan # You can now use the roundup-admin tool to pack the database # diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py index a313d04..c77a341 100644 --- a/roundup/roundupdb.py +++ b/roundup/roundupdb.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundupdb.py,v 1.43 2002-02-14 22:33:15 richard Exp $ +# $Id: roundupdb.py,v 1.44 2002-02-15 07:08:44 richard Exp $ __doc__ = """ Extending hyperdb with types specific to issue-tracking. @@ -42,6 +42,23 @@ def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')): return m.group(1), m.group(2) +def extractUserFromList(users): + '''Given a list of users, try to extract the first non-anonymous user + and return that user, otherwise return None + ''' + if len(users) > 1: + # make sure we don't match the anonymous or admin user + for user in users: + if user == '1': continue + if self.user.get(user, 'username') == 'anonymous': continue + # first valid match will do + return user + # well, I guess we have no choice + return user[0] + elif users: + return users[0] + return None + class Database: def getuid(self): """Return the id of the "user" node associated with the user @@ -54,26 +71,25 @@ class Database: user is created if they don't exist in the db already ''' (realname, address) = address - users = self.user.stringFind(address=address) - for dummy in range(2): - if len(users) > 1: - # make sure we don't match the anonymous or admin user - for user in users: - if user == '1': continue - if self.user.get(user, 'username') == 'anonymous': continue - # first valid match will do - return user - # well, I guess we have no choice - return user[0] - elif users: - return users[0] - # try to match the username to the address (for local - # submissions where the address is empty) - users = self.user.stringFind(username=address) + + # try a straight match of the address + user = extractUserFromList(self.user.stringFind(address=address)) + if user is not None: return user + + # try the user alternate addresses if possible + props = self.user.getprops() + if props.has_key('alternate_addresses'): + users = self.user.filter({'alternate_addresses': address}, + [], []) + user = extractUserFromList(users) + if user is not None: return user + + # try to match the username to the address (for local + # submissions where the address is empty) + user = extractUserFromList(self.user.stringFind(username=address)) # couldn't match address or username, so create a new user if create: - print 'CREATING USER', address return self.user.create(username=address, address=address, realname=realname) else: @@ -571,6 +587,9 @@ class IssueClass(Class): # # $Log: not supported by cvs2svn $ +# Revision 1.43 2002/02/14 22:33:15 richard +# . Added a uniquely Roundup header to email, "X-Roundup-Name" +# # Revision 1.42 2002/01/21 09:55:14 rochecompaan # Properties in change note are now sorted # diff --git a/roundup/templates/classic/dbinit.py b/roundup/templates/classic/dbinit.py index f458c65..e95104b 100644 --- a/roundup/templates/classic/dbinit.py +++ b/roundup/templates/classic/dbinit.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: dbinit.py,v 1.14 2002-01-14 02:20:15 richard Exp $ +# $Id: dbinit.py,v 1.15 2002-02-15 07:08:44 richard Exp $ import os @@ -63,7 +63,8 @@ def open(name=None): user = Class(db, "user", username=String(), password=Password(), address=String(), realname=String(), - phone=String(), organisation=String()) + phone=String(), organisation=String(), + alternate_addresses=String()) user.setkey("username") msg = FileClass(db, "msg", @@ -122,6 +123,15 @@ def init(adminpw): # # $Log: not supported by cvs2svn $ +# Revision 1.14 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# # Revision 1.13 2002/01/02 02:31:38 richard # Sorry for the huge checkin message - I was only intending to implement #496356 # but I found a number of places where things had been broken by transactions: diff --git a/roundup/templates/classic/html/user.item b/roundup/templates/classic/html/user.item index 1af6823..3443ca2 100644 --- a/roundup/templates/classic/html/user.item +++ b/roundup/templates/classic/html/user.item @@ -1,4 +1,4 @@ - + @@ -29,6 +29,12 @@ + + + + diff --git a/roundup/templates/extended/dbinit.py b/roundup/templates/extended/dbinit.py index 62c546e..0c13a6f 100644 --- a/roundup/templates/extended/dbinit.py +++ b/roundup/templates/extended/dbinit.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: dbinit.py,v 1.19 2002-01-14 02:20:15 richard Exp $ +# $Id: dbinit.py,v 1.20 2002-02-15 07:08:44 richard Exp $ import os @@ -63,7 +63,8 @@ def open(name=None): user = Class(db, "user", username=String(), password=Password(), address=String(), realname=String(), - phone=String(), organisation=String()) + phone=String(), organisation=String(), + alternate_addresses=String()) user.setkey("username") msg = FileClass(db, "msg", @@ -173,6 +174,15 @@ def init(adminpw): # # $Log: not supported by cvs2svn $ +# Revision 1.19 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# # Revision 1.18 2002/01/02 02:31:38 richard # Sorry for the huge checkin message - I was only intending to implement #496356 # but I found a number of places where things had been broken by transactions: diff --git a/roundup/templates/extended/html/user.item b/roundup/templates/extended/html/user.item index d201251..76bd4e5 100644 --- a/roundup/templates/extended/html/user.item +++ b/roundup/templates/extended/html/user.item @@ -1,4 +1,4 @@ - +
E-mail address
Alternate + E-mail addresses
+ One address per line
 
@@ -29,6 +29,10 @@ + + + + diff --git a/test/test_htmltemplate.py b/test/test_htmltemplate.py index f18bd7b..80506f3 100644 --- a/test/test_htmltemplate.py +++ b/test/test_htmltemplate.py @@ -8,7 +8,7 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # -# $Id: test_htmltemplate.py,v 1.8 2002-02-06 03:47:16 richard Exp $ +# $Id: test_htmltemplate.py,v 1.9 2002-02-15 07:08:45 richard Exp $ import unittest, cgi, time @@ -37,13 +37,15 @@ class Class: return 'the key'+nodeid elif attribute == 'html': return 'hello, I am HTML' + elif attribute == 'multiline': + return 'hello\nworld' def list(self): return ['1', '2'] def getprops(self): return {'string': String(), 'date': Date(), 'interval': Interval(), 'link': Link('other'), 'multilink': Multilink('other'), 'password': Password(), 'html': String(), 'key': String(), - 'novalue': String(), 'filename': String()} + 'novalue': String(), 'filename': String(), 'multiline': String()} def labelprop(self): return 'key' @@ -140,9 +142,29 @@ class NodeCase(unittest.TestCase): self.assertEqual(self.tf.do_field('multilink', size=10), '') +# def do_multiline(self, property, rows=5, cols=40) + def testMultiline_string(self): + self.assertEqual(self.tf.do_multiline('multiline'), + '') + self.assertEqual(self.tf.do_multiline('multiline', rows=10), + '') + self.assertEqual(self.tf.do_multiline('multiline', cols=10), + '') + + def testMultiline_nonstring(self): + s = _('[Multiline: not a string]') + self.assertEqual(self.tf.do_multiline('date'), s) + self.assertEqual(self.tf.do_multiline('interval'), s) + self.assertEqual(self.tf.do_multiline('password'), s) + self.assertEqual(self.tf.do_multiline('link'), s) + self.assertEqual(self.tf.do_multiline('multilink'), s) + # def do_menu(self, property, size=None, height=None, showid=0): def testMenu_nonlinks(self): - s = _('[Menu: not a link]') + s = _('[Menu: not a link]') self.assertEqual(self.tf.do_menu('string'), s) self.assertEqual(self.tf.do_menu('date'), s) self.assertEqual(self.tf.do_menu('interval'), s) @@ -236,7 +258,7 @@ class NodeCase(unittest.TestCase): def testReldate_date(self): self.assertEqual(self.tf.do_reldate('date'), '- 2y 1m') - date = self.tf.cl.get('1', 'date') + date = self.tf.cl.get('1', 'date') self.assertEqual(self.tf.do_reldate('date', pretty=1), date.pretty()) # def do_download(self, property): @@ -247,7 +269,7 @@ class NodeCase(unittest.TestCase): def testDownload_string(self): self.assertEqual(self.tf.do_download('string'), 'Node 1: ' - 'I am a string') + 'I am a string') def testDownload_file(self): self.assertEqual(self.tf.do_download('filename', is_download=1), @@ -268,7 +290,7 @@ class NodeCase(unittest.TestCase): def testDownload_multilink(self): self.assertEqual(self.tf.do_download('multilink'), 'the key1, ' - 'the key2') + 'the key2') # def do_checklist(self, property, reverse=0): def testChecklink_nonlinks(self): @@ -314,6 +336,9 @@ def suite(): # # $Log: not supported by cvs2svn $ +# Revision 1.8 2002/02/06 03:47:16 richard +# . #511586 ] unittest FAIL: testReldate_date +# # Revision 1.7 2002/01/23 20:09:41 jhermann # Proper fix for failing test # diff --git a/test/test_mailgw.py b/test/test_mailgw.py index f877a1d..f6a5ba8 100644 --- a/test/test_mailgw.py +++ b/test/test_mailgw.py @@ -8,7 +8,7 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # -# $Id: test_mailgw.py,v 1.12 2002-02-15 00:13:38 richard Exp $ +# $Id: test_mailgw.py,v 1.13 2002-02-15 07:08:45 richard Exp $ import unittest, cStringIO, tempfile, os, shutil, errno, imp, sys @@ -34,7 +34,8 @@ class MailgwTestCase(unittest.TestCase): self.db.user.create(username='Chef', address='chef@bork.bork.bork') self.db.user.create(username='richard', address='richard@test') self.db.user.create(username='mary', address='mary@test') - self.db.user.create(username='john', address='john@test') + self.db.user.create(username='john', address='john@test', + alternate_addresses='jondoe@test\njohn.doe@test') def tearDown(self): if os.path.exists(os.environ['SENDMAILDEBUG']): @@ -44,7 +45,7 @@ class MailgwTestCase(unittest.TestCase): except OSError, error: if error.errno not in (errno.ENOENT, errno.ESRCH): raise - def testNewIssue(self): + def xtestNewIssue(self): message = cStringIO.StringIO('''Content-Type: text/plain; charset="iso-8859-1" From: Chef +To: issue_tracker@fill.me.in. +Message-Id: +Subject: [issue] Testing... + +This is a test submission of a new issue. +''') + userlist = self.db.user.list() + handler = self.instance.MailGW(self.instance, self.db) + handler.main(message) + if os.path.exists(os.environ['SENDMAILDEBUG']): + error = open(os.environ['SENDMAILDEBUG']).read() + self.assertEqual('no error', error) + self.assertEqual(userlist, self.db.user.list(), + "user created when it shouldn't have been") + + def xtestNewIssueNoClass(self): message = cStringIO.StringIO('''Content-Type: text/plain; charset="iso-8859-1" From: Chef
E-mail address
Alternate E-mail addresses