Skip to content

Commit

Permalink
Improve template support
Browse files Browse the repository at this point in the history
refs #44
  • Loading branch information
radiac committed Dec 7, 2024
1 parent d0e5f0c commit 033f1fc
Show file tree
Hide file tree
Showing 11 changed files with 298 additions and 16 deletions.
8 changes: 8 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
Changelog
=========

0.10.0 - TBC
------------

Features:

* Improved template support (#44)


0.9.2 - 2024-10-14
------------------

Expand Down
109 changes: 109 additions & 0 deletions docs/templates.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
=========
Templates
=========

There are two ways to define templates for use with nanodjango apps:

* put them in a ``templates/`` dir next to your script
* define them on the ``app.templates`` dict

Whichever approach you take, you can render them using standard techniques, or the
``app.render`` shortcut method.


Defining templates
==================

To define a template in the same script as the rest of your code, assign the templates
to the ``app.templates`` dict, using the filename and relative path as the key, and the
template content as the value.

It is recommended that templates are defined at the bottom of your script, out of the
way of your code.

You can either assign by key:

.. code-block:: python
app.templates["base.html"] = """<!doctype html>
<html lang="en">
<body>
{% block content %}{% endblock %}
</body>"
</html>
"""
app.templates["myview/hello.html"] = "{% block content %}Hello{% endblock %}"
or by dict:

.. code-block:: python
app.templates = {
"base.html": """<!doctype html>
<html lang="en">
<body>
{% block content %}{% endblock %}
</body>"
</html>
""",
"myview/hello.html": """
{% extends "base.html" %}
{% block content %}Hello{% endblock %}
""",
}
This uses Django's ``locmem`` template loader, so these templates can be extended and
included as normal templates, and can work with files in a ``templates`` dir.

If a template path is defined as both a file and in the ``app.templates`` dict, the
template in the dict will be used.


Using templates
===============

Nanodjango provides a helper method to quickly render a template:
**app.render(__request, template_name, context=None, content_type=None, status=None,
using=None__)**

Example usage:

.. code-block:: python
@app.route("/")
def index(request):
return app.render(request, "index.html", {"books": Book.objects.all()})
app.templates = {
"index.html" : """
{% extends "base.html" %}
{% block content %}
<p>There are {{ books.count }} books:</p>
{% for book in books %}
<p>{{ book.title }}</p>
{% endfor %}
{% endblock %}
""",
...
}
This is a direct convenience wrapper for `django.shortcuts.render
<https://docs.djangoproject.com/en/dev/topics/http/shortcuts/#render>`. When converting
a script which calls ``app.render``, nanodjango will attempt to rewrite it to use the
standard Django shortcut.


Converting templates
====================

Running the ``nanodjango convert`` command on an app script will put templates in the
app's ``templates`` directory.

Files which are in a dir will be copied across.

Templates defined in the ``app.templates`` dict will be written out to files.

If the same template path is defined in both, the template from the dict will be written
to the file.
29 changes: 29 additions & 0 deletions examples/hello_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""
nanodjango - Django models, views and admin in a single file
Embedded template example
Usage::
nanodjango run hello_template.py
nanodjango convert hello_template.html /path/to/site --name=myproject
"""

from nanodjango import Django

app = Django(
# Avoid clashes with other examples
SQLITE_DATABASE="hello_template.sqlite3",
MIGRATIONS_DIR="hello_template_migrations",
)


@app.route("/")
def hello_world(request):
return app.render(request, "hello.html", context={"message": "Hello!"})


app.templates = {
"base.html": "<html><body><h1>Base title</h1>{% block content %}{% endblock %}</body></html>",
"hello.html": "{% extends 'base.html' %}{% block content %}<p>Hello, World!</p>{% endblock %}",
}
2 changes: 1 addition & 1 deletion examples/scale/scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Usage::
nanodjango scale.py convert /path/to/site --name=myproject
nanodjango convert scale.py /path/to/site --name=myproject
cd /path/to/site
./manage.py runserver 0:8000
"""
Expand Down
48 changes: 43 additions & 5 deletions nanodjango/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
import sys
from pathlib import Path
from types import ModuleType
from typing import TYPE_CHECKING, Any, Callable
from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence

from django import setup
from django import urls as django_urls
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.db.models import Model
from django.shortcuts import render
from django.views import View

from . import app_meta
Expand All @@ -23,6 +24,7 @@
if TYPE_CHECKING:
from pathlib import Path

from django.http import HttpRequest, HttpResponse
from ninja import NinjaAPI


Expand Down Expand Up @@ -56,6 +58,9 @@ class Django:
#: Whether this app has defined an @app.admin
has_admin: bool = False

#: In-memory template store - access via app.templates
_templates: dict[str, str]

#: Whether this app has any async views
_has_async_view: bool = False

Expand Down Expand Up @@ -125,6 +130,7 @@ def _config(self, _settings):
self.app_name = settings.DF_APP_NAME
self.app_module = app_meta.get_app_module()
self.app_path = Path(inspect.getfile(self.app_module))
self._templates = app_meta.get_templates()

# Import and apply glue after django.conf has its settings
from .django_glue.apps import prepare_apps
Expand Down Expand Up @@ -159,8 +165,15 @@ def instance_name(self):
)
return self._instance_name


