Barry Warsaw pushed to branch master at mailman / Mailman

Commits:
ad53d761 by Aurélien Bompard at 2016-01-13T14:20:16-05:00
Expose the ban list on the REST API

- - - - -
fe57b60b by Aurélien Bompard at 2016-01-13T14:20:16-05:00
Add the self_link for bans and factor some code

- - - - -
95446742 by Barry Warsaw at 2016-01-13T16:53:20-05:00
Clean up the branch for landing.

* Add NEWS
* Update copyright years.
* Adjust for new IAPI.path_to() interface.
* Style.
* Remove some unreachable code.
* Boost coverage.
* Change some Bad Requests into Not Founds.
* Do not include `list_id` in resource JSON for global bans.
* Rephrase some doctests.

- - - - -


8 changed files:

- src/mailman/docs/NEWS.rst
- src/mailman/interfaces/bans.py
- src/mailman/model/bans.py
- + src/mailman/rest/bans.py
- src/mailman/rest/docs/membership.rst
- src/mailman/rest/lists.py
- src/mailman/rest/root.py
- + src/mailman/rest/tests/test_bans.py


Changes:

=====================================
src/mailman/docs/NEWS.rst
=====================================
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -131,6 +131,8 @@ REST
    values accessible through the list's configuraiton resource.  POSTing to
    the resource with either ``send=True``, ``bump=True``, or both invokes the
    given action.
+ * Global and list-centric bans can now be managed through the REST API.
+   Given by Aurélien Bompard.
 
 Other
 -----


