summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/rest/configuration.py46
-rw-r--r--src/mailman/rest/docs/configuration.txt17
-rw-r--r--src/mailman/rest/helpers.py43
-rw-r--r--src/mailman/rest/validator.py7
4 files changed, 98 insertions, 15 deletions
diff --git a/src/mailman/rest/configuration.py b/src/mailman/rest/configuration.py
index 95bcf32db..05a062b02 100644
--- a/src/mailman/rest/configuration.py
+++ b/src/mailman/rest/configuration.py
@@ -31,7 +31,7 @@ from restish import http, resource
from mailman.config import config
from mailman.interfaces.autorespond import ResponseAction
from mailman.interfaces.mailinglist import IAcceptableAliasSet
-from mailman.rest.helpers import etag
+from mailman.rest.helpers import PATCH, etag
from mailman.rest.validator import Validator, enum_validator
@@ -232,18 +232,29 @@ class ListConfiguration(resource.Resource):
resource[attribute] = value
return http.ok([], etag(resource))
+ # XXX 2010-09-01 barry: Refactor {put,patch}_configuration() for common
+ # code paths.
+
+ def _set_writable_attributes(self, validator, request):
+ """Common code for setting all attributes given in the request.
+
+ Returns an HTTP 400 when a request tries to write to a read-only
+ attribute.
+ """
+ converted = validator(request)
+ for key, value in converted.items():
+ ATTRIBUTES[key].put(self._mlist, key, value)
+
@resource.PUT()
def put_configuration(self, request):
"""Set a mailing list configuration."""
attribute = self._attribute
if attribute is None:
- # Set all writable attributes.
+ validator = Validator(**VALIDATORS)
try:
- converted = Validator(**VALIDATORS)(request)
+ self._set_writable_attributes(validator, request)
except ValueError as error:
return http.bad_request([], str(error))
- for key, value in converted.items():
- ATTRIBUTES[key].put(self._mlist, key, value)
elif attribute not in ATTRIBUTES:
return http.bad_request(
[], b'Unknown attribute: {0}'.format(attribute))
@@ -253,9 +264,28 @@ class ListConfiguration(resource.Resource):
else:
validator = Validator(**{attribute: VALIDATORS[attribute]})
try:
- values = validator(request)
+ self._set_writable_attributes(validator, request)
except ValueError as error:
return http.bad_request([], str(error))
- ATTRIBUTES[attribute].put(
- self._mlist, attribute, values[attribute])
+ return http.ok([], '')
+
+ @PATCH()
+ def patch_configuration(self, request):
+ """Patch the configuration (i.e. partial update)."""
+ # Validate only the partial subset of attributes given in the request.
+ validationators = {}
+ for attribute in request.PATCH:
+ if attribute not in ATTRIBUTES:
+ return http.bad_request(
+ [], b'Unknown attribute: {0}'.format(attribute))
+ elif ATTRIBUTES[attribute].decoder is None:
+ return http.bad_request(
+ [], b'Read-only attribute: {0}'.format(attribute))
+ else:
+ validationators[attribute] = VALIDATORS[attribute]
+ validator = Validator(**validationators)
+ try:
+ self._set_writable_attributes(validator, request)
+ except ValueError as error:
+ return http.bad_request([], str(error))
return http.ok([], '')
diff --git a/src/mailman/rest/docs/configuration.txt b/src/mailman/rest/docs/configuration.txt
index 1ad374d32..63f87fde1 100644
--- a/src/mailman/rest/docs/configuration.txt
+++ b/src/mailman/rest/docs/configuration.txt
@@ -303,15 +303,20 @@ Changing a partial configuration
Using PATCH, you can change just one attribute.
-XXX WebOb does not currently support PATCH, so neither does restish.
-
-# >>> dump_json('http://localhost:8001/3.0/lists/'
-# ... 'test-one@example.com/config',
-# ... dict(real_name='My List'),
-# ... 'PATCH')
+ >>> dump_json('http://localhost:8001/3.0/lists/'
+ ... 'test-one@example.com/config',
+ ... dict(real_name='My List'),
+ ... 'PATCH')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 200
These values are changed permanently.
+ >>> print mlist.real_name
+ My List
+
Sub-resources
=============
diff --git a/src/mailman/rest/helpers.py b/src/mailman/rest/helpers.py
index 3117ed47e..dc67a699b 100644
--- a/src/mailman/rest/helpers.py
+++ b/src/mailman/rest/helpers.py
@@ -22,6 +22,7 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'ContainerMixin',
+ 'PATCH',
'etag',
'no_content',
'path_to',
@@ -29,13 +30,17 @@ __all__ = [
]
+import cgi
import json
import hashlib
+from cStringIO import StringIO
from datetime import datetime, timedelta
from flufl.enum import Enum
from lazr.config import as_boolean
from restish.http import Response
+from restish.resource import MethodDecorator
+from webob.multidict import MultiDict
from mailman.config import config
@@ -163,3 +168,41 @@ def restish_matcher(function):
def no_content():
"""204 No Content."""
return Response('204 No Content', [], None)
+
+
+# These two classes implement an ugly, dirty hack to work around the fact that
+# neither WebOb nor really the stdlib cgi module support non-standard HTTP
+# verbs such as PATCH. Note that restish handles it just fine in the sense
+# that the right method gets called, but without the following kludge, the
+# body of the request will never get decoded, so the method won't see any
+# data.
+#
+# Stuffing the MultiDict on request.PATCH is pretty ugly, but it mirrors
+# WebOb's use of request.POST and request.PUT for those standard verbs.
+# Besides, WebOb refuses to allow us to set request.POST. This does make
+# validators.py a bit more complicated. :(
+
+class PATCHWrapper:
+ """Hack to decode the request body for PATCH."""
+ def __init__(self, func):
+ self.func = func
+
+ def __call__(self, resource, request):
+ # We can't use request.body_file because that's a socket that's
+ # already had its data read off of. IOW, if we use that directly,
+ # we'll block here.
+ field_storage = cgi.FieldStorage(
+ fp=StringIO(request.body),
+ # Yes, lie about the method so cgi will do the right thing.
+ environ=dict(REQUEST_METHOD='POST'),
+ keep_blank_values=True)
+ request.PATCH = MultiDict.from_fieldstorage(field_storage)
+ return self.func(resource, request)
+
+
+class PATCH(MethodDecorator):
+ method = 'PATCH'
+
+ def __call__(self, func):
+ really_wrapped_func = PATCHWrapper(func)
+ return super(PATCH, self).__call__(really_wrapped_func)
diff --git a/src/mailman/rest/validator.py b/src/mailman/rest/validator.py
index e9bcf4729..cf51cee75 100644
--- a/src/mailman/rest/validator.py
+++ b/src/mailman/rest/validator.py
@@ -61,7 +61,12 @@ class Validator:
# in the pre-converted dictionary. All keys which show up more than
# once get a list value.
missing = object()
- for key, new_value in request.POST.items():
+ # This is a gross hack to allow PATCH. See helpers.py for details.
+ try:
+ items = request.PATCH.items()
+ except AttributeError:
+ items = request.POST.items()
+ for key, new_value in items:
old_value = form_data.get(key, missing)
if old_value is missing:
form_data[key] = new_value