Aurélien Bompard has proposed merging lp:~abompard/mailman/bug-1312884 into 
lp:mailman.

Requested reviews:
  Mailman Coders (mailman-coders)
Related bugs:
  Bug #1312884 in GNU Mailman: "REST: creating a user from an existing address"
  https://bugs.launchpad.net/mailman/+bug/1312884

For more details, see:
https://code.launchpad.net/~abompard/mailman/bug-1312884/+merge/244007

Implement the feature described in bug #1312884.
-- 
Your team Mailman Coders is requested to review the proposed merge of 
lp:~abompard/mailman/bug-1312884 into lp:mailman.
=== modified file 'src/mailman/rest/addresses.py'
--- src/mailman/rest/addresses.py	2014-08-15 16:17:03 +0000
+++ src/mailman/rest/addresses.py	2014-12-08 15:45:39 +0000
@@ -62,6 +62,9 @@
             representation['display_name'] = address.display_name
         if address.verified_on:
             representation['verified_on'] = address.verified_on
+        if address.user:
+            representation['user'] = path_to(
+                'users/{0}'.format(address.user.user_id.int))
         return representation
 
     def _get_collection(self, request):
@@ -156,6 +159,13 @@
         child = _VerifyResource(self._address, 'unverify')
         return child, []
 
+    @child()
+    def user(self, request, segments):
+        """/addresses/<email>/user"""
+        if self._address is None:
+            return NotFound(), []
+        from mailman.rest.users import AddressUser # avoid circular imports
+        return AddressUser(self._address)
 
 
 class UserAddresses(_AddressBase):

=== modified file 'src/mailman/rest/docs/addresses.rst'
--- src/mailman/rest/docs/addresses.rst	2014-04-15 21:54:35 +0000
+++ src/mailman/rest/docs/addresses.rst	2014-12-08 15:45:39 +0000
@@ -161,6 +161,7 @@
         original_email: [email protected]
         registered_on: 2005-08-01T07:49:23
         self_link: http://localhost:9001/3.0/addresses/[email protected]
+        user: http://localhost:9001/3.0/users/1
     http_etag: "..."
     start: 0
     total_size: 1
@@ -172,6 +173,7 @@
     original_email: [email protected]
     registered_on: 2005-08-01T07:49:23
     self_link: http://localhost:9001/3.0/addresses/[email protected]
+    user: http://localhost:9001/3.0/users/1
 
 A user can be associated with multiple email addresses.  You can add new
 addresses to an existing user.
@@ -208,6 +210,7 @@
         original_email: [email protected]
         registered_on: 2005-08-01T07:49:23
         self_link: http://localhost:9001/3.0/addresses/[email protected]
+        user: http://localhost:9001/3.0/users/1
     entry 1:
         display_name: Dave Person
         email: [email protected]
@@ -215,6 +218,7 @@
         original_email: [email protected]
         registered_on: 2005-08-01T07:49:23
         self_link: http://localhost:9001/3.0/addresses/[email protected]
+        user: http://localhost:9001/3.0/users/1
     entry 2:
         display_name: Davie P
         email: [email protected]
@@ -222,6 +226,7 @@
         original_email: [email protected]
         registered_on: 2005-08-01T07:49:23
         self_link: http://localhost:9001/3.0/addresses/[email protected]
+        user: http://localhost:9001/3.0/users/1
     http_etag: "..."
     start: 0
     total_size: 3

=== modified file 'src/mailman/rest/docs/users.rst'
--- src/mailman/rest/docs/users.rst	2013-03-21 18:35:58 +0000
+++ src/mailman/rest/docs/users.rst	2014-12-08 15:45:39 +0000
@@ -329,18 +329,21 @@
         registered_on: 2005-08-01T07:49:23
         self_link:
             http://localhost:9001/3.0/addresses/[email protected]
+        user: http://localhost:9001/3.0/users/6
     entry 1:
         email: [email protected]
         http_etag: "..."
         original_email: [email protected]
         registered_on: 2005-08-01T07:49:23
         self_link: http://localhost:9001/3.0/addresses/[email protected]
+        user: http://localhost:9001/3.0/users/6
     entry 2:
         email: [email protected]
         http_etag: "..."
         original_email: [email protected]
         registered_on: 2005-08-01T07:49:23
         self_link: http://localhost:9001/3.0/addresses/[email protected]