def route(self, pattern: str, *, re=False, include=None, name=None):
def route(
self,
pattern: str,
*,
re: bool = False,
include=None,
name: str | None = None,
template: bool | str | None = None,
):
"""
Decorator to add a view to the urls
Expand Down Expand Up @@ -302,6 +315,31 @@ def api(self):

return self._api

@property
def templates(self) -> dict[str, str]:
return self._templates

@templates.setter
def templates(self, data: dict[str, str]):
# Set or replace the templates dict, maintaining the single object reference
if self._templates:
self._templates.clear()
self._templates.update(data)

def render(
self,
request: HttpRequest,
template_name: str | Sequence[str],
context: Mapping[str, Any] | None = None,
content_type: str | None = None,
status: int | None = None,
using: str | None = None,
) -> HttpResponse:
"""
Convenience wrapper for ``django.shortcuts.render`` to save an import
"""
return render(request, template_name, context, content_type, status, using)

def _prepare(self, is_prod=False):
"""
Perform any final setup for this project after it has been imported:
Expand Down Expand Up @@ -518,7 +556,6 @@ def _pre_xsgi(self, is_prod=True):

settings.DEBUG = False


async def asgi(self, scope, receive, send, is_prod=True):
"""
ASGI handler
Expand All @@ -528,8 +565,9 @@ async def asgi(self, scope, receive, send, is_prod=True):
Alternatively run with uvicorn script:app.asgi
"""
from django.core.asgi import get_asgi_application

self._pre_xsgi(is_prod=is_prod)

application = get_asgi_application()
return await application(scope, receive, send)

Expand Down
8 changes: 7 additions & 1 deletion nanodjango/app_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from types import ModuleType
from typing import Any


#: Reference to app module
#:
#: Available after the app is imported, before app.run() is called
Expand All @@ -20,6 +19,9 @@
#: Used to configure settings
_app_conf: dict[str, Any] = {}

#: Dict for in-memory template definitions
_locmem_templates: dict[str, str] = {}


def get_app_module() -> ModuleType:
global _app_module
Expand All @@ -30,3 +32,7 @@ def get_app_module() -> ModuleType:

def get_app_conf() -> dict[str, Any]:
return _app_conf


def get_templates() -> dict[str, str]:
return _locmem_templates
23 changes: 23 additions & 0 deletions nanodjango/convert/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ def build(self) -> None:
plugins.build_settings_done(self)

self.copy_assets()
self.build_app_templates()

self.build_app_models()
plugins.build_app_models_done(self)
Expand Down Expand Up @@ -458,6 +459,21 @@ def copy_assets(self) -> None:

plugins.copy_assets(self)

def build_app_templates(self) -> None:
"""
Build ``templates/*html`` from the app.templates dict
Hooks:
build_templates: After the templates have been built
"""
dest_dir = self.app_path / "templates"
for template_name, template_str in self.app.templates.items():
path = dest_dir / template_name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(template_str)

plugins.build_app_templates(self)

def build_app_models(self) -> None:
"""
Build ``app/models.py`` and collect models in ``self.models`` (a list of
Expand Down Expand Up @@ -513,8 +529,15 @@ def build_app_views(self) -> None:
if not self.views and not extra_src:
return

extra_imports = ""
if any([v.has_render for v in self.views]):
# If we're converting an app.render, register that we know render
extra_imports = "from django.shortcuts import render"
resolver.global_refs.remove("render")

self.write_file(
self.app_path / "views.py",
extra_imports,
resolver.gen_src(),
"\n".join([app_view.src for app_view in self.views]),
"\n".join(extra_src),
Expand Down
Loading

0 comments on commit 033f1fc

Please sign in to comment.