Omar Jandali
Omar Jandali

Reputation: 824

Django Rest Framework viewset - ForeignKey filtering filter based on Username Issue

I have a django project and i have integrated the Django Rest Framework into the project for the back end. I have a Profile model. In the profile model, I have a user foreignkey that has a username field. The username is what I am currently using for filtering profiles.

I had everything working perfectly when the ListAPIView and RetrieveAPIView were seperated.

class ProfileListView(ListAPIView):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

class ProfileDetailView(RetrieveAPIView):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

    def get_object(self):
        return self.queryset.get(user__username=self.kwargs.get('username')) 

Another thing is that when the views were seperated, I set a username parameter in the url that was passed into the view. I am not sure how to integrrate that into viewsets.

path('users/', UserListView.as_view()),
path('users/<username>', UserDetailView.as_view()),

this is what it I have now

router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user')
router.register(r'profiles', ProfileViewSet, basename='profile')
urlpatterns = router.urls

I am trying to change my code so that rather than each view sperate I want to use a generic view set that is a single point with all views.

When I use look at the API in my web browser, the list view works but when I try to search a specific username, I get an error.

DoesNotExist at /api/users/profiles/omarjandali/
Profile matching query does not exist.
Request Method: GET
Request URL:    http://localhost:8000/api/users/profiles/omarjandali/
Django Version: 2.1.5
Exception Type: DoesNotExist
Exception Value:    
Profile matching query does not exist.
Exception Location: /Users/omarjandali/anaconda3/envs/splittapp/lib/python3.7/site-packages/django/db/models/query.py in get, line 399
Python Executable:  /Users/omarjandali/anaconda3/envs/splittapp/bin/python
Python Version: 3.7.2
Python Path:    
['/Users/omarjandali/Documents/splittapp/backend/src',
 '/Users/omarjandali/anaconda3/envs/splittapp/lib/python37.zip',
 '/Users/omarjandali/anaconda3/envs/splittapp/lib/python3.7',
 '/Users/omarjandali/anaconda3/envs/splittapp/lib/python3.7/lib-dynload',
 '/Users/omarjandali/anaconda3/envs/splittapp/lib/python3.7/site-packages']
Server time:    Wed, 23 Jan 2019 03:21:44 +0000

Upvotes: 0

Views: 1300

Answers (1)

Ken4scholars
Ken4scholars

Reputation: 6296

The thing is that viewsets have what is called a lookup_field inherited from the GenericAPIView. It is essentially a field used to retrieve a single object in the detail endpoint of the viewset. By default, it is usually the id field so it tries to search a profile with id=omarjandali but of course that does not exist as that is a username.

So if you want to use username instead of ids for lookups, you have to specify the lookup_field as username. So you should have something like this:

class MyViewSet(vieswts.ModelViewSet):
    ...
    lookup_field = 'user__username'
    ...

Bear in mind that this means you can no longer retrieve profiles by their id.

To allow more complex lookups using more than one lookup field, you have to override the get_object() method and implement the logic there.

For this, you can use this mixin I built and use in my projects.

class AlternateLookupFieldsMixin(object):
"""
Looks up objects for detail endpoints using alternate
lookup fields assigned in `alternate_lookup_fields` apart
from the default lookup_field. Only unique fields should be used
else Http404 is raised if multiple objects are found
"""

alternate_lookup_fields = []

def get_object(self):
    try:
        return super().get_object()
    except Http404:
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
        queryset = self.filter_queryset(self.get_queryset())
        obj = None
        for lookup_field in self.alternate_lookup_fields:
            filter_kwargs = {lookup_field: self.kwargs[lookup_url_kwarg]}
            try:
                obj = get_object_or_404(queryset, **filter_kwargs)
            except Http404:
                pass
        if obj:
            self.check_object_permissions(self.request, obj)
            return obj
        raise Http404

How to use it:

Add the mixin to your viewsets and specify the extra fields to be used for lookups in the alternate_lookup_fields list. This means that you can(most likely should) allow the original lookup_field to be id and add extra lookup fields in the list.

Please bear in mind that you should only use unique fields for lookups, else you will run into issues where multiple objects are found.

So, in your case, using the mixin will look like this:

class MyViewSet(AlternateLookupFieldsMixin, vieswts.ModelViewSet):
    ...
    alternate_lookup_fields = ['user__username']
    ...

Upvotes: 2

Related Questions