Getting non_field_errors using JSONField() - python

I'm trying to make a PATCH request using to the Django Rest Framework but get the following error:
{"image_data": [{"non_field_errors": ["Invalid data"]}]
I understand that JSONField() could give some issues so I have taken care of that by adding to_native and from_native, But, I'm still running into this issue. I don't think JSONField() is the problem here at all, but still worth mentioning.
I believe I'm doing something fundamentally wrong in how I am trying to update the related field.
Code below...
Models:
class Photo(models.Model):
user = models.ForeignKey(AppUser, help_text="Item belongs to.")
image_data = models.ForeignKey("PhotoData", null=True, blank=True)
class PhotoData(models.Model):
thisdata = JSONField()
Serializers:
class ExternalJSONField(serializers.WritableField):
def to_native(self, obj):
return json.dumps(obj)
def from_native(self, value):
try:
val = json.loads(value)
except TypeError:
raise serializers.ValidationError(
"Could not load json <{}>".format(value)
)
return val
class PhotoDataSerializer(serializers.ModelSerializer):
thisdata = ExternalJSONField()
class Meta:
model = PhotoData
fields = ("id", "thisdata")
class PhotoSerializer(serializers.ModelSerializer):
image_data = PhotoDataSerializer()
class Meta:
model = Photo
fields = ("id","user", "image_data")
PATCH:
> payload = {"image_data": {"thisdata": "{}"}}
> requests.patch("/photo/123/",payload )
I have also tried:
> payload = {"image_data": [{"thisdata": "{}"}]}
> requests.patch("/photo/123/",payload )
But again giving the same error:
[{"non_field_errors": ["Invalid data"]}]

The original idea of Django Rest Framework's serialization of relations is to not change values of related fields.
It means that your payload should contain a pk of PhotoData object, not a dataset for it.
It's like in models you can't assign a dict to a foreign key field.
Good (works only with serializers.PrimaryKeyRelatedField which contains problems itself):
payload = {"image_data": 2}
Bad (not works in DRF by default):
payload = {"image_data": {'thisdata': '{}'}}
Actually the data model that you provided doesn't need PhotoData at all (you can move thisdata field to Photo), but let's assume you have a special case even when Zen of Python says Special cases aren't special enough to break the rules..
So, here is some possible ways:
Using fields serializers (your original way)
What you want to do now is possible but is very ugly solution.
You may create a PhotoDataField (works for me, but not ready to use code, only for demonstration)
class PhotoDataField(serializers.PrimaryKeyRelatedField):
def field_to_native(self, *args):
"""
Use field_to_native from RelatedField for correct `to_native` result
"""
return super(serializers.RelatedField, self).field_to_native(*args)
# Prepare value to output
def to_native(self, obj):
if isinstance(obj, PhotoData):
return obj.thisdata
return super(PhotoDataField, self).to_native(obj)
# Handle input value
def field_from_native(self, data, files, field_name, into):
try:
int(data['image_data'])
except ValueError:
# Looks like we have a data for `thisdata` field here.
# So let's do write this to PhotoData model right now.
# Why? Because you can't do anything with `image_data` in further.
if not self.root.object.image_data:
# Create a new `PhotoData` instance and use it.
self.root.object.image_data = PhotoData.objects.create()
self.root.object.image_data.thisdata = data['image_data']
self.root.object.image_data.save()
return data['image_data']
except KeyError:
pass
# So native behaviour works (e.g. via web GUI)
return super(PhotoDataField, self).field_from_native(data, files, field_name, into)
and use it in PhotoSerializer
class PhotoSerializer(serializers.ModelSerializer):
image_data = PhotoDataField(read_only=False, source='image_data')
class Meta:
model = Photo
fields = ("id", "user", "image_data")
so the request will works well
payload = {"image_data": '{}'}
resp = requests.patch(request.build_absolute_uri("/api/photo/1/"), payload)
and the "good" request also
photodata = PhotoData.objects.get(pk=1)
payload = {"image_data": photodata.pk}
resp = requests.patch(request.build_absolute_uri("/api/photo/1/"), payload)
and in result you will see in GET request "image_data": <photodata's thisdata value>,.
But, even if you will fix the validation problems with this approach it still be a huge pain in ass as you can see from my code (this is only thing DRF can offers you when you want to "break a normal workflow", Tastypie offers more).
Normalize your code and use #action (recommended)
class PhotoDataSerializer(serializers.ModelSerializer):
class Meta:
model = PhotoData
fields = ("id", "thisdata")
class PhotoSerializer(serializers.ModelSerializer):
image_data = PhotoDataSerializer() # or serializers.RelatedField
class Meta:
model = Photo
fields = ("id", "user", "image_data", "test")
and now define a specific method in your api's view that you will be able to use to set the data for any photo
from rest_framework import viewsets, routers, generics
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
# ViewSets define the view behavior.
class PhotoViewSet(viewsets.ModelViewSet):
model = Photo
serializer_class = PhotoSerializer
#action(methods=['PATCH'])
def set_photodata(self, request, pk=None):
photo = self.get_object()
serializer = PhotoDataSerializer(data=request.DATA)
if serializer.is_valid():
if not photo.image_data:
photo.image_data = PhotoData.objects.create()
photo.save()
photo.image_data.thisdata = serializer.data
photo.image_data.save()
return Response({'status': 'ok'})
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
Now you can do almost the same request as you doing now, but you have much more extensibility and division of responsibilities in code.
See the URL, it's appended when you have #action's wrapped method.
payload = {"thisdata": '{"test": "ok"}'}
resp = requests.patch(request.build_absolute_uri("/api/photo/1/set_photodata/"), payload)
Hope this helps.

Related

Django: How to check if data is correct before saving it to a database on a post request?

I would like to be able to parse the data from a post request in my django rest api project by sending it to my function that will return true or false if the number is valid before saving it to the database and if it's wrong send a custom bad request message to the client that did the request.
I've been told I can overwrite the create method todo this but I'm not sure how to go about it.
My code so far looks like this:
class Messages(models.Model):
phone_number = models.CharField(max_length=256, default='')
message_body = models.CharField(max_length=256, default='')
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.phone_number + ' ' + self.message_body + ' ' + self.created
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
# I assume this is where I would do the check before saving it but not sure how? example would be like:
# if numberValid(self.phone_number):
# then save to the database
# else:
# then send back a bad request?
super(Messages, self).save(force_update=force_update)
send_sms(self.phone_number, self.message_body)
def delete(self, using=None, keep_parents=False):
super(Messages, self).delete(using=using, keep_parents=keep_parents)
So basically just would like some direction on how to solve this problem. Even helpful links would be appreciated. I did look on stackoverflow but was not successful, maybe I don't know how to phrase the question right when searching.
You can use DRF Serializer's validation. For example, create a serializer, and add a validation method naming validate_<field_name>. Then add the validation code there:
import re
class MessagesSerializer(serializers.ModelSerializer):
class Meta:
model = Messages
fields = "__all__"
def validate_phone_number(self, value):
rule = re.compile(r'(^[+0-9]{1,3})*([0-9]{10,11}$)')
if not rule.search(value):
raise serializers.ValidationError("Invalid Phone Number")
return value
And use it in the view:
class SomeView(APIView):
def post(self, request, *args, **kwargs):
serializer = MessagesSerializer(
data=request.data
)
if serializer.is_valid(): # will call the validate function
serializer.save()
return Response({'success': True})
else:
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST
)
Check the official documentation for how this is to be done: https://docs.djangoproject.com/en/2.2/ref/models/instances/#django.db.models.Model.clean
This method should be used to provide custom model validation, and to modify attributes on your model if desired. For instance, you could use it to automatically provide a value for a field, or to do validation that requires access to more than a single field:
def clean(self):
# Don't allow draft entries to have a pub_date.
if self.status == 'draft' and self.pub_date is not None:
raise ValidationError(_('Draft entries may not have a publication date.'))
# Set the pub_date for published items if it hasn't been set already.
if self.status == 'published' and self.pub_date is None:
self.pub_date = datetime.date.today()
Implement a clean method that will raise a ValidationError if it detects a problem with the data. You can then catch this in the view by calling model_obj.full_clean():
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
try:
article.full_clean()
except ValidationError as e:
non_field_errors = e.message_dict[NON_FIELD_ERRORS]
Model.save() is an option although it's more common to validate input data, like a phone number being posted, in the DRF Serializer.
Where to perform checks is a decision you can make based on the principal of separation of concerns.
You want to validate the fields before saving.
There are quite a few techniques to do that.
using serializers. If you are using django rest framework, then you can easily use serilizers for validating.
https://www.django-rest-framework.org/api-guide/validators/
django model validation. This is by overriding some of the available methods in models class.
https://docs.djangoproject.com/en/2.2/ref/models/instances/#validating-objects
For your scenario i would suggest second option. Override the method clean_fields as in documentation. Then call the method just before saving.

