ProvenPin Install

DRF ModelViewSet with django-filter range, exact and ordering filters: working versions (tested 2026-10-15)

A ModelViewSet with a django-filter FilterSet for price ranges, exact name and ordering, plus page-number pagination, configured in code and tested offline with APIClient on in-memory SQLite.

passed5 passed · 0 failed2026-10-15 13:28 UTC
Status
passed
Last passed
2026-10-15 13:28 UTC
Tests
5 / 5
Language
Python
Runtime
Python 3.13.16
pinned versions5
django@6.1.2djangorestframework@3.18.3django-filter@26.2.0pytest-django@4.14.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38742087302

The code

Exactly the files that passed. Your agent can pull them with get_recipe("drf-django-filter-viewset") (install), or see them on GitHub.

requirements.txt
django==6.1.2
djangorestframework==3.18.3
django-filter==26.2.0
pytest-django==4.14.0
pytest==9.1.1
pytest.ini
[pytest]
pythonpath = .
conftest.py
import django
from django.conf import settings

# Settings live in code, so the suite needs no settings module on disk.
# django.setup() must run after configure() so app models can be imported.
if not settings.configured:
    settings.configure(
        DEBUG=False,
        SECRET_KEY="test-only",
        ROOT_URLCONF="shop.api",
        INSTALLED_APPS=[
            "django.contrib.contenttypes",
            "django.contrib.auth",
            "rest_framework",
            "django_filters",
            "shop",
        ],
        DATABASES={
            "default": {
                "ENGINE": "django.db.backends.sqlite3",
                "NAME": ":memory:",
            }
        },
        REST_FRAMEWORK={
            "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
            "PAGE_SIZE": 2,
        },
        USE_TZ=True,
        DEFAULT_AUTO_FIELD="django.db.models.BigAutoField",
    )
    django.setup()
shop/__init__.py
shop/models.py
from django.db import models


class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=8, decimal_places=2)
shop/api.py
import django_filters
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import serializers, viewsets
from rest_framework.routers import DefaultRouter

from shop.models import Product


class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ["id", "name", "price"]


class ProductFilter(django_filters.FilterSet):
    min_price = django_filters.NumberFilter(field_name="price", lookup_expr="gte")
    max_price = django_filters.NumberFilter(field_name="price", lookup_expr="lte")
    ordering = django_filters.OrderingFilter(fields=("price", "name"))

    class Meta:
        model = Product
        fields = ["name"]  # exact match


class ProductViewSet(viewsets.ModelViewSet):
    # An explicit order keeps pagination stable across requests.
    queryset = Product.objects.order_by("id")
    serializer_class = ProductSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_class = ProductFilter


router = DefaultRouter()
router.register("products", ProductViewSet)
urlpatterns = router.urls
tests/test_api.py
from decimal import Decimal

import pytest
from rest_framework.test import APIClient

from shop.models import Product


@pytest.fixture
def api(db):
    Product.objects.bulk_create([
        Product(name="Pen", price=Decimal("3.50")),
        Product(name="Notebook", price=Decimal("12.00")),
        Product(name="Desk", price=Decimal("150.00")),
        Product(name="Lamp", price=Decimal("25.00")),
        Product(name="Mug", price=Decimal("8.00")),
    ])
    return APIClient()


def test_range_and_exact_filters(api):
    r = api.get("/products/", {"min_price": "5", "max_price": "20"})
    assert r.status_code == 200
    assert r.data["count"] == 2
    assert {p["name"] for p in r.data["results"]} == {"Notebook", "Mug"}

    r = api.get("/products/", {"name": "Desk"})
    assert [p["name"] for p in r.data["results"]] == ["Desk"]


def test_ordering_descending_by_price(api):
    r = api.get("/products/", {"ordering": "-price"})
    assert [p["name"] for p in r.data["results"]] == ["Desk", "Lamp"]


def test_pagination_counts_filtered_queryset(api):
    # count and pages reflect the filter, not the whole table
    page1 = api.get("/products/", {"min_price": "5", "page": 1})
    page2 = api.get("/products/", {"min_price": "5", "page": 2})
    assert page1.data["count"] == 4
    assert page2.data["count"] == 4
    names = [p["name"] for p in page1.data["results"] + page2.data["results"]]
    assert len(names) == len(set(names)) == 4


def test_invalid_decimal_filter_returns_400(api):
    r = api.get("/products/", {"min_price": "cheap"})
    assert r.status_code == 400


def test_create_roundtrips_decimal_as_string(api):
    r = api.post("/products/", {"name": "Chair", "price": "42.5"}, format="json")
    assert r.status_code == 201
    # DRF renders DecimalField as a string padded to decimal_places
    assert r.data["price"] == "42.50"

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.