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

Django template backend #37

Merged
merged 1 commit into from
Aug 23, 2024
Merged
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
7 changes: 6 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@ def django_env() -> None:
import django
from django.conf import settings

settings.configure(TEMPLATES=[{"BACKEND": "django.template.backends.django.DjangoTemplates"}])
settings.configure(
TEMPLATES=[
{"BACKEND": "django.template.backends.django.DjangoTemplates"},
{"BACKEND": "htpy.django.HtpyTemplateBackend", "NAME": "htpy"},
]
)
django.setup()
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## next
- Added support for passing data between components via Context. See the [Usage
docs](usage.md#passing-data-with-context) for more information. [PR #48](https://github.com/pelme/htpy/pull/48).
- Added Django template backend. The Django template backend allows you to
integrate htpy components directly with Django. [See the docs for more information](django.md#the-htpy-template-backend). [PR #37](https://github.com/pelme/htpy/pull/37).

## 24.8.1 - 2024-08-16
- Added the `comment()` function to render HTML comments.
Expand Down
43 changes: 43 additions & 0 deletions docs/django.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,46 @@ class ShoelaceInput(widgets.Widget):
def render(self, name, value, attrs=None, renderer=None):
return str(sl_input(attrs, name=name, value=value))
```

## The htpy Template Backend

htpy includes a custom template backend. It makes it possible to use htpy
instead of Django templates in places where a template name is required. This
can be used with generic views or third party applications built to be used with
Django templates.

To enable the htpy template backend, add `htpy.django.HtpyTemplateBackend` to
the `TEMPLATES` setting:

```py
TEMPLATES = [
... # Regular Django template configuration goes here
{"BACKEND": "htpy.django.HtpyTemplateBackend", "NAME": "htpy"}
]
```

In places that expect template names, such as generic views, specify the import
path as a string to a htpy component function:


```python title="pizza/views.py"
from django.views.generic import ListView
from pizza.models import Pizza


class PizzaListView(ListView):
model = Pizza
template_name = "pizza.components.pizza_list"
```

In `pizza/components.py`, create a function that accepts two arguments: the
template `Context` (a dictionary with the template variables) and a
`HttpRequest`. It should return the htpy response:

```python title="pizza/components.py"
from htpy import li, ul


def pizza_list(context, request):
return ul[(li[pizza.name] for pizza in context["object_list"])]
```
2 changes: 2 additions & 0 deletions examples/djangoproject/exampleproject/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"django.contrib.messages",
"django.contrib.staticfiles",
"index",
"pizza",
]

MIDDLEWARE = [
Expand Down Expand Up @@ -66,6 +67,7 @@
],
},
},
{"BACKEND": "htpy.django.HtpyTemplateBackend", "NAME": "htpy"},
]

WSGI_APPLICATION = "exampleproject.wsgi.application"
Expand Down
2 changes: 2 additions & 0 deletions examples/djangoproject/exampleproject/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from django.urls import path
from form.views import my_form
from index.views import index
from pizza.views import PizzaListView
from stream.views import stream
from widget.views import widget_view

Expand All @@ -27,5 +28,6 @@
path("form/", my_form),
path("widget/", widget_view),
path("stream/", stream),
path("pizza/", PizzaListView.as_view()),
path("admin/", admin.site.urls),
]
Empty file.
1 change: 1 addition & 0 deletions examples/djangoproject/pizza/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Register your models here.
6 changes: 6 additions & 0 deletions examples/djangoproject/pizza/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class TemplatebackendConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "pizza"
5 changes: 5 additions & 0 deletions examples/djangoproject/pizza/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from htpy import li, ul


def pizza_list(context, request):
return ul[(li[pizza.name] for pizza in context["object_list"])]
27 changes: 27 additions & 0 deletions examples/djangoproject/pizza/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.0.7 on 2024-08-13 20:04

from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="Pizza",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
],
),
]
Empty file.
5 changes: 5 additions & 0 deletions examples/djangoproject/pizza/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.db import models


class Pizza(models.Model):
name = models.CharField(max_length=100)
1 change: 1 addition & 0 deletions examples/djangoproject/pizza/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Create your tests here.
8 changes: 8 additions & 0 deletions examples/djangoproject/pizza/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.views.generic import ListView

from pizza.models import Pizza


class PizzaListView(ListView):
model = Pizza
template_name = "pizza.components.pizza_list"
36 changes: 36 additions & 0 deletions htpy/django.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

import typing as t

from django.template import Context, TemplateDoesNotExist
from django.utils.module_loading import import_string

from . import Element, render_node

if t.TYPE_CHECKING:
from collections.abc import Callable

from django.core.checks import Error
from django.http import HttpRequest


class _HtpyTemplate:
def __init__(self, func: Callable[[Context | None, HttpRequest | None], Element]) -> None:
self.func = func

def render(self, context: Context | None, request: HttpRequest | None) -> str:
return render_node(self.func(context, request))


class HtpyTemplateBackend:
def __init__(self, config: t.Any):
pass

def get_template(self, name: str) -> _HtpyTemplate:
try:
return _HtpyTemplate(import_string(name))
except ImportError:
raise TemplateDoesNotExist(name)

def check(self) -> list[Error]:
return []
36 changes: 34 additions & 2 deletions tests/test_django.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from typing import Any

import pytest
from django.core import management
from django.forms.utils import ErrorList
from django.template import Context, Template
from django.http import HttpRequest
from django.template import Context, Template, TemplateDoesNotExist
from django.template.loader import render_to_string
from django.utils.html import escape
from django.utils.safestring import SafeString

from htpy import div, li, ul
from htpy import Element, Node, div, li, ul

pytestmark = pytest.mark.usefixtures("django_env")

Expand All @@ -29,3 +34,30 @@ def test_explicit_escape() -> None:
def test_errorlist() -> None:
result = div[ErrorList(["my error"])]
assert str(result) == """<div><ul class="errorlist"><li>my error</li></ul></div>"""


def my_template(context: dict[str, Any], request: HttpRequest | None) -> Element:
return div[f"hey {context['name']}"]


def my_template_fragment(context: dict[str, Any], request: HttpRequest | None) -> Node:
return [div[f"hey {context['name']}"]]


class Test_template_loader:
def test_render_element(self) -> None:
result = render_to_string(__name__ + ".my_template", {"name": "andreas"})
assert result == "<div>hey andreas</div>"

def test_render_fragment(self) -> None:
result = render_to_string(__name__ + ".my_template_fragment", {"name": "andreas"})
assert result == "<div>hey andreas</div>"

def test_template_does_not_exist(self) -> None:
with pytest.raises(TemplateDoesNotExist):
render_to_string(__name__ + ".does_not_exist", {})

def test_system_checks_works(self) -> None:
# Django 5.1 requires template backends to implement a check() method.
# This test ensures that it does not crash.
management.call_command("check")