Django model multiply filter - django

I want to filter my model object using two filters.
So, it can be only one filter or both or none.
My solution is to use a lot of 'if':
if _topic or _curator:
if _topic and _curator:
queryset = Article.objects.filter(topic=_topic,curator=_curator)
elif _curator:
queryset = Article.objects.filter(curator=_curator)
else # so topic is the last choice
queryset = Article.objects.filter(topic=_topic)
else
queryset = Article.objects.all()
Can someone suggest an easier way to filter?

Queryset filters are cumulative.
queryset = Article.objects.all()
if _topic:
queryset = queryset.filter(topic=_topic)
if _curator:
queryset = queryset.filter(curator=_curator)

kwargs = {}
if _topic:
kwargs[topic] = _topic
if _curator:
kwargs[curator] = _curator
queryset = Article.objects.filter(**kwargs)

Related

Django merge querysets in one page

In django rest framework i am using pagination of 20 on a model.
Now i want to make a queryset which takes 5 items which has the featured field as true.
The other 15 should be items which has the featured field as false.
Does anyone have an idea how to make this work?
permission_classes = [AllowAny]
serializer_class = JobSerializer
pagination_class = Pagination
order_types = {
1: 'job_order', #by randomized job order
2: '-created_date', #newest to oldest
3: 'created_date', #oldest to newest
4: 'name', #by name A -> Z,
5: '-name' #by name Z -> A,
}
def get_queryset(self):
requirements = self.request.query_params.getlist('requirements', None)
order_param = self.request.query_params.get('order_by', 1)
#make querysets
queryset_featured = Job.objects.all().filter(active=True, featured=True).order_by(self.order_types[int(order_param)])
queryset_non_featured = Job.objects.all().filter(active=True, featured=False).order_by(self.order_types[int(order_param)])
return queryset.distinct()
Use a union of two separate queries:
https://docs.djangoproject.com/en/3.1/ref/models/querysets/#union
This will allow you to make one ORM query for the featured items with [0:5], and then union to an ORM query for non-featured items with [0:15].
I haven't tested this, but here's an example with your code:
queryset_featured = Job.objects.all().filter(active=True, featured=True).order_by(self.order_types[int(order_param)])[0:5]
queryset_non_featured = Job.objects.all().filter(active=True, featured=False).order_by(self.order_types[int(order_param)])[0:15]
queryset_combined = queryset_featured.union(queryset_non_featured)
for row in queryset_combined:
print(row)

Django-filter get all records when a specific value for a filter_field is passed

I am using django-filter to filter my Queryset on the basis of url params.
class WorklistViewSet(ModelViewSet):
serializer_class = MySerializer
queryset = MyModel.objects.all()
filter_backends = [DjangoFilterBackend, ]
filterset_fields = ['class', ]
# possible values of *class* which is allowed to be passed in the url params are ['first', 'second', 'ALL'].
class MyModel(BaseModel):
CLASS_CHOICES = (
(FIRST_CLASS, 'first'),
(SECOND_CLASS, 'second'),
)
class = models.CharField(choices=CLASS_CHOICES, max_length=3, )
URLs http://127.0.0.1:8000?class=first and http://127.0.0.1:8000?class=first are giving the expected results.
I want that when http://127.0.0.1:8000?class=ALL is called, all the records in my table should be listed i.e without filtering.
How can i do this while using django-filter ?
You may want to use Filter.method, as explained in the docs.
In your case, I would do as follows:
class F(django_filters.FilterSet):
klass = CharFilter(method='my_custom_filter')
class Meta:
model = MyModel
fields = ['klass']
def my_custom_filter(self, queryset, name, value):
if value == 'ALL':
return queryset
return queryset.filter(**{
name: value,
})
Be also reminded that class is a reserved word in Python and cannot be used as a variable name. I've used klass instead, although that's used as something else in many Python books and may be confusing.

Optimising number of queries within a DRF ModelSerializer

