How to deal with nested serializer fields in Django Rest Framework? - python

I have nested serializer (AmountSerializer). I need a field meal_name in one ViewSet. But when this field is nested, I don't need it to be seen in endpoint(in MealSerializer). How to exclude field from nested serializer when is it actually nested?
models.py:
class MealType(models.Model):
name = models.TextField()
def __str__(self):
return self.name
class Ingredient(models.Model):
name = models.TextField()
def __str__(self):
return self.name
class Meal(models.Model):
name = models.TextField()
type = models.ForeignKey(MealType, on_delete=models.CASCADE, default=None)
recipe = models.TextField()
photo = models.ImageField(null=True, height_field=None, width_field=None, max_length=None,upload_to='media/')
ingredients = models.ManyToManyField(Ingredient)
def __str__(self):
return self.name
class IngredientAmount(models.Model):
ingredient_name = models.ForeignKey(Ingredient, on_delete=models.CASCADE, default=None)
amount = models.FloatField(default=None)
meal = models.ForeignKey(Meal, on_delete=models.CASCADE, default=None, related_name='meal_id')
class Meta:
ordering = ['meal']
def __str__(self):
return self.ingredient_name
serializers.py:
class AmountSerializer(serializers.ModelSerializer):
ingredient_name= serializers.ReadOnlyField(source='ingredient_name.name')
-->#meal_name = serializers.ReadOnlyField(source='meal.name')
#I CAN'T use ReadOnlyField( #with write_only=True)
#i trired use PrimaryKeyRelatedField
# butgot AssertionError: Relational field must provide a `queryset` argument, override `get_queryset`, or set read_only=`True`.
class Meta:
model = IngredientAmount
fields = ('ingredient_name','amount','meal_name')
class MealSerializer(serializers.ModelSerializer):
type_name= serializers.ReadOnlyField(source='type.name')
ingredients = serializers.SlugRelatedField(read_only=True, slug_field='name', many=True)
amount = AmountSerializer(read_only=True, many=True,source='meal_id')
class Meta:
model = Meal
fields = ('id', 'name', 'type_name', 'recipe', 'photo', 'ingredients','amount')

I'd rather use a trick to exclude some of the fields that are not needed in certain situations. You can inherit your serializer from ExcludeFieldsModelSerializer, and exclude any fields that you want so that the serializer will not serialize that field.
class ExcludeFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `exclude_fields` argument that
controls which fields should be excluded from the serializer.
Plagiarised from https://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'exclude_fields' arg up to the superclass
exclude_fields = kwargs.pop('exclude_fields', None)
# Instantiate the superclass normally
super(ExcludeFieldsModelSerializer, self).__init__(*args, **kwargs)
if exclude_fields is not None:
# Drop any fields that are specified in the `exclude_fields` argument.
drop = set(exclude_fields)
for field_name in drop:
self.fields.pop(field_name)
class AmountSerializer(ExcludeFieldsModelSerializer):
ingredient_name= serializers.ReadOnlyField(source='ingredient_name.name')
meal_name = serializers.CharField(read_only=True, source='meal.name')
class Meta:
model = IngredientAmount
fields = ('ingredient_name','amount','meal_name')
class MealSerializer(serializers.ModelSerializer):
type_name= serializers.ReadOnlyField(source='type.name')
ingredients = serializers.SlugRelatedField(read_only=True, slug_field='name', many=True)
amount = AmountSerializer(read_only=True, many=True, source='meal_id', exclude_fields={'meal_name'})
class Meta:
model = Meal
fields = ('id', 'name', 'type_name', 'recipe', 'photo', 'ingredients','amount')

Related

Django rest framework - using SerializerMethodField with ModelSerializer

