fastlifeweb 0.27.0__py3-none-any.whl → 0.28.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 -0
- fastlife/__init__.py +2 -1
- fastlife/adapters/jinjax/renderer.py +8 -0
- fastlife/adapters/jinjax/widgets/union.py +1 -1
- fastlife/adapters/xcomponent/__init__.py +1 -0
- fastlife/adapters/xcomponent/catalog.py +11 -0
- fastlife/adapters/xcomponent/html/__init__.py +7 -0
- fastlife/adapters/xcomponent/html/collapsible.py +76 -0
- fastlife/adapters/xcomponent/html/form.py +437 -0
- fastlife/adapters/xcomponent/html/nav.py +60 -0
- fastlife/adapters/xcomponent/html/table.py +130 -0
- fastlife/adapters/xcomponent/html/text.py +30 -0
- fastlife/adapters/xcomponent/html/title.py +145 -0
- fastlife/adapters/xcomponent/icons/__init__.py +0 -0
- fastlife/adapters/xcomponent/icons/icons.py +93 -0
- fastlife/adapters/xcomponent/pydantic_form/__init__.py +0 -0
- fastlife/adapters/xcomponent/pydantic_form/components.py +121 -0
- fastlife/adapters/xcomponent/pydantic_form/widget_factory/__init__.py +1 -0
- fastlife/adapters/xcomponent/pydantic_form/widget_factory/base.py +40 -0
- fastlife/adapters/xcomponent/pydantic_form/widget_factory/bool_builder.py +45 -0
- fastlife/adapters/xcomponent/pydantic_form/widget_factory/emailstr_builder.py +50 -0
- fastlife/adapters/xcomponent/pydantic_form/widget_factory/enum_builder.py +49 -0
- fastlife/adapters/xcomponent/pydantic_form/widget_factory/factory.py +188 -0
- fastlife/adapters/xcomponent/pydantic_form/widget_factory/literal_builder.py +55 -0
- fastlife/adapters/xcomponent/pydantic_form/widget_factory/model_builder.py +66 -0
- fastlife/adapters/xcomponent/pydantic_form/widget_factory/secretstr_builder.py +48 -0
- fastlife/adapters/xcomponent/pydantic_form/widget_factory/sequence_builder.py +60 -0
- fastlife/adapters/xcomponent/pydantic_form/widget_factory/set_builder.py +85 -0
- fastlife/adapters/xcomponent/pydantic_form/widget_factory/simpletype_builder.py +48 -0
- fastlife/adapters/xcomponent/pydantic_form/widget_factory/union_builder.py +92 -0
- fastlife/adapters/xcomponent/pydantic_form/widgets/__init__.py +1 -0
- fastlife/adapters/xcomponent/pydantic_form/widgets/base.py +140 -0
- fastlife/adapters/xcomponent/pydantic_form/widgets/boolean.py +25 -0
- fastlife/adapters/xcomponent/pydantic_form/widgets/checklist.py +75 -0
- fastlife/adapters/xcomponent/pydantic_form/widgets/dropdown.py +72 -0
- fastlife/adapters/xcomponent/pydantic_form/widgets/hidden.py +25 -0
- fastlife/adapters/xcomponent/pydantic_form/widgets/mfa_code.py +25 -0
- fastlife/adapters/xcomponent/pydantic_form/widgets/model.py +49 -0
- fastlife/adapters/xcomponent/pydantic_form/widgets/sequence.py +74 -0
- fastlife/adapters/xcomponent/pydantic_form/widgets/text.py +121 -0
- fastlife/adapters/xcomponent/pydantic_form/widgets/union.py +81 -0
- fastlife/adapters/xcomponent/renderer.py +130 -0
- fastlife/assets/dist.css +4 -1
- fastlife/components/A.jinja +5 -1
- fastlife/config/configurator.py +7 -8
- fastlife/config/resources.py +9 -3
- fastlife/domain/model/template.py +6 -0
- fastlife/service/csrf.py +1 -1
- fastlife/service/templates.py +44 -2
- fastlife/template_globals.py +3 -0
- fastlife/views/pydantic_form.py +9 -9
- {fastlifeweb-0.27.0.dist-info → fastlifeweb-0.28.0.dist-info}/METADATA +6 -3
- {fastlifeweb-0.27.0.dist-info → fastlifeweb-0.28.0.dist-info}/RECORD +56 -18
- {fastlifeweb-0.27.0.dist-info → fastlifeweb-0.28.0.dist-info}/WHEEL +1 -1
- {fastlifeweb-0.27.0.dist-info → fastlifeweb-0.28.0.dist-info}/entry_points.txt +0 -0
- {fastlifeweb-0.27.0.dist-info → fastlifeweb-0.28.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
"""Handle Enum type."""
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
4
|
+
from enum import Enum
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from pydantic.fields import FieldInfo
|
8
|
+
|
9
|
+
from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
|
10
|
+
BaseWidgetBuilder,
|
11
|
+
)
|
12
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
|
13
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.dropdown import DropDownWidget
|
14
|
+
|
15
|
+
|
16
|
+
class EnumBuilder(BaseWidgetBuilder[Enum]):
|
17
|
+
"""Builder for Enum."""
|
18
|
+
|
19
|
+
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
20
|
+
"""True for Enum."""
|
21
|
+
return issubclass(typ, Enum)
|
22
|
+
|
23
|
+
def build(
|
24
|
+
self,
|
25
|
+
*,
|
26
|
+
field_name: str,
|
27
|
+
field_type: type[Any], # an enum subclass
|
28
|
+
field: FieldInfo | None,
|
29
|
+
value: Enum | None, # str | int | float,
|
30
|
+
form_errors: Mapping[str, Any],
|
31
|
+
removable: bool,
|
32
|
+
) -> Widget[Enum]:
|
33
|
+
"""Build the widget."""
|
34
|
+
options = [(item.name, item.value) for item in field_type] # type: ignore
|
35
|
+
return DropDownWidget(
|
36
|
+
name=field_name,
|
37
|
+
options=options, # type: ignore
|
38
|
+
removable=removable,
|
39
|
+
title=field.title or "" if field else "",
|
40
|
+
hint=field.description if field else None,
|
41
|
+
aria_label=(
|
42
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
43
|
+
if field and field.json_schema_extra
|
44
|
+
else None
|
45
|
+
),
|
46
|
+
token=self.factory.token,
|
47
|
+
value=str(value),
|
48
|
+
error=form_errors.get(field_name),
|
49
|
+
)
|
@@ -0,0 +1,188 @@
|
|
1
|
+
"""
|
2
|
+
Create markup for pydantic forms.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import secrets
|
6
|
+
from collections.abc import Mapping
|
7
|
+
from typing import TYPE_CHECKING, Any, get_origin
|
8
|
+
|
9
|
+
from markupsafe import Markup
|
10
|
+
from pydantic.fields import FieldInfo
|
11
|
+
|
12
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.base import CustomWidget, Widget
|
13
|
+
from fastlife.domain.model.form import FormModel
|
14
|
+
from fastlife.domain.model.template import XTemplate
|
15
|
+
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
from fastlife.service.templates import AbstractTemplateRenderer
|
18
|
+
|
19
|
+
from .base import BaseWidgetBuilder
|
20
|
+
from .bool_builder import BoolBuilder
|
21
|
+
from .emailstr_builder import EmailStrBuilder
|
22
|
+
from .enum_builder import EnumBuilder
|
23
|
+
from .literal_builder import LiteralBuilder
|
24
|
+
from .model_builder import ModelBuilder
|
25
|
+
from .secretstr_builder import SecretStrBuilder
|
26
|
+
from .sequence_builder import SequenceBuilder
|
27
|
+
from .set_builder import SetBuilder
|
28
|
+
from .simpletype_builder import SimpleTypeBuilder
|
29
|
+
from .union_builder import UnionBuilder
|
30
|
+
|
31
|
+
|
32
|
+
class OptionalFatalError(XTemplate):
|
33
|
+
template = """
|
34
|
+
<>
|
35
|
+
{
|
36
|
+
if message {
|
37
|
+
<FatalError message={message} />
|
38
|
+
}
|
39
|
+
}
|
40
|
+
</>
|
41
|
+
"""
|
42
|
+
message: str
|
43
|
+
|
44
|
+
|
45
|
+
class WidgetFactory:
|
46
|
+
"""
|
47
|
+
Form builder for pydantic model.
|
48
|
+
|
49
|
+
:param renderer: template engine to render widget.
|
50
|
+
:param token: reuse a token.
|
51
|
+
"""
|
52
|
+
|
53
|
+
def __init__(self, renderer: "AbstractTemplateRenderer", token: str | None = None):
|
54
|
+
self.renderer = renderer
|
55
|
+
self.token = token or secrets.token_urlsafe(4).replace("_", "-")
|
56
|
+
self.builders: list[BaseWidgetBuilder[Any]] = [
|
57
|
+
# Order is super important here
|
58
|
+
# starts by the union type
|
59
|
+
UnionBuilder(self),
|
60
|
+
# and to other types that have an origin
|
61
|
+
SetBuilder(self),
|
62
|
+
LiteralBuilder(self),
|
63
|
+
SequenceBuilder(self),
|
64
|
+
# from this part, order does not really matter
|
65
|
+
ModelBuilder(self),
|
66
|
+
BoolBuilder(self),
|
67
|
+
EnumBuilder(self),
|
68
|
+
EmailStrBuilder(self),
|
69
|
+
SecretStrBuilder(self),
|
70
|
+
# we keep simple types, str, int at the end
|
71
|
+
SimpleTypeBuilder(self),
|
72
|
+
]
|
73
|
+
|
74
|
+
def get_markup(
|
75
|
+
self,
|
76
|
+
model: FormModel[Any],
|
77
|
+
*,
|
78
|
+
removable: bool = False,
|
79
|
+
field: FieldInfo | None = None,
|
80
|
+
) -> Markup:
|
81
|
+
"""
|
82
|
+
Get the markup for the given model.
|
83
|
+
|
84
|
+
:param model: the model to build the html markup.
|
85
|
+
:param removable: Include a button to remove the model in the markup.
|
86
|
+
:param field: only build the markup of this field is not None.
|
87
|
+
"""
|
88
|
+
ret = Markup()
|
89
|
+
if model.fatal_error:
|
90
|
+
ret += Markup(
|
91
|
+
self.renderer.render_template(
|
92
|
+
OptionalFatalError(message=model.fatal_error)
|
93
|
+
)
|
94
|
+
)
|
95
|
+
ret += Markup(
|
96
|
+
self.get_widget(
|
97
|
+
model.model.__class__,
|
98
|
+
model.form_data,
|
99
|
+
model.errors,
|
100
|
+
prefix=model.prefix,
|
101
|
+
removable=removable,
|
102
|
+
field=field,
|
103
|
+
).to_html(self.renderer)
|
104
|
+
)
|
105
|
+
return ret
|
106
|
+
|
107
|
+
def get_widget(
|
108
|
+
self,
|
109
|
+
base: type[Any],
|
110
|
+
form_data: Mapping[str, Any],
|
111
|
+
form_errors: Mapping[str, Any],
|
112
|
+
*,
|
113
|
+
prefix: str,
|
114
|
+
removable: bool,
|
115
|
+
field: FieldInfo | None = None,
|
116
|
+
) -> Widget[Any]:
|
117
|
+
"""
|
118
|
+
build the widget for the given type and return it.
|
119
|
+
:param base: the type to build, it has to be a builtin or a Pydantic model.
|
120
|
+
:param form_data: form values to render.
|
121
|
+
:param form_errors: form errors to render.
|
122
|
+
"""
|
123
|
+
return self.build(
|
124
|
+
base,
|
125
|
+
value=form_data.get(prefix, {}),
|
126
|
+
form_errors=form_errors,
|
127
|
+
name=prefix,
|
128
|
+
removable=removable,
|
129
|
+
field=field,
|
130
|
+
)
|
131
|
+
|
132
|
+
def build(
|
133
|
+
self,
|
134
|
+
typ: type[Any],
|
135
|
+
*,
|
136
|
+
name: str = "",
|
137
|
+
value: Any,
|
138
|
+
removable: bool,
|
139
|
+
form_errors: Mapping[str, Any],
|
140
|
+
field: FieldInfo | None = None,
|
141
|
+
) -> Widget[Any]:
|
142
|
+
"""
|
143
|
+
build widget tree for the given type.
|
144
|
+
This function is recurive and shoud not be used directly.
|
145
|
+
The type is a composite, it can be pydantic model, builtin, list or unions.
|
146
|
+
|
147
|
+
The {meth}`WidgetFactory.get_widget` or {meth}`WidgetFactory.get_markup`
|
148
|
+
should be used.
|
149
|
+
|
150
|
+
:param typ: the type to build, it has to be a builtin or a Pydantic model.
|
151
|
+
:param name: name of the widget to build.
|
152
|
+
:param value: value for the widget.
|
153
|
+
:param removable: True if it has to include a remove button.
|
154
|
+
:param form_errors: errors in the form.
|
155
|
+
:param field: field information used to customize the widget.
|
156
|
+
"""
|
157
|
+
if field and field.metadata:
|
158
|
+
for widget in field.metadata:
|
159
|
+
if isinstance(widget, CustomWidget):
|
160
|
+
ret: Widget[Any] = widget.typ(
|
161
|
+
name=name,
|
162
|
+
value=value,
|
163
|
+
removable=removable,
|
164
|
+
title=field.title or "" if field else "",
|
165
|
+
hint=field.description if field else None,
|
166
|
+
aria_label=(
|
167
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
168
|
+
if field and field.json_schema_extra
|
169
|
+
else None
|
170
|
+
),
|
171
|
+
token=self.token,
|
172
|
+
error=form_errors.get(name),
|
173
|
+
)
|
174
|
+
return ret
|
175
|
+
|
176
|
+
type_origin = get_origin(typ)
|
177
|
+
for builder in self.builders:
|
178
|
+
if builder.accept(typ, type_origin):
|
179
|
+
return builder.build(
|
180
|
+
field_name=name,
|
181
|
+
field_type=typ,
|
182
|
+
field=field,
|
183
|
+
value=value,
|
184
|
+
form_errors=form_errors,
|
185
|
+
removable=removable,
|
186
|
+
)
|
187
|
+
|
188
|
+
raise NotImplementedError(f"{typ} not implemented") # coverage: ignore
|
@@ -0,0 +1,55 @@
|
|
1
|
+
"""Handle Literal type."""
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
4
|
+
from typing import Any, Literal
|
5
|
+
|
6
|
+
from pydantic.fields import FieldInfo
|
7
|
+
|
8
|
+
from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
|
9
|
+
BaseWidgetBuilder,
|
10
|
+
)
|
11
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.dropdown import DropDownWidget
|
12
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.hidden import HiddenWidget
|
13
|
+
from fastlife.domain.model.types import AnyLiteral
|
14
|
+
|
15
|
+
|
16
|
+
class LiteralBuilder(BaseWidgetBuilder[AnyLiteral]):
|
17
|
+
"""Builder for Literal."""
|
18
|
+
|
19
|
+
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
20
|
+
"""True for Literal."""
|
21
|
+
return origin is Literal
|
22
|
+
|
23
|
+
def build(
|
24
|
+
self,
|
25
|
+
*,
|
26
|
+
field_name: str,
|
27
|
+
field_type: type[Any], # a literal actually
|
28
|
+
field: FieldInfo | None,
|
29
|
+
value: AnyLiteral | None,
|
30
|
+
form_errors: Mapping[str, Any],
|
31
|
+
removable: bool,
|
32
|
+
) -> HiddenWidget | DropDownWidget:
|
33
|
+
"""Build the widget."""
|
34
|
+
choices: list[str] = field_type.__args__ # type: ignore
|
35
|
+
if len(choices) == 1:
|
36
|
+
return HiddenWidget(
|
37
|
+
name=field_name,
|
38
|
+
value=choices[0],
|
39
|
+
token=self.factory.token,
|
40
|
+
)
|
41
|
+
return DropDownWidget(
|
42
|
+
name=field_name,
|
43
|
+
options=choices, # type: ignore
|
44
|
+
removable=removable,
|
45
|
+
title=field.title or "" if field else "",
|
46
|
+
hint=field.description if field else None,
|
47
|
+
aria_label=(
|
48
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
49
|
+
if field and field.json_schema_extra
|
50
|
+
else None
|
51
|
+
),
|
52
|
+
token=self.factory.token,
|
53
|
+
value=str(value),
|
54
|
+
error=form_errors.get(field_name),
|
55
|
+
)
|
@@ -0,0 +1,66 @@
|
|
1
|
+
"""Handle Pydantic BaseModel type."""
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from pydantic import BaseModel
|
7
|
+
from pydantic.fields import FieldInfo
|
8
|
+
|
9
|
+
from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
|
10
|
+
BaseWidgetBuilder,
|
11
|
+
)
|
12
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
|
13
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.model import ModelWidget
|
14
|
+
|
15
|
+
|
16
|
+
class ModelBuilder(BaseWidgetBuilder[Mapping[str, Any]]):
|
17
|
+
"""Builder for Pydantic BaseModel values."""
|
18
|
+
|
19
|
+
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
20
|
+
"""True for Pydantic BaseModel."""
|
21
|
+
return issubclass(typ, BaseModel)
|
22
|
+
|
23
|
+
def build(
|
24
|
+
self,
|
25
|
+
*,
|
26
|
+
field_name: str,
|
27
|
+
field_type: type[BaseModel],
|
28
|
+
field: FieldInfo | None,
|
29
|
+
value: Mapping[str, Any] | None,
|
30
|
+
form_errors: Mapping[str, Any],
|
31
|
+
removable: bool,
|
32
|
+
) -> Widget[Any]:
|
33
|
+
"""Build the widget."""
|
34
|
+
value = value or {}
|
35
|
+
ret: dict[str, Any] = {}
|
36
|
+
for key, child_field in field_type.model_fields.items():
|
37
|
+
child_key = f"{field_name}.{key}" if field_name else key
|
38
|
+
if child_field.exclude:
|
39
|
+
continue
|
40
|
+
if child_field.annotation is None:
|
41
|
+
raise ValueError( # coverage: ignore
|
42
|
+
f"Missing annotation for {child_field} in {child_key}"
|
43
|
+
)
|
44
|
+
ret[key] = self.factory.build(
|
45
|
+
child_field.annotation,
|
46
|
+
name=child_key,
|
47
|
+
field=child_field,
|
48
|
+
value=value.get(key),
|
49
|
+
form_errors=form_errors,
|
50
|
+
removable=False,
|
51
|
+
)
|
52
|
+
return ModelWidget[Any](
|
53
|
+
name=field_name,
|
54
|
+
value=list(ret.values()),
|
55
|
+
removable=removable,
|
56
|
+
title=field.title or "" if field and field.title else "",
|
57
|
+
hint=field.description if field else None,
|
58
|
+
aria_label=(
|
59
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
60
|
+
if field and field.json_schema_extra
|
61
|
+
else None
|
62
|
+
),
|
63
|
+
token=self.factory.token,
|
64
|
+
error=form_errors.get(field_name),
|
65
|
+
nested=field is not None,
|
66
|
+
)
|
@@ -0,0 +1,48 @@
|
|
1
|
+
"""Handle Pydantic SecretStr type."""
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from pydantic import SecretStr
|
7
|
+
from pydantic.fields import FieldInfo
|
8
|
+
|
9
|
+
from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
|
10
|
+
BaseWidgetBuilder,
|
11
|
+
)
|
12
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
|
13
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.text import PasswordWidget
|
14
|
+
|
15
|
+
|
16
|
+
class SecretStrBuilder(BaseWidgetBuilder[SecretStr]):
|
17
|
+
"""Builder for Pydantic SecretStr."""
|
18
|
+
|
19
|
+
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
20
|
+
"""True for Pydantic SecretStr."""
|
21
|
+
return issubclass(typ, SecretStr)
|
22
|
+
|
23
|
+
def build(
|
24
|
+
self,
|
25
|
+
*,
|
26
|
+
field_name: str,
|
27
|
+
field_type: type[Any],
|
28
|
+
field: FieldInfo | None,
|
29
|
+
value: SecretStr | None,
|
30
|
+
form_errors: Mapping[str, Any],
|
31
|
+
removable: bool,
|
32
|
+
) -> Widget[SecretStr]:
|
33
|
+
"""Build the widget."""
|
34
|
+
return PasswordWidget(
|
35
|
+
name=field_name,
|
36
|
+
placeholder=str(field.examples[0]) if field and field.examples else None,
|
37
|
+
removable=removable,
|
38
|
+
title=field.title or "" if field else "",
|
39
|
+
hint=field.description if field else None,
|
40
|
+
aria_label=(
|
41
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
42
|
+
if field and field.json_schema_extra
|
43
|
+
else None
|
44
|
+
),
|
45
|
+
token=self.factory.token,
|
46
|
+
error=form_errors.get(field_name),
|
47
|
+
new_password="new-password" in field.metadata if field else False,
|
48
|
+
)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
"""Handle Sequence type."""
|
2
|
+
|
3
|
+
from collections.abc import Mapping, MutableSequence, Sequence
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from pydantic.fields import FieldInfo
|
7
|
+
|
8
|
+
from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
|
9
|
+
BaseWidgetBuilder,
|
10
|
+
)
|
11
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
|
12
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.sequence import SequenceWidget
|
13
|
+
|
14
|
+
|
15
|
+
class SequenceBuilder(BaseWidgetBuilder[Sequence[Any]]):
|
16
|
+
"""Builder for Sequence values."""
|
17
|
+
|
18
|
+
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
19
|
+
"""True for Sequence, MutableSequence or list"""
|
20
|
+
return origin is Sequence or origin is MutableSequence or origin is list
|
21
|
+
|
22
|
+
def build(
|
23
|
+
self,
|
24
|
+
*,
|
25
|
+
field_name: str,
|
26
|
+
field_type: type[Any],
|
27
|
+
field: FieldInfo | None,
|
28
|
+
value: Sequence[Any] | None,
|
29
|
+
form_errors: Mapping[str, Any],
|
30
|
+
removable: bool,
|
31
|
+
) -> Widget[Sequence[Any]]:
|
32
|
+
"""Build the widget."""
|
33
|
+
typ = field_type.__args__[0] # type: ignore
|
34
|
+
value = value or []
|
35
|
+
items: Sequence[Any] = [
|
36
|
+
self.factory.build(
|
37
|
+
typ, # type: ignore
|
38
|
+
name=f"{field_name}.{idx}",
|
39
|
+
value=v,
|
40
|
+
field=field,
|
41
|
+
form_errors=form_errors,
|
42
|
+
removable=True,
|
43
|
+
)
|
44
|
+
for idx, v in enumerate(value)
|
45
|
+
]
|
46
|
+
return SequenceWidget[Any](
|
47
|
+
name=field_name,
|
48
|
+
title=field.title or "" if field else "",
|
49
|
+
hint=field.description if field else None,
|
50
|
+
aria_label=(
|
51
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
52
|
+
if field and field.json_schema_extra
|
53
|
+
else None
|
54
|
+
),
|
55
|
+
value=items,
|
56
|
+
item_type=typ, # type: ignore
|
57
|
+
token=self.factory.token,
|
58
|
+
removable=removable,
|
59
|
+
error=form_errors.get(field_name),
|
60
|
+
)
|
@@ -0,0 +1,85 @@
|
|
1
|
+
"""Handle Set type."""
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
4
|
+
from enum import Enum
|
5
|
+
from typing import Any, Literal, get_origin
|
6
|
+
|
7
|
+
from pydantic.fields import FieldInfo
|
8
|
+
|
9
|
+
from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
|
10
|
+
BaseWidgetBuilder,
|
11
|
+
)
|
12
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
|
13
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.checklist import (
|
14
|
+
Checkable,
|
15
|
+
ChecklistWidget,
|
16
|
+
)
|
17
|
+
|
18
|
+
|
19
|
+
class SetBuilder(BaseWidgetBuilder[set[Any]]):
|
20
|
+
"""Builder for Set."""
|
21
|
+
|
22
|
+
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
23
|
+
"""True for Set"""
|
24
|
+
return origin is set
|
25
|
+
|
26
|
+
def build(
|
27
|
+
self,
|
28
|
+
*,
|
29
|
+
field_name: str,
|
30
|
+
field_type: type[Any],
|
31
|
+
field: FieldInfo | None,
|
32
|
+
value: set[Any] | None,
|
33
|
+
form_errors: Mapping[str, Any],
|
34
|
+
removable: bool,
|
35
|
+
) -> Widget[Any]:
|
36
|
+
"""Build the widget."""
|
37
|
+
choice_wrapper = field_type.__args__[0]
|
38
|
+
choices = []
|
39
|
+
choice_wrapper_origin = get_origin(choice_wrapper)
|
40
|
+
if choice_wrapper_origin:
|
41
|
+
if choice_wrapper_origin is Literal:
|
42
|
+
litchoice: list[str] = choice_wrapper.__args__ # type: ignore
|
43
|
+
choices = [
|
44
|
+
Checkable(
|
45
|
+
label=c,
|
46
|
+
value=c,
|
47
|
+
checked=c in value if value else False, # type: ignore
|
48
|
+
name=field_name,
|
49
|
+
token=self.factory.token,
|
50
|
+
error=form_errors.get(f"{field_name}-{c}"),
|
51
|
+
)
|
52
|
+
for c in litchoice
|
53
|
+
]
|
54
|
+
|
55
|
+
else:
|
56
|
+
raise NotImplementedError # coverage: ignore
|
57
|
+
elif issubclass(choice_wrapper, Enum):
|
58
|
+
choices = [
|
59
|
+
Checkable(
|
60
|
+
label=e.value,
|
61
|
+
value=e.name,
|
62
|
+
checked=e.name in value if value else False, # type: ignore
|
63
|
+
name=field_name,
|
64
|
+
token=self.factory.token,
|
65
|
+
error=form_errors.get(f"{field_name}-{e.name}"),
|
66
|
+
)
|
67
|
+
for e in choice_wrapper
|
68
|
+
]
|
69
|
+
else:
|
70
|
+
raise NotImplementedError # coverage: ignore
|
71
|
+
|
72
|
+
return ChecklistWidget(
|
73
|
+
name=field_name,
|
74
|
+
title=field.title or "" if field else "",
|
75
|
+
hint=field.description if field else None,
|
76
|
+
aria_label=(
|
77
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
78
|
+
if field and field.json_schema_extra
|
79
|
+
else None
|
80
|
+
),
|
81
|
+
token=self.factory.token,
|
82
|
+
value=choices,
|
83
|
+
removable=removable,
|
84
|
+
error=form_errors.get(field_name),
|
85
|
+
)
|
@@ -0,0 +1,48 @@
|
|
1
|
+
"""Handle simple types (str, int, float, ...)."""
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from pydantic.fields import FieldInfo
|
7
|
+
|
8
|
+
from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
|
9
|
+
BaseWidgetBuilder,
|
10
|
+
)
|
11
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
|
12
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.text import TextWidget
|
13
|
+
from fastlife.domain.model.types import Builtins
|
14
|
+
|
15
|
+
|
16
|
+
class SimpleTypeBuilder(BaseWidgetBuilder[Builtins]):
|
17
|
+
"""Builder for simple types."""
|
18
|
+
|
19
|
+
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
20
|
+
"""True for simple types: int, str, float, Decimal, UUID"""
|
21
|
+
return issubclass(typ, Builtins)
|
22
|
+
|
23
|
+
def build(
|
24
|
+
self,
|
25
|
+
*,
|
26
|
+
field_name: str,
|
27
|
+
field_type: type[Any],
|
28
|
+
field: FieldInfo | None,
|
29
|
+
value: Builtins | None,
|
30
|
+
form_errors: Mapping[str, Any],
|
31
|
+
removable: bool,
|
32
|
+
) -> Widget[Builtins]:
|
33
|
+
"""Build the widget."""
|
34
|
+
return TextWidget(
|
35
|
+
name=field_name,
|
36
|
+
placeholder=str(field.examples[0]) if field and field.examples else None,
|
37
|
+
title=field.title or "" if field else "",
|
38
|
+
hint=field.description if field else None,
|
39
|
+
aria_label=(
|
40
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
41
|
+
if field and field.json_schema_extra
|
42
|
+
else None
|
43
|
+
),
|
44
|
+
removable=removable,
|
45
|
+
token=self.factory.token,
|
46
|
+
value=str(value) if value else "",
|
47
|
+
error=form_errors.get(field_name),
|
48
|
+
)
|
@@ -0,0 +1,92 @@
|
|
1
|
+
"""Handle Union type."""
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
4
|
+
from types import NoneType
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from pydantic import ValidationError
|
8
|
+
from pydantic.fields import FieldInfo
|
9
|
+
|
10
|
+
from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
|
11
|
+
BaseWidgetBuilder,
|
12
|
+
)
|
13
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
|
14
|
+
from fastlife.adapters.xcomponent.pydantic_form.widgets.union import UnionWidget
|
15
|
+
from fastlife.shared_utils.infer import is_complex_type, is_union
|
16
|
+
|
17
|
+
|
18
|
+
class UnionBuilder(BaseWidgetBuilder[Any]):
|
19
|
+
"""Builder for Union."""
|
20
|
+
|
21
|
+
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
22
|
+
"""True for unions Union[A,B], A | B or event Optional[A], A | None"""
|
23
|
+
return is_union(typ)
|
24
|
+
|
25
|
+
def build(
|
26
|
+
self,
|
27
|
+
*,
|
28
|
+
field_name: str,
|
29
|
+
field_type: type[Any],
|
30
|
+
field: FieldInfo | None,
|
31
|
+
value: Any | None,
|
32
|
+
form_errors: Mapping[str, Any],
|
33
|
+
removable: bool,
|
34
|
+
) -> Widget[Any]:
|
35
|
+
"""Build the widget."""
|
36
|
+
types: list[type[Any]] = []
|
37
|
+
# required = True
|
38
|
+
for typ in field_type.__args__: # type: ignore
|
39
|
+
if typ is NoneType:
|
40
|
+
# required = False
|
41
|
+
continue
|
42
|
+
types.append(typ) # type: ignore
|
43
|
+
|
44
|
+
if (
|
45
|
+
not removable
|
46
|
+
and len(types) == 1
|
47
|
+
# if the optional type is a complex type,
|
48
|
+
and not is_complex_type(types[0])
|
49
|
+
):
|
50
|
+
return self.factory.build( # coverage: ignore
|
51
|
+
types[0],
|
52
|
+
name=field_name,
|
53
|
+
field=field,
|
54
|
+
value=value,
|
55
|
+
form_errors=form_errors,
|
56
|
+
removable=False,
|
57
|
+
)
|
58
|
+
child = None
|
59
|
+
if value:
|
60
|
+
for typ in types:
|
61
|
+
try:
|
62
|
+
typ(**value)
|
63
|
+
except ValidationError:
|
64
|
+
pass
|
65
|
+
else:
|
66
|
+
child = self.factory.build(
|
67
|
+
typ,
|
68
|
+
name=field_name,
|
69
|
+
field=field,
|
70
|
+
value=value,
|
71
|
+
form_errors=form_errors,
|
72
|
+
removable=False,
|
73
|
+
)
|
74
|
+
|
75
|
+
widget = UnionWidget[Any](
|
76
|
+
name=field_name,
|
77
|
+
# we assume those types are BaseModel
|
78
|
+
value=child,
|
79
|
+
children_types=types, # type: ignore
|
80
|
+
title=field.title or "" if field else "",
|
81
|
+
hint=field.description if field else None,
|
82
|
+
aria_label=(
|
83
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
84
|
+
if field and field.json_schema_extra
|
85
|
+
else None
|
86
|
+
),
|
87
|
+
token=self.factory.token,
|
88
|
+
removable=removable,
|
89
|
+
error=form_errors.get(field_name),
|
90
|
+
)
|
91
|
+
|
92
|
+
return widget
|