Renaming DRF serializer fields

I'm using DRF serializers to validate incoming data that I retrieve from a JSON API. I'm trying to rename some awkwardly named fields from the response, making it easier to use the serializer.data further on in my code.
Data received from the API looks like this:
{"FunnyNamedField": true, "AnotherWeirdField": false}
And handling code:
resp = requests.get([...])
resp.raise_for_status()
ser = MyFunnyDataSerializer(data=resp.json())
if ser.is_valid():
do_domething_with(ser.data)
I would like the serializer to translate the incoming field names to something more consise. ser.data could look like: {'funny': True, 'weird': False}.
What I tried but doesn't work as I hoped:
class MyFunnyDataSerializer(serializers.Serializer):
funny = serializers.Booleanfield(source='FunnyNamedField')
Is there any way to achieve this without reverting to a SerializerMethodField?
You can override BaseSerializer to achieve this:
from rest_framework import serializers
class CustomSerializer(serializers.BaseSerializer):
def to_representation(self, instance):
return {
<datas>
}
You can do some specific modifications on instance serialization with custom methods.
Another solution could be to write your own validator for one field: Field Validator Method.
So in this documentation example you could modify value before return it.
from rest_framework import serializers
class BlogPostSerializer(serializers.Serializer):
title = serializers.CharField(max_length=100)
content = serializers.CharField()
def validate_title(self, value):
"""
Check that the blog post is about Django.
"""
if 'django' not in value.lower():
raise serializers.ValidationError("Blog post is not about Django")
if value == "something":
value = "something_else"
return value
Hope it helps.

