Related
So I've built an API for movies dataset which contain following structure:
Models.py
class Directors(models.Model):
id = models.IntegerField(primary_key=True)
first_name = models.CharField(max_length=100, blank=True, null=True)
last_name = models.CharField(max_length=100, blank=True, null=True)
class Meta:
db_table = 'directors'
ordering = ['-id']
class Movies(models.Model):
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=100, blank=True, null=True)
year = models.IntegerField(blank=True, null=True)
rank = models.FloatField(blank=True, null=True)
class Meta:
db_table = 'movies'
ordering = ['-id']
class Actors(models.Model):
id = models.IntegerField(primary_key=True)
first_name = models.CharField(max_length=100, blank=True, null=True)
last_name = models.CharField(max_length=100, blank=True, null=True)
gender = models.CharField(max_length=20, blank=True, null=True)
class Meta:
db_table = 'actors'
ordering = ['-id']
class DirectorsGenres(models.Model):
director = models.ForeignKey(Directors,on_delete=models.CASCADE,related_name='directors_genres')
genre = models.CharField(max_length=100, blank=True, null=True)
prob = models.FloatField(blank=True, null=True)
class Meta:
db_table = 'directors_genres'
ordering = ['-director']
class MoviesDirectors(models.Model):
director = models.ForeignKey(Directors,on_delete=models.CASCADE,related_name='movies_directors')
movie = models.ForeignKey(Movies,on_delete=models.CASCADE,related_name='movies_directors')
class Meta:
db_table = 'movies_directors'
ordering = ['-director']
class MoviesGenres(models.Model):
movie = models.ForeignKey(Movies,on_delete=models.CASCADE,related_name='movies_genres')
genre = models.CharField(max_length=100, blank=True, null=True)
class Meta:
db_table = 'movies_genres'
ordering = ['-movie']
class Roles(models.Model):
actor = models.ForeignKey(Actors,on_delete=models.CASCADE,related_name='roles')
movie = models.ForeignKey(Movies,on_delete=models.CASCADE,related_name='roles')
role = models.CharField(max_length=100, blank=True, null=True)
class Meta:
db_table = 'roles'
ordering = ['-actor']
urls.py
from django.urls import path, include
from . import views
from api.views import getMovies, getGenres, getActors
urlpatterns = [
path('', views.getRoutes),
path('movies/', getMovies.as_view(), name='movies'),
path('movies/genres/', getGenres.as_view(), name='genres'),
path('actor_stats/<pk>', getActors.as_view(), name='actor_stats'),
]
serializer.py
from rest_framework import serializers
from movies.models import *
class MoviesSerializer(serializers.ModelSerializer):
class Meta:
model = Movies
fields = '__all__'
class DirectorsSerializer(serializers.ModelSerializer):
class Meta:
model = Directors
fields = '__all__'
class ActorsSerializer(serializers.ModelSerializer):
class Meta:
model = Actors
fields = '__all__'
class DirectorsGenresSerializer(serializers.ModelSerializer):
class Meta:
model = DirectorsGenres
fields = '__all__'
class MoviesDirectorsSerializer(serializers.ModelSerializer):
movie = MoviesSerializer(many = False)
director = DirectorsSerializer(many = False)
class Meta:
model = MoviesDirectors
fields = '__all__'
class MoviesGenresSerializer(serializers.ModelSerializer):
movie = MoviesSerializer(many = False)
class Meta:
model = MoviesGenres
fields = '__all__'
class RolesSerializer(serializers.ModelSerializer):
movie = MoviesSerializer(many = False)
actor = ActorsSerializer(many = False)
class Meta:
model = Roles
fields = '__all__'
views.py
class getMovies(ListAPIView):
directors = Directors.objects.all()
queryset = MoviesDirectors.objects.filter(director__in=directors)
serializer_class = MoviesDirectorsSerializer
pagination_class = CustomPagination
filter_backends = [DjangoFilterBackend]
filterset_fields = ['director__first_name', 'director__last_name']
class getGenres(ListAPIView):
movies = Movies.objects.all()
queryset = MoviesGenres.objects.filter(movie__in=movies).order_by('-genre')
serializer_class = MoviesGenresSerializer
pagination_class = CustomPagination
filter_backends = [DjangoFilterBackend]
filterset_fields = ['genre']
class getActors(ListAPIView):
queryset = Roles.objects.all()
serializer_class = RolesSerializer
pagination_class = CustomPagination
def get_queryset(self):
return super().get_queryset().filter(
actor_id=self.kwargs['pk']
)
Now I want to count number of movies by genre that actor with specific pk played in getActors class.
Like the number of movies by genre that actor participated in. E.g. Drama: 2, Horror: 3
Right now I am getting the overall count of movies count: 2:
GET /api/actor_stats/17
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": 800480,
"movie": {
"id": 105231,
"name": "Everybody's Business",
"year": 1993,
"rank": null
},
"actor": {
"id": 17,
"first_name": "Luis Roberto",
"last_name": "Formiga",
"gender": "M"
},
"role": "Grandfather"
},
{
"id": 800481,
"movie": {
"id": 242453,
"name": "OP Pro 88 - Barra Rio",
"year": 1988,
"rank": null
},
"actor": {
"id": 17,
"first_name": "Luis Roberto",
"last_name": "Formiga",
"gender": "M"
},
"role": "Himself"
}
]
}
What is the optimized way of achieving the following:
number_of_movies_by_genre
Drama: 2
Horror: 3
UPDATE
class RolesSerializer(serializers.Serializer):
id = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
top_genre = serializers.SerializerMethodField()
number_of_movies = serializers.SerializerMethodField()
number_of_movies_by_genre = serializers.SerializerMethodField()
most_frequent_partner = serializers.SerializerMethodField()
class Meta:
model = Roles
fields = '__all__'
def get_id(self, obj):
return obj.actor.id
def get_name(self, obj):
return f'{obj.actor.first_name} {obj.actor.last_name}'
def get_top_genre(self, obj):
number_by_genre = Roles.objects.filter(actor = obj.actor.id
).values('movie__movies_genres__genre').annotate(
genre = F('movie__movies_genres__genre'),
number_of_movies=Count('movie__movies_genres__genre'),
)
data = [s['number_of_movies'] for s in number_by_genre]
highest = max(data)
result = [s for s in data if s == highest]
return result
def get_number_of_movies(self, obj):
number_of_movies = Roles.objects.filter(actor = obj.actor.id
).values('movie__name').count()
return number_of_movies
def get_number_of_movies_by_genre(self, obj):
number_of_movies_by_genre = Roles.objects.filter(actor = obj.actor.id
).values('movie__movies_genres__genre').annotate(
genre=F('movie__movies_genres__genre'),
number_of_movies=Count('movie__movies_genres__genre'),
).values('genre', 'number_of_movies')
return number_of_movies_by_genre
def get_most_frequent_partner(self, obj):
partners = Roles.objects.filter(actor = obj.actor.id
).values('movie__id')
result = Roles.objects.filter(movie__in = partners
).values('actor').exclude(actor=obj.actor.id).annotate(
partner_actor_id = F('actor'),
partner_actor_name = Concat(F('actor__first_name'), Value(' '), F('actor__last_name')),
number_of_shared_movies =Count('actor'),
).values('partner_actor_id', 'partner_actor_name', 'number_of_shared_movies')
return result
The problem with that code is: It repeats the results by the number of movies. For instance if the actor have 5 movies the results will be repeated 5 times. Another issue is: in order to get top_genre and most_frequent_partner I'm using max() but then I just get the numbers and not the actual name of genre in (top_genre) and actor name in (most_frequent_partner). Since I use max() in a way to get more than one value. For instance in the top_genre: If the actor have 3 Drama, 3 Comedy, 1 Horror, 1 Documentary, I get the max in that way: [3,3], but how can I get the actual names out of these results? Same goes to most_frequent_partner.
Results looks like this so far:
{
"next": null,
"previous": null,
"count": 4,
"pagenum": null,
"results": [
{
"id": 36,
"name": "Benjamin 2X",
"top_genre": [
2,
2
],
"number_of_movies": 4,
"number_of_movies_by_genre": [
{
"movie__movies_genres__genre": null,
"genre": null,
"number_of_movies": 0
},
{
"movie__movies_genres__genre": "Documentary",
"genre": "Documentary",
"number_of_movies": 2
},
{
"movie__movies_genres__genre": "Music",
"genre": "Music",
"number_of_movies": 2
}
],
"most_frequent_partner": []
},
{
"id": 36,
"name": "Benjamin 2X",
"top_genre": [
2,
2
],
"number_of_movies": 4,
"number_of_movies_by_genre": [
{
"movie__movies_genres__genre": null,
"genre": null,
"number_of_movies": 0
},
{
"movie__movies_genres__genre": "Documentary",
"genre": "Documentary",
"number_of_movies": 2
},
{
"movie__movies_genres__genre": "Music",
"genre": "Music",
"number_of_movies": 2
}
],
"most_frequent_partner": []
},
{
"id": 36,
"name": "Benjamin 2X",
"top_genre": [
2,
2
],
"number_of_movies": 4,
"number_of_movies_by_genre": [
{
"movie__movies_genres__genre": null,
"genre": null,
"number_of_movies": 0
},
{
"movie__movies_genres__genre": "Documentary",
"genre": "Documentary",
"number_of_movies": 2
},
{
"movie__movies_genres__genre": "Music",
"genre": "Music",
"number_of_movies": 2
}
],
"most_frequent_partner": []
},
{
"id": 36,
"name": "Benjamin 2X",
"top_genre": [
2,
2
],
"number_of_movies": 4,
"number_of_movies_by_genre": [
{
"movie__movies_genres__genre": null,
"genre": null,
"number_of_movies": 0
},
{
"movie__movies_genres__genre": "Documentary",
"genre": "Documentary",
"number_of_movies": 2
},
{
"movie__movies_genres__genre": "Music",
"genre": "Music",
"number_of_movies": 2
}
],
"most_frequent_partner": []
}
]
}
What I want to see in the end:
{
"next": null,
"previous": null,
"count": 2,
"results": [
{
"id": 18 (actor_id),
"name": Bruce Buffer (actor_name),
"number of movies": 2,
"top genre": Drama, Documentary,
"number of movies by genre": Drama: 1, Documentary: 1,
"most frequent partner": partner_actor_id, partner_actor_name, number_of_shared_movies,
}
]
}
If you want, the number of movies by genre for a given actor what you can do is annotate and count aggregate
return Roles.objects.filter(
actor_id=self.kwargs['pk']
).values('movie__movies_genres__genre').annotate(
no_of_movies=Count('movie__movies_genres__genre'),
genre=F('movie__movies_genres__genre'),
)
Here first we filtered roles for a given actor
then values will group by genre then annotation is computed over all members of the group that count and get genre
and you can use SerializerMethodField to these calculated results
if you have a huge dataset it will not perform well, but you can create indexes accordingly still it will cost you 2-3 queries
you can learn more about Django queryset API
There many ways to implement this route, it depends on many criteria and how much it will be used .
i think a correct way is to create a dedicated model that would store actor stats with a one to one relation to actor and recompute the value each time a movie is added. But If you add movie often it could slow down your database.
You can also accept to have some outdated data for a while and update the table regularly using a background job and maybe using custom sql query that will ensure you better performance (bulk update).
I would start from your model, you have genres defined as a CharField in two of your models. By not isolating them anywhere, you need to look in both tables for all types of genres. If do not, then you are just supposing that all the genres you have in one table is also on the other one, which could not be true.
Also, querying string fields is not very efficient when in comparison to a int PK, so from the point of view of scaling this is bad. (Of course, i am saying that in general, as a good practice and not focused specifically in movie genres)
Your best option would be to have either a Genre Model or a choice field, where you define all possible genres.
As for the counting, you would do that inside your serializer class, by using a serializermethodfield.
The way I am doing it is like in getProductList function in my views.py and it is giving me each product with its variants in different objects for eg the same product with different size is put into a different object even if the product is the same like so:
JSON:
[
{
"prod_id": {
"id": 3,
"prod_name": "CRUISER-149S-DEERCALF",
"category": 2
},
"size_id": {
"id": 2,
"name": "26"
},
"color_id": 2,
"image_id": {
"id": 10,
"prod_image": "/media/products/IMG_7617_ftgqoa.jpg"
}
},
{
"prod_id": {
"id": 3,
"prod_name": "CRUISER-149S-DEERCALF",
"category": 2
},
"size_id": {
"id": 3,
"name": "27"
},
"color_id": 2,
"image_id": {
"id": 10,
"prod_image": "/media/products/IMG_7617_ftgqoa.jpg"
}
}
]
Is there a way to maybe make it more optimized? like have only one product object that has all the info of a single product like size for example can be a property with values of all sizes in it like size: [26, 27] instead of a different object if a size is different.
models.py
class Products(models.Model):
prod_name = models.CharField(max_length=200)
category = models.ForeignKey(Categories,null=True, on_delete=models.SET_NULL)
def __str__(self):
return '%s'%(self.prod_name)
class Variants(models.Model):
prod_id = models.ForeignKey(Products, on_delete=models.CASCADE)
size_id = models.ForeignKey(Size, null=True, on_delete=models.SET_NULL, related_name='variants')
color_id = models.ForeignKey(Color, null=True, on_delete=models.SET_NULL, default='')
image_id = models.ForeignKey(Image,null=True, on_delete=models.SET_NULL, default='')
def __str__(self):
return '%s %s %s %s %s %s %s'%(self.id , self.prod_id, self.size_id, self.image_id, self.color_id, self.quantity, self.price)
serializer.py
class ProductsSerializer(serializers.ModelSerializer):
# variants = serializers.StringRelatedField(many=True)
class Meta:
model = Products
fields = "__all__"
class SizeSerializer(serializers.ModelSerializer):
class Meta:
model = Size
fields = "__all__"
class VariantsSerializer(serializers.ModelSerializer):
prod_id = ProductsSerializer(many=False, read_only=True)
image_id = ImageSerializer(many=False, read_only=True)
size_id = SizeSerializer(many=False, read_only=True)
class Meta:
model = Variants
fields = ['prod_id', 'size_id', 'quantity', 'price', 'color_id', 'image_id']
views.py
def getProductList(request):
print(request.GET.get('category'))
if(request.method == 'GET'):
if request.GET.get('category') == 'all' :
productList = Variants.objects.all().select_related()
serializer = VariantsSerializer(productList, many=True)
return JsonResponse(serializer.data, safe=False)
return JsonResponse({'details': 'Error in getting product list'})
For my project I need to have a simple process
a form => many questions => for each questions a response type => if the response type is a multiple choice, then possible response are in a uniq group.
My goal is to be able to push though DRF/Django rest framework the whole initial form.
Here are my models:
class FormModels(models.Model):
class Status(models.IntegerChoices):
DRAFT = 1, _("DRAFT")
PUBLISHED = 2, _("PUBLISHED")
ARCHIVED = 5, _("ARCHIVED")
form_name = models.CharField(unique=True, blank=False, null=False, max_length=255)
form_model = models.CharField(unique=True, blank=False, null=False, max_length=255)
status = models.IntegerField(choices=Status.choices, blank=False, null=False, default=1)
def __str__(self):
return self.form_name
class FormResponseTypeModels(models.Model):
class Type(models.IntegerChoices):
TEXT = 1, _('TEXT')
MULTICHOICE = 2, _('MULTI')
type = models.IntegerField(choices=Type.choices, blank=False, null=False, default=1)
group = models.IntegerField(blank=False, null=True, default=1)
def __str__(self):
return str(self.type)
class Meta:
constraints = [
UniqueConstraint(fields=['group'], name='unique_group')
]
class MultipleChoiceDataModels(models.Model):
text = models.CharField(blank=False, null=False, max_length=255)
value = models.CharField(blank=False, null=False, max_length=255)
order = models.IntegerField(blank=False, null=False, default=1)
group_refid = models.ForeignKey(blank=False,
null=False,
default=1,
to=FormResponseTypeModels,
to_field="group",
related_name="groups",
on_delete=models.PROTECT)
def __str__(self):
return self.text
class FormQuestionModels(models.Model):
form_id = models.ForeignKey(to=FormModels, on_delete=models.PROTECT)
form_response_type = models.ForeignKey(to=FormResponseTypeModels, on_delete=models.PROTECT, related_name="test")
form_question = models.TextField(max_length=2000)
def __str__(self):
return self.form_question
Here is my serializers.py
class FormMultipleChoiceDataSerializer(serializers.ModelSerializer):
class Meta:
model = MultipleChoiceDataModels
fields = "__all__"
depth = 10
class FormResponseTypeSerializer(serializers.ModelSerializer):
groups = FormMultipleChoiceDataSerializer(many=True,
read_only=False,
required=False,
)
class Meta:
model = FormResponseTypeModels
fields = "__all__"
depth = 10
class FormQuestionSerializer(serializers.ModelSerializer):
form_response_type_set = FormResponseTypeSerializer(many=False,
read_only=False,
required=False
)
class Meta:
model = FormQuestionModels
fields = "__all__"
depth = 10
class FormSerializer(serializers.HyperlinkedModelSerializer):
questions = FormQuestionSerializer(many=True,
read_only=False,
source='formquestionmodels_set',
required=False)
class Meta:
model = FormModels
fields = "__all__"
def validate(self, attrs):
pass
return super().validate(attrs=attrs)
def create(self, validated_data):
# https://stackoverflow.com/questions/58023503/drf-serializer-field-renamed-to-its-source-in-validated-data
# question is 'formquestionmodels_set'
questions_data = validated_data.pop('formquestionmodels_set')
form = FormModels.objects.create(**validated_data)
for question_data in questions_data:
if "form_response_type_set" in questions_data:
form_response_type_set = question_data.pop("form_response_type_set")
max_group_id = None
if "formmultiplechoicedatamodel_set" in form_response_type_set:
form_response_type_set_groups = form_response_type_set.pop("formmultiplechoicedatamodel_set")
# find max group_id + 1
max_group_id = 1 + MultipleChoiceDataModels.objects.aggregate(Max("group_refid"))
for form_response_type_set_group in form_response_type_set_groups:
MultipleChoiceDataModels.objects.create(group_refid=max_group_id,
**form_response_type_set_group)
if max_group_id:
form_response_type = FormResponseTypeModels.objects.create(group=max_group_id,
**form_response_type_set)
else:
form_response_type = FormResponseTypeModels.objects.create(**form_response_type_set)
FormQuestionModels.objects.create(form_id=form, form_response_type=form_response_type, **question_data)
return form
Expected result would be:
{
"form_name": "test7",
"form_model": "test7",
"status": 1,
"questions": [
{
"form_question": "q1",
"form_response_type_set": {
"type": 1,
"group": [
{
"text": "a",
"value": "1",
"order": 1,
"group_refid": 1
}
]
}
},
{
"form_question": "q2",
"form_id": {
"form_name": "form_name",
"form_model": "form_model",
"status": 1
},
"form_response_type_set": {
"type": 1,
"group": [
{
"text": "a",
"value": "1",
"order": 1,
"group_refid": 1
}
]
}
}
]
}
But for the group part, I can't get it display. I got:
"questions": [
{
"id": 1,
"form_question": "q1",
"form_id": {
"id": 1,
"form_name": "t1",
"form_model": "t1",
"status": 2
},
"form_response_type": {
"id": 1,
"type": 1,
"group": null
}
},
{
"id": 2,
"form_question": "q2",
"form_id": {
"id": 1,
"form_name": "t1",
"form_model": "t1",
"status": 2
},
"form_response_type": {
"id": 2,
"type": 2,
"group": 1
}
}
],
"form_name": "t1",
"form_model": "t1",
"status": 2
}
]
With django shell I get this result:
FormResponseTypeSerializer():
id = IntegerField(label='ID', read_only=True)
groups = FormMultipleChoiceDataSerializer(many=True, read_only=False, required=False):
id = IntegerField(label='ID', read_only=True)
text = CharField(max_length=255)
value = CharField(max_length=255)
order = IntegerField(max_value=2147483647, min_value=-2147483648, required=False)
group_refid = NestedSerializer(read_only=True):
id = IntegerField(label='ID', read_only=True)
type = ChoiceField(choices=[(1, 'TEXT'), (2, 'MULTI')], required=False, validators=[<django.core.validators.MinValueValidator object>, <django.core.validators.MaxValueValidator object>])
group = IntegerField(allow_null=True, max_value=2147483647, min_value=-2147483648, required=False)
type = ChoiceField(choices=[(1, 'TEXT'), (2, 'MULTI')], required=False, validators=[<django.core.validators.MinValueValidator object>, <django.core.validators.MaxValueValidator object>])
group = IntegerField(allow_null=True, max_value=2147483647, min_value=-2147483648, required=False)
I've tried source and related name, many type of serializers, _set syntax for varialbe source ... , read 40+ stack overflow threads, but each time I'm stuck with this (also, if it matters, I would like to be able to post a new form at once with same endpoint)
Can you kindly point me to my error ?
I'm not sure what I did for it to work, but after lots of tries
using related_name in models,
changing the fields name to avoid using text, value and order,
deleting the database and apply migrations again,
set depth=1 to each parent serializer.
Now it works!
If someone have a good explanation, I would be glad to read it & learn!
models.py
class Sequence(models.Model):
category_id = models.ForeignKey(SequenceCategory, on_delete=models.CASCADE)
description = models.TextField()
code = models.CharField(max_length=50)
total_divisions = models.IntegerField()
status = models.BooleanField(default=True)
def __str__(self):
return self.code
class SequenceValue(models.Model):
TYPE_CHOICES =(
('N', 'Numeric'),
('A', 'Alphabet')
)
sequence_id = models.ForeignKey(Sequence, on_delete=models.CASCADE, blank=True, null=True, related_name='Sequence_details')
type = models.CharField(max_length=100, choices=TYPE_CHOICES)
value = models.CharField(max_length=50)
starting_value = models.CharField(max_length=50, null=True, blank=True)
increment_value = models.CharField(max_length=50, null=True, blank=True)
def __str__(self):
return self.value
serializers.py
class SequenceValueSerializer(serializers.ModelSerializer):
class Meta:
model = SequenceValue
# fields = '__all__'
exclude = ['sequence_id']
class SequenceSerializer(serializers.ModelSerializer):
Sequence_details = SequenceValueSerializer()
class Meta:
model = Sequence
fields = '__all__'
def create(self, validate_data):
Sequence_details_data = validate_data.pop('Sequence_details')
sequence_id = Sequence.objects.create(**validate_data)
SequenceValue.objects.create(sequence_id=sequence_id, **Sequence_details_data)
return sequence_id
views.py
class SequenceCreate(ListCreateAPIView):
queryset = Sequence.objects.all()
serializer_class = SequenceSerializer
Why do I get the error? When I refer to n number of articles I got a solution that put many=True something like this.
Sequence_details = SequenceValueSerializer(many=True)
But when I make changes on that then I can't get the Sequence details fields. How can I fix this issue? Can you give a solution, please?
Actual error-
Got AttributeError when attempting to get a value for field type on serializer SequenceValueSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the RelatedManager instance.
Original exception text was: 'RelatedManager' object has no attribute 'type'.
Data Passess
Data - {
"Sequence_details": [{
"type": "N",
"value": "123",
"starting_value": "1",
"increment_value": "1"
}],
"description": "asd",
"code": "qwe",
"total_divisions": 2,
"status": false,
"category_id": 3
}
Actual eroor screenshot
Adding many=True works because your request data passes Sequence_details as a list. In order to use it in your serializer, you can just iterate through the field and create your objects like this:
class SequenceSerializer(serializers.ModelSerializer):
Sequence_details = SequenceValueSerializer(many=True)
class Meta:
model = Sequence
fields = '__all__'
def create(self, validate_data):
Sequence_details_data = validate_data.pop('Sequence_details')
sequence_id = Sequence.objects.create(**validate_data)
for details in Sequence_details_data
SequenceValue.objects.create(sequence_id=sequence_id, **details)
return sequence_id
But if you don't want to do this, you can remove many=True and keep your serializer as is, but ensure that Sequence_details in your request data is not a list:
{
"Sequence_details": {
"type": "N",
"value": "123",
"starting_value": "1",
"increment_value": "1"
},
"description": "asd",
"code": "qwe",
"total_divisions": 2,
"status": false,
"category_id": 3
}
I can't comment so I will just mention it here :). type is actually a function in python which tells you about the class type of argument. I am not sure if this the issue but the can you try by renaming type to _type or something of your choice.
I'm having some trouble serializing many to many relationships with a through argument in DRF3
Very basically I have recipes and ingredients, combined through an intermediate model that specifies the amount and unit used of a particular ingredient.
These are my models:
from django.db import models
from dry_rest_permissions.generics import authenticated_users, allow_staff_or_superuser
from core.models import Tag, NutritionalValue
from usersettings.models import Profile
class IngredientTag(models.Model):
label = models.CharField(max_length=255)
def __str__(self):
return self.label
class Ingredient(models.Model):
recipe = models.ForeignKey('Recipe', on_delete=models.CASCADE)
ingredient_tag = models.ForeignKey(IngredientTag, on_delete=models.CASCADE)
amount = models.FloatField()
unit = models.CharField(max_length=255)
class RecipeNutrition(models.Model):
nutritional_value = models.ForeignKey(NutritionalValue, on_delete=models.CASCADE)
recipe = models.ForeignKey('Recipe', on_delete=models.CASCADE)
amount = models.FloatField()
class Recipe(models.Model):
name = models.CharField(max_length=255)
ingredients = models.ManyToManyField(IngredientTag, through=Ingredient)
tags = models.ManyToManyField(Tag, blank=True)
nutritions = models.ManyToManyField(NutritionalValue, through=RecipeNutrition)
owner = models.ForeignKey(Profile, on_delete=models.SET_NULL, blank=True, null=True)
def __str__(self):
return self.name
And these are currently my serializers:
from recipes.models import Recipe, IngredientTag, Ingredient
from rest_framework import serializers
class IngredientTagSerializer(serializers.ModelSerializer):
class Meta:
model = IngredientTag
fields = ('id', 'label')
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingredient
fields = ('amount', 'unit')
class RecipeSerializer(serializers.ModelSerializer):
class Meta:
model = Recipe
fields = ('id', 'url', 'name', 'ingredients', 'tags', 'nutritions', 'owner')
read_only_fields = ('owner',)
depth = 1
I've searched SO and the web quite a bit, but I can't figure it out. It would be great if someone could point me in the right direction.
I can get the list of ingredients to be returned like so:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"url": "http://localhost:8000/recipes/1/",
"name": "Hallo recept",
"ingredients": [
{
"id": 1,
"label": "Koek"
}
],
"tags": [],
"nutritions": [],
"owner": null
}
]
}
But what I want is for the amount and unit to also be returned!
I got what I wanted in the following way:
from recipes.models import Recipe, IngredientTag, Ingredient
from rest_framework import serializers
class IngredientTagSerializer(serializers.ModelSerializer):
class Meta:
model = IngredientTag
fields = ('id', 'label')
class IngredientSerializer(serializers.ModelSerializer):
ingredient_tag = IngredientTagSerializer()
class Meta:
model = Ingredient
fields = ('amount', 'unit', 'ingredient_tag')
class RecipeSerializer(serializers.ModelSerializer):
ingredients = IngredientSerializer(source='ingredient_set', many=True)
class Meta:
model = Recipe
fields = ('url', 'name', 'ingredients', 'tags', 'nutritions', 'owner')
read_only_fields = ('owner',)
depth = 1
using the ingredient_tag's ingredient_set as a source for IngredientSerializer resulted in the response I required:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"url": "http://localhost:8000/recipes/1/",
"name": "Hallo recept",
"ingredients": [
{
"amount": 200.0,
"unit": "g",
"ingredient_tag": {
"id": 1,
"label": "Koek"
}
},
{
"amount": 500.0,
"unit": "kg",
"ingredient_tag": {
"id": 3,
"label": "Sugar"
}
}
],
"tags": [],
"nutritions": [],
"owner": null
}
]
}
I don't know if this is the best way to go about it, so I'll wait til somebody who knows their DRF leaves a comment or perhaps someone posts something better before marking as answer.
While serializing the nested relations, you also have to serialize specifically those ManyToManyField.
Let me give you a small example:
class RecipeSerializer(serializers.ModelSerializer):
ingredients = serializers.SerializerMethodField()
def get_ingredients(self, obj):
serializer = IngredientSerializer(obj.ingredients)
return serializer.data
class Meta:
model = Recipe
fields = ('id', 'url', 'name', 'ingredients', 'tags', 'nutritions', 'owner')
read_only_fields = ('owner',)
depth = 1
Whatever your nested relation is (like ingredients, tags or nutritions), you can serialize them by creating a serializer method field. In that method, You can use your specific serializer so that it gives the json you want.
Be careful with the method name. If your ManyToManyField is "ingredients", your method name should be "ingredients" because DRF works with "get_".
For further information, check this:
Django Rest Framework - SerializerMethodField
Override the method to_representation of RecipeSerializer
and pass the instance of many to many filed to their serializer with many is True. or
tags = serializers.HyperlinkedRelatedField(
many=True,read_only=True,
)