summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/database/mailman.sql41
-rw-r--r--src/mailman/interfaces/address.py1
-rw-r--r--src/mailman/interfaces/user.py18
-rw-r--r--src/mailman/model/docs/users.txt87
-rw-r--r--src/mailman/model/user.py28
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)