Django-Rest-Framework APIView returning a model object and an image

suppose that I want to return in a APIView.get a (non model) object conatining 2 properties, one of them is a model object, and the other is a binary Image
I've tried several ways and had problems with serializer.
thanks!
Serializer:
class MyCustomSerializer(serializers.ModelSerializer):
class Meta:
model = MyCustom
fields = '__all__'
View:
class MyCustomGet(APIView):
def get(self, request, format=None):
serializer = MyCustomSerializer
obj = s.get_custom()
return Response({"obj": serializer(obj.obj).data, "image": obj.image})
get_custom:
class CustomClass:
obj = None
image = None
def get_custom():
r = CustomClass()
r.obj = MyCustom.objects.all().first()
r.image = PIL.Image.open('file/path.png')
return r
You are trying to render an image (binary data) in a JSON response (string). This would not work. If you want to pass the image in a JSON string, you have to encode it as a string and then decode it on the client side. An common example would be a base64-encoded string:
import io
import base64
...
def get_custom():
...
image = PIL.Image.open('file/path.png')
stream = io.StringIO()
image.save(stream, format='PNG')
return base64.b64encode(stream.getvalue())
Although not knowing anything about your REST endpoint design, a better solution in my opinion would be to declare a subresource with a separate view. Supposing you have a MyCustom resource in your REST schema accessible by api/mycustom/:id/ and served by MyCustomGet view, then e.g. a separate view could be responsible for offering the corresponding file under api/mycustom/:id/pic/:
import django.http
class MyCustomPicView(APIView):
def get(self, request):
...
image = PIL.Image.open('file/path.png')
return django.http.HttpResponse(image, content_type='image/png')
I'm not sure if this would be helpful, but this is what I did for a similar situation. I had one primary model object, and a semi-related object I wanted to tag in.
Basically, if your CustomClass model is somehow related to the image you need to include, just fetch it directly in the serializer instance.
This is sort of how I would try to do it:
class MyCustomSerializer(serializers.ModelSerializer):
image_binary = serializers.SerializerMethodField()
class Meta:
model = MyCustom
fields = '__all__'
def get_image_binary(self, obj):
return WhateverModel.objects.get(relation=obj).attribute
This simply adds some extra data to your model serializer, calculated on the fly by the serializer. You could even have a second serializer for the image_binary and in the get_image_binary() simply return the .data for the serializer, just like you did in your view.
This of course relies on the fact that the MyCustom object is somehow implicitly related to your image binary.
Firstly, in serializer you have mentioned model as MyCustom and your actual model name is CustomClass. Don't know if this is related or not. If image is the problem, you could convert the image to a string, and convert it back in the view/ wherever you want to use it.
class CustomClass:
obj = None
image = None
def get_custom():
r = CustomClass()
r.obj = MyCustom.objects.all().first()
import base64
with open("file/path.png", "rb") as imageFile:
str_ = base64.b64encode(imageFile.read())
r.image = str_
return r
To convert it back to an image:
fh = open("imageToSave.png", "wb")
fh.write(r.image.decode('base64'))
fh.close()
I don't know what your specific use case is, but this structure doesn't look right. I would use an ImageField to store the image, and in the serializer i would simply return image.url.

