How to deserialize foreignkey with django rest framework? - django

For example
class People(models.Model):
name = models.CharField(max_length=20)
class Blog(models.Model):
author = models.ForeignKeyField(People)
content = models.TextField()
and then,
class CreateBlogSerializer(serializers.Serializer):
#author id
author = serializers.IntegerField()
content = serializers.TextField()
In views, I need to get author_id , check if the id exists and get the author instance, it's fussy to do that.
serializer = CreateBlogSerializer(data=request.DATA)
if serializer.is_valid():
try:
author = Author.objects.get(pk=serializer.data["author"])
except Author.DoesNotExist:
return Response(data={"author does not exist"})
blog = Blog.objects.create(author=author, content=serializer.data["content"])
Is there a ForeignKeyField to deserialize and validate primarykey data and then return a instance.
class CreateBlogSerializer(serializers.Serializer):
author = serializers.ForeignKeyField(Author)
content = serializers.TextField()
serializer = CreateBlogSerializer(data=request.DATA)
if serializer.is_valid():
#after deserialization , the author id becomes author model instance
blog = Blog.objects.create(author=serializer.data["author"], content=serializer.data["content"])
else:
#the author id does not exist will cause serializer.is_valid=Flase
PS
I knew PrimaryKeyRelatedField in ModelSerializer,but I can't use ModelSerializer here, the model structures are complex, the above are just examples。
My first thought is to write a customer field.

class ForeignKeyField(WritableField):
def __init__(self, model_name, *args, **kwargs):
super(ForeignKeyField, self).__init__(*args, **kwargs)
self.model_name = model_name
self.model_instance = None
def from_native(self, pk):
if not isinstance(pk, int):
raise ValidationError("pk must be int")
try:
self.model_instance = self.model_name.objects.get(pk=pk)
return self.model_instance
except self.model_name.DoesNotExist:
raise ValidationError('object does not exist')
def to_native(self, obj):
return self.model_instance
I hacked it, but I don't konw why it works.
Usage:
there is a little difference
class t_serializer(serializers.Serializer):
author = ForeignKeyField(Author)
#api_view(["POST"])
def func(request):
serializer = t_serializer(data=request.DATA)
if serializer.is_valid():
print isinstance(serializer.data["author"], Author)
#print True
else:
print serializer.errors

It appears that what you want is custom validation code.
For this particular instance, you could write the following.
class CreateBlogSerializer(serializers.Serializer):
author = serializers.ForeignKeyField(Author)
content = serializers.TextField()
def validate_author(self, attrs, source):
"""
Verify that the author exists.
"""
author = attrs[source]
if Author.objects.filter(pk=author).exists():
return attrs
raise serializers.ValidationError("Specified Author does not exist!")
Now when you call serializer.is_valid() this check will occur.
So you can then do this elsewhere in your code,
if serializer.is_valid():
blog = Blog.objects.create(author=serializer.data["author"], content=serializer.data["content"])
And be sure that if the given blog is created, there is a corresponding Author already in the DB.
Detailed Explanation
So Django Rest Framework provides a method of adding custom validation to serializers. This can be done with by providing methods of the following format in the serializer class validate_<field name>. These methods will set the source value to name of the given field, in our example author, which can then be used with the attrs variable to get the current value of the field (attrs contains all the values passed into the serializer).
These validation methods are each called with then serializer.is_valid() method is called. They are supposed to just return attrs if the given test passes, and raise a ValidationError otherwise.
They can get somewhat complex, so it is probably best if you read the full documentation here, http://www.django-rest-framework.org/api-guide/serializers#validation

Related

How to use SlugRelatedField for incoming request (deserialise) and full serialiser for response for related fields

