Skip to content

Commit

Permalink
Merge 75d7996 into fc9db3b
Browse files Browse the repository at this point in the history
  • Loading branch information
georgedorn committed Jun 17, 2019
2 parents fc9db3b + 75d7996 commit 49f4d72
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 16 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ tests/tastypie.db
.tox
env
env3
.idea
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Contributors:
* Danny Roberts (dannyroberts) for very small change addressing noisy RemovedInDjango20Warning warnings
* Sam Thompson (georgedorn) for ongoing maintenance and release management.
* Matt Briançon (mattbriancon) for various patches
* Ketan Bhatt (ketanbhatt) for fixing buggy patch for models with a FileField/ImageField


Thanks to Tav for providing validate_jsonp.py, placed in public domain.
Expand Down
2 changes: 1 addition & 1 deletion tastypie/contrib/gis/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def hydrate(self, bundle):
return value
return json.dumps(value)

def dehydrate(self, obj, for_list=False):
def dehydrate(self, obj, for_list=False, for_update=False):
return self.convert(super(GeometryApiField, self).dehydrate(obj))

def convert(self, value):
Expand Down
39 changes: 28 additions & 11 deletions tastypie/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def default(self):

return self._default

def dehydrate(self, bundle, for_list=True):
def dehydrate(self, bundle, for_list=True, for_update=False):
"""
Takes data from the provided object and prepares it for the
resource.
Expand Down Expand Up @@ -143,10 +143,19 @@ def dehydrate(self, bundle, for_list=True):
if callable(current_object):
current_object = current_object()

return self.convert(current_object)
if for_update and hasattr(self, 'convert_for_update'):
# Some fields behave differently on Retrieve vs Update; e.g. prepending URL
# If we don't allow for this, PATCH will read missing fields and write
# the fully modified versions back to the db.
return self.convert_for_update(current_object)
else:
return self.convert(current_object)

if self.has_default():
return self.convert(self.default)
if for_update and hasattr(self, 'convert_for_update'):
return self.convert_for_update(self.default)
else:
return self.convert(self.default)
else:
return None

Expand Down Expand Up @@ -226,6 +235,13 @@ class FileField(ApiField):
dehydrated_type = 'string'
help_text = 'A file URL as a string. Ex: "http://media.example.com/media/photos/my_photo.jpg"'

def convert_for_update(self, value):
"""
During PATCH, don't modify the value to include the URL, or
it'll save that back to the field.
"""
return value

def convert(self, value):
if value is None:
return None
Expand Down Expand Up @@ -562,7 +578,7 @@ def to_class(self):

return self._to_class

def dehydrate_related(self, bundle, related_resource, for_list=True):
def dehydrate_related(self, bundle, related_resource, for_list=True, for_update=False):
"""
Based on the ``full_resource``, returns either the endpoint or the data
from ``full_dehydrate`` for the related resource.
Expand All @@ -579,7 +595,7 @@ def dehydrate_related(self, bundle, related_resource, for_list=True):
request=bundle.request,
objects_saved=bundle.objects_saved
)
return related_resource.full_dehydrate(bundle)
return related_resource.full_dehydrate(bundle, for_update)

