DRF - in filter, use field-value instead of default pk / id - django-rest-framework

I'm trying to use DRF's filters so that the URL query is like so:
/roadname/?road=M5
not like so
/roadinfo/?road=1
I can't seem to do it when I've got a ForeignKey relationship.
I've tried using lookup_field with no luck (although not sure how this would work for multiple filter fields anyway - I don't think that's the answer). I've tried using a get_queryset() method in views as in the second example in the documentation. A comment I came across suggested that this is bad RESTApi practice - is it? How would a user know to type in '1' to get results for 'M5' in a front-end client?
I've set up two really simple models (and serializers, views, etc.) to try these out as below.
If I use RoadName, I have to type the name into the filter search box (rather than having a dropdown), but the url query is how I want it.
If I use RoadInfo (which has a ForeignField to RoadName), I get a drop down in the filter box, but the url query uses the ForeignKey pk.
My question: How can I set it so that when I use RoadInfo, the query uses the field value rather than the id/pk?
Models
from django.db import models
class RoadName(models.Model):
road = models.CharField(max_length=50)
def __str__(self):
return str(self.road)
class RoadInfo(models.Model):
road = models.ForeignKey(RoadName, on_delete='CASCADE')
# other data
def __str__(self):
return str(self.road)
Serializers
from traffic.models import *
from rest_framework import serializers
class RoadNameSerializer(serializers.ModelSerializer):
road = serializers.CharField()
class Meta:
model = RoadName
exclude = ('id',)
class RoadInfoSerializer(serializers.ModelSerializer):
road = RoadNameSerializer()
class Meta:
model = RoadInfo
exclude = ('id',)
Views
from traffic.serializers import *
from traffic.models import *
from django_filters import rest_framework as filters
from rest_framework import viewsets
class RoadNameViewSet(viewsets.ReadOnlyModelViewSet):
""" List of all traffic count Counts """
queryset = RoadName.objects.all()
serializer_class = RoadNameSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = '__all__'
class RoadInfoViewSet(viewsets.ReadOnlyModelViewSet):
""" List of all traffic count Counts """
queryset = RoadInfo.objects.all()
serializer_class = RoadInfoSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = '__all__'

The data M5 on the road attribute of RoadName model. It can be filtered by road__road from RoadInfo model.
So, Try /roadname/?road__road=M5

Related

How to efficiently parse multiple data with json?

I have Django REST framework app
When for example I make POST book with title Hobbit
I want to fetch data into database but problem
https://www.googleapis.com/books/v1/volumes?q=Hobbit
I got many many book with this title and I only need specific data like:
my models:
class Book(models.Model):
title = models.CharField(
max_length=249,
null=True)
autors = models.CharField(
max_length=500,
null=True)
published_date = models.CharField(
max_length=100,
blank=True)
categories = models.CharField(
max_length=500,
null=True)
average_rating = models.PositiveBigIntegerField(
blank=True)
ratings_count = models.PositiveBigIntegerField(
null=True)
thumbnail = models.CharField(
max_length=300,
null=True)
What best practise to parse all this data and save into database ?
I already know how with requests get to the data I need.
Problem is how effectively use drf and implement this feature in the best way .
gp
As the names and types in the response are not exactly the same than in your model, you will need to do a little bit of custom mapping.
Then you have two choices to create your objects, either calling Book.objects.create() or serializer.save()
Most of the time, you will just want to stick to serializers.
import requests
import json
from book.models import Book
from book.serializers import BookSerializer
items = json.loads(requests.get("https://www.googleapis.com/books/v1/volumes?q=Hobbit").content).get('items')
for book in items:
mapping = {
'title': book['volumeInfo']['title'],
'autors': ','.join(book['volumeInfo']['authors']),
...
}
# Via a serializer
serializer = BookSerializer(**mapping)
if serializer.is_valid():
instance = serializer.save()
# Via Django ORM
instance = Book.objects.create(**mapping)
You can use a ModelSerializer (see here), that would look like this :
from rest_framework import serializers
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['title', 'autors', 'published_date', ...] # or '__all__' if you want them all

