Typorithm · medium

Routers distinguish collection and detail actions

pythondjangodrfindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Routers distinguish collection and detail actions

0%

Concept and contract

Register a scoped read-only viewset and test its generated routes.

A viewset groups related operations, and a router connects them to URLs and route names. This can reduce repeated URL configuration, but automatic routes still need a clear method and permission policy. A read-only resource should not inherit write operations accidentally.

The fixture uses ReadOnlyModelViewSet with an alice-scoped queryset. A collection action returns the countries observed in that owned selection. DefaultRouter registers the viewset with an explicit basename, allowing list, detail and action routes to be exercised through Django routing.

Mechanism and worked example

The action has detail=False, so it is registered on the collection rather than under an object identifier.

The action has detail=False, so it is registered on the collection rather than under an object identifier. The router also supplies the standard list and retrieve paths. APIClient passes through the real owned URL configuration, while override_settings and clear_url_caches keep each inspection isolated.

The country list is derived from the same get_queryset policy rather than querying every owner independently. A summary action can leak information even when ordinary detail reads are safe, so custom endpoints need the same review as generated ones.

Complete worked example

python

from drf_lab import setup
setup()

from django.test import override_settings
from django.urls import clear_url_caches
from rest_framework import serializers, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.routers import DefaultRouter
from rest_framework.test import APIClient
from drf_lab import reset_fixture
from drf_lab.models import Role

class PublicRole(serializers.ModelSerializer):
    class Meta:
        model = Role
        fields = ["id", "title", "country"]
class Roles(viewsets.ReadOnlyModelViewSet):
    serializer_class = PublicRole
    def get_queryset(self):
        return Role.objects.filter(owner__username="alice").order_by("id")
    @action(detail=False, methods=["get"])
    def countries(self, request):
        return Response({"countries": sorted(set(self.get_queryset().values_list("country", flat=True)))})

router = DefaultRouter()
router.register("roles", Roles, basename="owned-role")
urlpatterns = router.urls

def inspect_example(path):
    reset_fixture()
    with override_settings(ROOT_URLCONF=__name__):
        clear_url_caches()
        response = APIClient().get(path)
        body = {"error": response.data["detail"].code} if response.status_code >= 400 else response.json()
    clear_url_caches()
    return {"status": response.status_code, "body": body}
Reasoning
The action has detail=False, so it is registered on the collection rather than under an object identifier. The router also supplies the standard list and retrieve paths. APIClient passes through the real owned URL configuration, while override_settings and clear_url_caches keep each inspection isolated. The country list is derived from the same get_queryset policy rather than querying every owner independently. A summary action can leak information even when ordinary detail reads are safe, so custom endpoints need the same review as generated ones.

Focused decision: context is in the complete example

python

@action(detail=False, methods=["get"])
def countries(self, request):
    selected = self.get_queryset().values_list("country", flat=True)
    return Response({"countries": sorted(set(selected))})

router.register("roles", Roles, basename="owned-role")
Focused decision
Reconstruct this focused decision after the warm-up. The complete imports, model or application, and test client remain in the worked example. Typing records practice; only the downloadable computer lab executes Python.

Counterexamples and common mistakes

The counterexample marks the countries action as detail=True.

The counterexample marks the countries action as detail=True. The previous collection URL then no longer selects that action. The router may treat its text as a detail identifier and return not found. An action method existing in Python does not prove the intended route was registered.

Be careful when overriding get_permissions or serializer selection based on self.action: action availability depends on the dispatch stage. Review extra action permissions, public schema and supported methods when changing a viewset. Package fixes do not remove the need for explicit route tests.

Controlled mistake: observe the actual wrong behavior

python

from drf_lab import setup
setup()

from django.test import override_settings
from django.urls import clear_url_caches
from rest_framework import serializers, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.routers import DefaultRouter
from rest_framework.test import APIClient
from drf_lab import reset_fixture
from drf_lab.models import Role

class PublicRole(serializers.ModelSerializer):
    class Meta:
        model = Role
        fields = ["id", "title", "country"]
class Roles(viewsets.ReadOnlyModelViewSet):
    serializer_class = PublicRole
    def get_queryset(self):
        return Role.objects.filter(owner__username="alice").order_by("id")
    @action(detail=True, methods=["get"])
    def countries(self, request):
        return Response({"countries": sorted(set(self.get_queryset().values_list("country", flat=True)))})

router = DefaultRouter()
router.register("roles", Roles, basename="owned-role")
urlpatterns = router.urls

def inspect_example(path):
    reset_fixture()
    with override_settings(ROOT_URLCONF=__name__):
        clear_url_caches()
        response = APIClient().get(path)
        body = {"error": response.data["detail"].code} if response.status_code >= 400 else response.json()
    clear_url_caches()
    return {"status": response.status_code, "body": body}
Reasoning
The counterexample marks the countries action as detail=True. The previous collection URL then no longer selects that action. The router may treat its text as a detail identifier and return not found. An action method existing in Python does not prove the intended route was registered. Be careful when overriding get_permissions or serializer selection based on self.action: action availability depends on the dispatch stage. Review extra action permissions, public schema and supported methods when changing a viewset. Package fixes do not remove the need for explicit route tests.