def resource_from_uri(self, fk_resource, uri, request=None, related_obj=None, related_name=None):
"""
Expand Down Expand Up @@ -751,7 +767,7 @@ def contribute_to_class(self, cls, name):
# this gets the related_name of the one to one field of our model
self.related_name = related_field.related.field.name

def dehydrate(self, bundle, for_list=True):
def dehydrate(self, bundle, for_list=True, for_update=False):
foreign_obj = None

if callable(self.attribute):
Expand All @@ -777,7 +793,7 @@ def dehydrate(self, bundle, for_list=True):

fk_resource = self.get_related_resource(foreign_obj)
fk_bundle = Bundle(obj=foreign_obj, request=bundle.request)
return self.dehydrate_related(fk_bundle, fk_resource, for_list=for_list)
return self.dehydrate_related(fk_bundle, fk_resource, for_list=for_list, for_update=for_update)

def hydrate(self, bundle):
value = super(ToOneField, self).hydrate(bundle)
Expand Down Expand Up @@ -827,7 +843,7 @@ def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED,
full_detail=full_detail
)

def dehydrate(self, bundle, for_list=True):
def dehydrate(self, bundle, for_list=True, for_update=True):
if not bundle.obj or not bundle.obj.pk:
if not self.null:
raise ApiFieldError("The model '%r' does not have a primary key and can not be used in a ToMany context." % bundle.obj)
Expand Down Expand Up @@ -865,7 +881,8 @@ def dehydrate(self, bundle, for_list=True):
self.dehydrate_related(
Bundle(obj=m2m, request=bundle.request),
self.get_related_resource(m2m),
for_list=for_list
for_list=for_list,
for_update=for_update,
)
for m2m in the_m2ms
]
Expand Down Expand Up @@ -919,8 +936,8 @@ class TimeField(ApiField):
dehydrated_type = 'time'
help_text = 'A time as string. Ex: "20:05:23"'

def dehydrate(self, obj, for_list=True):
return self.convert(super(TimeField, self).dehydrate(obj))
def dehydrate(self, obj, for_list=True, for_update=False):
return self.convert(super(TimeField, self).dehydrate(obj, for_update=for_update))

def convert(self, value):
if isinstance(value, six.string_types):
Expand Down
8 changes: 4 additions & 4 deletions tastypie/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,7 @@ def get_via_uri(self, uri, request=None):

# Data preparation.

def full_dehydrate(self, bundle, for_list=False):
def full_dehydrate(self, bundle, for_list=False, for_update=False):
"""
Given a bundle with an object instance, extract the information from it
to populate the resource.
Expand All @@ -908,7 +908,7 @@ def full_dehydrate(self, bundle, for_list=False):
field_object.api_name = api_name
field_object.resource_name = resource_name

data[field_name] = field_object.dehydrate(bundle, for_list=for_list)
data[field_name] = field_object.dehydrate(bundle, for_list=for_list, for_update=for_update)

# Check for an optional method to do further dehydration.
method = getattr(self, "dehydrate_%s" % field_name, None)
Expand Down Expand Up @@ -1626,7 +1626,7 @@ def patch_list(self, request, **kwargs):

# The object does exist, so this is an update-in-place.
bundle = self.build_bundle(obj=obj, request=request)
bundle = self.full_dehydrate(bundle, for_list=True)
bundle = self.full_dehydrate(bundle, for_list=True, for_update=True)
bundle = self.alter_detail_data_to_serialize(request, bundle)
self.update_in_place(request, bundle, data)
except (ObjectDoesNotExist, MultipleObjectsReturned):
Expand Down Expand Up @@ -1693,7 +1693,7 @@ def patch_detail(self, request, **kwargs):
return http.HttpMultipleChoices("More than one resource is found at this URI.")

bundle = self.build_bundle(obj=obj, request=request)
bundle = self.full_dehydrate(bundle)
bundle = self.full_dehydrate(bundle, for_update=True)
bundle = self.alter_detail_data_to_serialize(request, bundle)

# Now update the bundle in-place.
Expand Down
2 changes: 2 additions & 0 deletions tests/core/tests/resource_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from tastypie import fields
from tastypie.resources import ModelResource
from core.models import Note, Subject
from core.tests.resources import BlankMediaBitResource
from core.tests.api import Api


Expand Down Expand Up @@ -31,6 +32,7 @@ class Meta:
api.register(CustomNoteResource())
api.register(UserResource())
api.register(SubjectResource())
api.register(BlankMediaBitResource())

