Django filter by two parameters - python

I want to filter by two parameters. I have this:
def get_queryset(self):
queryset = Product.objects.filter(
Q(category__in=self.request.GET.getlist("category")) &
Q(brand__in=self.request.GET.getlist("brand")))
return queryset
But this works only if two filters are chosen(otherwise it returns nothing). I want my program to filter by one filter if only one is chosen and filter by two parameters if two are chosen. How can I do that?

def get_queryset(self):
queryset = Product.objects.all()
if "category" in self.request.GET:
queryset = queryset.filter(category__in=self.request.GET.getlist("category"))
if "brand" in self.request.GET:
queryset = queryset.filter(brand__in=self.request.GET.getlist("brand"))
return queryset
Most methods applied to a queryset will return a new queryset. This allows you to add more parameters as required. As the queryset is lazy, it doesn't actually query the database until you start consuming data so there's no performance overhead to the above either.

You can do this.
def get_queryset(self):
kwargs = {}
if self.request.GET.getlist("category"):
kwargs["category__in"] = self.request.GET.getlist("category")
if self.request.GET.getlist("brand"):
kwargs["brand__in"] = self.request.GET.getlist("brand")
return Product.objects.filter(**kwargs)

You can actually write model manager for custom query_sets. For more information you can read the documentation: https://docs.djangoproject.com/en/3.0/topics/db/managers/#calling-custom-queryset-methods-from-the-manager
class ProductQuerySet(models.query.QuerySet):
def active(self):
return self.filter(active=True)
def search(self, query):
lookups = (Q(title__icontains=query) |
Q(description__icontains=query) |
Q(price__icontains=query) |
Q(tag__title__icontains=query)) # (model_name)__(model_field)__(filter)
return self.filter(lookups).distinct()
class ProductManager(models.Manager):
def get_queryset(self):
return ProductQuerySet(self.model, using=self._db)
def active(self):
return self.get_queryset().active()
def search(self, query):
return self.get_queryset().active().search(query)
In Views.py:
def get_queryset(self, *args, **kwargs):
request = self.request
query = request.GET.get('q', None)
if query is not None:
query = query.strip()
return Product.objects.search(query)
return Product.objects.featured()
I hope that will help you. Good Luck

Related

Django Rest Framework: Sum of serializermethodfields

I'm having trouble getting this to work.. I have the following Serializer:
class OwnArbeitstagListSerializer(serializers.ModelSerializer):
stundensumme = serializers.SerializerMethodField()
class Meta:
model = Arbeitstag
ordering = ['-datum']
fields = ('id', 'datum', 'in_abrechnung', 'stundensumme')
depth=0
def get_stundensumme(self, obj):
return Stunden.objects.filter(arbeitstagid=obj.id).aggregate(Sum('stunden'))['stunden__sum']
.. returning the sum of worked hours per day (The model is named "workday"). That works so far. Now I want to have a ModelViewset returning a list of workdays:
class OwnArbeitstagListViewSet(viewsets.ReadOnlyModelViewSet):
filter_class = ArbeitstagListFilter
filter_fields = ('datum',)
filter_backends = (DjangoFilterBackend, filters.OrderingFilter, filters.SearchFilter,)
ordering =["-datum"]
serializer_class = OwnArbeitstagListSerializer
def get_queryset(self):
return Arbeitstag.objects.filter(userid=self.request.user.id)
You see, I'm filtering it by User and by date (with the filterbackend). But now, I want to have an additional field which gives me the sum of the serializermethodfield "stundensumme". Its a sum of a sum. And it should only calculate the sum of the displayed objects (with datefilter applied).
I'm having trouble because (I assume) the Seriealizermethodfield only gets calculated when serializing, and I guess thats to late to get the values for my sum. I have tried this, but it cant find the serializermethodfield "stundensumme" to calculate a sum of:
class CustomPageNumberPagination(PageNumberPagination):
def get_paginated_response(self, data, summe):
return Response(OrderedDict([
('count', self.page.paginator.count),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('summe', summe),
('results', data)
]))
class OwnArbeitstagListViewSet(viewsets.ReadOnlyModelViewSet):
pagination_class = CustomPageNumberPagination
filter_class = ArbeitstagListFilter
filter_fields = ('datum',)
filter_backends = (DjangoFilterBackend, filters.OrderingFilter, filters.SearchFilter,)
ordering =["-datum"]
serializer_class = OwnArbeitstagListSerializer
def get_queryset(self):
arbeitstag = Arbeitstag.objects.all().filter(userid=self.request.user.id)
return arbeitstag
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
self.summe = queryset.aggregate(Sum('stundensumme'))['stundensumme__sum']
return super().list(request, *args, **kwargs)
def get_paginated_response(self, data):
return self.paginator.get_paginated_response(data, self.summe)
If you take a look on list() method implementation you will find this:
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
You are right with your guess that stundensumme field only get pouplauted with serialization so you have two options:
use anotate() and add it in you defiend get_queryset() so each object will have that field and you don't need to provide an implementation for the field at serializer and you will just add the field in fields attribute as you did
in get_paginated_response() do your sum on data as it's a list returend by the serializer so you can easily manipulate it for example:
def get_summe(self, data):
sum = 0
for item in data:
sum += item['stundensumme']
return
and simply use it
def get_paginated_response(self, data):
return Response(OrderedDict([
('count', self.page.paginator.count),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('summe', self.get_summe(data)),
('results', data)
]))
and don't forgot to remove the extra code you added since we don't need it in either two ways

