Django REST Framework: I want to resolve n+1 in SerializerMethodField - django

I am trying to create a queryset that returns Boolean from a queryset prefetched with a reverse reference by SerializerMethodField, as shown in the code below.
I'm creating one that determines if there is an object for the current user and returns Boolean.
However, when I use the prefetched queryset to filter by the current user as shown below, a new queryset is issued instead of the prefetched queryset, and the n+1 problem occurs.
In the following code, how can we save the queryset and return
Booelan?
# serializers.py
class VideoSerializer(serializers.ModelSerializer):
is_viewed = serializers.SerializerMethodField()
is_favorited = serializers.SerializerMethodField()
is_wl = serializers.SerializerMethodField()
class Meta:
model = Video
fields = (
"pk",
"is_viewed",
"is_favorited",
"is_wl",
)
#staticmethod
def setup_eager_loading(queryset):
queryset.prefetch_related('history_set', 'favorite_set')
def get_is_viewed(self, obj):
user = self.context["request"].user
if user.is_authenticated:
try:
obj.history_set.get(user=user) # <- here
return True
except History.DoesNotExist:
pass
return False
def get_is_favorited(self, obj):
user = self.context["request"].user
if user.is_authenticated:
try:
obj.favorite_set.get(user=user) # <- here
return True
except Favorite.DoesNotExist:
pass
return False
def get_is_wl(self, obj):
user = self.context["request"].user
if user.is_authenticated:
try:
Track.objects.get(playlist__user=user, playlist__is_wl=True, video=obj)
return True
except Track.DoesNotExist:
pass
return False
A large number of query sets were issued.
#models.py
class Video(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField("title", max_length=300)
def __str__(self):
return self.title
class History(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE
)
video = models.ForeignKey(Video, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.user}, {self.video.title}"
class Favorite(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE
)
video = models.ForeignKey(Video, on_delete=models.CASCADE)
created_at = models.DateTimeField(default=timezone.now)
def __str__(self):
return f"{self.user}, {self.video.title}"

You can use Exists subquery.
Video.objects.annotate(is_favorite=Exists(Favorite.objects.filter(user=self.request.user, video=OuterRef("id"))))
After that you can access is_favorite attribute.
class VideoSerializer(serializers.ModelSerializer):
is_viewed = serializers.SerializerMethodField()
is_favorited = serializers.SerializerMethodField()
is_wl = serializers.SerializerMethodField()
class Meta:
model = Video
fields = (
"pk",
"is_viewed",
"is_favorited",
"is_wl",
)
#staticmethod
def setup_eager_loading(queryset):
queryset.prefetch_related('history_set', 'favorite_set')
def get_is_viewed(self, obj):
user = self.context["request"].user
if user.is_authenticated:
try:
obj.history_set.get(user=user) # <- here
return True
except History.DoesNotExist:
pass
return False
def get_is_favorited(self, obj):
return obj.is_favorite
def get_is_wl(self, obj):
user = self.context["request"].user
if user.is_authenticated:
try:
Track.objects.get(playlist__user=user, playlist__is_wl=True, video=obj)
return True
except Track.DoesNotExist:
pass
return False
You can add annotations for other fields (is_views, is_wl)

Another query being made with prefetch_related method call is intended. You can find it in django documentation.
https://docs.djangoproject.com/en/3.2/ref/models/querysets/
I think using subquery and Exists query expression is better option, as 'Utkucan Bıyıklı' suggested.

By reverse reference, i understand you are referring a foreignkey field, in that case you need to use select_related, prefetch_related is used for many-many fields.
Based on it you can use any of the below code. make sure to return the queryset.
#staticmethod
def setup_eager_loading(queryset):
#prefetch_related for 'to-many' relationships
queryset.prefetch_related('history_set', 'favorite_set')
return quesyset
#staticmethod
def setup_eager_loading(queryset):
#select_related for 'foreign key' relationships
queryset = queryset.select_related('history_set', 'favorite_set')
return queryset

Related

ValueError: Cannot assign "1": "LeadFacilityAssign.assigned_facilities" must be a "Facility" instance

I've been trying to create an api endpoint to update my "lead" objects and add a list of facilities to them when sending a put request (each time a different amount of facilities). The lead objects already exist inside the database so do the facility objects. Since i need a date and time associated to each facility when they are being added to a lead i created the "LeadFacilityAssign" class.
Since i wasn't able to get it to work i tried to do it just with a post request for now, during the lead creation process. I was told that i need to use bulk_create if i need to add more than one facility this way. I couldn't find anything on bulk_create inside the drf documentation so i decided to do this for now just with one facility and improve my code from there one issue at a time since i'm new to drf.
Does anyone know what is causing this error? I tried a few different things but nothing worked so far.
ValueError: Cannot assign "1": "LeadFacilityAssign.assigned_facilities" must be a "Facility" instance.
serializers.py
class LeadUpdateSerializer(serializers.ModelSerializer):
is_owner = serializers.SerializerMethodField()
assigned_facilities = serializers.IntegerField(required=True)
datetime = serializers.DateTimeField(required=True)
class Meta:
model = Lead
fields = (
"id",
"first_name",
"last_name",
"assigned_facilities",
"datetime",
)
read_only_fields = ("id", "created_at", "agent", "is_owner")
def get_is_owner(self, obj):
user = self.context["request"].user
return obj.agent == user
def create(self, validated_data):
assigned_facilities = validated_data.pop("assigned_facilities")
datetime = validated_data.pop("datetime")
instance = Lead.objects.create(**validated_data)
instance.leadfacility.create(assigned_facilities=assigned_facilities,datetime=datetime)
print(instance)
return instance
models.py
class Facility(models.Model):
name = models.CharField(max_length=150, null=True, blank=False)
def __str__(self):
return self.name
class Lead(models.Model):
first_name = models.CharField(max_length=40, null=True, blank=True)
last_name = models.CharField(max_length=40, null=True, blank=True)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class LeadFacilityAssign(models.Model):
assigned_facilities = models.ForeignKey(Facility, on_delete=models.CASCADE, related_name='leadfacility')
lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name='leadfacility')
datetime = models.DateTimeField()
views.py
class LeadCreateView(CreateAPIView):
permission_classes = [IsAuthenticated, IsLeadOwner]
serializer_class = LeadUpdateSerializer
def perform_create(self, serializer):
serializer.save(agent=self.request.user)
class LeadUpdateView(UpdateAPIView):
permission_classes = [IsAuthenticated, IsLeadOwner]
serializer_class = LeadUpdateSerializer
def get_queryset(self):
return Lead.objects.all()
You are trying to add Integer value into FK field.
You have 2 options. You can change the serializer field.
assigned_facilities = serializers.PrimaryKeyRelatedField(queryset=Facility.objects.all(), required=True, write_only=True)
OR
assigned_facilities = serializers.IntegerField(required=True, write_only=True)
instance.leadfacility.create(assigned_facilities_id=assigned_facilities,datetime=datetime)
I would rather use 1 option.
Another potential solution you can apply:
class LeadUpdateSerializer(serializers.ModelSerializer):
is_owner = serializers.SerializerMethodField()
assigned_facilities = serializers.IntegerField(required=True)
datetime = serializers.DateTimeField(required=True)
class Meta:
model = Lead
fields = (
"id",
"first_name",
"last_name",
"assigned_facilities",
"datetime",
)
read_only_fields = ("id", "created_at", "agent", "is_owner")
def validate_assigned_facility(self, facility_pk)->:
assigned_facility = Facility.objects.filter(pk=facility_pk).first()
if assigned_facility:
return assigned_facility
raise ValidationError('Facility not found, provide a valid pk')
def get_is_owner(self, obj):
user = self.context["request"].user
return obj.agent == user
def create(self, validated_data):
assigned_facilities = validated_data.pop("assigned_facilities")
datetime = validated_data.pop("datetime")
instance = Lead.objects.create(**validated_data)
instance.leadfacility.create(assigned_facilities=assigned_facilities,datetime=datetime)
print(instance)
return instance
This solution is kind of big but is so flexible 'cause give you the opportunity to add more business logic around the input and the expected data in the model or datasource.

