summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2011-09-23 15:31:49 -0400
committerBarry Warsaw2011-09-23 15:31:49 -0400
commit416db276612ac6338524ce350e3b87216ffaffb7 (patch)
treeb29605f97447d5a4daf32f392b9c8f9d41981bd2 /src
parente4b34fefabd2ae3f14db83e96076fe741aa7c5b8 (diff)
downloadmailman-416db276612ac6338524ce350e3b87216ffaffb7.tar.gz
mailman-416db276612ac6338524ce350e3b87216ffaffb7.tar.zst
mailman-416db276612ac6338524ce350e3b87216ffaffb7.zip
Diffstat (limited to 'src')
-rw-r--r--src/mailman/docs/NEWS.rst4
-rw-r--r--src/mailman/interfaces/mailinglist.py4
-rw-r--r--src/mailman/interfaces/member.py8
-rw-r--r--src/mailman/rest/addresses.py13
-rw-r--r--src/mailman/rest/docs/preferences.rst233
-rw-r--r--src/mailman/rest/members.py25
-rw-r--r--src/mailman/rest/preferences.py119
-rw-r--r--src/mailman/rest/root.py19
-rw-r--r--src/mailman/rest/tests/test_membership.py11
-rw-r--r--src/mailman/rest/tests/test_root.py93
-rw-r--r--src/mailman/rest/users.py13
-rw-r--r--src/mailman/rest/validator.py11
12 files changed, 541 insertions, 12 deletions
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index 3c175d0ed..a81bff574 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -38,6 +38,10 @@ Architecture
REST
----
+ * Preferences for addresses, users, and members can be accessed, changed, and
+ deleted through the REST interface. Hierarchical, combined preferences for
+ members, and system preferences can be read through the REST interface.
+ (LP: #821438)
* The IMailingList attribute ``host_name`` has been renamed to ``mail_host``
for consistency. This changes the REST API for mailing list
resources. (LP: #787599)
diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py
index 9ae779409..c08e257c1 100644
--- a/src/mailman/interfaces/mailinglist.py
+++ b/src/mailman/interfaces/mailinglist.py
@@ -32,6 +32,8 @@ __all__ = [
from flufl.enum import Enum
from zope.interface import Interface, Attribute
+from mailman.interfaces.member import MemberRole
+
class Personalization(Enum):
@@ -232,7 +234,7 @@ class IMailingList(Interface):
:rtype: Roster
"""
- def subscribe(subscriber, role):
+ def subscribe(subscriber, role=MemberRole.member):
"""Subscribe the given address or user to the mailing list.
:param subscriber: The address or user to subscribe to the mailing
diff --git a/src/mailman/interfaces/member.py b/src/mailman/interfaces/member.py
index 0a8a5cfd5..5073b3059 100644
--- a/src/mailman/interfaces/member.py
+++ b/src/mailman/interfaces/member.py
@@ -184,7 +184,7 @@ class IMember(Interface):
receive_list_copy = Attribute(
"""Should an explicit recipient receive a list copy?
- Unlike going through the preferences, this attribute return the
+ Unlike going through `preferences`, this attribute returns the
preference value based on the following lookup order:
1. The member
@@ -196,7 +196,7 @@ class IMember(Interface):
receive_own_postings = Attribute(
"""Should the poster get a list copy of their own messages?
- Unlike going through the preferences, this attribute return the
+ Unlike going through `preferences`, this attribute returns the
preference value based on the following lookup order:
1. The member
@@ -208,7 +208,7 @@ class IMember(Interface):
delivery_mode = Attribute(
"""The preferred delivery mode.
- Unlike going through the preferences, this attribute return the
+ Unlike going through `preferences`, this attribute returns the
preference value based on the following lookup order:
1. The member
@@ -220,7 +220,7 @@ class IMember(Interface):
delivery_status = Attribute(
"""The delivery status.
- Unlike going through the preferences, this attribute return the
+ Unlike going through `preferences`, this attribute returns the
preference value based on the following lookup order:
1. The member
diff --git a/src/mailman/rest/addresses.py b/src/mailman/rest/addresses.py
index a97043e9c..5d479d9cb 100644
--- a/src/mailman/rest/addresses.py
+++ b/src/mailman/rest/addresses.py
@@ -33,6 +33,7 @@ from zope.component import getUtility
from mailman.rest.helpers import CollectionMixin, etag, path_to
from mailman.rest.members import MemberCollection
+from mailman.rest.preferences import Preferences
from mailman.interfaces.usermanager import IUserManager
@@ -102,6 +103,18 @@ class AnAddress(_AddressBase):
return http.not_found()
return AddressMemberships(self._address)
+ @resource.child()
+ def preferences(self, request, segments):
+ """/addresses/<email>/preferences"""
+ if len(segments) != 0:
+ return http.bad_request()
+ if self._address is None:
+ return http.not_found()
+ child = Preferences(
+ self._address.preferences,
+ 'addresses/{0}'.format(self._address.email))
+ return child, []
+
class UserAddresses(_AddressBase):
diff --git a/src/mailman/rest/docs/preferences.rst b/src/mailman/rest/docs/preferences.rst
new file mode 100644
index 000000000..e82fd6001
--- /dev/null
+++ b/src/mailman/rest/docs/preferences.rst
@@ -0,0 +1,233 @@
+===========
+Preferences
+===========
+
+Addresses have preferences.
+::
+
+ >>> from mailman.interfaces.usermanager import IUserManager
+ >>> from zope.component import getUtility
+ >>> user_manager = getUtility(IUserManager)
+
+ >>> anne = user_manager.create_address('anne@example.com')
+ >>> transaction.commit()
+
+Although to start with, an address has no preferences.
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/anne@example.com'
+ ... '/preferences')
+ http_etag: "..."
+ self_link: http://localhost:9001/3.0/addresses/anne@example.com/preferences
+
+Once the address is given some preferences, they are available through the
+REST API.
+
+ >>> anne.preferences.acknowledge_posts = True
+ >>> transaction.commit()
+ >>> dump_json('http://localhost:9001/3.0/addresses/anne@example.com'
+ ... '/preferences')
+ acknowledge_posts: True
+ http_etag: "..."
+ self_link: http://localhost:9001/3.0/addresses/anne@example.com/preferences
+
+Similarly, users have their own set of preferences, which also start out empty.
+::
+
+ >>> bart = user_manager.create_user('bart@example.com')
+ >>> transaction.commit()
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/bart@example.com'
+ ... '/preferences')
+ http_etag: "..."
+ self_link: http://localhost:9001/3.0/addresses/bart@example.com/preferences
+
+Setting a preference on the user's address does not set them on the user.
+::
+
+ >>> list(bart.addresses)[0].preferences.acknowledge_posts = True
+ >>> transaction.commit()
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/bart@example.com'
+ ... '/preferences')
+ acknowledge_posts: True
+ http_etag: "..."
+ self_link: http://localhost:9001/3.0/addresses/bart@example.com/preferences
+
+ >>> dump_json('http://localhost:9001/3.0/users/1/preferences')
+ http_etag: "..."
+ self_link: http://localhost:9001/3.0/users/1/preferences
+
+Users have their own set of preferences.
+
+ >>> bart.preferences.receive_own_postings = False
+ >>> transaction.commit()
+ >>> dump_json('http://localhost:9001/3.0/users/1/preferences')
+ http_etag: "..."
+ receive_own_postings: False
+ self_link: http://localhost:9001/3.0/users/1/preferences
+
+Similarly, members have their own separate set of preferences, and just like
+the above, setting a preference on the member's address or user does not set
+the preference on the member.
+::
+
+ >>> from mailman.interfaces.member import MemberRole
+ >>> mlist = create_list('test@example.com')
+ >>> bart_member = mlist.subscribe(list(bart.addresses)[0])
+ >>> bart_member.preferences.receive_list_copy = False
+ >>> transaction.commit()
+ >>> dump_json('http://localhost:9001/3.0/members/1/preferences')
+ http_etag: "..."
+ receive_list_copy: False
+ self_link: http://localhost:9001/3.0/members/1/preferences
+
+
+Changing preferences
+====================
+
+Preferences for the address, user, or member can be changed through the API.
+You can change all the preferences for a particular object by using an HTTP
+PUT operation.
+::
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/bart@example.com'
+ ... '/preferences', {
+ ... 'acknowledge_posts': True,
+ ... 'delivery_mode': 'plaintext_digests',
+ ... 'delivery_status': 'by_user',
+ ... 'preferred_language': 'ja',
+ ... 'receive_list_copy': True,
+ ... 'receive_own_postings': False,
+ ... }, method='PUT')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/bart@example.com'
+ ... '/preferences')
+ acknowledge_posts: True
+ delivery_mode: plaintext_digests
+ delivery_status: by_user
+ http_etag: "..."
+ preferred_language: ja
+ receive_list_copy: True
+ receive_own_postings: False
+ self_link: http://localhost:9001/3.0/addresses/bart@example.com/preferences
+
+You can also update just a few of the attributes using PATCH.
+::
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/bart@example.com'
+ ... '/preferences', {
+ ... 'delivery_mode': 'plaintext_digests',
+ ... 'receive_list_copy': False,
+ ... }, method='PATCH')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/bart@example.com'
+ ... '/preferences')
+ acknowledge_posts: True
+ delivery_mode: plaintext_digests
+ delivery_status: by_user
+ http_etag: "..."
+ preferred_language: ja
+ receive_list_copy: False
+ receive_own_postings: False
+ self_link: http://localhost:9001/3.0/addresses/bart@example.com/preferences
+
+
+Deleting preferences
+====================
+
+Preferences for any of the levels, member, user, or address, can be entirely
+deleted.
+::
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/anne@example.com'
+ ... '/preferences', {
+ ... 'preferred_language': 'ja',
+ ... }, method='PATCH')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/anne@example.com'
+ ... '/preferences')
+ acknowledge_posts: True
+ http_etag: "5219245d1eea98bc107032013af20ef91bfb5c51"
+ preferred_language: ja
+ self_link: http://localhost:9001/3.0/addresses/anne@example.com/preferences
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/anne@example.com'
+ ... '/preferences', method='DELETE')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/anne@example.com'
+ ... '/preferences')
+ http_etag: "..."
+ self_link: http://localhost:9001/3.0/addresses/anne@example.com/preferences
+
+
+Combined member preferences
+===========================
+
+The member resource provides a way to access the set of preference in effect
+for a specific subscription. This stacks the preferences, so that a value is
+always available. The preference value is looked up first on the member,
+falling back to the address, then user, then system preference.
+
+Preferences accessed through this interface are always read only.
+
+ >>> dump_json('http://localhost:9001/3.0/members/1/all/preferences')
+ acknowledge_posts: True
+ delivery_mode: plaintext_digests
+ delivery_status: by_user
+ http_etag: "..."
+ preferred_language: ja
+ receive_list_copy: False
+ receive_own_postings: False
+ self_link: http://localhost:9001/3.0/members/1/all/preferences
+
+These preferences cannot be changed.
+
+ >>> dump_json('http://localhost:9001/3.0/members/1/all/preferences', {
+ ... 'delivery_status': 'enabled',
+ ... }, method='PATCH')
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 405: 405 Method Not Allowed
+
+
+System preferences
+==================
+
+The Mailman system itself has a default set of preference. All preference
+lookups fall back to these values, which are read-only.
+
+ >>> dump_json('http://localhost:9001/3.0/system/preferences')
+ acknowledge_posts: False
+ delivery_mode: regular
+ delivery_status: enabled
+ hide_address: True
+ http_etag: "..."
+ preferred_language: en
+ receive_list_copy: True
+ receive_own_postings: True
+ self_link: http://localhost:9001/3.0/system/preferences
+
+These preferences cannot be changed.
+
+ >>> dump_json('http://localhost:9001/3.0/system/preferences', {
+ ... 'delivery_status': 'enabled',
+ ... }, method='PATCH')
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 405: 405 Method Not Allowed
diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py
index f15a2204a..bf739ed32 100644
--- a/src/mailman/rest/members.py
+++ b/src/mailman/rest/members.py
@@ -44,6 +44,7 @@ from mailman.interfaces.user import UnverifiedAddressError
from mailman.interfaces.usermanager import IUserManager
from mailman.rest.helpers import (
CollectionMixin, PATCH, etag, no_content, path_to)
+from mailman.rest.preferences import Preferences, ReadOnlyPreferences
from mailman.rest.validator import (
Validator, enum_validator, subscriber_validator)
@@ -115,6 +116,30 @@ class AMember(_MemberBase):
return http.not_found()
return http.ok([], self._resource_as_json(self._member))
+ @resource.child()
+ def preferences(self, request, segments):
+ """/members/<id>/preferences"""
+ if len(segments) != 0:
+ return http.bad_request()
+ if self._member is None:
+ return http.not_found()
+ child = Preferences(
+ self._member.preferences,
+ 'members/{0}'.format(self._member.member_id.int))
+ return child, []
+
+ @resource.child()
+ def all(self, request, segments):
+ """/members/<id>/all/preferences"""
+ if len(segments) == 0:
+ return http.not_found()
+ if self._member is None:
+ return http.not_found()
+ child = ReadOnlyPreferences(
+ self._member,
+ 'members/{0}/all'.format(self._member.member_id.int))
+ return child, []
+
@resource.DELETE()
def delete(self, request):
"""Delete the member (i.e. unsubscribe)."""
diff --git a/src/mailman/rest/preferences.py b/src/mailman/rest/preferences.py
new file mode 100644
index 000000000..d231d1055
--- /dev/null
+++ b/src/mailman/rest/preferences.py
@@ -0,0 +1,119 @@
+# 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/>.
+
+"""Preferences."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'ReadOnlyPreferences',
+ 'Preferences',
+ ]
+
+
+from lazr.config import as_boolean
+from restish import http, resource
+
+from mailman.interfaces.member import DeliveryMode, DeliveryStatus
+from mailman.rest.helpers import PATCH, etag, no_content, path_to
+from mailman.rest.validator import (
+ Validator, enum_validator, language_validator)
+
+
+PREFERENCES = (
+ 'acknowledge_posts',
+ 'delivery_mode',
+ 'delivery_status',
+ 'hide_address',
+ 'preferred_language',
+ 'receive_list_copy',
+ 'receive_own_postings',
+ )
+
+
+
+class ReadOnlyPreferences(resource.Resource):
+ """.../<object>/preferences"""
+
+ def __init__(self, parent, base_url):
+ self._parent = parent
+ self._base_url = base_url
+
+ @resource.GET()
+ def preferences(self, segments):
+ resource = dict()
+ for attr in PREFERENCES:
+ # Handle this one specially.
+ if attr == 'preferred_language':
+ continue
+ value = getattr(self._parent, attr, None)
+ if value is not None:
+ resource[attr] = value
+ # Add the preferred language, if it's not missing.
+ preferred_language = self._parent.preferred_language
+ if preferred_language is not None:
+ resource['preferred_language'] = preferred_language.code
+ # Add the self link.
+ resource['self_link'] = path_to(
+ '{0}/preferences'.format(self._base_url))
+ return http.ok([], etag(resource))
+
+
+
+class Preferences(ReadOnlyPreferences):
+ """Preferences which can be changed."""
+
+ def patch_put(self, request, is_optional):
+ if self._parent is None:
+ return http.not_found()
+ kws = dict(
+ acknowledge_posts=as_boolean,
+ delivery_mode=enum_validator(DeliveryMode),
+ delivery_status=enum_validator(DeliveryStatus),
+ preferred_language=language_validator,
+ receive_list_copy=as_boolean,
+ receive_own_postings=as_boolean,
+ )
+ if is_optional:
+ # For a PUT, all attributes are optional.
+ kws['_optional'] = kws.keys()
+ try:
+ values = Validator(**kws)(request)
+ except ValueError as error:
+ return http.bad_request([], str(error))
+ for key, value in values.items():
+ setattr(self._parent, key, value)
+ return no_content()
+
+ @PATCH()
+ def patch_preferences(self, request):
+ """Patch the preferences."""
+ return self.patch_put(request, is_optional=True)
+
+ @resource.PUT()
+ def put_preferences(self, request):
+ """Change all preferences."""
+ return self.patch_put(request, is_optional=False)
+
+ @resource.DELETE()
+ def delete_preferences(self, request):
+ """Delete all preferences."""
+ for attr in PREFERENCES:
+ if hasattr(self._parent, attr):
+ setattr(self._parent, attr, None)
+ return no_content()
diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py
index dc9717de4..9ba6ad20d 100644
--- a/src/mailman/rest/root.py
+++ b/src/mailman/rest/root.py
@@ -29,12 +29,14 @@ from base64 import b64decode
from restish import guard, http, resource
from mailman.config import config
+from mailman.core.constants import system_preferences
from mailman.core.system import system
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 AMember, AllMembers, FindMembers
+from mailman.rest.preferences import ReadOnlyPreferences
from mailman.rest.users import AUser, AllUsers
@@ -73,11 +75,18 @@ class TopLevel(resource.Resource):
@resource.child()
def system(self, request, segments):
"""/<api>/system"""
- resource = dict(
- mailman_version=system.mailman_version,
- python_version=system.python_version,
- self_link=path_to('system'),
- )
+ if len(segments) == 0:
+ resource = dict(
+ mailman_version=system.mailman_version,
+ python_version=system.python_version,
+ self_link=path_to('system'),
+ )
+ elif len(segments) > 1:
+ return http.bad_request()
+ elif segments[0] == 'preferences':
+ return ReadOnlyPreferences(system_preferences, 'system'), []
+ else:
+ return http.bad_request()
return http.ok([], etag(resource))
@resource.child()
diff --git a/src/mailman/rest/tests/test_membership.py b/src/mailman/rest/tests/test_membership.py
index 2d2351fca..ea3357506 100644
--- a/src/mailman/rest/tests/test_membership.py
+++ b/src/mailman/rest/tests/test_membership.py
@@ -229,6 +229,17 @@ class TestMembership(unittest.TestCase):
else:
raise AssertionError('Expected HTTPError')
+ def test_member_all_without_preferences(self):
+ # /members/<id>/all should return a 404 when it isn't trailed by
+ # `preferences`
+ try:
+ # For Python 2.6
+ call_api('http://localhost:9001/3.0/members/1/all')
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 404)
+ else:
+ raise AssertionError('Expected HTTPError')
+
def test_suite():
diff --git a/src/mailman/rest/tests/test_root.py b/src/mailman/rest/tests/test_root.py
new file mode 100644
index 000000000..856767947
--- /dev/null
+++ b/src/mailman/rest/tests/test_root.py
@@ -0,0 +1,93 @@
+# 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/>.
+
+"""REST root object tests."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'test_suite',
+ ]
+
+
+import unittest
+
+from urllib2 import HTTPError
+
+from mailman.testing.helpers import call_api
+from mailman.testing.layers import RESTLayer
+
+
+
+class TestSystem(unittest.TestCase):
+ layer = RESTLayer
+
+ def test_system_url_too_long(self):
+ # /system/foo/bar is not allowed.
+ try:
+ # For Python 2.6.
+ call_api('http://localhost:9001/3.0/system/foo/bar')
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 400)
+ else:
+ raise AssertionError('Expected HTTPError')
+
+ def test_system_url_not_preferences(self):
+ # /system/foo where `foo` is not `preferences`.
+ try:
+ # For Python 2.6.
+ call_api('http://localhost:9001/3.0/system/foo')
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 400)
+ else:
+ raise AssertionError('Expected HTTPError')
+
+ def test_system_preferences_are_read_only(self):
+ # /system/preferences are read-only.
+ try:
+ # For Python 2.6.
+ call_api('http://localhost:9001/3.0/system/preferences', {
+ 'acknowledge_posts': True,
+ }, method='PATCH')
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 405)
+ else:
+ raise AssertionError('Expected HTTPError')
+ # /system/preferences are read-only.
+ try:
+ # For Python 2.6.
+ call_api('http://localhost:9001/3.0/system/preferences', {
+ 'acknowledge_posts': False,
+ 'delivery_mode': 'regular',
+ 'delivery_status': 'enabled',
+ 'hide_address': True,
+ 'preferred_language': 'en',
+ 'receive_list_copy': True,
+ 'receive_own_postings': True,
+ }, method='PUT')
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 405)
+ else:
+ raise AssertionError('Expected HTTPError')
+
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(TestSystem))
+ return suite
diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py
index 3fdf5d7e8..857d29471 100644
--- a/src/mailman/rest/users.py
+++ b/src/mailman/rest/users.py
@@ -34,6 +34,7 @@ from mailman.interfaces.address import ExistingAddressError
from mailman.interfaces.usermanager import IUserManager
from mailman.rest.addresses import UserAddresses
from mailman.rest.helpers import CollectionMixin, etag, no_content, path_to
+from mailman.rest.preferences import Preferences
from mailman.rest.validator import Validator
from mailman.utilities.passwords import (
encrypt_password, make_user_friendly_password)
@@ -152,3 +153,15 @@ class AUser(_UserBase):
return http.not_found()
getUtility(IUserManager).delete_user(self._user)
return no_content()
+
+ @resource.child()
+ def preferences(self, request, segments):
+ """/addresses/<email>/preferences"""
+ if len(segments) != 0:
+ return http.bad_request()
+ if self._user is None:
+ return http.not_found()
+ child = Preferences(
+ self._user.preferences,
+ 'users/{0}'.format(self._user.user_id.int))
+ return child, []
diff --git a/src/mailman/rest/validator.py b/src/mailman/rest/validator.py
index cf32ca838..60d8a22d6 100644
--- a/src/mailman/rest/validator.py
+++ b/src/mailman/rest/validator.py
@@ -23,11 +23,15 @@ __metaclass__ = type
__all__ = [
'Validator',
'enum_validator',
+ 'language_validator',
'subscriber_validator',
]
from uuid import UUID
+from zope.component import getUtility
+
+from mailman.interfaces.languages import ILanguageManager
COMMASPACE = ', '
@@ -46,8 +50,6 @@ class enum_validator:
return self._enum_class[enum_value]
-
-
def subscriber_validator(subscriber):
"""Convert an email-or-int to an email-or-UUID."""
try:
@@ -56,6 +58,11 @@ def subscriber_validator(subscriber):
return subscriber
+def language_validator(code):
+ """Convert a language code to a Language object."""
+ return getUtility(ILanguageManager)[code]
+
+
class Validator:
"""A validator of parameter input."""