+        user: http://localhost:9001/3.0/users/6
     entry 3:
         display_name: Fred Person
         email: [email protected]
@@ -348,6 +351,7 @@
         original_email: [email protected]
         registered_on: 2005-08-01T07:49:23
         self_link: http://localhost:9001/3.0/addresses/[email protected]
+        user: http://localhost:9001/3.0/users/6
     http_etag: "..."
     start: 0
     total_size: 4

=== modified file 'src/mailman/rest/tests/test_addresses.py'
--- src/mailman/rest/tests/test_addresses.py	2014-11-15 17:01:30 +0000
+++ src/mailman/rest/tests/test_addresses.py	2014-12-08 15:45:39 +0000
@@ -206,3 +206,139 @@
                     'email': '[email protected]',
                     })
         self.assertEqual(cm.exception.code, 404)
+
+    def test_address_with_user(self):
+        with transaction():
+            anne = getUtility(IUserManager).create_user('[email protected]')
+        json, headers = call_api(
+                'http://localhost:9001/3.0/addresses/[email protected]')
+        self.assertEqual(headers['status'], '200')
+        self.assertIn("user", json)
+        self.assertEqual(json["user"], "http://localhost:9001/3.0/users/1";)
+
+    def test_address_without_user(self):
+        with transaction():
+            anne = getUtility(IUserManager).create_address('[email protected]')
+        json, headers = call_api(
+                'http://localhost:9001/3.0/addresses/[email protected]')
+        self.assertEqual(headers['status'], '200')
+        self.assertNotIn("user", json)
+
+    def test_user_subresource_on_unlinked_address(self):
+        with transaction():
+            anne = getUtility(IUserManager).create_address('[email protected]')
+        with self.assertRaises(HTTPError) as cm:
+            call_api(
+                'http://localhost:9001/3.0/addresses/[email protected]/user')
+        self.assertEqual(cm.exception.code, 404)
+
+    def test_user_subresource(self):
+        user_manager = getUtility(IUserManager)
+        with transaction():
+            anne = user_manager.create_user('[email protected]', 'Anne')
+        json, headers = call_api(
+                'http://localhost:9001/3.0/addresses/[email protected]/user')
+        self.assertEqual(headers['status'], '200')
+        self.assertEqual(json["user_id"], 1)
+        self.assertEqual(json["display_name"], "Anne")
+        self.assertEqual(json["self_link"], "http://localhost:9001/3.0/users/1";)
+
+    def test_user_subresource_post(self):
+        user_manager = getUtility(IUserManager)
+        with transaction():
+            anne = user_manager.create_user('[email protected]', 'Anne')
+            anne_addr = user_manager.create_address('[email protected]')
+        response, headers = call_api(
+            'http://localhost:9001/3.0/addresses/[email protected]/user', {
+                'user_id': anne.user_id.int,
+                })
+        self.assertEqual(headers['status'], '200')
+        self.assertEqual(anne_addr.user, anne)
+        self.assertEqual(sorted([a.email for a in anne.addresses]),
+                         ['[email protected]', '[email protected]'])
+
+    def test_user_subresource_post_new_user(self):
+        user_manager = getUtility(IUserManager)
+        with transaction():
+            anne_addr = user_manager.create_address('[email protected]')
+        response, headers = call_api(
+            'http://localhost:9001/3.0/addresses/[email protected]/user', {
+                'display_name': 'Anne',
+                })
+        self.assertEqual(headers['status'], '201')
+        anne = user_manager.get_user('[email protected]')
+        self.assertTrue(anne is not None)
+        self.assertEqual(anne.display_name, 'Anne')
+        self.assertEqual([a.email for a in anne.addresses],
+                         ['[email protected]'])
+        self.assertEqual(anne_addr.user, anne)
+        self.assertEqual(headers['location'],
+                         'http://localhost:9001/3.0/users/1')
+
+    def test_user_subresource_post_conflict(self):
+        with transaction():
+            anne = getUtility(IUserManager).create_user('[email protected]')
+        with self.assertRaises(HTTPError) as cm:
+            call_api(
+                'http://localhost:9001/3.0/addresses/[email protected]/user', {
+                    'email': '[email protected]',
+                })
+        self.assertEqual(cm.exception.code, 409)
+
+    def test_user_subresource_post_new_user_no_autocreate(self):
+        with transaction():
+            anne = getUtility(IUserManager).create_address('[email protected]')
+        with self.assertRaises(HTTPError) as cm:
+            json, headers = call_api(
+                'http://localhost:9001/3.0/addresses/[email protected]/user', {
+                    'display_name': 'Anne',
+                    'autocreate': 0,
+                })
+            print("test_user_subresource_post_new_user_no_autocreate", headers, json)
+        self.assertEqual(cm.exception.code, 404)
+
+    def test_user_subresource_unlink(self):
+        user_manager = getUtility(IUserManager)
+        with transaction():
+            anne = user_manager.create_user('[email protected]')
+        response, headers = call_api(
+                'http://localhost:9001/3.0/addresses/[email protected]/user',
+                method="DELETE")
+        self.assertEqual(headers["status"], '204')
+        anne_addr = user_manager.get_address('[email protected]')
+        self.assertTrue(anne_addr.user is None, "The address is still linked")
+        self.assertTrue(user_manager.get_user('[email protected]') is None)
+
+    def test_user_subresource_put(self):
+        user_manager = getUtility(IUserManager)
+        with transaction():
+            anne_1 = user_manager.create_user('[email protected]', 'Anne 1')
+            anne_2 = user_manager.create_user(display_name='Anne 2')
+        response, headers = call_api(
+            'http://localhost:9001/3.0/addresses/[email protected]/user', {
+                'user_id': anne_2.user_id.int,
+                }, method="PUT")
+        self.assertEqual(headers['status'], '200')
+        self.assertEqual(anne_1.addresses, [])
+        self.assertEqual([a.email for a in anne_2.addresses],
+                         ['[email protected]'])
+        self.assertEqual(anne_2,
+            user_manager.get_address('[email protected]').user)
+
+    def test_user_subresource_put_create(self):
+        user_manager = getUtility(IUserManager)
+        with transaction():
+            anne = user_manager.create_user('[email protected]', 'Anne')
+        response, headers = call_api(
+            'http://localhost:9001/3.0/addresses/[email protected]/user', {
+                'email': '[email protected]',
+                }, method="PUT")
+        self.assertEqual(headers['status'], '201')
+        self.assertEqual(anne.addresses, [])
+        anne_person = user_manager.get_user('[email protected]')
+        self.assertTrue(anne_person is not None)
+        self.assertEqual(sorted([a.email for a in anne_person.addresses]),
+                         ["[email protected]", "[email protected]"])
+        anne_addr = user_manager.get_address('[email protected]')
+        self.assertTrue(anne_addr is not None)
+        self.assertEqual(anne_addr.user, anne_person)