Apply filters to any or a certain many-to-many record

Consider these models:
from django.db import models
class Group(models.Model):
name = models.CharField()
class Person(models.Model):
height = models.PositiveIntegerField()
weight = models.PositiveIntegerField()
gender = models.CharField()
groups = models.ManyToManyField(Group, blank=True)
and the DRF views
from rest_framework import viewsets
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters import rest_framework as filters
from .serializers import GroupSerializer
from ..models import Group
class GroupViewSet(viewsets.ModelViewSet):
queryset = Group.objects.all().distinct()
serializer_class = GroupSerializer
filter_backends = (filters.DjangoFilterBackend,
SearchFilter, OrderingFilter)
filter_class = GroupFilter
A group can have 0,1,2 or more Persons, with 1 and 2 being the most common and where these 1 and 2 are clearly defined. Think of it as Facebook's chat: you have one-on-one chat most commonly, but sometimes you can have a group chat. When is one-one-one chat, 1 is sender, 2 is receiver.
I need to filter these records from DRF, when browsing the GroupViewSet and filter by Person attributes, where I can apply a group of filters to any Person or a certain Person.
For any person, no matter to which a certain condition is applied, is clear:
/api/group/?person__height__gt=100&person__weight__gt=200
But for a certain person, where a group of conditions apply to that person, in the URL, I could have something like:
/api/group/?person__0__height__gt=100&person__0__weight__gt=200&person__1__height__lte=200
And declare these into my custom FilterSet:
from django.db.models.constants import LOOKUP_SEP
class GroupFilter(filters.FilterSet):
person__0__height = filters.NumberFilter(method='person_filter')
person__0__height__gt = filters.NumberFilter(method='person_filter')
person__0__height__lt = filters.NumberFilter(method='person_filter')
# ... and so on for the rest of the possibilities
def person_filter(self, queryset, name, value):
m2mfield, index, field, *comparison = name.split(LOOKUP_SEP, 3)
# do subqueries based on the above and construct queryset filter.
But as you can imagine, this implies that I'll have a lot of boilerplate code. In my real models there are many fields and the above "solution" seems hacky to me.
So the question is: is there an easier/cleaner way to achieve the above filtering?
Maybe by dynamically declaring the person__0__height__gt attributes, for which I couldn't yet find a solution.
Note that I do not know the IDs of the Person entities upfront. Those person__0, person__1 are array indexes.
try this for cleaning code :
class GroupFilter(django_filters.FilterSet):
person_range = django_filters.NumericRangeFilter(field_name='person__0__height', lookup_expr='range')
person = django_filters.NumberFilter(field_name='person__0__height', lookup_expr='exact')
class Meta:
model = Group
fields = ('person_range','person',)
and call with url like this :
127.0.0.1:8000/yourpath/?person=180&person_range_min=130&person_range_max=210

How do I specify multiple column name in a Django search field?

i use django filter backend
I define multiple columns in a search field, as follows:
class tableViewSet(ModelViewSet):
"""
A simple ViewSet for viewing and editing accounts.
"""
queryset = table.objects.all()
serializer_class = WebSerializer
pagination_class = StandardResultsSetPagination
filter_backends = (DjangoFilterBackend,SearchFilter)
search_fields = ('name','family','tel',)
i want to make an api that handle some query like this:
select * from table1 where name like ('tom') and family like ('%anderson%') and tel like ('%0223654%')
Is there any way to specify the column name in the API?
For example:
http://127.0.0.1/api-user/table/search?name=tom&family=andeson&tel=0223455
You should rather use DjangoFilterBackend instead of SearchFilter if you want to filter per specific attribute.
As you are not looking for exact search, you will have to create a FilterSet and use filter_class in you view.
Something like:
class TableFilter(django_filters.FilterSet):
name = django_filters.CharFilter(name='name', lookup_expr='icontains')
family = django_filters.CharFilter(name='family', lookup_expr='icontains')
tel = django_filters.CharFilter(name='tel', lookup_expr='icontains')
class Meta:
model = table
fields = ['name', 'family', 'tel']
class tableViewSet(ModelViewSet):
"""
A simple ViewSet for viewing and editing accounts.
"""
queryset = table.objects.all()
serializer_class = WebSerializer
pagination_class = StandardResultsSetPagination
filter_backends = (DjangoFilterBackend,SearchFilter)
filter_class = TableFilter
Specifying the field in the query string is unnecessary. The following request should work:
http://127.0.0.1/api-user/table?search=something
relevant docs

