Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add render_block function #60

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
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
46 changes: 46 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,52 @@ There are also two additional errors that can be raised:
``UnsupportedEngine``
Raised if a template backend besides the Django backend is used.

Testing the context used by render_block
========================================

If you write tests with the test client, you typically use the test client's
``get()`` and ``post()`` methods to test your view code. The return value of
these methods is a ``Response`` object. It is not the same as the
``HttpResponse`` object returned by your view. It has some additional data,
such as the context that was used while rendering a template.

The function ``render_block()`` returns a ``BlockOfTemplateResponse`` object,
which has been prepared to make the context available to the response in
tests. However, its ``notify_block_render()`` method must be mocked so that
it sends a specific signal. This signal is handled by the test client to
add the context to the ``Response`` object.

One way to mock the ``notify_block_render()`` method is to use the following
setup and tear-down code in your test classes:

.. code-block:: python

from unittest.mock import patch
from django.test.signals import template_rendered

class TestYourCode(TestCase):

def setUp(self):
self.mock_method = patch(
"render_block.BlockOfTemplateResponse.notify_block_render"
).start()
self.mock_method.side_effect = lambda template, context: template_rendered.send(
sender=None, template=template, context=context
)

def tearDown(self):
self.mock_method.stop()

Assuming a view exists that uses ``render_block()`` and you want to test the context that
was passed as parameter to ``render_block()``, you can access the context in your tests,
like this:

.. code-block:: python

response = client.get(reverse("logbook:messages_overview"))
assert response.status_code == 200
assert response.context["messages"] == ["Disk is full.", "Uninstalled unused apps."]

Contributing
============

Expand Down
14 changes: 12 additions & 2 deletions render_block/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
from render_block.base import render_block_to_string
from render_block.base import (
BlockOfTemplateResponse,
render_block,
render_block_to_string,
)
from render_block.exceptions import BlockNotFound, UnsupportedEngine

__all__ = ["BlockNotFound", "UnsupportedEngine", "render_block_to_string"]
__all__ = [
"BlockNotFound",
"BlockOfTemplateResponse",
"UnsupportedEngine",
"render_block",
"render_block_to_string",
]
74 changes: 71 additions & 3 deletions render_block/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import List, Optional, Tuple, Union
from typing import Any, Dict, List, Optional, Tuple, Union

from django.http import HttpRequest
from django.http import HttpRequest, HttpResponse
from django.template import Context, loader
from django.template.backends.django import Template as DjangoTemplate
from django.template.response import TemplateResponse

