Django - multiple fields lookup - python

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

Related

Loop through queryset and annotate lookup value to records

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.

Lookup django model using two or more different keys

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))
...

django rest framework manually display 404 page

So I have typical generic view:
class FooListAPIView(generics.ListAPIView):
serializer_class = FooSerializer
lookup_fields = ('area_id', 'category_id', )
def get_queryset(self):
area = Area.objects.get(pk=self.kwargs.get('area_id'))
area_tree = area.get_tree(parent=area) #returns queryset
category = Category.objects.get(pk=self.kwargs.get('category_id'))
queryset = Foo.objects.filter(area__in=area_tree, category=category)
return queryset
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.lookup_fields:
filter[field] = self.kwargs[field]
return get_object_or_404(queryset, **filter)
My problem is, if i try get area or category objects, which doesn't exist, browser throws me error:
Area matching query does not exist.
How can I make it so, that when Area matching query does not exist, I get standard rest framework 404 response?
The problem here is that get_queryset doesn't really expect any failures. In your case, although you are returning a queryset, you seem to be hitting the database with the Area.objects.get(pk=self.kwargs.get('area_id')) call. When this fails, it violates the I/O defined by get_queryset which isn't expecting the Area.DoesNotExist exception. So it fails and you end up with a Django 500 error.
What you need to ensure is that the get_queryset method, returns a queryset, preferably without making any calls to the DB (I say preferably, since there is no such rule that says it shouldn't hit the DB, but its generally understood that get_queryset wont be the one to actually perform the DB query). Then if you must, you can freely perform any get operations on your DB inside the get_object with the get_object_or_404 shortcut. Since get_object_or_404 raises an Http404 exception and get_object knows how to handle this exception, it will gracefully return the 404 page that you are expecting.
If you can ensure your area.get_tree implementation can work with a parent queryset, instead of a parent object, then you could do something like this:
class FooListAPIView(generics.ListAPIView):
serializer_class = FooSerializer
lookup_fields = ('area_id', 'category_id', )
def get_queryset(self):
area = Area.objects.filter(pk=self.kwargs.get('area_id'))
area_tree = area.get_tree(parent=area) #returns queryset
category = Category.objects.filter(pk=self.kwargs.get('category_id'))
queryset = Foo.objects.filter(area__in=area_tree, category__in=category)
return queryset
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.lookup_fields:
filter[field] = self.kwargs[field]
return get_object_or_404(queryset, **filter)
If you are unable to get area.tree to work without a queryset, then you can delay some of your get_queryset logic to get_object. Like so:
from django.http import Http404
class FooListAPIView(generics.ListAPIView):
serializer_class = FooSerializer
lookup_fields = ('area_id', 'category_id', )
queryset = Foo.objects.all()
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
area = get_object_or_404(Area, **{'pk': self.kwargs.get('area_id')})
area_tree = area.get_tree(parent=area)
category = get_object_or_404(Category, **{'pk': self.kwargs.get('category_id')})
queryset = queryset.filter(area__in=area_tree, category=category)
filter = {}
for field in self.lookup_fields:
filter[field] = self.kwargs[field]
return get_object_or_404(queryset, **filter)

Multiple lookup_fields for django rest framework

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

Ordering by MethodSerializer

I have a serializer that has a methodfield popular such as it returns a specific float. Is there a way to order by the serializermethodfield?
Code Snippets:
serializers.py
class PostListSerializer(serializers.HyperlinkedModelSerializer):
popular = serializers.SerializerMethodField('popularity')
def popularity(self, obj):
time = datetime.datetime.now()
n = obj.created.replace(tzinfo=None)
r = time.replace(tzinfo=None)
x = r - n
a = float(str(x.total_seconds()//(60*60*24)))
view = obj.post.count()
return view / a
views.py
class PopularPostsSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostListSerializer
filter_backends = (filters.OrderingFilter,)
ordering_fields = ('popular')
Filter backends are based on changing the queryset. So they only work for fields in the database, not calculated fields.
So you I see two possibilities:
put the popularity into the database, update it when saving the Post or other data changes that the number depends on.
sort the result yourself. Just override list in the viewset, read the ordering-argument yourself and sort the results you get from calling list from your parent.

Categories

Resources