=====================================
src/mailman/interfaces/bans.py
=====================================
--- a/src/mailman/interfaces/bans.py
+++ b/src/mailman/interfaces/bans.py
@@ -97,3 +97,10 @@ class IBanManager(Interface):
             or not.
         :rtype: bool
         """
+
+    def __iter__():
+        """Iterate over all banned addresses.
+
+        :return: The list of all banned addresses.
+        :rtype: list of `str`
+        """


=====================================
src/mailman/model/bans.py
=====================================
--- a/src/mailman/model/bans.py
+++ b/src/mailman/model/bans.py
@@ -111,3 +111,8 @@ class BanManager:
                     re.match(ban.email, email, re.IGNORECASE) is not None):
                     return True
         return False
+
+    @dbconnection
+    def __iter__(self, store):
+        """See `IBanManager`."""
+        yield from store.query(Ban).filter_by(list_id=self._list_id)


=====================================
src/mailman/rest/bans.py
=====================================
--- /dev/null
+++ b/src/mailman/rest/bans.py
@@ -0,0 +1,115 @@
+# Copyright (C) 2016 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/>.
+
+"""REST for banned emails."""
+
+__all__ = [
+    'BannedEmail',
+    'BannedEmails',
+    ]
+
+
+from mailman.interfaces.bans import IBanManager
+from mailman.rest.helpers import (
+    CollectionMixin, bad_request, child, created, etag, no_content, not_found,
+    okay)
+from mailman.rest.validator import Validator
+
+
+class _BannedBase:
+    """Common base class."""
+
+    def __init__(self, mlist):
+        self._mlist = mlist
+        self.ban_manager = IBanManager(self._mlist)
+
+    def _location(self, email):
+        if self._mlist is None:
+            base_location = ''
+        else:
+            base_location = 'lists/{}/'.format(self._mlist.list_id)
+        return self.api.path_to('{}bans/{}'.format(base_location, email))
+
+
+class BannedEmail(_BannedBase):
+    """A banned email."""
+
+    def __init__(self, mlist, email):
+        super().__init__(mlist)
+        self._email = email
+
+    def on_get(self, request, response):
+        """Get a banned email."""
+        if self.ban_manager.is_banned(self._email):
+            resource = dict(
+                email=self._email,
+                self_link=self._location(self._email),
+                )
+            if self._mlist is not None:
+                resource['list_id'] = self._mlist.list_id
+            okay(response, etag(resource))
+        else:
+            not_found(response, 'Email is not banned: {}'.format(self._email))
+
+    def on_delete(self, request, response):
+        """Remove an email from the ban list."""
+        if self.ban_manager.is_banned(self._email):
+            self.ban_manager.unban(self._email)
+            no_content(response)
+        else:
+            not_found(response, 'Email is not banned: {}'.format(self._email))
+
+
+class BannedEmails(_BannedBase, CollectionMixin):
+    """The list of all banned emails."""
+
+    def _resource_as_dict(self, ban):
+        """See `CollectionMixin`."""
+        resource = dict(
+            email=ban.email,
+            self_link=self._location(ban.email),
+            )
+        if ban.list_id is not None:
+            resource['list_id'] = ban.list_id
+        return resource
+
+    def _get_collection(self, request):
+        """See `CollectionMixin`."""
+        return list(self.ban_manager)
+
+    def on_get(self, request, response):
+        """/bans"""
+        resource = self._make_collection(request)
+        okay(response, etag(resource))
+
+    def on_post(self, request, response):
+        """Ban some email from subscribing."""
+        validator = Validator(email=str)
+        try:
+            email = validator(request)['email']
+        except ValueError as error:
+            bad_request(response, str(error))
+            return
+        if self.ban_manager.is_banned(email):
+            bad_request(response, b'Address is already banned')
+        else:
+            self.ban_manager.ban(email)
+            created(response, self._location(email))
+
+    @child(r'^(?P<email>[^/]+)')
+    def email(self, request, segments, **kw):
+        return BannedEmail(self._mlist, kw['email'])


=====================================
src/mailman/rest/docs/membership.rst
=====================================
--- a/src/mailman/rest/docs/membership.rst
+++ b/src/mailman/rest/docs/membership.rst
@@ -965,3 +965,97 @@ The moderation action for a member can be changed by 
PATCH'ing the
     ...
     moderation_action: hold
     ...
+
+
+Handling the list of banned addresses
+=====================================
+
+To ban an address from subscribing you can POST to the ``/bans`` child
+of any list using the REST API.
+
+    >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com/bans',
+    ...           {'email': 'ban...@example.com'})
+    content-length: 0
+    ...
+    location: .../3.0/lists/ant.example.com/bans/ban...@example.com
+    ...
+    status: 201
+
+This address is now banned, and you can get the list of banned addresses by
+issuing a GET request on the ``/bans`` child.
+
+    >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com/bans')
+    entry 0:
+        email: ban...@example.com
+        http_etag: "..."
+        list_id: ant.example.com
+        self_link: .../3.0/lists/ant.example.com/bans/ban...@example.com
+    ...
+
+You can always GET a single banned address.
+
+    >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com'
+    ...           '/bans/ban...@example.com')
+    email: ban...@example.com
+    http_etag: "..."
+    list_id: ant.example.com
+    self_link: .../3.0/lists/ant.example.com/bans/ban...@example.com
+
+Unbanning addresses is also possible by issuing a DELETE request.
+
+    >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com'
+    ...           '/bans/ban...@example.com',
+    ...           method='DELETE')
+    content-length: 0
+    ...
+    status: 204
+
+After unbanning, the address is not shown in the ban list anymore.
+
+    >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com/bans')
+    http_etag: "..."
+    start: 0
+    total_size: 0
+
+Global bans prevent an address from subscribing to any mailing list, and they
+can be added via the top-level ``bans`` resource.
+
+    >>> dump_json('http://localhost:9001/3.0/bans',
+    ...           {'email': 'ban...@example.com'})
+    content-length: 0
+    ...
+    location: http://localhost:9001/3.0/bans/ban...@example.com
+    ...
+    status: 201
+
+Note that entries in the global bans do not have a ``list_id`` field.
+::
+
+    >>> dump_json('http://localhost:9001/3.0/bans')
+    entry 0:
+        email: ban...@example.com
+        http_etag: "..."
+        self_link: http://localhost:9001/3.0/bans/ban...@example.com
+    ...
+
+    >>> dump_json('http://localhost:9001/3.0/bans/ban...@example.com')
+    email: ban...@example.com
+    http_etag: "..."
+    self_link: http://localhost:9001/3.0/bans/ban...@example.com
+
+As with list-centric bans, you can delete a global ban.
+
+    >>> dump_json('http://localhost:9001/3.0/bans/ban...@example.com',
+    ...           method='DELETE')
+    content-length: 0
+    ...
+    status: 204
+
+    >>> dump_json('http://localhost:9001/3.0/bans/ban...@example.com')
+    Traceback (most recent call last):
+    ...
+    urllib.error.HTTPError: HTTP Error 404: ...
+    >>> dump_json('http://localhost:9001/3.0/bans')
+    http_etag: "..."
+    start: 0
+    total_size: 0


=====================================
src/mailman/rest/lists.py
=====================================
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -39,6 +39,7 @@ from mailman.interfaces.mailinglist import IListArchiverSet
 from mailman.interfaces.member import MemberRole
 from mailman.interfaces.styles import IStyleManager
 from mailman.interfaces.subscriptions import ISubscriptionService
+from mailman.rest.bans import BannedEmails
 from mailman.rest.listconf import ListConfiguration
 from mailman.rest.helpers import (
     CollectionMixin, GetterSetter, NotFound, accepted, bad_request, child,
@@ -198,6 +199,13 @@ class AList(_ListBase):
             return NotFound(), []
         return ListDigest(self._mlist)
 
+    @child()
+    def bans(self, request, segments):
+        """Return a collection of mailing list's banned addresses."""
+        if self._mlist is None:
+            return NotFound(), []
+        return BannedEmails(self._mlist)
+
 
 
 class AllLists(_ListBase):