My db model is of events and each event is connected to a venue.
When I retrieve a list of events I use:
venue = VenueSerializer(read_only=True)
When I post to my drf endpoint I use:
venue = serializers.SlugRelatedField(
allow_null=True,
queryset=Venue.objects.all(),
required=False,
slug_field='id')
However this causes that in the response I recieve from the post request, the venue is serialised as a slug field. I want it to use the VenueSerialiser for the response.
I came accross https://stackoverflow.com/a/49035208/5683904 but it only works on the Viewset itself.
#serializer_class = EventSerializer
read_serializer_class = EventSerializer
create_serializer_class = EventCreateUpdateSerializer
I need to build this functionality into the serialiser itself since it is shared with other components.
The Problem
The SlugRelatedField's to_representation method is coded to return the value of the slug_field keyword argument that you pass to it during initialization.
Workarounds
Extend SlugRelatedField and override it's to_representation method to return the complete object instead of the slug. This could be a little tricky because the actual model instance isn't a part of the class.
Have two fields, one for the slug and another for the actual object. This is way easier to implement.
Here's how you can implement the second workaround:
venue = VenueSerializer(read_only=True)
venue_id = serializers.SlugRelatedField(
write_only=True
allow_null=True,
queryset=Venue.objects.all(),
required=False,
slug_field='id')
UPDATE: This is apparently a pretty wanted feature in DRF. I've found a way to implement the first workaround as well. It deals with PrimaryKeyRelatedField but you could probably modify it to work with SlugRelatedField too. Here it is:
from collections import OrderedDict
from rest_framework import serializers
class AsymetricRelatedField(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
return self.serializer_class(value).data
def get_queryset(self):
if self.queryset:
return self.queryset
return self.serializer_class.Meta.model.objects.all()
def get_choices(self, cutoff=None):
queryset = self.get_queryset()
if queryset is None:
return {}
if cutoff is not None:
queryset = queryset[:cutoff]
return OrderedDict([
(
item.pk,
self.display_value(item)
)
for item in queryset
])
def use_pk_only_optimization(self):
return False
#classmethod
def from_serializer(cls, serializer, name=None, args=(), kwargs={}):
if name is None:
name = f"{serializer.__class__.name}AsymetricAutoField"
return type(name, [cls], {"serializer_class": serializer})
You can then use this field like this:
class FooSerializer(serilizers.ModelSerializer):
bar = AsymetricRelatedField(BarSerializer)
class Meta:
model = Foo
You can find the original discussion about this here

Django SerializerMethodField can't save a decimal

So, according to the docs, SerializerMethodField is a read-only field.
Well in my case, it's interfering with my write:
# old value is 2.5
data={'score': 1.7}
serializer = ScoreTraitSerializer(
score_trait, data=data, partial=True)
if serializer.is_valid():
new_score_trait = serializer.save()
Now if I inspect the new_score_trait, my score is still 2.5.
The serializer looks as such:
score = serializers.SerializerMethodField()
def get_score(self, obj):
if isinstance(obj.score, decimal.Decimal):
return float(obj.score)
else:
return obj.score
If I comment out my SerializerMethodField, I can save the new decimal value (but can't serialize it).
So ... am I using my serializer correctly? Why does my write to the serializer hitting the SerializerMethodField?
Thanks in advance
SerializerMethodField is a read-only field.Only used for to_representation, it's used for list/retrieve not create/update.
the serializer field score must conflict with model field score,try change it to:
float_score = serializers.SerializerMethodField(required=False)
def get_float_score (self, obj):
if isinstance(obj.score, decimal.Decimal):
return float(obj.score)
else:
return obj.score
See the source code you will know why:
class SerializerMethodField(Field):
"""
A read-only field that get its representation from calling a method on the
parent serializer class. The method called will be of the form
"get_{field_name}", and should take a single argument, which is the
object being serialized.
For example:
class ExampleSerializer(self):
extra_info = SerializerMethodField()
def get_extra_info(self, obj):
return ... # Calculate some data to return.
"""
def __init__(self, method_name=None, **kwargs):
self.method_name = method_name
kwargs['source'] = '*'
kwargs['read_only'] = True
super(SerializerMethodField, self).__init__(**kwargs)

Handling multiple query params

I have a create user view and here I first register a normal user and then create a player object for that user which has a fk relation with the user.
In my case, I have three different types of users
I created a view to handle register all three different types of users, but my player user has a lot of extra model fields and storing all query params in variables will make it messy.
Is there a better way to handle this, including validation?
TLDR; I created a view to handle register all three different types of users, but my player user has a lot of extra model fields and storing all query params in variables will make it messy. Is there a better way to handle this, including validation?
This is my view.
class CreateUser(APIView):
"""
Creates the User.
"""
def post(self, request):
email = request.data.get('email', None).strip()
password = request.data.get('password', None).strip()
name = request.data.get('name', None).strip()
phone = request.data.get('phone', None)
kind = request.query_params.get('kind', None).strip()
print(kind)
serializer = UserSerializer(data={'email': email, 'password':password})
serializer.is_valid(raise_exception=True)
try:
User.objects.create_user(email=email,
password=password)
user_obj = User.objects.get(email=email)
except:
raise ValidationError('User already exists')
if kind == 'academy':
Academy.objects.create(email=email, name=name, phone=phone, user=user_obj)
if kind == 'coach':
Coach.objects.create(email=email, name=name, phone=phone, user=user_obj)
if kind == 'player':
Player.objects.create(----------)
return Response(status=200)
Use a Model Serializer
In your case, define it in serializers.py like this:
from rest_framework import serializers
class CustomBaseSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['user'] = self.context['user']
return super(CustomBaseSerializer, self).create(validated_data)
class PlayerSerializer(CustomBaseSerializer):
class Meta:
model = Player
fields = ('count', 'height', 'right_handed', 'location',
'size', 'benchmark_swingspeed',
'benchmark_impactspeed', 'benchmark_stance',
'benchmark_balanceindex',)
class AcademySerializer(CustomBaseSerializer):
class Meta:
model = Academy
fields = '__all__' # Usually better to explicitly list fields
class CoachSerializer(CustomBaseSerializer):
class Meta:
model = Coach
fields = '__all__'
Then in your view
class CreateUser(APIView):
"""
Creates the User.
"""
def post(self, request):
print(kind)
try:
user = User.objects.get(email=request.data.get('email'))
except User.DoesNotExist:
pass
else:
raise ValidationError('User already exists')
user_serializer = UserSerializer(data=request.data)
user_serializer.is_valid(raise_exception=True)
user = user_serializer.save()
if kind == 'academy':
serializer_class = AcademySerializer
if kind == 'coach':
serializer_class = CoachSerializer
if kind == 'player':
serializer_class = PlayerSerializer
serializer = serializer_class(data=request.data, context={'user': user})
serializer.save()
return Response(serializer.data) # Status is 200 by default so you don't need to include it. RESTful API's should return the instance created, this also delivers the newly generated primary key back to the client.
# Oh and if you do serialize the object in the response, write serializers for academy and coach too, so the api response is consistent
Serializers are really powerful and useful. It is well worth thoroughly reading the docs.
First, I'd recommend to POST the parameters in a JSON or a form, instead of using the query params. But regardless the method, the solution is pretty much the same.
First, you could define the fields you're interested in a list. For example:
FIELDS = (
'count',
'height',
'right_handed',
'location',
'size',
'benchmark_swingspeed',
'benchmark_impactspeed',
'benchmark_stance',
'benchmark_balanceindex',
)
And then get all the values from the query params and store them in a dict, like:
player_params = {}
for field in FIELDS:
player_params[field] = request.query_params.get(field)
Now you have all the params required for a player in a dict and you can pass it to the Player model as **kwargs. Of course you'll probably need some validation. But in the end, you'll be able to do the following:
Player.objects.create(user=user_obj, **player_params)

DRF: accessing a SerializerMethodField during serializer validation

I'm using Django Rest Framework 3.0 and I have a model:
class Vote(models.Model):
name = ...
token = models.CharField(max_length=50)
where token is a unique identifier that I generate from the request IP information to prevent the same user voting twice
I have a serializer:
class VoteSerializer(serializers.ModelSerializer):
name = ...
token = serializers.SerializerMethodField()
class Meta:
model = Vote
fields = ("id", "name", "token")
def validate(self, data):
if Rating.objects.filter(token=data['token'], name=data['name']).exists():
raise serializers.ValidationError("You have already voted for this")
return data
def get_token(self, request):
s = ''.join((self.context['request'].META['REMOTE_ADDR'], self.context['request'].META.get('HTTP_USER_AGENT', '')))
return md5(s).hexdigest()
and a CreateView
But I am getting a
KeyError: 'token'
when I try to post and create a new Vote. Why is the token field not included in the data when validating?
The docs mention:
It can be used to add any sort of data to the serialized representation of your object.
So I would have thought it would be also available during validate?
Investigating, it seems that SerializerMethodField fields are called after validation has occurred (without digging into the code, I don't know why this is - it seems counter intuitive).
I have instead moved the relevant code into the view (which actually makes more sense conceptually to be honest).
To get it working, I needed to do the following:
class VoteCreateView(generics.CreateAPIView):
serializer_class = VoteSerializer
def get_serializer(self, *args, **kwargs):
# kwarg.data is a request MergedDict which is immutable so we have
# to copy the data to a dict first before inserting our token
d = {}
for k, v in kwargs['data'].iteritems():
d[k] = v
d['token'] = self.get_token()
kwargs['data'] = d
return super(RatingCreateView, self).get_serializer(*args, **kwargs)
def get_token(self):
s = ''.join((self.request.META['REMOTE_ADDR'], self.request.META.get('HTTP_USER_AGENT', '')))
return md5(s).hexdigest()
I really hope this isn't the correct way to do this as it seems totally convoluted for what appears to be a pretty straight forward situation. Hopefully someone else can post a better approach to this.

ModelChoiceField in forms.Form won't validate if queryset is overridden

I have a django ModelChoiceField that won't validate if I override the queryset.
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
form = PersonalNote()
form.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)
The form.is_valid() error is: "Select a valid choice. That choice is not one of the available choices".
If Tile.objects.none() is replaced with Tile.objects.all() it validates, but loads far too much data from the database. I've also tried:
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
def __init__(self, *args, **kwargs):
yyy = kwargs.pop('yyy', None)
super(PersonalNote, self).__init__(*args, **kwargs)
if yyy:
self.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)
What might be wrong here? Note the real application also overrides the label, but that does not seem to be a factor here:
class ModelChoiceField2(forms.ModelChoiceField):
def label_from_instance(self, obj):
assert isinstance(obj,Tile)
return obj.child_title()
After 2 hours I found the solution. Because you specified a queryset of none in the class definition, when you instantiate that PersonalNote(request.POST) to be validated it is referenceing a null query set
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
To fix this, when you create your form based on a POST request be sure to overwrite your queryset AGAIN before you check is_valid()
def some_view_def(request):
form = PersonalNote(request.POST)
**form.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)**
if form.is_valid():
#Do whatever it is
When you pass an empty queryset to ModelChoiceField you're saying that nothing will be valid for that field. Perhaps you could filter the queryset so there aren't too many options.
I also had this problem. The idea is to dynamically change the queryset of a ModelChoiceField based on a condition (in my case it was a filter made by another ModelChoiceField).
So, having the next model as example:
class FilterModel(models.Model):
name = models.CharField()
class FooModel(models.Model):
filter_field = models.ForeignKey(FilterModel)
name = models.CharField()
class MyModel(models.Model):
foo_field = models.ForeignKey(FooModel)
As you can see, MyModel has a foreign key with FooModel, but not with FilterModel. So, in order to filter the FooModel options, I added a new ModelChoiceField on my form:
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
# your code here
self.fields['my_filter_field'] = forms.ModelChoiceField(FilterModel, initial=my_filter_field_selected)
self.fields['my_filter_field'].queryset = FilterModel.objects.all()
Then, on your Front-End you can use Ajax to load the options of foo_field, based on the selected value of my_filter_field. At this point everyting should be working. But, when the form is loaded, it will bring all the posible options from FooModel. To avoid this, you need to dynamically change the queryset of foo_field.
On my form view, I passed a new argument to MyForm:
id_filter_field = request.POST.get('my_filter_field', None)
form = MyForm(data=request.POST, id_filter_field=id_filter_field)
Now, you can use that argument on MyForm to change the queryset:
class MyForm(forms.ModelForm):
# your code here
def __init__(self, *args, **kwargs):
self.id_filter_field = kwargs.pop('id_filter_field', None)
# your code here
if self.id_filter_field:
self.fields['foo_field'].queryset = FooModel.objects.filter(filter_field_id=self.id_filter_field)
else:
self.fields['foo_field'].queryset = FooModel.objects.none()