Altering django-filter default behaviour - python

This is a django-filter app specific guestion.
Has anyone tried to introduce conditions for the filters to query according to the condition?
Let me give an example:
Suppose we have a Product model. It can be filtered according to its name and price.
The default django-filter behaviour is that, as we use more filters and chain them together, they filter data using AND statements (it narrows the search).
I'd like to change this behaviour and add a ChoiceFilter, say with two options: AND as well as OR. From this point, the filter should work according to what a user have selected.
Eg. if a user query for products with name__startswith="Juice" OR price__lte=10.00, it should list all the products with names starting with Juice as well as products with price below 10.00.
Django-filter docs say that the filter can take an argument:
action
An optional callable that tells the filter how to handle the queryset. It recieves a
QuerySet and the value to filter on and should return a Queryset that is filtered
appropriately.
which seems to be what I am looking for, but the docs lacks any further explanation. Suggestions please?
#EDIT:
This is views.py:
def product_list(request):
f = ProductFilter(request.GET, queryset=Product.objects.all())
return render_to_response('my_app/template.html', {'filter': f})

Because of the way the final queryset is constructed, making each filter be ORed together is difficult. Essentially, the code works like this:
FilterSet, filterset.py line 253:
#property
def qs(self):
qs = self.queryset.all()
for filter_ in self.filters():
qs = filter_.filter(qs)
Filters, filters.py line 253:
def filter(self, qs):
return qs.filter(name=self.value)
Each filter can decide how to apply itself to the incoming queryset, and all filters, as currently implemented, filter the incoming queryset using AND. You could make a new set of filters that OR themselves to the incoming queryset, but there is no way of overriding the behaviour from the FilterSet side.

action won't cut it. This callback is used for particular filter field and only has access to that field's value.
The cleanest way would be to create multi-widget filter field, similar to RangeField. Check out the source.
So instead two date fields you use name, price and the logic type [AND|OR] as fields, this way you have access to all these values at once to use in custom queryset.
EDIT 1:
This is a little gist I wrote to show how to query two fields with selected operator.
https://gist.github.com/mariodev/6689472
Usage:
class ProductFilter(django_filters.FilterSet):
nameprice = NamePriceFilter()
class Meta:
model = Product
fields = ['nameprice']
It's actually not very flexible in terms of re-usage, but certainly can be re-factored to make it useful.

In order to make filters work with OR, you should make a subclass of FilterSet and override qs from Tim's answer like this:
#property
def qs(self):
qs = self.queryset.none()
for filter_ in self.filters():
qs |= filter_.filter(self.queryset.all())
I haven't tested this, but I think you got the idea. QuerySets support bitwise operations, so you can easily combine results of two filters with OR.

class FileFilterSet(django_filters.FilterSet):
class Meta:
model = File
fields = ['project']
def __init__(self, *args, **kwargs):
super(FileFilterSet, self).__init__(*args, **kwargs)
for name, field in self.filters.items():
if isinstance(field, ModelChoiceFilter):
field.extra['empty_label'] = None
field.extra['initial'] = Project.objects.get(pk=2)
# field.extra['queryset'] = Project.objects.filter(pk=2)
class FileFilter(FilterView):
model = File
template_name = 'files_list.html'
filterset_class = FileFilterSet

Related

Getting random object of a model with django-rest-framework after applying filters

So going based on this Stack Overflow question:
Getting random object of a model with django-rest-framework
I am trying to figure out how to do this, but after applying the filter backend.
I have this class with these methods
class DictionaryRandomView(generics.ListAPIView):
def get_queryset(self):
return Dictionary.objects.all()
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)
And several filter backends (can include if desired).
The problem is that I need to apply the filter backend before querying.
Should I do this inside the list method, or the get_queryset method?
For example, I have levels associated with a dictionary (for learning a language).
I want to limit my query to ONLY words with say, level 6, but then get random values (a dynamic number that I can pass as a filter) inside that set.
So pseudo-code for what I am trying to do would be something like this:
get_random_value("SELECT * FROM dictionary WHERE level = 6")
How can I do that in a DRF listAPIView?
I think the easiest way would be to filter by level in the get_queryset method and then apply the other filters using the filter backends.
For the ordering part you can use queryset.order_by('?') if you need to randomly sort the results, but be careful with it as it can be ver resource intensive.
Additionally, I would also suggest to keep your response object consistent (i.e. always send a paginated response for this endpoint) so you don't have to deal with different objects in your frontend.

Filter Django ResourceRelatedField's queryset

In our project we are using ResourceRelatedField for a foreign key field in one of our serializers to comply with JSON:API format. This is how it looks:
types = ResourceRelatedField(
queryset=Type.objects,
many=True
)
The problem that I have is that I want to exclude some of the items from the queryset of this field so that I don't get all the items from the Type model, but a subset.
If I write something like this it doesn't work:
types = ResourceRelatedField(
queryset=Type.objects.exclude(id=13),
many=True
)
Didn't find anything related in the documentation.
Perhaps You can use a SerializerMethodResourceRelatedField? (not tested).
types = SerializerMethodResourceRelatedField(many=True)
def get_types(self, obj):
return Type.objects.exclude(id=13)

Django admin change list view disable sorting for some fields