try:
from django.template.backends.jinja2 import Template as Jinja2Template
Expand All @@ -27,10 +28,13 @@ def render_block_to_string(
Loads the given template_name and renders the given block with the given
dictionary as context. Returns a string.

template_name
:param template_name:
The name of the template to load and render. If it's a list of
template names, Django uses select_template() instead of
get_template() to find the template.
:param block_name: The name of the block to load.
:param context: The context dictionary used while rendering the template.
:param request: The request that triggers the rendering of the block.
"""

# Like render_to_string, template_name can be a string or a list/tuple.
Expand All @@ -55,3 +59,67 @@ def render_block_to_string(
raise UnsupportedEngine(
"Can only render blocks from the Django template backend."
)


def render_block(
request: HttpRequest,
template_name: str,
block_name: str,
context: Optional[Dict[str, Any]] = None,
status: Optional[int] = None,
) -> HttpResponse:
"""
Loads the given template_name and renders the given block with the given dictionary
as context. Returns a HttpResponseForBlock instance.

:param request: The request that triggers the rendering of the block.
:param template_name:
The name of the template to load and render. If it's a list of
template names, Django uses select_template() instead of
get_template() to find the template.
:param block_name: The name of the block to load.
:param context: The context dictionary used while rendering the template.
:param status: The status of the response.
"""
return BlockOfTemplateResponse(
request=request,
template_name=template_name,
block_name=block_name,
context=context,
status=status,
)


class BlockOfTemplateResponse(TemplateResponse):
"""
This class implements a TemplateResponse that only renders a block from the
template.
"""

def __init__(
self,
request: HttpRequest,
template_name: str,
block_name: str,
context: Optional[Dict[str, Any]] = None,
status: Optional[int] = None,
):
super().__init__(
request=request, template=template_name, context=context, status=status
)
self.block_name = block_name

@property
def rendered_content(self) -> str:
context = self.resolve_context(self.context_data)
self.notify_block_render(self.template_name, context)
return render_block_to_string(
self.template_name, self.block_name, context=context, request=self._request
)

def notify_block_render(
self,
template_name: Union[List[str], Tuple[str, ...], DjangoTemplate, str],
context: Union[Dict[str, Any], None],
) -> None:
pass
3 changes: 3 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,7 @@
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"

INSTALLED_APPS = ("tests",)

AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",)

ROOT_URLCONF = "tests.urls"
152 changes: 147 additions & 5 deletions tests/tests.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
from unittest import skip
from unittest.mock import patch

from django.template import Context
from django.test import RequestFactory, TestCase, modify_settings, override_settings
from django.test import (
Client,
RequestFactory,
TestCase,
modify_settings,
override_settings,
)
from django.test.signals import template_rendered
from django.urls import reverse

from render_block import BlockNotFound, UnsupportedEngine, render_block_to_string


class TestDjango(TestCase):
"""Test the Django templating engine."""
class TestRenderBlockToStringForDjango(TestCase):
"""Test render_block_to_string for the Django templating engine."""

def assertExceptionMessageEquals(self, exception: Exception, expected: str) -> None:
self.assertEqual(expected, exception.args[0])
Expand Down Expand Up @@ -175,6 +184,72 @@ def test_request_context(self) -> None:
self.assertEqual(result, "/dummy-url")


@modify_settings(
INSTALLED_APPS={
"prepend": [
"django.contrib.auth",
"django.contrib.contenttypes",
],
},
)
class TestRenderBlockForDjango(TestCase):
"""Test render_block for the Django templating engine."""

def setUp(self) -> None:
self.mock_method = patch(
"render_block.BlockOfTemplateResponse.notify_block_render"
).start()
self.mock_method.side_effect = lambda template, context: template_rendered.send(
sender=None, template=template, context=context
)

def tearDown(self) -> None:
self.mock_method.stop()

def assertExceptionMessageEquals(self, exception: Exception, expected: str) -> None:
self.assertEqual(expected, exception.args[0])

def test_block(self) -> None:
client = Client()
data = {
"template_name": "test5.html",
"block_name": "block2",
"foo": "bar",
}
response = client.post(reverse("block"), data=data)

result = response.content.decode("utf-8")
self.assertEqual(result, "bar")
self.assertEqual(response.context["foo"], "bar")

@override_settings(
TEMPLATES=[
{
"BACKEND": "django.template.backends.dummy.TemplateStrings",
"DIRS": ["tests/templates"],
"APP_DIRS": True,
}
]
)
def test_different_backend(self) -> None:
"""
Ensure an exception is thrown if a different backed from the Django
backend is used.
"""
client = Client()
data = {
"template_name": "test5.html",
"block_name": "block2",
"foo": "bar",
}
with self.assertRaises(UnsupportedEngine) as exc:
client.post(reverse("block"), data=data)

self.assertExceptionMessageEquals(
exc.exception, "Can only render blocks from the Django template backend."
)


@override_settings(
TEMPLATES=[
{
Expand All @@ -184,8 +259,8 @@ def test_request_context(self) -> None:
}
]
)
class TestJinja2(TestCase):
"""Test the Django templating engine."""
class TestRenderBlockToStringForJinja2(TestCase):
"""Test render_block_to_string for the Jinja2 templating engine."""

def assertExceptionMessageEquals(self, exception: Exception, expected: str) -> None:
self.assertEqual(expected, exception.args[0])
Expand Down Expand Up @@ -271,3 +346,70 @@ def test_context(self) -> None:
data = "block2 from test5"
result = render_block_to_string("test5.html", "block2", {"foo": data})
self.assertEqual(result, data)


@override_settings(
TEMPLATES=[
{
"BACKEND": "django.template.backends.jinja2.Jinja2",
"DIRS": ["tests/templates"],
"APP_DIRS": True,
}
]
)
class TestRenderBlockForJinja2(TestCase):
"""Test render_block for the Jinja2 templating engine."""

def setUp(self) -> None:
self.mock_method = patch(
"render_block.BlockOfTemplateResponse.notify_block_render"
).start()
self.mock_method.side_effect = lambda template, context: template_rendered.send(
sender=None, template=template, context=context
)

def tearDown(self) -> None:
self.mock_method.stop()

def assertExceptionMessageEquals(self, exception: Exception, expected: str) -> None:
self.assertEqual(expected, exception.args[0])

def test_block(self) -> None:
client = Client()
data = {
"template_name": "test5.html",
"block_name": "block2",
"foo": "bar",
}
response = client.post(reverse("block"), data=data)

result = response.content.decode("utf-8")
self.assertEqual(result, "bar")
self.assertEqual(response.context["foo"], "bar")

@override_settings(
TEMPLATES=[
{
"BACKEND": "django.template.backends.dummy.TemplateStrings",
"DIRS": ["tests/templates"],
"APP_DIRS": True,
}
]
)
def test_different_backend(self) -> None:
"""
Ensure an exception is thrown if a different backed from the Django
backend is used.
"""
client = Client()
data = {
"template_name": "test5.html",
"block_name": "block2",
"foo": "bar",
}
with self.assertRaises(UnsupportedEngine) as exc:
client.post(reverse("block"), data=data)

self.assertExceptionMessageEquals(
exc.exception, "Can only render blocks from the Django template backend."
)
7 changes: 7 additions & 0 deletions tests/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import path

from tests import views

urlpatterns = [
path("block", views.BlockView.as_view(), name="block"),
]
29 changes: 29 additions & 0 deletions tests/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Any, Dict, Optional

from django.http import HttpRequest, HttpResponse
from django.views import View

from render_block import render_block


class BlockView(View):
"""
This view simply calls render_block with parameters from the data of the request.
"""

def post(self, request: HttpRequest) -> HttpResponse:
context: Optional[Dict[str, Any]] = {
key: request.POST.get(key)
for key in request.POST.keys()
if key not in ("template_name", "block_name")
}
if context == {}:
context = None

return render_block(
request,
template_name=request.POST.get("template_name"),
block_name=request.POST.get("block_name"),
context=context,
status=request.POST.get("status"),
)
Loading