diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/rest/configuration.py | 46 | ||||
| -rw-r--r-- | src/mailman/rest/docs/configuration.txt | 17 | ||||
| -rw-r--r-- | src/mailman/rest/helpers.py | 43 | ||||
| -rw-r--r-- | src/mailman/rest/validator.py | 7 |
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 |
