Aggregation by Foreign Key and other field in Django Admin - python

I'm working in Django and having an issue displaying something properly in my Admin site. These are the models
class IndexSetSize(models.Model):
""" A time series of sizes for each index set """
index_set = models.ForeignKey(IndexSet, on_delete=models.CASCADE)
byte_size = models.BigIntegerField()
timestamp = models.DateTimeField()
class IndexSet(models.Model):
title = models.CharField(max_length=4096)
# ... some other stuff that isn't really important
def __str__(self):
return f"{self.title}"
It is displaying all the appropriate data I need, but, I want to display the sum of IndexSetSize, grouped by the index_set key and also grouped by the timestamp (There can be multiple occurrences of an IndexSet for a given timestamp, so I want to add up all the byte_sizes). Currently is just showing every single record. Additionally, I would prefer the total_size field to be sortable
Current Admin model looks like:
class IndexSetSizeAdmin(admin.ModelAdmin):
""" View-only admin for index set sizes """
fields = ["index_set", "total_size", "byte_size", "timestamp"]
list_display = ["index_set", "total_size", "timestamp"]
search_fields = ["index_set"]
list_filter = ["index_set__title"]
def total_size(self, obj):
""" Returns human readable size """
if obj.total_size:
return humanize.naturalsize(obj.total_size)
return "-"
total_size.admin_order_field = 'total_size'
def get_queryset(self, request):
queryset = super().get_queryset(request).select_related()
queryset = queryset.annotate(
total_size=Sum('byte_size', filter=Q(index_set__in_graylog=True)))
return queryset
It seems the proper way to do a group by in Django is to use .values(), although if I use that in get_queryset, an error is thrown saying Cannot call select_related() after .values() or .values_list(). I'm having trouble finding in the documentation if there's a 'correct' way to values/annotate/aggregate that will work correctly with get_queryset. It's a pretty simple sum/group by query I'm trying to do, but I'm not sure what the "Django way" is to accomplish it.
Thanks

I don't think you would be able to return the full queryset and group by index_set in get_queryset as you can't select all columns but group by an individual column in sql
SELECT *, SUM(index_size) FROM indexsetsize GROUP BY index_set // doesn't work
You could perform an extra query in the total_size method to get the aggregated value. However, this would perform the query for every row returned and slow your page load down.
def total_size(self, obj):
""" Returns human readable size """
return humanize.naturalsize(sum(IndexSetSize.objects.filter(
index_set=obj.index_set).values_list(
'byte_size', flat=True)))
total_size.admin_order_field = 'total_size'
It would be better to perform this annotation within the IndexSetAdmin as the index_set will already be grouped through the reverse foreign key. This will mean you can perform the annotation in get_queryset. I would also set the related_name on the foreign key on IndexSetSize so you can access the realted IndexSetSize objects from IndexSet using that name.
class IndexSetSize(models.Model):
index_set = models.ForeignKey(IndexSet, on_delete=models.CASCADE, related_name='index_set_sizes')
...
class IndexSetAdmin(admin.ModelAdmin):
...
def total_size(self, obj):
""" Returns human readable size """
if obj.total_size:
return humanize.naturalsize(obj.total_size)
return "-"
def get_queryset(self, request):
queryset = super().get_queryset(request).prefetch_related('index_set_sizes').annotate(
total_size=Sum('index_set_sizes__byte_size')).order_by('total_size')
return queryset

Related

How to display only one record from child model for each header item in Django ListView using distinct()