=====================================
src/mailman/rest/root.py
=====================================
--- a/src/mailman/rest/root.py
+++ b/src/mailman/rest/root.py
@@ -32,6 +32,7 @@ from mailman.core.system import system
 from mailman.interfaces.listmanager import IListManager
 from mailman.model.uid import UID
 from mailman.rest.addresses import AllAddresses, AnAddress
+from mailman.rest.bans import BannedEmail, BannedEmails
 from mailman.rest.domains import ADomain, AllDomains
 from mailman.rest.helpers import (
     BadRequest, NotFound, child, etag, no_content, not_found, okay)
@@ -290,6 +291,17 @@ class TopLevel:
             return BadRequest(), []
 
     @child()
+    def bans(self, request, segments):
+        """/<api>/bans
+           /<api>/bans/<email>
+        """
+        if len(segments) == 0:
+            return BannedEmails(None)
+        else:
+            email = segments.pop(0)
+            return BannedEmail(None, email), segments
+
+    @child()
     def reserved(self, request, segments):
         """/<api>/reserved/[...]"""
         return Reserved(segments), []


=====================================
src/mailman/rest/tests/test_bans.py
=====================================
--- /dev/null
+++ b/src/mailman/rest/tests/test_bans.py
@@ -0,0 +1,88 @@
+# Copyright (C) 2016 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 address bans."""
+
+__all__ = [
+    'TestBans',
+    ]
+
+
+import unittest
+
+from mailman.app.lifecycle import create_list
+from mailman.database.transaction import transaction
+from mailman.interfaces.bans import IBanManager
+from mailman.testing.layers import RESTLayer
+from mailman.testing.helpers import call_api
+from urllib.error import HTTPError
+
+
+class TestBans(unittest.TestCase):
+    layer = RESTLayer
+
+    def setUp(self):
+        with transaction():
+            self._mlist = create_list('a...@example.com')
+
+    def test_get_missing_banned_address(self):
+        with self.assertRaises(HTTPError) as cm:
+            call_api('http://localhost:9001/3.0/lists/ant.example.com'
+                     '/bans/notban...@example.com')
+        self.assertEqual(cm.exception.code, 404)
+        self.assertEqual(cm.exception.reason,
+                         b'Email is not banned: notban...@example.com')
+
+    def test_delete_missing_banned_address(self):
+        with self.assertRaises(HTTPError) as cm:
+            call_api('http://localhost:9001/3.0/lists/ant.example.com'
+                     '/bans/notban...@example.com',
+                     method='DELETE')
+        self.assertEqual(cm.exception.code, 404)
+        self.assertEqual(cm.exception.reason,
+                         b'Email is not banned: notban...@example.com')
+
+    def test_not_found_after_unbanning(self):
+        manager = IBanManager(self._mlist)
+        with transaction():
+            manager.ban('ban...@example.com')
+        url = ('http://localhost:9001/3.0/lists/ant.example.com'
+               '/bans/ban...@example.com')
+        response, content = call_api(url)
+        self.assertEqual(response['email'], 'ban...@example.com')
+        response, content = call_api(url, method='DELETE')
+        self.assertEqual(content.status, 204)
+        with self.assertRaises(HTTPError) as cm:
+            call_api(url)
+        self.assertEqual(cm.exception.code, 404)
+        self.assertEqual(cm.exception.reason,
+                         b'Email is not banned: ban...@example.com')
+
+    def test_not_found_after_unbanning_global(self):
+        manager = IBanManager(None)
+        with transaction():
+            manager.ban('ban...@example.com')
+        url = ('http://localhost:9001/3.0/bans/ban...@example.com')
+        response, content = call_api(url)
+        self.assertEqual(response['email'], 'ban...@example.com')
+        response, content = call_api(url, method='DELETE')
+        self.assertEqual(content.status, 204)
+        with self.assertRaises(HTTPError) as cm:
+            call_api(url)
+        self.assertEqual(cm.exception.code, 404)
+        self.assertEqual(cm.exception.reason,
+                         b'Email is not banned: ban...@example.com')



View it on GitLab: 
https://gitlab.com/mailman/mailman/compare/187dad97bf278b0ca9d080774072e8fb235154cc...95446742669349777ee4101237a76395f1dfaa87
_______________________________________________
Mailman-checkins mailing list
Mailman-checkins@python.org
Unsubscribe: 
https://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org

Reply via email to