diff options
| -rw-r--r-- | Mailman/MailList.py | 8 | ||||
| -rw-r--r-- | Mailman/constants.py | 22 | ||||
| -rw-r--r-- | Mailman/database/model/__init__.py | 9 | ||||
| -rw-r--r-- | Mailman/database/model/address.py | 21 | ||||
| -rw-r--r-- | Mailman/database/model/language.py | 2 | ||||
| -rw-r--r-- | Mailman/database/model/mailinglist.py | 122 | ||||
| -rw-r--r-- | Mailman/database/model/member.py | 61 | ||||
| -rw-r--r-- | Mailman/database/model/profile.py | 31 | ||||
| -rw-r--r-- | Mailman/database/model/roster.py | 177 | ||||
| -rw-r--r-- | Mailman/database/model/user.py | 10 | ||||
| -rw-r--r-- | Mailman/database/model/version.py | 8 | ||||
| -rw-r--r-- | Mailman/database/usermanager.py | 51 | ||||
| -rw-r--r-- | Mailman/docs/membership.txt | 183 | ||||
| -rw-r--r-- | Mailman/interfaces/address.py | 6 | ||||
| -rw-r--r-- | Mailman/interfaces/member.py | 48 | ||||
| -rw-r--r-- | Mailman/interfaces/mlistrosters.py | 33 | ||||
| -rw-r--r-- | Mailman/interfaces/profile.py | 4 | ||||
| -rw-r--r-- | Mailman/interfaces/roster.py | 22 | ||||
| -rw-r--r-- | Mailman/interfaces/user.py | 4 | ||||
| -rw-r--r-- | Mailman/interfaces/usermanager.py | 33 | ||||
| -rw-r--r-- | Mailman/testing/test_membership.py | 7 |
21 files changed, 569 insertions, 293 deletions
diff --git a/Mailman/MailList.py b/Mailman/MailList.py index 0e57aa0a8..9dff467ce 100644 --- a/Mailman/MailList.py +++ b/Mailman/MailList.py @@ -185,14 +185,6 @@ class MailList(object, HTMLFormatter, Deliverer, ListAdmin, - # IMailingListIdentity - - @property - def fqdn_listname(self): - return Utils.fqdn_listname(self._data.list_name, self._data.host_name) - - - # IMailingListAddresses @property diff --git a/Mailman/constants.py b/Mailman/constants.py index 852704364..7b3876a2a 100644 --- a/Mailman/constants.py +++ b/Mailman/constants.py @@ -18,6 +18,9 @@ """Various constants and enumerations.""" from munepy import Enum +from zope.interface import implements + +from Mailman.interfaces import IPreferences @@ -42,3 +45,22 @@ class DeliveryStatus(Enum): by_bounces = 3 # Delivery was disabled by an administrator or moderator by_moderator = 4 + + + +class MemberRole(Enum): + member = 1 + owner = 2 + moderator = 3 + + + +class SystemDefaultPreferences(object): + implements(IPreferences) + + acknowledge_posts = False + hide_address = True + preferred_language = 'en' + receive_list_copy = True + receive_own_postings = True + delivery_mode = DeliveryMode.regular diff --git a/Mailman/database/model/__init__.py b/Mailman/database/model/__init__.py index 11ca11f89..612510632 100644 --- a/Mailman/database/model/__init__.py +++ b/Mailman/database/model/__init__.py @@ -19,9 +19,7 @@ __all__ = [ 'Address', 'Language', 'MailingList', - 'Profile', - 'Roster', - 'RosterSet', + 'Preferences', 'User', 'Version', ] @@ -44,9 +42,8 @@ from Mailman.configuration import config from Mailman.database.model.address import Address from Mailman.database.model.language import Language from Mailman.database.model.mailinglist import MailingList -from Mailman.database.model.profile import Profile -from Mailman.database.model.roster import Roster -from Mailman.database.model.rosterset import RosterSet +from Mailman.database.model.member import Member +from Mailman.database.model.profile import Preferences from Mailman.database.model.user import User from Mailman.database.model.version import Version diff --git a/Mailman/database/model/address.py b/Mailman/database/model/address.py index 53d5016e5..450c40235 100644 --- a/Mailman/database/model/address.py +++ b/Mailman/database/model/address.py @@ -21,11 +21,12 @@ from zope.interface import implements from Mailman.interfaces import IAddress - -ROSTER_KIND = 'Mailman.database.model.roster.Roster' -USER_KIND = 'Mailman.database.model.user.User' +MEMBER_KIND = 'Mailman.database.model.member.Member' +PREFERENCE_KIND = 'Mailman.database.model.profile.Preferences' +USER_KIND = 'Mailman.database.model.user.User' + class Address(Entity): implements(IAddress) @@ -35,8 +36,10 @@ class Address(Entity): has_field('registered_on', DateTime) has_field('validated_on', DateTime) # Relationships - has_and_belongs_to_many('rosters', of_kind=ROSTER_KIND) - belongs_to('user', of_kind=USER_KIND) + belongs_to('user', of_kind=USER_KIND) + belongs_to('preferences', of_kind=PREFERENCE_KIND) + # Options + using_options(shortnames=True) def __str__(self): return formataddr((self.real_name, self.address)) @@ -44,3 +47,11 @@ class Address(Entity): def __repr__(self): return '<Address: %s [%s]>' % ( str(self), ('verified' if self.verified else 'not verified')) + + def subscribe(self, mlist, role): + from Mailman.database.model import Member + # This member has no preferences by default. + member = Member(role=role, + mailing_list=mlist.fqdn_listname, + address=self) + return member diff --git a/Mailman/database/model/language.py b/Mailman/database/model/language.py index 3597a128d..e065d5bad 100644 --- a/Mailman/database/model/language.py +++ b/Mailman/database/model/language.py @@ -20,3 +20,5 @@ from elixir import * class Language(Entity): has_field('code', Unicode) + # Options + using_options(shortnames=True) diff --git a/Mailman/database/model/mailinglist.py b/Mailman/database/model/mailinglist.py index 28e2c11dc..edd2eab0d 100644 --- a/Mailman/database/model/mailinglist.py +++ b/Mailman/database/model/mailinglist.py @@ -50,16 +50,6 @@ class MailingList(Entity): has_field('one_last_digest', PickleType), has_field('volume', Integer), has_field('last_post_time', Float), - # OldStyleMemberships attributes, temporarily stored as pickles. - has_field('bounce_info', PickleType), - has_field('delivery_status', PickleType), - has_field('digest_members', PickleType), - has_field('language', PickleType), - has_field('members', PickleType), - has_field('passwords', PickleType), - has_field('topics_userinterest', PickleType), - has_field('user_options', PickleType), - has_field('usernames', PickleType), # Attributes which are directly modifiable via the web u/i. The more # complicated attributes are currently stored as pickles, though that # will change as the schema and implementation is developed. @@ -163,113 +153,29 @@ class MailingList(Entity): has_field('umbrella_member_suffix', Unicode), has_field('unsubscribe_policy', Integer), has_field('welcome_msg', Unicode), - # Indirect relationships - has_field('owner_rosterset', Unicode), - has_field('moderator_rosterset', Unicode), # Relationships ## has_and_belongs_to_many( ## 'available_languages', ## of_kind='Mailman.database.model.languages.Language') + # Options + using_options(shortnames=True) def __init__(self, fqdn_listname): super(MailingList, self).__init__() listname, hostname = split_listname(fqdn_listname) self.list_name = listname self.host_name = hostname - # Create two roster sets, one for the owners and one for the - # moderators. MailingLists are connected to RosterSets indirectly, in - # order to preserve the ability to store user data and list data in - # different databases. - name = fqdn_listname + ' owners' - self.owner_rosterset = name - roster = config.user_manager.create_roster(name) - config.user_manager.create_rosterset(name).add(roster) - name = fqdn_listname + ' moderators' - self.moderator_rosterset = name - roster = config.user_manager.create_roster(name) - config.user_manager.create_rosterset(name).add(roster) - - def delete_rosters(self): - listname = fqdn_listname(self.list_name, self.host_name) - # Delete the list owner roster and roster set. - name = listname + ' owners' - roster = config.user_manager.get_roster(name) - assert roster, 'Missing roster: %s' % name - config.user_manager.delete_roster(roster) - rosterset = config.user_manager.get_rosterset(name) - assert rosterset, 'Missing roster set: %s' % name - config.user_manager.delete_rosterset(rosterset) - name = listname + ' moderators' - roster = config.user_manager.get_roster(name) - assert roster, 'Missing roster: %s' % name - config.user_manager.delete_roster(roster) - rosterset = config.user_manager.get_rosterset(name) - assert rosterset, 'Missing roster set: %s' % name - config.user_manager.delete_rosterset(rosterset) - - # IMailingListRosters + # Create several rosters for filtering out or querying the membership + # table. + from Mailman.database.model import roster + self.owners = roster.OwnerRoster(self) + self.moderators = roster.ModeratorRoster(self) + self.administrators = roster.AdministratorRoster(self) + self.members = roster.MemberRoster(self) + self.regular_members = roster.RegularMemberRoster(self) + self.digest_members = roster.DigestMemberRoster(self) @property - def owners(self): - for user in _collect_users(self.owner_rosterset): - yield user - - @property - def moderators(self): - for user in _collect_users(self.moderator_rosterset): - yield user - - @property - def administrators(self): - for user in _collect_users(self.owner_rosterset, - self.moderator_rosterset): - yield user - - @property - def owner_rosters(self): - rosterset = config.user_manager.get_rosterset(self.owner_rosterset) - for roster in rosterset.rosters: - yield roster - - @property - def moderator_rosters(self): - rosterset = config.user_manager.get_rosterset(self.moderator_rosterset) - for roster in rosterset.rosters: - yield roster - - def add_owner_roster(self, roster): - rosterset = config.user_manager.get_rosterset(self.owner_rosterset) - rosterset.add(roster) - - def delete_owner_roster(self, roster): - rosterset = config.user_manager.get_rosterset(self.owner_rosterset) - rosterset.delete(roster) - - def add_moderator_roster(self, roster): - rosterset = config.user_manager.get_rosterset(self.moderator_rosterset) - rosterset.add(roster) - - def delete_moderator_roster(self, roster): - rosterset = config.user_manager.get_rosterset(self.moderator_rosterset) - rosterset.delete(roster) - - - -def _collect_users(*rosterset_names): - users = set() - for name in rosterset_names: - # We have to indirectly look up the roster set's name in the user - # manager. This is how we enforce separation between the list manager - # and the user manager storages. - rosterset = config.user_manager.get_rosterset(name) - assert rosterset is not None, 'No RosterSet named: %s' % name - for roster in rosterset.rosters: - # Rosters collect addresses. It's not required that an address is - # linked to a user, but it must be the case that all addresses on - # the owner roster are linked to a user. Get the user that's - # linked to each address and add it to the set. - for address in roster.addresses: - user = config.user_manager.get_user(address.address) - assert user is not None, 'Unlinked address: ' + address.address - users.add(user) - return users + def fqdn_listname(self): + """See IMailingListIdentity.""" + return fqdn_listname(self.list_name, self.host_name) diff --git a/Mailman/database/model/member.py b/Mailman/database/model/member.py new file mode 100644 index 000000000..db37ebb49 --- /dev/null +++ b/Mailman/database/model/member.py @@ -0,0 +1,61 @@ +# 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. + +from elixir import * +from zope.interface import implements + +from Mailman.Utils import split_listname +from Mailman.constants import SystemDefaultPreferences +from Mailman.database.types import EnumType +from Mailman.interfaces import IMember, IPreferences + + +ADDRESS_KIND = 'Mailman.database.model.address.Address' +PREFERENCE_KIND = 'Mailman.database.model.profile.Preferences' + + + +class Member(Entity): + implements(IMember) + + has_field('role', EnumType) + has_field('mailing_list', Unicode) + # Relationships + belongs_to('address', of_kind=ADDRESS_KIND) + belongs_to('_preferences', of_kind=PREFERENCE_KIND) + # Options + using_options(shortnames=True) + + def __repr__(self): + return '<Member: %s on %s as %s>' % ( + self.address, self.mailing_list, self.role) + + @property + def preferences(self): + from Mailman.database.model import MailingList + if self._preferences: + return self._preferences + if self.address.preferences: + return self.address.preferences + if 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, + host_name=host_name) + if mlist.preferences: + return mlist.preferences + return SystemDefaultPreferences diff --git a/Mailman/database/model/profile.py b/Mailman/database/model/profile.py index 49e108728..935ae08fb 100644 --- a/Mailman/database/model/profile.py +++ b/Mailman/database/model/profile.py @@ -19,13 +19,18 @@ from elixir import * from email.utils import formataddr from zope.interface import implements -from Mailman.constants import DeliveryMode +from Mailman.constants import SystemDefaultPreferences as Prefs from Mailman.database.types import EnumType -from Mailman.interfaces import IProfile +from Mailman.interfaces import IPreferences +ADDRESS_KIND = 'Mailman.database.model.address.Address' +MEMBER_KIND = 'Mailman.database.model.member.Member' +USER_KIND = 'Mailman.database.model.user.User' -class Profile(Entity): - implements(IProfile) + + +class Preferences(Entity): + implements(IPreferences) has_field('acknowledge_posts', Boolean) has_field('hide_address', Boolean) @@ -33,14 +38,14 @@ class Profile(Entity): has_field('receive_list_copy', Boolean) has_field('receive_own_postings', Boolean) has_field('delivery_mode', EnumType) - # Relationships - belongs_to('user', of_kind='Mailman.database.model.user.User') + # Options + using_options(shortnames=True) def __init__(self): - super(Profile, self).__init__() - self.acknowledge_posts = False - self.hide_address = True - self.preferred_language = 'en' - self.receive_list_copy = True - self.receive_own_postings = True - self.delivery_mode = DeliveryMode.regular + super(Preferences, self).__init__() + self.acknowledge_posts = Prefs.acknowledge_posts + self.hide_address = Prefs.hide_address + self.preferred_language = Prefs.preferred_language + self.receive_list_copy = Prefs.receive_list_copy + self.receive_own_postings = Prefs.receive_own_postings + self.delivery_mode = Prefs.delivery_mode diff --git a/Mailman/database/model/roster.py b/Mailman/database/model/roster.py index bf8447433..03aa9efc3 100644 --- a/Mailman/database/model/roster.py +++ b/Mailman/database/model/roster.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 by the Free Software Foundation, Inc. +# 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 @@ -15,37 +15,164 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. -from elixir import * +"""An implementation of an IRoster. + +These are hard-coded rosters which know how to filter a set of members to find +the ones that fit a particular role. These are used as the member, owner, +moderator, and administrator roster filters. +""" + from zope.interface import implements -from Mailman.Errors import ExistingAddressError +from Mailman.constants import DeliveryMode, MemberRole +from Mailman.database.model import Member from Mailman.interfaces import IRoster -ADDRESS_KIND = 'Mailman.database.model.address.Address' -ROSTERSET_KIND = 'Mailman.database.model.rosterset.RosterSet' + +class AbstractRoster(object): + """An abstract IRoster class. + This class takes the simple approach of implemented the 'users' and + 'addresses' properties in terms of the 'members' property. This may not + be the most efficient way, but it works. -class Roster(Entity): + This requires that subclasses implement the 'members' property. + """ implements(IRoster) - has_field('name', Unicode) - # Relationships - has_and_belongs_to_many('addresses', of_kind=ADDRESS_KIND) - has_and_belongs_to_many('roster_set', of_kind=ROSTERSET_KIND) + def __init__(self, mlist): + self._mlist = mlist + + @property + def members(self): + raise NotImplementedError + + @property + def users(self): + # Members are linked to addresses, which in turn are linked to users. + # So while the 'members' attribute does most of the work, we have to + # keep a set of unique users. It's possible for the same user to be + # subscribed to a mailing list multiple times with different + # addresses. + users = set(member.address.user for member in self.members) + for user in users: + yield user + + @property + def addresses(self): + # Every Member is linked to exactly one address so the 'members' + # attribute does most of the work. + for member in self.members: + yield member.address + + + +class MemberRoster(AbstractRoster): + """Return all the members of a list.""" + + name = 'member' + + @property + def members(self): + # Query for all the Members which have a role of MemberRole.member and + # are subscribed to this mailing list. XXX we have to use a private + # data attribute of MailList for now. + for member in Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.member): + yield member + + + +class OwnerRoster(AbstractRoster): + """Return all the owners of a list.""" + + name = 'owner' + + @property + def members(self): + # Query for all the Members which have a role of MemberRole.member and + # are subscribed to this mailing list. XXX we have to use a private + # data attribute of MailList for now. + for member in Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.owner): + yield member + + + +class ModeratorRoster(AbstractRoster): + """Return all the owners of a list.""" + + name = 'moderator' + + @property + def members(self): + # Query for all the Members which have a role of MemberRole.member and + # are subscribed to this mailing list. XXX we have to use a private + # data attribute of MailList for now. + for member in Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.moderator): + yield member + + + +class AdministratorRoster(AbstractRoster): + """Return all the administrators of a list.""" + + name = 'administrator' + + @property + def members(self): + # Administrators are defined as the union of the owners and the + # moderators. Until I figure out a more efficient way of doing this, + # this will have to do. + owners = Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.owner) + moderators = Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.moderator) + members = set(owners) + members.update(set(moderators)) + for member in members: + yield member + + + +class RegularMemberRoster(AbstractRoster): + """Return all the regular delivery members of a list.""" + + name = 'regular_members' + + @property + def members(self): + # Query for all the Members which have a role of MemberRole.member and + # are subscribed to this mailing list. Then return only those members + # that have a regular delivery mode. + for member in Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.member): + if member.preferences.delivery_mode == DeliveryMode.regular: + yield member + + + +_digest_modes = ( + DeliveryMode.mime_digests, + DeliveryMode.plaintext_digests, + DeliveryMode.summary_digests, + ) + + + +class DigestMemberRoster(AbstractRoster): + """Return all the regular delivery members of a list.""" + + name = 'regular_members' - def create(self, email_address, real_name=None): - """See IRoster""" - from Mailman.database.model.address import Address - addr = Address.get_by(address=email_address) - if addr: - raise ExistingAddressError(email_address) - addr = Address(address=email_address, real_name=real_name) - # Make sure all the expected links are made, including to the null - # (i.e. everyone) roster. - self.addresses.append(addr) - addr.rosters.append(self) - null_roster = Roster.get_by(name='') - null_roster.addresses.append(addr) - addr.rosters.append(null_roster) - return addr + @property + def members(self): + # Query for all the Members which have a role of MemberRole.member and + # are subscribed to this mailing list. Then return only those members + # that have one of the digest delivery modes. + for member in Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.member): + if member.preferences.delivery_mode in _digest_modes: + yield member diff --git a/Mailman/database/model/user.py b/Mailman/database/model/user.py index be634b9df..d646606a9 100644 --- a/Mailman/database/model/user.py +++ b/Mailman/database/model/user.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 by the Free Software Foundation, Inc. +# 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 @@ -23,15 +23,19 @@ from Mailman import Errors from Mailman.database.model import Address from Mailman.interfaces import IUser +ADDRESS_KIND = 'Mailman.database.model.address.Address' + + class User(Entity): implements(IUser) has_field('real_name', Unicode) has_field('password', Unicode) # Relationships - has_one('profile', of_kind='Mailman.database.model.profile.Profile') - has_many('addresses', of_kind='Mailman.database.model.address.Address') + has_many('addresses', of_kind=ADDRESS_KIND) + # Options + using_options(shortnames=True) def link(self, address): if address.user is not None: diff --git a/Mailman/database/model/version.py b/Mailman/database/model/version.py index e22e8ae11..7b12778ce 100644 --- a/Mailman/database/model/version.py +++ b/Mailman/database/model/version.py @@ -19,7 +19,7 @@ from elixir import * class Version(Entity): - with_fields( - component = Field(String), - version = Field(Integer), - ) + has_field('component', Unicode) + has_field('version', Integer) + # Options + using_options(shortnames=True) diff --git a/Mailman/database/usermanager.py b/Mailman/database/usermanager.py index 97a740803..ed0b552a8 100644 --- a/Mailman/database/usermanager.py +++ b/Mailman/database/usermanager.py @@ -35,47 +35,18 @@ from Mailman.interfaces import IUserManager class UserManager(object): implements(IUserManager) - def __init__(self): - # Create the null roster if it does not already exist. It's more - # likely to exist than not so try to get it before creating it. - lockfile = os.path.join(config.LOCK_DIR, '<umgrcreatelock>') - with LockFile(lockfile): - roster = self.get_roster('') - if roster is None: - self.create_roster('') - objectstore.flush() - - def create_roster(self, name): - roster = Roster.get_by(name=name) - if roster: - raise Errors.RosterExistsError(name) - return Roster(name=name) - - def get_roster(self, name): - return Roster.get_by(name=name) - - def delete_roster(self, roster): - roster.delete() - - @property - def rosters(self): - for roster in Roster.select(): - yield roster - - def create_rosterset(self, name): - return RosterSet(name=name) - - def delete_rosterset(self, rosterset): - rosterset.delete() - - def get_rosterset(self, name): - return RosterSet.get_by(name=name) - - def create_user(self): + def create_user(self, address=None, real_name=None): user = User() - # Users always have a profile - user.profile = Profile() - user.profile.user = user + # Users always have preferences + user.preferences = Preferences() + user.preferences.user = user + if real_name: + user.real_name = real_name + if address: + kws = dict(address=address) + if real_name: + kws['real_name'] = real_name + user.link(Address(**kws)) return user def delete_user(self, user): diff --git a/Mailman/docs/membership.txt b/Mailman/docs/membership.txt new file mode 100644 index 000000000..36b1508a8 --- /dev/null +++ b/Mailman/docs/membership.txt @@ -0,0 +1,183 @@ +List memberships +================ + +Users represent people in Mailman. Users control email addresses, and rosters +are collectons of members. A member gives an email address a role, such as +'member', 'administrator', or 'moderator'. Roster sets are collections of +rosters and a mailing list has a single roster set that contains all its +members, regardless of that member's role. + +Mailing lists and roster sets have an indirect relationship, through the +roster set's name. Roster also have names, but are related to roster sets +by a more direct containment relationship. This is because it is possible to +store mailing list data in a different database than user data. + +When we create a mailing list, it starts out with no members... + + >>> from Mailman.configuration import config + >>> from Mailman.database import flush + >>> mlist = config.list_manager.create('_xtest@example.com') + >>> flush() + >>> mlist + <mailing list "_xtest@example.com" (unlocked) at ...> + >>> sorted(member.address.address for member in mlist.members.members) + [] + >>> sorted(user.real_name for user in mlist.members.users) + [] + >>> sorted(address.address for member in mlist.members.addresses) + [] + +...no owners... + + >>> sorted(member.address.address for member in mlist.owners.members) + [] + >>> sorted(user.real_name for user in mlist.owners.users) + [] + >>> sorted(address.address for member in mlist.owners.addresses) + [] + +...no moderators... + + >>> sorted(member.address.address for member in mlist.moderators.members) + [] + >>> sorted(user.real_name for user in mlist.moderators.users) + [] + >>> sorted(address.address for member in mlist.moderators.addresses) + [] + +...and no administrators. + + >>> sorted(member.address.address + ... for member in mlist.administrators.members) + [] + >>> sorted(user.real_name for user in mlist.administrators.users) + [] + >>> sorted(address.address for member in mlist.administrators.addresses) + [] + + + +Administrators +-------------- + +A mailing list's administrators are defined as union of the list's owners and +the list's moderators. We can add new owners or moderators to this list by +assigning roles to users. First we have to create the user, because there are +no users in the user database yet. + + >>> user_1 = config.user_manager.create_user( + ... 'aperson@example.com', 'Anne Person') + >>> flush() + >>> user_1.real_name + 'Anne Person' + >>> sorted(address.address for address in user_1.addresses) + ['aperson@example.com'] + +We can add Anne as an owner of the mailing list, by creating a member role for +her. + + >>> from Mailman.constants import MemberRole + >>> address_1 = list(user_1.addresses)[0] + >>> address_1.address + 'aperson@example.com' + >>> address_1.subscribe(mlist, MemberRole.owner) + <Member: Anne Person <aperson@example.com> on + _xtest@example.com as MemberRole.owner> + >>> flush() + >>> sorted(member.address.address for member in mlist.owners.members) + ['aperson@example.com'] + >>> sorted(user.real_name for user in mlist.owners.users) + ['Anne Person'] + >>> sorted(address.address for address in mlist.owners.addresses) + ['aperson@example.com'] + +Adding Anne as a list owner also makes her an administrator, but does not make +her a moderator. Nor does it make her a member of the list. + + >>> sorted(user.real_name for user in mlist.administrators.users) + ['Anne Person'] + >>> sorted(user.real_name for user in mlist.moderators.users) + [] + >>> sorted(user.real_name for user in mlist.members.users) + [] + +We can add Ben as a moderator of the list, by creating a different member role +for him. + + >>> user_2 = config.user_manager.create_user( + ... 'bperson@example.com', 'Ben Person') + >>> flush() + >>> user_2.real_name + 'Ben Person' + >>> address_2 = list(user_2.addresses)[0] + >>> address_2.address + 'bperson@example.com' + >>> address_2.subscribe(mlist, MemberRole.moderator) + <Member: Ben Person <bperson@example.com> + on _xtest@example.com as MemberRole.moderator> + >>> flush() + >>> sorted(member.address.address for member in mlist.moderators.members) + ['bperson@example.com'] + >>> sorted(user.real_name for user in mlist.moderators.users) + ['Ben Person'] + >>> sorted(address.address for address in mlist.moderators.addresses) + ['bperson@example.com'] + +Now, both Anne and Ben are list administrators. + + >>> sorted(member.address.address + ... for member in mlist.administrators.members) + ['aperson@example.com', 'bperson@example.com'] + >>> sorted(user.real_name for user in mlist.administrators.users) + ['Anne Person', 'Ben Person'] + >>> sorted(address.address for address in mlist.administrators.addresses) + ['aperson@example.com', 'bperson@example.com'] + + +Members +------- + +Similarly, list members are born of users being given the proper role. It's +more interesting here because these roles should have a preference which can +be used to decide whether the member is to get regular delivery or digest +delivery. Without a preference, Mailman will fall back first to the address's +preference, then the user's preference, then the list's preference. Start +without any member preference to see the system defaults. + + >>> user_3 = config.user_manager.create_user( + ... 'cperson@example.com', 'Claire Person') + >>> flush() + >>> user_3.real_name + 'Claire Person' + >>> address_3 = list(user_3.addresses)[0] + >>> address_3.address + 'cperson@example.com' + >>> address_3.subscribe(mlist, MemberRole.member) + <Member: Claire Person <cperson@example.com> + on _xtest@example.com as MemberRole.member> + >>> flush() + +Claire will be a regular delivery member but not a digest member. + + >>> sorted(address.address for address in mlist.members.addresses) + ['cperson@example.com'] + >>> sorted(address.address for address in mlist.regular_members.addresses) + ['cperson@example.com'] + >>> sorted(address.address for address in mlist.digest_members.addresses) + [] + +It's easy to make the list administrators members of the mailing list too. + + >>> for address in mlist.administrators.addresses: + ... address.subscribe(mlist, MemberRole.member) + <Member: Ben Person <bperson@example.com> on + _xtest@example.com as MemberRole.member> + <Member: Anne Person <aperson@example.com> on + _xtest@example.com as MemberRole.member> + >>> flush() + >>> sorted(address.address for address in mlist.members.addresses) + ['aperson@example.com', 'bperson@example.com', 'cperson@example.com'] + >>> sorted(address.address for address in mlist.regular_members.addresses) + ['aperson@example.com', 'bperson@example.com', 'cperson@example.com'] + >>> sorted(address.address for address in mlist.digest_members.addresses) + [] diff --git a/Mailman/interfaces/address.py b/Mailman/interfaces/address.py index 5f6a9193d..1363d56ab 100644 --- a/Mailman/interfaces/address.py +++ b/Mailman/interfaces/address.py @@ -42,3 +42,9 @@ class IAddress(Interface): """The date and time at which this email address was validated, or None if the email address has not yet been validated. The specific method of validation is not defined here.""") + + def subscribe(mlist, role): + """Subscribe the address to the given mailing list with the given role. + + role is a Mailman.constants.MemberRole enum. + """ diff --git a/Mailman/interfaces/member.py b/Mailman/interfaces/member.py new file mode 100644 index 000000000..5d5e3f717 --- /dev/null +++ b/Mailman/interfaces/member.py @@ -0,0 +1,48 @@ +# 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. + + +"""Interface describing the basics of a member.""" + +from zope.interface import Interface, Attribute + + + +class IMember(Interface): + """A member of a mailing list.""" + + mailing_list = Attribute( + """The mailing list subscribed to.""") + + address = Attribute( + """The email address that's subscribed to the list.""") + + preferences = Attribute( + """The set of preferences for this subscription. + + This will return an IPreferences object using the following lookup + rules: + + 1. member + 2. address + 3. user + 4. mailing list + 5. system default + """) + + role = Attribute( + """The role of this membership.""") diff --git a/Mailman/interfaces/mlistrosters.py b/Mailman/interfaces/mlistrosters.py index 1b407f472..9cd20e3ef 100644 --- a/Mailman/interfaces/mlistrosters.py +++ b/Mailman/interfaces/mlistrosters.py @@ -46,27 +46,6 @@ class IMailingListRosters(Interface): This includes the IUsers who are both owners and moderators of the mailing list.""") - owner_rosters = Attribute( - """An iterator over the IRosters containing all the owners of this - mailing list.""") - - moderator_rosters = Attribute( - """An iterator over the IRosters containing all the moderators of this - mailing list.""") - - def add_owner_roster(roster): - """Add an IRoster to this mailing list's set of owner rosters.""" - - def delete_owner_roster(roster): - """Remove an IRoster from this mailing list's set of owner rosters.""" - - def add_moderator_roster(roster): - """Add an IRoster to this mailing list's set of moderator rosters.""" - - def delete_moderator_roster(roster): - """Remove an IRoster from this mailing list's set of moderator - rosters.""" - members = Attribute( """An iterator over all the members of the mailing list, regardless of whether they are to receive regular messages or digests, or whether @@ -82,15 +61,3 @@ class IMailingListRosters(Interface): postings to this mailing list, regardless of whether they have their deliver disabled or not, or of the type of digest they are to receive.""") - - member_rosters = Attribute( - """An iterator over the IRosters containing all the members of this - mailing list.""") - - def add_member_roster(roster): - """Add the given IRoster to the list of rosters for the members of this - mailing list.""" - - def remove_member_roster(roster): - """Remove the given IRoster to the list of rosters for the members of - this mailing list.""" diff --git a/Mailman/interfaces/profile.py b/Mailman/interfaces/profile.py index ed3968f9d..a0b5131cb 100644 --- a/Mailman/interfaces/profile.py +++ b/Mailman/interfaces/profile.py @@ -15,13 +15,13 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. -"""Interface for a profile, which describes delivery related information.""" +"""Interface for preferences.""" from zope.interface import Interface, Attribute -class IProfile(Interface): +class IPreferences(Interface): """Delivery related information.""" acknowledge_posts = Attribute( diff --git a/Mailman/interfaces/roster.py b/Mailman/interfaces/roster.py index 7ddbd5101..281e02a05 100644 --- a/Mailman/interfaces/roster.py +++ b/Mailman/interfaces/roster.py @@ -22,21 +22,25 @@ from zope.interface import Interface, Attribute class IRoster(Interface): - """A roster is a collection of IUsers.""" + """A roster is a collection of IMembers.""" name = Attribute( """The name for this roster. Rosters are considered equal if they have the same name.""") - addresses = Attribute( - """An iterator over all the addresses managed by this roster.""") + members = Attribute( + """An iterator over all the IMembers managed by this roster.""") + + users = Attribute( + """An iterator over all the IUsers reachable by this roster. - def create(email_address, real_name=None): - """Create an IAddress and return it. + This returns all the users for all the members managed by this roster. + """) - email_address is textual email address to add. real_name is the - optional real name that gets associated with the email address. + addresses = Attribute( + """An iterator over all the IAddresses reachable by this roster. - Raises ExistingAddressError if address already exists. - """ + This returns all the addresses for all the users for all the members + managed by this roster. + """) diff --git a/Mailman/interfaces/user.py b/Mailman/interfaces/user.py index 6990eee4b..bed1db08d 100644 --- a/Mailman/interfaces/user.py +++ b/Mailman/interfaces/user.py @@ -30,8 +30,8 @@ class IUser(Interface): password = Attribute( """This user's password information.""") - profile = Attribute( - """The default IProfile for this user.""") + preferences = Attribute( + """The default preferences for this user.""") addresses = Attribute( """An iterator over all the IAddresses controlled by this user.""") diff --git a/Mailman/interfaces/usermanager.py b/Mailman/interfaces/usermanager.py index 302fe9b60..38dd06dfa 100644 --- a/Mailman/interfaces/usermanager.py +++ b/Mailman/interfaces/usermanager.py @@ -33,27 +33,6 @@ class IUserManager(Interface): IUsers in all IRosters. """ - def create_roster(name): - """Create and return the named IRoster. - - Raises RosterExistsError if the named roster already exists. - """ - - def get_roster(name): - """Return the named IRoster. - - Raises NoSuchRosterError if the named roster doesnot yet exist. - """ - - def delete_roster(name): - """Delete the named IRoster. - - Raises NoSuchRosterError if the named roster doesnot yet exist. - """ - - rosters = Attribute( - """An iterator over all IRosters managed by this user manager.""") - def create_user(): """Create and return an IUser.""" @@ -68,15 +47,3 @@ class IUserManager(Interface): users = Attribute( """An iterator over all the IUsers managed by this user manager.""") - - def create_rosterset(): - """Create and return a new IRosterSet. - - IRosterSets manage groups of IRosters. - """ - - def delete_rosterset(rosterset): - """Delete the given IRosterSet.""" - - def get_rosterset(serial): - """Return the IRosterSet that matches the serial number, or None.""" diff --git a/Mailman/testing/test_membership.py b/Mailman/testing/test_membership.py index 8e8285034..8a25287ac 100644 --- a/Mailman/testing/test_membership.py +++ b/Mailman/testing/test_membership.py @@ -19,6 +19,7 @@ import os import time +import doctest import unittest from Mailman import MailList @@ -30,6 +31,8 @@ from Mailman.UserDesc import UserDesc from Mailman.configuration import config from Mailman.testing.base import TestBase +OPTIONFLAGS = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE + def password(cleartext): @@ -387,6 +390,6 @@ class TestMembers(TestBase): def test_suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(TestNoMembers)) - suite.addTest(unittest.makeSuite(TestMembers)) + suite.addTest(doctest.DocFileSuite('../docs/membership.txt', + optionflags=OPTIONFLAGS)) return suite |