I have the following models.
models.py
class Language(models.Model):
name = models.CharField(max_length=255, unique=True)
class Subject(models.Model):
name = models.CharField(max_length=255, unique=True)
class Term(models.Model):
subject = models.ForeignKey(Subject, on_delete=models.CASCADE)
language = models.ForeignKey(Language, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
definition = models.TextField()
image = models.ImageField(default='', blank=True)
I want to write a Serializer that will return a list of subjects. Each subject has a term field, which contains None or a single object, depending on the condition.
serializers.py
class TermSerializer(serializers.ModelSerializer):
language = serializers.CharField(source='language.name')
def to_representation(self, data):
data = data.get(language=self.context['language'])
return super(TermSerializer, self).to_representation(data)
class Meta:
model = Term
fields = ('id', 'language', 'name', 'definition', 'image')
class FilteredSubjectSerializer(serializers.ListSerializer):
def to_representation(self, data):
if self.context['condition']:
terms = Term.objects.filter(language=self.context['language'])
data = data.filter(term__in=terms)
return super(FilteredSubjectSerializer, self).to_representation(data)
class SubjectSerializer(serializers.ModelSerializer):
term = serializers.SerializerMethodField()
def get_term(self, term):
if self.context['condition']:
return TermSerializer(many=False, source='term_set').data
return None
class Meta:
model = Term
list_serializer_class = FilteredSubjectSerializer
fields = ('id', 'name', 'definition', 'image', 'term')
The problem is that when condition == True, the ViewSet returns incorrect data. All fields inside term have a default value.
It works fine if I write SubjectSerializer like this:
class SubjectSerializer(serializers.ModelSerializer):
term = serializers.TermSerializer(many=False, source='term_set')
class Meta:
model = Term
list_serializer_class = FilteredSubjectSerializer
fields = ('id', 'name', 'definition', 'image', 'term')
But the case when condition == False doesn't work.
Try passing the serializer the instance and the context.
Like this:
def get_term(self, instance):
if self.context['condition']:
return TermSerializer(many=False, source='term_set', instance=instance, context=self.context).data
return None

ManyToManyField does not show

Want to use REST API to populate my tables but my field does not display on the API page.
Models (Series, Roster):
class Series(models.Model):
(...)
def __str__(self):
return self.title
class Roster(models.Model):
(...)
series = models.ManyToManyField(Series)
(...)
def __str__(self):
return self.name
Serializers:
class SeriesSerializer(serializers.ModelSerializer):
class Meta:
model = Series
fields = ('id', 'title', 'icon')
read_only_fields = ('slug',)
class RosterSerializer(serializers.ModelSerializer):
series = SeriesSerializer(many=True, read_only=True)
class Meta:
model = Roster
fields = ('id', 'name', 'number', 'primary_color', 'secondary_color', 'image', 'series')
Views:
class SeriesView(viewsets.ModelViewSet):
serializer_class = SeriesSerializer
queryset = Series.objects.all()
class RosterView(viewsets.ModelViewSet):
serializer_class = RosterSerializer
queryset = Roster.objects.all()
Unsure where I am mistepping here.
So it turns out that all I needed to do was remove
series = SeriesSerializer(many=True, read_only=True)
and adjust my series field to
series = models.ForeignKey(Series, on_delete=models.CASCADE, blank=True, null=True)
No idea why this ended up working though so an explanation would still be cool.

How to serialize each foreign key object with different serializer class in django rest framework

So I'm wondering if it is possible to serialize each foreign key object with different serializer in django rest framework.
What I mean is:
I have my models like
class KingdomModel(models.Model):
kingdom_name = models.CharField(max_length=32)
owner = models.OneToOneField(User, on_delete=models.CASCADE)
faction = models.CharField(max_length=10)
class CityModel(models.Model):
kingdom = models.ForeignKey(KingdomModel, on_delete=models.CASCADE, related_name="cities")
city_name = models.CharField(max_length=32)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
"""
... other fields aswell
"""
class ArmyModel(models.Model):
home_city = models.ForeignKey(CityModel, on_delete=models.CASCADE, null=True, related_name="own_troops")
current_city = models.ForeignKey(CityModel, on_delete=models.CASCADE, null=True, related_name="all_troops", blank=True)
status = models.CharField(max_length=32)
action_done_time = models.DateTimeField(default=None, null=True, blank=True)
target_city = models.ForeignKey(CityModel, on_delete=models.CASCADE, null=True, related_name="incoming_troops", default=None, blank=True)
# Shared troops
settlers = models.IntegerField(default=0)
# Gaul troops
pikemen = models.IntegerField(default=0)
swordmen = models.IntegerField(default=0)
riders = models.IntegerField(default=0)
# Roman troops
legionaries = models.IntegerField(default=0)
praetorian = models.IntegerField(default=0)
And I am trying to serialize the armies based on the kingdoms faction. Which works fine when talking about own_troops because they are always going to be serialized with the same serializer, like so.
class CitySerializer(serializers.ModelSerializer):
own_troops = serializers.SerializerMethodField()
incoming_troops = serializers.SerializerMethodField()
def get_own_troops(self, city_obj):
if(KingdomModel.objects.get(owner=city_obj.owner).faction == "Gaul"):
return GaulTroopsSerializer(instance=city_obj.own_troops, context=self.context, many=True, required=False, read_only=False).data
elif(KingdomModel.objects.get(owner=city_obj.owner).faction == "Roman"):
return RomanTroopsSerializer(instance=city_obj.own_troops, context=self.context, many=True, required=False, read_only=False).data
class RomanTroopsSerializer(serializers.ModelSerializer):
class Meta:
model = ArmyModel
fields = ['id', 'home_city', 'current_city', 'target_city', 'status', 'action_done_time', 'settlers', 'legionaries', 'praetorian']
class GaulTroopsSerializer(serializers.ModelSerializer):
class Meta:
model = ArmyModel
fields = ['id', 'home_city', 'current_city', 'target_city', 'status', 'action_done_time', 'settlers', 'pikemen', 'swordmen', 'riders']
But if I try to apply the same logic to serializing the incoming_troops, it will always serialize all of the objects in the list with the first serializer. This was my hopeless attempt at serializing each foreign key with different serializer based on the data inside the relation.
def get_incoming_troops(self, city_obj):
for data in GaulTroopsSerializer(instance=city_obj.incoming_troops, context=self.context, many=True, required=False, read_only=False).data:
print(data)
home_city_obj = CityModel.objects.get(id=data['home_city'])
if(KingdomModel.objects.get(owner=home_city_obj.owner).faction == "Gaul"):
return GaulTroopsSerializer(instance=city_obj.incoming_troops, context=self.context, many=True, required=False, read_only=False).data
else:
return RomanTroopsSerializer(instance=city_obj.incoming_troops, context=self.context, many=True, required=False, read_only=False).data
class Meta:
model = CityModel
fields = ['id', 'owner', 'city_name', 'x_coordinate', 'y_coordinate', 'last_updated', 'max_warehouse_capacity', 'max_grain_silo_capacity', 'wood_ammount', 'wheat_ammount', 'stone_ammount', 'iron_ammount', 'resource_fields', 'buildings','incoming_troops', 'own_troops', 'all_troops']
read_only_fields = ['id', 'max_warehouse_capacity', 'max_grain_silo_capacity']
I know I could just have multiple models for all of the different factions armies, but for now I am just wondering if this is possible in django / drf?
Answering my own question because I got it working and what I did is the following:
First of all I scraped the multiple troop serializers. And had just one army serializer where I switch the fields according to the faction.
This is my ArmySerializer now
class ArmySerializer(serializers.ModelSerializer):
class Meta:
model = ArmyModel
fields = ['id', 'home_city', 'current_city', 'target_city', 'status', 'action_done_time']
def to_representation(self, instance):
try:
del self.fields # Clear the cache
except AttributeError:
pass
if("faction" in self.context and len(self.context['faction']) > 0):
print(self.context['faction'])
self.fields['settlers'] = serializers.IntegerField()
if(self.context['faction'][0] == "Gaul"):
self.fields['pikemen'] = serializers.IntegerField()
self.fields['swordmen'] = serializers.IntegerField()
self.fields['riders'] = serializers.IntegerField()
elif(self.context['faction'][0] == "Roman"):
self.fields['legionaries'] = serializers.IntegerField()
self.fields['praetorian'] = serializers.IntegerField()
if(len(self.context['faction']) > 1):
self.context['faction'].pop(0)
return super().to_representation(instance)
And in CitySerializer I am populating the context['faction'] list like this:
class CitySerializer(serializers.ModelSerializer):
own_troops = serializers.SerializerMethodField()
incoming_troops = serializers.SerializerMethodField()
def get_own_troops(self, instance):
if(KingdomModel.objects.get(owner=instance.owner).faction == "Gaul"):
self.context["faction"] = ["Gaul"]
return ArmySerializer(instance=instance.own_troops, context=self.context, many=True, required=False, read_only=False).data
elif(KingdomModel.objects.get(owner=instance.owner).faction == "Roman"):
self.context["faction"] = ["Roman"]
return ArmySerializer(instance=instance.own_troops, context=self.context, many=True, required=False, read_only=False).data
def get_incoming_troops(self, city_obj):
self.context['faction'] = []
for data in ArmySerializer(instance=city_obj.incoming_troops, context=self.context, many=True, required=False, read_only=False).data:
home_city = CityModel.objects.get(id=data['home_city'])
sender_faction = KingdomModel.objects.get(owner=home_city.owner).faction
if(sender_faction == "Gaul"):
self.context['faction'] += ["Gaul"]
else:
self.context['faction'] += ["Roman"]
return ArmySerializer(instance=city_obj.incoming_troops, context=self.context, many=True, required=False, read_only=False).data
Also it should be said that, this introduced a new problem when creating an army with POST requests. The fields that are dynamically added in the to_representation method are not validated by default, so they are not present in validated_data. There might be a way to override validation but I just took them from the raw request data for now and it seems to be working fine.

How to serialize only certain elements of M2M set of the object being serialized?

So I have these models:
class Group(models.Model):
name = models.CharField(max_length=255, blank=True, null=True, default=None)
class Member(models.Model):
name = models.CharField(max_length=255, blank=True, null=True, default=None)
group = models.ForeignKey(Group, related_name='members', on_delete=models.CASCADE)
is_deleted = models.BooleanField()
And these serializers:
class GroupSerializer(serializers.ModelSerializer):
members = MemberSerializer(many=True, read_only=True)
class Meta:
model = Group
fields = '__all__'
class MemberSerializer(serializers.ModelSerializer):
# some field defintions here
class Meta:
model = Member
fields = '__all__'
And view:
class GroupViewSet(viewsets.ModelViewSet):
queryset = Group.objects.all()
serializer_class = GroupSerializer
Now what I want to do basically, is not return "members" of Group that has is_deleted set to True.
So if I have a Group with 3 Member where 1 has is_deleted set to True, I want my serializer to return the Group with 2 Members.
How do I go about achieving this?
To achieve this you can override the members field in the GroupSerializer to return a filtered queryset
class GroupSerializer(serializers.ModelSerializer):
members = serializers.SerializerMethodField()
def get_members(self, obj):
qs = Members.objects.filter(group=obj, is_deleted=True)
serialized = MemberSerializer(qs, many=True)
return serialized.data
class Meta:
model = Group
fields = '__all__'
This will return the member objects with is_deleted=True for the specific group.
Use serializer method field in members serializer
https://www.django-rest-framework.org/api-guide/fields/#serializermethodfield
You can modify the MemberSerializer which only return the is_deleted false field.
class MemberSerializer(serializers.ModelSerializer):
is_deleted_false = = serializers.SerializerMethodField(read_only=True)
#staticmethod
def get_is_deleted_false(obj):
if obj.is_deleted == False:
return obj.is_deleted
else:
pass
class Meta:
model = Member
fields = ('id', 'name', 'group', 'is_deleted_false')

Queryset is returned inside children key

I wanted to list all the reviews along with the replies to specific review of specific restaurant but i could only list the review. While trying to fetch all the replies to its specific review, i called the children() which is defined in Review model. It returned queryset. On the other hand i want all the specific replies with the name of replier.
serializers.py
class ReviewSeraializer(ModelSerializer):
reply_count = SerializerMethodField()
children = SerializerMethodField()
class Meta:
model = Review
read_only = ('id',)
fields = ('id','content_type','object_id','parent','review','children','reply_count','created')
def get_reply_count(self, obj):
if obj.is_parent:
return obj.children().count()
return 0
def get_children(self, obj):
obj_children = []
if obj.is_parent:
return str(obj.children())
# for obj in obj.children():
# print(obj.review)
# obj_children.append(obj.review)
# return str(obj_children)
return None
review/models.py
class Review(models.Model):
reviewer = models.ForeignKey(User, null=True)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
parent = models.ForeignKey("self", null=True, blank=True, related_name="parent_review")
review = models.TextField()
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
objects = ReviewManager()
def children(self): # replies
return Review.objects.filter(parent=self)
#property
def is_parent(self):
if self.parent is not None:
return False
return True
How to list all the replies and replier name inside children key of a specific review?
Add another serializer that can serialize your children.
class ReviewSeraializerChild(serializers.ModelSerializer):
class Meta:
model = Review
class ReviewSeraializer(serializers.ModelSerializer):
reply_count = serializers.SerializerMethodField()
children = ReviewSeraializerChild(many=True)
class Meta:
model = Review
read_only = ('id',)
fields = ('id', 'content_type', 'object_id', 'parent', 'review', 'children', 'reply_count', 'created')
def get_reply_count(self, obj):
if obj.is_parent:
return obj.children().count()
return 0
But you must write your custom create() and update() functions for nested fields.

Categories