TypeError: Object of type ManyRelatedManager is not JSON serializable in django rest framework

I am trying to add some students to a teacher class using their ids as primary key but I am getting above error.
I have models of Teachers and Students like this.
class Student(TimeStampAbstractModel):
user = models.OneToOneField(User, related_name="student", on_delete=models.CASCADE)
college_name = models.CharField(max_length=255, default="", blank=True)
address = models.CharField(max_length=255, default="", blank=True)
def __str__(self):
return self.user.name
class Teacher(TimeStampAbstractModel):
user = models.OneToOneField(User, related_name="teacher", on_delete=models.CASCADE)
address = models.CharField(max_length=255, default="", blank=True)
students_in_class = models.ManyToManyField(Student,related_name="teacher")
def __str__(self):
return self.user.name
Here a teacher model can have many students in a class with thier ids. I have used an put api call to add the students to the teacher in one click.
My view:
from rest_framework import status
class AddtoClassView(APIView):
def put(self,request,pk,*args,**kwargs):
id =pk
teacher = Teacher.objects.get(id=id)
serializer = TeacherSerializer(teacher,data=request.data)
if serializer.is_valid():
serializer.save()
print("iam if")
return Response({
"message":"Student has been added to class.",
"data": serializer.data
},status=status.HTTP_200_OK)
# else:
print("iam else")
return Response(serializer.data)
My serializer:
class TeacherSerializer(serializers.ModelSerializer):
students_in_class = serializers.PrimaryKeyRelatedField(
read_only= True
)
address = serializers.CharField(required=False)
# user = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Teacher
fields = ["address","students_in_class"]
# fields = '__all__'
def update(self, instance, validated_data):
instance.address = validated_data.get("address")
instance.save()
stu = validated_data.get("students_in_class")
print(stu)
if stu is not None:
print("iam stu")
instance.students_in_class.add(stu)
instance.save()
super(TeacherSerializer,self).update(instance, validated_data)
return instance
Here I have used students_in_class as pk field ( i still havent understand when to use integarfield and when to use pk field). I know since i am adding the ids to the student_in_class field i am not supposed to use it as read_only = true, however i had to use otherwise it generates error. How to solve this? Also, i dont really know which fields to define as which in serializer class.
Updated code:
class TeacherSerializer(serializers.ModelSerializer):
# students_in_class = serializers.PrimaryKeyRelatedField(
# many = True, read_only= True
# )
students_in_class = serializers.ListField(
source="students_in_class.all",
child=serializers.PrimaryKeyRelatedField(queryset=Student.objects.all()),
)
address = serializers.CharField(required=False)
# user = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Teacher
fields = ["address","students_in_class"]
# fields = '__all__'
def update(self, instance, validated_data):
instance.address = validated_data['students_in_class']['all']
instance.save()
stu = validated_data.get("students_in_class")
print(stu)
if stu is not None:
print("iam stu")
instance.students_in_class.add(stu)
instance.save()
super(TeacherSerializer,self).update(instance, validated_data)
return instance
Since you are using m2m field, you need list of ids for students_in_class. So the solution will be something like this. (Disclaimer: Code not tested).
class TeacherSerializer(serializers.ModelSerializer):
students_in_class = serializers.ListField(
source="students_in_class.all",
child=serializers.PrimaryKeyRelatedField(queryset=Student.objects.all()),
)
Serialization error will be solved because now you have included students_in_class.all as source. You need to access the data with something like this: validated_data['students_in_class']['all']
If you want to serialize your output in different way, you could set students_in_class as write_only and override serializer representation as needed.:
class TeacherSerializer(serializers.ModelSerializer):
students_in_class = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Student.objects.all()),
write_only=True
)
# your code
def to_representation(self, instance):
ret = super().to_representation(instance)
ret['students_in_class'] = StudentSerializer(instance.students_in_class.all(), many=True).data
return ret
The following code worked:
class TeacherSerializer(serializers.ModelSerializer):
students_in_class = serializers.PrimaryKeyRelatedField(
many = True,queryset=Student.objects.all()
)
address = serializers.CharField(required=False)
class Meta:
model = Teacher
fields = ["address","students_in_class"]
def update(self, instance, validated_data):
instance.address = validated_data.get("address")
instance.save()
stu = validated_data.pop("students_in_class")
for stus in stu:
instance.students_in_class.add(stus)
instance.save()
super(TeacherSerializer,self).update(instance, validated_data)
return instance

