I'm looking into "nested-nested" serializers, where there's a nested serializer nested in another one. It's a cooking service where a recipe has multiple directions, each having multiple ingredients. I set that up with foreign keys.
Here are my models:
class Category(models.Model):
"""Model representing a recipe category"""
name = models.CharField(max_length=200, help_text="Enter a recipe category (e.g Baking)")
def __str__(self):
"""String for representing the Model object."""
return self.name
class Ingredient(models.Model):
"""Model representing an ingredient in a direction"""
name = models.CharField(max_length=250, help_text="The ingredient's name")
quantity = models.CharField(max_length=200, help_text="How much of this ingredient.")
direction = models.ForeignKey("Direction", help_text="This ingredient's direction", on_delete=models.CASCADE, related_name='ingredients')
def __str__(self):
"""String for representing this recipe ingredient"""
return f'{self.quantity} {self.ingredient}'
class Direction(models.Model):
"""Model representing a step in a recipe"""
title=models.CharField(max_length=200)
text=models.TextField(blank=True, help_text="Describe this step.")
recipe=models.ForeignKey("Recipe", help_text="This direction's recipe", on_delete=models.CASCADE, related_name='directions')
def __str__(self):
"""String for representing the Direction"""
return self.title
class Recipe(models.Model):
"""Model representing a recipe."""
title = models.CharField(max_length=200)
notes = models.TextField(max_length=1000, help_text="Enter notes, reviews, ...")
photos = models.ImageField(null=True, blank=True)
category = models.ForeignKey(Category, help_text="This recipe's category", on_delete=models.CASCADE, related_name='recipes')
def __str__(self):
"""String for representing the Model object."""
return self.title
def get_absolute_url(self):
"""Returns the url to access a detail record for this recipe."""
I read the docs and thought I could just use a nested serializer in another, which threw me an error stating "direct assignment to the reverse side of a related set is prohibited."
I then made it work by using these serializers:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('name',)
def create(self, validated_data):
category = Category.objects.create(**validated_data)
return category
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingredient
fields = ('quantity', 'name',)
def create(self, validated_data):
ingredient = Ingredient.objects.create(**validated_data)
return ingredient
def update(self, instance, validated_data):
instance.quantity = validated_data.get('quantity', instance.quantity)
instance.name = validated_data.get('name', instance.name)
instance.direction = validated_data.get('direction', instance.direction)
instance.save()
return instance
class DirectionSerializer(serializers.ModelSerializer):
ingredients = IngredientSerializer(many=True)
class Meta:
model = Direction
fields = ('title', 'text', 'ingredients', )
def create(self, validated_data):
direction = Direction.objects.create(**validated_data)
return direction
def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
instance.text = validated_data.get('text', instance.text)
instance.recipe = validated_data.get('recipe', instance.recipe)
instance.save()
return instance
class RecipeSerializer(serializers.ModelSerializer):
directions = DirectionSerializer(many=True)
category = CategorySerializer()
class Meta:
model = Recipe
fields = ('title', 'notes', 'photos', 'category', 'directions')
def create(self, validated_data):
directions_data = validated_data.pop('directions')
category_data = validated_data.pop('category')
category = Category.objects.create(**category_data)
validated_data["category"] = category
recipe = Recipe.objects.create(**validated_data)
recipe.category = category
for direction_data in directions_data:
ingredients_data = direction_data.pop("ingredients")
direction = Direction.objects.create(recipe=recipe, **direction_data)
for ingredient_data in ingredients_data:
Ingredient.objects.create(direction=direction, **ingredient_data)
return recipe
def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
instance.notes = validated_data.get('notes', instance.notes)
instance.photos = validated_data.get('photos', instance.photos)
instance.category = validated_data.get('category', instance.category)
instance.save()
return instance
I am not sure, what of this is actually necessary at this point. Basically I create the nested structure in the create() function of the recipe.
Now to the real questions:
This does feel like a hack. Is is this correct and intended way of achieving multiple nested serializers?
If I do it this way, do I even the need create() and update() functions?
Thank you very much.
Related
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')
Id like to know if there is a way to display on my admin section the number of party that an author have in the Author admin section call num_party ? To me i have to loop through the model and count how many party an author has, it's seems easy to say but not easy to do, so someone could help me ?
Here is my Party Model:
class Party(models.Model):
title = models.CharField(max_length=200)
place = models.ForeignKey('Place', on_delete=models.CASCADE, null=True)
author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True)
date = models.DateField(null=True, blank=True)
genre = models.ManyToManyField(Genre, help_text='Select a genre for this partty')
ticket_available = models.IntegerField()
ticket_price = models.PositiveIntegerField(default=5)
language = models.ManyToManyField('Language')
insider_secret = models.ManyToManyField('InsiderSecret')
#benef = models.IntegerField(default=5)
def get_benef(self):
return self.ticket_available * self.ticket_price
benef = property(get_benef)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('book-detail', args=[str(self.id)])
def display_genre(self):
return ', '.join(genre.name for genre in self.genre.all()[:3])
display_genre.short_description = 'Genre'
def display_secret(self):
return ', '.join(insider_secret.secret for insider_secret in self.insider_secret.all()[:3])
display_secret.short_description = 'Insider Secret'
class Author(models.Model):
"""Model representing an author."""
username = models.CharField(max_length=100, help_text="How do you want to be call by people as organisor ? ", default="Bestpartyorganisorintown")
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
date_of_birth = models.DateField(null=True, blank=True)
class Meta:
ordering = ['last_name', 'first_name']
def get_absolute_url(self):
"""Returns the url to access a particular author instance."""
return reverse('author-detail', args=[str(self.id)])
def __str__(self):
"""String for representing the Model object."""
return f'{self.username}'
Here is my admin code:
#admin.register(Party)
class PartyAdmin(admin.ModelAdmin):
list_display = ('title', 'author', 'display_genre', 'ticket_price','ticket_available','display_secret','benef')
readonly_fields = ('benef',)
class AuthorAdmin(admin.ModelAdmin):
list_display = ('username', 'first_name', 'date_of_birth')
fields = [('first_name', 'last_name'),'username', 'date_of_birth']
# Register the admin class with the associated model
admin.site.register(Author, AuthorAdmin)
class InsiderSecretAdmin(admin.ModelAdmin):
list_display = ('secret',)
You can add custom methods to list_display like this:
class AuthorAdmin(admin.ModelAdmin):
list_display = ('username', 'first_name', 'date_of_birth', 'party_count')
fields = [('first_name', 'last_name'),'username', 'date_of_birth']
def party_count(obj):
return obj.party_set.count()
However this will make a count query for every author object. Instead you can override the default queryset and annotate the count:
class AuthorAdmin(admin.ModelAdmin):
list_display = ('username', 'first_name', 'date_of_birth', 'party_count')
fields = [('first_name', 'last_name'),'username', 'date_of_birth']
def get_queryset(self, request):
qs = super(AuthorAdmin, self).get_queryset(request)
return qs.annotate(party_count=Count('party_set'))
def party_count(obj):
return obj.party_count
i tried what you did, and now it display me 1 for all my Authors, i don't really understand what i did wrong. Id like it to display me the num of party for each individual user, someone can help ?
here is the code
class AuthorAdmin(admin.ModelAdmin):
list_display = ('username', 'first_name', 'date_of_birth','party_count')
fields = [('first_name', 'last_name'),'username', 'date_of_birth']
def get_queryset(self, request):
return Author.objects.annotate(party_count=Count('party'))
def party_count(self, Author):
return Author.party_count
def __str__(self):
return self.party_count
[enter image description here][1]
[1]: https://i.stack.imgur.com/PVhYQ.png
I am trying to create a detail-list of my actor where it will show all the shows he has been a cast member of. This is part of mozilla's challenge yourself section at the end of the tutorial.
I am having trouble filtering my class Cast so that I can get a specific Actor.
I do not understand why the filter is not working. If self.object has a value of '3' for example, it should be filtering out all of the actors and only display the actor with the id of 3. But that does not seem to be the case. I also am not understanding the error code it is tossing out. My Cast class does have a foreignkey to person.
Similar to my show details page, instead of casts, I want it to be the actor's starred in movies.
Image of my Show Details Page
View
class ActorDetailView(generic.DetailView):
model = Cast
template_name = 'show/actor-detail.html'
def get_context_data(self, **kwargs):
context = super(ActorDetailView, self).get_context_data(**kwargs)
context['casts'] = Cast.objects.filter(person_id=self.object)
return context
Models
class Person(models.Model):
name = models.CharField(max_length=128)
def __str__(self):
return self.name
class Character(models.Model):
name = models.CharField(max_length=128)
on_which_show = models.ForeignKey('Show', on_delete=models.SET_NULL, null=True)
def __str__(self):
return self.name
class Cast(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
cast_show = models.ForeignKey('Show',on_delete=models.CASCADE)
character = models.ForeignKey(Character, on_delete=models.CASCADE, null=True)
def get_absolute_url(self):
return reverse('actor-detail', args=[str(self.id)])
class Show(models.Model):
title = models.CharField(max_length=200)
genre = models.ManyToManyField(Genre, help_text='Select a genre for this book')
language = models.ForeignKey('Language', on_delete=models.SET_NULL, null=True)
summary = models.TextField(max_length=1000, help_text='Enter a brief description of the show', null=True)
cast_of_the_show = models.ManyToManyField(Person,through='Cast')
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('show-detail', args=[str(self.id)])
The model for your ActorDetailView should be Person instead of Cast. Then you can use the person record to get all of the casts they belong to.
class ActorDetailView(generic.DetailView):
model = Person
template_name = 'show/actor-detail.html'
def get_context_data(self, **kwargs):
context = super(ActorDetailView, self).get_context_data(**kwargs)
context['casts'] = Cast.objects.filter(person=self.object)
return context
I have two models related through a one to one field, and I want to create a REST services that manages them both as they where one. The post works perfectly so far, and new instances are created in both models as they where one, but the put method just does nothing. It raises no error or anything, it just leaves the data unchanged.
These are my models:
class Company(models.Model):
name = models.CharField(max_length=40)
legal_name = models.CharField(max_length=100)
url = models.URLField()
address = models.TextField(max_length=400)
def __str__(self):
return "[{}]{}".format(self.id, self.name)
class Hotel(models.Model):
company = models.OneToOneField('Company', on_delete=models.CASCADE, primary_key=True)
code = models.CharField(max_length=10)
city = models.ForeignKey('City', on_delete=models.PROTECT, related_name='city_hotels')
category = models.ForeignKey('HotelCategory', on_delete=models.PROTECT, related_name='category_hotels')
capacity = models.IntegerField()
position = models.DecimalField(max_digits=11, decimal_places=2, default=0.00)
in_pickup = models.BooleanField(default=False)
def __str__(self):
return self.company.name
This is my ViewSet:
class HotelViewSet(viewsets.ModelViewSet):
queryset = models.Hotel.objects.all()
serializer_class = serializers.HotelSerializer
These are my serializers:
class CompanySerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = models.Company
fields = ('id', 'name', 'legal_name', 'url', 'address')
class HotelSerializer(serializers.HyperlinkedModelSerializer):
company = CompanySerializer()
class Meta:
model = models.Hotel
fields = ('company', 'code', 'city', 'category', 'capacity', 'position', 'in_pickup')
def create(self, validated_data):
company_data = validated_data.pop('company')
new_company=models.Company.objects.create(**company_data)
hotel = models.Hotel.objects.create(company=new_company, **validated_data)
return hotel
def update(self, instance, validated_data):
company = models.Company(
id=instance.company.id,
name=instance.company.name,
legal_name=instance.company.legal_name,
url=instance.company.url,
address=instance.company.address
)
company.save()
instance.save()
return instance
I found that instance carries the original data, and validated_data carries the new data. I was saving the original data back.
I had to replace instance data with validated_data data and then save instance:
def update(self, instance, validated_data):
company_data = validated_data.pop('company')
company = models.Company.objects.get(id=instance.company.id)
company.name = company_data.get('name')
company.legal_name = company_data.get('legal_name')
company.tax_id = company_data.get('tax_id')
company.url = company_data.get('url')
company.address = company_data.get('address')
instance.company = company
instance.code = validated_data.get('code', instance.code)
instance.city = validated_data.get('city', instance.city)
instance.category = validated_data.get('category', instance.category)
instance.capacity = validated_data.get('capacity', instance.capacity)
instance.position = validated_data.get('position', instance.position)
instance.in_pickup = validated_data.get('in_pickup', instance.in_pickup)
instance.is_active = validated_data.get('is_active', instance.is_active)
company.save()
instance.save()
return instance
put method works handsomely now.
You are creating a new instance of company instead of updating the one that belongs to the Hotel instance.
company = Company.objects.get(id=instance.company.id)
company.name = instance.company.name
company.legal_name = instance.company.name
company.url = instance.company.url
company.address = instance.company.address
company.save()
instance.save()
I have tried all the solutions. Still cannot resolve it. Here are the codes.
models.py
class Car(models.Model):
car_name = models.CharField(max_length=250)
car_description = models.CharField(max_length=250)
def __str__(self):
return self.car_name + ' - ' + str(self.pk)
class Owners(models.Model):
car = models.ForeignKey(Car, on_delete=models.CASCADE, default=0)
owner_name = models.CharField(max_length=250)
owner_desc = models.CharField(max_length=250)
def get_absolute_url(self):
return reverse('appname:index')
def __str__(self):
return self.owner_name + ' - ' + self.owner_desc
serializers.py
class OwnersSerializer(serializers.ModelSerializer):
class Meta:
model = Owners
fields = '__all__'
class CarSerializer(serializers.ModelSerializer):
owners = OwnersSerializer(many=True, read_only=True)
class Meta:
model = Car
fields = '__all__'
views.py
class CarList(APIView):
def get(self, request):
cars = Car.objects.all()
serializer = CarSerializer(cars, many=True)
return Response(serializer.data)
def post(self):
pass
I can't get to view all the 'Owner' objects related to a certain object of the 'Car' class.
You need to define a related name on the ForeignKey to create the reverse reference.
class Owners(models.Model):
car = models.ForeignKey(Car, on_delete=models.CASCADE, default=0, related_name='owners')