Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,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",
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"