DRF: Conditially change serializer - python

I'm using Django 3.0 and I have a serializer using django-rest-framework. Let's say that for example I have a Forum object. Each forum has an owner that is a user.
In my GET /forums/ endpoint, I'd like to just have the owner_id. However, in my GET /forums/<forum_id>/ endpoint I'd like to return the entire embedded object.
Is there any way to have one serializer support both of these scenarios? If not, I would hate to have to make two serializers just to support this.
class ForumSerializer(serializers.ModelSerializer, compact=True):
if self.compact is False:
owner = UserSerializer(source='owner', read_only=True)
else:
owner_id = serializers.UUIDField(source='owner_id')
...
How can I achieve this compact thing?
class Meta:
fields = [...]
read_only_fields = ['owner', 'owner_id']

You can add a SerializerMethodField like this:
class ForumSerializer(serializers.ModelSerializer):
owner = serializer.SerializerMethodField()
def get_owner(self, obj):
if self.context['is_compact'] == True:
return obj.owner.pk
else:
return UserSerializer(obj.owner).data
class Meta:
model = YourModel
fields = '__all__'
# Usage in view
serializer = ForumSerializer(context={'is_compact':True})
I am passing is_compact value through serializer's extra context.

create two serializer classes
class ForumSerializerId(ModelSerializer):
class Meta:
model = Forum
fields = ['forum_id']
class ForumSerializerDetail(ModelSerializer):
class Meta:
model = Forum
on your view.py
forums(request):
forum_list = Forum.objects.all()
forum_serializer = ForumSerializerId(forum_list,many=True)
return Response({"form":forum_serializer.data})
forum_detail(request,pk):
forum = get_object_or_404(Forum,pk)
forum_serializer = ForumSerializerDetail(forum)
return Response({"form":forum_serializer.data})

Related

Django rest framework nested serializer create method

