Django-filter multiple URL parameters - python

I am using Django-filter app to construct search on my website. This is the code:
class PropertyFilter(django_filters.FilterSet):
city = django_filters.ModelMultipleChoiceFilter(queryset=City.objects.all(), widget = CheckboxSelectMultiple)
trade_type = django_filters.ModelMultipleChoiceFilter(queryset=Trade.objects.all(), widget = CheckboxSelectMultiple)
class Meta:
model = Property
fields = ['city', 'trade_type']
The problem is that when user marks two cities, Django-filter only filters objects via last URL parameter (city no. 2 in this casse):
http://example.org/lt/list/city=1&city=2
Models.py:
class City(models.Model):
name = models.CharField(max_length=250, verbose_name=_('Name'))
Maybe I am doing something wrong ?

You could create a plural version of your query string and accept a list as the filter argument:
http://example.org/lt/list/?cities=1,2
class CustomFilterList(django_filters.Filter):
def filter(self, qs, value):
if value not in (None, ''):
values = [v for v in value.split(',')]
return qs.filter(**{'%s__%s' % (self.name, self.lookup_type): values})
return qs
class PropertyFilter(django_filters.FilterSet):
city = django_filters.ModelMultipleChoiceFilter(queryset=City.objects.all(), widget = CheckboxSelectMultiple)
trade_type = django_filters.ModelMultipleChoiceFilter(queryset=Trade.objects.all(), widget = CheckboxSelectMultiple)
cities = CustomFilterList(name="city", lookup_type="in")
class Meta:
model = Property
fields = ['cities', 'city', 'trade_type']
Check out this answer for filtering a list of values properly:
Possible to do an `in` `lookup_type` through the django-filter URL parser?

You can get it to work with the same URL you were trying. Follow my example. You have to pass the choices that you want to filter with.
The URL that I'm calling:
http://example.org/product-list/gender=1&gender=2
filters.py
GENDER_CHOICES = tuple(
ProductAttributeOptions.objects.filter(group__name='gender').values_list('id', 'option'))
class ProductFilter(django_filters.FilterSet):
gender = django_filters.MultipleChoiceFilter(choices=GENDER_CHOICES,
method='filter_gender')
def filter_gender(self, qs, name, value):
result = qs.filter(Q(attribute_values__attribute__name='gender',
attribute_values__value_option__in=value))
return result
class Meta:
model = Product
fields = ('gender')
Hope this could help. I took inspiration from the official docs.

The best way is to use custom filter from Django filter DOC
in my case i used (',') to split the url should look like this:
localhost:8000/?city=1,2,3 (you can use Strings values)
class F(django_filters.FilterSet):
city = CharFilter(method='my_custom_filter')
class Meta:
model = Property
fields = ['city','trade_type']
def my_custom_filter(self, queryset, name, value):
value_list = value.split(u',') #split the values by ,
return queryset.filter(**{
name+"__in": value_list, #add __in to get each value of the list
})

Related

Using Django-Filter for a Many-to-many relationship

Essentially, I've been using Django-Filter to filter a large list of publications on various fields. One field is keywords, which has a many-to-many relationship through a PublicationKeywords table. The models look as follows (withholding certain fields and information):
class Publication(models.Model):
keywords = models.ManyToManyField(Keyword, through='PublicationKeywords')
class Keyword(models.Model):
value = models.CharField(max_length=100)
class PublicationKeywords(models.Model):
keyword = models.ForeignKey(Keyword, db_column='keyword_id',
on_delete=models.SET_NULL,
null=True)
publication = models.ForeignKey(Publication, db_column='publication_id',
on_delete=models.SET_NULL,
null=True)
Is it possible to use a ModelChoiceFilter or ModelMultipleChoiceFilter in this case to do something similar to
PublicationKeywords.objects.filter(keyword__value_in=keyword_list).distinct('publication')
Basic filter set up looks as follows
class PublicationFilter(django_filters.FilterSet):
title = django_filters.CharFilter(lookup_expr='icontains')
state = django_filters.ChoiceFilter(
choices=STATE_CHOICES)
sac = django_filters.ModelChoiceFilter(queryset=Sac.objects.all())
published_date_after = DateFilter(
field_name='published_date', lookup_expr=('gte'))
published_date_before = DateFilter(
field_name='published_date', lookup_expr=('lte'))
data_begin_year = django_filters.NumberFilter(
lookup_expr='gte')
data_end_year = django_filters.NumberFilter(
lookup_expr='lte')
keywords = django_filters.ModelMultipleChoiceFilter(queryset=?)
# TODO figure out how to get this keywords filter to work like
# PublicationKeywords.objects.filter(keyword__value_in=keyword_list).distinct('publication')
class Meta:
model = Publication
strict = False
fields = ['title', 'state', 'sac', 'published_date',
'data_begin_year', 'data_end_year', 'keywords']
def __init__(self, *args, **kwargs):
super(PublicationFilter, self).__init__(*args, **kwargs)
if self.data == {}:
self.queryset = self.queryset.none()
I don't know if there was a simpler solution, but I could not get Django-Filter to properly filter by multiple keywords, so I altered my views.py. A little bit hacky, but since the keywords were captured properly in the querystring, it was simple enough to grab those values and put them into a list through self.request.GET, and then conditionally set the queryset passed in PublicationFilter.
keyword_list = []
try:
keyword_list = self.request.GET['keywords'].split(", ")
except:
pass
if len(keyword_list) > 1:
filter_list = PublicationFilter(
self.request.GET, queryset=Publication.objects.filter(keywords__value__in=keyword_list).distinct())
else:
filter_list = PublicationFilter(
self.request.GET, queryset=Publication.objects.all())
paginator = Paginator(filter_list.qs, 10)

