DRF serializer filtering - django

I have a serializer that gives me everything fine.
ModelClassASerializer((serializers.ModelSerializer)):
.....
status = serializers.SerializerMethodField()
def get_status(self, obj):
....
status = ModelB.objects.get(id=obj.id).status
....
return status
class Meta:
model = ModelClassA
fields = (...)
But if I want to make a filtering based on that status, I can't. I am using django_filters.rest_framework.FilterSet for the filtering. There is no relation between models.
What is the best way to do that filtering?

It looks like objects in ModelA share the same IDs as those in ModelB. If that's the case, you can use a subquery to match on IDs. If the IDs do not correspond with each other, then this query will be nonsensical. You want to create the following queryset:
from django.db.models import Subquery
from myapp.models import ModelA, ModelB
pks = ModelB.objects.filter(status='foo').values('pk')
ModelA.objects.filter(pk__in=Subquery(pks))
To create the above will django-filter, you'll need to use the method argument on a filter.
from django_filters import rest_framework as filters
class ModelAFilter(filters.FilterSet):
status = filters.ChoiceFilter(choices=(('foo', 'Foo'), ...), method='filter_status')
class Meta:
model = ModelA
fields = []
def filter_status(self, queryset, name, value):
pks = ModelB.objects.filter(status=value).values('pk')
return queryset.filter(pk__in=Subquery(pks))

Related

Speeding up Django Rest Framework Model Serializer N+1 Query problem

