summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Mailman/database/model/mailinglist.py3
-rw-r--r--Mailman/database/model/member.py3
-rw-r--r--Mailman/database/model/user.py2
-rw-r--r--Mailman/database/usermanager.py24
-rw-r--r--Mailman/docs/addresses.txt218
-rw-r--r--Mailman/docs/mlist-rosters.txt127
-rw-r--r--Mailman/docs/users.txt75
-rw-r--r--Mailman/interfaces/user.py9
-rw-r--r--Mailman/interfaces/usermanager.py29
-rw-r--r--Mailman/testing/test_mlist_rosters.py30
10 files changed, 219 insertions, 301 deletions
diff --git a/Mailman/database/model/mailinglist.py b/Mailman/database/model/mailinglist.py
index edd2eab0d..1e7823911 100644
--- a/Mailman/database/model/mailinglist.py
+++ b/Mailman/database/model/mailinglist.py
@@ -22,6 +22,8 @@ from Mailman.Utils import fqdn_listname, split_listname
from Mailman.configuration import config
from Mailman.interfaces import *
+PREFERENCE_KIND = 'Mailman.database.model.profile.Preferences'
+
class MailingList(Entity):
@@ -154,6 +156,7 @@ class MailingList(Entity):
has_field('unsubscribe_policy', Integer),
has_field('welcome_msg', Unicode),
# Relationships
+ belongs_to('preferences', of_kind=PREFERENCE_KIND)
## has_and_belongs_to_many(
## 'available_languages',
## of_kind='Mailman.database.model.languages.Language')
diff --git a/Mailman/database/model/member.py b/Mailman/database/model/member.py
index db37ebb49..bcf859cf4 100644
--- a/Mailman/database/model/member.py
+++ b/Mailman/database/model/member.py
@@ -51,7 +51,8 @@ class Member(Entity):
return self._preferences
if self.address.preferences:
return self.address.preferences
- if self.address.user.preferences:
+ # It's possible this address isn't linked to a user.
+ if self.address.user and self.address.user.preferences:
return self.address.user.preferences
list_name, host_name = split_listname(self.mailing_list)
mlist = MailingList.get_by(list_name=list_name,
diff --git a/Mailman/database/model/user.py b/Mailman/database/model/user.py
index 06fb1f6f3..45fcbfdd3 100644
--- a/Mailman/database/model/user.py
+++ b/Mailman/database/model/user.py
@@ -55,5 +55,5 @@ class User(Entity):
self.addresses.remove(address)
def controls(self, address):
- found = Address.get_by(address=address.address)
+ found = Address.get_by(address=address)
return bool(found and found.user is self)
diff --git a/Mailman/database/usermanager.py b/Mailman/database/usermanager.py
index 95e4949ea..2f12188aa 100644
--- a/Mailman/database/usermanager.py
+++ b/Mailman/database/usermanager.py
@@ -53,3 +53,27 @@ class UserManager(object):
def get_user(self, address):
found = Address.get_by(address=address)
return found and found.user
+
+ def create_address(self, address, real_name=None):
+ found = Address.get_by(address=address)
+ if found:
+ raise Errors.ExistingAddressError(address)
+ if real_name is None:
+ real_name = ''
+ address = Address(address=address, real_name=real_name)
+ return address
+
+ def delete_address(self, address):
+ # If there's a user controlling this address, it has to first be
+ # unlinked before the address can be deleted.
+ if address.user:
+ address.user.unlink(address)
+ address.delete()
+
+ def get_address(self, address):
+ return Address.get_by(address=address)
+
+ @property
+ def addresses(self):
+ for address in Address.select():
+ yield address
diff --git a/Mailman/docs/addresses.txt b/Mailman/docs/addresses.txt
index a8cf9f655..c6a2a9873 100644
--- a/Mailman/docs/addresses.txt
+++ b/Mailman/docs/addresses.txt
@@ -1,144 +1,194 @@
-Email addresses and rosters
-===========================
+Email addresses
+===============
-Addresses represent email address, and nothing more. Some addresses are tied
-to users that Mailman knows about. For example, a list member is a user that
-the system knows about, but a non-member posting from a brand new email
-address is a counter-example.
-
-
-Creating a roster
------------------
-
-Email address objects are tied to rosters, and rosters are tied to the user
-manager. To get things started, access the global user manager and create a
-new roster.
+Addresses represent a text email address, along with some meta data about
+those addresses, such as their registration date, and whether and when they've
+been validated. Addresses may be linked to the users that Mailman knows
+about. Addresses are subscribed to mailing lists though members.
>>> from Mailman.database import flush
>>> from Mailman.configuration import config
>>> mgr = config.user_manager
- >>> roster_1 = mgr.create_roster('roster-1')
- >>> sorted(roster_1.addresses)
- []
Creating addresses
------------------
-Creating a simple email address object is straight forward.
+Addresses are created directly through the user manager, which starts out with
+no addresses.
- >>> addr_1 = roster_1.create('aperson@example.com')
+ >>> sorted(address.address for address in mgr.addresses)
+ []
+
+Creating an unlinked email address is straightforward.
+
+ >>> address_1 = mgr.create_address('aperson@example.com')
>>> flush()
- >>> addr_1.address
- 'aperson@example.com'
- >>> addr_1.real_name is None
- True
+ >>> sorted(address.address for address in mgr.addresses)
+ ['aperson@example.com']
-You can also create an email address object with a real name.
+However, such addresses have no real name.
- >>> addr_2 = roster_1.create('bperson@example.com', 'Barney Person')
- >>> addr_2.address
- 'bperson@example.com'
- >>> addr_2.real_name
- 'Barney Person'
+ >>> address_1.real_name
+ ''
-You can also iterate through all the addresses on a roster.
+You can also create an email address object with a real name.
- >>> sorted(addr.address for addr in roster_1.addresses)
+ >>> address_2 = mgr.create_address('bperson@example.com', 'Ben Person')
+ >>> flush()
+ >>> sorted(address.address for address in mgr.addresses)
['aperson@example.com', 'bperson@example.com']
+ >>> sorted(address.real_name for address in mgr.addresses)
+ ['', 'Ben Person']
-You can create another roster and add a bunch of existing addresses to the
-second roster.
+You can assign real names to existing addresses.
- >>> roster_2 = mgr.create_roster('roster-2')
+ >>> address_1.real_name = 'Anne Person'
>>> flush()
- >>> sorted(roster_2.addresses)
- []
- >>> for address in roster_1.addresses:
- ... roster_2.addresses.append(address)
- >>> roster_2.create('cperson@example.com', 'Charlie Person')
- <Address: Charlie Person <cperson@example.com> [not verified]>
- >>> sorted(addr.address for addr in roster_2.addresses)
+ >>> sorted(address.real_name for address in mgr.addresses)
+ ['Anne Person', 'Ben Person']
+
+These addresses are not linked to users, and can be seen by searching the user
+manager for an associated user.
+
+ >>> print mgr.get_user('aperson@example.com')
+ None
+ >>> print mgr.get_user('bperson@example.com')
+ None
+
+You can create email addresses that are linked to users by using a different
+interface.
+
+ >>> user_1 = mgr.create_user('cperson@example.com', 'Claire Person')
+ >>> flush()
+ >>> sorted(address.address for address in mgr.addresses)
['aperson@example.com', 'bperson@example.com', 'cperson@example.com']
+ >>> sorted(address.real_name for address in mgr.addresses)
+ ['Anne Person', 'Ben Person', 'Claire Person']
-The first roster hasn't been affected.
+And now you can find the associated user.
- >>> sorted(addr.address for addr in roster_1.addresses)
- ['aperson@example.com', 'bperson@example.com']
+ >>> print mgr.get_user('aperson@example.com')
+ None
+ >>> print mgr.get_user('bperson@example.com')
+ None
+ >>> mgr.get_user('cperson@example.com')
+ <User "Claire Person" at ...>
-Removing addresses
+Deleting addresses
------------------
-You can remove an address from a roster just by deleting it.
+You can remove an unlinked address from the usre manager.
- >>> for addr in roster_1.addresses:
- ... if addr.address == 'aperson@example.com':
- ... break
- >>> addr.address
- 'aperson@example.com'
- >>> roster_1.addresses.remove(addr)
- >>> sorted(addr.address for addr in roster_1.addresses)
- ['bperson@example.com']
+ >>> mgr.delete_address(address_1)
+ >>> flush()
+ >>> sorted(address.address for address in mgr.addresses)
+ ['bperson@example.com', 'cperson@example.com']
+ >>> sorted(address.real_name for address in mgr.addresses)
+ ['Ben Person', 'Claire Person']
-Again, this doesn't affect the other rosters.
+Deleting a linked address does not delete the user, but it does unlink the
+address from the user.
- >>> sorted(addr.address for addr in roster_2.addresses)
- ['aperson@example.com', 'bperson@example.com', 'cperson@example.com']
+ >>> sorted(address.address for address in user_1.addresses)
+ ['cperson@example.com']
+ >>> user_1.controls('cperson@example.com')
+ True
+ >>> address_3 = list(user_1.addresses)[0]
+ >>> mgr.delete_address(address_3)
+ >>> flush()
+ >>> sorted(address.address for address in user_1.addresses)
+ []
+ >>> user_1.controls('cperson@example.com')
+ False
+ >>> sorted(address.address for address in mgr.addresses)
+ ['bperson@example.com']
Registration and validation
---------------------------
Addresses have two dates, the date the address was registered on and the date
-the address was validated on. Neither date isset by default.
+the address was validated on. Neither date is set by default.
- >>> addr = roster_1.create('dperson@example.com', 'David Person')
- >>> addr.registered_on is None
- True
- >>> addr.validated_on is None
- True
+ >>> address_4 = mgr.create_address('dperson@example.com', 'Dan Person')
+ >>> flush()
+ >>> print address_4.registered_on
+ None
+ >>> print address_4.validated_on
+ None
The registered date takes a Python datetime object.
>>> from datetime import datetime
- >>> addr.registered_on = datetime(2007, 5, 8, 22, 54, 1)
- >>> print addr.registered_on
+ >>> address_4.registered_on = datetime(2007, 5, 8, 22, 54, 1)
+ >>> flush()
+ >>> print address_4.registered_on
2007-05-08 22:54:01
- >>> addr.validated_on is None
- True
+ >>> print address_4.validated_on
+ None
And of course, you can also set the validation date.
- >>> addr.validated_on = datetime(2007, 5, 13, 22, 54, 1)
- >>> print addr.registered_on
+ >>> address_4.validated_on = datetime(2007, 5, 13, 22, 54, 1)
+ >>> flush()
+ >>> print address_4.registered_on
2007-05-08 22:54:01
- >>> print addr.validated_on
+ >>> print address_4.validated_on
2007-05-13 22:54:01
-The null roster
----------------
+Subscriptions
+-------------
-All address objects that have been created are members of the null roster.
+Addresses get subscribed to mailing lists, not users. When the address is
+subscribed, a role is specified.
- >>> all = mgr.get_roster('')
- >>> sorted(addr.address for addr in all.addresses)
- ['aperson@example.com', 'bperson@example.com',
- 'cperson@example.com', 'dperson@example.com']
+ >>> address_5 = mgr.create_address('eperson@example.com', 'Elly Person')
+ >>> mlist = config.list_manager.create('_xtext@example.com')
+ >>> from Mailman.constants import MemberRole
+ >>> address_5.subscribe(mlist, MemberRole.owner)
+ <Member: Elly Person <eperson@example.com> on
+ _xtext@example.com as MemberRole.owner>
+ >>> address_5.subscribe(mlist, MemberRole.member)
+ <Member: Elly Person <eperson@example.com> on
+ _xtext@example.com as MemberRole.member>
+ >>> flush()
-And conversely, all addresses should have the null roster on their list of
-rosters.
+Now that Elly is both an owner and a member of the mailing list.
- >>> for addr in all.addresses:
- ... assert all in addr.rosters, 'Address is missing null roster'
+ >>> sorted(mlist.owners.members)
+ [<Member: Elly Person <eperson@example.com> on
+ _xtext@example.com as MemberRole.owner>]
+ >>> sorted(mlist.moderators.members)
+ []
+ >>> sorted(mlist.administrators.members)
+ [<Member: Elly Person <eperson@example.com> on
+ _xtext@example.com as MemberRole.owner>]
+ >>> sorted(mlist.members.members)
+ [<Member: Elly Person <eperson@example.com> on
+ _xtext@example.com as MemberRole.member>]
+ >>> sorted(mlist.regular_members.members)
+ [<Member: Elly Person <eperson@example.com> on
+ _xtext@example.com as MemberRole.member>]
+ >>> sorted(mlist.digest_members.members)
+ []
Clean up
--------
- >>> for roster in mgr.rosters:
- ... mgr.delete_roster(roster)
+ >>> for mlist in config.list_manager.mailing_lists:
+ ... config.list_manager.delete(mlist)
+ >>> for user in mgr.users:
+ ... mgr.delete_user(user)
+ >>> for address in mgr.addresses:
+ ... mgr.delete_address(address)
>>> flush()
- >>> sorted(roster.name for roster in mgr.rosters)
+ >>> sorted(config.list_manager.names)
+ []
+ >>> sorted(mgr.users)
+ []
+ >>> sorted(mgr.addresses)
[]
diff --git a/Mailman/docs/mlist-rosters.txt b/Mailman/docs/mlist-rosters.txt
deleted file mode 100644
index 096ceb7d2..000000000
--- a/Mailman/docs/mlist-rosters.txt
+++ /dev/null
@@ -1,127 +0,0 @@
-Mailing list rosters
-====================
-
-Mailing lists use rosters to manage and organize users for various purposes.
-In order to allow for separate storage of mailing list data and user data, the
-connection between mailing list objects and rosters is indirect. Mailing
-lists manage roster names, and these roster names are used to find the rosters
-that contain the actual users.
-
-
-Privileged rosters
-------------------
-
-Mailing lists have two types of privileged users, owners and moderators.
-Owners get to change the configuration of mailing lists and moderators get to
-approve or deny held messages and subscription requests.
-
-When a mailing list is created, it automatically contains a roster for the
-list owners and a roster for the list moderators.
-
- >>> from Mailman.database import flush
- >>> from Mailman.configuration import config
- >>> mlist = config.list_manager.create('_xtest@example.com')
- >>> flush()
- >>> sorted(roster.name for roster in mlist.owner_rosters)
- ['_xtest@example.com owners']
- >>> sorted(roster.name for roster in mlist.moderator_rosters)
- ['_xtest@example.com moderators']
-
-These rosters are initially empty.
-
- >>> owner_roster = list(mlist.owner_rosters)[0]
- >>> sorted(address for address in owner_roster.addresses)
- []
- >>> moderator_roster = list(mlist.moderator_rosters)[0]
- >>> sorted(address for address in moderator_roster.addresses)
- []
-
-You can create new rosters and add them to the list of owner or moderator
-rosters.
-
- >>> roster_1 = config.user_manager.create_roster('roster-1')
- >>> roster_2 = config.user_manager.create_roster('roster-2')
- >>> roster_3 = config.user_manager.create_roster('roster-3')
- >>> flush()
-
-Make roster-1 an owner roster, roster-2 a moderator roster, and roster-3 both
-an owner and a moderator roster.
-
- >>> mlist.add_owner_roster(roster_1)
- >>> mlist.add_moderator_roster(roster_2)
- >>> mlist.add_owner_roster(roster_3)
- >>> mlist.add_moderator_roster(roster_3)
- >>> flush()
-
- >>> sorted(roster.name for roster in mlist.owner_rosters)
- ['_xtest@example.com owners', 'roster-1', 'roster-3']
- >>> sorted(roster.name for roster in mlist.moderator_rosters)
- ['_xtest@example.com moderators', 'roster-2', 'roster-3']
-
-
-Privileged users
-----------------
-
-Rosters are the lower level way of managing owners and moderators, but usually
-you just want to know which users have owner and moderator privileges. You
-can get the list of such users by using different attributes.
-
-Because the rosters are all empty to start with, we can create a bunch of
-users that will end up being our owners and moderators.
-
- >>> aperson = config.user_manager.create_user()
- >>> bperson = config.user_manager.create_user()
- >>> cperson = config.user_manager.create_user()
-
-These users need addresses, because rosters manage addresses.
-
- >>> address_1 = roster_1.create('aperson@example.com', 'Anne Person')
- >>> aperson.link(address_1)
- >>> address_2 = roster_2.create('bperson@example.com', 'Ben Person')
- >>> bperson.link(address_2)
- >>> address_3 = roster_1.create('cperson@example.com', 'Claire Person')
- >>> cperson.link(address_3)
- >>> roster_3.addresses.append(address_3)
- >>> flush()
-
-Now that everything is set up, we can iterate through the various collections
-of privileged users. Here are the owners of the list.
-
- >>> from Mailman.interfaces import IUser
- >>> addresses = []
- >>> for user in mlist.owners:
- ... assert IUser.providedBy(user), 'Non-IUser owner found'
- ... for address in user.addresses:
- ... addresses.append(address.address)
- >>> sorted(addresses)
- ['aperson@example.com', 'cperson@example.com']
-
-Here are the moderators of the list.
-
- >>> addresses = []
- >>> for user in mlist.moderators:
- ... assert IUser.providedBy(user), 'Non-IUser moderator found'
- ... for address in user.addresses:
- ... addresses.append(address.address)
- >>> sorted(addresses)
- ['bperson@example.com', 'cperson@example.com']
-
-The administrators of a mailing list are the union of the owners and
-moderators.
-
- >>> addresses = []
- >>> for user in mlist.administrators:
- ... assert IUser.providedBy(user), 'Non-IUser administrator found'
- ... for address in user.addresses:
- ... addresses.append(address.address)
- >>> sorted(addresses)
- ['aperson@example.com', 'bperson@example.com', 'cperson@example.com']
-
-
-Clean up
---------
-
- >>> config.list_manager.delete(mlist)
- >>> flush()
- >>> [name for name in config.list_manager.names]
- []
diff --git a/Mailman/docs/users.txt b/Mailman/docs/users.txt
index caad6b216..a65527eff 100644
--- a/Mailman/docs/users.txt
+++ b/Mailman/docs/users.txt
@@ -1,83 +1,44 @@
Users
=====
-Users are entities that combine addresses, preferences, and a password
-scheme. Password schemes can be anything from a traditional
-challenge/response type password string to an OpenID url.
+Users are entities that represent people. A user has a real name and a
+password. Optionally a user may have some preferences and a set of addresses
+they control.
-
-Create, deleting, and managing users
-------------------------------------
-
-Users are managed by the IUserManager. Users don't have any unique
-identifying information, and no such id is needed to create them.
+See usermanager.txt for examples of how to create, delete, and find users.
>>> from Mailman.database import flush
>>> from Mailman.configuration import config
>>> mgr = config.user_manager
- >>> user = mgr.create_user()
-Users have a real name, a password scheme, a default profile, and a set of
-addresses that they control. All of these data are None or empty for a newly
-created user.
- >>> user.real_name is None
- True
- >>> user.password is None
- True
- >>> user.addresses
- []
+User data
+---------
-You can iterate over all the users in a user manager.
-
- >>> another_user = mgr.create_user()
- >>> flush()
- >>> all_users = list(mgr.users)
- >>> len(list(all_users))
- 2
- >>> user is not another_user
- True
- >>> user in all_users
- True
- >>> another_user in all_users
- True
-
-You can also delete users from the user manager.
-
- >>> mgr.delete_user(user)
- >>> mgr.delete_user(another_user)
- >>> flush()
- >>> len(list(mgr.users))
- 0
-
-
-Simple user information
------------------------
-
-Users may have a real name and a password scheme.
+Users may have a real name and a password.
>>> user = mgr.create_user()
>>> user.password = 'my password'
>>> user.real_name = 'Zoe Person'
>>> flush()
- >>> only_person = list(mgr.users)[0]
- >>> only_person.password
- 'my password'
- >>> only_person.real_name
- 'Zoe Person'
+ >>> sorted(user.real_name for user in mgr.users)
+ ['Zoe Person']
+ >>> sorted(user.password for user in mgr.users)
+ ['my password']
The password and real name can be changed at any time.
>>> user.real_name = 'Zoe X. Person'
>>> user.password = 'another password'
- >>> only_person.real_name
- 'Zoe X. Person'
- >>> only_person.password
- 'another password'
+ >>> flush()
+ >>> sorted(user.real_name for user in mgr.users)
+ ['Zoe X. Person']
+ >>> sorted(user.password for user in mgr.users)
+ ['another password']
-Users and addresses
--------------------
+Users addresses
+---------------
One of the pieces of information that a user links to is a set of email
addresses, in the form of IAddress objects. A user can control many
diff --git a/Mailman/interfaces/user.py b/Mailman/interfaces/user.py
index bed1db08d..9e89c2416 100644
--- a/Mailman/interfaces/user.py
+++ b/Mailman/interfaces/user.py
@@ -36,6 +36,15 @@ class IUser(Interface):
addresses = Attribute(
"""An iterator over all the IAddresses controlled by this user.""")
+ def register(address):
+ """Register the given email address and link it to this user.
+
+ In this case, 'address' is a text email address, not an IAddress
+ object. Raises AddressAlreadyLinkedError if this IAddress is already
+ linked to another user. If the corresponding IAddress already exists
+ but is not linked, then it is simply linked to the user.
+ """
+
def link(address):
"""Link this user to the given IAddress.
diff --git a/Mailman/interfaces/usermanager.py b/Mailman/interfaces/usermanager.py
index d239ea5b3..f201b3591 100644
--- a/Mailman/interfaces/usermanager.py
+++ b/Mailman/interfaces/usermanager.py
@@ -37,7 +37,9 @@ class IUserManager(Interface):
"""Create and return an IUser.
When address is given, an IAddress is also created and linked to the
- new IUser object. It is an error if the address already exists.
+ new IUser object. If the address already exists, an
+ ExistingAddressError is raised. If the address exists but is already
+ linked to another user, an AddressAlreadyLinkedError is raised.
When real_name is given, the IUser's real_name is set to this string.
If an IAddress is also created and linked, its real_name is set to the
@@ -55,3 +57,28 @@ class IUserManager(Interface):
users = Attribute(
"""An iterator over all the IUsers managed by this user manager.""")
+
+ def create_address(address, real_name=None):
+ """Create and return an unlinked IAddress object.
+
+ address is the text email address. If real_name is not given, it
+ defaults to the empty string. If the IAddress already exists an
+ ExistingAddressError is raised.
+ """
+
+ def delete_address(address):
+ """Delete the given IAddress object.
+
+ If this IAddress linked to a user, it is first unlinked before it is
+ deleted.
+ """
+
+ def get_address(address):
+ """Find and return an IAddress.
+
+ 'address' is a text email address. None is returned if there is no
+ registered IAddress for the given text address.
+ """
+
+ addresses = Attribute(
+ """An iterator over all the IAddresses managed by this manager.""")
diff --git a/Mailman/testing/test_mlist_rosters.py b/Mailman/testing/test_mlist_rosters.py
deleted file mode 100644
index e8713b828..000000000
--- a/Mailman/testing/test_mlist_rosters.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Copyright (C) 2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Doctest harness for the IMailingListRosters interface."""
-
-import doctest
-import unittest
-
-options = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE
-
-
-def test_suite():
- suite = unittest.TestSuite()
- suite.addTest(doctest.DocFileSuite('../docs/mlist-rosters.txt',
- optionflags=options))
- return suite