Django-filter, how to make multiple fields search? (with django-filter!)

How can I make multiple fields search with Django-filter from model like:
class Location(models.Model):
loc = models.CharField(max_length=100, blank=True)
loc_mansioned = models.CharField(max_length=100, blank=True)
loc_country = models.CharField(max_length=100, blank=True)
loc_modern = models.CharField(max_length=100, blank=True)
I need one input field on my website, that can search over all fields of Location model
You can probably create a custom filter and do something like this:
from django.db.models import Q
import django_filters
class LocationFilter(django_filters.FilterSet):
q = django_filters.CharFilter(method='my_custom_filter', label="Search")
class Meta:
model = Location
fields = ['q']
def my_custom_filter(self, queryset, name, value):
return queryset.filter(
Q(loc__icontains=value) |
Q(loc_mansioned__icontains=value) |
Q(loc_country__icontains=value) |
Q(loc_modern__icontains=value)
)
This would filter by any of of those fields. You can replace the icontains with whatever you want.
This is perfect. I'm trying to do a dynamic filter, with a switch to get one more field in the search if checked. Something like this:
def my_custom_filter(self, queryset, name, value):
return Reference.objects.filter(
Q(ref_title__icontains=value))
def my_custom_filter_with_description(self, queryset, name, value):
return Reference.objects.filter(
Q(ref_title__icontains=value) | Q(complete_description__icontains=value))
But I have no clue how to link the switch to the class
Due that you've defined Location as an object, to filter by multiple fields just use the filter method.
filterlocation = Location.objects.filter(loc=formloc, loc_mansioned=formlocmansioned, loc_country=formloccountry, loc_modern=formlocmodern)
But you need to implement a better way to use this filters, so only the result that have all conditions will be displayed.
Another solution, since the other one was not working directly:
#staticmethod
def filter_stock(qs, name, value):
return qs.filter(
Q(ticker__exact=value) | Q(company__iexact=value)
)

Django Rest Framework: How to modify output structure?

Is there a way to group fields in Serializer/ModelSerializer or to modify JSON structure?
There is a Location model:
class Location(Model):
name_en = ...
name_fr = ...
...
If I use ModelSerializer I get plain representation of the object fields like:
{'name_en':'England','name_fr':'Angleterre'}
I want to group some fields under "names" key so I get
{'names':{'name_en':'England','name_fr':'Angleterre'}}
I know I can create custom fields but I want to know if there is a more straightforward way. I tried
Meta.fields = {'names':['name_en','name_fr']...}
which doesn't work
I think it is better using a property. Here is the whole example.
class Location(models.Model):
name_en = models.CharField(max_length=50)
name_fr = models.CharField(max_length=50)
#property
def names(self):
lst = {field.name: getattr(self, field.name)
for field in self.__class__._meta.fields
if field.name.startswith('name_')}
return lst
In views:
class LocationViewSet(viewsets.ModelViewSet):
model = models.Location
serializer_class = serializers.LocationSerializer
queryset = models.Location.objects.all()
And in serializers:
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = ('id', 'names')
My result for my fake data:
[{
"id": 1,
"names": {
"name_en": "England",
"name_fr": "Angleterre"}
}]
Try to create a wrapper serialzer and place the LocationSerializer inside it
class LocationSerialzer(serializers.ModelSerialzer):
name_en = ...
name_fr = ...
...
class MySerializer(serializers.ModelSerializer):
name = LocationSerialzer()
...
Using the above method , you can apply your own customization without being limited to drf custom fields.
You could also not use a property on the model and but use a SerializerMethodField on your serializer like in this implementation.
We used here a _meta.fields, like in the other implementation, to get all the fields that starts with name_ so we can dynamically get the output you desired
class LocationSerializer(serializers.ModelSerializer):
names = serializers.SerializerMethodField()
def get_names(self, obj):
lst = {field.name: getattr(obj, field.name)
for field in obj.__class__._meta.fields
if field.name.startswith('name_')}
return lst
class Meta:
model = Location
fields = ('id', 'names')

