diff options
| -rw-r--r-- | Mailman/database/model/mailinglist.py | 3 | ||||
| -rw-r--r-- | Mailman/database/model/member.py | 3 | ||||
| -rw-r--r-- | Mailman/database/model/user.py | 2 | ||||
| -rw-r--r-- | Mailman/database/usermanager.py | 24 | ||||
| -rw-r--r-- | Mailman/docs/addresses.txt | 218 | ||||
| -rw-r--r-- | Mailman/docs/mlist-rosters.txt | 127 | ||||
| -rw-r--r-- | Mailman/docs/users.txt | 75 | ||||
| -rw-r--r-- | Mailman/interfaces/user.py | 9 | ||||
| -rw-r--r-- | Mailman/interfaces/usermanager.py | 29 | ||||
| -rw-r--r-- | Mailman/testing/test_mlist_rosters.py | 30 |
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 |