Is there are way(s) to disable the sorting function for some fields in django admin change list so for those fields, users cannot click the column header to sort the list.
I tried on the following method, but it doesn't work.
https://djangosnippets.org/snippets/2580/
I also tired to override the changelist_view in ModelAdmin but also nothing happen.
def changelist_view(self, request, extra_context=None):
self.ordering_fields = ['id']
return super(MyModelAdmin, self).changelist_view(request, extra_context)
In the above case, I would like to only allow user to sort the list by ID.
Anyone has suggestion? Thanks.
For Django 1.7 (or the version that I last use) do not support such things. One possible dirty work-around could be defining a model class method and using that method instead of model field.
class TestClass(Model):
some_field = (.....)
other_field = (........)
def show_other_field(self):
return self.other_field
class TestClassAdmin(ModelAdmin):
list_display = ("some_field", "show_other_field")
Since show_other_field is a model class method, django do not knows how to sort (or process) the return result of that method.
But as I said, this is a dirty hack that might require more processing (and maybe more database calls) according to use-case than displaying a field of a model.
Extra: If you want to make a model method sortable, you must pass admin_order_field value like:
def show_other_field(self):
return self.other_field
show_other_field.admin_order_field = "other_field"
That will make your model method sortable in admin list_display. But you have to pass a field or relation that is usable in the order_by method of database api.
TestClass.objects.filter(....).order_by(<admin_order_field>)

How to filter Haystack SearchQuerySets by related models

How do you filter/join a Haystack SearchQuerySet by related model fields?
I have a query like:
sqs = SearchQuerySet().models(models.Person)
and this returns the same results that the equivalent admin page returns.
However, if I try and filter by model records linked by a foreign key:
sqs = sqs.filter(workplace__role__name='teacher')
it returns nothing, even though the page /admin/myapp/person/?workplace__role__name=teacher returns several records.
I don't want to do any full-text searching of these related models. I only want to do a simple exact-match filter. Is that possible with Haystack?
You cannot perform joins using a search engine like the ones supported by haystack.
To make queries like this you need to add the information you want to filter on in a "denormalized" fashion in your search index:
class ProfileIndex(indexes.SearchIndex, indexes.Indexable):
# your other fields, most likely model attributes
role_name = indexes.CharField()
def get_model(self):
return Person
def prepare_role_name(self, person):
return person.workplace.role_name
Then you can filter on a field role_name. Just make sure to update your index if eg. the name changes, then you have to update all the according entries in the search index.
You can also do this:
class ProfileIndex(indexes.SearchIndex, indexes.Indexable):
# your other fields, most likely model attributes
role_name = indexes.CharField(model_attr='workplace__role__name')
def get_model(self):
return Person
And you can filter by role_name.
I saw it here. http://django-haystack.readthedocs.org/en/latest/searchindex_api.html

Tastypie Dehydrate reverse relation count

I have a simple model which includes a product and category table. The Product model has a foreign key Category.
When I make a tastypie API call that returns a list of categories /api/vi/categories/
I would like to add a field that determines the "product count" / the number of products that have a giving category. The result would be something like:
category_objects[
{
id: 53
name: Laptops
product_count: 7
},
...
]
The following code is working but the hit on my DB is heavy
def dehydrate(self, bundle):
category = Category.objects.get(pk=bundle.obj.id)
products = Product.objects.filter(category=category)
bundle.data['product_count'] = products.count()
return bundle
Is there a more efficient way to build this query? Perhaps with annotate ?
You can use prefetch_related method of QuerSet to reverse select_related.
Asper documentation,
prefetch_related(*lookups)
Returns a QuerySet that will automatically
retrieve, in a single batch, related objects for each of the specified
lookups.
This has a similar purpose to select_related, in that both are
designed to stop the deluge of database queries that is caused by
accessing related objects, but the strategy is quite different.
If you change your dehydrate function to following then database will be hit single time.
def dehydrate(self, bundle):
category = Category.objects.prefetch_related("product_set").get(pk=bundle.obj.id)
bundle.data['product_count'] = category.product_set.count()
return bundle
UPDATE 1
You should not initialize queryset inside dehydrate function. queryset should be always set in Meta class only. Please have a look at following example from django-tastypie documentation.
class MyResource(ModelResource):
class Meta:
queryset = User.objects.all()
excludes = ['email', 'password', 'is_staff', 'is_superuser']
def dehydrate(self, bundle):
# If they're requesting their own record, add in their email address.
if bundle.request.user.pk == bundle.obj.pk:
# Note that there isn't an ``email`` field on the ``Resource``.
# By this time, it doesn't matter, as the built data will no
# longer be checked against the fields on the ``Resource``.
bundle.data['email'] = bundle.obj.email
return bundle
As per official django-tastypie documentation on dehydrate() function,
dehydrate
The dehydrate method takes a now fully-populated bundle.data & make
any last alterations to it. This is useful for when a piece of data
might depend on more than one field, if you want to shove in extra
data that isn’t worth having its own field or if you want to
dynamically remove things from the data to be returned.
dehydrate() is only meant to make any last alterations to bundle.data.
Your code does additional count query for each category. You're right about annotate being helpfull in this kind of a problem.
Django will include all queryset's fields in GROUP BY statement. Notice .values() and empty .group_by() serve limiting field set to required fields.
cat_to_prod_count = dict(Product.objects
.values('category_id')
.order_by()
.annotate(product_count=Count('id'))
.values_list('category_id', 'product_count'))
The above dict object is a map [category_id -> product_count].
It can be used in dehydrate method:
bundle.data['product_count'] = cat_to_prod_count[bundle.obj.id]
If that doesn't help, try to keep similar counter on category records and use singals to keep it up to date.
Note categories are usually a tree-like beings and you probably want to keep count of all subcategories as well.
In that case look at the package django-mptt.

Categories