Foreign key field disappears in swagger docs after adding depth attribute in Serializer

Whenever I define the depth attribute, the foreign key field from swagger docs in POST section disappears. That seems strange because I required depth = 1 when I want related data in my GET request. So I can not remove this in order to get this related field parameter in the POST section.
Here is the case.
Model:
from django.db import models
from django.conf import settings
# Create your models here.
User = settings.AUTH_USER_MODEL
class Todo(models.Model):
user = models.ForeignKey(User)
title = models.CharField("Title", max_length=255)
completed = models.BooleanField("Completed")
Serializer without depth =1.
from rest_framework import serializers
from models import Todo
class TodoSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Todo
Swagger output:
Now If I add depth = 1 than Swagger does not display related field.
Let me know if anyone has any clue about this.
Thanks :)
Finally after digging into this, I come up with solution by which we can avoid this issue and achieve the expected solution.
So the solution is "Instead of using depth = 1 attribute we can using related serializer instance it self where it works similar to depth functionality."
Here is tested solution
Model:
from django.db import models
from django.conf import settings
User = settings.AUTH_USER_MODEL
class Todo(models.Model):
user = models.ForeignKey(User)
title = models.CharField("Title", max_length=255)
completed = models.BooleanField("Completed")
Serializer
from rest_framework import serializers
from django.conf import settings
from models import Todo
User = settings.AUTH_USER_MODEL
class UserSerializer(serializers.HyperlinkedSerializer):
class Meta:
model = User
class TodoSerializer(serializers.HyperlinkedModelSerializer):
user = UserSerializer()
class Meta:
model = Todo
fields = ('user', 'title', 'completed')
Swagger Output:
This solution is kind of different approach in order to achieve the required functionality, But still I am expecting an official solution from django-rest-swagger team, Even I have posted the same query on django-rest-swagger github repo here.
One solution is to just don't use depth and override to_representation method of serializer:
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
def to_representation(self, instance):
r = super(TodoSerializer, self).to_representation(instance)
r.update({'user': UserSerializer().to_representation(instance.user)})
return r
This way, in post everything will be as it was, and in get when return json of todo then to_representation will be called and will add user to json data.
You need to update your serialzer as follows
class TodoSerializer(serializers.HyperlinkedModelSerializer):
creator = serializers.RelatedField(queryset=User.objects.all())
class Meta:
model = Todo
fields = ("name", "task", "creator")
depth = 1
you need to mentions fields and RelatedField in your serializers

Possible to do an `in` `lookup_type` through the django-filter URL parser?