How to use Django REST Serializers?

So, after reading the Django REST Framework document, and a bunch of tutorials, I am still having trouble understanding how to use the Django serializers to convert incoming POST (JSON) data into a Python object (sorry, I'm new).
Given that I am posting a JSON string to, say, api/foo/bar, how do I write its serializer?
Example JSON:
{ 'name': 'Mr. Foo', address:'Bar Street' }
My controller, Foo contains a bar method as follows:
#detail_route(
methods=['post']
)
def bar(self, request, uuid=None):
serializer = MySampleSerializer(data=request.DATA)
something.clone(serializer.object)
return Response(status=status.HTTP_201_CREATED)
Can somebody explain to me what should my serializer look like? And how do I access the serialized data from the serializer?
As you do not want to use a model, you have to create the serializer from scratch. Something like this should maybe work:
class MySerializer(serializers.Serializer):
name = serializers.CharField(max_length = 100)
adress = serializers.CharField(max_length = 100)
And then you could use it in a request like this:
def bar(self, request, uuid=None):
data = JSONParser().parse(request)
serializer = MySerializer(data = data)
return Response(status=status.HTTP_201_CREATED)
Note however, as you have not created an Django model, you will not be able to save the serialized data (and thus nothing will be saved in the database)
Basically, you pass in the JSON data to the serializer, and then access the data field which will return an ordered dictionary.
def bar(self, request, uuid=None):
serializer = MySampleSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
my_object = serializer.data # Grab the dict of values
To define a serializer:
class MySampleSerializer(serializers.Serializer):
name = serializers.CharField(max_length=30)
address = serializers.CharField(max_length=30)
You do not have to use the ModelSerializer:
from rest_framework import serializers
class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField()
and access:
serializer = CommentSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
by the way, all the above is from the DRF website

Django-tastypie pass request.user to custom save method

Since my model's custom save method takes request.user as an argument I'm unable to do POST/PUT requests.
TypeError at /api/obsadmin/observation/23
save() takes at least 2 arguments (1 given)
I'm using SessionAuthentication() and have included the CSRF token.
Here's the relevant model part:
def save(self, user, owner=None, *args, **kwargs):
self.updated_by = user.id
super(ObsModel, self).save(*args, **kwargs)
And the resource:
class ObservationResource2(ModelResource):
comments = fields.ToManyField(CommentResource2, 'comments', full=True, null=True)
class Meta:
queryset = Observation.objects.filter(is_verified=True)
authentication = SessionAuthentication()
authorization = DjangoAuthorization()
resource_name = 'observation'
always_return_data = True
I've just achieved this same end goal by using the built-in hydrate methods to modify the data prior to saving. The current request is available in bundle.request inside the hydrate methods. See the docs here.
I have a Friend model exposed via FriendResource that I want to link to the creating Django user via a user ForeignKey field.
My example Resource code:
class FriendResource(ModelResource):
class Meta:
queryset = Friend.objects.all()
resource_name = 'friend'
excludes = ['slug',]
authentication = SessionAuthentication()
authorization = DjangoAuthorization()
always_return_data = True
def get_object_list(self, request):
return super(FriendResource, self).get_object_list(request).filter(user=request.user)
def hydrate(self, bundle):
bundle.obj.user = bundle.request.user
return bundle
Hope that helps!
You could override the default save() method on your ModelResource subclass. Looking at the default implementation shows that save() is called with a bundle object which has both the request and the object to be saved.
Unfortunately, there's no easy way to change this without copying most of that code because changing a Django model's save() signature is fairly uncommon. You might be able to do something like this, although I'd recommend testing it carefully:
from functools import partial
try:
old_save = bundle.obj.save
bundle.obj.save = partial(old_save, user=bundle.request.user)
return super(FooResource, self).save(bundle)
finally:
bundle.obj.save = old_save
References:
obj_create: docs source
obj_update: docs source
save: source

Categories