Django Rest Framework nested serializer gives attribute error

I am trying to setup a nested serializer below I've posted the models and serializer. why am I getting the below error ?
Got AttributeError when attempting to get a value for field `balance_sheet` on serializer `StockSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Stock` instance.
Original exception text was: 'Stock' object has no attribute 'balance_sheet'.
serializers.py
class StockSerializer(serializers.ModelSerializer):
income_statement = IncomeStatementSerializer(many=True)
balance_sheet = BalanceSheetSerializer(many=True)
cashflows_statement = CashflowsStatementSerializer(many=True)
def create(self, validated_data):
temp_income_statement_data = validated_data.pop("income_statement")
temp_balance_sheet_data = validated_data.pop("balance_sheet")
temp_cashflows_statement_data = validated_data.pop("cashflows_statement")
new_stock = Stock.objects.create(**validated_data)
for i in temp_income_statement_data:
IncomeStatement.objects.create(**i, ticker=new_stock)
for x in temp_balance_sheet_data:
BalanceSheet.objects.create(**x, ticker=new_stock)
for y in temp_cashflows_statement_data:
CashflowsStatement.objects.create(**y, ticker=new_stock)
return new_stock
IncomeStatementSerializer, BalanceSheetSerializer and CashflowsStatementSerializer are all typical ModelSerializer's
models.py
class Stock(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
ticker = models.CharField(max_length=10, unique=True, primary_key=True)
slug = models.SlugField(default="", editable=False)
def save(self, *args, **kwargs):
value = self.ticker
self.slug = slugify(value, allow_unicode=True)
super().save(*args, **kwargs)
def __str__(self):
return self.ticker
class Meta:
verbose_name = "stock"
verbose_name_plural = "stocks"
ordering = ["ticker"]
IncomeStatement, BalanceSheet and CashflowsStatement are all typical models.Model with a ForeignKey relationship to Stock
views.py
class StockList(generics.ListCreateAPIView):
queryset = Stock.objects.all()
serializer_class = StockSerializer
lookup_field = "slug"

Django REST Framework : serializer fields are missing in the response

I'm using Django 2.0 and Django REST Framework.
I have two models contact and transaction as below
contact model
class Contact(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100, blank=True, null=True)
amount given model
class AmountGiven(models.Model):
contact = models.ForeignKey(Contact, on_delete=models.PROTECT)
amount = models.FloatField(help_text='Amount given to the contact')
interest_rate = models.FloatField(blank=True, default=None, null=True, help_text='% of interest to be calculated')
_given_date = models.DateTimeField(
db_column='given_date',
default=timezone.now,
help_text='Date and time when amount was given to the contact'
)
def __str__(self):
return str(self.amount)
#property
def given_date(self):
return self._given_date
#given_date.setter
def given_date(self, value):
self._given_date = value
#property
def interest_to_pay(self):
if self.interest_rate:
datetime_diff = datetime.now(get_localzone()) - self.given_date
days = datetime_diff.days
duration_in_year = days/365
simple_interest_amount = (self.amount * duration_in_year * self.interest_rate)/100
return simple_interest_amount
return 0
#property
def total_payable(self):
return self.amount + self.interest_to_pay
#property
def amount_due(self):
returned_amount = 0
for returned in self.amountreturned_set.all():
returned_amount += returned.amount
return self.total_payable - returned_amount
and ContactSerializer
class ContactSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedRelatedField(
view_name='contacts:detail',
read_only=True
)
user = serializers.CurrentUserDefault()
amount_due = ReadOnlyField(source='amountgiven__amount_due')
class Meta:
model = Contact
fields = ('url', 'id', 'first_name', 'last_name', 'full_name', 'amount_due')
and in views.py
class ContactViewSet(viewsets.ModelViewSet):
serializer_class = ContactSerializer
permission_classes = (IsAuthenticated, AdminAuthenticationPermission,)
def get_queryset(self):
return Contact.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
But there is no field as amount_due and url in the response returned while making the request to /contacts/ endpoint with GET method.
Based on your comment, you want the sum of all the amounts(please edit your question). so you should use annotate in your queryset:
from django.db.models import Sum
def get_queryset(self):
return Contact.objects.filter(user=self.request.user).annotate(amount_due=Sum('amountgiven_set__amount'))
(I recommend using modelManager for the queryset and the filtering instead of doing it here)
and add a field like this to your serializer:
amount_due = serializer.IntegerFiled()
Your modeling doesn't allow you to access amount_due in the way which you'd like.
Your Contact model does not have amountgiven attribute.
It does however have amountgiven_set which you can use to obtain a queryset of amountgiven for given contact.
But there can be multiple ones so you need to decide which amount_due you want to display in your serializer.
You can use SerializerMethodField to serializer the value of amount_due which you would like:
class ContactSerializer(serializers.HyperlinkedModelSerializer):
amount_due = serializers.SerializerMethodField()
def get_amount_due(self, obj):
amountgiven = obj.amountgiven_set.first()
return amountgiven.amount_due
But again, as i already mentioned - amountgiven_set returns a queryset where you can have multiple objects.
In case you are sure you have only one for given contact, you can use first() as in my example to get it.

Django - Design choices, Override the model save() so it palys nice with contrib.admin?

I'm with some design issues in Django and getting all to play nice with contrib.admin.
My main problem is with Admin Inlines and the save_formset() method. I created a create() classmethod for the model but this do not play nice with save_formset(). I think Django admin have a way of doing this, not with a create() method. In the create() method in the AdPrice I basicaly want to update the field 'tags' in the model Ad.
My question: Instead of creating a create() classmethod it would be nice to override the model save() method so I don't have problems with contrib.admin?
My code:
Models:
class Ad(models.Model):
title = models.CharField(max_length=250)
name = models.CharField(max_length=100)
description = models.TextField()
tags = TaggableManager()
comment = models.TextField(null=True, blank=True)
user_inserted = models.ForeignKey(User, null=True, blank=True, related_name='user_inserted_ad')
date_inserted = models.DateTimeField(auto_now_add=True)
user_updated = models.ForeignKey(User, null=True, blank=True, related_name='user_updated_ad')
date_updated = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.name
class AdPrice(models.Model):
ad = models.ForeignKey(Ad)
name = models.CharField(max_length=50)
price = models.DecimalField(max_digits=6, decimal_places=2)
user_inserted = models.ForeignKey(User, null=True, blank=True, related_name='user_inserted_ad_price')
date_inserted = models.DateTimeField(auto_now_add=True)
user_updated = models.ForeignKey(User, null=True, blank=True, related_name='user_updated_ad_price')
date_updated = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.name
#classmethod
def create(cls, ad_id, name, price, date_inserted, user_inserted_id):
# Save price
new_register = AdPrice(ad_id=ad_id, name=name, price=price, date_inserted=date_inserted,
user_inserted=User.objects.get(id=user_inserted_id))
new_register.save()
# Add tags to Ad tags field
# AD SOME CODE HERE # To do
Admin:
class AdPriceInline(admin.TabularInline):
model = AdPrice
fieldsets = (
(None, {
'fields': ('name', 'price')
}),
)
class AdAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'title', 'telephone', 'comment',
'user_inserted', 'date_inserted', 'user_updated', 'date_updated')
fieldsets = (
(None, {
'fields': ('id', 'name', 'title', 'description', 'comment')
}),
)
inlines = (
AdPriceInline,
)
readonly_fields = ('id',)
def save_model(self, request, obj, form, change):
if change == False:
if getattr(obj, 'user_inserted', None) is None:
obj.user_inserted = request.user
super(AdAdmin, self).save_model(request, obj, form, change) # This line save the data on the Ad Model, now I have the pk to use bellow
obj.save()
# In the first insert, create a line in the AdHist model
# ad_status_id = 1 (Pending) | ad_change_reasons = 1(Insertion)
AdHist.create(ad_id=obj.id, datetime_begin=datetime.datetime.now(), datetime_end=None, ad_status_id=1, ad_change_reason_id=1,
user_inserted_id=request.user.id)
elif change == True:
if getattr(obj, 'user_updated', None) is None:
obj.user_updated = request.user
else:
obj.user_updated = request.user
if getattr(obj, 'date_updated', None) is None:
obj.date_updated = datetime.datetime.now()
else:
obj.date_updated = datetime.datetime.now()
obj.save()
def save_formset(self, request, form, formset, change):
if change == False:
instances = formset.save(commit=False)
for instance in instances:
if formset.model == AdPrice:
AdPrice.create(ad_id=instance.ad_id, name=instance.name, price=instance.price, date_inserted=datetime.datetime.now(),
user_inserted_id=request.user.id)
elif change == True:
for form in formset.forms:
None #form.instance.ad_id
formset.save()
A tricky part about overriding a model's save() method is that it doesn't get used in bulk operations, like bulk_create. Django recommends using a signal for a case like this (see the docs on how to set them up in your app).
Your signal might look something like this:
from django.db.models.signals import post_save
from .models import YourModel
def handle_model_post_save(sender, **kwargs):
# Ensure we are only running this code on create
if kwargs['created'] and kwargs['instance']:
# Your logic goes here
With this, the code in the signal will only run when a new instance of your model is created, not when existing instances are updated.