Skip to content

Instantly share code, notes, and snippets.

@jacobian
Created June 21, 2011 16:33

Revisions

  1. jacobian revised this gist Jun 21, 2011. 1 changed file with 9 additions and 0 deletions.
    9 changes: 9 additions & 0 deletions patchable_example.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,9 @@
    from tastypie.resources import ModelResource

    class ContactResource(Patchable, ModelResource):

    class Meta(object):
    list_allowed_methods = ['get', 'post', 'put', 'delete', 'patch']
    detail_allowed_methods = ['get', 'put', 'delete', 'patch']
    queryset = Contact.objects.select_related('from_account', 'to_account')
    resource_name = 'contacts'
  2. jacobian revised this gist Jun 21, 2011. No changes.
  3. jacobian created this gist Jun 21, 2011.
    176 changes: 176 additions & 0 deletions patchable.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,176 @@
    from django.core import urlresolvers
    from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
    from django.db import transaction
    from tastypie.exceptions import BadRequest
    from tastypie.http import HttpAccepted, HttpGone, HttpMultipleChoices
    from tastypie.utils import dict_strip_unicode_keys

    class Patchable(object):
    """
    Mixin adding PATCH support to a ModelResource.
    """
    def patch_list(self, request, **kwargs):
    """
    Updates a collection in-place.
    The exact behavior of PATCH to a list resource is still the matter of
    some debate in REST circles, and the PATCH RFC isn't standard. So the
    behavior this method implements (described below) is something of a
    stab in the dark. It's mostly cribbed from GData, with a smattering
    of ActiveResource-isms and maybe even an original idea or two.
    The PATCH format is one that's similar to the response retuend from
    a GET on a list resource::
    {
    "objects": [{object}, {object}, ...],
    "deleted_objects": ["URI", "URI", "URI", ...],
    }
    For each object in "objects":
    * If the dict does not have a "resource_uri" key then the item is
    considered "new" and is handled like a POST to the resource list.
    * If the dict has a "resource_uri" key and the resource_uri refers
    to an existing resource then the item is a update; it's treated
    like a PATCH to the corresponding resource detail.
    * If the dict has a "resource_uri" but the resource *doesn't* exist,
    then this is considered to be a create-via-PUT.
    Each entry in "deleted_objects" referes to a resource URI of an existing
    resource to be deleted; each is handled like a DELETE to the relevent
    resource.
    In any case:
    * If there's a resource URI it *must* refer to a resource of this
    type. It's an error to include a URI of a different resource.
    * There's no checking of "sub permissions" -- that is, if "delete"
    isn't in detail_allowed_methods an object still could be deleted
    with PATCH.
    XXX Is this the correct behavior?
    * PATCH is all or nothing. If a single sub-operation fails, the
    entire request will fail and all resources will be rolled back.
    """
    convert_post_to_patch(request)

    deserialized = self.deserialize(request, request.raw_post_data, format=request.META.get('CONTENT_TYPE', 'application/json'))
    if "objects" not in deserialized:
    raise BadRequest("Invalid data sent.")

    with transaction.commit_on_success():
    for data in deserialized["objects"]:
    # If there 's a resource_uri then this is either an
    # update-in-place or a create-via-PUT.
    if "resource_uri" in data:
    uri = data.pop('resource_uri')
    pk = self.determine_pk_from_uri(uri)
    try:
    obj = self.cached_obj_get(request=request, pk=pk)
    except (ObjectDoesNotExist, MultipleObjectsReturned):
    # The object referenced by resource_uri doesn't exist,
    # so this is a create-by-PUT equivalent.
    data = self.alter_deserialized_detail_data(request, data)
    bundle = self.build_bundle(data=dict_strip_unicode_keys(data))
    self.is_valid(bundle, request)
    self.obj_create(bundle, request=request, pk=pk)

    else:
    # The object does exist, so this is an update-in-place.
    bundle = self.full_dehydrate(obj=obj)
    bundle = self.alter_detail_data_to_serialize(request, bundle)
    self.update_in_place(request, bundle, data)
    else:
    # There's no resource URI, so this is a create call just
    # like a POST to the list resource.
    data = self.alter_deserialized_detail_data(request, data)
    bundle = self.build_bundle(data=dict_strip_unicode_keys(data))
    self.is_valid(bundle, request)
    self.obj_create(bundle, request=request)

    for uri in deserialized.get('deleted_objects', []):
    pk = self.determine_pk_from_uri(uri)
    self.obj_delete(request=request, pk=pk)

    return HttpAccepted()

    def patch_detail(self, request, **kwargs):
    """
    Updates a resource in-place.
    """
    convert_post_to_patch(request)

    # This looks a bit weird, I know. We want to be able to validate the
    # update, but we can't just pass the partial data into the validator --
    # "required" fields aren't really required on an update, right? Instead,
    # we basically simulate a PUT by pulling out the original data and
    # updating it in-place.

    # So first pull out the original object. This is essentially get_detail.
    try:
    obj = self.cached_obj_get(request=request, **self.remove_api_resource_names(kwargs))
    except ObjectDoesNotExist:
    return HttpGone()
    except MultipleObjectsReturned:
    return HttpMultipleChoices("More than one resource is found at this URI.")

    bundle = self.full_dehydrate(obj=obj)
    bundle = self.alter_detail_data_to_serialize(request, bundle)

    # Now update the bundle in-place.
    deserialized = self.deserialize(request, request.raw_post_data, format=request.META.get('CONTENT_TYPE', 'application/json'))
    self.update_in_place(request, bundle, deserialized)
    return HttpAccepted()

    def update_in_place(self, request, original_bundle, new_data):
    """
    Update the object in original_bundle in-place using new_data.
    """
    original_bundle.data.update(**dict_strip_unicode_keys(new_data))

    # Now we've got a bundle with the new data sitting in it and we're
    # we're basically in the same spot as a PUT request. SO the rest of this
    # function is cribbed from put_detail.
    self.alter_deserialized_detail_data(request, original_bundle.data)
    self.is_valid(original_bundle, request)
    return self.obj_update(original_bundle, request=request, pk=original_bundle.obj.pk)

    def determine_pk_from_uri(self, uri):
    """
    Reverse-engineer the primary key out of a resource URI.
    """
    try:
    m = urlresolvers.resolve(uri)
    except urlresolvers.Resolver404:
    raise BadRequest("Invalid PATCH resource URI.")
    if m.view_name != 'api_dispatch_detail':
    raise BadRequest("PATCH resource URI doesn't point to a resource.")
    if m.kwargs.get('resource_name') != self._meta.resource_name:
    raise BadRequest("PATCH resource URI refers to the wrong type of resource.")
    return m.kwargs.get('pk')

    def convert_post_to_patch(request):
    """
    Force Django th process PATCH.
    See tastypie.resources.convert_post_to_put for details.
    """
    if hasattr(request, 'PATCH'):
    return
    if hasattr(request, '_post'):
    del request._post
    del request._files
    try:
    request.method = "POST"
    request._load_post_and_files()
    request.method = "PATCH"
    except AttributeError:
    request.META['REQUEST_METHOD'] = 'POST'
    request._load_post_and_files()
    request.META['REQUEST_METHOD'] = 'PATCH'
    request.PATCH = request.POST