Django Rest Framework: Multiple branches of API w/ varying queryset/permissions

Viewed 153

In my Django-Rest-Framework application, I have the need to divide the API into two branches such as:

  • /api/public/...
  • /api/private/...

Consider a model Analysis like so:

from django.db import models
from django.conf import settings

class Analysis(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    public = models.BooleanField(default=True)

What I want then is:

  • /api/public/analysis => filter queryset with Q(public=True)
  • /api/private/analysis => filter queryset with Q(user=request.user) | Q(public=False)

Similar pattern is required in 5+ models.

I have thought of 2 half-baked solutions:

1. AbstractViewSet sub-classed into PublicViewSet and PrivateViewSet

class AbstractAnalysisViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Analysis.objects.all()
    serializer_class = AnalysisSerializer
    ordering = [
        "-created_at",
    ]
    permission_classes = [IsAuthenticated,]

class PrivateAnalysisViewSet(AbstractAnalysisViewSet):
    def get_permissions(self):
        permissions = super(PrivateAnalysisViewSet, self).get_permissions()
        if self.action in ["retrieve"]:
            permissions.append(HasObjectPermission())
        return permissions

    def get_queryset(self):
        queryset = super(PrivateAnalysisViewSet, self).get_queryset()
        if self.action in ["list"]:
            auth_user = self.request.user
            query = Q(user=auth_user) | Q(public=False)
            queryset = queryset.filter(query)
        return queryset

class PublicAnalysisViewSet(AbstractAnalysisViewSet):
    def get_permissions(self):
        permissions = super(PublicAnalysisViewSet, self).get_permissions()
        if self.action in ["retrieve"]:
            permissions.append(IsPublicObjectPermission())
        return permissions

    def get_queryset(self):
        queryset = super(AnalysisViewSet, self).get_queryset()
        if self.action in ["list"]:
            query = Q(public=True)
            queryset = queryset.filter(query)
        return queryset

This would work but I don't really want to duplicate views into multiple classes like that because then I also need to register multiple viewsets with the DRF router. As I said, I have to apply this same pattern in 5 models and that means 5*3=15 viewset classes which is hard to maintain over time and the number increases fast as my application grows in size.

2. Determine private or public by parsing the self.request.path

class AnalysisViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Analysis.objects.all()
    serializer_class = AnalysisSerializer
    ordering = [
        "-created_at",
    ]
    permission_classes = [IsAuthenticated,]

    def get_permissions(self):
        permissions = super(PublicAnalysisViewSet, self).get_permissions()
        if self.action in ["retrieve"]:
            if "/private/" in self.request.path:
                permissions.append(HasObjectPermission())
            else:
                permissions.append(IsPublicObjectPermission())
        return permissions

I do like this approach but I can't figure out how to register this viewset with the DRF router such that it allows prefixing of /public/ or /private/.

Any other cool ideas to solve this are welcome and much appreciated.

1 Answers

I figured out how to register the path with the DRF router:

from django.urls import re_path, include
from rest_framework import routers
from .views import AnalysisViewSet

router = routers.DefaultRouter(trailing_slash=False)
router.register(r"analysis", AnalysisViewSet)

urlpatterns = [
    re_path(r"(public|private)/", include(router.urls)),
]

EDIT: For more verbosity and strictness:

urls.py

re_path(r"(?P<private_or_public_prefix>public|private)/", include(router.urls)),

views.py

    def get_permissions(self):
        permissions = super(PublicAnalysisViewSet, self).get_permissions()
        if self.action in ["retrieve"]:
            matcher = self.request.resolver_match
            if matcher.kwargs["private_or_public_prefix"] == "private":
                permissions.append(HasObjectPermission())
            else:
                permissions.append(IsPublicObjectPermission())
        return permissions

Related