I am using two related models in my Django application. The objects so created in the models are being displayed using the listview class. In the child model I can create multiple rows based on some key date. When I try to display values from both the models, all the child objects for the respective FK fields are displayed (wherever more than one records are there).
Let me make the situation clearer as below:
models.py
class MatPriceDoc(models.Model):
mat_price_doc_number = models.IntegerField(null=True, blank=True.....)
mat_num = models.ForeignKey(Material, on_delete=.......)
mat_group = models.ForeignKey(MatGrp, on_delete=.......)
doc_type = models.CharField(max_length=2, null=True, default='RF'....)
create_date = models.DateField(default=timezone.now,...)
class MatPriceItems(models.Model):
price_doc_header = models.ForeignKey(MatPriceDoc, on_delete=...)
price_item_num = models.CharField(max_length=3, default=10,...)
price_valid_from_date = models.DateField(null=True, verbose_name='From date')
price_valid_to_date = models.DateField(null=True, verbose_name='To date')
mat_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True,...)
views.py
class MatPriceListView(ListView):
template_name = "mat_price_list.html"
context_object_name = 'matprice'
model = MatPriceDoc
def get_context_data(self, **kwargs):
context = super(MatPriceListView, self).get_context_data(**kwargs)
context.update({
'mat_price_item_list': MatPriceItems.objects.distinct().order_by('price_doc_header'), #This is where I have tried **distinct()**
})
return context
def get_queryset(self):
return MatPriceDoc.objects.order_by('mat_num')
Now the material price changes periodically and there would always be more than one price item for each material for each key date (price_valid_from_date). My objective is to display the latest price of all those materials for which price documents exist. My predicament is how to pick up only one of the many price items for the same material / document combination. My code line 'mat_price_item_list': MatPriceItems.objects.distinct().order_by('price_doc_header'), is of course not yielding any result (all price items are being displayed in successive columns).
Is there a way to show only one price item in the listview?
Edit
In the following image the prices maintained for various dates for materials are shown. What I was trying to get was only the price for the latest (valid) date is displayed for a particular material. So in the instant case (as displayed), prices for only 4th March 2020 should be displayed for each item MS and HSD.
Edit 2
This is how the child model data for an instance of header doc number (no. 32 here) looks like (image grab from the child table using data browser):
The columns are: Child obj. item no. / Price / Valid From / Valid To / Hdr Doc no. / Child Obj Row no.
My thinking was: Can I not pick up only the first object (a subset) from a collection of doc number? In the instant case (ref the image), doc no. 32 has three child items (bearing number 148, 149 and 156). Is it not possible to only pick up item number 156 and discard the rest?
I tried:
MatPriceItems.objects.order_by('-price_valid_to_date').first()
but raises error "MatPriceItems" object is not iterable.
What would enable me to get the only item for a header item and display it?
I believe you need the following:
class MatPriceListView(ListView):
template_name = "mat_price_list.html"
context_object_name = 'matprice'
model = MatPriceDoc
def get_context_data(self, **kwargs):
context = super(MatPriceListView, self).get_context_data(**kwargs)
context.update({
'mat_price_item_list': MatPriceItems.objects.all().('-price_doc_header').first(),
})
return context
def get_queryset(self):
return MatPriceDoc.objects.order_by('mat_num')
The key line change being:
MatPriceItems.objects.all().order_by('-price_doc_header').first()
Such that you order by the price_doc_header descending (hence the minus infront of that value). Then take the .first() in the returned <QuerySet> object.
Likewise, the following would also be valid:
MatPriceItems.objects.all().order_by('price_doc_header').last()
You could also make use of Django's built in aggregation in the ORM:
from django.db.models import Max
MatPriceItems.objects.all().aggregate(Max('price_doc_header'))
Putting this all together, I would say the best solution for readabiltiy of the code would be using Django's built-in Max function:
from django.db.models import Max
class MatPriceListView(ListView):
template_name = "mat_price_list.html"
context_object_name = 'matprice'
model = MatPriceDoc
def get_context_data(self, **kwargs):
context = super(MatPriceListView, self).get_context_data(**kwargs)
context.update({
'mat_price_item_list': MatPriceItems.objects.all().aggregate(Max('price_doc_header'))
})
return context
def get_queryset(self):
return MatPriceDoc.objects.order_by('mat_num')
Because it tells any colleague exactly what you're doing: taking the maximum value.
Update: On a second glance of your question - I believe you may also need to do some sort of .filter()...then make an .annotate() for the Max...
I'm also adding what I now believe is the correct answer based on a re-reading of your question.
I feel like the query needs to be ammended to:
MatPriceItems.objects.all().select_related(
'mat_price_docs'
).order_by(
'price_doc_header__mat_num'
)
i.e.,
class MatPriceListView(ListView):
template_name = "mat_price_list.html"
context_object_name = 'matprice'
model = MatPriceDoc
def get_context_data(self, **kwargs):
context = super(MatPriceListView, self).get_context_data(**kwargs)
context.update({
'mat_price_item_list': MatPriceItems.objects.all().select_related('mat_price_docs').order_by('price_doc_header__mat_num'),
})
return context
def get_queryset(self):
return MatPriceDoc.objects.order_by('mat_num')
To achieve this, you also add the related_name argument to the price_doc_header ForeignKey field.
class MatPriceItems(models.Model):
price_doc_header = models.ForeignKey(MatPriceDoc, related_name='mat_price_docs', on_delete=...)
price_item_num = models.CharField(max_length=3, default=10,...)
price_valid_from_date = models.DateField(null=True, verbose_name='From date')
price_valid_to_date = models.DateField(null=True, verbose_name='To date')
mat_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True,...)
As always, any changes to your models - please re-run the makemigarations and migrate management commands.
Edit: You may even be able to define a new queryset using an annotation to link to the ForeignKey:
def get_queryset(self):
return MatPriceItems.objects.all().order_by(
'price_doc_header__mat_num'
).annotate(
mat_price_doc_number = F('price_doc_header__mat_price_doc_number')
...
)
N.B. You can import the F() expression as from django.db.models import F

