summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2011-04-22 19:26:43 -0400
committerBarry Warsaw2011-04-22 19:26:43 -0400
commitb5b015ce524157cfef4b795e4b0c7ff17b4d0fe2 (patch)
tree260169450ab382cf59143f0fe8bae0fc0507452f /src
parent6959e6adcb582172aeec01ef6b6a75b9ba85017b (diff)
downloadmailman-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.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)