Skip to content
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ optional = [
"django-filter",
"django-guardian>=2.4.0,<3.6",
"inflection==0.5.1",
"jinja2>=3.0",
"legacy-cgi; python_version>='3.13'",
"markdown>=3.3.7",
"psycopg[binary]>=3.1.8",
Expand Down
27 changes: 27 additions & 0 deletions rest_framework/jinja2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from jinja2.ext import Extension
from markupsafe import Markup

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, template_pack=None):
"""Render a complete HTML form for the given serializer."""
style = {"template_pack": template_pack} if template_pack else {}
renderer = HTMLFormRenderer()
return Markup(renderer.render(serializer.data, None, {"style": style}))

def render_field(self, field, style=None):
"""Render an individual HTML field."""
renderer = (
style.get("renderer", HTMLFormRenderer()) if style else HTMLFormRenderer()
)
html = renderer.render_field(field, style or {})
return Markup(html)
121 changes: 121 additions & 0 deletions tests/test_jinja2_templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import importlib

import pytest

from rest_framework import serializers


def load_jinja():
pytest.importorskip("jinja2")
jinja2 = importlib.import_module("jinja2")
DRFExtension = importlib.import_module("rest_framework.jinja2").DRFExtension
return jinja2.Environment, DRFExtension


class SimpleSerializer(serializers.Serializer):
name = serializers.CharField()
age = serializers.IntegerField()


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()

# Setup Jinja2 environment with the new DRF extension
env = Environment(extensions=[DRFExtension])

# 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)

serializer = SimpleSerializer(data={"name": "Mohammed", "age": 30})
serializer.is_valid()

# Render the template
output = template.render(serializer=serializer)

# Assertions: Verify the output contains expected HTML form elements
assert (
"<form" in output or "name" in output.lower()
), "Expected form rendering to contain field data"
assert "Mohammed" in output, "Expected rendered form to contain the serializer data"


def test_jinja2_render_field_individual():
"""
Test that render_field works individually in Jinja2.
"""
Environment, DRFExtension = load_jinja()
env = Environment(extensions=[DRFExtension])
template_str = "{{ render_field(serializer['name']) }}"
template = env.from_string(template_str)

serializer = SimpleSerializer(data={"name": "Ahmed", "age": 25})
serializer.is_valid()

output = template.render(serializer=serializer)

assert (
"Ahmed" in output or "name" in output.lower()
), "Expected field rendering to work"


def test_jinja2_render_form_with_template_pack():
"""
Test that render_form mirrors the template-tag API in Jinja2.
"""
Environment, DRFExtension = load_jinja()
env = Environment(extensions=[DRFExtension])
template = env.from_string(
'{{ render_form(serializer, template_pack="rest_framework/vertical") }}'
)

serializer = SimpleSerializer(data={"name": "Ali", "age": 41})
serializer.is_valid()

output = template.render(serializer=serializer)

assert "Ali" in output, "Expected rendered form to contain serializer data"


def test_jinja2_render_field_with_style():
"""
Test that render_field accepts the same style mapping the template tag does.
"""
Environment, DRFExtension = load_jinja()
env = Environment(extensions=[DRFExtension])
template = env.from_string(
"{{ render_field(serializer['name'], style={'base_template': 'textarea.html'}) }}"
)

serializer = SimpleSerializer(data={"name": "Asiya", "age": 23})
serializer.is_valid()

output = template.render(serializer=serializer)

assert "Asiya" in output, "Expected rendered field to contain serializer data"


def test_jinja2_render_form_with_autoescape_preserves_html():
"""
Test that extension output is marked safe so autoescaped environments keep HTML intact.
"""
Environment, DRFExtension = load_jinja()
env = Environment(autoescape=True, extensions=[DRFExtension])
template = env.from_string("{{ render_form(serializer) }}")

serializer = SimpleSerializer(data={"name": "Hajar", "age": 28})
serializer.is_valid()

output = template.render(serializer=serializer)

assert (
'<div class="form-group' in output
), "Expected rendered form HTML to remain unescaped"
assert "Hajar" in output, "Expected rendered form to contain serializer data"
Loading