diff options
| author | Barry Warsaw | 2011-04-22 19:26:43 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2011-04-22 19:26:43 -0400 |
| commit | b5b015ce524157cfef4b795e4b0c7ff17b4d0fe2 (patch) | |
| tree | 260169450ab382cf59143f0fe8bae0fc0507452f /src | |
| parent | 6959e6adcb582172aeec01ef6b6a75b9ba85017b (diff) | |
| download | mailman-b5b015ce524157cfef4b795e4b0c7ff17b4d0fe2.tar.gz mailman-b5b015ce524157cfef4b795e4b0c7ff17b4d0fe2.tar.zst mailman-b5b015ce524157cfef4b795e4b0c7ff17b4d0fe2.zip | |
Give IMembers a unique member id. We have to do this in order to give them a
path at the root of the resource tree (i.e. /members/X) and because when
members can be subscribed by their IUser record (for preferred address), their
canonical location cannot contain the address they are subscribed with. When
their preferred address changes (without otherwise touching their membership),
this address will change and that's not good for a canonical location.
Other changes:
* Added IMember.member_id attribute
* Added ISubscriptionService.get_member() so member records can be retrieve by
member id.
* We can now get individual members by id via the REST API.
* Extend the UniqueIDFactory so that it can take a 'context' (defaulting to
None). The context is only used in the testing infrastructure so that
separate files can be used for user ids and member ids. Otherwise, we'd
have gaps in those sequences.
* When *not* in testing mode, ensure that UIDs cannot be reused by keeping a
table of all UIDs ever handed out. We *should* never get collisions, but
this ensures it.
* Clean up mailman.sql
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/database/mailman.sql | 17 | ||||
| -rw-r--r-- | src/mailman/interfaces/member.py | 3 | ||||
| -rw-r--r-- | src/mailman/interfaces/membership.py | 9 | ||||
| -rw-r--r-- | src/mailman/interfaces/user.py | 2 | ||||
| -rw-r--r-- | src/mailman/model/member.py | 21 | ||||
| -rw-r--r-- | src/mailman/model/tests/test_uid.py | 48 | ||||
| -rw-r--r-- | src/mailman/model/uid.py | 72 | ||||
| -rw-r--r-- | src/mailman/model/user.py | 5 | ||||
| -rw-r--r-- | src/mailman/rest/adapters.py | 13 | ||||
| -rw-r--r-- | src/mailman/rest/docs/membership.txt | 116 | ||||
| -rw-r--r-- | src/mailman/rest/members.py | 32 | ||||
| -rw-r--r-- | src/mailman/rest/root.py | 6 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_membership.py | 19 | ||||
| -rw-r--r-- | src/mailman/utilities/uid.py | 25 |
14 files changed, 317 insertions, 71 deletions
diff --git a/src/mailman/database/mailman.sql b/src/mailman/database/mailman.sql index 5af8656c2..deb266297 100644 --- a/src/mailman/database/mailman.sql +++ b/src/mailman/database/mailman.sql @@ -19,8 +19,7 @@ CREATE TABLE acceptablealias ( ); CREATE INDEX ix_acceptablealias_mailing_list_id ON acceptablealias (mailing_list_id); -CREATE INDEX ix_acceptablealias_alias - ON acceptablealias ("alias"); +CREATE INDEX ix_acceptablealias_alias ON acceptablealias ("alias"); CREATE TABLE address ( id INTEGER NOT NULL, @@ -197,6 +196,7 @@ CREATE TABLE mailinglist ( CREATE TABLE member ( id INTEGER NOT NULL, + _member_id TEXT, role TEXT, mailing_list TEXT, moderation_action INTEGER, @@ -211,6 +211,9 @@ CREATE TABLE member ( CONSTRAINT member_user_id_fk FOREIGN KEY (user_id) REFERENCES user (id) ); +CREATE INDEX ix_member__member_id ON member (_member_id); +CREATE INDEX ix_member_address_id ON member (address_id); +CREATE INDEX ix_member_preferences_id ON member (preferences_id); CREATE TABLE message ( id INTEGER NOT NULL, @@ -287,8 +290,6 @@ CREATE TABLE version ( CREATE INDEX ix__request_mailing_list_id ON _request (mailing_list_id); CREATE INDEX ix_address_preferences_id ON address (preferences_id); CREATE INDEX ix_address_user_id ON address (user_id); -CREATE INDEX ix_member_address_id ON member (address_id); -CREATE INDEX ix_member_preferences_id ON member (preferences_id); CREATE INDEX ix_pendedkeyvalue_pended_id ON pendedkeyvalue (pended_id); CREATE INDEX ix_user_preferences_id ON user (preferences_id); @@ -298,3 +299,11 @@ CREATE TABLE ban ( mailing_list TEXT, PRIMARY KEY (id) ); + +CREATE TABLE uid ( + -- Keep track of all assigned unique ids to prevent re-use. + id INTEGER NOT NULL, + uid TEXT, + PRIMARY KEY (id) + ); +CREATE INDEX ix_uid_uid ON uid (uid);
\ No newline at end of file diff --git a/src/mailman/interfaces/member.py b/src/mailman/interfaces/member.py index a5e693411..fc635f21c 100644 --- a/src/mailman/interfaces/member.py +++ b/src/mailman/interfaces/member.py @@ -121,6 +121,9 @@ class NotAMemberError(MembershipError): class IMember(Interface): """A member of a mailing list.""" + member_id = Attribute( + """The member's unique, random identifier (sha1 hex digest).""") + mailing_list = Attribute( """The mailing list subscribed to.""") diff --git a/src/mailman/interfaces/membership.py b/src/mailman/interfaces/membership.py index fb3e0c6a2..ec5f9ea69 100644 --- a/src/mailman/interfaces/membership.py +++ b/src/mailman/interfaces/membership.py @@ -45,6 +45,15 @@ class ISubscriptionService(Interface): :rtype: list of `IMember` """ + def get_member(member_id): + """Return a member record matching the member id. + + :param member_id: A member id. + :type member_id: unicode + :return: The matching member, or None if no matching member is found. + :rtype: `IMember` + """ + def __iter__(): """See `get_members()`.""" diff --git a/src/mailman/interfaces/user.py b/src/mailman/interfaces/user.py index ceffec57f..46dd3ed63 100644 --- a/src/mailman/interfaces/user.py +++ b/src/mailman/interfaces/user.py @@ -54,7 +54,7 @@ class IUser(Interface): """This user's password information.""") user_id = Attribute( - """The user's unique, random, identifier (sha1 hex digest).""") + """The user's unique, random identifier (sha1 hex digest).""") created_on = Attribute( """The date and time at which this user was created.""") diff --git a/src/mailman/model/member.py b/src/mailman/model/member.py index 50ae921c3..9f3c1a58a 100644 --- a/src/mailman/model/member.py +++ b/src/mailman/model/member.py @@ -38,6 +38,10 @@ from mailman.interfaces.listmanager import IListManager from mailman.interfaces.member import IMember, MemberRole from mailman.interfaces.user import IUser from mailman.interfaces.usermanager import IUserManager +from mailman.utilities.uid import UniqueIDFactory + + +uid_factory = UniqueIDFactory(context='members') @@ -45,6 +49,7 @@ class Member(Model): implements(IMember) id = Int(primary=True) + _member_id = Unicode() role = Enum() mailing_list = Unicode() moderation_action = Enum() @@ -57,6 +62,7 @@ class Member(Model): _user = Reference(user_id, 'User.id') def __init__(self, role, mailing_list, subscriber): + self._member_id = uid_factory.new_uid() self.role = role self.mailing_list = mailing_list if IAddress.providedBy(subscriber): @@ -85,13 +91,20 @@ class Member(Model): self.address, self.mailing_list, self.role) @property + def member_id(self): + """See `IMember`.""" + return self._member_id + + @property def address(self): + """See `IMember`.""" return (self._user.preferred_address if self._address is None else self._address) @property def user(self): + """See `IMember`.""" return (self._user if self._address is None else getUtility(IUserManager).get_user(self._address.email)) @@ -111,33 +124,41 @@ class Member(Model): @property def acknowledge_posts(self): + """See `IMember`.""" return self._lookup('acknowledge_posts') @property def preferred_language(self): + """See `IMember`.""" return self._lookup('preferred_language') @property def receive_list_copy(self): + """See `IMember`.""" return self._lookup('receive_list_copy') @property def receive_own_postings(self): + """See `IMember`.""" return self._lookup('receive_own_postings') @property def delivery_mode(self): + """See `IMember`.""" return self._lookup('delivery_mode') @property def delivery_status(self): + """See `IMember`.""" return self._lookup('delivery_status') @property def options_url(self): + """See `IMember`.""" # XXX Um, this is definitely wrong return 'http://example.com/' + self.address.email def unsubscribe(self): + """See `IMember`.""" config.db.store.remove(self.preferences) config.db.store.remove(self) diff --git a/src/mailman/model/tests/test_uid.py b/src/mailman/model/tests/test_uid.py new file mode 100644 index 000000000..6f5cde4d2 --- /dev/null +++ b/src/mailman/model/tests/test_uid.py @@ -0,0 +1,48 @@ +# Copyright (C) 2011 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Test the UID model class.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'test_suite', + ] + + +import unittest + +from mailman.model.uid import UID +from mailman.testing.layers import ConfigLayer + + + +class TestUID(unittest.TestCase): + layer = ConfigLayer + + def test_record(self): + UID.record('abc') + UID.record('def') + self.assertRaises(ValueError, UID.record, 'abc') + + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestUID)) + return suite diff --git a/src/mailman/model/uid.py b/src/mailman/model/uid.py new file mode 100644 index 000000000..5b8f027d6 --- /dev/null +++ b/src/mailman/model/uid.py @@ -0,0 +1,72 @@ +# Copyright (C) 2011 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Unique IDs.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'UID', + ] + + +from storm.locals import Int, Unicode + +from mailman.config import config +from mailman.database.model import Model + + + +class UID(Model): + """Enforce uniqueness of uids through a database table. + + This is used so that unique ids don't have to be tracked by each + individual model object that uses them. So for example, when a user is + deleted, we don't have to keep separate track of its uid to prevent it + from ever being used again. This class, hooked up to the + `UniqueIDFactory` serves that purpose. + + There is no interface for this class, because it's purely an internal + implementation detail. + + Since it's a hash, it's overwhelmingly more common for a uid to be + unique. + """ + id = Int(primary=True) + uid = Unicode() + + def __init__(self, uid): + super(UID, self).__init__() + self.uid = uid + config.db.store.add(self) + + def __repr__(self): + return '<UID {0} at {1}>'.format(self.uid, id(self)) + + @staticmethod + def record(uid): + """Record the uid in the database. + + :param uid: The unique id. + :type uid: unicode + :raises ValueError: if the id is not unique. + """ + existing = config.db.store.find(UID, uid=uid) + if existing.count() != 0: + raise ValueError(uid) + return UID(uid) diff --git a/src/mailman/model/user.py b/src/mailman/model/user.py index 7c4c15d36..a29837bb4 100644 --- a/src/mailman/model/user.py +++ b/src/mailman/model/user.py @@ -37,7 +37,10 @@ from mailman.model.address import Address from mailman.model.preferences import Preferences from mailman.model.roster import Memberships from mailman.utilities.datetime import factory as date_factory -from mailman.utilities.uid import factory as uid_factory +from mailman.utilities.uid import UniqueIDFactory + + +uid_factory = UniqueIDFactory(context='users') diff --git a/src/mailman/rest/adapters.py b/src/mailman/rest/adapters.py index 5cbb89bc1..792609161 100644 --- a/src/mailman/rest/adapters.py +++ b/src/mailman/rest/adapters.py @@ -31,11 +31,13 @@ from zope.component import getUtility from zope.interface import implements from mailman.app.membership import add_member, delete_member +from mailman.config import config from mailman.core.constants import system_preferences from mailman.interfaces.address import InvalidEmailAddressError from mailman.interfaces.listmanager import IListManager, NoSuchListError from mailman.interfaces.member import DeliveryMode from mailman.interfaces.membership import ISubscriptionService +from mailman.model.member import Member from mailman.utilities.passwords import make_user_friendly_password @@ -64,6 +66,17 @@ class SubscriptionService: sorted(mailing_list.members.members, key=address_of_member)) return members + def get_member(self, member_id): + """See `ISubscriptionService`.""" + members = config.db.store.find( + Member, + Member._member_id == member_id) + if members.count() == 0: + return None + else: + assert members.count() == 1, 'Too many matching members' + return members[0] + def __iter__(self): for member in self.get_members(): yield member diff --git a/src/mailman/rest/docs/membership.txt b/src/mailman/rest/docs/membership.txt index e4ab02ef7..70f7fc357 100644 --- a/src/mailman/rest/docs/membership.txt +++ b/src/mailman/rest/docs/membership.txt @@ -44,11 +44,23 @@ the REST interface. address: bperson@example.com fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/bperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/1 + user: http://localhost:9001/3.0/users/1 http_etag: "..." start: 0 total_size: 1 +Bart's specific membership can be accessed directly: + + >>> dump_json('http://localhost:9001/3.0/members/1') + address: bperson@example.com + fqdn_listname: test-one@example.com + http_etag: ... + role: member + self_link: http://localhost:9001/3.0/members/1 + user: http://localhost:9001/3.0/users/1 + When Cris also joins the mailing list, her subscription is also available via the REST interface. @@ -58,12 +70,16 @@ the REST interface. address: bperson@example.com fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/bperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/1 + user: http://localhost:9001/3.0/users/1 entry 1: address: cperson@example.com fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/cperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/2 + user: http://localhost:9001/3.0/users/2 http_etag: "..." start: 0 total_size: 2 @@ -79,17 +95,23 @@ subscribes, she is returned first. address: aperson@example.com fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/aperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/3 + user: http://localhost:9001/3.0/users/3 entry 1: address: bperson@example.com fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/bperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/1 + user: http://localhost:9001/3.0/users/1 entry 2: address: cperson@example.com fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/cperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/2 + user: http://localhost:9001/3.0/users/2 http_etag: "..." start: 0 total_size: 3 @@ -102,32 +124,44 @@ address. Anna and Cris subscribe to this new mailing list. >>> subscribe(mlist_two, 'Anna') >>> subscribe(mlist_two, 'Cris') +User ids are different than member ids. + >>> dump_json('http://localhost:9001/3.0/members') entry 0: address: aperson@example.com fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/aperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/4 + user: http://localhost:9001/3.0/users/3 entry 1: address: cperson@example.com fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/cperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/5 + user: http://localhost:9001/3.0/users/2 entry 2: address: aperson@example.com fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/aperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/3 + user: http://localhost:9001/3.0/users/3 entry 3: address: bperson@example.com fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/bperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/1 + user: http://localhost:9001/3.0/users/1 entry 4: address: cperson@example.com fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/cperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/2 + user: http://localhost:9001/3.0/users/2 http_etag: "..." start: 0 total_size: 5 @@ -140,12 +174,16 @@ We can also get just the members of a single mailing list. address: aperson@example.com fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/aperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/4 + user: http://localhost:9001/3.0/users/3 entry 1: address: cperson@example.com fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/cperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/5 + user: http://localhost:9001/3.0/users/2 http_etag: ... start: 0 total_size: 2 @@ -167,37 +205,51 @@ test-one mailing list. address: dperson@example.com fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/alpha@example.com/moderator/dperson@example.com + role: moderator + self_link: http://localhost:9001/3.0/members/7 + user: http://localhost:9001/3.0/users/4 entry 1: address: aperson@example.com fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/aperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/4 + user: http://localhost:9001/3.0/users/3 entry 2: address: cperson@example.com fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/cperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/5 + user: http://localhost:9001/3.0/users/2 entry 3: address: cperson@example.com fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/test-one@example.com/owner/cperson@example.com + role: owner + self_link: http://localhost:9001/3.0/members/6 + user: http://localhost:9001/3.0/users/2 entry 4: address: aperson@example.com fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/aperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/3 + user: http://localhost:9001/3.0/users/3 entry 5: address: bperson@example.com fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/bperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/1 + user: http://localhost:9001/3.0/users/1 entry 6: address: cperson@example.com fqdn_listname: test-one@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/test-one@example.com/member/cperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/2 + user: http://localhost:9001/3.0/users/2 http_etag: "..." start: 0 total_size: 7 @@ -206,9 +258,9 @@ test-one mailing list. Joining a mailing list ====================== -A user can be subscribed to a mailing list via the REST API. Actually, -addresses not users are subscribed to mailing lists, but addresses are always -tied to users. A subscribed user is called a member. +A user can be subscribed to a mailing list via the REST API, either by a +specific address, or more generally by their preferred address. A subscribed +user is called a member. Elly subscribes to the alpha mailing list. By default, get gets a regular delivery. Since Elly's email address is not yet known to Mailman, a user is @@ -221,8 +273,9 @@ created for her. ... }) content-length: 0 date: ... - location: http://localhost:9001/3.0/lists/alpha@example.com/member/eperson@example.com - ... + location: http://localhost:9001/3.0/members/8 + server: ... + status: 201 Elly is now a member of the mailing list. :: @@ -241,7 +294,9 @@ Elly is now a member of the mailing list. address: eperson@example.com fqdn_listname: alpha@example.com http_etag: ... - self_link: http://localhost:9001/3.0/lists/alpha@example.com/member/eperson@example.com + role: member + self_link: http://localhost:9001/3.0/members/8 + user: http://localhost:9001/3.0/users/5 ... @@ -254,8 +309,7 @@ so she leaves from the mailing list. # Ensure our previous reads don't keep the database lock. >>> transaction.abort() - >>> dump_json('http://localhost:9001/3.0/lists/alpha@example.com' - ... '/member/eperson@example.com', + >>> dump_json('http://localhost:9001/3.0/members/8', ... method='DELETE') content-length: 0 ... @@ -281,9 +335,9 @@ Fred joins the alpha mailing list but wants MIME digest delivery. ... 'delivery_mode': 'mime_digests', ... }) content-length: 0 - ... - location: http://localhost:9001/3.0/lists/alpha@example.com/member/fperson@example.com - ... + date: ... + location: http://localhost:9001/3.0/members/9 + server: ... status: 201 >>> fred = user_manager.get_user('fperson@example.com') diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py index 0f74e20d7..1f6f1a913 100644 --- a/src/mailman/rest/members.py +++ b/src/mailman/rest/members.py @@ -29,12 +29,11 @@ __all__ = [ from operator import attrgetter from restish import http, resource -from urllib import quote from zope.component import getUtility from mailman.app.membership import delete_member from mailman.interfaces.address import InvalidEmailAddressError -from mailman.interfaces.listmanager import NoSuchListError +from mailman.interfaces.listmanager import IListManager, NoSuchListError from mailman.interfaces.member import ( AlreadySubscribedError, DeliveryMode, MemberRole, NotAMemberError) from mailman.interfaces.membership import ISubscriptionService @@ -52,8 +51,9 @@ class _MemberBase(resource.Resource, CollectionMixin): return dict( fqdn_listname=member.mailing_list, address=member.address.email, - self_link=path_to('lists/{0}/{1}/{2}'.format( - member.mailing_list, role, member.address.email)), + role=role, + user=path_to('users/{0}'.format(member.user.user_id)), + self_link=path_to('members/{0}'.format(member.member_id)), ) def _get_collection(self, request): @@ -64,12 +64,9 @@ class _MemberBase(resource.Resource, CollectionMixin): class AMember(_MemberBase): """A member.""" - def __init__(self, mailing_list, role, address): - self._mlist = mailing_list - self._role = role - self._address = address - roster = self._mlist.get_roster(role) - self._member = roster.get_member(self._address) + def __init__(self, member_id): + self._member_id = member_id + self._member = getUtility(ISubscriptionService).get_member(member_id) @resource.GET() def member(self, request): @@ -82,9 +79,12 @@ class AMember(_MemberBase): # Leaving a list is a bit different than deleting a moderator or # owner. Handle the former case first. For now too, we will not send # an admin or user notification. - if self._role is MemberRole.member: + if self._member is None: + return http.not_found() + mlist = getUtility(IListManager).get(self._member.mailing_list) + if self._member.role is MemberRole.member: try: - delete_member(self._mlist, self._address, False, False) + delete_member(mlist, self._member.address.email, False, False) except NotAMemberError: return http.not_found() else: @@ -92,6 +92,7 @@ class AMember(_MemberBase): return http.ok([], '') + class AllMembers(_MemberBase): """The members.""" @@ -114,12 +115,7 @@ class AllMembers(_MemberBase): return http.bad_request([], b'Invalid email address') except ValueError as error: return http.bad_request([], str(error)) - # wsgiref wants headers to be bytes, not unicodes. Also, we have to - # quote any unsafe characters in the address. Specifically, we need - # to quote forward slashes, but not @-signs. - quoted_address = quote(member.address.email, safe=b'@') - location = path_to('lists/{0}/member/{1}'.format( - member.mailing_list, quoted_address)) + location = path_to('members/{0}'.format(member.member_id)) # Include no extra headers or body. return http.created(location, [], None) diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py index 99f28cbb2..0c48ad6b9 100644 --- a/src/mailman/rest/root.py +++ b/src/mailman/rest/root.py @@ -34,7 +34,7 @@ from mailman.rest.addresses import AllAddresses, AnAddress from mailman.rest.domains import ADomain, AllDomains from mailman.rest.helpers import etag, path_to from mailman.rest.lists import AList, AllLists -from mailman.rest.members import AllMembers +from mailman.rest.members import AMember, AllMembers from mailman.rest.users import AUser, AllUsers @@ -121,7 +121,9 @@ class TopLevel(resource.Resource): """/<api>/members""" if len(segments) == 0: return AllMembers() - return http.bad_request() + else: + member_id = segments.pop(0) + return AMember(member_id), segments @resource.child() def users(self, request, segments): diff --git a/src/mailman/rest/tests/test_membership.py b/src/mailman/rest/tests/test_membership.py index 933d1d368..1a911c688 100644 --- a/src/mailman/rest/tests/test_membership.py +++ b/src/mailman/rest/tests/test_membership.py @@ -35,6 +35,7 @@ from mailman.config import config from mailman.interfaces.usermanager import IUserManager from mailman.testing.helpers import call_api from mailman.testing.layers import RESTLayer +from mailman.utilities.datetime import now @@ -77,9 +78,7 @@ class TestMembership(unittest.TestCase): # Try to leave a mailing list using an invalid membership address. try: # For Python 2.6. - call_api('http://localhost:9001/3.0/lists/test@example.com' - '/member/nobody', - method='DELETE') + call_api('http://localhost:9001/3.0/members/1', method='DELETE') except HTTPError as exc: self.assertEqual(exc.code, 404) self.assertEqual(exc.msg, '404 Not Found') @@ -90,8 +89,7 @@ class TestMembership(unittest.TestCase): anne = self._usermanager.create_address('anne@example.com') self._mlist.subscribe(anne) config.db.commit() - url = ('http://localhost:9001/3.0/lists/test@example.com' - '/member/anne@example.com') + url = 'http://localhost:9001/3.0/members/1' content, response = call_api(url, method='DELETE') # For a successful DELETE, the response code is 200 and there is no # content. @@ -146,14 +144,21 @@ class TestMembership(unittest.TestCase): self.assertEqual(content, None) self.assertEqual(response.status, 201) self.assertEqual(response['location'], - 'http://localhost:9001/3.0/lists/test@example.com' - '/member/hugh%2Fperson@example.com') + 'http://localhost:9001/3.0/members/1') # Reset any current transaction. config.db.abort() members = list(self._mlist.members.members) self.assertEqual(len(members), 1) self.assertEqual(members[0].address.email, 'hugh/person@example.com') + ## def test_join_as_user_with_preferred_address(self): + ## anne = self._usermanager.create_user('anne@example.com') + ## list(anne.addresses)[0].verified_on = now() + ## self._mlist.subscribe(anne) + ## config.db.commit() + ## content, response = call_api('http://localhost:9001/3.0/members') + ## raise AssertionError('incomplete test') + def test_suite(): diff --git a/src/mailman/utilities/uid.py b/src/mailman/utilities/uid.py index 3d58cace5..e44ed2983 100644 --- a/src/mailman/utilities/uid.py +++ b/src/mailman/utilities/uid.py @@ -38,6 +38,7 @@ import hashlib from flufl.lock import Lock from mailman.config import config +from mailman.model.uid import UID from mailman.testing import layers from mailman.utilities.passwords import SALT_LENGTH @@ -46,13 +47,15 @@ from mailman.utilities.passwords import SALT_LENGTH class UniqueIDFactory: """A factory for unique ids.""" - def __init__(self): + def __init__(self, context=None): # We can't call reset() when the factory is created below, because # config.VAR_DIR will not be set at that time. So initialize it at # the first use. self._uid_file = None self._lock_file = None self._lockobj = None + self._context = context + layers.MockAndMonkeyLayer.register_reset(self.reset) @property def _lock(self): @@ -60,6 +63,8 @@ class UniqueIDFactory: # These will get automatically cleaned up by the test # infrastructure. self._uid_file = os.path.join(config.VAR_DIR, '.uid') + if self._context: + self._uid_file += '.' + self._context self._lock_file = self._uid_file + '.lock' self._lockobj = Lock(self._lock_file) return self._lockobj @@ -76,11 +81,18 @@ class UniqueIDFactory: # tests) that it will not be a problem. Maybe. return self._next_uid() salt = os.urandom(SALT_LENGTH) - h = hashlib.sha1(repr(time.time())) - h.update(salt) - if bytes is not None: - h.update(bytes) - return unicode(h.hexdigest(), 'us-ascii') + while True: + h = hashlib.sha1(repr(time.time())) + h.update(salt) + if bytes is not None: + h.update(bytes) + uid = unicode(h.hexdigest(), 'us-ascii') + try: + UID.record(uid) + except ValueError: + pass + else: + return uid def _next_uid(self): with self._lock: @@ -106,4 +118,3 @@ class UniqueIDFactory: factory = UniqueIDFactory() -layers.MockAndMonkeyLayer.register_reset(factory.reset) |