=== modified file 'src/mailman/rest/users.py'
--- src/mailman/rest/users.py	2014-11-16 21:28:05 +0000
+++ src/mailman/rest/users.py	2014-12-08 15:45:39 +0000
@@ -39,7 +39,8 @@
 from mailman.rest.addresses import UserAddresses
 from mailman.rest.helpers import (
     BadRequest, CollectionMixin, GetterSetter, NotFound, bad_request, child,
-    created, etag, forbidden, no_content, not_found, okay, paginate, path_to)
+    created, etag, forbidden, no_content, not_found, okay, paginate, path_to,
+    conflict)
 from mailman.rest.preferences import Preferences
 from mailman.rest.validator import PatchValidator, Validator
 
@@ -62,6 +63,34 @@
     cleartext_password=PasswordEncrypterGetterSetter(),
     )
 
+CREATION_FIELDS = dict(
+    email=unicode,
+    display_name=unicode,
+    password=unicode,
+    _optional=('display_name', 'password'),
+    )
+
+
+def create_user(arguments, response):
+    """Create a new user."""
+    # We can't pass the 'password' argument to the user creation method,
+    # so strip that out (if it exists), then create the user, adding the
+    # password after the fact if successful.
+    password = arguments.pop('password', None)
+    try:
+        user = getUtility(IUserManager).create_user(**arguments)
+    except ExistingAddressError as error:
+        bad_request(
+            response, b'Address already exists: {0}'.format(error.address))
+        return
+    if password is None:
+        # This will have to be reset since it cannot be retrieved.
+        password = generate(int(config.passwords.password_length))
+    user.password = config.password_context.encrypt(password)
+    location = path_to('users/{0}'.format(user.user_id.int))
+    created(response, location)
+    return user
+
 
 
 class _UserBase(CollectionMixin):
