Search code examples
pythondjangodjango-rest-frameworkdjango-views

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?


Solution

  • 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')