First time developing a django application, and am trying to do something somewhat non-standard...
Is there a way to configure a view that will allow a user to look up a certain model by either one of two unique model attributes.
Ideally, both of these URL schemes would be possible
urlpatterns = [
path('api/somemodel/<int:model_id>/', views.SomeModelDetailView.as_view())
path('api/somemodel/<str:model_name>/', views.SomeModelDetailView.as_view())
]
A simplified example model... Both the id and the name are guaranteed to be unique. Also, by convention, my data is entered in such a way that a name will always be a string and never an integer
from django.db import models
class SomeModel(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100, unique=True)
Currently, I have this working with the following view...
from rest_framework import generics
from rest_framework import status
from rest_framework.response import Response
from . import models
class SomeModelDetailView(generics.RetrieveAPIView):
queryset = models.SomeModel.objects.all()
serializer_class = serializers.SomeModelSerializer
def get(self, request, model_name=None, model_id=None, format=None):
field = None
key = None
try:
if model_id:
field = "model_id"
key = model_id
m = models.SomeModel.objects.get(id=model_id)
elif model_name:
field = "model_name"
key = model_name
m = models.SomeModel.objects.get(name=model_name)
else:
return Response("Neither model_id nor model_name were provided", status=status.HTTP_400_BAD_REQUEST)
except models.SomeModel.DoesNotExist:
return Response("Unknown {field}: {key}".format(field=field, key=key), status=status.HTTP_400_BAD_REQUEST)
serializer_class = self.get_serializer_class()
serializer = serializer_class(m)
return Response(serializer.data)
However, I am wondering if there is a better way that fits more into a ViewSet/Router (or other) DRF mechanic.
Any ideas?
I think both existing answers (Don's and changak's) are very informative... however I wanted to take it a step further.
This is what I ended up with - it is inspired from Changak's answer however is slightly more generic
class MultiKeyGetObject(generics.GenericAPIView):
def __init__(self):
if not hasattr(self, 'lookup_fields'):
raise AssertionError("Expected view {} to have `.lookup_fields` attribute".format(self.__class__.__name__))
def get_object(self):
for field in self.lookup_fields:
if field in self.kwargs:
self.lookup_field = field
break
else:
raise AssertionError(
'Expected view %s to be called with one of the lookup_fields: %s' %
(self.__class__.__name__, self.lookup_fields))
return super().get_object()
I also loved learning about Q objects from Don - I can imagine a use case where you would want to retrieve objects using ALL of the lookup fields (either an AND or an OR). I feel this is getting into filter territory, however it may be useful...
from functools import reduce
from operator import or_
from rest_framework.generics import get_object_or_404
def get_object(self):
query = reduce(or_, [Q(**{field: self.kwargs[field]}) for field in self.lookup_fields if field in self.kwargs])
obj = get_object_or_404(self.get_queryset(), query)
self.check_object_permissions(self.request, obj)
return obj
Both of the above methods can then be used by a view such as...
class SomeObjectDetailAPIView(MultiKeyGetObject, generics.RetrieveUpdateDestroyAPIView):
serializer_class = serializers.SomeModelSerializer
queryset = models.SomeModel.objects.all()
lookup_fields = ('id', 'name')
I believe you can do that in a generic way, first change the urlpttern to this:
urlpatterns = [
path('api/somemodel/<str:pk>/', views.SomeModelDetailView.as_view()),
] ### this path matches both of the keys you wanted str and integer(integer is a str too)
then in the view you only need to override the get_object() in this way(don't override get() that is not the drf way):
from rest_framework import generics
from rest_framework import status
from rest_framework.response import Response
from . import models
class SomeModelDetailView(generics.RetrieveAPIView):
queryset = models.SomeModel.objects.all()
serializer_class = serializers.SomeModelSerializer
lookup_url_kwarg = 'pk'
def get_object(self):
pk = self.kwargs[self.lookup_url_kwarg] ## first get value the url parameter(pk)
### then here convert its type to int if it's an integer,
### it's not a bad thing, path() will have done this if we specify its type `int` in the url
try:
self.kwargs[self.lookup_url_kwarg] = int(pk)
self.lookup_field = 'id' ### change the lookup field to 'id' if it's an integer
except:
self.lookup_field = 'name' ### change the lookup field to 'name' if it's a str
return super(SomeModelDetailView, self).get_object() ## finally call the super get_object
This is possible using what Django calls "Q objects". These allow you to perform logical operations to queries allowing you to query for id=model_id or name=model_name.
For example:
from django.db.models import Q
...
m = models.SomeModel.objects.get(Q(id=model_id) | Q(name=model_name))
...
Related
I'm getting trouble when trying to queryset multiple pks (pk and pk2) in my URL.
I want to do something like this:
urlpatterns = [
path('tableau/<int:pk>/liste/<int:pk2>', views.ListeDetail.as_view()),
]
I need a detail view of liste (pk2) contained in a tableau (pk).
So far, here is my class but it does'nt work properly.
class ListeDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ListeSerializer
def get_queryset(self):
queryset = Liste.objects.filter(tableau_id=self.kwargs["pk"])
return queryset
Anyone know how to work with multiple pks in the url ?
urlpatterns = [
path('tableau/<int:tableau_id>/liste/<int:sub_id>', views.ListeDetail.as_view()),
]
Here you are dealing with nested resources, you need provide lookup_url_kwarg to filter the first level of resource.
from rest_framework.generics import get_object_or_404
class ListeDetail(generics.RetrieveUpdateDestroyAPIView):
lookup_url_kwarg = 'tableau_id' # get your tableau_id
serializer_class = ListeSerializer
def get_queryset(self):
queryset = Liste.objects.filter(tableau_id=self.kwargs["tableau_id"])
return queryset
def get_object(self):
sub_id = self.kwargs['sub_id']
# replace subobjects to your `related_name`(reverse) name
obj = get_object_or_404(Liste.objects.get(tableau_id=self.kwargs["tableau_id"]).subobjects.all(), id = sub_id)
return obj
EDIT
override the get_object method you can adjust the detail view behavior
I'm a beginner and develop a little REST API project with Django rest framework. There are a bunch of records in PostgreSQL database with a text field and I have some lists of keywords. I'm trying to filter data which contain words from one or some my lists of keywords in this text field.
Can you advise me another way around to organize filtering in DRF by using a whole list of keywords at once without entering them in a form?
I'm trying to do it with django_filters
Here if filter class:
# filter
class DataFilter(django_filters.rest_framework.FilterSet):
keyword = CharFilter(field_name='description', lookup_expr='icontains')
class Meta:
model = Data
fields = ('keyword', )
Here if view class:
# view
class DataList(generics.ListAPIView):
def get_queryset(self):
return Data.objects.filter(deadline__gte=date.today())
serializer_class = DataSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = DataFilter
But in this case, it filters only by one word which I enter in the form.
I think you can do it like this:
First, create a new filter set subclassing from BaseInFilter and CharFilter:
class CharInFilter(django_filters.BaseInFilter, django_filters.CharFilter):
pass
Then, update your FilterSet class like this:
class DataFilter(django_filters.FilterSet):
keyword__in = CharInFilter(field_name='keyword', lookup_expr='in')
class Meta:
model = Data
fields = []
Then you can use this FilterSet(same as your current implementation) like this:
class DataList(generics.ListAPIView):
def get_queryset(self):
return Data.objects.filter(deadline__gte=date.today())
serializer_class = DataSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = DataFilter
While using this filterset in DRF template, you need to input your values in comma separated format, like this:
In my case, I need to filter by multiple keywords and after to exclude from the filtered qs another set of keywords.
Something like base_url/?kwords=kw1,kw2,kw3&exclude=e_kw1,e_kw2&others....
Inheriting from filters.BaseInFilter did not work for me... it is strange since BaseInFilter inherits from BaseCSVFilter, or I made a mistake.
My solution:
from django.db.models import Q
from django_filters import rest_framework as filters
from products.models import Product
class KwordIncludeFilter(filters.BaseCSVFilter):
def filter(self, qs, values):
query = Q()
if not values:
return qs
else:
for value in values:
value = value.strip()
query |= Q(name__icontains=value)
qs = qs.filter(query)
return qs
class KwordExcludeFilter(filters.BaseCSVFilter):
def filter(self, qs, values):
query = Q()
if not values:
return qs
else:
for value in values:
value = value.strip()
query |= Q(name__icontains=value)
qs = qs.exclude(query)
return qs
class ProductFilter(filters.FilterSet):
kwords = KwordIncludeFilter()
exclude = KwordExcludeFilter()
class Meta:
model = Product
fields = {
'some_other_model_filed': ['some_lookup']
}
I am creating my first Django project . I have taken 30,000 values as input and want to show particular values according to primary key .
Code:
class employeesList(APIView):
def get(self,request):
employees1 = employees.objects.all()
with open('tracking_ids.csv') as f:
reader = csv.reader(f)
for row in reader:
_, created = employees.objects.get_or_create(
AWB=row[0],
Pickup_Pincode=row[1],
Drop_Pincode=row[2],
)
serializer = employeesSerializer(employees1 , many=True)
return Response(serializer.data)
def post(self,request):
# employees1 = employees.objects.get(id=request.employees.AWB)
employees1 = employees.objects.all()
serializer = employeesSerializer(employees1 , many=True)
return Response(serializer.data)
If I enter http://127.0.0.1:8000/employees/ in URL , I get all the values . I want the URL to be like http://127.0.0.1:8000/employees/P01001168074 and show values of P01001168074 where P01001168074 is primary ID .
I have read
1:showing the model values of specific user django
2)editing user details in python django rest framework
but they are different
Can it be done and if it can , then how ?
Presuming that you are using Django 2.0 you must configure a path than can capture your parameter as seen in the documentation
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
class A(models.Model):
x = models.CharField(max_length=250)
class MySerializer(ModelSerializer):
class Meta:
model = A
fields = ('x',)
class MyListView(APIView):
def get(self, request, *args, **kwargs):
# simply get all the objects, serialize and return the response
elems = A.objects.all()
response_data = MySerializer(elems, many=True).data
return Response(data=response_data)
class MyDetailView(APIView):
def get(self, request, *args, **kwargs):
# we obtain the parameter from the URL
desired_item = kwargs.get("desired_item", None)
# we filter the objects by it and get our instance
element = A.objects.filter(x=desired_item).first()
# serialize the instance and return the response
response_data = MySerializer(element).data
return Response(data=response_data)
# all we have to now is define the paths for the list and the detail views.
urlpatterns = [
path('employees/', MyListView.as_view()),
path('employees/<str:desired_item>', MyDetailView.as_view())
]
One good option is to use a viewset that already includes a list and detail endpoint and requires little to code for a default simple setup
views.py
from rest_framework import viewsets
class EmployeeViewSet(viewsets.ModelViewSet):
serializer_class = EmployeeSerializer
queryset = Employee.objects.all()
urls.py
from rest_framework.routers import SimpleRouter
from views import EmployeeViewSet
router = SimpleRouter()
router.register(r'employees', EmployeeViewSet, base_name='employees')
urlpatterns = router.get_urls()
You can read more about viewsets in the DRF docs
I have multiple API which historically work using id as the lookup field:
/api/organization/10
I have a frontend consuming those api.
I'm building a new interface and for some reasons, I would like to use a slug instead an id:
/api/organization/my-orga
The API is built with Django Rest Framework. Except the change of lookup field, the api behavior should stay the same.
Is there a solution to allow my API to work with both a slug and a pk ? Those two path should give them same results:
/api/organization/10
/api/organization/my-orga
Here is my API definition:
# urls.py
router = DefaultRouter()
router.register(r'organization', Organization)
urlpatterns = router.urls
#view.py
class Organization(viewsets.ModelViewSet):
queryset = OrganisationGroup.objects.all()
serializer_class = OrganizationSerializer
# serializer.py
class OrganizationSerializer(PermissionsSerializer):
class Meta:
model = Organization
Try this
from django.db.models import Q
import operator
from functools import reduce
from django.shortcuts import get_object_or_404
class MultipleFieldLookupMixin(object):
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filter = {}
for field in self.lookup_fields:
filter[field] = self.kwargs[field]
q = reduce(operator.or_, (Q(x) for x in filter.items()))
return get_object_or_404(queryset, q)
Then in View
class Organization(MultipleFieldLookupMixin, viewsets.ModelViewSet):
queryset = OrganisationGroup.objects.all()
serializer_class = OrganizationSerializer
lookup_fields = ('pk', 'another field')
I solved the similar problem by overriding retrieve method and check pk field's value against any pattern. For example if it consists of only numbers.
def retrieve(self, request, *args, **kwargs):
if kwargs['pk'].isdigit():
return super(Organization, self).retrieve(request, *args, **kwargs)
else:
# get and return object however you want here.
I know you asked this question quite a time ago, but here is the complete solution i got from all answers, considering both views and urls:
Put this in your views.py: (With a little edit from drf)
class MultipleFieldLookupMixin(object):
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.lookup_fields:
if self.kwargs.get(field, None):
filter[field] = self.kwargs[field]
obj = get_object_or_404(queryset, **filter) # Lookup the object
self.check_object_permissions(self.request, obj)
return obj
Then inherit your view from this Mixin and add fields you want to lookup_fields. Like this:
class YourDetailView(MultipleFieldLookupMixin, RetrieveUpdateAPIView):
...
lookup_fields = ['pk', 'slug','code']
And in urls.py:
re_path(r'^organization/(?P<pk>[0-9]+)/$',
YourDetailView),
re_path(r'^organization/(?P<slug>[-a-zA-Z0-9_]+)/$',
YourDetailView),
re_path(r'^organization/sth_else/(?P<code>[0-9]+)/$',
YourDetailView),
class MultipleFieldLookupMixin(object):
"""
Apply this mixin to any view or viewset to get multiple field filtering
based on a `lookup_fields` attribute, instead of the default single field filtering.
"""
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.lookup_fields:
if self.kwargs[field]: # Ignore empty fields.
filter[field] = self.kwargs[field]
return get_object_or_404(queryset, **filter) # Lookup the object
class RetrieveUserView(MultipleFieldLookupMixin, generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
lookup_fields = ('account', 'username')
I think best way is to override the get_object(self) method
class Organization(generics.RetrieveAPIView):
serializer_class = OrganizationSerializer
queryset = Organization.objects.all()
multiple_lookup_fields = ['pk', 'slug']
def get_object(self):
queryset = self.get_queryset()
filter = {}
for field in self.multiple_lookup_fields:
filter[field] = self.kwargs[field]
obj = get_object_or_404(queryset, **filter)
self.check_object_permissions(self.request, obj)
return obj
There are a lot of answers here already, but none provide a full description including the mixin, view, and url configuration. This answer does.
This is the mixin that works best, it is slightly modified from https://www.django-rest-framework.org/api-guide/generic-views/#creating-custom-mixins to not error out on non-existing fields.
class MultipleFieldLookupMixin:
"""
Apply this mixin to any view or viewset to get multiple field filtering
based on a `lookup_fields` attribute, instead of the default single field filtering.
Source: https://www.django-rest-framework.org/api-guide/generic-views/#creating-custom-mixins
Modified to not error out for not providing all fields in the url.
"""
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filter = {}
for field in self.lookup_fields:
if self.kwargs.get(field): # Ignore empty fields.
filter[field] = self.kwargs[field]
obj = get_object_or_404(queryset, **filter) # Lookup the object
self.check_object_permissions(self.request, obj)
return obj
Now add the view as follows, it is important to have the Mixin first, otherwise the get_object method is not overwritten:
class RudAPIView(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView):
...
lookup_fields = ['pk', 'other_field']
Now, for the urls, we use default converters. It is important int comes first as that one will actually check if it is an int, and if not fallback to str. If you have more complex fields, you need to resort to regex.
path('efficiency/<int:pk>/', views.RudAPIView.as_view(), name='something-rud'),
path('efficiency/<string:other_field>/', views.RudAPIView.as_view(), name='something-rud'),
I think the fundamental answer is that this would not be good REST/API design and just isn't something DRF would enable.
The official docs have an example for this at https://www.django-rest-framework.org/api-guide/generic-views/#creating-custom-mixins
Also, you need to modify the urls.py adding a new route for the same view, but with the new field name.
If you still would like to use Viewsets without breaking it apart, here you go.
(Test passed on my end)
import operator
from functools import reduce
from django.db.models import Q
from django.shortcuts import get_object_or_404
class MultipleFieldLookupMixin(object):
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filters = {}
pk_fields = ["pk", "id"]
for field in self.lookup_fields:
identifier = self.kwargs[self.lookup_field]
if (field in pk_fields and identifier.isdigit()) or field not in pk_fields:
filters[field] = self.kwargs[self.lookup_field]
q = reduce(operator.or_, (Q(x) for x in filters.items()))
obj = get_object_or_404(queryset, q)
self.check_object_permissions(self.request, obj)
return obj
This is my latest version that supports primary key fields that not necessary are strings, I think is more resilient.
import operator
from functools import reduce
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.core.exceptions import ValidationError
class MultipleFieldLookupMixin:
"""
Apply this mixin to any view or viewset to get multiple field filtering
based on a `lookup_fields` attribute, instead of the default single field filtering.
"""
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filters = {}
for field in self.lookup_fields:
try:
# Validate the data type we got is a valid data type for the field we are setting
self.get_serializer_class().Meta.model._meta.get_field(field).to_python(
self.kwargs[self.lookup_field]
)
filters[field] = self.kwargs[self.lookup_field]
except ValidationError:
continue
query = reduce(operator.or_, (Q(x) for x in filters.items()))
obj = get_object_or_404(queryset, query)
self.check_object_permissions(self.request, obj)
return obj
In my project I use django rest framework. To filter the results I use django_filters backend.
There is my code:
models.py
from django.db import models
class Region(models.Model):
name = models.CharField(max_length=100, blank=True, null=False)
class Town(models.Model):
region = models.ForeignKey(Region)
name = models.CharField(max_length=100, blank=True, null=False')
filters.py
import django_filters
from models import Town
class TownFilter(django_filters.FilterSet):
region = django_filters.CharFilter(name="region__name", lookup_type="contains")
town = django_filters.CharFilter(name="name", lookup_type="contains")
class Meta:
model = Town
fields = ['region', 'town']
views.py
from models import Town
from rest_framework import generics
from serializers import TownSerializer
from filters import TownFilter
class TownList(generics.ListAPIView):
queryset = Town.objects.all()
serializer_class = TownSerializer
filter_class = TownFilter
So, I can write ?region=Region_name&town=Town_name to the end of the request url, and the result will be filtered.
But I want to use only one get param in the request url, which can have region or town name as value. For example ?search=Region_name and ?search=Town_name. How can I do this?
There are a few options, but the easiest way is to just override 'get_queryset' in your API view.
Example from the docs adapted to your use case:
class TownList(generics.ListAPIView):
queryset = Town.objects.all()
serializer_class = TownSerializer
filter_class = TownFilter(generics.ListAPIView)
serializer_class = PurchaseSerializer
def get_queryset(self):
queryset = Town.objects.all()
search_param = self.request.QUERY_PARAMS.get('search', None)
if search_param is not None:
"""
set queryset here or use your TownFilter
"""
return queryset
Another way is to set your search_fields on the list api view class in combination use the SearchFilter class. The problem is that if you're filtering over multiple models, you may have to do some additional implementation here to make sure it's looking at exactly what you want. If you're not doing anything fancy, just put double underscores for region for example: region__name
With dj-rest-filters, you can write your filters with similar syntax as of serializer. For your case, it will be like this
from djfilters import filters
class MyFilter(filters.Filter):
search = filters.CharField()
def filter_search(self, qs, value):
qs = qs.filter(#Your filter logic here)
return qs