Skip to content

Commit

Permalink
Rename templates module, return Template type
Browse files Browse the repository at this point in the history
  • Loading branch information
davegaeddert committed Nov 27, 2023
1 parent b8c759f commit 856b045
Show file tree
Hide file tree
Showing 22 changed files with 197 additions and 178 deletions.
9 changes: 5 additions & 4 deletions bolt-auth/bolt/auth/forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import unicodedata

from bolt import forms, jinja
from bolt import forms
from bolt.templates import Template
from bolt.auth import authenticate, get_user_model, password_validation
from bolt.auth.models import User
from bolt.auth.tokens import default_token_generator
Expand Down Expand Up @@ -279,16 +280,16 @@ def send_mail(
"""
Send a bolt.mail.EmailMultiAlternatives to `to_email`.
"""
template = jinja.environment.from_string(subject_template_name)
template = Template(subject_template_name)
subject = template.render(context)
# Email subject *must not* contain newlines
subject = "".join(subject.splitlines())
template = jinja.environment.from_string(email_template_name)
template = Template(email_template_name)
body = template.render(context)

email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
if html_email_template_name is not None:
template = jinja.environment.from_string(html_email_template_name)
template = Template(html_email_template_name)
html_email = template.render(context)
email_message.attach_alternative(html_email, "text/html")

Expand Down
2 changes: 1 addition & 1 deletion bolt-htmx/bolt/htmx/jinja.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import jinja2
from jinja2.ext import Extension

from bolt.jinja.extensions import InclusionTagExtension
from bolt.templates.jinja.extensions import InclusionTagExtension


class HTMXJSExtension(InclusionTagExtension):
Expand Down
11 changes: 4 additions & 7 deletions bolt-htmx/bolt/htmx/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,18 @@
class HTMXViewMixin:
htmx_template_name = ""

def get_template_response(self, context=None) -> HttpResponse:
def get_template(self):
if self.is_htmx_request and self.htmx_fragment_name:
from .jinja import HTMXFragmentExtension

template = self.get_template()
if context is None:
context = self.get_context()
rendered = HTMXFragmentExtension.render_template_fragment(
template = super().get_template()
return HTMXFragmentExtension.render_template_fragment(
template=template,
fragment_name=self.htmx_fragment_name,
context=context,
)
return HttpResponse(rendered, content_type=self.content_type)

return super().get_template_response(context=context)
return super().get_template()

def get_response(self):
if self.is_htmx_request:
Expand Down
2 changes: 1 addition & 1 deletion bolt-importmap/bolt/importmap/jinja.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json

from bolt.jinja.extensions import InclusionTagExtension
from bolt.runtime import settings
from bolt.templates.jinja.extensions import InclusionTagExtension

from .core import Importmap

Expand Down
2 changes: 1 addition & 1 deletion bolt-oauth/bolt/oauth/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from bolt import jinja
from bolt.auth.mixins import LoginRequiredMixin
from bolt.http import HttpResponseBadRequest, HttpResponseRedirect
from bolt.templates import jinja
from bolt.views import View

from .exceptions import (
Expand Down
2 changes: 1 addition & 1 deletion bolt-pages/bolt/pages/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import frontmatter
import pycmarkgfm

from bolt.jinja import environment
from bolt.runtime import settings
from bolt.templates.jinja import environment
from bolt.utils.functional import cached_property


Expand Down
2 changes: 1 addition & 1 deletion bolt-tailwind/bolt/tailwind/jinja.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from bolt.assets.finders import APP_ASSETS_DIR
from bolt.jinja.extensions import InclusionTagExtension
from bolt.runtime import settings
from bolt.templates.jinja.extensions import InclusionTagExtension


class TailwindCSSExtension(InclusionTagExtension):
Expand Down
2 changes: 1 addition & 1 deletion bolt-toolbar/bolt/toolbar/jinja.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from bolt.jinja.extensions import InclusionTagExtension
from bolt.runtime import settings
from bolt.templates.jinja.extensions import InclusionTagExtension
from bolt.utils.module_loading import import_string


Expand Down
114 changes: 114 additions & 0 deletions bolt/http/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,3 +717,117 @@ def __init__(
kwargs.setdefault("content_type", "application/json")
data = json.dumps(data, cls=encoder, **json_dumps_params)
super().__init__(content=data, **kwargs)


class ContentNotRenderedError(Exception):
pass


class RenderableResponse(HttpResponse):
non_picklable_attrs = HttpResponse.non_picklable_attrs | frozenset(
["render_func", "context_data", "_post_render_callbacks", "_request"]
)

def __init__(
self,
render_func,
context=None,
content_type=None,
status=None,
charset=None,
headers=None,
):
self.render_func = render_func

# It would seem obvious to call these next two members 'template' and
# 'context', but those names are reserved as part of the test Client
# API. To avoid the name collision, we use different names.
self.context_data = context

self._post_render_callbacks = []

# content argument doesn't make sense here because it will be replaced
# with rendered template so we always pass empty string in order to
# prevent errors and provide shorter signature.
super().__init__("", content_type, status, charset=charset, headers=headers)

# _is_rendered tracks whether the template and context has been baked
# into a final response.
# Super __init__ doesn't know any better than to set self.content to
# the empty string we just gave it, which wrongly sets _is_rendered
# True, so we initialize it to False after the call to super __init__.
self._is_rendered = False

def __getstate__(self):
"""
Raise an exception if trying to pickle an unrendered response. Pickle
only rendered data, not the data used to construct the response.
"""
if not self._is_rendered:
raise ContentNotRenderedError(
"The response content must be rendered before it can be pickled."
)
return super().__getstate__()

@property
def rendered_content(self):
"""Return the freshly rendered content for the template and context
described by the TemplateResponse.
This *does not* set the final content of the response. To set the
response content, you must either call render(), or set the
content explicitly using the value of this property.
"""
return self.render_func(self.context_data)

def add_post_render_callback(self, callback):
"""Add a new post-rendering callback.
If the response has already been rendered,
invoke the callback immediately.
"""
if self._is_rendered:
callback(self)
else:
self._post_render_callbacks.append(callback)

def render(self):
"""Render (thereby finalizing) the content of the response.
If the content has already been rendered, this is a no-op.
Return the baked response instance.
"""
retval = self
if not self._is_rendered:
self.content = self.rendered_content
for post_callback in self._post_render_callbacks:
newretval = post_callback(retval)
if newretval is not None:
retval = newretval
return retval

@property
def is_rendered(self):
return self._is_rendered

def __iter__(self):
if not self._is_rendered:
raise ContentNotRenderedError(
"The response content must be rendered before it can be iterated over."
)
return super().__iter__()

@property
def content(self):
if not self._is_rendered:
raise ContentNotRenderedError(
"The response content must be rendered before it can be accessed."
)
return super().content

@content.setter
def content(self, value):
"""Set the content for the response."""
HttpResponse.content.fset(self, value)
self._is_rendered = True
15 changes: 0 additions & 15 deletions bolt/jinja/context.py

This file was deleted.

6 changes: 6 additions & 0 deletions bolt/templates/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .core import Template, TemplateFileMissing

__all__ = [
"Template",
"TemplateFileMissing",
]
21 changes: 21 additions & 0 deletions bolt/templates/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import jinja2

from .jinja import environment


class TemplateFileMissing(Exception):
pass


class Template:
# TODO allow str template content?
def __init__(self, filename: str) -> None:
self.filename = filename

try:
self._jinja_template = environment.get_template(filename)
except jinja2.TemplateNotFound:
raise TemplateFileMissing(f"Template file {filename} not found")

def render(self, context: dict) -> str:
return self._jinja_template.render(context)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
32 changes: 32 additions & 0 deletions bolt/views/base.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
import logging

from bolt.csrf.middleware import get_token
from bolt.http import (
HttpRequest,
HttpResponse,
HttpResponseBase,
HttpResponseNotAllowed,
JsonResponse,
)
from bolt.http.response import RenderableResponse
from bolt.templates import Template
from bolt.utils.decorators import classonlymethod
from bolt.utils.functional import lazy
from bolt.utils.html import format_html
from bolt.utils.safestring import SafeString

from .exceptions import HttpResponseException


def csrf_input(request):
return format_html(
'<input type="hidden" name="csrfmiddlewaretoken" value="{}">',
get_token(request),
)


csrf_input_lazy = lazy(csrf_input, SafeString, str)
csrf_token_lazy = lazy(get_token, str)

logger = logging.getLogger("bolt.request")


Expand Down Expand Up @@ -53,6 +70,16 @@ def view(request, *args, **kwargs):

return view

def get_base_context(self) -> dict:
return {
"request": self.request,
"csrf_input": csrf_input_lazy(self.request),
"csrf_token": csrf_token_lazy(self.request),
}

def get_context(self) -> dict:
return {}

def get_response(self) -> HttpResponseBase:
if not self.request.method:
raise AttributeError("HTTP method is not set")
Expand Down Expand Up @@ -83,6 +110,11 @@ def get_response(self) -> HttpResponseBase:
if isinstance(result, dict):
return JsonResponse(result)

# TODO or see if has a render func?
if isinstance(result, Template):
context = {**self.get_base_context(), **self.get_context()}
return RenderableResponse(result.render, context)

raise ValueError(f"Unexpected view return type: {type(result)}")

def _http_method_not_allowed(self) -> HttpResponse:
Expand Down
2 changes: 1 addition & 1 deletion bolt/views/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class ObjectTemplateViewMixin:

def get(self) -> HttpResponse:
self.load_object()
return self.get_template_response()
return self.get_template()

def load_object(self) -> None:
try:
Expand Down
Loading

0 comments on commit 856b045

Please sign in to comment.