diff options
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) |
