plain.templates 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain_templates-0.1.0/.gitignore +27 -0
- plain_templates-0.1.0/LICENSE +28 -0
- plain_templates-0.1.0/PKG-INFO +407 -0
- plain_templates-0.1.0/README.md +1 -0
- plain_templates-0.1.0/plain/templates/CHANGELOG.md +16 -0
- plain_templates-0.1.0/plain/templates/README.md +395 -0
- plain_templates-0.1.0/plain/templates/__init__.py +17 -0
- plain_templates-0.1.0/plain/templates/config.py +6 -0
- plain_templates-0.1.0/plain/templates/core.py +40 -0
- plain_templates-0.1.0/plain/templates/default_settings.py +2 -0
- plain_templates-0.1.0/plain/templates/jinja/__init__.py +78 -0
- plain_templates-0.1.0/plain/templates/jinja/environments.py +64 -0
- plain_templates-0.1.0/plain/templates/jinja/extensions.py +46 -0
- plain_templates-0.1.0/plain/templates/jinja/filters.py +55 -0
- plain_templates-0.1.0/plain/templates/jinja/globals.py +16 -0
- plain_templates-0.1.0/plain/templates/views.py +292 -0
- plain_templates-0.1.0/pyproject.toml +19 -0
- plain_templates-0.1.0/tests/app/settings.py +5 -0
- plain_templates-0.1.0/tests/app/templates/404.html +7 -0
- plain_templates-0.1.0/tests/app/templates/500.html +7 -0
- plain_templates-0.1.0/tests/app/urls.py +6 -0
- plain_templates-0.1.0/tests/conftest.py +26 -0
- plain_templates-0.1.0/tests/error_routers.py +54 -0
- plain_templates-0.1.0/tests/public/test_error_responses.py +87 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.venv
|
|
2
|
+
/.env
|
|
3
|
+
*.egg-info
|
|
4
|
+
*.py[co]
|
|
5
|
+
__pycache__
|
|
6
|
+
.pytest_cache
|
|
7
|
+
*.DS_Store
|
|
8
|
+
*.swp
|
|
9
|
+
*.swo
|
|
10
|
+
|
|
11
|
+
/*.code-workspace
|
|
12
|
+
|
|
13
|
+
# Test apps
|
|
14
|
+
plain*/tests/.plain
|
|
15
|
+
|
|
16
|
+
# Agent scratch files
|
|
17
|
+
/scratch
|
|
18
|
+
|
|
19
|
+
# Plain temp dirs
|
|
20
|
+
.plain
|
|
21
|
+
|
|
22
|
+
.vscode
|
|
23
|
+
/.claude/settings.local.json
|
|
24
|
+
/.claude/skills/announcements/
|
|
25
|
+
/CLAUDE.local.md
|
|
26
|
+
/.benchmarks
|
|
27
|
+
.claude/worktrees
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023, Dropseed, LLC
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plain.templates
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Render HTML templates using Jinja2.
|
|
5
|
+
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Requires-Dist: jinja2>=3.1.2
|
|
10
|
+
Requires-Dist: plain<1.0.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# plain.templates
|
|
14
|
+
|
|
15
|
+
**Render HTML templates using Jinja2.**
|
|
16
|
+
|
|
17
|
+
- [Overview](#overview)
|
|
18
|
+
- [Template files](#template-files)
|
|
19
|
+
- [Template-rendering views](#template-rendering-views)
|
|
20
|
+
- [TemplateView](#templateview)
|
|
21
|
+
- [FormView](#formview)
|
|
22
|
+
- [DetailView, CreateView, UpdateView, DeleteView, ListView](#object-views)
|
|
23
|
+
- [Template context](#template-context)
|
|
24
|
+
- [Built-in globals](#built-in-globals)
|
|
25
|
+
- [Built-in filters](#built-in-filters)
|
|
26
|
+
- [Custom globals and filters](#custom-globals-and-filters)
|
|
27
|
+
- [Custom template extensions](#custom-template-extensions)
|
|
28
|
+
- [Rendering templates manually](#rendering-templates-manually)
|
|
29
|
+
- [Custom Jinja environment](#custom-jinja-environment)
|
|
30
|
+
- [Forms](#forms)
|
|
31
|
+
- [FAQs](#faqs)
|
|
32
|
+
- [Installation](#installation)
|
|
33
|
+
|
|
34
|
+
## Overview
|
|
35
|
+
|
|
36
|
+
Plain uses Jinja2 for template rendering. You can refer to the [Jinja documentation](https://jinja.palletsprojects.com/en/stable/) for all of the features available.
|
|
37
|
+
|
|
38
|
+
Templates are typically used with [`TemplateView`](./views.py#TemplateView) or one of its subclasses (see [Template-rendering views](#template-rendering-views)).
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# app/views.py
|
|
42
|
+
from plain.templates.views import TemplateView
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ExampleView(TemplateView):
|
|
46
|
+
template_name = "example.html"
|
|
47
|
+
|
|
48
|
+
def get_template_context(self):
|
|
49
|
+
context = super().get_template_context()
|
|
50
|
+
context["message"] = "Hello, world!"
|
|
51
|
+
return context
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<!-- app/templates/example.html -->
|
|
56
|
+
{% extends "base.html" %}
|
|
57
|
+
|
|
58
|
+
{% block content %}
|
|
59
|
+
<h1>{{ message }}</h1>
|
|
60
|
+
{% endblock %}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Template files
|
|
64
|
+
|
|
65
|
+
Template files can live in two locations:
|
|
66
|
+
|
|
67
|
+
1. **`app/templates/`** - Your app's templates (highest priority)
|
|
68
|
+
2. **`{package}/templates/`** - Templates inside any installed package
|
|
69
|
+
|
|
70
|
+
All template directories are merged together, so you can override templates from installed packages by creating a file with the same name in `app/templates/`.
|
|
71
|
+
|
|
72
|
+
## Template-rendering views
|
|
73
|
+
|
|
74
|
+
`plain.templates.views` ships the view classes that render templates. The base [`View`](../../../plain/plain/views/README.md) class lives in core `plain.views` and doesn't know about templates — install `plain.templates` to use any of these.
|
|
75
|
+
|
|
76
|
+
### TemplateView
|
|
77
|
+
|
|
78
|
+
[`TemplateView`](./views.py#TemplateView) renders a Jinja template:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from plain.templates.views import TemplateView
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ExampleView(TemplateView):
|
|
85
|
+
template_name = "example.html"
|
|
86
|
+
|
|
87
|
+
def get_template_context(self):
|
|
88
|
+
context = super().get_template_context()
|
|
89
|
+
context["message"] = "Hello, world!"
|
|
90
|
+
return context
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
For simple pages that don't need custom context, configure `TemplateView` directly in your URL routes:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from plain.templates.views import TemplateView
|
|
97
|
+
from plain.urls import path, Router
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class AppRouter(Router):
|
|
101
|
+
routes = [
|
|
102
|
+
path("/example/", TemplateView.as_view(template_name="example.html")),
|
|
103
|
+
]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### FormView
|
|
107
|
+
|
|
108
|
+
[`FormView`](./views.py#FormView) handles displaying and processing [forms](../../../plain/plain/forms/README.md). The form is automatically available in your template as `form`:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from plain.templates.views import FormView
|
|
112
|
+
from .forms import ExampleForm
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ExampleView(FormView):
|
|
116
|
+
template_name = "example.html"
|
|
117
|
+
form_class = ExampleForm
|
|
118
|
+
success_url = "."
|
|
119
|
+
|
|
120
|
+
def form_valid(self, form):
|
|
121
|
+
return super().form_valid(form)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Object views
|
|
125
|
+
|
|
126
|
+
[`DetailView`](./views.py#DetailView), [`CreateView`](./views.py#CreateView), [`UpdateView`](./views.py#UpdateView), [`DeleteView`](./views.py#DeleteView), and [`ListView`](./views.py#ListView) provide standard CRUD scaffolding. Each requires you to implement `get_object()` or `get_objects()`:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from plain.templates.views import DetailView
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ExampleDetailView(DetailView):
|
|
133
|
+
template_name = "detail.html"
|
|
134
|
+
|
|
135
|
+
def get_object(self):
|
|
136
|
+
return MyObjectClass.query.get(
|
|
137
|
+
id=self.url_kwargs["id"],
|
|
138
|
+
user=self.request.user,
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The single object is exposed in templates as `object`; list views expose `objects`. Set `context_object_name` for a more descriptive name.
|
|
143
|
+
|
|
144
|
+
## Template context
|
|
145
|
+
|
|
146
|
+
When using `TemplateView`, you pass data to templates by overriding `get_template_context()`:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from plain.templates.views import TemplateView
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ProductView(TemplateView):
|
|
153
|
+
template_name = "product.html"
|
|
154
|
+
|
|
155
|
+
def get_template_context(self):
|
|
156
|
+
context = super().get_template_context()
|
|
157
|
+
context["product"] = Product.objects.get(id=self.url_kwargs["id"])
|
|
158
|
+
context["related_products"] = Product.objects.filter(category=context["product"].category)[:5]
|
|
159
|
+
return context
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The context is then available in your template:
|
|
163
|
+
|
|
164
|
+
```html
|
|
165
|
+
<h1>{{ product.name }}</h1>
|
|
166
|
+
<ul>
|
|
167
|
+
{% for item in related_products %}
|
|
168
|
+
<li>{{ item.name }}</li>
|
|
169
|
+
{% endfor %}
|
|
170
|
+
</ul>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Built-in globals
|
|
174
|
+
|
|
175
|
+
Plain provides several [global functions](./jinja/globals.py) available in all templates:
|
|
176
|
+
|
|
177
|
+
| Global | Description |
|
|
178
|
+
| ---------------------------- | ---------------------------------- |
|
|
179
|
+
| `asset(path)` | Returns the URL for a static asset |
|
|
180
|
+
| `url(name, *args, **kwargs)` | Reverses a URL by name |
|
|
181
|
+
| `Paginator` | The Paginator class for pagination |
|
|
182
|
+
| `now()` | Returns the current datetime |
|
|
183
|
+
| `timedelta` | The timedelta class for date math |
|
|
184
|
+
| `localtime(dt)` | Converts a datetime to local time |
|
|
185
|
+
|
|
186
|
+
```html
|
|
187
|
+
<link rel="stylesheet" href="{{ asset('css/style.css') }}">
|
|
188
|
+
<a href="{{ url('product_detail', id=product.id) }}">View</a>
|
|
189
|
+
<p>Generated at {{ now() }}</p>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Built-in filters
|
|
193
|
+
|
|
194
|
+
Plain includes several [filters](./jinja/filters.py) for common operations:
|
|
195
|
+
|
|
196
|
+
| Filter | Description |
|
|
197
|
+
| ----------------------------- | ------------------------------------ |
|
|
198
|
+
| `strftime(format)` | Formats a datetime |
|
|
199
|
+
| `strptime(format)` | Parses a string to datetime |
|
|
200
|
+
| `fromtimestamp(ts)` | Creates datetime from timestamp |
|
|
201
|
+
| `fromisoformat(s)` | Creates datetime from ISO string |
|
|
202
|
+
| `localtime(tz)` | Converts to local timezone |
|
|
203
|
+
| `timeuntil` | Human-readable time until a date |
|
|
204
|
+
| `timesince` | Human-readable time since a date |
|
|
205
|
+
| `json_script(id)` | Outputs JSON safely in a script tag |
|
|
206
|
+
| `islice(stop)` | Slices iterables (useful for dicts) |
|
|
207
|
+
| `pluralize(singular, plural)` | Returns plural suffix based on count |
|
|
208
|
+
|
|
209
|
+
```html
|
|
210
|
+
<p>Posted {{ post.created_at|timesince }} ago</p>
|
|
211
|
+
<p>{{ items|length }} item{{ items|length|pluralize }}</p>
|
|
212
|
+
<p>{{ 5 }} ox{{ 5|pluralize("en") }}</p>
|
|
213
|
+
{{ data|json_script("page-data") }}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Custom globals and filters
|
|
217
|
+
|
|
218
|
+
You can register your own globals and filters in `app/templates.py` (or `{package}/templates.py`). These files are automatically imported when the template environment loads.
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
# app/templates.py
|
|
222
|
+
from plain.templates import register_template_filter, register_template_global
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@register_template_filter
|
|
226
|
+
def camel_case(value):
|
|
227
|
+
"""Convert a string to CamelCase."""
|
|
228
|
+
return value.replace("_", " ").title().replace(" ", "")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@register_template_global
|
|
232
|
+
def app_version():
|
|
233
|
+
"""Return the current app version."""
|
|
234
|
+
return "1.0.0"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Now you can use these in templates:
|
|
238
|
+
|
|
239
|
+
```html
|
|
240
|
+
<p>{{ "my_variable"|camel_case }}</p> <!-- outputs: MyVariable -->
|
|
241
|
+
<footer>Version {{ app_version() }}</footer>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
You can also register non-callable values as globals by providing a name:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from plain.templates import register_template_global
|
|
248
|
+
|
|
249
|
+
register_template_global("1.0.0", name="APP_VERSION")
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Custom template extensions
|
|
253
|
+
|
|
254
|
+
For more complex template features, you can create Jinja extensions. The [`InclusionTagExtension`](./jinja/extensions.py#InclusionTagExtension) base class makes it easy to create custom tags that render their own templates.
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
# app/templates.py
|
|
258
|
+
from plain.templates import register_template_extension
|
|
259
|
+
from plain.templates.jinja.extensions import InclusionTagExtension
|
|
260
|
+
from plain.runtime import settings
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@register_template_extension
|
|
264
|
+
class AlertExtension(InclusionTagExtension):
|
|
265
|
+
tags = {"alert"}
|
|
266
|
+
template_name = "components/alert.html"
|
|
267
|
+
|
|
268
|
+
def get_context(self, context, *args, **kwargs):
|
|
269
|
+
return {
|
|
270
|
+
"message": args[0] if args else "",
|
|
271
|
+
"type": kwargs.get("type", "info"),
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```html
|
|
276
|
+
<!-- app/templates/components/alert.html -->
|
|
277
|
+
<div class="alert alert-{{ type }}">{{ message }}</div>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```html
|
|
281
|
+
<!-- Usage in any template -->
|
|
282
|
+
{% alert "Something happened!" type="warning" %}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Rendering templates manually
|
|
286
|
+
|
|
287
|
+
You can render templates outside of views using the [`Template`](./core.py#Template) class.
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
from plain.templates import Template
|
|
291
|
+
|
|
292
|
+
html = Template("email/welcome.html").render({
|
|
293
|
+
"user_name": "Alice",
|
|
294
|
+
"activation_url": "https://example.com/activate/abc123",
|
|
295
|
+
})
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
If the template file doesn't exist, a [`TemplateFileMissing`](./core.py#TemplateFileMissing) exception is raised.
|
|
299
|
+
|
|
300
|
+
## Custom Jinja environment
|
|
301
|
+
|
|
302
|
+
By default, Plain uses a [`DefaultEnvironment`](./jinja/environments.py#DefaultEnvironment) that configures Jinja2 with sensible defaults:
|
|
303
|
+
|
|
304
|
+
- **Autoescaping** enabled for security
|
|
305
|
+
- **StrictUndefined** so undefined variables raise errors
|
|
306
|
+
- **Auto-reload** in debug mode
|
|
307
|
+
- **Loop controls** extension (`break`, `continue`)
|
|
308
|
+
- **Debug** extension
|
|
309
|
+
|
|
310
|
+
You can customize the environment by creating your own class and pointing to it in settings:
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
# app/jinja.py
|
|
314
|
+
from plain.templates.jinja.environments import DefaultEnvironment
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class CustomEnvironment(DefaultEnvironment):
|
|
318
|
+
def __init__(self):
|
|
319
|
+
super().__init__()
|
|
320
|
+
# Add your customizations here
|
|
321
|
+
self.globals["CUSTOM_SETTING"] = "value"
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
# app/settings.py
|
|
326
|
+
TEMPLATES_JINJA_ENVIRONMENT = "app.jinja.CustomEnvironment"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## FAQs
|
|
330
|
+
|
|
331
|
+
#### Why am I getting "undefined variable" errors?
|
|
332
|
+
|
|
333
|
+
Plain uses Jinja's `StrictUndefined` mode, which raises an error when you reference a variable that doesn't exist in the context. This helps catch typos and missing data early. Make sure you're passing all required variables in `get_template_context()`.
|
|
334
|
+
|
|
335
|
+
#### Why does my template show an error about a callable?
|
|
336
|
+
|
|
337
|
+
Plain's template environment prevents accidentally rendering callables (functions, methods) directly. If you see an error like "X is callable, did you forget parentheses?", you probably need to add `()` to call the function:
|
|
338
|
+
|
|
339
|
+
```html
|
|
340
|
+
<!-- Wrong -->
|
|
341
|
+
{{ user.get_full_name }}
|
|
342
|
+
|
|
343
|
+
<!-- Correct -->
|
|
344
|
+
{{ user.get_full_name() }}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
#### How do I use Jinja's loop controls?
|
|
348
|
+
|
|
349
|
+
Plain enables the `loopcontrols` extension by default, so you can use `break` and `continue` in loops:
|
|
350
|
+
|
|
351
|
+
```html
|
|
352
|
+
{% for item in items %}
|
|
353
|
+
{% if item.skip %}
|
|
354
|
+
{% continue %}
|
|
355
|
+
{% endif %}
|
|
356
|
+
{% if item.stop %}
|
|
357
|
+
{% break %}
|
|
358
|
+
{% endif %}
|
|
359
|
+
<p>{{ item.name }}</p>
|
|
360
|
+
{% endfor %}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
#### Where can I learn more about Jinja2?
|
|
364
|
+
|
|
365
|
+
The [Jinja2 documentation](https://jinja.palletsprojects.com/en/stable/) covers all the template syntax, including conditionals, loops, macros, and inheritance.
|
|
366
|
+
|
|
367
|
+
## Forms
|
|
368
|
+
|
|
369
|
+
Forms are rendered manually using the bound field attributes:
|
|
370
|
+
|
|
371
|
+
```html
|
|
372
|
+
<form method="post">
|
|
373
|
+
<div>
|
|
374
|
+
<label for="{{ form.email.html_id }}">Email</label>
|
|
375
|
+
<input
|
|
376
|
+
type="email"
|
|
377
|
+
name="{{ form.email.html_name }}"
|
|
378
|
+
id="{{ form.email.html_id }}"
|
|
379
|
+
value="{{ form.email.value }}"
|
|
380
|
+
>
|
|
381
|
+
{% for error in form.email.errors %}
|
|
382
|
+
<p>{{ error }}</p>
|
|
383
|
+
{% endfor %}
|
|
384
|
+
</div>
|
|
385
|
+
<button type="submit">Submit</button>
|
|
386
|
+
</form>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Each bound field provides: `html_name`, `html_id`, `value`, `errors`, `field`, `initial`.
|
|
390
|
+
|
|
391
|
+
## Installation
|
|
392
|
+
|
|
393
|
+
Install the `plain.templates` package:
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
uv add plain.templates
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Then add it to `INSTALLED_PACKAGES`:
|
|
400
|
+
|
|
401
|
+
```python
|
|
402
|
+
# app/settings.py
|
|
403
|
+
INSTALLED_PACKAGES = [
|
|
404
|
+
"plain.templates",
|
|
405
|
+
# ...
|
|
406
|
+
]
|
|
407
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
./plain/templates/README.md
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# plain-templates changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0](https://github.com/dropseed/plain/releases/plain-templates@0.1.0) (2026-05-12)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- First release. `plain.templates` is now a separate package, extracted from `plain` core ([19b622a7ca](https://github.com/dropseed/plain/commit/19b622a7ca)). It owns:
|
|
8
|
+
- The Jinja2 engine, `DefaultEnvironment`, the `TEMPLATES_JINJA_ENVIRONMENT` setting
|
|
9
|
+
- `Template` and `TemplateFileMissing`
|
|
10
|
+
- `register_template_global`, `register_template_filter`, `register_template_extension`
|
|
11
|
+
- The template-rendering view bases: `TemplateView`, `FormView`, `DetailView`, `CreateView`, `UpdateView`, `DeleteView`, `ListView` at `plain.templates.views`
|
|
12
|
+
- The `{status_code}.html` error rendering wired through `plain` core's framework default exception handler (lazy import — degrades to plain text if this package isn't installed)
|
|
13
|
+
|
|
14
|
+
### Upgrade instructions
|
|
15
|
+
|
|
16
|
+
- Install the package and add it to `INSTALLED_PACKAGES` — see the [`plain` 0.143.0 release notes](../../../plain/plain/CHANGELOG.md) for the full migration.
|