Serialize two querysets with DjangoRestFramework

I'm trying to serialize more than one queryset, but I noticed that only one of them becomes serialized. This is the approach I'm currently trying.
class GetDetails(APIView):
def get(self, request):
todays_date = time.strftime('%Y-%m-%d')
#Get results according to specified criteria
queryset = People.objects.filter(date = todays_date, assigned_to = 1)
#Check if any querysets are available
if queryset:
#Iterate through each queryset, serialize and return a response
for person in queryset:
serializer=ASerializer(person)
return Response(serializer.data)
else:
return Response({'TODO':'TODO'})
First of all the way you are performing queryset is not right, you are iterating over queryset with person variable and you are not using person variable at all.
And for the question, use many=True attribute of serializer. It will create a list of serialized items for you.
like this:
class GetDetails(APIView):
def get(self, request):
todays_date = time.strftime('%Y-%m-%d')
queryset = People.objects.filter(date = todays_date, assigned_to = 1)
return Response(ASerializer(many=True).to_representation(queryset))
Maybe your code should be like this:
def get(self, request):
todays_date = time.strftime('%Y-%m-%d')
queryset = People.objects.filter(date = todays_date, assigned_to = 1)
if queryset.exists(): # Lazy check if entry Exists.
serializer=ASerializer(queryset, many=True)
return Response(serializer.data)
else:
return Response({'TODO':'TODO'})

Combine ListModelMixin with APIView to show pagination