Independent practice

Rename the countries collection action URL to markets with url_path, while preserving its Python method and scope.

Rename the countries collection action URL to markets with url_path, while preserving its Python method and scope. Keep the ordinary detail route unchanged and let the former countries URL stop acting as that collection operation.

Test /roles/markets/, a known detail and the former collection name. Do not add a silent alias unless the real compatibility policy requires one. The small task focuses on the difference between Python method naming and the public URL chosen by the router.

Step 1: Expose the collection action at markets while preserving detail reads and deliberate old-path behavior. Step 2: Retain the working imports and application structure. Change the stated policy, run the supplied practice cases, then explain the newly accepted or rejected boundary.

Practice starter: solve before reading the solution

python

from drf_lab import setup
setup()

from django.test import override_settings
from django.urls import clear_url_caches
from rest_framework import serializers, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.routers import DefaultRouter
from rest_framework.test import APIClient
from drf_lab import reset_fixture
from drf_lab.models import Role

class PublicRole(serializers.ModelSerializer):
    class Meta:
        model = Role
        fields = ["id", "title", "country"]
class Roles(viewsets.ReadOnlyModelViewSet):
    serializer_class = PublicRole
    def get_queryset(self):
        return Role.objects.filter(owner__username="alice").order_by("id")
    @action(detail=False, methods=["get"])
    def countries(self, request):
        return Response({"countries": sorted(set(self.get_queryset().values_list("country", flat=True)))})

router = DefaultRouter()
router.register("roles", Roles, basename="owned-role")
urlpatterns = router.urls

def inspect_example(path):
    reset_fixture()
    with override_settings(ROOT_URLCONF=__name__):
        clear_url_caches()
        response = APIClient().get(path)
        body = {"error": response.data["detail"].code} if response.status_code >= 400 else response.json()
    clear_url_caches()
    return {"status": response.status_code, "body": body}

# Independent task: Expose the collection action at markets while preserving detail reads and deliberate old-path behavior.
Reasoning
Expose the collection action at markets while preserving detail reads and deliberate old-path behavior.
  • Hint 1: Find the one field or branch that owns this policy.
  • Hint 2: Compare the first changed acceptance case with the retained baseline.

Solution, tests and trade-offs

The solution adds url_path="markets" to the action declaration.

The solution adds url_path="markets" to the action declaration. The collection result stays the same, generated detail handling remains intact and the old path no longer identifies the custom operation. The test compares real routed responses rather than calling countries directly.

In production, changing an established route can affect clients, caches and links. Choose aliases or versioning deliberately when needed. A basename controls generated naming; it does not implement ownership, authentication or an API version by itself.

Practice solution and acceptance checks

python

from drf_lab import setup
setup()

from django.test import override_settings
from django.urls import clear_url_caches
from rest_framework import serializers, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.routers import DefaultRouter
from rest_framework.test import APIClient
from drf_lab import reset_fixture
from drf_lab.models import Role

class PublicRole(serializers.ModelSerializer):
    class Meta:
        model = Role
        fields = ["id", "title", "country"]
class Roles(viewsets.ReadOnlyModelViewSet):
    serializer_class = PublicRole
    def get_queryset(self):
        return Role.objects.filter(owner__username="alice").order_by("id")
    @action(detail=False, methods=["get"], url_path="markets")
    def countries(self, request):
        return Response({"countries": sorted(set(self.get_queryset().values_list("country", flat=True)))})

router = DefaultRouter()
router.register("roles", Roles, basename="owned-role")
urlpatterns = router.urls

def inspect_example(path):
    reset_fixture()
    with override_settings(ROOT_URLCONF=__name__):
        clear_url_caches()
        response = APIClient().get(path)
        body = {"error": response.data["detail"].code} if response.status_code >= 400 else response.json()
    clear_url_caches()
    return {"status": response.status_code, "body": body}
Reasoning
The solution adds url_path="markets" to the action declaration. The collection result stays the same, generated detail handling remains intact and the old path no longer identifies the custom operation. The test compares real routed responses rather than calling countries directly. In production, changing an established route can affect clients, caches and links. Choose aliases or versioning deliberately when needed. A basename controls generated naming; it does not implement ownership, authentication or an API version by itself.

Interview reasoning and next step

Explain detail=False using the actual registered URL, then contrast it with a detail action requiring an object lookup.

Explain detail=False using the actual registered URL, then contrast it with a detail action requiring an object lookup. Identify why calling the method directly would miss the controlled routing mistake.

Next, authenticate real requests with an owned token table. Router registration selects an operation, but the caller still needs a verified principal before protected data is returned.

Optional stretch — after the core task works: Add one independently reasoned boundary case and explain its expected value before running it.

Computer practice: https://applydjinn.com/typorithm/drf-lab-20261105.zip. Install pinned dependencies in an isolated Python environment. Run verify.py, then change the working exercise baseline and run --practice drf-viewsets. The real DRF clients, serializers, schemas and SQLite transactions use owned fixtures only. No production database, feed, account or paid service is used. This lab executes Python; the PC typing trainer and native reading progress do not execute it.

Register a scoped read-only viewset and test its generated routes.