Modifying value on serialization - Django Rest Framework

I have a model which contains sensitive data, let's say a social security number, I would like to transform that data on serialization to display only the last four digits.
I have the full social security number stored: 123-45-6789.
I want my serializer output to contain: ***-**-6789
My model:
class Employee (models.Model):
name = models.CharField(max_length=64,null=True,blank=True)
ssn = models.CharField(max_length=16,null=True,blank=True)
My serializer:
class EmployeeSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
class Meta:
model = Employee
fields = ('id','ssn')
read_only_fields = ['id']
You can use SerializerMethodField:
class EmployeeSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
ssn = SerializerMethodField()
class Meta:
model = Employee
fields = ('id','ssn')
read_only_fields = ['id']
def get_ssn(self, obj):
return '***-**-{}'.format(obj.ssn.split('-')[-1]
If you don't need to update the ssn, just shadow the field with a SerializerMethodField and define get_ssn(self, obj) on the serializer.
Otherwise, the most straightforward way is probably to just override .to_representation():
def to_representation(self, obj):
data = super(EmployeeSerializer, self).to_representation(obj)
data['ssn'] = self.mask_ssn(data['ssn'])
return data
Please add special case handling ('ssn' in data) as necessary.
Elaborating on #dhke’s answer, if you want to be able to reuse this logic to modify serialization across multiple serializers, you can write your own field and use that as a field in your serializer, such as:
from rest_framework import serializers
from rest_framework.fields import CharField
from utils import mask_ssn
class SsnField(CharField):
def to_representation(self, obj):
val = super().to_representation(obj)
return mask_ssn(val) if val else val
class EmployeeSerializer(serializers.ModelSerializer):
ssn = SsnField()
class Meta:
model = Employee
fields = ('id', 'ssn')
read_only_fields = ['id']
You can also extend other fields like rest_framework.fields.ImageField to customize how image URLs are serialized (which can be nice if you’re using an image CDN on top of your images that lets you apply transformations to the images).

General way of filtering by IDs with DRF

Is there a generic way that I can filter by an array of IDs when using DRF?
For example, if I wanted to return all images with the following IDs, I would do this:
/images/?ids=1,2,3,4
My current implementation is to do the following:
# filter
class ProjectImageFilter(django_filters.FilterSet):
"""
Filter on existing fields, or defined query_params with
associated functions
"""
ids = django_filters.MethodFilter(action='id_list')
def id_list(self, queryset, value):
"""
Filter by IDs by passing in a query param of this structure
`?ids=265,263`
"""
id_list = value.split(',')
return queryset.filter(id__in=id_list)
class Meta:
model = ProjectImage
fields = ['ids',]
# viewset
class Images(viewsets.ModelViewSet):
"""
Images associated with a project
"""
serializer_class = ImageSerializer
queryset = ProjectImage.objects.all()
filter_class = ProjectImageFilter
However, in this case ProjectImageFilter requires a model to be specified ( ProjectImage). Is there a way that I can just generally define this filter so I can use it on multiple ViewSets with different models?
One solution without django-filters is to just super() override get_queryset. Here is an example:
class MyViewSet(view.ViewSet):
# your code
def get_queryset(self):
queryset = super(MyViewSet, self).get_queryset()
ids = self.request.query_params.get('ids', None)
if ids:
ids_list = ids.split(',')
queryset = queryset.filter(id__in=ids_list)
return queryset
The library django-filter has support for this using BaseInFilter, in conjunction with DRF.
From their docs:
class NumberRangeFilter(BaseRangeFilter, NumberFilter):
pass
class F(FilterSet):
id__range = NumberRangeFilter(field_name='id', lookup_expr='range')
class Meta:
model = User
User.objects.create(username='alex')
User.objects.create(username='jacob')
User.objects.create(username='aaron')
User.objects.create(username='carl')
# Range: User with IDs between 1 and 3.
f = F({'id__range': '1,3'})
assert len(f.qs) == 3

Categories