I want to show the pagination feature in my API and I am using APIView with multiple serializers.
I know it is very easy to show pagination with ListView.
I have seen somewhere that combining ListModelMixin and APIView works but if my code is as follows:
class ListModelMixin(object):
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serilaizer.data)
class ItemsAPIView(APIView):
permission_classes = (permissions.IsAuthenticated,)
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
def get(self, request, format=None):
"""
Return a list of all devices of this user.
"""
reply = {}
try:
products = BaseItem.objects.owned_items().filter(owner=request.user)
reply['data'] = OwnedItemSerializer(products, many=True).data
items = BaseItem.objects.dev_items().filter(owner=request.user)
reply['data'].extend(ItemSerializer(items, many=True).data)
except:
reply['data'] = []
return Response(reply, status.HTTP_200_OK)
How can I combine them so I can get paginated results?
Thanks in advance!
First things first, what you are currently doing is too complex without reason.
In order to achieve a "paginatable" queryset, it is preferred to change your owned_items() and dev_items() in simple filter combinations, rather than model methods. To clarify by example:
products = BaseItem.objects.filter(owner=request.user, owned=True)
instead of
products = BaseItem.objects.owned_items().filter(owner=request.user)
That way, you can produce a single queryset which will be easier to paginate:
user_items = BaseItem.objects.filter(
Q(owner=request.user, owned=True) |
Q(owner=request.user, dev=True)
)
Note 1: You can simplify things further if you like, but that gets out of scope of your question. As food for thought, check this out:
user_items = BaseItem.objects.filter(owner=request.user).distinct()
Note 2: You should use a single serializer for a single model because what you are doing adds complexity without benefit (high risk-low reward situation)
With the above mentioned and assumed:
There are some ways to achieve what you want here:
By utilizing GeneriAPIView and ListModelMixin you can refactor your class in such a way to have a .list() method with auto-pagination:
from rest_framework import mixins, generics
class ItemsAPIView(mixins.ListModelMixin, generics.GenericAPIView,):
permission_classes = (permissions.IsAuthenticated,)
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
serializer_class = OwnedItemSerializer
# You can define .get in here if you really need it.
# You can also override .list to add specific functionality
If you don't want to use the above, and you want to keep your APIView, then you can keep your get method and provide pagination for it as mentioned in this Q&A example:
class ItemsAPIView(APIView):
permission_classes = (permissions.IsAuthenticated,)
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
serializer_class = MyNewUnifiedSerializerClass
def get(self, request):
user_items = BaseItem.objects.filter(
owner=request.user
).distinct()
page = self.paginate_queryset(user_items)
if page is not None:
serializer = self.serializer_class(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(user_items, many=True)
return Response(serializer.data)
# Now add the pagination handlers taken from
# django-rest-framework/rest_framework/generics.py
#property
def paginator(self):
"""
The paginator instance associated with the view, or `None`.
"""
if not hasattr(self, '_paginator'):
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
return self._paginator
def paginate_queryset(self, queryset):
"""
Return a single page of results,
or `None` if pagination is disabled.
"""
if self.paginator is None:
return None
return self.paginator.paginate_queryset(
queryset,
self.request,
view=self
)
def get_paginated_response(self, data):
"""
Return a paginated style `Response` object
for the given output data.
"""
assert self.paginator is not None
return self.paginator.get_paginated_response(data)

Optimizing djangos count() method

So I have this queryset:
from django.contrib.gis.db.models.query import GeoQuerySet
from django.db import models as base_models
class RestaurantsQuerySet(GeoQuerySet):
def get_list(self, lng, lat):
reference_point = Point(lng, lat, srid=SRID)
return self.annotate(rating=models.Avg('comments__rating'))\
.annotate(distance=Distance('location', reference_point))
def count(self):
return self.values('id').aggregate(count=base_models.Count('id'))['count']
I thought that the query would look something like:
SELECT COUNT("__col1")
FROM (
SELECT "restaurants_restaurant"."id" AS "__col1"
FROM "restaurants_restaurant"
GROUP BY "restaurants_restaurant"."id") subquery
And instead django orm creates this little monstrosity:
SELECT COUNT("__col1")
FROM (
SELECT "restaurants_restaurant"."id" AS Col1, "restaurants_restaurant"."id" AS "__col1"
FROM "restaurants_restaurant"
LEFT OUTER JOIN "comments_comment" ON ("restaurants_restaurant"."id" = "comments_comment"."restaurant_id")
GROUP BY "restaurants_restaurant"."id", ST_Distance_Sphere("restaurants_restaurant"."location",
ST_GeomFromEWKB('\x0101000020e61000003eb555a41d2d4b405a338d81d0a73240'::bytea
))) subquery
First method to be called is get_list. It looks as if django would "remember" that call and that the qs was annotated with rating and distance and places it also into the count query. So I guess the question is - how do I "reset" this queryset to the state before annotating it?
EDIT:
Okay, seems my question was not complete. I also have a RestaurantsList view defined as follows:
class RestaurantList(generics.ListAPIView):
def get_queryset(self):
return Restaurant.objects.get_list(self._lng, self._lat)
I took a look into the entrails of django-rest-framework and I can see this:
class ListModelMixin(object):
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
So it looks like it always uses the queryset that was returned from the get_queryset method and since it's annotated with distance and rating it ends up being included in the count query. Still no solution to this...
After browsing through django-rest-framework code I came up with this idea:
Override default Paginator class for a given view:
class RestaurantList(generics.ListAPIView):
pagination_class = custom.LimitOffsetPagination
Create custom pagination class:
class LimitOffsetPagination(pagination.LimitOffsetPagination):
def __init__(self):
self._countable_queryset = None
self._was_counted = False
def was_initialized(self):
return self._countable_queryset is not None
def set_raw_queryset_for_count(self, queryset: QuerySet):
self._countable_queryset = queryset
def paginate_queryset(self, queryset, request, view=None):
self.limit = self.get_limit(request)
if self.limit is None:
return None
self.offset = self.get_offset(request)
self.count = self._countable_queryset.count()
self.request = request
if self.count > self.limit and self.template is not None:
self.display_page_controls = True
return list(queryset[self.offset:self.offset + self.limit])
Override views paginator property:
#property
def paginator(self):
paginator = super().paginator
if not paginator.was_initialized():
paginator.set_raw_queryset_for_count(Restaurant.objects.all())
return paginator
Now the count query will look a bit more friendly
SELECT COUNT(*) AS "__count" FROM "restaurants_restaurant"

Bulk set properties to django queryset

I have model like this:
class MyModel(models.Model):
#fields
def __init__(self, *args, **kwargs):
self._issue_status = None
super(...).__init__(...)
#property
def issue_status(self):
if self._issue_status is None:
self.get_issue_status()
return self._issue_status
where get_issue_status goes to api and take status. But api can bulk return statuses and i want to use it when get queryset of MyModel.
So i write own manager:
class MyModelManager(models.Manager):
def get_queryset(self):
queryset = super(...).get_queryset()
issues = self.bulk_get_issues()
set_issue_statuses(queryset)
return queryset
It works nice for getting all objects. But when i try to filter queryset - status property for every object is None. How can i fix that? Thank.
I think you want to override your get_queryset function,
something like:
def get_queryset(self, request):
qs = super(...).get_queryset(request)
the_filter = ["None", "", "SKIPPED", ]
return qs.exclude(status__in=the_filter)

Categories

Resources