diff options
| -rw-r--r-- | src/mailman/database/mailman.sql | 41 | ||||
| -rw-r--r-- | src/mailman/interfaces/address.py | 1 | ||||
| -rw-r--r-- | src/mailman/interfaces/user.py | 18 | ||||
| -rw-r--r-- | src/mailman/model/docs/users.txt | 87 | ||||
| -rw-r--r-- | src/mailman/model/user.py | 28 |
5 files changed, 155 insertions, 20 deletions
diff --git a/src/mailman/database/mailman.sql b/src/mailman/database/mailman.sql index 7c09fb79f..64dc21cde 100644 --- a/src/mailman/database/mailman.sql +++ b/src/mailman/database/mailman.sql @@ -5,7 +5,8 @@ CREATE TABLE _request ( data_hash TEXT, mailing_list_id INTEGER, PRIMARY KEY (id), - CONSTRAINT _request_mailing_list_id_fk FOREIGN KEY(mailing_list_id) REFERENCES mailinglist (id) + CONSTRAINT _request_mailing_list_id_fk + FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id) ); CREATE TABLE acceptablealias ( @@ -14,7 +15,7 @@ CREATE TABLE acceptablealias ( mailing_list_id INTEGER NOT NULL, PRIMARY KEY (id), CONSTRAINT acceptablealias_mailing_list_id_fk - FOREIGN KEY(mailing_list_id) REFERENCES mailinglist (id) + FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id) ); CREATE INDEX ix_acceptablealias_mailing_list_id ON acceptablealias (mailing_list_id); @@ -31,8 +32,10 @@ CREATE TABLE address ( user_id INTEGER, preferences_id INTEGER, PRIMARY KEY (id), - CONSTRAINT address_user_id_fk FOREIGN KEY(user_id) REFERENCES user (id), - CONSTRAINT address_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id) + CONSTRAINT address_user_id_fk + FOREIGN KEY (user_id) REFERENCES user (id), + CONSTRAINT address_preferences_id_fk + FOREIGN KEY (preferences_id) REFERENCES preferences (id) ); CREATE TABLE autoresponserecord ( @@ -43,11 +46,9 @@ CREATE TABLE autoresponserecord ( date_sent TIMESTAMP, PRIMARY KEY (id), CONSTRAINT autoresponserecord_address_id_fk - FOREIGN KEY (address_id) - REFERENCES address (id), + FOREIGN KEY (address_id) REFERENCES address (id), CONSTRAINT autoresponserecord_mailing_list_id - FOREIGN KEY (mailing_list_id) - REFERENCES mailinglist (id) + FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id) ); CREATE INDEX ix_autoresponserecord_address_id ON autoresponserecord (address_id); @@ -61,8 +62,7 @@ CREATE TABLE contentfilter ( filter_type INTEGER, PRIMARY KEY (id), CONSTRAINT contentfilter_mailing_list_id - FOREIGN KEY (mailing_list_id) - REFERENCES mailinglist (id) + FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id) ); CREATE INDEX ix_contentfilter_mailing_list_id ON contentfilter (mailing_list_id); @@ -202,8 +202,10 @@ CREATE TABLE member ( address_id INTEGER, preferences_id INTEGER, PRIMARY KEY (id), - CONSTRAINT member_address_id_fk FOREIGN KEY(address_id) REFERENCES address (id), - CONSTRAINT member_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id) + CONSTRAINT member_address_id_fk + FOREIGN KEY (address_id) REFERENCES address (id), + CONSTRAINT member_preferences_id_fk + FOREIGN KEY (preferences_id) REFERENCES preferences (id) ); CREATE TABLE message ( id INTEGER NOT NULL, @@ -219,9 +221,9 @@ CREATE TABLE onelastdigest ( delivery_mode TEXT, PRIMARY KEY (id), CONSTRAINT onelastdigest_mailing_list_id_fk - FOREIGN KEY(mailing_list_id) REFERENCES mailinglist(id), + FOREIGN KEY (mailing_list_id) REFERENCES mailinglist(id), CONSTRAINT onelastdigest_address_id_fk - FOREIGN KEY(address_id) REFERENCES address(id) + FOREIGN KEY (address_id) REFERENCES address(id) ); CREATE TABLE pended ( id INTEGER NOT NULL, @@ -235,7 +237,8 @@ CREATE TABLE pendedkeyvalue ( value TEXT, pended_id INTEGER, PRIMARY KEY (id), - CONSTRAINT pendedkeyvalue_pended_id_fk FOREIGN KEY(pended_id) REFERENCES pended (id) + CONSTRAINT pendedkeyvalue_pended_id_fk + FOREIGN KEY (pended_id) REFERENCES pended (id) ); CREATE TABLE preferences ( id INTEGER NOT NULL, @@ -254,11 +257,13 @@ CREATE TABLE user ( password BINARY, _user_id TEXT, _created_on TIMESTAMP, + _preferred_address_id INTEGER, preferences_id INTEGER, PRIMARY KEY (id), - CONSTRAINT user_preferences_id_fk - FOREIGN KEY(preferences_id) - REFERENCES preferences (id) + CONSTRAINT user_preferences_id_fk + FOREIGN KEY (preferences_id) REFERENCES preferences (id), + CONSTRAINT _preferred_address_id_fk + FOREIGN KEY (_preferred_address_id) REFERENCES address (id) ); CREATE INDEX ix_user_user_id ON user (_user_id); diff --git a/src/mailman/interfaces/address.py b/src/mailman/interfaces/address.py index be5443437..2f6d05bba 100644 --- a/src/mailman/interfaces/address.py +++ b/src/mailman/interfaces/address.py @@ -41,6 +41,7 @@ class AddressError(MailmanError): """A general address-related error occurred.""" def __init__(self, address): + super(AddressError, self).__init__() self.address = address def __str__(self): diff --git a/src/mailman/interfaces/user.py b/src/mailman/interfaces/user.py index 2c2652413..ceffec57f 100644 --- a/src/mailman/interfaces/user.py +++ b/src/mailman/interfaces/user.py @@ -22,11 +22,26 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ 'IUser', + 'UnverifiedAddressError', ] from zope.interface import Interface, Attribute +from mailman.interfaces.errors import MailmanError + + + +class UnverifiedAddressError(MailmanError): + """Unverified address cannot be used as a user's preferred address.""" + + def __init__(self, address): + super(UnverifiedAddressError, self).__init__() + self.address = address + + def __str__(self): + return self.address + class IUser(Interface): @@ -47,6 +62,9 @@ class IUser(Interface): addresses = Attribute( """An iterator over all the `IAddresses` controlled by this user.""") + preferred_address = Attribute( + """The user's preferred `IAddress`. This must be validated.""") + memberships = Attribute( """A roster of this user's memberships.""") diff --git a/src/mailman/model/docs/users.txt b/src/mailman/model/docs/users.txt index f15d5453b..1813ef636 100644 --- a/src/mailman/model/docs/users.txt +++ b/src/mailman/model/docs/users.txt @@ -4,7 +4,8 @@ Users Users are entities that represent people. A user has a real name and a optional encoded password. A user may also have an optional preferences and a -set of addresses they control. +set of addresses they control. They can even have a *preferred address*, +i.e. one that they use by default. See `usermanager.txt`_ for examples of how to create, delete, and find users. @@ -142,6 +143,90 @@ But don't try to unlink the address from a user it's not linked to. AddressNotLinkedError: zperson@example.net +Preferred address +================= + +Users can register a preferred address. When subscribing to a mailing list, +unless some other address is explicitly specified, the user will be subscribed +with their preferred address. This allows them to change their preferred +address once, and have all their subscriptions automatically track this +change. + +By default, a user has no preferred address. + + >>> user_2 = user_manager.create_user() + >>> print user_2.preferred_address + None + +Even when a user registers an address, this address will not be set as the +preferred address. + + >>> anne = user_2.register('anne@example.com', 'Anne Person') + >>> print user_2.preferred_address + None + +The preferred address must be explicitly registered, however only verified +address may be registered as preferred. + + >>> anne + <Address: Anne Person <anne@example.com> [not verified] at ...> + >>> user_2.preferred_address = anne + Traceback (most recent call last): + ... + UnverifiedAddressError: Anne Person <anne@example.com> + +Once the address has been verified though, it can be set as the preferred +address, but only if the address is either controlled by the user or +uncontrolled. In the latter case, setting it as the preferred address makes +it controlled by the user. +:: + + >>> from mailman.utilities.datetime import now + >>> anne.verified_on = now() + >>> anne + <Address: Anne Person <anne@example.com> [verified] at ...> + >>> user_2.controls(anne.email) + True + >>> user_2.preferred_address = anne + >>> user_2.preferred_address + <Address: Anne Person <anne@example.com> [verified] at ...> + + >>> aperson = user_manager.create_address('aperson@example.com') + >>> user_2.controls(aperson.email) + False + >>> aperson.verified_on = now() + >>> user_2.preferred_address = aperson + >>> user_2.controls(aperson.email) + True + + >>> zperson = list(user_1.addresses)[0] + >>> zperson.verified_on = now() + >>> user_2.controls(zperson.email) + False + >>> user_1.controls(zperson.email) + True + >>> user_2.preferred_address = zperson + Traceback (most recent call last): + ... + AddressAlreadyLinkedError: Zoe Person <zperson@example.com> + +A user can disavow their preferred address. + + >>> user_2.preferred_address + <Address: aperson@example.com [verified] at ...> + >>> del user_2.preferred_address + >>> print user_2.preferred_address + None + +The preferred address always shows up in the set of addresses controlled by +this user. + + >>> for address in user_2.addresses: + ... print address.email + anne@example.com + aperson@example.com + + Users and preferences ===================== diff --git a/src/mailman/model/user.py b/src/mailman/model/user.py index f0048c5f4..7c4c15d36 100644 --- a/src/mailman/model/user.py +++ b/src/mailman/model/user.py @@ -32,7 +32,7 @@ from mailman.config import config from mailman.database.model import Model from mailman.interfaces.address import ( AddressAlreadyLinkedError, AddressNotLinkedError) -from mailman.interfaces.user import IUser +from mailman.interfaces.user import IUser, UnverifiedAddressError from mailman.model.address import Address from mailman.model.preferences import Preferences from mailman.model.roster import Memberships @@ -53,6 +53,8 @@ class User(Model): _created_on = DateTime() addresses = ReferenceSet(id, 'Address.user_id') + _preferred_address_id = Int() + _preferred_address = Reference(_preferred_address_id, 'Address.id') preferences_id = Int() preferences = Reference(preferences_id, 'Preferences.id') @@ -93,6 +95,30 @@ class User(Model): raise AddressNotLinkedError(address) address.user = None + @property + def preferred_address(self): + """See `IUser`.""" + return self._preferred_address + + @preferred_address.setter + def preferred_address(self, address): + """See `IUser`.""" + if address.verified_on is None: + raise UnverifiedAddressError(address) + if self.controls(address.email): + # This user already controls the email address. + pass + elif address.user is None: + self.link(address) + elif address.user != self: + raise AddressAlreadyLinkedError(address) + self._preferred_address = address + + @preferred_address.deleter + def preferred_address(self): + """See `IUser`.""" + self._preferred_address = None + def controls(self, email): """See `IUser`.""" found = config.db.store.find(Address, email=email) |