Within Django Rest Framework's serialiser it is possible to add more data to the serialised object than in the original Model.
This is useful for when calculating statistical information, on the server-side, and adding this extra information when responding to an API call.
As I understand, adding extra data is done using a SerializerMethodField, where each field is implemented by a get_... function.
However, if you have a number of these SerializerMethodFields, each one can be querying the Model/database separately, for what might be essentially the same data.
Is it possible to query the database once, store the list/result as a data member of the ModelSerializer object, and use the result of the queryset in many functions?
Here's a very simple example, just for illustration:
############## Model
class Employee(Model):
SALARY_TYPE_CHOICES = (('HR', 'Hourly Rate'), ('YR', 'Annual Salary'))
salary_type = CharField(max_length=2, choices=SALARY_TYPE_CHOICES, blank=False)
salary = PositiveIntegerField(blank=True, null=True, default=0)
company = ForeignKey(Company, related_name='employees')
class Company(Model):
name = CharField(verbose_name='company name', max_length=100)
############## View
class CompanyView(RetrieveAPIView):
queryset = Company.objects.all()
lookup_field='id'
serializer_class = CompanySerialiser
class CompanyListView(ListAPIView):
queryset = Company.objects.all()
serializer_class = CompanySerialiser
############## Serializer
class CompanySerialiser(ModelSerializer):
number_employees = SerializerMethodField()
total_salaries_estimate = SerializerMethodField()
class Meta:
model = Company
fields = ['id', 'name',
'number_employees',
'total_salaries_estimate',
]
def get_number_employees(self, obj):
return obj.employees.count()
def get_total_salaries_estimate(self, obj):
employee_list = obj.employees.all()
salaries_estimate = 0
HOURS_PER_YEAR = 8*200 # 8hrs/day, 200days/year
for empl in employee_list:
if empl.salary_type == 'YR':
salaries_estimate += empl.salary
elif empl.salary_type == 'HR':
salaries_estimate += empl.salary * HOURS_PER_YEAR
return salaries_estimate
The Serialiser can be optimised to:
use an object data member to store the result from the query set,
only retrieve the queryset once,
re-use the result of the queryset for all extra information provided in SerializerMethodFields.
Example:
class CompanySerialiser(ModelSerializer):
def __init__(self, *args, **kwargs):
super(CompanySerialiser, self).__init__(*args, **kwargs)
self.employee_list = None
number_employees = SerializerMethodField()
total_salaries_estimate = SerializerMethodField()
class Meta:
model = Company
fields = ['id', 'name',
'number_employees',
'total_salaries_estimate',
]
def _populate_employee_list(self, obj):
if not self.employee_list: # Query the database only once.
self.employee_list = obj.employees.all()
def get_number_employees(self, obj):
self._populate_employee_list(obj)
return len(self.employee_list)
def get_total_salaries_estimate(self, obj):
self._populate_employee_list(obj)
salaries_estimate = 0
HOURS_PER_YEAR = 8*200 # 8hrs/day, 200days/year
for empl in self.employee_list:
if empl.salary_type == 'YR':
salaries_estimate += empl.salary
elif empl.salary_type == 'HR':
salaries_estimate += empl.salary * HOURS_PER_YEAR
return salaries_estimate
This works for the single retrieve CompanyView. And, in fact saves one query/context-switch/round-trip to the database; I've eliminated the "count" query.
However, it does not work for the list view CompanyListView, because it seems that the serialiser object is created once and reused for each Company. So, only the first Company's list of employees is stored in the objects "self.employee_list" data member, and thus, all other companies erroneously get given the data from the first company.
Is there a best practice solution to this type of problem? Or am I just wrong to use the ListAPIView, and if so, is there an alternative?
I think this issue can be solved if you can pass the queryset to the CompanySerialiser with data already fetched.
You can do the following changes
class CompanyListView(ListAPIView):
queryset = Company.objects.all().prefetch_related('employee_set')
serializer_class = CompanySerialiser`
And instead of count use len function because count does the query again.
class CompanySerialiser(ModelSerializer):
number_employees = SerializerMethodField()
total_salaries_estimate = SerializerMethodField()
class Meta:
model = Company
fields = ['id', 'name',
'number_employees',
'total_salaries_estimate',
]
def get_number_employees(self, obj):
return len(obj.employees.all())
def get_total_salaries_estimate(self, obj):
employee_list = obj.employees.all()
salaries_estimate = 0
HOURS_PER_YEAR = 8*200 # 8hrs/day, 200days/year
for empl in employee_list:
if empl.salary_type == 'YR':
salaries_estimate += empl.salary
elif empl.salary_type == 'HR':
salaries_estimate += empl.salary * HOURS_PER_YEAR
return salaries_estimate
Since the data is prefetched, serializer will not do any additional query for all. But make sure you are not doing any kind of filter because another query will execute in that case.
As mentioned by #Ritesh Agrawal, you simply need to prefetch the data. However, I advise to do the aggregations directly inside the database instead of using Python:
class CompanySerializer(ModelSerializer):
number_employees = IntegerField()
total_salaries_estimate = FloatField()
class Meta:
model = Company
fields = ['id', 'name',
'number_employees',
'total_salaries_estimate', ...
]
class CompanyListView(ListAPIView):
queryset = Company.objects.annotate(
number_employees=Count('employees'),
total_salaries_estimate=Sum(
Case(
When(employees__salary_type=Value('HR'),
then=F('employees_salary') * Value(8 * 200)
),
default=F('employees__salary'),
output_field=IntegerField() #optional a priori, because you only manipulate integers
)
)
)
serializer_class = CompanySerializer
Notes:
I haven't tested this code, but I'm using the same kind of syntax for my own projects. If you encounter errors (like 'cannot determine type of output' or similar), try wrapping F('employees_salary') * Value(8 * 200) inside an ExpressionWrapper(..., output_field=IntegerField()).
Using aggregation, you can apply filters on the queryset afterwards. However, if you're prefetching your related Employees, then you cannot filter the related objects anymore (as mentioned in the previous answer). BUT, if you already know you'll need the list of employees with hourly rate, you can do .prefetch_related(Prefetch('employees', queryset=Employee.object.filter(salary_type='HR'), to_attr="hourly_rate_employees")).
Relevant documentation:
Query optimization
Aggregation
Hope this will help you ;)

Django- filtering of the filter object

I want to make a complex filtering on the page using the FilterSets. This is my Filterset, nicely showing me tuples from chosen time and with chosen parameters.
# filters.py
class workFilter(filters.FilterSet):
start__gt = filters.DateTimeFilter(name='time_start', lookup_expr='gte')
start__lt = filters.DateTimeFilter(name='time_end', lookup_expr='lte')
class Meta:
model = Work
fields = ('machine', 'program')
But I want to add charts explaining the queried data. For that I need informations, like overall count of time. I am querying them like that:
#views.py
def search(request):
work_list = Work.objects.all()
work_filter = workFilter(request.GET, queryset=work_list)
filter_backends = (filters.DjangoFilterBackend,)
#some queries to add to context, such as
sum_work = Work.objects.aggregate(Sum('time'))['time__sum']
return render_to_response(
TEMPLATE_DIRS + 'index.html',
{
'filter': praca_filter,
'sum_work': sum_work,
}
)
But sadly, those queries are according to whole database, not to my filtered set of object.
How can I make queries on filtered set work_filter?
Define sum_work as a property of your FilterSet.
class WorkFilter(filters.FilterSet):
start__gt = filters.DateTimeFilter(name='time_start', lookup_expr='gte')
start__lt = filters.DateTimeFilter(name='time_end', lookup_expr='lte')
class Meta:
model = Work
fields = ('machine', 'program')
#property
def work_sum(self):
qs = super(WorkFilter, self).qs
return qs.aggregate(Sum('time'))['time__sum']
Then when you pass your filter through to your view you just need {{ filter.work_sum }} in your template.

Why isn't the filter working?

I want to filter my product by country and brand, for that reason I have created a view:
class CategoryView(ListView):
template_name = '__index.html'
context_object_name = 'products'
paginate_by = 20
def get_queryset(self):
queryset = Product.objects.filter(category__slug=self.kwargs.get('slug')).order_by('-created')
request = self.request
# Filter By Brand and Country
if request.GET.get('country'):
print(request.GET.get('country'))
queryset.filter(brand__country__slug=request.GET.get('country'))
if request.GET.get('brand'):
print(request.GET.get('brand'))
queryset.filter(brand__slug=request.GET.get('brand'))
print(queryset[0].brand.slug)
print(queryset[0].brand.country.slug)
return queryset
But products isn't filtering when i pass querystring like that: ?brand=astra-gold&country=chehiya and print function show me:
chehiya
astra-gold
veneto
italiya
As you can see chehiya != italiya and astra-gold != veneto. Bun why this happen?
When you call filter() on a queryset, it creates a new queryset. The original queryset is unchanged. For your filter to have an effect, you need to assign the result back to queryset, for example:
if request.GET.get('country'):
print(request.GET.get('country'))
queryset = queryset.filter(brand__country__slug=request.GET.get('country'))
I forgot this
queryset = queryset.filter(brand__country__slug=request.GET.get('country'))