fastlifeweb 0.20.0__py3-none-any.whl → 0.21.0__py3-none-any.whl
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.
- CHANGELOG.md +11 -1
- fastlife/__init__.py +5 -0
- fastlife/adapters/jinjax/renderer.py +4 -52
- fastlife/adapters/jinjax/widget_factory/bool_builder.py +2 -2
- fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +5 -4
- fastlife/adapters/jinjax/widget_factory/enum_builder.py +2 -2
- fastlife/adapters/jinjax/widget_factory/factory.py +20 -21
- fastlife/adapters/jinjax/widget_factory/literal_builder.py +7 -6
- fastlife/adapters/jinjax/widget_factory/model_builder.py +3 -3
- fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +2 -2
- fastlife/adapters/jinjax/widget_factory/sequence_builder.py +3 -3
- fastlife/adapters/jinjax/widget_factory/set_builder.py +2 -2
- fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +7 -8
- fastlife/adapters/jinjax/widget_factory/union_builder.py +3 -3
- fastlife/adapters/jinjax/widgets/base.py +35 -35
- fastlife/adapters/jinjax/widgets/boolean.py +13 -34
- fastlife/adapters/jinjax/widgets/checklist.py +36 -42
- fastlife/adapters/jinjax/widgets/dropdown.py +32 -38
- fastlife/adapters/jinjax/widgets/hidden.py +7 -15
- fastlife/adapters/jinjax/widgets/model.py +33 -40
- fastlife/adapters/jinjax/widgets/sequence.py +61 -40
- fastlife/adapters/jinjax/widgets/text.py +39 -78
- fastlife/adapters/jinjax/widgets/union.py +50 -57
- fastlife/components/CsrfToken.jinja +1 -1
- fastlife/components/pydantic_form/Widget.jinja +4 -3
- fastlife/config/configurator.py +65 -19
- fastlife/config/exceptions.py +0 -2
- fastlife/config/views.py +0 -2
- fastlife/domain/__init__.py +1 -0
- fastlife/domain/model/__init__.py +1 -0
- fastlife/domain/model/security.py +19 -0
- fastlife/domain/model/template.py +30 -0
- fastlife/domain/model/types.py +17 -0
- fastlife/request/request.py +19 -0
- fastlife/security/csrf.py +3 -13
- fastlife/services/templates.py +9 -42
- fastlife/services/translations.py +12 -0
- fastlife/templates/__init__.py +1 -6
- fastlife/templates/inline.py +18 -14
- {fastlifeweb-0.20.0.dist-info → fastlifeweb-0.21.0.dist-info}/METADATA +1 -1
- {fastlifeweb-0.20.0.dist-info → fastlifeweb-0.21.0.dist-info}/RECORD +44 -49
- fastlife/components/pydantic_form/Boolean.jinja +0 -13
- fastlife/components/pydantic_form/Checklist.jinja +0 -21
- fastlife/components/pydantic_form/Dropdown.jinja +0 -18
- fastlife/components/pydantic_form/Hidden.jinja +0 -3
- fastlife/components/pydantic_form/Model.jinja +0 -30
- fastlife/components/pydantic_form/Sequence.jinja +0 -47
- fastlife/components/pydantic_form/Text.jinja +0 -11
- fastlife/components/pydantic_form/Textarea.jinja +0 -38
- fastlife/components/pydantic_form/Union.jinja +0 -34
- fastlife/templates/binding.py +0 -52
- {fastlifeweb-0.20.0.dist-info → fastlifeweb-0.21.0.dist-info}/WHEEL +0 -0
- {fastlifeweb-0.20.0.dist-info → fastlifeweb-0.21.0.dist-info}/entry_points.txt +0 -0
- {fastlifeweb-0.20.0.dist-info → fastlifeweb-0.21.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,57 +3,59 @@ Widget for field of type Union.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
from collections.abc import Sequence
|
6
|
-
from typing import
|
6
|
+
from typing import Union
|
7
7
|
|
8
8
|
from markupsafe import Markup
|
9
|
-
from pydantic import BaseModel
|
9
|
+
from pydantic import BaseModel, Field
|
10
10
|
|
11
11
|
from fastlife.services.templates import AbstractTemplateRenderer
|
12
12
|
|
13
|
-
from .base import TypeWrapper, Widget
|
13
|
+
from .base import TWidget, TypeWrapper, Widget
|
14
14
|
|
15
15
|
|
16
|
-
class UnionWidget(Widget[
|
16
|
+
class UnionWidget(Widget[TWidget]):
|
17
17
|
"""
|
18
18
|
Widget for union types.
|
19
|
+
"""
|
19
20
|
|
20
|
-
|
21
|
-
:
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
21
|
+
template = """
|
22
|
+
<pydantic_form.Widget :widget_id="id" :removable="removable">
|
23
|
+
<div id="{{id}}">
|
24
|
+
<Details>
|
25
|
+
<Summary :id="id + '-union-summary'">
|
26
|
+
<H3 :class="H3_SUMMARY_CLASS">{{title}}</H3>
|
27
|
+
<pydantic_form.Error :text="error" />
|
28
|
+
</Summary>
|
29
|
+
<div hx-sync="this" id="{{id}}-child">
|
30
|
+
{% if child %}
|
31
|
+
{{ child }}
|
32
|
+
{% else %}
|
33
|
+
{% for typ in types %}
|
34
|
+
<Button type="button"
|
35
|
+
hx-target="closest div"
|
36
|
+
:hx-get="typ.url"
|
37
|
+
:hx-vals="typ.params|tojson"
|
38
|
+
:id="typ.id"
|
39
|
+
onclick={{ "document.getElementById('" + id + "-remove-btn').hidden=false" }}
|
40
|
+
:class="SECONDARY_BUTTON_CLASS">{{typ.title}}</Button>
|
41
|
+
{% endfor %}
|
42
|
+
{% endif %}
|
43
|
+
</div>
|
44
|
+
<Button type="button" :id="id + '-remove-btn'" :hx-target="'#' + id"
|
45
|
+
:hx-vals="parent_type.params|tojson" :hx-get="parent_type.url" :hidden="not child"
|
46
|
+
:class="SECONDARY_BUTTON_CLASS">
|
47
|
+
Remove
|
48
|
+
</Button>
|
49
|
+
</Details>
|
50
|
+
</div>
|
51
|
+
</pydantic_form.Widget>
|
30
52
|
"""
|
31
53
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
hint: str | None = None,
|
38
|
-
aria_label: str | None = None,
|
39
|
-
value: Widget[Any] | None,
|
40
|
-
error: str | None = None,
|
41
|
-
children_types: Sequence[type[BaseModel]],
|
42
|
-
removable: bool = False,
|
43
|
-
token: str,
|
44
|
-
):
|
45
|
-
super().__init__(
|
46
|
-
name,
|
47
|
-
value=value,
|
48
|
-
error=error,
|
49
|
-
title=title,
|
50
|
-
hint=hint,
|
51
|
-
aria_label=aria_label,
|
52
|
-
token=token,
|
53
|
-
removable=removable,
|
54
|
-
)
|
55
|
-
self.children_types = children_types
|
56
|
-
self.parent_name = name
|
54
|
+
children_types: Sequence[type[BaseModel]]
|
55
|
+
parent_type: TypeWrapper | None = Field(default=None)
|
56
|
+
|
57
|
+
types: Sequence[TypeWrapper] | None = Field(default=None)
|
58
|
+
child: str = Field(default="")
|
57
59
|
|
58
60
|
def build_types(self, route_prefix: str) -> Sequence[TypeWrapper]:
|
59
61
|
"""Wrap types in the union in order to get the in their own widgets."""
|
@@ -62,24 +64,15 @@ class UnionWidget(Widget[Widget[Any]]):
|
|
62
64
|
for typ in self.children_types
|
63
65
|
]
|
64
66
|
|
65
|
-
def get_template(self) -> str:
|
66
|
-
return "pydantic_form.Union.jinja"
|
67
|
-
|
68
67
|
def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
|
69
68
|
"""Return the html version."""
|
70
|
-
child = Markup(self.value.to_html(renderer)) if self.value else ""
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
renderer.route_prefix,
|
79
|
-
self.parent_name,
|
80
|
-
self.token,
|
81
|
-
title=self.title,
|
82
|
-
),
|
83
|
-
child=child,
|
84
|
-
)
|
69
|
+
self.child = Markup(self.value.to_html(renderer)) if self.value else ""
|
70
|
+
self.types = self.build_types(renderer.route_prefix)
|
71
|
+
self.parent_type = TypeWrapper(
|
72
|
+
Union[tuple(self.children_types)], # type: ignore # noqa: UP007
|
73
|
+
renderer.route_prefix,
|
74
|
+
self.name,
|
75
|
+
self.token,
|
76
|
+
title=self.title,
|
85
77
|
)
|
78
|
+
return Markup(renderer.render_template(self))
|
@@ -2,4 +2,4 @@
|
|
2
2
|
a :jinjax:component:`Hidden` field automaticaly injected in every
|
3
3
|
:jinjax:component:`Form` to protect against CSRF Attacks.
|
4
4
|
#}
|
5
|
-
<Hidden :name="csrf_token.name" :value="csrf_token.value" />
|
5
|
+
<Hidden :name="request.csrf_token.name" :value="request.csrf_token.value" />
|
@@ -1,11 +1,12 @@
|
|
1
1
|
{# doc Base component for widget #}
|
2
2
|
{# def
|
3
|
-
|
3
|
+
widget_id: Annotated[str, "widget to display."],
|
4
|
+
removable: Annotated[bool, "Set to true to add a remove button"],
|
4
5
|
#}
|
5
|
-
{% set container_id =
|
6
|
+
{% set container_id = widget_id + "-container" %}
|
6
7
|
<div id="{{container_id}}">
|
7
8
|
{{ content }}
|
8
|
-
{% if
|
9
|
+
{% if removable %}
|
9
10
|
<Button type="button" :onclick={{"document.getElementById('" + container_id + "').remove()" }}>
|
10
11
|
Remove
|
11
12
|
</Button>
|
fastlife/config/configurator.py
CHANGED
@@ -12,8 +12,9 @@ phase.
|
|
12
12
|
"""
|
13
13
|
|
14
14
|
import logging
|
15
|
+
from asyncio import iscoroutine
|
15
16
|
from collections import defaultdict
|
16
|
-
from collections.abc import Callable,
|
17
|
+
from collections.abc import Callable, Sequence
|
17
18
|
from enum import Enum
|
18
19
|
from pathlib import Path
|
19
20
|
from types import ModuleType
|
@@ -34,6 +35,7 @@ from fastlife.routing.router import Router
|
|
34
35
|
from fastlife.security.csrf import check_csrf
|
35
36
|
from fastlife.services.policy import check_permission
|
36
37
|
from fastlife.shared_utils.resolver import resolve, resolve_maybe_relative
|
38
|
+
from fastlife.templates.inline import is_inline_template_returned
|
37
39
|
|
38
40
|
from .registry import DefaultRegistry, TRegistry
|
39
41
|
from .settings import Settings
|
@@ -43,6 +45,7 @@ if TYPE_CHECKING:
|
|
43
45
|
from fastlife.services.templates import (
|
44
46
|
AbstractTemplateRendererFactory, # coverage: ignore
|
45
47
|
)
|
48
|
+
from fastlife.templates.inline import InlineTemplate
|
46
49
|
|
47
50
|
from fastlife.services.locale_negociator import LocaleNegociator
|
48
51
|
|
@@ -143,6 +146,8 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
143
146
|
] = {}
|
144
147
|
|
145
148
|
self._registered_permissions: set[str] = set()
|
149
|
+
|
150
|
+
self._renderer_globals: dict[str, Any] = {}
|
146
151
|
self.scanner = venusian.Scanner(fastlife=self)
|
147
152
|
self.include("fastlife.views")
|
148
153
|
self.include("fastlife.middlewares")
|
@@ -441,6 +446,53 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
441
446
|
)
|
442
447
|
return self
|
443
448
|
|
449
|
+
def add_renderer_global(
|
450
|
+
self, name: str, value: Any, *, evaluate: bool = True
|
451
|
+
) -> None:
|
452
|
+
"""
|
453
|
+
Add a rendering global value.
|
454
|
+
|
455
|
+
:param name: the name or key of the global value available in the template.
|
456
|
+
:param value: a value, or a callable or
|
457
|
+
an async function with a request in parameter that will evaluate the value.
|
458
|
+
:param evaluate: set to false if you want to inject helper methods in the
|
459
|
+
template.
|
460
|
+
"""
|
461
|
+
self._renderer_globals[name] = value, evaluate
|
462
|
+
|
463
|
+
async def _build_renderer_globals(self, request: Request) -> dict[str, Any]:
|
464
|
+
"""
|
465
|
+
Build globals variables accessible in any templates.
|
466
|
+
|
467
|
+
* `request` is the {class}`current request <fastlife.request.request.Request>`
|
468
|
+
* `authenticated_user` is used to access to the authenticated user if the
|
469
|
+
security policy has been installed.
|
470
|
+
* `csrf_token` is used to build for {jinjax:component}`CsrfToken`.
|
471
|
+
* `gettext`, `ngettext`, `dgettext`, `dngettext`, `pgettext`, `dpgettext`,
|
472
|
+
`npgettext`, `dnpgettext` methods are installed for i18n purpose.
|
473
|
+
"""
|
474
|
+
lczr = request.registry.localizer(request)
|
475
|
+
custom_globals = {}
|
476
|
+
for key, (val, evaluate) in self._renderer_globals.items():
|
477
|
+
if evaluate and callable(val):
|
478
|
+
val = val(request)
|
479
|
+
if iscoroutine(val):
|
480
|
+
val = await val
|
481
|
+
custom_globals[key] = val
|
482
|
+
return {
|
483
|
+
"request": request,
|
484
|
+
"gettext": lczr.gettext,
|
485
|
+
"ngettext": lczr.ngettext,
|
486
|
+
"dgettext": lczr.dgettext,
|
487
|
+
"dngettext": lczr.dngettext,
|
488
|
+
"pgettext": lczr.pgettext,
|
489
|
+
"dpgettext": lczr.dpgettext,
|
490
|
+
"npgettext": lczr.npgettext,
|
491
|
+
"dnpgettext": lczr.dnpgettext,
|
492
|
+
**custom_globals,
|
493
|
+
**request.renderer_globals,
|
494
|
+
}
|
495
|
+
|
444
496
|
def add_route(
|
445
497
|
self,
|
446
498
|
name: str,
|
@@ -466,8 +518,6 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
466
518
|
:param path: path of the route, use `{curly_brace}` to inject FastAPI Path
|
467
519
|
parameters.
|
468
520
|
:param endpoint: the function that will reveive the request.
|
469
|
-
:param template: the template rendered by the
|
470
|
-
{class}`fastlife.service.templates.AbstractTemplateRenderer`.
|
471
521
|
:param permission: a permission to validate by the
|
472
522
|
{class}`Security Policy <fastlife.security.policy.AbstractSecurityPolicy>`.
|
473
523
|
:param status_code: customize response status code.
|
@@ -479,17 +529,21 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
479
529
|
self._registered_permissions.add(permission)
|
480
530
|
dependencies.append(Depends(check_permission(permission)))
|
481
531
|
|
482
|
-
if
|
532
|
+
if is_inline_template_returned(endpoint):
|
483
533
|
|
484
|
-
def render(
|
534
|
+
async def render(
|
485
535
|
request: Request,
|
486
|
-
resp: Annotated[Response |
|
536
|
+
resp: Annotated["Response | InlineTemplate", Depends(endpoint)],
|
487
537
|
) -> Response:
|
488
538
|
if isinstance(resp, Response):
|
489
539
|
return resp
|
540
|
+
|
541
|
+
template = resp.renderer
|
542
|
+
globs = await self._build_renderer_globals(request)
|
490
543
|
return request.registry.get_renderer(template)(request).render(
|
491
544
|
template,
|
492
545
|
params=resp,
|
546
|
+
globals=globs,
|
493
547
|
)
|
494
548
|
|
495
549
|
endpoint = render
|
@@ -523,9 +577,8 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
523
577
|
def add_exception_handler(
|
524
578
|
self,
|
525
579
|
status_code_or_exc: int | type[Exception],
|
526
|
-
handler:
|
580
|
+
handler: Callable[..., "Response | InlineTemplate"],
|
527
581
|
*,
|
528
|
-
template: str | None = None,
|
529
582
|
status_code: int = 500,
|
530
583
|
) -> Self:
|
531
584
|
"""
|
@@ -533,7 +586,7 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
533
586
|
|
534
587
|
"""
|
535
588
|
|
536
|
-
def exception_handler(request: BaseRequest, exc: Exception) -> Any:
|
589
|
+
async def exception_handler(request: BaseRequest, exc: Exception) -> Any:
|
537
590
|
# FastAPI exception handler does not provide our request object
|
538
591
|
# it seems like it is rebuild from the asgi scope. Even the router
|
539
592
|
# class is wrong.
|
@@ -544,18 +597,11 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
544
597
|
if isinstance(resp, Response):
|
545
598
|
return resp
|
546
599
|
|
547
|
-
|
548
|
-
|
549
|
-
"No template set for "
|
550
|
-
f"{exc.__module__}:{exc.__class__.__qualname__} but "
|
551
|
-
f"{handler.__module__}:{handler.__qualname__} "
|
552
|
-
"did not return a Response"
|
553
|
-
)
|
554
|
-
|
555
|
-
return req.registry.get_renderer(template)(req).render(
|
556
|
-
template,
|
600
|
+
return req.registry.get_renderer(resp.renderer)(req).render(
|
601
|
+
resp.template,
|
557
602
|
params=resp,
|
558
603
|
status_code=status_code,
|
604
|
+
globals=(await self._build_renderer_globals(req)),
|
559
605
|
)
|
560
606
|
|
561
607
|
self.exception_handlers.append((status_code_or_exc, exception_handler))
|
fastlife/config/exceptions.py
CHANGED
@@ -11,7 +11,6 @@ from .configurator import VENUSIAN_CATEGORY, Configurator
|
|
11
11
|
def exception_handler(
|
12
12
|
exception: type[Exception],
|
13
13
|
*,
|
14
|
-
template: str | None = None,
|
15
14
|
status_code: int | None = None,
|
16
15
|
) -> Callable[..., Any]:
|
17
16
|
"""
|
@@ -34,7 +33,6 @@ def exception_handler(
|
|
34
33
|
config.add_exception_handler(
|
35
34
|
exception,
|
36
35
|
wrapped,
|
37
|
-
template=template,
|
38
36
|
**({} if status_code is None else {"status_code": status_code}),
|
39
37
|
)
|
40
38
|
|
fastlife/config/views.py
CHANGED
@@ -31,7 +31,6 @@ def view_config(
|
|
31
31
|
path: str,
|
32
32
|
*,
|
33
33
|
permission: str | None = None,
|
34
|
-
template: str | None = None,
|
35
34
|
status_code: int | None = None,
|
36
35
|
methods: list[str] | None = None,
|
37
36
|
) -> Callable[..., Any]:
|
@@ -72,7 +71,6 @@ def view_config(
|
|
72
71
|
permission=permission,
|
73
72
|
status_code=status_code,
|
74
73
|
methods=methods,
|
75
|
-
template=template,
|
76
74
|
)
|
77
75
|
|
78
76
|
venusian.attach(wrapped, callback, category=VENUSIAN_CATEGORY) # type: ignore
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Framework core domain."""
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Core Domain Classes."""
|
@@ -0,0 +1,19 @@
|
|
1
|
+
"""Models relative to the security."""
|
2
|
+
|
3
|
+
import secrets
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
|
8
|
+
def create_csrf_token() -> str:
|
9
|
+
"""A helper that create a csrf token."""
|
10
|
+
return secrets.token_urlsafe(5)
|
11
|
+
|
12
|
+
|
13
|
+
class CSRFToken(BaseModel):
|
14
|
+
"""Represent the CSRF Token"""
|
15
|
+
|
16
|
+
name: str
|
17
|
+
"""Name of the token while serialized."""
|
18
|
+
value: str
|
19
|
+
"""Value that must match between parts, cookie and posted form."""
|
@@ -0,0 +1,30 @@
|
|
1
|
+
"""Inline templates."""
|
2
|
+
|
3
|
+
from typing import ClassVar
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
from pydantic.config import ConfigDict
|
7
|
+
|
8
|
+
|
9
|
+
class InlineTemplate(BaseModel):
|
10
|
+
"""
|
11
|
+
Inline templates are used to encourage the location of behavior and the view typing.
|
12
|
+
|
13
|
+
Pages produce templates that are not reusable and don't need to be reusable
|
14
|
+
in there essence, they don't need to be in a component library.
|
15
|
+
They use a component lirary to stay small but contains a view logic
|
16
|
+
tighly coupled with the view and its code can stay in the same module of that view.
|
17
|
+
"""
|
18
|
+
|
19
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
20
|
+
|
21
|
+
template: ClassVar[str]
|
22
|
+
"""The template string to render."""
|
23
|
+
renderer: ClassVar[str]
|
24
|
+
"""Template render engine to use."""
|
25
|
+
|
26
|
+
|
27
|
+
class JinjaXTemplate(InlineTemplate):
|
28
|
+
"""Template that render JinjaX"""
|
29
|
+
|
30
|
+
renderer = ".jinja"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"""Types that are serialized over HTTP and forms."""
|
2
|
+
|
3
|
+
from decimal import Decimal
|
4
|
+
from typing import Any
|
5
|
+
from uuid import UUID
|
6
|
+
|
7
|
+
from pydantic.networks import EmailStr
|
8
|
+
|
9
|
+
Builtins = str | int | str | float | Decimal | UUID | EmailStr
|
10
|
+
"""Builtins types."""
|
11
|
+
|
12
|
+
|
13
|
+
AnyLiteral = Any
|
14
|
+
"""
|
15
|
+
Something like Literal[...] or Literal[...] which does not exists
|
16
|
+
or Idon't know where it is hidden.
|
17
|
+
"""
|
fastlife/request/request.py
CHANGED
@@ -6,6 +6,7 @@ from fastapi import Request as FastAPIRequest
|
|
6
6
|
from fastapi.params import Depends
|
7
7
|
|
8
8
|
from fastlife.config.registry import DefaultRegistry, TRegistry
|
9
|
+
from fastlife.domain.model.security import CSRFToken, create_csrf_token
|
9
10
|
|
10
11
|
if TYPE_CHECKING:
|
11
12
|
from fastlife.security.policy import ( # coverage: ignore
|
@@ -25,11 +26,29 @@ class GenericRequest(FastAPIRequest, Generic[TRegistry]):
|
|
25
26
|
security_policy: "AbstractSecurityPolicy[Any, TRegistry] | None"
|
26
27
|
"""Request locale used for the i18n of the response."""
|
27
28
|
|
29
|
+
renderer_globals: dict[str, Any]
|
30
|
+
|
28
31
|
def __init__(self, registry: TRegistry, request: FastAPIRequest) -> None:
|
29
32
|
super().__init__(request.scope, request.receive)
|
30
33
|
self.registry = registry
|
31
34
|
self.locale_name = registry.locale_negociator(self)
|
32
35
|
self.security_policy = None # build it from the ? registry
|
36
|
+
self.renderer_globals = {}
|
37
|
+
self._csrf_token: CSRFToken | None = None
|
38
|
+
|
39
|
+
@property
|
40
|
+
def csrf_token(self) -> CSRFToken:
|
41
|
+
if self._csrf_token is None:
|
42
|
+
name = self.registry.settings.csrf_token_name
|
43
|
+
value = self.cookies.get(name) or create_csrf_token()
|
44
|
+
self._csrf_token = CSRFToken(name=name, value=value)
|
45
|
+
return self._csrf_token
|
46
|
+
|
47
|
+
def add_renderer_globals(self, **kwargs: Any) -> None:
|
48
|
+
"""
|
49
|
+
Add global variables to the template renderer context for the current request.
|
50
|
+
"""
|
51
|
+
self.renderer_globals.update(kwargs)
|
33
52
|
|
34
53
|
async def has_permission(
|
35
54
|
self, permission: str
|
fastlife/security/csrf.py
CHANGED
@@ -18,7 +18,6 @@ no way to prevent to set the cookie in the request.
|
|
18
18
|
|
19
19
|
"""
|
20
20
|
|
21
|
-
import secrets
|
22
21
|
from collections.abc import Callable, Coroutine
|
23
22
|
from typing import Any
|
24
23
|
|
@@ -31,11 +30,6 @@ class CSRFAttack(Exception):
|
|
31
30
|
"""
|
32
31
|
|
33
32
|
|
34
|
-
def create_csrf_token() -> str:
|
35
|
-
"""A helper that create a csrf token."""
|
36
|
-
return secrets.token_urlsafe(5)
|
37
|
-
|
38
|
-
|
39
33
|
def check_csrf() -> Callable[[Request], Coroutine[Any, Any, bool]]:
|
40
34
|
"""
|
41
35
|
A global application dependency, that is always active.
|
@@ -56,15 +50,11 @@ def check_csrf() -> Callable[[Request], Coroutine[Any, Any, bool]]:
|
|
56
50
|
!= "application/x-www-form-urlencoded"
|
57
51
|
):
|
58
52
|
return True
|
59
|
-
csrf_token_name = request.registry.settings.csrf_token_name
|
60
|
-
|
61
|
-
cookie = request.cookies.get(csrf_token_name)
|
62
|
-
if not cookie:
|
63
|
-
raise CSRFAttack("CSRF token did not match")
|
64
53
|
|
54
|
+
token = request.csrf_token
|
65
55
|
form_data = await request.form()
|
66
|
-
value = form_data.get(
|
67
|
-
if value !=
|
56
|
+
value = form_data.get(token.name)
|
57
|
+
if value != token.value:
|
68
58
|
raise CSRFAttack("CSRF token did not match")
|
69
59
|
|
70
60
|
return True
|
fastlife/services/templates.py
CHANGED
@@ -9,11 +9,10 @@ More template engine can be registered using the configurator method
|
|
9
9
|
"""
|
10
10
|
|
11
11
|
import abc
|
12
|
-
from collections.abc import
|
12
|
+
from collections.abc import Mapping
|
13
13
|
from typing import Any
|
14
14
|
|
15
15
|
from fastlife import Request, Response
|
16
|
-
from fastlife.security.csrf import create_csrf_token
|
17
16
|
from fastlife.templates.inline import InlineTemplate
|
18
17
|
|
19
18
|
TemplateParams = Mapping[str, Any]
|
@@ -30,6 +29,7 @@ class AbstractTemplateRenderer(abc.ABC):
|
|
30
29
|
|
31
30
|
def __init__(self, request: Request) -> None:
|
32
31
|
self.request = request
|
32
|
+
self.globals: dict[str, Any] = {}
|
33
33
|
|
34
34
|
@property
|
35
35
|
def route_prefix(self) -> str:
|
@@ -43,27 +43,21 @@ class AbstractTemplateRenderer(abc.ABC):
|
|
43
43
|
status_code: int = 200,
|
44
44
|
content_type: str = "text/html",
|
45
45
|
globals: Mapping[str, Any] | None = None,
|
46
|
-
params:
|
47
|
-
_create_csrf_token: Callable[..., str] = create_csrf_token,
|
46
|
+
params: InlineTemplate,
|
48
47
|
) -> Response:
|
49
48
|
"""
|
50
49
|
Render the template and build the HTTP Response.
|
51
50
|
"""
|
52
51
|
request = self.request
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
)
|
57
|
-
if isinstance(params, InlineTemplate):
|
58
|
-
data = self.render_inline(params)
|
59
|
-
else:
|
60
|
-
data = self.render_template(template, **params)
|
52
|
+
if globals:
|
53
|
+
self.globals.update(globals)
|
54
|
+
data = self.render_template(params)
|
61
55
|
resp = Response(
|
62
56
|
data, status_code=status_code, headers={"Content-Type": content_type}
|
63
57
|
)
|
64
58
|
resp.set_cookie(
|
65
|
-
|
66
|
-
request.
|
59
|
+
request.csrf_token.name,
|
60
|
+
request.csrf_token.value,
|
67
61
|
secure=request.url.scheme == "https",
|
68
62
|
samesite="strict",
|
69
63
|
max_age=60 * 15,
|
@@ -71,34 +65,7 @@ class AbstractTemplateRenderer(abc.ABC):
|
|
71
65
|
return resp
|
72
66
|
|
73
67
|
@abc.abstractmethod
|
74
|
-
def render_template(
|
75
|
-
self,
|
76
|
-
template: str,
|
77
|
-
*,
|
78
|
-
globals: Mapping[str, Any] | None = None,
|
79
|
-
**params: Any,
|
80
|
-
) -> str:
|
81
|
-
"""
|
82
|
-
Render the given template with the given params.
|
83
|
-
|
84
|
-
While rendering templates, the globals parameter is keps by the instantiated
|
85
|
-
renderer and sent to every rendering made by the request.
|
86
|
-
This is used by the pydantic form method that will render other templates
|
87
|
-
for the request.
|
88
|
-
In traditional frameworks, only one template is rendered containing the whole
|
89
|
-
pages. But, while rendering a pydantic form, every field is rendered in its
|
90
|
-
distinct template. The template renderer keep the globals and git it back
|
91
|
-
to every templates. This can be used to fillout options in a select without
|
92
|
-
performing an ajax request for example.
|
93
|
-
|
94
|
-
:param template: name of the template to render.
|
95
|
-
:param globals: some variable that will be passed to all rendered templates.
|
96
|
-
:param params: paramaters that are limited to the main rendered templates.
|
97
|
-
:return: The template rendering result.
|
98
|
-
"""
|
99
|
-
|
100
|
-
@abc.abstractmethod
|
101
|
-
def render_inline(self, template: InlineTemplate) -> str:
|
68
|
+
def render_template(self, template: InlineTemplate) -> str:
|
102
69
|
"""
|
103
70
|
Render an inline template.
|
104
71
|
|
@@ -117,6 +117,18 @@ class Localizer:
|
|
117
117
|
ret = ret.format(**mapping)
|
118
118
|
return ret
|
119
119
|
|
120
|
+
def npgettext(
|
121
|
+
self,
|
122
|
+
context: str,
|
123
|
+
singular: str,
|
124
|
+
plural: str,
|
125
|
+
n: int,
|
126
|
+
mapping: dict[str, str] | None = None,
|
127
|
+
) -> str:
|
128
|
+
ret = self.global_translations.npgettext(context, singular, plural, n)
|
129
|
+
mapping_num = {"num": n, **(mapping or {})}
|
130
|
+
return ret.format(**mapping_num)
|
131
|
+
|
120
132
|
def dnpgettext(
|
121
133
|
self,
|
122
134
|
domain: str,
|
fastlife/templates/__init__.py
CHANGED
@@ -2,11 +2,6 @@
|
|
2
2
|
Utilities for rendering HTML templates for page and components as FastAPI dependencies.
|
3
3
|
"""
|
4
4
|
|
5
|
-
from .binding import Template, template
|
6
5
|
from .inline import InlineTemplate
|
7
6
|
|
8
|
-
__all__ = [
|
9
|
-
"Template",
|
10
|
-
"template",
|
11
|
-
"InlineTemplate",
|
12
|
-
]
|
7
|
+
__all__ = ["InlineTemplate"]
|
fastlife/templates/inline.py
CHANGED
@@ -1,22 +1,26 @@
|
|
1
1
|
"""Inline templates."""
|
2
2
|
|
3
|
-
|
3
|
+
import inspect
|
4
|
+
from collections.abc import Callable
|
5
|
+
from typing import Any, get_args
|
4
6
|
|
5
|
-
from
|
6
|
-
from
|
7
|
+
from fastlife.domain.model.template import InlineTemplate
|
8
|
+
from fastlife.shared_utils.infer import is_union
|
7
9
|
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
11
|
+
def is_inline_template_returned(endpoint: Callable[..., Any]) -> bool:
|
12
|
+
signature = inspect.signature(endpoint)
|
13
|
+
return_annotation = signature.return_annotation
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
"""
|
15
|
+
if isinstance(return_annotation, type) and issubclass(
|
16
|
+
return_annotation, InlineTemplate
|
17
|
+
):
|
18
|
+
return True
|
18
19
|
|
19
|
-
|
20
|
+
if is_union(return_annotation):
|
21
|
+
return any(
|
22
|
+
isinstance(arg, type) and issubclass(arg, InlineTemplate)
|
23
|
+
for arg in get_args(return_annotation)
|
24
|
+
)
|
20
25
|
|
21
|
-
|
22
|
-
"""The template string to render."""
|
26
|
+
return False
|