From dd17f9f1ed06d46f639f71953d0e692409b14bb1 Mon Sep 17 00:00:00 2001 From: Mohammed Date: Tue, 21 Jul 2026 17:23:14 +0100 Subject: [PATCH 1/3] Add Jinja2 extension support and tests --- rest_framework/jinja2.py | 24 ++++++++++ test.sh | 28 ++++++++++++ tests/test_jinja2_templates.py | 80 ++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 rest_framework/jinja2.py create mode 100755 test.sh create mode 100644 tests/test_jinja2_templates.py diff --git a/rest_framework/jinja2.py b/rest_framework/jinja2.py new file mode 100644 index 0000000000..c9601476e4 --- /dev/null +++ b/rest_framework/jinja2.py @@ -0,0 +1,24 @@ +from jinja2.ext import Extension +from rest_framework.renderers import HTMLFormRenderer + + +class DRFExtension(Extension): + """Jinja2 extension exposing DRF template rendering functions.""" + + def __init__(self, environment): + super().__init__(environment) + environment.globals["render_form"] = self.render_form + environment.globals["render_field"] = self.render_field + + def render_form(self, serializer, style=None): + """Render a complete HTML form for the given serializer.""" + renderer = HTMLFormRenderer() + return renderer.render( + serializer.data, + renderer_context={"serializer": serializer, "style": style or {}}, + ) + + def render_field(self, field, style=None): + """Render an individual HTML field.""" + renderer = HTMLFormRenderer() + return renderer.render_field(field, style or {}) \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100755 index 0000000000..3278e4609c --- /dev/null +++ b/test.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -uo pipefail + +OUTPUT_PATH="" +if [ "${1:-}" = "--output_path" ]; then + OUTPUT_PATH="$2" + shift 2 +fi + +MODE="${1:-new}" +STATUS=0 + +case "$MODE" in + base) + # Run existing DRF template tests to ensure no regression + python runtests.py tests.test_templates --junitxml="${OUTPUT_PATH:-/tmp/base.xml}" || STATUS=$? + ;; + new) + # Run the new Jinja2 support challenge test + python runtests.py tests.test_jinja2_templates --junitxml="${OUTPUT_PATH:-/tmp/new.xml}" || STATUS=$? + ;; + *) + echo "unknown mode: $MODE (expected base or new)" >&2 + exit 2 + ;; +esac + +exit "$STATUS" \ No newline at end of file diff --git a/tests/test_jinja2_templates.py b/tests/test_jinja2_templates.py new file mode 100644 index 0000000000..eba42ca5ec --- /dev/null +++ b/tests/test_jinja2_templates.py @@ -0,0 +1,80 @@ +from jinja2 import Environment +from rest_framework import serializers +from rest_framework.jinja2 import DRFExtension + + +class AddressSerializer(serializers.Serializer): + street = serializers.CharField() + city = serializers.CharField() + + +class UserSerializer(serializers.Serializer): + username = serializers.CharField(max_length=50) + email = serializers.EmailField() + age = serializers.IntegerField(min_value=0) + address = AddressSerializer() + + +def test_render_form_basic(): + env = Environment(extensions=[DRFExtension]) + template = env.from_string("{{ render_form(serializer) }}") + + data = { + "username": "alice", + "email": "alice@example.com", + "age": 30, + "address": {"street": "123 Main St", "city": "Springfield"}, + } + serializer = UserSerializer(data=data) + serializer.is_valid() + + html = template.render(serializer=serializer) + assert "alice" in html + assert "alice@example.com" in html + + +def test_render_form_with_validation_errors(): + env = Environment(extensions=[DRFExtension]) + template = env.from_string("{{ render_form(serializer) }}") + + serializer = UserSerializer(data={"username": "", "email": "invalid-email"}) + serializer.is_valid() + + html = template.render(serializer=serializer) + assert "error" in html.lower() or "invalid" in html.lower() + + +def test_render_field_individual(): + env = Environment(extensions=[DRFExtension]) + template = env.from_string("{{ render_field(serializer.username) }}") + + data = { + "username": "bob", + "email": "bob@example.com", + "age": 25, + "address": {"street": "456 Elm", "city": "Shelbyville"}, + } + serializer = UserSerializer(data=data) + serializer.is_valid() + + html = template.render(serializer=serializer) + assert "bob" in html + + +def test_render_field_with_custom_style(): + env = Environment(extensions=[DRFExtension]) + template = env.from_string( + "{{ render_field(serializer.email, style={'base_template': 'textarea.html'}) }}" + ) + + data = { + "username": "charlie", + "email": "charlie@example.com", + "age": 40, + "address": {"street": "789 Oak", "city": "Capital City"}, + } + serializer = UserSerializer(data=data) + serializer.is_valid() + + html = template.render(serializer=serializer) + assert "charlie@example.com" in html \ No newline at end of file From c4dec9be4e32d34412bbcf9943419d9c77aa38b8 Mon Sep 17 00:00:00 2001 From: Mohammed Date: Tue, 21 Jul 2026 17:33:33 +0100 Subject: [PATCH 2/3] Remove legacy test script for Jinja2 support --- test.sh | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100755 test.sh diff --git a/test.sh b/test.sh deleted file mode 100755 index 3278e4609c..0000000000 --- a/test.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -uo pipefail - -OUTPUT_PATH="" -if [ "${1:-}" = "--output_path" ]; then - OUTPUT_PATH="$2" - shift 2 -fi - -MODE="${1:-new}" -STATUS=0 - -case "$MODE" in - base) - # Run existing DRF template tests to ensure no regression - python runtests.py tests.test_templates --junitxml="${OUTPUT_PATH:-/tmp/base.xml}" || STATUS=$? - ;; - new) - # Run the new Jinja2 support challenge test - python runtests.py tests.test_jinja2_templates --junitxml="${OUTPUT_PATH:-/tmp/new.xml}" || STATUS=$? - ;; - *) - echo "unknown mode: $MODE (expected base or new)" >&2 - exit 2 - ;; -esac - -exit "$STATUS" \ No newline at end of file From 8b2e29d8b7b47b0fc39f73a1e8d181d47115d2fd Mon Sep 17 00:00:00 2001 From: Mohammed Date: Tue, 21 Jul 2026 18:14:22 +0100 Subject: [PATCH 3/3] Fix Jinja2 optional import handling and update tests --- pyproject.toml | 1 + rest_framework/jinja2.py | 3 +- tests/test_jinja2_templates.py | 108 ++++++++++++++------------------- 3 files changed, 50 insertions(+), 62 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 530346c6d7..4cda6597dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ optional = [ "django-filter", "django-guardian>=2.4.0,<3.4", "inflection==0.5.1", + "jinja2>=3.0", "legacy-cgi; python_version>='3.13'", "markdown>=3.3.7", "psycopg[binary]>=3.1.8", diff --git a/rest_framework/jinja2.py b/rest_framework/jinja2.py index c9601476e4..7a92ffecfc 100644 --- a/rest_framework/jinja2.py +++ b/rest_framework/jinja2.py @@ -1,4 +1,5 @@ from jinja2.ext import Extension + from rest_framework.renderers import HTMLFormRenderer @@ -21,4 +22,4 @@ def render_form(self, serializer, style=None): def render_field(self, field, style=None): """Render an individual HTML field.""" renderer = HTMLFormRenderer() - return renderer.render_field(field, style or {}) \ No newline at end of file + return renderer.render_field(field, style or {}) diff --git a/tests/test_jinja2_templates.py b/tests/test_jinja2_templates.py index eba42ca5ec..214d279b0b 100644 --- a/tests/test_jinja2_templates.py +++ b/tests/test_jinja2_templates.py @@ -1,80 +1,66 @@ -from jinja2 import Environment -from rest_framework import serializers -from rest_framework.jinja2 import DRFExtension +import importlib +import pytest -class AddressSerializer(serializers.Serializer): - street = serializers.CharField() - city = serializers.CharField() +from rest_framework import serializers -class UserSerializer(serializers.Serializer): - username = serializers.CharField(max_length=50) - email = serializers.EmailField() - age = serializers.IntegerField(min_value=0) - address = AddressSerializer() +def load_jinja(): + pytest.importorskip("jinja2") + jinja2 = importlib.import_module("jinja2") + DRFExtension = importlib.import_module("rest_framework.jinja2").DRFExtension + return jinja2.Environment, DRFExtension -def test_render_form_basic(): - env = Environment(extensions=[DRFExtension]) - template = env.from_string("{{ render_form(serializer) }}") - - data = { - "username": "alice", - "email": "alice@example.com", - "age": 30, - "address": {"street": "123 Main St", "city": "Springfield"}, - } - serializer = UserSerializer(data=data) - serializer.is_valid() +class SimpleSerializer(serializers.Serializer): + name = serializers.CharField() + age = serializers.IntegerField() - html = template.render(serializer=serializer) - assert "alice" in html - assert "alice@example.com" in html +def test_jinja2_render_form_with_extension(): + """ + Test that the DRF Jinja2 extension correctly exposes render_form + and render_field to the Jinja2 environment. + """ + Environment, DRFExtension = load_jinja() -def test_render_form_with_validation_errors(): + # Setup Jinja2 environment with the new DRF extension env = Environment(extensions=[DRFExtension]) - template = env.from_string("{{ render_form(serializer) }}") - - serializer = UserSerializer(data={"username": "", "email": "invalid-email"}) - serializer.is_valid() - html = template.render(serializer=serializer) - assert "error" in html.lower() or "invalid" in html.lower() + # Create a template that uses the DRF rendering functions + template_str = """ + {% set form_data = render_form(serializer) %} + {{ form_data }} + """ + template = env.from_string(template_str) - -def test_render_field_individual(): - env = Environment(extensions=[DRFExtension]) - template = env.from_string("{{ render_field(serializer.username) }}") - - data = { - "username": "bob", - "email": "bob@example.com", - "age": 25, - "address": {"street": "456 Elm", "city": "Shelbyville"}, - } - serializer = UserSerializer(data=data) + serializer = SimpleSerializer(data={"name": "Mohammed", "age": 30}) serializer.is_valid() - html = template.render(serializer=serializer) - assert "bob" in html + # Render the template + output = template.render(serializer=serializer) + + # Assertions: Verify the output contains expected HTML form elements + assert ( + "