I am converting a UNIX date to a string date and passing it as a custom read-only field. What would be the best way to use django-filter to be able to filter this custom field? The error I get is Cannot resolve keyword 'convert_time' into the field. Choices are:
Models class
class AccountT(models.Model):
created_date_t = models.BigIntegerField(blank=True, null=True)
def convert_time(self):
result = time.strftime("%D", time.localtime(self.created_date_t))
return result
Serializer Class
class AccountTSerializer(serializers.ModelSerializer):
created_date = serializers.ReadOnlyField(source='convert_time')
class Meta:
model = AccountT
fields = ('othermodelfield','othermodelfield', 'created_date',)
ListAPIView
class AccountTListView(generics.ListAPIView):
serializer_class = AccountTSerializer
queryset = AccountT.objects.all()
filter_backends = (filters.DjangoFilterBackend, filters.OrderingFilter,)
filter_fields = ('othermodelfield','created_date_t')
The filterset_fields option is a shortcut that inspects model fields (not serializer fields) in order to generate filters. Since created_date isn't a model field, you'll need to manually declare a filter on a filterset_class. Declared filters can take advantage of the method argument, which will allow you to transform the incoming date into your timestamp. Something like...
# filters.py
from django_filters import rest_framework as filters
class AccountTFilter(filters.FilterSet):
# could alternatively use IsoDateTimeFilter instead of assuming local time.
created_date = filters.DateTimeFilter(name='created_date_t', method='filter_timestamp')
class Meta:
model = models.AccountT
# 'filterset_fields' simply proxies the 'Meta.fields' option
# Also, it isn't necessary to include declared fields here
fields = ['othermodelfield']
def filter_timestamp(self, queryset, name, value):
# transform datetime into timestamp
value = ...
return queryset.filter(**{name: value})
# views.py
class AccountTListView(generics.ListAPIView):
filterset_class = filters.AccountTFilter
...
Note: The old filter_* options have since been renamed to filtserset_*.
class AccountT(models.Model):
created_date_t = models.BigIntegerField(blank=True, null=True)
created_date = models.DateField(null=True)
def save(self, *args, **kwargs):
self.created_date = self.convert_time()
return super(IncomeExpense, self).save(*args, **kwargs)
def convert_time(self):
result = time.strftime("%D", time.localtime(self.created_date_t))
return result
class AccountTListView(generics.ListAPIView):
serializer_class = AccountTSerializer
queryset = AccountT.objects.all()
filter_backends = (filters.DjangoFilterBackend, filters.OrderingFilter,)
filter_fields = ('othermodelfield','created_date')
class AccountTFilter(FilterSet):
class Meta:
model = AccountT
fields = {
'created_date': ['gte', 'lte'],
}
class AccountTListView(generics.ListAPIView):
serializer_class = AccountTSerializer
queryset = AccountT.objects.all()
filter_backends = (filters.DjangoFilterBackend, filters.OrderingFilter,)
filter_class = AccountTFilter
To achieve this I would create a custom field on the serializer (ConvertTimeField):
import time
from .models import AccountT
from rest_framework import serializers
# Custom serializer field
# https://www.django-rest-framework.org/api-guide/fields/#custom-fields
class ConvertTimeField(serializers.ReadOnlyField):
"""
Convert time Field.
"""
def to_representation(self, value):
return time.strftime("%D", time.localtime(value))
def to_internal_value(self, data):
"""
Here is where you could convert the incoming value
(It's only needed if you want to perform modification before writing to the database)
"""
# Serializer
class AccountTSerializer(serializers.ModelSerializer):
created_date = ConvertTimeField(source='created_date_t')
class Meta:
model = AccountT
fields = ('othermodelfield', 'othermodelfield', 'created_date')
NOTE: You still need to pass your filter argument in the same format i.e UNIX epoch when you filter. If you need to change that you can convert your filter query param as suggested here: https://www.django-rest-framework.org/api-guide/filtering/#filtering-against-query-parameters. (Although, there are also other ways you could try accomplish that)
Related
I'm using DjangoFilterBackend to handle query parameters on a ModelViewSet. My expectation would be that if nothing in the table matches one of the query params, that it would just return an empty array.
Instead it returns:
{
"document_guid": [
"Select a valid choice. That choice is not one of the available choices."
]
}
Which just seems like an odd response to me so I'm wondering: how can I change this behavior?
EDIT: To clarify changing the behavior, is there a way to change it in DjangoFilterBackend as opposed to overriding the behavior in the views.py?
Would much rather just have the following indicating nothing matched:
[]
I'm aware of this similar question, but I'm already doing what they suggested:
https://stackoverflow.com/a/62219220/3123109
Here's the URI where there are no records that match document_guid (but there are some that match user_guid):
localhost/api/d2u/?user_guid=55d57235-79b0-ec11-9840-000d3a5a343c&document_guid=df4c9c11-49dd-ec11-a7b6-000d3a3667a1
Here's the related code:
# views.py
class Document2UserSyncViewSet(viewsets.ModelViewSet):
filter_backends = [DjangoFilterBackend]
filter_fields = [
'id',
'document_guid',
'user_guid'
]
permission_classes = [PowerAutomatePermissions]
queryset = Document2User.objects.all().order_by('id')
serializer_class = Document2UserSerializer
# serializer.py
class Document2UserSerializer(serializers.ModelSerializer):
"""Serializer for the d2u model.
Returns all fields.
"""
class Meta:
model = Document2User
fields = '__all__'
# models.py
class Document2User(models.Model):
document_guid = models.ForeignKey(
Documents,
to_field='guid',
on_delete=models.CASCADE,
db_column='document_guid'
)
user_guid = models.ForeignKey(
CRMFields,
to_field='guid',
on_delete=models.CASCADE,
db_column='user_guid'
)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'document_to_user'
unique_together = [['user_guid', 'document_guid']]
I talked to the developer who suggested doing the following which has worked for me. This will return an empty array when there are not matches instead of that message.
# utils/filters.py
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import serializers
class StrictDjangoFilterBackend(DjangoFilterBackend):
"""Return no results if the query doesn't validate.
"""
def filter_queryset(self, request, queryset, view):
try:
return super().filter_queryset(request, queryset, view)
except serializers.ValidationError:
return queryset.none()
# documents/views.py
from utils.filters import StrictDjangoFilterBackend
class Document2UserSyncViewSet(viewsets.ModelViewSet):
filter_backends = [StrictDjangoFilterBackend]
filter_fields = [
'id',
'document_guid',
'user_guid'
]
permission_classes = [PowerAutomatePermissions]
queryset = Document2User.objects.all().order_by('id')
serializer_class = Document2UserSerializer
I need to create a DRF list view that shows each course along with a boolean field signifying whether the user requesting the view is subscribed to the course.
Course subscriptions are stored in the following model:
class Subscription(models.Model):
user = models.ForeignKey(
CustomUser, related_name='subscriptions', null=False,
on_delete=CASCADE)
course = models.ForeignKey(
Course, related_name='subscriptions', null=False,
on_delete=CASCADE)
class Meta:
ordering = ['course', 'user']
unique_together = [['user', 'course']]
This is the view I am writing:
class CourseListView(generics.ListAPIView):
permission_classes = [IsAuthenticated, ]
queryset = Course.objects.all()
serializer_class = CourseSerializer
def isSubscribed(self, request, course):
sub = Subscription.objects.filter(
user=request.user, course=course).first()
return True if sub else False
def list(self, request, format=None):
queryset = Course.objects.all()
serializer = CourseSerializer(queryset, many=True)
return Response(serializer.data)
I am looking for a way to modify the list method, so as to add to the response the information about whether request.user is subscribed to each of the courses.
The best solution I have for now is to construct serializer manually, which would look (at the level of pseudo-code) something like this:
serializer = []
for course in querySet:
course['sub'] = self.isSubscribed(request, course)
serializer.append(CourseSerializer(course))
I suspect there should be a better (standard, idiomatic, less convoluted) way for adding a custom field in a list view, but could not find it. In addition, I am wondering whether it is possible to avoid a database hit for every course.
You can do that easily with Exists:
just change your queryset in your view:
from django.db.models import Exists, OuterRef
class CourseListView(generics.ListAPIView):
permission_classes = [IsAuthenticated, ]
serializer_class = CourseSerializer
def get_queryset(self):
subquery = Subscription.objects.filter(user=request.user, course=OuterRef('id'))
return Course.objects.annotate(sub=Exists(subquery))
and add a field for it in your serializer:
class CourseSerializer(serializers.ModelSerializer):
sub = serializers.BooleanField(read_only=True)
class Meta:
model = Course
fields = '__all__'
view.py
class charity_totals(generics.ListAPIView):
serializer_class= CharityTotalSerializer
queryset=Transaction.objects.all()
def get_queryset(self):
queryset = super().get_queryset()
user_id = self.request.GET.get('userID')
if user_id is None:
return queryset
queryset = queryset.filter(userID=user_id)
return queryset.values('charityID').annotate(total_donation=Sum('transactionAmount'))
serializer.py
class CharityTotalSerializer(ModelSerializer):
charity_name= serializer.ReadOnlyField(source='charityID.charityName')
total_donation= serializer.DecimalField(max_digits=64,decimal_places=2)
class Meta:
model = Transaction
fields = ['charity_name','total_donation']
model
class Transaction(models.Model):
transactionAmount = models.DecimalField(max_digits=6, decimal_places=2)
userID = models.ForeignKey(User,on_delete=models.CASCADE)
charityID = models.ForeignKey(Charity,on_delete=models.CASCADE, related_name='charity_set')
processed = models.BooleanField(auto_created=True, default=False)
transactionDate = models.DateField(auto_now_add=True)
Off of a request such as this http://localhost:8000/requests/charitytotal/?userID=1 my json response is limited to just the [{"total_donation":"3.00"},{"total_donation":"17.00"}] and is not including the charity names that are specified in the serializer. From what I understand the .values should return a dict of both the charityID and the total_donation that was specified which should be able to interact with my serializer. Any Insight would be appreciated
That is because you are returning Values queryset from get_queryset method when you are getting the value of userID in request.GET. Also I assume it is important for you return that way so that you can group by and sum values of total donation. So, I think you can approach something like this:
First, change get_queryset method to return the name of the charity annotated:
from django.db.models import F, Sum
...
def get_queryset(self):
queryset = super().get_queryset()
user_id = self.request.GET.get('userID')
if user_id is not None:
queryset = queryset.filter(userID=user_id)
return queryset.values('charityID').annotate(total_donation=Sum('transactionAmount')).annotate(charity_name=F('charityID__charityName'))
Then update the serializer like this:
class CharityTotalSerializer(ModelSerializer):
charity_name= serializer.ReadOnlyField() # no need to define source
total_donation= serializer.DecimalField(max_digits=64,decimal_places=2)
class Meta:
model = Transaction
fields = ['charity_name','total_donation']
Also, better to have charityName unique(using unique=True in models), so that it does not produce confusing results.
You have to put it into Serializer and it will work fine.
charity_name = serializer.SerializerMethodField()
def get_charity_name(self, instance):
return instance.charityID.name
I am accessing the related field's data in my SerializerMethodField and there is a query for every object that is being rendered. My models look like (will keep short for brevity):
class Listing(models.Model):
variant = models.ForeignKey(to='Variant', related_name='variant_listings')
seller = models.ForeignKey(to='user.Seller', related_name='seller_listings')
locality = models.ForeignKey(to='user.Locality', blank=True, null=True)
price = models.IntegerField(blank=True, null=True)
Variant, Seller and Locality are all related models.
My Viewset:
class ListingViewSet(viewsets.ModelViewSet):
"""Viewset class for Listing model"""
queryset = Listing.objects.all()
serializer_class = ListingSerializer
pagination_class = TbPagination
filter_backends = (filters.DjangoFilterBackend,)
filter_class = ListingFilter
def get_queryset(self):
listing_qs = Listing.objects.filter(status='active')
listing_qs = ListingSerializer.setup_eager_loading(listing_qs)
return listing_qs
And my serializer:
class ListingSerializer(serializers.ModelSerializer):
"""Serializer class for Listing model"""
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related('variant', 'seller', 'locality')
return queryset
#staticmethod
def get_car_link(obj):
variant_name_slug = obj.variant.name.replace(' ', '-').replace('+', '')
return '/buy-' + obj.seller.city.name.lower() + '/' + variant_name_slug
car_link = serializers.SerializerMethodField(read_only=True)
#staticmethod
def get_car_info(obj):
return {
'id': obj.id,
'variant_name': obj.variant.name,
'localities': obj.locality.name,
}
car_info = serializers.SerializerMethodField(read_only=True)
#staticmethod
def get_image_urls(obj):
caption_ids = [1, 2, 3, 5, 7, 8, 18]
attachments_qs = Attachment.objects.filter(listing_id=obj.id, caption_id__in=caption_ids)
image_urls = []
for attachment in attachments_qs:
url = str(obj.id) + '-' + str(attachment.file_number) + '-360.jpg'
image_urls.append(url)
return image_urls
image_urls = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Listing
fields = ('car_link', 'car_info', 'sort_by', 'image_urls')
For each listing returned by the listing viewset, there is a query for every related field accessed in the SerializerMethodField.
I found some related questions like this. But that didn't help. Also, I tried doing prefetch_related on my get_queryset method of the viewset and also implemented eager loading with the help of this article. But nothing helped.
Is there any way to avoid these queries?
Edit
The get_car_info function written above, contains a few more fields (along with the ones already present) which are required separately in a nested JSON by the name of car_info in the final serialized data that is being rendered at the front end.
I have used this article:
http://ses4j.github.io/2015/11/23/optimizing-slow-django-rest-framework-performance/
I created a set up eager loading method in my serializer, like so:
class EagerGetProjectSerializer(serializers.ModelSerializer):
lead_researcher = UserSerializer()
participating_researcher = UserSerializer(many=True)
client = ProjectClientSerializer()
test_items = TestItemSerializer(many=True)
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.select_related('lead_researcher', 'client')
queryset = queryset.prefetch_related('participating_researcher',
'test_items')
return queryset
class Meta:
model = Project
fields = '__all__'
Notice that when referencing the objects you want to pull in the serializer you have to use the foreign key related name attribute.
and called in my view, before accessing the serializer:
class SingleProject(APIView):
def get(self, request):
ans = Project.objects.filter(id=project_id)
qs = EagerGetProjectSerializer.setup_eager_loading(ans)
serializer = EagerGetProjectSerializer(qs, many=True)
I am trying to figure out the best way to add annotated fields, such as any aggregated (calculated) fields to DRF (Model)Serializers. My use case is simply a situation where an endpoint returns fields that are NOT stored in a database but calculated from a database.
Let's look at the following example:
models.py
class IceCreamCompany(models.Model):
name = models.CharField(primary_key = True, max_length = 255)
class IceCreamTruck(models.Model):
company = models.ForeignKey('IceCreamCompany', related_name='trucks')
capacity = models.IntegerField()
serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
class Meta:
model = IceCreamCompany
desired JSON output:
[
{
"name": "Pete's Ice Cream",
"total_trucks": 20,
"total_capacity": 4000
},
...
]
I have a couple solutions that work, but each have some issues.
Option 1: add getters to model and use SerializerMethodFields
models.py
class IceCreamCompany(models.Model):
name = models.CharField(primary_key=True, max_length=255)
def get_total_trucks(self):
return self.trucks.count()
def get_total_capacity(self):
return self.trucks.aggregate(Sum('capacity'))['capacity__sum']
serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
def get_total_trucks(self, obj):
return obj.get_total_trucks
def get_total_capacity(self, obj):
return obj.get_total_capacity
total_trucks = SerializerMethodField()
total_capacity = SerializerMethodField()
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
The above code can perhaps be refactored a bit, but it won't change the fact that this option will perform 2 extra SQL queries per IceCreamCompany which is not very efficient.
Option 2: annotate in ViewSet.get_queryset
models.py as originally described.
views.py
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.all()
serializer_class = IceCreamCompanySerializer
def get_queryset(self):
return IceCreamCompany.objects.annotate(
total_trucks = Count('trucks'),
total_capacity = Sum('trucks__capacity')
)
This will get the aggregated fields in a single SQL query but I'm not sure how I would add them to the Serializer as DRF doesn't magically know that I've annotated these fields in the QuerySet. If I add total_trucks and total_capacity to the serializer, it will throw an error about these fields not being present on the Model.
Option 2 can be made work without a serializer by using a View but if the model contains a lot of fields, and only some are required to be in the JSON, it would be a somewhat ugly hack to build the endpoint without a serializer.
Possible solution:
views.py
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.all()
serializer_class = IceCreamCompanySerializer
def get_queryset(self):
return IceCreamCompany.objects.annotate(
total_trucks=Count('trucks'),
total_capacity=Sum('trucks__capacity')
)
serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
total_trucks = serializers.IntegerField()
total_capacity = serializers.IntegerField()
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
By using Serializer fields I got a small example to work. The fields must be declared as the serializer's class attributes so DRF won't throw an error about them not existing in the IceCreamCompany model.
I made a slight simplification of elnygreen's answer by annotating the queryset when I defined it. Then I don't need to override get_queryset().
# views.py
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.annotate(
total_trucks=Count('trucks'),
total_capacity=Sum('trucks__capacity'))
serializer_class = IceCreamCompanySerializer
# serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
total_trucks = serializers.IntegerField()
total_capacity = serializers.IntegerField()
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
As elnygreen said, the fields must be declared as the serializer's class attributes to avoid an error about them not existing in the IceCreamCompany model.
You can hack the ModelSerializer constructor to modify the queryset it's passed by a view or viewset.
class IceCreamCompanySerializer(serializers.ModelSerializer):
total_trucks = serializers.IntegerField(readonly=True)
total_capacity = serializers.IntegerField(readonly=True)
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
def __new__(cls, *args, **kwargs):
if args and isinstance(args[0], QuerySet):
queryset = cls._build_queryset(args[0])
args = (queryset, ) + args[1:]
return super().__new__(cls, *args, **kwargs)
#classmethod
def _build_queryset(cls, queryset):
# modify the queryset here
return queryset.annotate(
total_trucks=...,
total_capacity=...,
)
There is no significance in the name _build_queryset (it's not overriding anything), it just allows us to keep the bloat out of the constructor.