diff options
| -rw-r--r-- | Mailman/database/model/address.py | 2 | ||||
| -rw-r--r-- | Mailman/database/model/mailinglist.py | 3 | ||||
| -rw-r--r-- | Mailman/database/model/member.py | 20 | ||||
| -rw-r--r-- | Mailman/database/model/profile.py | 11 | ||||
| -rw-r--r-- | Mailman/database/model/roster.py | 16 | ||||
| -rw-r--r-- | Mailman/database/model/user.py | 15 | ||||
| -rw-r--r-- | Mailman/database/types.py | 4 | ||||
| -rw-r--r-- | Mailman/database/usermanager.py | 2 | ||||
| -rw-r--r-- | Mailman/docs/usermanager.txt | 9 | ||||
| -rw-r--r-- | Mailman/docs/users.txt | 145 | ||||
| -rw-r--r-- | Mailman/interfaces/address.py | 3 | ||||
| -rw-r--r-- | Mailman/interfaces/profile.py | 38 | ||||
| -rw-r--r-- | Mailman/interfaces/user.py | 19 | ||||
| -rw-r--r-- | Mailman/testing/test_user.py | 4 |
14 files changed, 171 insertions, 120 deletions
diff --git a/Mailman/database/model/address.py b/Mailman/database/model/address.py index 450c40235..7500197a8 100644 --- a/Mailman/database/model/address.py +++ b/Mailman/database/model/address.py @@ -50,8 +50,10 @@ class Address(Entity): def subscribe(self, mlist, role): from Mailman.database.model import Member + from Mailman.database.model import Preferences # This member has no preferences by default. member = Member(role=role, mailing_list=mlist.fqdn_listname, address=self) + member.preferences = Preferences() return member diff --git a/Mailman/database/model/mailinglist.py b/Mailman/database/model/mailinglist.py index 1e7823911..edd2eab0d 100644 --- a/Mailman/database/model/mailinglist.py +++ b/Mailman/database/model/mailinglist.py @@ -22,8 +22,6 @@ 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): @@ -156,7 +154,6 @@ 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 bcf859cf4..89a98d327 100644 --- a/Mailman/database/model/member.py +++ b/Mailman/database/model/member.py @@ -19,7 +19,6 @@ 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 @@ -36,27 +35,10 @@ class Member(Entity): has_field('mailing_list', Unicode) # Relationships belongs_to('address', of_kind=ADDRESS_KIND) - belongs_to('_preferences', of_kind=PREFERENCE_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 - # 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, - 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 935ae08fb..33511f54b 100644 --- a/Mailman/database/model/profile.py +++ b/Mailman/database/model/profile.py @@ -19,7 +19,6 @@ from elixir import * from email.utils import formataddr from zope.interface import implements -from Mailman.constants import SystemDefaultPreferences as Prefs from Mailman.database.types import EnumType from Mailman.interfaces import IPreferences @@ -41,11 +40,5 @@ class Preferences(Entity): # Options using_options(shortnames=True) - def __init__(self): - 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 + def __repr__(self): + return '<Preferences object at %#x>' % id(self) diff --git a/Mailman/database/model/roster.py b/Mailman/database/model/roster.py index 03aa9efc3..ee50cddf0 100644 --- a/Mailman/database/model/roster.py +++ b/Mailman/database/model/roster.py @@ -25,6 +25,7 @@ moderator, and administrator roster filters. from zope.interface import implements from Mailman.constants import DeliveryMode, MemberRole +from Mailman.constants import SystemDefaultPreferences from Mailman.database.model import Member from Mailman.interfaces import IRoster @@ -137,6 +138,17 @@ class AdministratorRoster(AbstractRoster): +def _delivery_mode(member): + if member.preferences.delivery_mode is not None: + return member.preferences.delivery_mode + if member.address.preferences.delivery_mode is not None: + return member.address.preferences.delivery_mode + if (member.address.user and + member.address.user.preferences.delivery_mode is not None): + return member.address.user.preferences.delivery_mode + return SystemDefaultPreferences.delivery_mode + + class RegularMemberRoster(AbstractRoster): """Return all the regular delivery members of a list.""" @@ -149,7 +161,7 @@ class RegularMemberRoster(AbstractRoster): # 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: + if _delivery_mode(member) == DeliveryMode.regular: yield member @@ -174,5 +186,5 @@ class DigestMemberRoster(AbstractRoster): # 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: + if _delivery_mode(member) in _digest_modes: yield member diff --git a/Mailman/database/model/user.py b/Mailman/database/model/user.py index 45fcbfdd3..9419f181d 100644 --- a/Mailman/database/model/user.py +++ b/Mailman/database/model/user.py @@ -21,6 +21,7 @@ from zope.interface import implements from Mailman import Errors from Mailman.database.model import Address +from Mailman.database.model import Preferences from Mailman.interfaces import IUser ADDRESS_KIND = 'Mailman.database.model.address.Address' @@ -57,3 +58,17 @@ class User(Entity): def controls(self, address): found = Address.get_by(address=address) return bool(found and found.user is self) + + def register(self, address, real_name=None): + # First, see if the address already exists + addrobj = Address.get_by(address=address) + if addrobj is None: + if real_name is None: + real_name = '' + addrobj = Address(address=address, real_name=real_name) + # Link the address to the user if it is not already linked. + if addrobj.user is not None: + raise Errors.AddressAlreadyLinkedError(addrobj) + addrobj.user = self + self.addresses.append(addrobj) + return addrobj diff --git a/Mailman/database/types.py b/Mailman/database/types.py index 00ad29559..79ea8767d 100644 --- a/Mailman/database/types.py +++ b/Mailman/database/types.py @@ -28,11 +28,15 @@ class EnumType(types.TypeDecorator): impl = types.String def convert_bind_param(self, value, engine): + if value is None: + return None return '%s:%s.%d' % (value.enumclass.__module__, value.enumclass.__name__, int(value)) def convert_result_value(self, value, engine): + if value is None: + return None path, intvalue = value.rsplit(':', 1) modulename, classname = intvalue.rsplit('.', 1) __import__(modulename) diff --git a/Mailman/database/usermanager.py b/Mailman/database/usermanager.py index 2f12188aa..569ca2152 100644 --- a/Mailman/database/usermanager.py +++ b/Mailman/database/usermanager.py @@ -40,6 +40,7 @@ class UserManager(object): user.real_name = (real_name if real_name is not None else '') if address: user.link(Address(address=address, real_name=user.real_name)) + user.preferences = Preferences() return user def delete_user(self, user): @@ -61,6 +62,7 @@ class UserManager(object): if real_name is None: real_name = '' address = Address(address=address, real_name=real_name) + address.preferences = Preferences() return address def delete_address(self, address): diff --git a/Mailman/docs/usermanager.txt b/Mailman/docs/usermanager.txt index 389c68956..1f863b606 100644 --- a/Mailman/docs/usermanager.txt +++ b/Mailman/docs/usermanager.txt @@ -20,7 +20,7 @@ Creating users There are several ways you can create a user object. The simplest is to create a 'blank' user by not providing an address or real name at creation time. This user will have an empty string as their real name, but will not -have a password or preferences. +have a password. >>> from Mailman.interfaces import IUser >>> user = mgr.create_user() @@ -31,11 +31,14 @@ have a password or preferences. [] >>> user.real_name '' - >>> print user.preferences - None >>> print user.password None +The user has preferences, but none of them will be specified. + + >>> print user.preferences + <Preferences ...> + A user can be assigned a real name. >>> user.real_name = 'Anne Person' diff --git a/Mailman/docs/users.txt b/Mailman/docs/users.txt index a65527eff..2b60ed0bc 100644 --- a/Mailman/docs/users.txt +++ b/Mailman/docs/users.txt @@ -17,9 +17,9 @@ User data Users may have a real name and a password. - >>> user = mgr.create_user() - >>> user.password = 'my password' - >>> user.real_name = 'Zoe Person' + >>> user_1 = mgr.create_user() + >>> user_1.password = 'my password' + >>> user_1.real_name = 'Zoe Person' >>> flush() >>> sorted(user.real_name for user in mgr.users) ['Zoe Person'] @@ -28,8 +28,8 @@ Users may have a real name and a password. The password and real name can be changed at any time. - >>> user.real_name = 'Zoe X. Person' - >>> user.password = 'another password' + >>> user_1.real_name = 'Zoe X. Person' + >>> user_1.password = 'another password' >>> flush() >>> sorted(user.real_name for user in mgr.users) ['Zoe X. Person'] @@ -41,101 +41,124 @@ 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 -addresses, but addresses may be control by only one user. +addresses they control, in the form of IAddress objects. A user can control +many addresses, but addresses may be controlled by only one user. -Given a user and an address, you can link the two together. +The easiest way to link a user to an address is to just register the new +address on a user object. - >>> roster = mgr.get_roster('') - >>> address = roster.create('aperson@example.com', 'Anne Person') - >>> user.link(address) + >>> user_1.register('zperson@example.com', 'Zoe Person') + <Address: Zoe Person <zperson@example.com> [not verified]> + >>> user_1.register('zperson@example.org') + <Address: zperson@example.org [not verified]> >>> flush() - >>> sorted(address.address for address in user.addresses) - ['aperson@example.com'] + >>> sorted(address.address for address in user_1.addresses) + ['zperson@example.com', 'zperson@example.org'] + >>> sorted(address.real_name for address in user_1.addresses) + ['', 'Zoe Person'] + +You can also create the address separately and then link it to the user. + + >>> address_1 = mgr.create_address('zperson@example.net') + >>> user_1.link(address_1) + >>> flush() + >>> sorted(address.address for address in user_1.addresses) + ['zperson@example.com', 'zperson@example.net', 'zperson@example.org'] + >>> sorted(address.real_name for address in user_1.addresses) + ['', '', 'Zoe Person'] But don't try to link an address to more than one user. >>> another_user = mgr.create_user() - >>> another_user.link(address) + >>> another_user.link(address_1) Traceback (most recent call last): ... - AddressAlreadyLinkedError: Anne Person <aperson@example.com> + AddressAlreadyLinkedError: zperson@example.net You can also ask whether a given user controls a given address. - >>> user.controls(address) + >>> user_1.controls(address_1.address) True - >>> not_my_address = roster.create('bperson@example.com', 'Ben Person') - >>> user.controls(not_my_address) + >>> user_1.controls('bperson@example.com') False Given a text email address, the user manager can find the user that controls that address. - >>> mgr.get_user('aperson@example.com') is user + >>> mgr.get_user('zperson@example.com') is user_1 True - >>> mgr.get_user('bperson@example.com') is None + >>> mgr.get_user('zperson@example.net') is user_1 True + >>> mgr.get_user('zperson@example.org') is user_1 + True + >>> print mgr.get_user('bperson@example.com') + None Addresses can also be unlinked from a user. - >>> user.unlink(address) - >>> user.controls(address) + >>> user_1.unlink(address_1) + >>> user_1.controls('zperson@example.net') False - >>> mgr.get_user('aperson@example.com') is None - True + >>> print mgr.get_user('aperson@example.net') + None But don't try to unlink the address from a user it's not linked to. - >>> user.unlink(address) + >>> user_1.unlink(address_1) Traceback (most recent call last): ... - AddressNotLinkedError: Anne Person <aperson@example.com> - >>> another_user.unlink(address) + AddressNotLinkedError: zperson@example.net + >>> another_user.unlink(address_1) Traceback (most recent call last): ... - AddressNotLinkedError: Anne Person <aperson@example.com> - >>> mgr.delete_user(another_user) + AddressNotLinkedError: zperson@example.net -Users and profiles ------------------- +Users and preferences +--------------------- -Users always have a default profile. +This is a helper function for the following section. - >>> from Mailman.interfaces import IProfile - >>> IProfile.providedBy(user.profile) - True - -A profile is a set of preferences such as whether the user wants to receive an -acknowledgment of all of their posts to a mailing list... + >>> def show_prefs(prefs): + ... print 'acknowledge_posts :', prefs.acknowledge_posts + ... print 'preferred_language :', prefs.preferred_language + ... print 'receive_list_copy :', prefs.receive_list_copy + ... print 'receive_own_postings :', prefs.receive_own_postings + ... print 'delivery_mode :', prefs.delivery_mode - >>> user.profile.acknowledge_posts - False +Users have preferences, but these preferences have no default settings. -...whether the user wants to hide their email addresses on web pages and in -postings to the list... + >>> from Mailman.interfaces import IPreferences + >>> show_prefs(user_1.preferences) + acknowledge_posts : None + preferred_language : None + receive_list_copy : None + receive_own_postings : None + delivery_mode : None - >>> user.profile.hide_address - True +Some of these preferences are booleans and they can be set to True or False. -...the language code for the user's preferred language... - - >>> user.profile.preferred_language - 'en' - -...whether the user wants to receive the list's copy of a message if they are -explicitly named in one of the recipient headers... - - >>> user.profile.receive_list_copy - True - -...whether the user wants to receive a copy of their own postings... + >>> from Mailman.constants import DeliveryMode + >>> prefs = user_1.preferences + >>> prefs.acknowledge_posts = True + >>> prefs.preferred_language = 'it' + >>> prefs.receive_list_copy = False + >>> prefs.receive_own_postings = False + >>> prefs.delivery_mode = DeliveryMode.regular + >>> flush() + >>> show_prefs(user_1.preferences) + acknowledge_posts : True + preferred_language : it + receive_list_copy : False + receive_own_postings : False + delivery_mode : DeliveryMode.regular - >>> user.profile.receive_own_postings - True -...and the preferred delivery method. +Clean up +-------- - >>> print user.profile.delivery_mode - DeliveryMode.regular + >>> for user in mgr.users: + ... mgr.delete_user(user) + >>> flush() + >>> sorted(mgr.users) + [] diff --git a/Mailman/interfaces/address.py b/Mailman/interfaces/address.py index 1363d56ab..c367774c3 100644 --- a/Mailman/interfaces/address.py +++ b/Mailman/interfaces/address.py @@ -48,3 +48,6 @@ class IAddress(Interface): role is a Mailman.constants.MemberRole enum. """ + + preferences = Attribute( + """This address's preferences.""") diff --git a/Mailman/interfaces/profile.py b/Mailman/interfaces/profile.py index a0b5131cb..17cfebae6 100644 --- a/Mailman/interfaces/profile.py +++ b/Mailman/interfaces/profile.py @@ -25,29 +25,37 @@ class IPreferences(Interface): """Delivery related information.""" acknowledge_posts = Attribute( - """Boolean specifying whether to send an acknowledgment receipt for - every posting to the mailing list. - """) + """Send an acknowledgment for every posting? - hide_address = Attribute( - """Boolean specifying whether to hide this email address from fellow - list members. - """) + This preference can be True, False, or None. True means the user is + sent a receipt for each message they send to the mailing list. False + means that no receipt is sent. None means no preference is + specified.""") preferred_language = Attribute( - """Preferred language for interacting with a mailing list.""") + """The preferred language for interacting with a mailing list. + + This is either the language code for the preferred language, or None + meaning no preferred language is specified.""") receive_list_copy = Attribute( - """Boolean specifying whether to receive a list copy if the user is - explicitly named in one of the recipient headers. - """) + """Should an explicit recipient receive a list copy? + + When a list member is explicitly named in a message's recipients + (e.g. the To or CC headers), and this preference is True, the + recipient will still receive a list copy of the message. When False, + this list copy will be suppressed. None means no preference is + specified.""") receive_own_postings = Attribute( - """Boolean specifying whether to receive a list copy of the user's own - postings to the mailing list. - """) + """Should the poster get a list copy of their own messages? + + When this preference is True, a list copy will be sent to the poster + of all messages. When False, this list copy will be suppressed. None + means no preference is specified.""") delivery_mode = Attribute( """The preferred delivery mode. - This is an enum constant of the type DeliveryMode.""") + This is an enum constant of the type DeliveryMode. It may also be + None which means that no preference is specified.""") diff --git a/Mailman/interfaces/user.py b/Mailman/interfaces/user.py index 9e89c2416..f7647597d 100644 --- a/Mailman/interfaces/user.py +++ b/Mailman/interfaces/user.py @@ -30,19 +30,21 @@ class IUser(Interface): password = Attribute( """This user's password information.""") - preferences = Attribute( - """The default preferences for this user.""") - addresses = Attribute( """An iterator over all the IAddresses controlled by this user.""") - def register(address): + def register(address, real_name=None): """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. + object. If real_name is not given, the empty string is used. + + 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, in which case + real_name is ignored. + + Return the new IAddress object. """ def link(address): @@ -66,3 +68,6 @@ class IUser(Interface): 'address' is a text email address. This method returns true if the user controls the given email address, otherwise false. """ + + preferences = Attribute( + """This user's preferences.""") diff --git a/Mailman/testing/test_user.py b/Mailman/testing/test_user.py index 1c075a164..59b686795 100644 --- a/Mailman/testing/test_user.py +++ b/Mailman/testing/test_user.py @@ -20,7 +20,9 @@ import doctest import unittest -options = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE +options = (doctest.ELLIPSIS + | doctest.NORMALIZE_WHITESPACE + | doctest.REPORT_NDIFF) def test_suite(): |