I'm using django-filter with django-rest-framework and I'm trying to instantiate a filter that accepts lists of numbers for filtering the query set down
class MyFilter(django_filters.FilterSet):
ids = django_filters.NumberFilter(name='id',lookup_type='in')
class Meta:
model = MyModel
fields = ('ids',)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_class = MyFilter
If I pass in a comma separated list of integers, the filter is ignored altogether.
If I pass in a single integer, it gets through django-filter into django's form validator and complains:
'Decimal' object is not iterable
Is there a way to create a django-filter object which can handle a list of integers and properly filter down the queryset?
For better or worse, I created a custom filter for this:
class IntegerListFilter(django_filters.Filter):
def filter(self,qs,value):
if value not in (None,''):
integers = [int(v) for v in value.split(',')]
return qs.filter(**{'%s__%s'%(self.name, self.lookup_type):integers})
return qs
Which is used like:
class MyFilter(django_filters.FilterSet):
ids = IntegerListFilter(name='id',lookup_type='in')
class Meta:
model = MyModel
fields = ('ids',)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_class = MyFilter
Now my interface accepts comma-delimited lists of integers.
I know this is an old post, but there is now a better solution. The change that makes it correct is posted here.
They added a BaseInFilter and a BaseRangeFilter. The documentation is here.
Big picture, BaseFilter checks for CSV, and then when mixed with another filter it does what you are asking. Your code can now be written like:
class NumberInFilter(filters.BaseInFilter, filters.NumberFilter):
pass
class MyModelViewSet(viewsets.ModelViewSet):
ids = NumberInFilter(name='id', lookup_expr='in')
class Meta:
model = MyModel
fields = ['ids']
Here's a complete solution:
from django_filters import Filter, FilterSet
from rest_framework.filters import DjangoFilterBackend
from rest_framework.viewsets import ModelViewSet
from .models import User
from .serializers import UserSerializer
class ListFilter(Filter):
def filter(self, qs, value):
if not value:
return qs
self.lookup_type = 'in'
values = value.split(',')
return super(ListFilter, self).filter(qs, values)
class UserFilter(FilterSet):
ids = ListFilter(name='id')
class Meta:
model = User
fields = ['ids']
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
queryset = User.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_class = UserFilter
According to a post in the django-filter issues:
from django_filters import Filter
from django_filters.fields import Lookup
class ListFilter(Filter):
def filter(self, qs, value):
return super(ListFilter, self).filter(qs, Lookup(value.split(u","), "in"))
I have personally used this without any issue in my projects, and it works without having to create a per-type filter.
Based on #yndolok answer I have come to a general solution. I think filtering by a list of ids is a very common task and therefore should be included in the FilterBackend:
class ListFilter(django_filters.Filter):
"""Class to filter from list of integers."""
def filter(self, qs, value):
"""Filter function."""
if not value:
return qs
self.lookup_type = 'in'
try:
map(int, value.split(','))
return super(ListFilter, self).filter(qs, value.split(','))
except ValueError:
return super(ListFilter, self).filter(qs, [None])
class FilterBackend(filters.DjangoFilterBackend):
"""A filter backend that includes ListFilter."""
def get_filter_class(self, view, queryset=None):
"""Append ListFilter to AutoFilterSet."""
filter_fields = getattr(view, 'filter_fields', None)
if filter_fields:
class AutoFilterSet(self.default_filter_set):
ids = ListFilter(name='id')
class Meta:
model = queryset.model
fields = list(filter_fields) + ["ids"]
return AutoFilterSet
else:
return super(FilterBackend, self).get_filter_class(view, queryset)
Uptodate solution:
from django_filters import rest_framework as filters
name-->field_name
lookup_type-->lookup_expr
class IntegerListFilter(filters.Filter):
def filter(self,qs,value):
if value not in (None,''):
integers = [int(v) for v in value.split(',')]
return qs.filter(**{'%s__%s'%(self.field_name, self.lookup_expr):integers})
return qs
class MyFilter(filters.FilterSet):
ids = IntegerListFilter(field_name='id',lookup_expr='in')
class Meta:
model = MyModel
fields = ('ids',)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_class = MyFilter
As I have answered here DjangoFilterBackend with multiple ids, it is now pretty easy to make a filter that accepts list and validates the contents
For Example:
from django_filters import rest_framework as filters
class NumberInFilter(filters.BaseInFilter, filters.NumberFilter):
pass
class MyFilter(filters.FilterSet):
id_in = NumberInFilter(field_name='id', lookup_expr='in')
class Meta:
model = MyModel
fields = ['id_in', ]
This will accept a list of integers from a get parameter. For example /endpoint/?id_in=1,2,3

Resources