I have created a nested serializer, when I try to post data in it it keeps on displaying either the foreign key value cannot be null or dictionary expected. I have gone through various similar questions and tried the responses but it is not working for me. Here are the models
##CLasses
class Classes(models.Model):
class_name = models.CharField(max_length=255)
class_code = models.CharField(max_length=255)
created_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.class_name
class Meta:
ordering = ['class_code']
##Streams
class Stream(models.Model):
stream_name = models.CharField(max_length=255)
classes = models.ForeignKey(Classes,related_name="classes",on_delete=models.CASCADE)
created_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.stream_name
class Meta:
ordering = ['stream_name']
Here is the view
class StreamViewset(viewsets.ModelViewSet):
queryset = Stream.objects.all()
serializer_class = StreamSerializer
Here is the serializer class
class StreamSerializer(serializers.ModelSerializer):
# classesDetails = serializers.SerializerMethodField()
classes = ClassSerializer()
class Meta:
model = Stream
fields = '__all__'
def create(self,validated_data):
classes = Classes.objects.get(id=validated_data["classes"])
return Stream.objects.create(**validated_data, classes=classes)
# def perfom_create(self,serializer):
# serializer.save(classes=self.request.classes)
#depth = 1
# def get_classesDetails(self, obj):
# clas = Classes.objects.get(id=obj.classes)
# classesDetails = ClassSerializer(clas).data
# return classesDetails
I have tried several ways of enabling the create method but like this displays an error {"classes":{"non_field_errors":["Invalid data. Expected a dictionary, but got int."]}}. Any contribution would be deeply appreciated
This is a very common situation when developing APIs with DRF.
The problem
Before DRF reaches the create() method, it validates the input, which I assume has a form similar to
{
"classes": 3,
"stream_name": "example"
}
This means that, since it was specified that
classes = ClassSerializer()
DRF is trying to build the classes dictionary from the integer. Of course, this will fail, and you can see that from the error dictionary
{"classes":{"non_field_errors":["Invalid data. Expected a dictionary, but got int."]}}
Solution 1 (requires a new writable field {field_name}_id)
A possible solution is to set read_only=True in your ClassSerializer, and use an alternative name for the field when writing, it's common to use {field_name}_id. That way, the validation won't be done. See this answer for more details.
class StreamSerializer(serializers.ModelSerializer):
classes = ClassSerializer(read_only=True)
class Meta:
model = Stream
fields = (
'pk',
'stream_name',
'classes',
'created_date',
'classes_id',
)
extra_kwargs = {
'classes_id': {'source': 'classes', 'write_only': True},
}
This is a clean solution but requires changing the user API. In case that's not an option, proceed to the next solution.
Solution 2 (requires overriding to_internal_value)
Here we override the to_internal_value method. This is where the nested ClassSerializer is throwing the error. To avoid this, we set that field to read_only and manage the validation and parsing in the method.
Note that since we're not declaring a classes field in the writable representation, the default action of super().to_internal_value is to ignore the value from the dictionary.
from rest_framework.exceptions import ValidationError
class StreamSerializer(serializers.ModelSerializer):
classes = ClassSerializer(read_only=True)
def to_internal_value(self, data):
classes_pk = data.get('classes')
internal_data = super().to_internal_value(data)
try:
classes = Classes.objects.get(pk=classes_pk)
except Classes.DoesNotExist:
raise ValidationError(
{'classes': ['Invalid classes primary key']},
code='invalid',
)
internal_data['classes'] = classes
return internal_data
class Meta:
model = Stream
fields = (
'pk',
'stream_name',
'classes',
'created_date',
)
With this solution you can use the same field name for both reading and writing, but the code is a bit messy.
Additional notes
You're using the related_name argument incorrectly, see this question. It's the other way around,
classes = models.ForeignKey(
Classes,
related_name='streams',
on_delete=models.CASCADE,
)
In this case it should be streams.
Kevin Languasco describes the behaviour of the create method quite well and his solutions are valid ones. I would add a variation to solution 1:
class StreamSerializer(serializers.ModelSerializer):
classes = ClassSerializer(read_only=True)
classes_id = serializers.IntegerField(write_only=True)
def create(self,validated_data):
return Stream.objects.create(**validated_data, classes=classes)
class Meta:
model = Stream
fields = (
'pk',
'stream_name',
'classes',
'classes_id',
'created_date',
)
The serializer will work without overriding the create method, but you can still do so if you want to as in your example.
Pass the value classes_id in the body of your POST method, not classes. When deserializing the data, the validation will skip classes and will check classes_id instead.
When serializing the data (when you perform a GET request, for example), classes will be used with your nested dictionary and classes_id will be omitted.
You can also solve this issue in such a way,
Serializer class
# Classes serializer
class ClassesSerializer(ModelSerializer):
class Meta:
model = Classes
fields = '__all__'
# Stream serializer
class StreamSerializer(ModelSerializer):
classes = ClassesSerializer(read_only=True)
class Meta:
model = Stream
fields = '__all__'
View
# Create Stream view
#api_view(['POST'])
def create_stream(request):
classes_id = request.data['classes'] # or however you are sending the id
serializer = StreamSerializer(data=request.data)
if serializer.is_valid():
classes_instance = get_object_or_404(Classes, id=classes_id)
serializer.save(classes=classes_instance)
else:
return Response(serializer.errors)
return Response(serializer.data)

How do you add existing objects to a ManyToMany field using Serializer?