Django rest framework custom filter backend data duplication

I am trying to make my custom filter and ordering backend working with default search backend in django rest framework. The filtering and ordering working perfectly with each other, but when search is included in the query and i am trying to order query by object name, then data duplication is happening.
I tried to print queries and queries size, but it seems ok when i logging it in the filters, but in a response i have different object counts(ex. 79 objects in filter query, 170 duplicated objects in the final result)
Here is my filterset class
class PhonesFilterSet(rest_filters.FilterSet):
brands = InListFilter(field_name='brand__id')
os_ids = InListFilter(field_name='versions__os')
version_ids = InListFilter(field_name='versions')
launched_year_gte = rest_filters.NumberFilter(field_name='phone_launched_date__year', lookup_expr='gte')
ram_gte = rest_filters.NumberFilter(field_name='internal_memories__value', method='get_rams')
ram_memory_unit = rest_filters.NumberFilter(field_name='internal_memories__units', method='get_ram_units')
def get_rams(self, queryset, name, value):
#here is the problem filter
#that not works with ordering by name
q=queryset.filter(Q(internal_memories__memory_type=1) & Q(internal_memories__value__gte=value))
print('filter_set', len(q))
print('filter_set_query', q.query)
return q
def get_ram_units(self, queryset, name, value):
return queryset.filter(Q(internal_memories__memory_type=1) & Q(internal_memories__units=value))
class Meta:
model = Phone
fields = ['brands', 'os_ids', 'version_ids', 'status', 'ram_gte']
My ordering class:
class CustomFilterBackend(filters.OrderingFilter):
allowed_custom_filters = ['ram', 'camera', 'year']
def get_ordering(self, request, queryset, view):
params = request.query_params.get(self.ordering_param)
if params:
fields = [param.strip() for param in params.split(',')]
ordering = [f for f in fields if f in self.allowed_custom_filters]
if ordering:
return ordering
# No ordering was included, or all the ordering fields were invalid
return self.get_default_ordering(view)
def filter_queryset(self, request, queryset, view):
ordering = self.get_ordering(request, queryset, view)
if ordering:
if 'ram' in ordering:
max_ram = Max('internal_memories__value', filter=Q(internal_memories__memory_type=1))
queryset = queryset.annotate(max_ram=max_ram).order_by('-max_ram')
elif 'camera' in ordering:
max_camera = Max('camera_pixels__megapixels', filter=Q(camera_pixels__camera_type=0))
queryset = queryset.annotate(max_camera=max_camera).order_by('-max_camera')
elif 'year' in ordering:
queryset = queryset.filter(~Q(phone_released_date=None)).order_by('-phone_released_date__year')
elif 'name' in ordering:
#here is the problem ordering
#thats not working with filter
#with one to many relations
queryset = queryset.order_by('-brand__name', '-model__name')
return queryset
Viewset class:
class PhoneViewSet(viewsets.ModelViewSet):
queryset = Phone.objects.all()
serializer_class = PhoneSerializer
filter_backends = (filters.SearchFilter, CustomFilterBackend, django_filters.rest_framework.DjangoFilterBackend)
search_fields = ('brand__name', 'model__name')
ordering_fields = ('brand__name', 'model__name')
filter_class = PhonesFilterSet
As a result i am expecting no data duplication when i am applying ordering with filter and search. My question is why the number of objects is different in filter and in the response, where the data is becoming duplicated? I have no idea where to start debugging from this point. Thanks in advance.
Using distinct() should fix this:
Returns a new QuerySet that uses SELECT DISTINCT in its SQL query. This eliminates duplicate rows from the query results.
By default, a QuerySet will not eliminate duplicate rows. In practice, this is rarely a problem, because simple queries such as Blog.objects.all() don’t introduce the possibility of duplicate result rows. However, if your query spans multiple tables, it’s possible to get duplicate results when a QuerySet is evaluated. That’s when you’d use distinct().
Note however, that you still might get duplicate results:
Any fields used in an order_by() call are included in the SQL SELECT columns. This can sometimes lead to unexpected results when used in conjunction with distinct(). If you order by fields from a related model, those fields will be added to the selected columns and they may make otherwise duplicate rows appear to be distinct. Since the extra columns don’t appear in the returned results (they are only there to support ordering), it sometimes looks like non-distinct results are being returned.
https://docs.djangoproject.com/en/2.2/ref/models/querysets/#django.db.models.query.QuerySet.distinct
If you are using PostgreSQL, you can specify the names of fields to which the DISTINCT should apply. This might help. (I'm not sure.) For more on this, see the link above.
So, I'd return queryset.distinct() in the methods where you commented that you get issues. I would not apply it always (as I had written in my comment above for debugging) because you don't need it for simple queries.

How to generate feed from different models in Django?

So, I have two models called apartments and jobs. It's easy to display contents of both models separately, but what I can't figure out is how to display the mix feed of both models based on the date.
jobs = Job.objects.all().order_by('-posted_on')
apartments = Apartment.objects.all().order_by('-date')
The posted date on job is represented by 'posted_by' and the posted date on apartment is represented by 'date'. How can I combine both of these and sort them according to the date posted? I tried combining both of these models in a simpler way like:
new_feed = list(jobs) + list(apartments)
This just creates the list of both of these models, but they are not arranged based on date.
I suggest two ways to achieve that.
With union() New in Django 1.11.
Uses SQL’s UNION operator to combine the results of two or more QuerySets
You need to to make sure that you have a unique name for the ordered field
Like date field for job and also apartment
jobs = Job.objects.all().order_by('-posted_on')
apartments = Apartment.objects.all().order_by('-date')
new_feed = jobs.union(apartments).order_by('-date')
Note with this options, you need to have the same field name to order them.
Or
With chain(), used for treating consecutive sequences as a single sequence and use sorted() with lambda to sort them
from itertools import chain
# remove the order_by() in each queryset, use it once with sorted
jobs = Job.objects.all()
apartments = Apartment.objects.all()
result_list = sorted(chain(job, apartments),
key=lambda instance: instance.date)
With this option, you don't really need to rename or change one of your field names, just add a property method, let's choose the Job Model
class Job(models.Model):
''' fields '''
posted_on = models.DateField(......)
#property
def date(self):
return self.posted_on
So now, both of your models have the attribute date, you can use chain()
result_list = sorted(chain(job, apartments),
key=lambda instance: instance.date)
A good way to do that is to use adapter design pattern. The idea is that we introduce an auxiliary data structure that can be used for the purpose of sorting these model objects. This method has several benefits over trying to fit both models to have the identically named attribute used for sorting. The most important is that the change won't affect any other code in your code base.
First, you fetch your objects as you do but you don't have to fetch them sorted, you can fetch all of them in arbitrary order. You may also fetch just top 100 of them in the sorted order. Just fetch what fits your requirements here:
jobs = Job.objects.all()
apartments = Apartment.objects.all()
Then, we build an auxiliary list of tuples (attribute used for sorting, object), so:
auxiliary_list = ([(job.posted_on, job) for job in jobs]
+ [(apartment.date, apartment) for apartment in apartments])
now, it's time to sort. We're going to sort this auxiliary list. By default, python sort() method sorts tuples in lexicographical order, which mean it will use the first element of the tuples i.e. posted_on and date attributes for ordering. Parameter reverse is set to True for sorting in decreasing order i.e. as you want them in your feed.
auxiliary_list.sort(reverse=True)
now, it's time to return only second elements of the sorted tuples:
sorted_feed = [obj for _, obj in auxiliary_list]
Just keep in mind that if you expect your feed to be huge then sorting these elements in memory is not the best way to do this, but I guess this is not your concern here.
I implemented this in the following ways.
I Video model and Article model that had to be curated as a feed. I made another model called Post, and then had a OneToOne key from both Video & Article.
# apps.feeds.models.py
from model_utils.models import TimeStampedModel
class Post(TimeStampedModel):
...
#cached_property
def target(self):
if getattr(self, "video", None) is not None:
return self.video
if getattr(self, "article", None) is not None:
return self.article
return None
# apps/videos/models.py
class Video(models.Model):
post = models.OneToOneField(
"feeds.Post",
on_delete=models.CASCADE,
)
...
# apps.articles.models.py
class Article(models.Model):
post = models.OneToOneField(
"feeds.Post",
on_delete=models.CASCADE,
)
...
Then for the feed API, I used django-rest-framework to sort on Post queryset's created timestamp. I customized serializer's method and added queryset annotation for customization etc. This way I was able to get either Article's or Video's data as nested dictionary from the related Post instance.
The advantage of this implementation is that you can optimize the queries easily with annotation, select_related, prefetch_related methods that works well on Post queryset.
# apps.feeds.serializers.py
class FeedSerializer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ("type",)
def to_representation(self, instance) -> OrderedDict:
ret = super().to_representation(instance)
if isinstance(instance.target, Video):
ret["data"] = VideoSerializer(
instance.target, context={"request": self.context["request"]}
).data
else:
ret["data"] = ArticleSerializer(
instance.target, context={"request": self.context["request"]}
).data
return ret
def get_type(self, obj):
return obj.target._meta.model_name
#staticmethod
def setup_eager_loading(qs):
"""
Inspired by:
http://ses4j.github.io/2015/11/23/optimizing-slow-django-rest-framework-performance/
"""
qs = qs.select_related("live", "article")
# other db optimizations...
...
return qs
# apps.feeds.viewsets.py
class FeedViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = FeedSerializer
permission_classes = (IsAuthenticatedOrReadOnly,)
def get_queryset(self):
qs = super().get_queryset()
qs = self.serializer_class().setup_eager_loading(qs)
return as
...

Django-filter with DRF - How to do 'and' when applying multiple values with the same lookup?

This is a slightly simplified example of the filterset I'm using, which I'm using with the DjangoFilterBackend for Django Rest Framework. I'd like to be able to send a request to /api/bookmarks/?title__contains=word1&title__contains=word2 and have results returned that contain both words, but currently it ignores the first parameter and only filters for word2.
Any help would be very appreciated!
class BookmarkFilter(django_filters.FilterSet):
class Meta:
model = Bookmark
fields = {
'title': ['startswith', 'endswith', 'contains', 'exact', 'istartswith', 'iendswith', 'icontains', 'iexact'],
}
class BookmarkViewSet(viewsets.ModelViewSet):
serializer_class = BookmarkSerializer
permission_classes = (IsAuthenticated,)
filter_backends = (DjangoFilterBackend,)
filter_class = BookmarkFilter
ordering_fields = ('title', 'date', 'modified')
ordering = '-modified'
page_size = 10
The main problem is that you need a filter that understands how to operate on multiple values. There are basically two options:
Use MultipleChoiceFilter (not recommended for this instance)
Write a custom filter class
Using MultipleChoiceFilter
class BookmarkFilter(django_filters.FilterSet):
title__contains = django_filters.MultipleChoiceFilter(
name='title',
lookup_expr='contains',
conjoined=True, # uses AND instead of OR
choices=[???],
)
class Meta:
...
While this retains your desired syntax, the problem is that you have to construct a list of choices. I'm not sure if you can simplify/reduce the possible choices, but off the cuff it seems like you would need to fetch all titles from the database, split the titles into distinct words, then create a set to remove duplicates. This seems like it would be expensive/slow depending on how many records you have.
Custom Filter
Alternatively, you can create a custom filter class - something like the following:
class MultiValueCharFilter(filters.BaseCSVFilter, filters.CharFilter):
def filter(self, qs, value):
# value is either a list or an 'empty' value
values = value or []
for value in values:
qs = super(MultiValueCharFilter, self).filter(qs, value)
return qs
class BookmarkFilter(django_filters.FilterSet):
title__contains = MultiValueCharFilter(name='title', lookup_expr='contains')
class Meta:
...
Usage (notice that the values are comma-separated):
GET /api/bookmarks/?title__contains=word1,word2
Result:
qs.filter(title__contains='word1').filter(title__contains='word2')
The syntax is changed a bit, but the CSV-based filter doesn't need to construct an unnecessary set of choices.
Note that it isn't really possible to support the ?title__contains=word1&title__contains=word2 syntax as the widget can't render a suitable html input. You would either need to use SelectMultiple (which again, requires choices), or use javascript on the client to add/remove additional text inputs with the same name attribute.
Without going into too much detail, filters and filtersets are just an extension of Django's forms.
A Filter has a form Field, which in turn has a Widget.
A FilterSet is composed of Filters.
A FilterSet generates an inner form based on its filters' fields.
Responsibilities of each filter component:
The widget retrieves the raw value from the data QueryDict.
The field validates the raw value.
The filter constructs the filter() call to the queryset, using the validated value.
In order to apply multiple values for the same filter, you would need a filter, field, and widget that understand how to operate on multiple values.
The custom filter achieves this by mixing in BaseCSVFilter, which in turn mixes in a "comma-separation => list" functionality into the composed field and widget classes.
I'd recommend looking at the source code for the CSV mixins, but in short:
The widget splits the incoming value into a list of values.
The field validates the entire list of values by validating individual values on the 'main' field class (such as CharField or IntegerField). The field also derives the mixed in widget.
The filter simply derives the mixed in field class.
The CSV filter was intended to be used with in and range lookups, which accept a list of values. In this case, contains expects a single value. The filter() method fixes this by iterating over the values and chaining together individual filter calls.
You can create custom list field something like this:
from django.forms.widgets import SelectMultiple
from django import forms
class ListField(forms.Field):
widget = SelectMultiple
def __init__(self, field, *args, **kwargs):
super(ListField, self).__init__( *args, **kwargs)
self.field = field
def validate(self, value):
super(ListField, self).validate(value)
for val in value:
self.field.validate(val)
def run_validators(self, value):
for val in value:
self.field.run_validators(val)
def to_python(self, value):
if not value:
return []
elif not isinstance(value, (list, tuple)):
raise ValidationError(self.error_messages['invalid_list'], code='invalid_list')
return [self.field.to_python(val) for val in value]
and create custom filter using MultipleChoiceFilter:
class ContainsListFilter(django_filters.MultipleChoiceFilter):
field_class = ListField
def get_filter_predicate(self, v):
name = '%s__contains' % self.name
try:
return {name: getattr(v, self.field.to_field_name)}
except (AttributeError, TypeError):
return {name: v}
After that you can create FilterSet with your custom filter:
from django.forms import CharField
class StorageLocationFilter(django_filters.FilterSet):
title_contains = ContainsListFilter(field=CharField())
Working for me. Hope it will be useful for you.
Here is a sample code that just works:
it supports - product?name=p1,p2,p3 and will return products with name (p1,p2,p3)
def resolve_csvfilter(queryset, name, value):
lookup = { f'{name}__in': value.split(",") }
queryset = queryset.filter(**lookup)
return queryset
class ProductFilterSet(FilterSet):
name = CharFilter(method=resolve_csvfilter)
class Meta:
model = Product
fields = ['name']
Ref: https://django-filter.readthedocs.io/en/master/guide/usage.html#customize-filtering-with-filter-method
https://github.com/carltongibson/django-filter/issues/137

How to add filters to a query dynamically in Django?

In my viewSet I am doing a query,
queryset= Books.objects.all();
Now from an ajax call I get my filter values from UI i.e. age,gender, etc. of auther.There will be a total of 5 filters.
Now the problem which I ran into is how am I going to add filters to my query(only those filters which have any value).
What I tried is I checked for individual filter value and did query, but that way it fails as if the user remove the filter value or add multiple filters.
Any better suggestion how to accomplish this?
Here's a bit more generic one. It will apply filters to your queryset if they are passed as the GET parameters. If you're doing a POST call, just change the name in the code.
import operator
from django.db.models import Q
def your_view(self, request, *args, **kwargs):
# Here you list all your filter names
filter_names = ('filter_one', 'filter_two', 'another_one', )
queryset = Books.objects.all();
filter_clauses = [Q(filter=request.GET[filter])
for filter in filter_names
if request.GET.get(filter)]
if filter_clauses:
queryset = queryset.filter(reduce(operator.and_, filter_clauses))
# rest of your view
Note that you can use lookup expressions in your filters' names. For example, if you want to filter books with price lower or equal to specified in filter, you could just use price__lte as a filter name.
You haven't shown any code, so you haven't really explained what the problem is:
Start with the queryset Book.objects.all(). For each filter, check if there is a value for the filter in request.POST, and if so, filter the queryset. Django querysets are lazy, so only the final queryset will be evaluated.
queryset = Book.objects.all()
if request.POST.get('age'):
queryset = queryset.filter(author__age=request.POST['age'])
if request.POST.get('gender'):
queryset = queryset.filter(author__gender=request.POST['gender'])
...
You can simply get the request.GET content as a dict (making sure to convert the values to string or a desired type as they'd be list by default i.e: dict(request.GET) would give you something like {u'a': [u'val']}.
Once you are sure you have a dictionary of keys matching your model fields, you can simply do:
filtered = queryset.filter(**dict_container)
Maybe django-filter would help simplify the solutions others have given?
Something like:
class BookFilter(django_filters.FilterSet):
class Meta:
model = Book
fields = ['author__age', 'author__gender', ...]
Then the view looks like:
def book_list(request):
f = BookFilter(request.GET, queryset=Book.objects.all())
return render_to_response('my_app/template.html', {'filter': f})
For more information see the documentation.
this worked for me, I've merged Alex Morozov answer with Dima answer
import operator
def your_view(self, request, *args, **kwargs):
# Here you list all your filter names
filter_names = ('filter_one', 'filter_two', 'another_one', )
queryset = Books.objects.all();
filter_clauses = [Q(**{filter: request.GET[filter]})
for filter in filter_names
if request.GET.get(filter)]
if filter_clauses:
queryset = queryset.filter(reduce(operator.and_, filter_clauses))
# rest of your view
You can do something like that
class BooksAPI(viewsets.ModelViewSet):
queryset = Books.objects.none()
def get_queryset(self):
argumentos = {}
if self.request.query_params.get('age'):
argumentos['age'] = self.request.query_params.get('age')
if self.request.query_params.get('gender'):
argumentos['gender'] = self.request.query_params.get('gender')
if len(argumentos) > 0:
books = Books.objects.filter(**argumentos)
else:
books = Books.objects.all()
return books
For a very simple equality check, here is my solution using a helper function in a ModelViewSet.
The helper function check_for_params creates a dictionary of request parameters passed in the URL.
Alter the ModelViewSet get_queryset() method by filtering the Django QuerySet with a single filter clause which prevents multiple queries being called by chaining filters.
I could tried to use the Django Q() object but could not get it to only make a single call.
def check_for_params(request, param_check_list: List) -> dict:
"""
Create a dictionary of params passed through URL.
Parameters
----------
request - DRF Request object.
param_check_list - List of params potentially passed in the url.
"""
if not param_check_list:
print("No param_check_list passed.")
else:
param_dict = {}
for p in param_check_list:
param_dict[p] = request.query_params.get(p, None)
return param_dict
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
authentication_classes = [SessionAuthentication]
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""
Return a queryset and apply filters, if applicable.
Info
----
Building the queryset.filter method by unpacking the key-value pairs this way,
creates a single filter clause and prevents multiple queries from being called
by chaining filters.
"""
queryset = MyModel.objects.all()
param_check_list = ['param1', 'param2', 'param3']
params = check_for_params(self.request, param_check_list)
filtered = {k: v for k, v in params.items() if v}
# Calling filter() only once here prevents multiple queries.
return queryset.filter(**filtered)

Categories