@@ -105,30 +134,12 @@
     def on_post(self, request, response):
         """Create a new user."""
         try:
-            validator = Validator(email=unicode,
-                                  display_name=unicode,
-                                  password=unicode,
-                                  _optional=('display_name', 'password'))
+            validator = Validator(**CREATION_FIELDS)
             arguments = validator(request)
         except ValueError as error:
             bad_request(response, str(error))
             return
-        # We can't pass the 'password' argument to the user creation method,
-        # so strip that out (if it exists), then create the user, adding the
-        # password after the fact if successful.
-        password = arguments.pop('password', None)
-        try:
-            user = getUtility(IUserManager).create_user(**arguments)
-        except ExistingAddressError as error:
-            bad_request(
-                response, b'Address already exists: {0}'.format(error.address))
-            return
-        if password is None:
-            # This will have to be reset since it cannot be retrieved.
-            password = generate(int(config.passwords.password_length))
-        user.password = config.password_context.encrypt(password)
-        location = path_to('users/{0}'.format(user.user_id.int))
-        created(response, location)
+        create_user(arguments, response)
 
 
 
@@ -242,6 +253,93 @@
 
 
 
+class AddressUser(_UserBase):
+    """The user linked to an address."""
+
+    def __init__(self, address):
+        self._address = address
+        self._user = address.user
+
+    def on_get(self, request, response):
+        """Return a single user end-point."""
+        if self._user is None:
+            not_found(response)
+        else:
+            okay(response, self._resource_as_json(self._user))
+
+    def on_delete(self, request, response):
+        """Delete the named user, all her memberships, and addresses."""
+        if self._user is None:
+            not_found(response)
+            return
+        self._user.unlink(self._address)
+        no_content(response)
+
+    def on_post(self, request, response):
+        """Link a user to the address, and create it if needed."""
+        if self._user:
+            conflict(response)
+            return
+        # Check for an existing user
+        fields = CREATION_FIELDS.copy()
+        del fields["email"]
+        fields["user_id"] = int
+        fields["autocreate"] = int
+        fields["_optional"] = fields["_optional"] + ('user_id', 'autocreate')
+        try:
+            validator = Validator(**fields)
+            arguments = validator(request)
+        except ValueError as error:
+            bad_request(response, str(error))
+            return
+        user_manager = getUtility(IUserManager)
+        if "user_id" in arguments:
+            user_id = UUID(int=arguments["user_id"])
+            user = user_manager.get_user_by_id(user_id)
+            if user is None:
+                not_found(response, b"No user with ID {0}".format(
+                                    arguments["user_id"]))
+                return
+            okay(response)
+        else:
+            if arguments.get("autocreate", True): # autocreate by default
+                arguments.pop("autocreate", None)
+                user = create_user(arguments, response) # will set "201 Created"
+            else:
+                not_found(response, b"No such user, and automatic "
+                          "creation is disabled.")
+                return
+        user.link(self._address)
+
+    def on_put(self, request, response):
+        """Set or replace the addresse's user."""
+        if self._user:
+            self._user.unlink(self._address)
+        # Process post data and check for an existing user
+        fields = CREATION_FIELDS.copy()
+        fields["user_id"] = int
+        fields["_optional"] = fields["_optional"] + ('user_id', 'email')
+        try:
+            validator = Validator(**fields)
+            arguments = validator(request)
+        except ValueError as error:
+            bad_request(response, str(error))
+            return
+        user_manager = getUtility(IUserManager)
+        if 'user_id' in arguments:
+            user_id = UUID(int=arguments["user_id"])
+            user = user_manager.get_user_by_id(user_id)
+            if user is None:
+                not_found(response, b"No user with ID {0}".format(
+                                     arguments["user_id"]))
+                return
+            okay(response)
+        else:
+            user = create_user(arguments, response) # will set "201 Created"
+        user.link(self._address)
+
+
+
 class Login:
     """<api>/users/<uid>/login"""
 

_______________________________________________
Mailman-coders mailing list
[email protected]
https://mail.python.org/mailman/listinfo/mailman-coders

Reply via email to