I am trying to create an API with Artists and Songs, with a ManyToMany relationship between the two. Using the API to create a Song with an Artist that is not in the database works fine. The problem arises when I attempt to use the POST method to create a new Song with an Artist that already exists in the database. I tried overwriting the SongSerializer create() method using get_or_create() based on another post here, but I kept getting Bad Request errors when the Artist already exists in the database. The relevant code snippets:
models.py
class Artist(models.Model):
artist_name = models.CharField(max_length=200, unique=True)
class Meta:
ordering = ['artist_name']
def __str__(self):
return self.artist_name
class Song(models.Model):
song_title = models.CharField(max_length=200)
artists = models.ManyToManyField(Artist, related_name='songs')
class Meta:
ordering = ['song_title']
def __str__(self):
return self.song_title
serializers.py
class ArtistNameSerializer(serializers.ModelSerializer):
class Meta:
model = Artist
fields = ('artist_name',)
def to_representation(self, value):
return value.artist_name
class SongTitleSerializer(serializers.ModelSerializer):
songs = serializers.PrimaryKeyRelatedField(read_only=True, many=True)
def to_representation(self, value):
return value.song_title
class Meta:
model = Song
fields = ('songs',)
class ArtistSerializer(serializers.HyperlinkedModelSerializer):
songs = SongTitleSerializer(read_only=True, many=True)
class Meta:
model = Artist
fields = ('id', 'artist_name', 'songs')
class SongSerializer(serializers.HyperlinkedModelSerializer):
artists = ArtistNameSerializer(many=True)
class Meta:
model = Song
fields = ('id', 'song_title', 'artists',)
def create(self, validated_data):
artist_data = validated_data.pop('artists')
song = Song.objects.create(**validated_data)
song.save()
for artist_item in artist_data:
a, created = Artist.objects.get_or_create(artist_name=artist_item['artist_name'])
song.artists.add(a)
return song
I've done some tests and it looks like the program doesn't even go into the create() method I'm using, going straight to showing me the Bad Request error. What am I missing? Thanks in advance!
On you Artist model you have a constrain on the artist_model field (unique=True)
if you print the serializer in question with:
print(SongSerializer())
you get something like this:
SongSerializer():
id = IntegerField(label='ID', read_only=True)
song_title = CharField(max_length=200)
artists = ArtistNameSerializer(many=True):
artist_name = CharField(max_length=200, validators=[<UniqueValidator(queryset=Artist.objects.all())>])
under the artist_name field is a Validator "UniqueValidator"
so in case of a write operation you can disable the validator in the serializer with:
class ArtistNameSerializer(serializers.ModelSerializer):
class Meta:
model = models.Artist
fields = ('artist_name',)
extra_kwargs = {
'artist_name': {
'validators': [],
}
}
hope this help..

DRF: Serializer for heterogneous list of related models

