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
@@ -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,13 +1,14 @@
|
|
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.
|
10
|
+
from fastlife.domain.model.template import JinjaXTemplate
|
11
|
+
from fastlife.service.templates import AbstractTemplateRenderer
|
11
12
|
from fastlife.shared_utils.infer import is_union
|
12
13
|
|
13
14
|
T = TypeVar("T")
|
@@ -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
|
+
"""
|
@@ -3,8 +3,9 @@ Widget for field of type Set.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
from collections.abc import Sequence
|
6
|
+
from typing import Self
|
6
7
|
|
7
|
-
from pydantic import BaseModel, Field
|
8
|
+
from pydantic import BaseModel, Field, model_validator
|
8
9
|
|
9
10
|
from .base import Widget
|
10
11
|
|
@@ -19,52 +20,45 @@ class Checkable(BaseModel):
|
|
19
20
|
checked: bool
|
20
21
|
error: str | None = Field(default=None)
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
id = f"{self.name}-{self.value}-{self.token}"
|
25
|
-
return id.replace(".", "-").replace("_", "-")
|
23
|
+
id: str | None = Field(default=None)
|
24
|
+
field_name: str | None = Field(default=None)
|
26
25
|
|
27
|
-
@
|
28
|
-
def
|
29
|
-
|
26
|
+
@model_validator(mode="after")
|
27
|
+
def fill_props(self) -> Self:
|
28
|
+
self.id = f"{self.name}-{self.value}-{self.token}".replace(".", "-")
|
29
|
+
self.field_name = f"{self.name}[]"
|
30
|
+
return self
|
30
31
|
|
31
32
|
|
32
33
|
class ChecklistWidget(Widget[Sequence[Checkable]]):
|
33
34
|
"""
|
34
35
|
Widget for field of type Set.
|
35
|
-
|
36
|
-
:param name: field name.
|
37
|
-
:param title: title for the widget.
|
38
|
-
:param hint: hint for human.
|
39
|
-
:param aria_label: html input aria-label value.
|
40
|
-
:param value: current value.
|
41
|
-
:param error: error of the value if any.
|
42
|
-
:param removable: display a button to remove the widget for optional fields.
|
43
|
-
:param token: token used to get unique id on the form.
|
44
36
|
"""
|
45
37
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
38
|
+
template = """
|
39
|
+
<pydantic_form.Widget :widget_id="id" :removable="removable">
|
40
|
+
<div class="pt-4">
|
41
|
+
<Details>
|
42
|
+
<Summary :id="id + '-summary'">
|
43
|
+
<H3 :class="H3_SUMMARY_CLASS">{{title}}</H3>
|
44
|
+
<pydantic_form.Error :text="error" />
|
45
|
+
</Summary>
|
46
|
+
<div>
|
47
|
+
{% for value in value %}
|
48
|
+
<div class="flex items-center mb-4">
|
49
|
+
<Checkbox :name="value.field_name" type="checkbox"
|
50
|
+
:id="value.id"
|
51
|
+
:value="value.value"
|
52
|
+
:checked="value.checked" />
|
53
|
+
<Label :for="value.id"
|
54
|
+
class="ms-2 text-base text-neutral-900 dark:text-white">
|
55
|
+
{{- value.label -}}
|
56
|
+
</Label>
|
57
|
+
<pydantic_form.Error :text="value.error" />
|
58
|
+
</div>
|
59
|
+
{% endfor %}
|
60
|
+
</div>
|
61
|
+
</Details>
|
62
|
+
</div>
|
63
|
+
</pydantic_form.Widget>
|
64
|
+
"""
|
@@ -4,53 +4,47 @@ Widget for field of type Enum or Literal.
|
|
4
4
|
|
5
5
|
from collections.abc import Sequence
|
6
6
|
|
7
|
+
from pydantic import Field, field_validator
|
8
|
+
|
7
9
|
from .base import Widget
|
8
10
|
|
9
11
|
|
10
12
|
class DropDownWidget(Widget[str]):
|
11
13
|
"""
|
12
14
|
Widget for field of type Enum or Literal.
|
15
|
+
"""
|
13
16
|
|
14
|
-
|
15
|
-
:
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
template = """
|
18
|
+
<pydantic_form.Widget :widget_id="id" :removable="removable">
|
19
|
+
<div class="pt-4">
|
20
|
+
<Label :for="id">{{title}}</Label>
|
21
|
+
<Select :name="name" :id="id">
|
22
|
+
{%- for opt in options -%}
|
23
|
+
<Option :value="opt.value" id={{id + "-" + opt.value.replace(" ", " -")}}
|
24
|
+
:selected="value==opt.value">
|
25
|
+
{{- opt.text -}}
|
26
|
+
</Option>
|
27
|
+
{%- endfor -%}
|
28
|
+
</Select>
|
29
|
+
<pydantic_form.Error :text="error" />
|
30
|
+
<pydantic_form.Hint :text="hint" />
|
31
|
+
</div>
|
32
|
+
</pydantic_form.Widget>
|
23
33
|
"""
|
24
34
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
removable: bool = False,
|
36
|
-
token: str | None = None,
|
37
|
-
) -> None:
|
38
|
-
super().__init__(
|
39
|
-
name,
|
40
|
-
value=value,
|
41
|
-
error=error,
|
42
|
-
title=title,
|
43
|
-
token=token,
|
44
|
-
removable=removable,
|
45
|
-
hint=hint,
|
46
|
-
aria_label=aria_label,
|
47
|
-
)
|
48
|
-
self.options: list[dict[str, str]] = []
|
35
|
+
options: list[dict[str | int, str | int]] = Field(default_factory=list)
|
36
|
+
|
37
|
+
@field_validator("options", mode="before")
|
38
|
+
@classmethod
|
39
|
+
def validate_options(
|
40
|
+
cls, options: Sequence[str | int | tuple[str | int, str | int]] | None
|
41
|
+
) -> Sequence[dict[str | int, str | int]]:
|
42
|
+
if not options:
|
43
|
+
return []
|
44
|
+
ret: list[dict[str | int, str | int]] = []
|
49
45
|
for opt in options:
|
50
46
|
if isinstance(opt, tuple):
|
51
|
-
|
47
|
+
ret.append({"value": opt[0], "text": opt[1]})
|
52
48
|
else:
|
53
|
-
|
54
|
-
|
55
|
-
def get_template(self) -> str:
|
56
|
-
return "pydantic_form.Dropdown.jinja"
|
49
|
+
ret.append({"value": opt, "text": opt})
|
50
|
+
return ret
|
@@ -1,33 +1,25 @@
|
|
1
1
|
"""Hidden fields"""
|
2
2
|
|
3
|
-
from
|
3
|
+
from fastlife.domain.model.types import Builtins
|
4
4
|
|
5
5
|
from .base import Widget
|
6
6
|
|
7
7
|
|
8
|
-
class HiddenWidget(Widget[
|
8
|
+
class HiddenWidget(Widget[Builtins]):
|
9
9
|
'''
|
10
10
|
Widget to annotate to display a field as an hidden field.
|
11
11
|
|
12
12
|
::
|
13
13
|
from pydantic import BaseModel
|
14
|
+
from fastlife.adapters.jinjax.widgets.base import CustomWidget
|
14
15
|
from fastlife.adapters.jinjax.widgets.hidden import HiddenWidget
|
15
16
|
|
16
17
|
class MyForm(BaseModel):
|
17
|
-
id: Annotated[str, HiddenWidget] = Field(...)
|
18
|
+
id: Annotated[str, CustomWidget(HiddenWidget)] = Field(...)
|
18
19
|
"""Identifier in the database."""
|
19
20
|
|
20
21
|
'''
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
*,
|
26
|
-
value: str,
|
27
|
-
token: str,
|
28
|
-
**kwargs: Any,
|
29
|
-
) -> None:
|
30
|
-
super().__init__(name, value=value, token=token)
|
31
|
-
|
32
|
-
def get_template(self) -> str:
|
33
|
-
return "pydantic_form.Hidden.jinja"
|
23
|
+
template = """
|
24
|
+
<Hidden :name="name" :value="value" :id="id" />
|
25
|
+
"""
|
@@ -1,51 +1,44 @@
|
|
1
1
|
"""Pydantic models"""
|
2
2
|
|
3
3
|
from collections.abc import Sequence
|
4
|
-
from typing import Any
|
5
4
|
|
6
5
|
from markupsafe import Markup
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
6
|
+
from pydantic import Field
|
7
|
+
|
8
|
+
from fastlife.service.templates import AbstractTemplateRenderer
|
9
|
+
|
10
|
+
from .base import TWidget, Widget
|
11
|
+
|
12
|
+
|
13
|
+
class ModelWidget(Widget[Sequence[TWidget]]):
|
14
|
+
template = """
|
15
|
+
<pydantic_form.Widget :widget_id="id" :removable="removable">
|
16
|
+
<div id="{{id}}"{% if nested %} class="m-4"{%endif%}>
|
17
|
+
{% if nested %}
|
18
|
+
<Details>
|
19
|
+
<Summary :id="id + '-summary'">
|
20
|
+
<H3 :class="H3_SUMMARY_CLASS">{{title}}</H3>
|
21
|
+
<pydantic_form.Error :text="error" />
|
22
|
+
</Summary>
|
23
|
+
<div>
|
24
|
+
{% for child in children_widgets %}
|
25
|
+
{{ child }}
|
26
|
+
{% endfor %}
|
27
|
+
</div>
|
28
|
+
</Details>
|
29
|
+
{% else %}
|
30
|
+
{% for child in children_widgets %}
|
31
|
+
{{ child }}
|
32
|
+
{% endfor %}
|
33
|
+
{% endif %}
|
34
|
+
</div>
|
35
|
+
</pydantic_form.Widget>
|
36
|
+
"""
|
37
|
+
|
38
|
+
nested: bool = Field(default=False)
|
39
|
+
children_widgets: list[str] | None = Field(default=None)
|
41
40
|
|
42
41
|
def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
|
43
42
|
"""Return the html version."""
|
44
|
-
|
45
|
-
|
46
|
-
"widget": self,
|
47
|
-
"children_widget": children_widget,
|
48
|
-
}
|
49
|
-
return Markup(
|
50
|
-
renderer.render_template(self.get_template(), globals=None, **kwargs)
|
51
|
-
)
|
43
|
+
self.children_widgets = [child.to_html(renderer) for child in self.value or []]
|
44
|
+
return Markup(renderer.render_template(self))
|