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
CHANGELOG.md
CHANGED
@@ -1,4 +1,14 @@
|
|
1
|
-
## 0.
|
1
|
+
## 0.21.0 - Released on 2024-11-15
|
2
|
+
* Make the InlineTemplate the only way to render views template.
|
3
|
+
* Breaking change: template args is not supported in Configutor.add_route.
|
4
|
+
* Breaking change: template args is not supported in @view_config.
|
5
|
+
* Breaking change: template and Template dedendencies have been removed.
|
6
|
+
* Add new method in the configurator to register global vars for template:
|
7
|
+
{meth}`fastlife.config.configurator.GenericConfigurator.add_renderer_global`.
|
8
|
+
* Add npgettext i18n helper method support.
|
9
|
+
* Remove babel from dependency list (only a dev dependency).
|
10
|
+
|
11
|
+
## 0.20.1 - Released on 2024-11-09
|
2
12
|
* Add a new class GenericRegistry in order to properly type custom Configurator / Registry / Settings
|
3
13
|
* Using InlineTemplate, we can pass arbitrary types for pydantic form
|
4
14
|
|
fastlife/__init__.py
CHANGED
@@ -3,6 +3,7 @@ from importlib import metadata
|
|
3
3
|
__version__ = metadata.version("fastlifeweb")
|
4
4
|
|
5
5
|
from fastapi import Response
|
6
|
+
from fastapi.responses import RedirectResponse
|
6
7
|
|
7
8
|
from .config import (
|
8
9
|
Configurator,
|
@@ -15,6 +16,7 @@ from .config import (
|
|
15
16
|
resource_view,
|
16
17
|
view_config,
|
17
18
|
)
|
19
|
+
from .domain.model.template import JinjaXTemplate
|
18
20
|
from .request import GenericRequest, Registry, Request, get_request
|
19
21
|
|
20
22
|
# from .request.form_data import model
|
@@ -39,4 +41,7 @@ __all__ = [
|
|
39
41
|
"get_request",
|
40
42
|
"Registry",
|
41
43
|
"Response",
|
44
|
+
"RedirectResponse",
|
45
|
+
# Template
|
46
|
+
"JinjaXTemplate",
|
42
47
|
]
|
@@ -4,7 +4,7 @@ Template rending based on JinjaX.
|
|
4
4
|
|
5
5
|
import logging
|
6
6
|
import textwrap
|
7
|
-
from collections.abc import
|
7
|
+
from collections.abc import Sequence
|
8
8
|
from typing import (
|
9
9
|
TYPE_CHECKING,
|
10
10
|
Any,
|
@@ -66,58 +66,10 @@ class JinjaxRenderer(AbstractTemplateRenderer):
|
|
66
66
|
super().__init__(request)
|
67
67
|
self.catalog = catalog
|
68
68
|
self.settings = request.registry.settings
|
69
|
-
self.globals: MutableMapping[str, Any] = {}
|
70
69
|
self.translations = get_localizer(request)
|
70
|
+
self.globals["pydantic_form"] = self.pydantic_form
|
71
71
|
|
72
|
-
def
|
73
|
-
"""
|
74
|
-
Build globals variables accessible in any templates.
|
75
|
-
|
76
|
-
* `request` is the {class}`current request <fastlife.request.request.Request>`
|
77
|
-
* `csrf_token` is used to build for {jinjax:component}`CsrfToken`.
|
78
|
-
"""
|
79
|
-
settings = self.settings
|
80
|
-
ret = {
|
81
|
-
"request": self.request,
|
82
|
-
"csrf_token": {
|
83
|
-
"name": settings.csrf_token_name,
|
84
|
-
"value": self.request.scope.get(settings.csrf_token_name, ""),
|
85
|
-
},
|
86
|
-
"pydantic_form": self.pydantic_form,
|
87
|
-
"localizer": self.translations,
|
88
|
-
**self.globals,
|
89
|
-
}
|
90
|
-
return ret
|
91
|
-
|
92
|
-
def render_template(
|
93
|
-
self,
|
94
|
-
template: str,
|
95
|
-
*,
|
96
|
-
globals: Mapping[str, Any] | None = None,
|
97
|
-
**params: Any,
|
98
|
-
) -> str:
|
99
|
-
"""
|
100
|
-
Render the JinjaX component with the given parameter.
|
101
|
-
|
102
|
-
:param template: the template to render
|
103
|
-
:param globals: parameters that will be used by the JinjaX component and all its
|
104
|
-
child components without "props drilling".
|
105
|
-
:param params: parameters used to render the template.
|
106
|
-
"""
|
107
|
-
|
108
|
-
template = template[: -len(self.settings.jinjax_file_ext) - 1]
|
109
|
-
if globals:
|
110
|
-
self.globals.update(globals)
|
111
|
-
|
112
|
-
self.catalog.jinja_env.install_gettext_translations( # type: ignore
|
113
|
-
self.translations, newstyle=True
|
114
|
-
)
|
115
|
-
|
116
|
-
return self.catalog.render( # type: ignore
|
117
|
-
template, __globals=self.build_globals(), **params
|
118
|
-
)
|
119
|
-
|
120
|
-
def render_inline(self, template: InlineTemplate) -> str:
|
72
|
+
def render_template(self, template: InlineTemplate) -> str:
|
121
73
|
"""
|
122
74
|
Render the JinjaX component with the given parameter.
|
123
75
|
|
@@ -134,7 +86,7 @@ class JinjaxRenderer(AbstractTemplateRenderer):
|
|
134
86
|
return self.catalog.render(
|
135
87
|
template.__class__.__qualname__,
|
136
88
|
__source=src,
|
137
|
-
__globals=self.
|
89
|
+
__globals=self.globals,
|
138
90
|
**params,
|
139
91
|
)
|
140
92
|
|
@@ -28,9 +28,9 @@ class BoolBuilder(BaseWidgetBuilder[bool]):
|
|
28
28
|
) -> BooleanWidget:
|
29
29
|
"""Build the widget."""
|
30
30
|
return BooleanWidget(
|
31
|
-
field_name,
|
31
|
+
name=field_name,
|
32
32
|
removable=removable,
|
33
|
-
title=field.title if field else "",
|
33
|
+
title=field.title or "" if field else "",
|
34
34
|
hint=field.description if field else None,
|
35
35
|
aria_label=(
|
36
36
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -8,9 +8,10 @@ from pydantic.networks import EmailStr
|
|
8
8
|
|
9
9
|
from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
|
10
10
|
from fastlife.adapters.jinjax.widgets.text import TextWidget
|
11
|
+
from fastlife.domain.model.types import Builtins
|
11
12
|
|
12
13
|
|
13
|
-
class EmailStrBuilder(BaseWidgetBuilder[
|
14
|
+
class EmailStrBuilder(BaseWidgetBuilder[Builtins]):
|
14
15
|
"""Builder for Pydantic EmailStr."""
|
15
16
|
|
16
17
|
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
@@ -23,17 +24,17 @@ class EmailStrBuilder(BaseWidgetBuilder[EmailStr]):
|
|
23
24
|
field_name: str,
|
24
25
|
field_type: type[Any],
|
25
26
|
field: FieldInfo | None,
|
26
|
-
value:
|
27
|
+
value: Builtins | None,
|
27
28
|
form_errors: Mapping[str, Any],
|
28
29
|
removable: bool,
|
29
30
|
) -> TextWidget:
|
30
31
|
"""Build the widget."""
|
31
32
|
return TextWidget(
|
32
|
-
field_name,
|
33
|
+
name=field_name,
|
33
34
|
input_type="email",
|
34
35
|
placeholder=str(field.examples[0]) if field and field.examples else None,
|
35
36
|
removable=removable,
|
36
|
-
title=field.title if field else "",
|
37
|
+
title=field.title or "" if field else "",
|
37
38
|
hint=field.description if field else None,
|
38
39
|
aria_label=(
|
39
40
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -31,10 +31,10 @@ class EnumBuilder(BaseWidgetBuilder[Enum]):
|
|
31
31
|
"""Build the widget."""
|
32
32
|
options = [(item.name, item.value) for item in field_type] # type: ignore
|
33
33
|
return DropDownWidget(
|
34
|
-
field_name,
|
34
|
+
name=field_name,
|
35
35
|
options=options, # type: ignore
|
36
36
|
removable=removable,
|
37
|
-
title=field.title if field else "",
|
37
|
+
title=field.title or "" if field else "",
|
38
38
|
hint=field.description if field else None,
|
39
39
|
aria_label=(
|
40
40
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -4,15 +4,16 @@ Create markup for pydantic forms.
|
|
4
4
|
|
5
5
|
import secrets
|
6
6
|
from collections.abc import Mapping
|
7
|
-
from
|
8
|
-
from typing import Any, cast, get_origin
|
7
|
+
from typing import TYPE_CHECKING, Any, get_origin
|
9
8
|
|
10
9
|
from markupsafe import Markup
|
11
10
|
from pydantic.fields import FieldInfo
|
12
11
|
|
13
|
-
from fastlife.adapters.jinjax.widgets.base import Widget
|
12
|
+
from fastlife.adapters.jinjax.widgets.base import CustomWidget, Widget
|
14
13
|
from fastlife.request.form import FormModel
|
15
|
-
|
14
|
+
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
from fastlife.services.templates import AbstractTemplateRenderer
|
16
17
|
|
17
18
|
from .base import BaseWidgetBuilder
|
18
19
|
from .bool_builder import BoolBuilder
|
@@ -35,7 +36,7 @@ class WidgetFactory:
|
|
35
36
|
:param token: reuse a token.
|
36
37
|
"""
|
37
38
|
|
38
|
-
def __init__(self, renderer: AbstractTemplateRenderer, token: str | None = None):
|
39
|
+
def __init__(self, renderer: "AbstractTemplateRenderer", token: str | None = None):
|
39
40
|
self.renderer = renderer
|
40
41
|
self.token = token or secrets.token_urlsafe(4).replace("_", "-")
|
41
42
|
self.builders: list[BaseWidgetBuilder[Any]] = [
|
@@ -131,24 +132,22 @@ class WidgetFactory:
|
|
131
132
|
"""
|
132
133
|
if field and field.metadata:
|
133
134
|
for widget in field.metadata:
|
134
|
-
if
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
if field and field.json_schema_extra
|
146
|
-
else None
|
147
|
-
),
|
148
|
-
token=self.token,
|
149
|
-
error=form_errors.get(name),
|
135
|
+
if isinstance(widget, CustomWidget):
|
136
|
+
ret: Widget[Any] = widget.typ(
|
137
|
+
name=name,
|
138
|
+
value=value,
|
139
|
+
removable=removable,
|
140
|
+
title=field.title or "" if field else "",
|
141
|
+
hint=field.description if field else None,
|
142
|
+
aria_label=(
|
143
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
144
|
+
if field and field.json_schema_extra
|
145
|
+
else None
|
150
146
|
),
|
147
|
+
token=self.token,
|
148
|
+
error=form_errors.get(name),
|
151
149
|
)
|
150
|
+
return ret
|
152
151
|
|
153
152
|
type_origin = get_origin(typ)
|
154
153
|
for builder in self.builders:
|
@@ -8,9 +8,10 @@ from pydantic.fields import FieldInfo
|
|
8
8
|
from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
|
9
9
|
from fastlife.adapters.jinjax.widgets.dropdown import DropDownWidget
|
10
10
|
from fastlife.adapters.jinjax.widgets.hidden import HiddenWidget
|
11
|
+
from fastlife.domain.model.types import AnyLiteral
|
11
12
|
|
12
13
|
|
13
|
-
class LiteralBuilder(BaseWidgetBuilder[
|
14
|
+
class LiteralBuilder(BaseWidgetBuilder[AnyLiteral]):
|
14
15
|
"""Builder for Literal."""
|
15
16
|
|
16
17
|
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
@@ -23,7 +24,7 @@ class LiteralBuilder(BaseWidgetBuilder[str]): # str|int|bool
|
|
23
24
|
field_name: str,
|
24
25
|
field_type: type[Any], # a literal actually
|
25
26
|
field: FieldInfo | None,
|
26
|
-
value:
|
27
|
+
value: AnyLiteral | None,
|
27
28
|
form_errors: Mapping[str, Any],
|
28
29
|
removable: bool,
|
29
30
|
) -> HiddenWidget | DropDownWidget:
|
@@ -31,15 +32,15 @@ class LiteralBuilder(BaseWidgetBuilder[str]): # str|int|bool
|
|
31
32
|
choices: list[str] = field_type.__args__ # type: ignore
|
32
33
|
if len(choices) == 1:
|
33
34
|
return HiddenWidget(
|
34
|
-
field_name,
|
35
|
+
name=field_name,
|
35
36
|
value=choices[0],
|
36
37
|
token=self.factory.token,
|
37
38
|
)
|
38
39
|
return DropDownWidget(
|
39
|
-
field_name,
|
40
|
-
options=choices,
|
40
|
+
name=field_name,
|
41
|
+
options=choices, # type: ignore
|
41
42
|
removable=removable,
|
42
|
-
title=field.title if field else "",
|
43
|
+
title=field.title or "" if field else "",
|
43
44
|
hint=field.description if field else None,
|
44
45
|
aria_label=(
|
45
46
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -47,11 +47,11 @@ class ModelBuilder(BaseWidgetBuilder[Mapping[str, Any]]):
|
|
47
47
|
form_errors=form_errors,
|
48
48
|
removable=False,
|
49
49
|
)
|
50
|
-
return ModelWidget(
|
51
|
-
field_name,
|
50
|
+
return ModelWidget[Any](
|
51
|
+
name=field_name,
|
52
52
|
value=list(ret.values()),
|
53
53
|
removable=removable,
|
54
|
-
title=field.title if field and field.title else "",
|
54
|
+
title=field.title or "" if field and field.title else "",
|
55
55
|
hint=field.description if field else None,
|
56
56
|
aria_label=(
|
57
57
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -30,11 +30,11 @@ class SecretStrBuilder(BaseWidgetBuilder[SecretStr]):
|
|
30
30
|
) -> Widget[SecretStr]:
|
31
31
|
"""Build the widget."""
|
32
32
|
return TextWidget(
|
33
|
-
field_name,
|
33
|
+
name=field_name,
|
34
34
|
input_type="password",
|
35
35
|
placeholder=str(field.examples[0]) if field and field.examples else None,
|
36
36
|
removable=removable,
|
37
|
-
title=field.title if field else "",
|
37
|
+
title=field.title or "" if field else "",
|
38
38
|
hint=field.description if field else None,
|
39
39
|
aria_label=(
|
40
40
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -41,9 +41,9 @@ class SequenceBuilder(BaseWidgetBuilder[Sequence[Any]]):
|
|
41
41
|
)
|
42
42
|
for idx, v in enumerate(value)
|
43
43
|
]
|
44
|
-
return SequenceWidget(
|
45
|
-
field_name,
|
46
|
-
title=field.title if field else "",
|
44
|
+
return SequenceWidget[Any](
|
45
|
+
name=field_name,
|
46
|
+
title=field.title or "" if field else "",
|
47
47
|
hint=field.description if field else None,
|
48
48
|
aria_label=(
|
49
49
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -65,8 +65,8 @@ class SetBuilder(BaseWidgetBuilder[set[Any]]):
|
|
65
65
|
raise NotImplementedError # coverage: ignore
|
66
66
|
|
67
67
|
return ChecklistWidget(
|
68
|
-
field_name,
|
69
|
-
title=field.title if field else "",
|
68
|
+
name=field_name,
|
69
|
+
title=field.title or "" if field else "",
|
70
70
|
hint=field.description if field else None,
|
71
71
|
aria_label=(
|
72
72
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -1,23 +1,22 @@
|
|
1
1
|
"""Handle simple types (str, int, float, ...)."""
|
2
2
|
|
3
3
|
from collections.abc import Mapping
|
4
|
-
from decimal import Decimal
|
5
4
|
from typing import Any
|
6
|
-
from uuid import UUID
|
7
5
|
|
8
6
|
from pydantic.fields import FieldInfo
|
9
7
|
|
10
8
|
from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
|
11
9
|
from fastlife.adapters.jinjax.widgets.base import Widget
|
12
10
|
from fastlife.adapters.jinjax.widgets.text import TextWidget
|
11
|
+
from fastlife.domain.model.types import Builtins
|
13
12
|
|
14
13
|
|
15
|
-
class SimpleTypeBuilder(BaseWidgetBuilder[
|
14
|
+
class SimpleTypeBuilder(BaseWidgetBuilder[Builtins]):
|
16
15
|
"""Builder for simple types."""
|
17
16
|
|
18
17
|
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
19
18
|
"""True for simple types: int, str, float, Decimal, UUID"""
|
20
|
-
return issubclass(typ,
|
19
|
+
return issubclass(typ, Builtins)
|
21
20
|
|
22
21
|
def build(
|
23
22
|
self,
|
@@ -25,15 +24,15 @@ class SimpleTypeBuilder(BaseWidgetBuilder[str | int | str | float | Decimal | UU
|
|
25
24
|
field_name: str,
|
26
25
|
field_type: type[Any],
|
27
26
|
field: FieldInfo | None,
|
28
|
-
value:
|
27
|
+
value: Builtins | None,
|
29
28
|
form_errors: Mapping[str, Any],
|
30
29
|
removable: bool,
|
31
|
-
) -> Widget[
|
30
|
+
) -> Widget[Builtins]:
|
32
31
|
"""Build the widget."""
|
33
32
|
return TextWidget(
|
34
|
-
field_name,
|
33
|
+
name=field_name,
|
35
34
|
placeholder=str(field.examples[0]) if field and field.examples else None,
|
36
|
-
title=field.title if field else "",
|
35
|
+
title=field.title or "" if field else "",
|
37
36
|
hint=field.description if field else None,
|
38
37
|
aria_label=(
|
39
38
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -70,12 +70,12 @@ class UnionBuilder(BaseWidgetBuilder[Any]):
|
|
70
70
|
removable=False,
|
71
71
|
)
|
72
72
|
|
73
|
-
widget = UnionWidget(
|
74
|
-
field_name,
|
73
|
+
widget = UnionWidget[Any](
|
74
|
+
name=field_name,
|
75
75
|
# we assume those types are BaseModel
|
76
76
|
value=child,
|
77
77
|
children_types=types, # type: ignore
|
78
|
-
title=field.title if field else "",
|
78
|
+
title=field.title or "" if field else "",
|
79
79
|
hint=field.description if field else None,
|
80
80
|
aria_label=(
|
81
81
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -1,12 +1,13 @@
|
|
1
1
|
"""Widget base class."""
|
2
2
|
|
3
|
-
import abc
|
4
3
|
import secrets
|
5
4
|
from collections.abc import Mapping
|
6
|
-
from typing import Any, Generic, TypeVar
|
5
|
+
from typing import Any, Generic, Self, TypeVar
|
7
6
|
|
8
7
|
from markupsafe import Markup
|
8
|
+
from pydantic import Field, model_validator
|
9
9
|
|
10
|
+
from fastlife.domain.model.template import JinjaXTemplate
|
10
11
|
from fastlife.services.templates import AbstractTemplateRenderer
|
11
12
|
from fastlife.shared_utils.infer import is_union
|
12
13
|
|
@@ -21,7 +22,7 @@ def get_title(typ: type[Any]) -> str:
|
|
21
22
|
)
|
22
23
|
|
23
24
|
|
24
|
-
class Widget(
|
25
|
+
class Widget(JinjaXTemplate, Generic[T]):
|
25
26
|
"""
|
26
27
|
Base class for widget of pydantic fields.
|
27
28
|
|
@@ -39,48 +40,37 @@ class Widget(abc.ABC, Generic[T]):
|
|
39
40
|
|
40
41
|
name: str
|
41
42
|
"variable name, nested variables have dots."
|
42
|
-
|
43
|
+
id: str = Field(default="")
|
44
|
+
"variable name, nested variables have dots."
|
45
|
+
value: T | None = Field(default=None)
|
43
46
|
"""Value of the field."""
|
44
|
-
title: str
|
47
|
+
title: str = Field(default="")
|
45
48
|
"Human title for the widget."
|
46
|
-
hint: str
|
49
|
+
hint: str | None = Field(default=None)
|
47
50
|
"A help message for the the widget."
|
48
|
-
|
51
|
+
|
52
|
+
error: str | None = Field(default=None)
|
53
|
+
"Error message."
|
54
|
+
|
55
|
+
aria_label: str | None = Field(default=None)
|
49
56
|
"Non visible text alternative."
|
50
|
-
token: str
|
57
|
+
token: str = Field(default="")
|
51
58
|
"unique token to ensure id are unique in the DOM."
|
52
|
-
removable: bool
|
59
|
+
removable: bool = Field(default=False)
|
53
60
|
"Indicate that the widget is removable from the dom."
|
54
61
|
|
55
|
-
|
56
|
-
|
57
|
-
name
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
token: str | None = None,
|
64
|
-
aria_label: str | None = None,
|
65
|
-
removable: bool = False,
|
66
|
-
):
|
67
|
-
self.name = name
|
68
|
-
self.value = value
|
69
|
-
self.error = error
|
70
|
-
self.title = title or name.split(".")[-1]
|
71
|
-
self.hint = hint or ""
|
72
|
-
self.aria_label = aria_label or ""
|
73
|
-
self.token = token or secrets.token_urlsafe(4).replace("_", "-")
|
74
|
-
self.removable = removable
|
75
|
-
self.id = f"{self.name}-{self.token}".replace("_", "-").replace(".", "-")
|
76
|
-
|
77
|
-
@abc.abstractmethod
|
78
|
-
def get_template(self) -> str:
|
79
|
-
"""Get the widget component template."""
|
62
|
+
@model_validator(mode="after")
|
63
|
+
def fill_props(self) -> Self:
|
64
|
+
self.title = self.title or self.name.split(".")[-1]
|
65
|
+
self.token = self.token or secrets.token_urlsafe(4).replace("_", "-")
|
66
|
+
self.id = self.id or f"{self.name}-{self.token}".replace("_", "-").replace(
|
67
|
+
".", "-"
|
68
|
+
)
|
69
|
+
return self
|
80
70
|
|
81
71
|
def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
|
82
72
|
"""Return the html version."""
|
83
|
-
return Markup(renderer.render_template(self
|
73
|
+
return Markup(renderer.render_template(self))
|
84
74
|
|
85
75
|
|
86
76
|
def _get_fullname(typ: type[Any]) -> str:
|
@@ -90,6 +80,16 @@ def _get_fullname(typ: type[Any]) -> str:
|
|
90
80
|
return f"{typ.__module__}:{typ.__name__}"
|
91
81
|
|
92
82
|
|
83
|
+
TWidget = TypeVar("TWidget", bound=Widget[Any])
|
84
|
+
|
85
|
+
|
86
|
+
class CustomWidget(Generic[TWidget]):
|
87
|
+
typ: type[Any]
|
88
|
+
|
89
|
+
def __init__(self, typ: type[TWidget]) -> None:
|
90
|
+
self.typ = typ
|
91
|
+
|
92
|
+
|
93
93
|
class TypeWrapper:
|
94
94
|
"""
|
95
95
|
Wrap children types for union type.
|
@@ -8,39 +8,18 @@ from .base import Widget
|
|
8
8
|
class BooleanWidget(Widget[bool]):
|
9
9
|
"""
|
10
10
|
Widget for field of type bool.
|
11
|
-
|
12
|
-
:param name: field name.
|
13
|
-
:param title: title for the widget.
|
14
|
-
:param hint: hint for human.
|
15
|
-
:param aria_label: html input aria-label value.
|
16
|
-
:param value: current value.
|
17
|
-
:param error: error of the value if any.
|
18
|
-
:param removable: display a button to remove the widget for optional fields.
|
19
|
-
:param token: token used to get unique id on the form.
|
20
11
|
"""
|
21
12
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
name,
|
36
|
-
title=title,
|
37
|
-
hint=hint,
|
38
|
-
aria_label=aria_label,
|
39
|
-
value=value,
|
40
|
-
error=error,
|
41
|
-
removable=removable,
|
42
|
-
token=token,
|
43
|
-
)
|
44
|
-
|
45
|
-
def get_template(self) -> str:
|
46
|
-
return "pydantic_form.Boolean.jinja"
|
13
|
+
template = """
|
14
|
+
<pydantic_form.Widget :widget_id="id" :removable="removable">
|
15
|
+
<div class="pt-4">
|
16
|
+
<div class="flex items-center">
|
17
|
+
<Checkbox :name="name" :id="id" :checked="value" value="1" />
|
18
|
+
<Label :for="id" class="ms-2 text-base text-neutral-900 dark:text-white">
|
19
|
+
{{title|safe}}
|
20
|
+
</Label>
|
21
|
+
</div>
|
22
|
+
<pydantic_form.Error :text="error" />
|
23
|
+
</div>
|
24
|
+
</pydantic_form.Widget>
|
25
|
+
"""
|