summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2011-04-22 19:26:43 -0400
committerBarry Warsaw2011-04-22 19:26:43 -0400
commitb5b015ce524157cfef4b795e4b0c7ff17b4d0fe2 (patch)
tree260169450ab382cf59143f0fe8bae0fc0507452f
parent6959e6adcb582172aeec01ef6b6a75b9ba85017b (diff)
downloadmailman-b5b015ce524157cfef4b795e4b0c7ff17b4d0fe2.tar.gz
mailman-b5b015ce524157cfef4b795e4b0c7ff17b4d0fe2.tar.zst
mailman-b5b015ce524157cfef4b795e4b0c7ff17b4d0fe2.zip
-rw-r--r--src/mailman/database/mailman.sql17
-rw-r--r--src/mailman/interfaces/member.py3
-rw-r--r--src/mailman/interfaces/membership.py9
-rw-r--r--src/mailman/interfaces/user.py2
-rw-r--r--src/mailman/model/member.py21
-rw-r--r--src/mailman/model/tests/test_uid.py48
-rw-r--r--src/mailman/model/uid.py72
-rw-r--r--src/mailman/model/user.py5
-rw-r--r--src/mailman/rest/adapters.py13
-rw-r--r--src/mailman/rest/docs/membership.txt116
-rw-r--r--src/mailman/rest/members.py32
-rw-r--r--src/mailman/rest/root.py6
-rw-r--r--src/mailman/rest/tests/test_membership.py19
-rw-r--r--src/mailman/utilities/uid.py25
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)