I have a DRF ModelSerializer class that serializes anOrder model. This serializer has a field:
num_modelA = serializers.SerializerMethodField()
`
def get_num_modelA(self, o):
r = ModelA.objects.filter(modelB__modelC__order=o).count()
return r
Where ModelA has a ForeignKey field modelB, ModelB has a ForeignKey field modelC, and ModelC has a ForeignKey field order.
The problem with this is obviously that for each order that gets serialized it makes an additional query to the DB which slows performance down.
I've implemented a static method setup_eager_loading as described here that fixed the N+1 query problem for other fields I was having.
#staticmethod
def setup_eager_loading(queryset):
# select_related for "to-one" relationships
queryset = queryset.select_related('modelD','modelE')
return queryset
My idea was I could use prefetch_related as well to reduce the number of queries. But I am unsure how to do this since Order and ModelA are separated by multiple foreign keys. Let me know if any other information would be useful
You can work with an annotation:
from django.db.models import Count
# …
#staticmethod
def setup_eager_loading(queryset):
# select_related for "to-one" relationships
return queryset.select_related('modelD','modelE').annotate(
num_modelA=Count('modelC__modelB__modelA')
)
in the serializer for your Order, you can then use num_modelA as an IntegerField:
from rest_framework import serializers
class OrderSerializer(serializers.ModelSerializer):
num_modelA = serializers.IntegerField()
class Meta:
model = Order
fields = ['num_modelA', 'and', 'other', 'fields']

Django Rest Framework - Filter by custom field

I have Serializer which defines a custom column, in the serializers.py which I am trying to filter a query on.
The itemsoutstanding column example below, I am trying to filter it from my view but it returns "is not defined"
class OverView(serializers.ModelSerializer):
itemsoutstanding = serializers.SerializerMethodField()
class Meta:
model = Order
fields = ['id','total_price', 'created_at','itemsoutstanding']
def get_itemsoutstanding(self, obj):
count= Items.objects.filter(order=obj.id).count()
return count
In my view I am trying to filter on the serializer column, but it says it's not defined
queryset = Order.objects.all()
serializer_class = OverView
queryset = Order.objects.filter(shop=shop)
queryset = queryset.filter(itemsoutstanding> 0)
Is there any way to filter based on the serializer columns?
You will need to annotate the queryset in the definition, you may need to check the django docs for annotations and probably adjust this code to your models but the idea is this:
from django.db.models import Count
queryset = Order.objects.annotate(itemsoutstanding=Count("items"))
Then you can use the itemsoutstanting in the filter and in the serializer as a field:
class OverView(serializers.ModelSerializer):
class Meta:
model = Order
fields = ['id','total_price', 'created_at','itemsoutstanding']
The queryset has nothing to do with the serializer, so it does not know of your defined field in the serializer. That custom field is only added to the serialized data and not the queryset. What you need to do is annotate the field in the view when you get the queryset.

Serialize foreign key object that is AbstractUser

I am serializing a model using the Django REST framework successfully, but would like to add a field from a related model. I have seen other posts describe how to do this using nested serializers, however mine is different because the other model I am trying to access is an AbstractUser class.
I would like to serialize the UserDefinedEquipName field from CustomUser.
models (some fields removed for clarity):
accounts/models.py
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
UserDefinedEquipName = models.CharField(max_length=50, default = "Default equip",)
....
builds/models.py
from accounts.models import CustomUser
from django.contrib.auth import get_user_model
class Build(models.Model):
author = models.ForeignKey(get_user_model(),on_delete=models.CASCADE,)
machineName = models.OneToOneField(max_length=50,blank=True,)
....
So my thought is to pass the value into the serializer but can't seem to figure out how to access the value without getting error AttributeError: type object 'Build' has no attribute 'CustomUser'
I have tried:
My serializers.py:
from rest_framework import serializers
from .models import Data, Build, CustomUser
from django.contrib.auth.models import AbstractUser
class buildStatsAPI_serializer(serializers.ModelSerializer):
equipName = Build.CustomUser.UserDefinedEquipName
#also tried:
#equipName = Build.CustomUser__set.all()
class Meta:
fields = ('id','author','machineName','equipName',)
model = Build
Am I missing something small here? Or is there a much better way of doing this. It seems like if this wasn't an AbstractUser class it would be much easier.
EDIT - Added views.py
class buildStatsAPI(generics.ListCreateAPIView):#for build stats JSON
permission_classes = (permissions.IsAuthenticated,)
serializer_class = buildStatsAPI_serializer
def get_queryset(self):
machinesOwned =CustomUser.objects.filter(customerTag=self.request.user.customerTag).filter(isDevice=True)
machineList = []
for machine in machinesOwned:
machineList = machineList + [machine.id]
query = Build.objects.filter(deleted=0, author_id__in=machineList,).values().order_by('pk')
return query
I think you are defining the Serializer improperly. You can't directly reference Model in a serializer. You need to use any kind of fields. For example, if you use SerializerMethodField, you can try like this:
class buildStatsAPI_serializer(serializers.ModelSerializer):
equipName = serializers.SerializerMethodField()
class Meta:
fields = ('id','author','machineName','equipName',)
model = Build
def get_equipName(self, obj):
# here obj is a build model object
return obj.author.UserDefinedEquipName
Update
Please update your get_queryset method so that it returns a queryset like this(I have refactored it a bit):
def get_queryset(self):
query = Build.objects.filter(deleted=0, author__customerTag=self.request.user.customerTag, author__isDevice=True) # removed .values() because it will return a dictionary
return query

Django - set filter field label or verbose_name

I'm displaying a table of data using django-tables2.
For filtering I'm using the solution from here:
How do I filter tables with Django generic views?
My problem is only that I can't set the labels for the filter form. This is also imposible to google as words "django, form, filter, label" are quite general :(
My filter class:
import django_filters as filters
from models import Sale
class SaleFilter(filters.FilterSet):
class Meta:
model = Sale
fields = ['CompanyProductID', 'CompanySellerID', 'CompanyRegisterID']
labels = {
'CompanyProductID': 'Article',
'CompanySellerID': 'Seller',
'CompanyRegisterID': 'Cash register'
} #THIS IS NOT WORKING
To set custom labels you can do it this way. Not sure if it is a new functionality.
import django_filters as filters
from models import Sale
class SaleFilter(filters.FilterSet):
CompanyProdutID = filters.CharFilter(label='Article')
CompanySellerID = filters.CharFilter(label='Seller')
CompanyRegisterID = filters.CharFilter(label='Cash register')
class Meta:
model = Sale
fields = ['CompanyProductID', 'CompanySellerID', 'CompanyRegisterID']
Use the filter that you want for each field.
docs
Note:
for some reason
import django_filters as filters
filters.CharField(...)
is not working for me. I have to use it like this:
from django_filters import CharFilter
CharFilter(...)
Previous answer will duplicate the filter fields. Here is how to do it:
def __init__(self, *args, **kwargs):
super(SaleFilter, self).__init__(*args, **kwargs)
self.filters['CompanyProductID'].label="Article"
self.filters['CompanySellerID'].label="Seller"
self.filters['CompanyRegisterID'].label="Cash register"
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = ['manufacturer']
def __init__(self, *args, **kwargs):
super(ProductFilter, self).__init__(*args, **kwargs)
self.filters['manufacturer'].extra.update(
{'empty_label': 'All Manufacturers'})
I actually believe the op was asking about the "Label name". Not the field name. In order to do this simply do something like the following.
class Name_of_Filter(django_filters.FilterSet):
#example of how to set custom labels
your_field_name = django_filters.WhateverFilterYouWantHere(label='Whatever you want')
class Meta:
model = Your_Model_Here
fields = ['your_field_name']
#could also do something like '__all__' to get all the fields for that table just have to refer to your models to get the field name

Django M2M Filter QuerySet With Exact M2M Set

Suppose I have the following 3 models, where each of 2 of the models have a M2M relationship with the 3rd model.
class FirstModel(models.Model):
third_model = models.ManyToMany('ThirdModel')
class SecondModel(models.Model):
third_model = models.ManyToMany('ThirdModel')
class ThirdModel(models.Model):
pass
Now, suppose further I have a specific SecondModel object and a FirstModel QuerySet. I need to filter the QuerySet so that the resulting QuerySet contains only the FirstModel objects that have the exact same M2M relationship set with ThirdModel as the SecondModel object's M2M relationship set with ThirdModel.
def some_filtering_method(first_model_qs, second_model):
third_models_set = second_model.third_model_set.all()
first_model_ids = list()
for third_model in third_models_set:
first_model_ids.append(
[first_model.pk
for first_model in third_model.first_model_set.all()])
intersection_of_first_model_ids = get_intersection(first_model_ids)
return first_model_qs.filter(pk__in=intersection_of_first_model_ids)
Is there a more Pythonic way to do this in Django? I tried the following with no success (after reviewing the raw query it's obvious why it won't work).
import operator
from django.db.models import Q
def some_filtering_method(first_model_qs, second_model):
return first_model_qs.filter(
reduce(
operator.and_,
(Q(third_model_set__contains=x)
for x in second_model.third_model_set.all())
)
)
I'm not sure about the most Pythonic way, but the probably the most Djangonic way would be the use of a custom Manager.
In Django 1.6 and prior, you need to set up the Manager and QuerySet separately:
class SharedM2MSetQuerySet(models.QuerySet):
def shared(self):
pass
# your code here
class SharedM2MSetManager(models.Manager):
def get_queryset(self):
return PersonQuerySet(self.model, using=self._db)
def shared(self,other):
return self.get_queryset().shared(other)
class FirstModel(models.Model):
third_model = models.ManyToMany('ThirdModel')
manager = SharedM2MSetQuerySet.as_manager()
If you are using Django 1.7, you can shortcut a lot of this by using a QuerySet as a Manager, like so:
class SharedM2MSetQuerySet(models.Manager):
def shared(self,other):
pass
# your code here
class FirstModel(models.Model):
third_model = models.ManyToMany('ThirdModel')
manager = SharedM2MSetQuerySet.as_manager()
class SecondModel(models.Model):
third_model = models.ManyToMany('ThirdModel')
class ThirdModel(models.Model):
pass
In both cases, this then allows you do do things like:
my_obj = SecondModel.objects.first()
for i in FirstModel.objects.shared(my_obj):
do_the_thing(i)