Roughly said, I have the following schema in ORM:
class Page(models.Model):
title = models.CharField(max_length=255, null=False, blank=False)
#property
def content(self):
return [Video.objects.all()[0], Text.objects.all()[0], Video.objects.all()[1]]
and I have the following set of classes to support serialization for detailed view:
class ContentSerializer(serializers.ListSerializer):
class Meta:
model = ???
fields = '???'
class PageDetailSerializer(serializers.ModelSerializer):
content = ContentSerializer(many=True)
class Meta:
model = Page
fields = ('title', 'content', )
So I'm looking for a way to serialize that Page.content property - which is:
a list;
will contain heterogeneous data (combination of, let's say Video, Audio, Text and other models.
So I need somehow patch one of builtin serializers to iterate thru the list and check type of each object. And then decide how to serialize each one. E.g. I could prepare kind of dynamically created ModelSerializer with:
obj_type = type(obj)
class ContentModelSerializer(serializers.ModelSerializer):
class Meta:
model = obj_type
fields = '__all__'
serialized_obj = ContentModelSerializer(obj)
How could I implement that?
You can simply achieve this by overriding the to_representation method of Page serializer. like this:
class PageDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = ('title', 'content', )
def to_representation(self, instance):
ctx = super(PageDetailSerializer, self).to_representation(instance)
content = instance.content # property field of page, will return list of items
serialized_content = []
for c in content:
if type(c) == Video:
serialized_content.append({... serialized data of video type ..})
elif type(c) == ...
# other conditions here..
I had googled a lot before found the solution. This article has a reference to SerializerMethodField, which let you add custom handler for a field. And the final solution, which worked for me is:
class PageDetailSerializer(serializers.ModelSerializer):
_cache_serializers = {}
content = serializers.SerializerMethodField()
class Meta:
model = Page
fields = ('title', 'content', )
def _get_content_item_serializer(self, content_item_type):
if content_item_type not in self._cache_serializers:
class ContentItemSerializer(serializers.ModelSerializer):
class Meta:
model = content_item_type
exclude = ('id', 'page', )
self._cache_serializers[content_item_type] = ContentItemSerializer
return self._cache_serializers[content_item_type]
def get_content(self, page):
return [
self._get_content_item_serializer(type(content_item))(content_item).data for content_item in page.content
]

Django Rest Framework Custom Serializer Method

I'm trying to create a custom serializer method that counts the number of passed and failed quizzes from my QuizResults model. A failed quiz is under .7 and a passed quiz is .7 or over.
I want to be able to look into the Users QuizResult and count the number of passed quizzes(.7 or over). I would then duplicate the method to count the failed quizzes (under .7).
So far I don't have much idea on how to do so. I want to be able to grab the percent_correct field of the model and do a calculation and add it to a field in the serializer called "quiz_passed".
Here is my QuizResult model:
class QuizResult(models.Model):
quiz = models.ForeignKey(Quiz)
user = models.ForeignKey(User, related_name='quiz_parent')
percent_correct = models.FloatField(validators=[MinValueValidator(0.0), MaxValueValidator(1.0)])
date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return 'Quiz Results for : ' + self.quiz.title
Here is my serializer:
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
todo_count = serializers.IntegerField(source='todo_parent.count', read_only=True)
discussion_count = serializers.IntegerField(source='comment_parent.count', read_only=True)
quiz_passed = serializers.SerializerMethodField()
class Meta:
model = User
fields = ('todo_count', 'discussion_count', 'quiz_passed', 'username', )
def get_quiz_passed(self, obj):
return passed
Any help is appreciated.
Edit:
I extended the User model and added a model method like you suggested.
class Profile(User):
def get_quizzes_passed_count(self):
return self.quiz_parent.filter(percent_correct__gte=0.8).count()
I then added your suggestion to my ProfileSerializer.
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
todo_count = serializers.IntegerField(source='todo_parent.count', read_only=True)
discussion_count = serializers.IntegerField(source='comment_parent.count', read_only=True)
num_quizzes_passed = serializers.ReadOnlyField(source="get_quizzes_passed_count")
class Meta:
model = Profile
fields = ('todo_count', 'discussion_count', 'num_quizzes_passed', 'username')
Unfortunately when I add this nothing appears in the framework once these have been added. Any suggestions? Thank you.
You can use a model method on the user model to count that user's number of passed quizzes:
class User(models.model):
# rest of your User attributes
def get_quizzes_passed_count(self):
return self.quiz_parent.filter(percent_correct__gte=0.7).count()
Then add that to your serializer using a DRF ReadOnlyField to serialize that method:
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
todo_count = serializers.IntegerField(
source='todo_parent.count', read_only=True
)
discussion_count = serializers.IntegerField(
source='comment_parent.count', read_only=True
)
quiz_passed = serializers.SerializerMethodField()
num_quizzes_passed = serializers.ReadOnlyField(source="get_quizzes_passed_count")
class Meta:
model = User
fields = ('todo_count', 'discussion_count', 'quiz_passed', 'username', )
def get_quiz_passed(self, obj):
return passed
You can duplicate this for the number of failed quizzes.

Django Rest Framework: dynamic database on POST - RelatedField or PrimaryKeyRelatedField

I'm developing RESTFul services with DRF and I have multiple databases depending on the country (see my last question here)
I'm having a problem now with relationships, I have two models: Category and SubCategory:
class SubCategory(models.Model):
objects = CountryQuerySet.as_manager()
id = models.AutoField(primary_key=True,db_column='sub_category_id')
name = models.TextField()
female_items_in_category = models.BooleanField()
male_items_in_category = models.BooleanField()
kids_items_in_category = models.BooleanField()
category = models.ForeignKey('Category')
class Meta:
managed = True
db_table = Constants().SUBCATEGORY
And the serializer is:
class SubCategorySerializer(serializers.ModelSerializer):
category = PrimaryKeyRelatedField(queryset=Category.objects.using('es').all())
class Meta:
model = SubCategory
fields = ('id', 'name','female_items_in_category','male_items_in_category','kids_items_in_category','category')
If I don't set the queryset with the proper country it fails, because it doesn't know where to get the category.
Here the problem
I already set the country in the serializer context (in the ModelViewSet):
def get_serializer_context(self):
return {Constants().COUNTRY: self.kwargs.get(Constants().COUNTRY)}
But I can not find the proper way to get the self.context.get(Constants().COUNTRY) in the serializer.
Do you any have an idea to solve this? Thanks!
Well, I found a solution to my problem: I overwrite the method get_fields in the serializer:
def get_fields(self, *args, **kwargs):
fields = super(SubCategorySerializer, self).get_fields()
country = self.context.get(Constants().COUNTRY)
qs = Category.objects.using(country).all()
fields['category'].queryset = qs
return fields
And that works!

Categories

Resources