Created
June 21, 2011 16:33
Revisions
-
jacobian revised this gist
Jun 21, 2011 . 1 changed file with 9 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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' -
jacobian revised this gist
Jun 21, 2011 . No changes.There are no files selected for viewing
-
jacobian created this gist
Jun 21, 2011 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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