urlpatterns = [
url(r'^api/', include(api.urls)),
Expand Down
85 changes: 85 additions & 0 deletions tests/core/tests/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,26 @@ def hydrate_note(self, bundle):
return bundle


class AlwaysDataBlankMediaBitResource(ModelResource):
# Allow ``note`` to be omitted, even though it's a required field.
note = fields.ToOneField(NoteResource, 'note', blank=True)

class Meta:
queryset = MediaBit.objects.all()
always_return_data = True
authorization = Authorization()

# We'll custom populate the note here if it's not present.
# Doesn't make a ton of sense in this context, but for things
# like ``user`` or ``site`` that you can autopopulate based
# on the request.
def hydrate_note(self, bundle):
if not bundle.data.get('note'):
bundle.obj.note = Note.objects.get(pk=1)

return bundle


class TestOptionsResource(ModelResource):
class Meta:
queryset = Note.objects.all()
Expand Down Expand Up @@ -3366,6 +3386,71 @@ def test_patch_detail_use_in(self):
self.assertTrue("title" in data)
self.assertTrue("is_active" in data)

def test_patch_list_filefield_fix(self):
resource = BlankMediaBitResource()
request = HttpRequest()
request.GET = {'format': 'json'}
request.method = 'PATCH'
request._read_started = False

self.assertEqual(MediaBit.objects.count(), 1)
request._raw_post_data = request._body = '{"objects": [{"title": "Funny Dog Picture", "note": "/api/v1/notes/2/", "image": "lulz/dogz.gif"}, {"resource_uri": "/api/v1/blankmediabit/1/", "title": "Super Funny Cat Picture"}], "deleted_objects": []}'
resp = resource.patch_list(request)
self.assertEqual(resp.status_code, 202)
self.assertEqual(resp.content.decode('utf-8'), '')
self.assertEqual(MediaBit.objects.count(), 2)
new_mediabit = MediaBit.objects.get(title='Funny Dog Picture')
self.assertEqual(new_mediabit.image.name, "lulz/dogz.gif")
updated_mediabit = MediaBit.objects.get(pk=1)
self.assertEqual(updated_mediabit.title, "Super Funny Cat Picture")

self.assertEqual(updated_mediabit.image.name, "lulz/catz.gif") # Check if name isn't replaced by url

def test_patch_detail_filefield_fix(self):
resource = BlankMediaBitResource()
request = HttpRequest()
request.GET = {'format': 'json'}
request.method = 'PATCH'
request._read_started = False

self.assertEqual(MediaBit.objects.count(), 1)

# Change title, do not touch the image
request._raw_post_data = request._body = '{"title": "Boring Cat Picture"}'

resp = resource.patch_detail(request, pk=10)
self.assertEqual(resp.status_code, 404)

resp = resource.patch_detail(request, pk=1)
self.assertEqual(resp.status_code, 202)
self.assertEqual(MediaBit.objects.count(), 1)
mediabit = MediaBit.objects.get(pk=1)
self.assertEqual(mediabit.title, "Boring Cat Picture")
self.assertEqual(mediabit.image.name, "lulz/catz.gif") # imagefield still stores the name and not url

# Change image
request._raw_post_data = request._body = '{"image": "lulz/catz_new.gif"}'

resp = resource.patch_detail(request, pk=1)
self.assertEqual(resp.status_code, 202)
self.assertEqual(MediaBit.objects.count(), 1)
mediabit = MediaBit.objects.get(pk=1)
self.assertEqual(mediabit.title, "Boring Cat Picture") # Title is the same as what it was set before
self.assertEqual(mediabit.image.name, "lulz/catz_new.gif") # imagefield stores new image

always_resource = ALwaysDataBlankMediaBitResource()
request._raw_post_data = request._body = '{"title": "Cat is Funny Again!"}'
resp = always_resource.patch_detail(request, pk=1)
self.assertEqual(resp.status_code, 202)
data = json.loads(resp.content.decode('utf-8'))
self.assertTrue("id" in data)
self.assertEqual(data["id"], 1)
self.assertTrue("title" in data)
self.assertEqual(data["title"], u'Cat is Funny Again!')
self.assertTrue("image" in data)
self.assertEqual(data["image"], u'http://localhost:8080/media/lulz/catz_new.gif') # Response data contains the URL as dehydrated by the user
self.assertTrue("resource_uri" in data)

def test_dispatch_list(self):
resource = NoteResource()
request = HttpRequest()
Expand Down

0 comments on commit 49f4d72

Please sign in to comment.