fastlifeweb 0.20.1__py3-none-any.whl → 0.22.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 +18 -1
- fastlife/__init__.py +45 -13
- fastlife/adapters/__init__.py +1 -1
- fastlife/adapters/fastapi/__init__.py +9 -0
- fastlife/adapters/fastapi/form.py +26 -0
- fastlife/{request → adapters/fastapi}/form_data.py +1 -1
- fastlife/{request → adapters/fastapi}/localizer.py +4 -2
- fastlife/adapters/fastapi/request.py +33 -0
- fastlife/{routing → adapters/fastapi/routing}/route.py +3 -3
- fastlife/{routing → adapters/fastapi/routing}/router.py +1 -1
- fastlife/adapters/itsdangerous/__init__.py +3 -0
- fastlife/adapters/itsdangerous/session.py +50 -0
- fastlife/adapters/jinjax/jinjax_ext/inspectable_component.py +7 -7
- fastlife/adapters/jinjax/jinjax_ext/jinjax_doc.py +1 -1
- fastlife/adapters/jinjax/renderer.py +9 -57
- 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 +32 -23
- 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 +36 -36
- 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 +36 -43
- fastlife/adapters/jinjax/widgets/sequence.py +63 -42
- fastlife/adapters/jinjax/widgets/text.py +39 -78
- fastlife/adapters/jinjax/widgets/union.py +51 -58
- fastlife/components/CsrfToken.jinja +1 -1
- fastlife/components/Form.jinja +1 -1
- fastlife/components/pydantic_form/FatalError.jinja +8 -0
- fastlife/components/pydantic_form/Widget.jinja +4 -3
- fastlife/config/__init__.py +3 -6
- fastlife/config/configurator.py +80 -32
- fastlife/config/exceptions.py +0 -2
- fastlife/config/resources.py +1 -2
- fastlife/config/views.py +2 -4
- fastlife/domain/__init__.py +1 -0
- fastlife/domain/model/__init__.py +1 -0
- fastlife/domain/model/asgi.py +3 -0
- fastlife/domain/model/csrf.py +19 -0
- fastlife/{request → domain/model}/form.py +13 -22
- fastlife/{request → domain/model}/request.py +26 -30
- fastlife/domain/model/security_policy.py +105 -0
- fastlife/{templates/inline.py → domain/model/template.py} +8 -0
- fastlife/domain/model/types.py +17 -0
- fastlife/middlewares/base.py +1 -1
- fastlife/middlewares/reverse_proxy/x_forwarded.py +1 -2
- fastlife/middlewares/session/__init__.py +2 -2
- fastlife/middlewares/session/middleware.py +4 -3
- fastlife/middlewares/session/serializer.py +0 -44
- fastlife/{services/policy.py → service/check_permission.py} +1 -1
- fastlife/{security → service}/csrf.py +5 -15
- fastlife/{services → service}/locale_negociator.py +5 -8
- fastlife/{config → service}/registry.py +13 -7
- fastlife/service/security_policy.py +100 -0
- fastlife/{services → service}/templates.py +10 -48
- fastlife/{services → service}/translations.py +15 -0
- fastlife/{config/settings.py → settings.py} +6 -12
- fastlife/shared_utils/infer.py +24 -1
- fastlife/{templates/constants.py → template_globals.py} +2 -2
- fastlife/testing/testclient.py +2 -2
- fastlife/views/__init__.py +1 -0
- fastlife/views/pydantic_form.py +6 -0
- {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/METADATA +1 -1
- {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/RECORD +79 -80
- tailwind.config.js +1 -1
- 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/request/__init__.py +0 -5
- fastlife/security/__init__.py +0 -1
- fastlife/security/policy.py +0 -188
- fastlife/templates/__init__.py +0 -12
- fastlife/templates/binding.py +0 -52
- /fastlife/{routing → adapters/fastapi/routing}/__init__.py +0 -0
- /fastlife/{services → service}/__init__.py +0 -0
- {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/WHEEL +0 -0
- {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/entry_points.txt +0 -0
- {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,52 +2,73 @@ from collections.abc import Sequence
|
|
2
2
|
from typing import Any
|
3
3
|
|
4
4
|
from markupsafe import Markup
|
5
|
+
from pydantic import Field
|
5
6
|
|
6
|
-
from fastlife.
|
7
|
-
|
8
|
-
from .base import TypeWrapper, Widget
|
9
|
-
|
10
|
-
|
11
|
-
class SequenceWidget(Widget[Sequence[
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
7
|
+
from fastlife.service.templates import AbstractTemplateRenderer
|
8
|
+
|
9
|
+
from .base import TWidget, TypeWrapper, Widget
|
10
|
+
|
11
|
+
|
12
|
+
class SequenceWidget(Widget[Sequence[TWidget]]):
|
13
|
+
template = """
|
14
|
+
<pydantic_form.Widget :widget_id="id" :removable="removable">
|
15
|
+
<Details :id="id">
|
16
|
+
<Summary :id="id + '-summary'">
|
17
|
+
<H3 :class="H3_SUMMARY_CLASS">{{title}}</H3>
|
18
|
+
<pydantic_form.Error :text="error" />
|
19
|
+
</Summary>
|
20
|
+
<div>
|
21
|
+
{% set fnGetName = "get" + id.replace("-", "_") %}
|
22
|
+
<script>
|
23
|
+
function {{ fnGetName }} () {
|
24
|
+
const el = document.getElementById("{{id}}-content");
|
25
|
+
const len = el.dataset.length;
|
26
|
+
el.dataset.length = parseInt(len) + 1;
|
27
|
+
return "{{wrapped_type.name}}." + len;
|
28
|
+
}
|
29
|
+
</script>
|
30
|
+
|
31
|
+
<div id="{{id}}-content" class="m-4"
|
32
|
+
data-length="{{children_widgets|length|string}}">
|
33
|
+
{% set container_id = id + "-children-container" %}
|
34
|
+
<div id="{{container_id}}">
|
35
|
+
{% for child in children_widgets %}
|
36
|
+
{{ child }}
|
37
|
+
{% endfor%}
|
38
|
+
</div>
|
39
|
+
</div>
|
40
|
+
|
41
|
+
<div>
|
42
|
+
{% set container_id = "#" + id + "-children-container" %}
|
43
|
+
{% set add_id = id + "-add" %}
|
44
|
+
{% set vals = 'js:{"name": '
|
45
|
+
+ fnGetName
|
46
|
+
+ '(), "token": "'
|
47
|
+
+ wrapped_type.token + '", "removable": true}' %}
|
48
|
+
<Button type="button"
|
49
|
+
:hx-target="container_id" hx-swap="beforeend"
|
50
|
+
:id="add_id"
|
51
|
+
:hx-vals="vals"
|
52
|
+
:hx-get="wrapped_type.url">
|
53
|
+
Add
|
54
|
+
</Button>
|
55
|
+
</div>
|
56
|
+
</div>
|
57
|
+
</Details>
|
58
|
+
</pydantic_form.Widget>
|
59
|
+
"""
|
60
|
+
|
61
|
+
item_type: type[Any]
|
62
|
+
wrapped_type: TypeWrapper | None = Field(default=None)
|
63
|
+
children_widgets: list[str] = Field(default=None)
|
39
64
|
|
40
65
|
def build_item_type(self, route_prefix: str) -> TypeWrapper:
|
41
66
|
return TypeWrapper(self.item_type, route_prefix, self.name, self.token)
|
42
67
|
|
43
68
|
def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
|
44
69
|
"""Return the html version."""
|
45
|
-
|
46
|
-
|
47
|
-
renderer.
|
48
|
-
|
49
|
-
|
50
|
-
type=self.build_item_type(renderer.route_prefix),
|
51
|
-
children_widgets=children,
|
52
|
-
)
|
53
|
-
)
|
70
|
+
self.wrapped_type = self.build_item_type(renderer.route_prefix)
|
71
|
+
self.children_widgets = [
|
72
|
+
Markup(item.to_html(renderer)) for item in self.value or []
|
73
|
+
]
|
74
|
+
return Markup(renderer.render_template(self))
|
@@ -1,52 +1,31 @@
|
|
1
1
|
from collections.abc import Sequence
|
2
2
|
|
3
|
+
from pydantic import Field
|
4
|
+
|
5
|
+
from fastlife.domain.model.types import Builtins
|
6
|
+
|
3
7
|
from .base import Widget
|
4
8
|
|
5
9
|
|
6
|
-
class TextWidget(Widget[
|
10
|
+
class TextWidget(Widget[Builtins]):
|
7
11
|
"""
|
8
12
|
Widget for text like field (email, ...).
|
9
|
-
|
10
|
-
:param name: input name.
|
11
|
-
:param title: title for the widget.
|
12
|
-
:param hint: hint for human.
|
13
|
-
:param aria_label: html input aria-label value.
|
14
|
-
:param placeholder: html input placeholder value.
|
15
|
-
:param error: error of the value if any.
|
16
|
-
:param value: current value.
|
17
|
-
:param removable: display a button to remove the widget for optional fields.
|
18
|
-
:param token: token used to get unique id on the form.
|
19
13
|
"""
|
20
14
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
removable: bool = False,
|
33
|
-
token: str,
|
34
|
-
) -> None:
|
35
|
-
super().__init__(
|
36
|
-
name,
|
37
|
-
value=value,
|
38
|
-
title=title,
|
39
|
-
hint=hint,
|
40
|
-
aria_label=aria_label,
|
41
|
-
token=token,
|
42
|
-
error=error,
|
43
|
-
removable=removable,
|
44
|
-
)
|
45
|
-
self.placeholder = placeholder or ""
|
46
|
-
self.input_type = input_type
|
15
|
+
template = """
|
16
|
+
<pydantic_form.Widget :widget_id="id" :removable="removable">
|
17
|
+
<div class="pt-4">
|
18
|
+
<Label :for="id">{{title}}</Label>
|
19
|
+
<pydantic_form.Error :text="error" />
|
20
|
+
<Input :name="name" :value="value" :type="input_type" :id="id"
|
21
|
+
:aria-label="aria_label" :placeholder="placeholder" />
|
22
|
+
<pydantic_form.Hint :text="hint" />
|
23
|
+
</div>
|
24
|
+
</pydantic_form.Widget>
|
25
|
+
"""
|
47
26
|
|
48
|
-
|
49
|
-
|
27
|
+
input_type: str = Field(default="text")
|
28
|
+
placeholder: str | None = Field(default=None)
|
50
29
|
|
51
30
|
|
52
31
|
class TextareaWidget(Widget[Sequence[str]]):
|
@@ -54,12 +33,13 @@ class TextareaWidget(Widget[Sequence[str]]):
|
|
54
33
|
Render a Textearea for a string or event a sequence of string.
|
55
34
|
|
56
35
|
```
|
36
|
+
from fastlife.adapters.jinjax.widgets.base import CustomWidget
|
57
37
|
from fastlife.adapters.jinjax.widgets.text import TextareaWidget
|
58
38
|
from pydantic import BaseModel, Field, field_validator
|
59
39
|
|
60
40
|
class TaggedParagraphForm(BaseModel):
|
61
|
-
paragraph: Annotated[str, TextareaWidget] = Field(...)
|
62
|
-
tags: Annotated[Sequence[str], TextareaWidget] = Field(
|
41
|
+
paragraph: Annotated[str, CustomWidget(TextareaWidget)] = Field(...)
|
42
|
+
tags: Annotated[Sequence[str], CustomWidget(TextareaWidget)] = Field(
|
63
43
|
default_factory=list,
|
64
44
|
title="Tags",
|
65
45
|
description="One tag per line",
|
@@ -69,43 +49,24 @@ class TextareaWidget(Widget[Sequence[str]]):
|
|
69
49
|
def split(cls, s: Any) -> Sequence[str]:
|
70
50
|
return s.split() if s else []
|
71
51
|
```
|
72
|
-
|
73
|
-
:param name: input name.
|
74
|
-
:param title: title for the widget.
|
75
|
-
:param hint: hint for human.
|
76
|
-
:param aria_label: html input aria-label value.
|
77
|
-
:param placeholder: html input placeholder value.
|
78
|
-
:param error: error of the value if any.
|
79
|
-
:param value: current value.
|
80
|
-
:param removable: display a button to remove the widget for optional fields.
|
81
|
-
:param token: token used to get unique id on the form.
|
82
|
-
|
83
52
|
"""
|
84
53
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
hint=hint,
|
103
|
-
aria_label=aria_label,
|
104
|
-
token=token,
|
105
|
-
error=error,
|
106
|
-
removable=removable,
|
107
|
-
)
|
108
|
-
self.placeholder = placeholder or ""
|
54
|
+
template = """
|
55
|
+
<pydantic_form.Widget :widget_id="id" :removable="removable">
|
56
|
+
<div class="pt-4">
|
57
|
+
<Label :for="id">{{title}}</Label>
|
58
|
+
<pydantic_form.Error :text="error" />
|
59
|
+
<Textarea :name="name" :id="id" :aria-label="aria_label">
|
60
|
+
{%- if v is string -%}
|
61
|
+
{{- v -}}}
|
62
|
+
{%- else -%}
|
63
|
+
{%- for v in value %}{{v}}
|
64
|
+
{% endfor -%}
|
65
|
+
{% endif %}
|
66
|
+
</Textarea>
|
67
|
+
<pydantic_form.Hint :text="hint" />
|
68
|
+
</div>
|
69
|
+
</pydantic_form.Widget>
|
70
|
+
"""
|
109
71
|
|
110
|
-
|
111
|
-
return "pydantic_form.Textarea.jinja"
|
72
|
+
placeholder: str = Field(default="")
|
@@ -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
|
-
from fastlife.
|
11
|
+
from fastlife.service.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" />
|
fastlife/components/Form.jinja
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
Create html ``<form>`` node with htmx support by default.
|
3
3
|
A :jinjax:component:`CsrfToken` will always be included in the form
|
4
4
|
and will be checked by the
|
5
|
-
:func:`csrf policy method <fastlife.
|
5
|
+
:func:`csrf policy method <fastlife.service.csrf.check_csrf>`.
|
6
6
|
|
7
7
|
::
|
8
8
|
|
@@ -0,0 +1,8 @@
|
|
1
|
+
{# doc display an error for a field. #}
|
2
|
+
{# def message: Annotated[str | None, "error message"] #}
|
3
|
+
{%- if message -%}
|
4
|
+
<div class="flex items-center bg-red-50 border border-red-400 text-red-700" role="alert">
|
5
|
+
<icons.Fire class="m-3 w-16 h-16 fill-orange-500" />
|
6
|
+
<span class="sm:inline text-xl">{{ message }}</span>
|
7
|
+
</div>
|
8
|
+
{%- endif -%}
|
@@ -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/__init__.py
CHANGED
@@ -1,19 +1,16 @@
|
|
1
1
|
"""Configure fastlife app for dependency injection."""
|
2
2
|
|
3
3
|
from .configurator import Configurator, GenericConfigurator, configure
|
4
|
-
from .
|
4
|
+
from .exceptions import exception_handler
|
5
5
|
from .resources import resource, resource_view
|
6
|
-
from .settings import Settings
|
7
6
|
from .views import view_config
|
8
7
|
|
9
8
|
__all__ = [
|
10
9
|
"Configurator",
|
11
10
|
"GenericConfigurator",
|
12
11
|
"configure",
|
13
|
-
"
|
12
|
+
"exception_handler",
|
14
13
|
"resource",
|
15
14
|
"resource_view",
|
16
|
-
"
|
17
|
-
"DefaultRegistry",
|
18
|
-
"Settings",
|
15
|
+
"view_config",
|
19
16
|
]
|
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
|
@@ -26,25 +27,29 @@ from fastapi.params import Depends as DependsType
|
|
26
27
|
from fastapi.staticfiles import StaticFiles
|
27
28
|
from fastapi.types import IncEx
|
28
29
|
|
30
|
+
from fastlife.adapters.fastapi.request import GenericRequest, Request
|
31
|
+
from fastlife.adapters.fastapi.routing.route import Route
|
32
|
+
from fastlife.adapters.fastapi.routing.router import Router
|
29
33
|
from fastlife.config.openapiextra import OpenApiTag
|
34
|
+
from fastlife.domain.model.template import InlineTemplate
|
30
35
|
from fastlife.middlewares.base import AbstractMiddleware
|
31
|
-
from fastlife.
|
32
|
-
from fastlife.
|
33
|
-
from fastlife.
|
34
|
-
from fastlife.
|
35
|
-
from fastlife.
|
36
|
-
from fastlife.shared_utils.resolver import
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
from fastlife.service.check_permission import check_permission
|
37
|
+
from fastlife.service.csrf import check_csrf
|
38
|
+
from fastlife.service.registry import DefaultRegistry, TRegistry
|
39
|
+
from fastlife.settings import Settings
|
40
|
+
from fastlife.shared_utils.infer import is_inline_template_returned
|
41
|
+
from fastlife.shared_utils.resolver import (
|
42
|
+
resolve,
|
43
|
+
resolve_maybe_relative,
|
44
|
+
)
|
40
45
|
|
41
46
|
if TYPE_CHECKING:
|
42
|
-
from fastlife.
|
43
|
-
from fastlife.
|
47
|
+
from fastlife.service.security_policy import AbstractSecurityPolicy
|
48
|
+
from fastlife.service.templates import (
|
44
49
|
AbstractTemplateRendererFactory, # coverage: ignore
|
45
50
|
)
|
46
51
|
|
47
|
-
from fastlife.
|
52
|
+
from fastlife.service.locale_negociator import LocaleNegociator
|
48
53
|
|
49
54
|
log = logging.getLogger(__name__)
|
50
55
|
VENUSIAN_CATEGORY = "fastlife"
|
@@ -143,6 +148,8 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
143
148
|
] = {}
|
144
149
|
|
145
150
|
self._registered_permissions: set[str] = set()
|
151
|
+
|
152
|
+
self._renderer_globals: dict[str, Any] = {}
|
146
153
|
self.scanner = venusian.Scanner(fastlife=self)
|
147
154
|
self.include("fastlife.views")
|
148
155
|
self.include("fastlife.middlewares")
|
@@ -441,6 +448,53 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
441
448
|
)
|
442
449
|
return self
|
443
450
|
|
451
|
+
def add_renderer_global(
|
452
|
+
self, name: str, value: Any, *, evaluate: bool = True
|
453
|
+
) -> None:
|
454
|
+
"""
|
455
|
+
Add a rendering global value.
|
456
|
+
|
457
|
+
:param name: the name or key of the global value available in the template.
|
458
|
+
:param value: a value, or a callable or
|
459
|
+
an async function with a request in parameter that will evaluate the value.
|
460
|
+
:param evaluate: set to false if you want to inject helper methods in the
|
461
|
+
template.
|
462
|
+
"""
|
463
|
+
self._renderer_globals[name] = value, evaluate
|
464
|
+
|
465
|
+
async def _build_renderer_globals(self, request: Request) -> dict[str, Any]:
|
466
|
+
"""
|
467
|
+
Build globals variables accessible in any templates.
|
468
|
+
|
469
|
+
* `request` is the {class}`current request <fastlife.request.request.Request>`
|
470
|
+
* `authenticated_user` is used to access to the authenticated user if the
|
471
|
+
security policy has been installed.
|
472
|
+
* `csrf_token` is used to build for {jinjax:component}`CsrfToken`.
|
473
|
+
* `gettext`, `ngettext`, `dgettext`, `dngettext`, `pgettext`, `dpgettext`,
|
474
|
+
`npgettext`, `dnpgettext` methods are installed for i18n purpose.
|
475
|
+
"""
|
476
|
+
lczr = request.registry.localizer(request)
|
477
|
+
custom_globals = {}
|
478
|
+
for key, (val, evaluate) in self._renderer_globals.items():
|
479
|
+
if evaluate and callable(val):
|
480
|
+
val = val(request)
|
481
|
+
if iscoroutine(val):
|
482
|
+
val = await val
|
483
|
+
custom_globals[key] = val
|
484
|
+
return {
|
485
|
+
"request": request,
|
486
|
+
"gettext": lczr.gettext,
|
487
|
+
"ngettext": lczr.ngettext,
|
488
|
+
"dgettext": lczr.dgettext,
|
489
|
+
"dngettext": lczr.dngettext,
|
490
|
+
"pgettext": lczr.pgettext,
|
491
|
+
"dpgettext": lczr.dpgettext,
|
492
|
+
"npgettext": lczr.npgettext,
|
493
|
+
"dnpgettext": lczr.dnpgettext,
|
494
|
+
**custom_globals,
|
495
|
+
**request.renderer_globals,
|
496
|
+
}
|
497
|
+
|
444
498
|
def add_route(
|
445
499
|
self,
|
446
500
|
name: str,
|
@@ -466,10 +520,8 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
466
520
|
:param path: path of the route, use `{curly_brace}` to inject FastAPI Path
|
467
521
|
parameters.
|
468
522
|
:param endpoint: the function that will reveive the request.
|
469
|
-
:param template: the template rendered by the
|
470
|
-
{class}`fastlife.service.templates.AbstractTemplateRenderer`.
|
471
523
|
:param permission: a permission to validate by the
|
472
|
-
{class}`Security Policy <fastlife.
|
524
|
+
{class}`Security Policy <fastlife.service.security_policy.AbstractSecurityPolicy>`.
|
473
525
|
:param status_code: customize response status code.
|
474
526
|
:param methods: restrict route to a list of http methods.
|
475
527
|
:return: the configurator.
|
@@ -479,17 +531,21 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
479
531
|
self._registered_permissions.add(permission)
|
480
532
|
dependencies.append(Depends(check_permission(permission)))
|
481
533
|
|
482
|
-
if
|
534
|
+
if is_inline_template_returned(endpoint):
|
483
535
|
|
484
|
-
def render(
|
536
|
+
async def render(
|
485
537
|
request: Request,
|
486
|
-
resp: Annotated[Response |
|
538
|
+
resp: Annotated["Response | InlineTemplate", Depends(endpoint)],
|
487
539
|
) -> Response:
|
488
540
|
if isinstance(resp, Response):
|
489
541
|
return resp
|
542
|
+
|
543
|
+
template = resp.renderer
|
544
|
+
globs = await self._build_renderer_globals(request)
|
490
545
|
return request.registry.get_renderer(template)(request).render(
|
491
546
|
template,
|
492
547
|
params=resp,
|
548
|
+
globals=globs,
|
493
549
|
)
|
494
550
|
|
495
551
|
endpoint = render
|
@@ -523,9 +579,8 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
523
579
|
def add_exception_handler(
|
524
580
|
self,
|
525
581
|
status_code_or_exc: int | type[Exception],
|
526
|
-
handler:
|
582
|
+
handler: Callable[..., "Response | InlineTemplate"],
|
527
583
|
*,
|
528
|
-
template: str | None = None,
|
529
584
|
status_code: int = 500,
|
530
585
|
) -> Self:
|
531
586
|
"""
|
@@ -533,7 +588,7 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
533
588
|
|
534
589
|
"""
|
535
590
|
|
536
|
-
def exception_handler(request: BaseRequest, exc: Exception) -> Any:
|
591
|
+
async def exception_handler(request: BaseRequest, exc: Exception) -> Any:
|
537
592
|
# FastAPI exception handler does not provide our request object
|
538
593
|
# it seems like it is rebuild from the asgi scope. Even the router
|
539
594
|
# class is wrong.
|
@@ -544,18 +599,11 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
544
599
|
if isinstance(resp, Response):
|
545
600
|
return resp
|
546
601
|
|
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,
|
602
|
+
return req.registry.get_renderer(resp.renderer)(req).render(
|
603
|
+
resp.template,
|
557
604
|
params=resp,
|
558
605
|
status_code=status_code,
|
606
|
+
globals=(await self._build_renderer_globals(req)),
|
559
607
|
)
|
560
608
|
|
561
609
|
self.exception_handlers.append((status_code_or_exc, exception_handler))
|