Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow submitting empty data for related fields that are not required #36

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 42 additions & 21 deletions conduit/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ def hydrate_request_data(self, request, *args, **kwargs):
model_field = self.Meta.model._meta.get_field(fieldname)
data[fieldname] = self._from_basic_type(model_field, data[fieldname])
except FieldDoesNotExist:
logger.info('Could not find field: {0}'.format(fieldname))
# We don't try to modify fields we don't know about
# or artificial fields like resource_uri
pass
Expand Down Expand Up @@ -632,7 +633,7 @@ def save_fk_objs(self, request, *args, **kwargs):
for bundle in kwargs['bundles']:
obj = bundle['obj']
request_data = bundle['request_data']

related_data = None
# Get all ForeignKey fields on the Model
fk_fieldnames = self._get_type_fieldnames(obj, models.ForeignKey)
# Get explicit FK fields which are not Model fields
Expand All @@ -641,26 +642,39 @@ def save_fk_objs(self, request, *args, **kwargs):
fk_fieldnames = set(fk_fieldnames)

for fieldname in fk_fieldnames:
# Only updated the related field if data was specified
if fieldname in request_data:
related_data = request_data.get(fieldname)

# If we are using a related resource field, use it
conduit_field = self._get_explicit_field_by_attribute(fieldname)
if conduit_field:
try:
conduit_field.save_related(request, self, obj, related_data)
except HttpInterrupt as e:
# Raise the error but specify it as occuring within
# the related field
error_dict = {fieldname: json.loads(e.response.content)}
response = self.create_json_response(py_obj=error_dict, status=e.response.status_code)
raise HttpInterrupt(response)
# Get the data to process
required = True
opts = obj._meta.get_field(fieldname)

if opts.blank and opts.null:
required = False

if fieldname not in request_data and required:
error_dict = {
fieldname: 'This field is required.'
}
response = self.create_json_response(py_obj=error_dict, status=500)
raise HttpInterrupt(response)

# Otherwise we do it simply with primary keys
else:
id_fieldname = '{0}_id'.format(fieldname)
setattr(obj, id_fieldname, related_data)
if fieldname in request_data:
related_data = request_data[fieldname]

# If we are using a related resource field, use it
conduit_field = self._get_explicit_field_by_attribute(fieldname)
if conduit_field and related_data:
try:
conduit_field.save_related(request, self, obj, related_data)
except HttpInterrupt as e:
# Raise the error but specify it as occuring within
# the related field
error_dict = {fieldname: json.loads(e.response.content)}
response = self.create_json_response(py_obj=error_dict, status=e.response.status_code)
raise HttpInterrupt(response)

# Otherwise we do it simply with primary keys
elif related_data:
id_fieldname = '{0}_id'.format(fieldname)
setattr(obj, id_fieldname, related_data)

return request, args, kwargs

Expand Down Expand Up @@ -698,7 +712,14 @@ def update_objs_from_data(self, request, *args, **kwargs):
for bundle in kwargs['bundles']:
obj = bundle['obj']
self._update_from_dict(obj, bundle['request_data'])
obj.save()
try:
obj.save()
except Exception, err:
error_dict = {'message': err.message}
logger.debug(obj)
logger.exception(err)
response = self.create_json_response(py_obj=error_dict, status=500)
raise HttpInterrupt(response)
# Refetch the object so that we can return an accurate
# representation of the data that is being persisted
bundle['obj'] = self.Meta.model.objects.get(**{self.Meta.pk_field: getattr( obj, self.Meta.pk_field )})
Expand Down
44 changes: 35 additions & 9 deletions conduit/test/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,56 @@
from conduit.api import Api
from conduit.test.testcases import ConduitTestCase

from api.views import BarResource, ContentTypeResource, FooResource, ItemResource
from api.views import BarResource, ContentTypeResource, FooResource, ItemResource, FooBarResource

from example.models import Bar, Baz, Foo, Item
from example.models import Bar, Baz, Foo, Item, FooBar


class ResourceTestCase(ConduitTestCase):
def setUp(self):
self.item_resource = ItemResource()

self.bar_resource = BarResource()

self.foo_resource = FooResource()
self.foobar_resource = FooBarResource()

api = Api(name='v1')
api.register(self.item_resource)
api.register(self.bar_resource)
api.register(self.foo_resource)
api.register(self.foobar_resource)

self.foo_uri = self.foo_resource._get_resource_uri()
self.foobar_uri = self.foobar_resource._get_resource_uri()
self.item_uri = self.item_resource._get_resource_uri()

self.bar_ctype = ContentType.objects.get(name='bar')

def test_fk_post_list(self):
data = [
{
'name': 'Chicken',
'bar': {
'name': 'Moose'
},
'foo': 'field that doesn\'t exist'
},
{
'name': 'Cow',
'parking lot': 123
}
]

resp = self.client.post(
self.foobar_uri,
json.dumps(data),
content_type='application/json'
)
content = json.loads(resp.content.decode())

self.assertEqual(resp.status_code, 500)
self.assertEqual(content, {'bar': 'This field is required.'})
self.assertEqual(FooBar.objects.count(), 0)

def test_gfk_post_list(self):
data = [
{
Expand Down Expand Up @@ -301,8 +331,6 @@ def test_gfk_foo_resource(self):

def test_related_fields_empty_data(self):
data = {
# 'bar': {},
# 'bazzes': {},
'boolean': False,
'decimal': '110.12',
'float_field': 100000.123456789,
Expand Down Expand Up @@ -342,6 +370,4 @@ def test_gfk_empty_data(self):

self.assertTrue(content['id'])
self.assertTrue(content['resource_uri'])
self.assertEqual(content['content_type'], None)
self.assertEqual(content['content_type_id'], None)
self.assertEqual(content['object_id'], None)
self.assertFalse(content['content_object'])
2 changes: 2 additions & 0 deletions example/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
ContentTypeResource,
FooResource,
ItemResource,
FooBarResource,
)


Expand All @@ -16,6 +17,7 @@
api.register(ContentTypeResource())
api.register(FooResource())
api.register(ItemResource())
api.register(FooBarResource())

urlpatterns = patterns('',
url(r'^', include(api.urls))
Expand Down
10 changes: 9 additions & 1 deletion example/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from conduit.api import ModelResource
from conduit.api.fields import ForeignKeyField, ManyToManyField, GenericForeignKeyField
from example.models import Bar, Baz, Foo, Item
from example.models import Bar, Baz, Foo, Item, FooBar


class BarResource(ModelResource):
Expand Down Expand Up @@ -54,3 +54,11 @@ class Fields:
},
embed=True
)


class FooBarResource(ModelResource):
class Meta(ModelResource.Meta):
model = FooBar

class Fields:
bar = ForeignKeyField(attribute='bar', resource_cls=BarResource)
12 changes: 10 additions & 2 deletions example/example/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@ class Foo(models.Model):
birthday = models.DateField(auto_now_add=True)
decimal = models.DecimalField(max_digits=5, decimal_places=2)
file_field = models.FileField(upload_to='test')
bar = models.ForeignKey(Bar, null=True)
bar = models.ForeignKey(Bar, blank=True, null=True)
bazzes = models.ManyToManyField(Baz)


class Item(models.Model):
content_type = models.ForeignKey(ContentType, null=True)
content_type = models.ForeignKey(ContentType, blank=True, null=True)
object_id = models.PositiveIntegerField(null=True)
content_object = generic.GenericForeignKey('content_type', 'object_id')


class FooBar(models.Model):
"""
A model with a required ForeignKey.
"""
name = models.CharField(max_length=255)
bar = models.ForeignKey(Bar)