Skip to content

Instantly share code, notes, and snippets.

@jacobian
Created June 21, 2011 16:33
Show Gist options
  • Save jacobian/1038251 to your computer and use it in GitHub Desktop.
Save jacobian/1038251 to your computer and use it in GitHub Desktop.
Adds PATCH support to tastypie
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
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'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment