I want to lookup a value, for each record in a queryset, and add that value to the queryset.
I have the following in my view.
class InvoiceViewSet(ModelViewSet):
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer
def get_queryset(self):
user = self.request.user
customer_id = Customer.objects.only('ref').get(user_id=user.id)
queryset = Invoice.objects.filter(supplier_id=customer_id.ref)
for invoice in queryset:
program = FunderProgramMember.objects.get(supplier=invoice.supplier_id, buyer=invoice.buyer)
invoice.annotate(discount_rate=Value(program.discount_rate))
return queryset
Since the invoices in the queryset could have different discounts, I loop through the queryset and add the relevant discount.
I am receiving the following error: 'Invoice' object has no attribute 'annotate'
I can annotate to the queryset (which doesn't help me since the records in the queryset won't all have the same discount) but it seems I can't annotate to a record in the queryset.
Is there another way of achieving this?
And even if I could annotate to individual record, I am not sure whether those values would be passed with my return queryset?
EDIT:
Not sure whether this is the best way of doing it..but it seems to work:
class InvoiceViewSet(ModelViewSet):
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer
def get_queryset(self):
user = self.request.user
customer_id = Customer.objects.only('ref').get(user_id=user.id)
queryset = Invoice.objects.filter(supplier_id=customer_id.ref)
for invoice in queryset:
program = FunderProgramMember.objects.get(supplier=invoice.supplier_id, buyer=invoice.buyer)
invoice.discount_rate = program.discount_rate
return queryset
You can use Subquery expressions to support this annotation like this:
program_discount_subquery = FunderProgramMember.objects.filter(
supplier=OuterRef('supplier'), buyer=OuterRef('buyer')
).values('discount_rate')[:1]
queryset = Invoice.objects.filter(
supplier_id=customer_id.ref
).annotate(discount_rate=Subquery(program_discount_subquery))
Each invoice in the queryset will then have an attribute discount_rate.
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've an endpoint that accepts either the uuid or the phonenumber of the user
url(r'^(?P<uuid>[0-9A-Fa-f+-]+)/$', view_name.as_view()),
Now, I've a queryset that filter accordingly. Here it is.
class UserDetails(RetrieveUpdateAPIView):
serializer_class = UserSerializer
lookup_field = 'uuid'
def get_queryset(self):
"""
Over-riding queryset.
Filter user based on the user_id(Works for both msisdn & uuid).
"""
msisdn_or_uuid = self.kwargs[self.lookup_field]
queryset = Users.objects
try: # checking if the forwarded param is user_id or msisdn.
UUID(msisdn_or_uuid)
instance = queryset.filter(uuid=msisdn_or_uuid)
except ValueError:
instance = queryset.filter(msisdn=msisdn_or_uuid)
print instance # prints a queryset. But returns 404.
return instance
Now the problem is whenever phone number is passed, it returns 404 not found. But the objects clearly exist.
Is there any setting in DRF that filters on two or more fields simultaneously without over-riding the get_queryset.??
I found a related question, but couldn't make it work. Where am I going wrong?
UPDATE
This is what I've tried. It works. But would like to hear better solutions(if any)
class FilterByUuidMsisdnMixin(object):
"""
Mixin to filter by multiple lookup_fields.
Apply this mixin to any view or viewset to get multiple field (only uuid &
msisdn) filtering, instead of the default single field filtering.
"""
def get_object(self):
"""Over-riding get_object."""
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
field = self.kwargs.get(self.lookup_field)
filters = {}
try: # checking if the forwarded param is user_id or msisdn.
UUID(field)
filters['uuid'] = field # filter by uuid.
except ValueError:
filters['msisdn'] = field # filter by msisdn.
obj = get_object_or_404(queryset, **filters) # Lookup the object
self.check_object_permissions(self.request, obj) # check permissions.
return obj
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