fastlifeweb 0.9.7__py3-none-any.whl → 0.11.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.
- fastlife/__init__.py +2 -2
- fastlife/config/__init__.py +13 -0
- fastlife/{configurator → config}/configurator.py +64 -41
- fastlife/{configurator → config}/registry.py +2 -10
- fastlife/{configurator → config}/settings.py +7 -3
- fastlife/middlewares/__init__.py +7 -0
- fastlife/middlewares/base.py +24 -0
- fastlife/middlewares/reverse_proxy/__init__.py +16 -0
- fastlife/middlewares/reverse_proxy/x_forwarded.py +1 -14
- fastlife/middlewares/session/__init__.py +16 -0
- fastlife/middlewares/session/middleware.py +6 -1
- fastlife/middlewares/session/serializer.py +21 -0
- fastlife/request/__init__.py +5 -0
- fastlife/request/{model_result.py → form.py} +21 -9
- fastlife/request/form_data.py +28 -3
- fastlife/request/request.py +18 -0
- fastlife/routing/__init__.py +7 -0
- fastlife/routing/route.py +45 -0
- fastlife/routing/router.py +12 -4
- fastlife/security/__init__.py +1 -0
- fastlife/security/csrf.py +29 -11
- fastlife/security/policy.py +6 -2
- fastlife/shared_utils/__init__.py +1 -0
- fastlife/shared_utils/infer.py +7 -0
- fastlife/shared_utils/resolver.py +10 -2
- fastlife/templates/A.jinja +33 -9
- fastlife/templates/Button.jinja +55 -32
- fastlife/templates/Checkbox.jinja +20 -6
- fastlife/templates/CsrfToken.jinja +4 -0
- fastlife/templates/Details.jinja +31 -3
- fastlife/templates/Form.jinja +45 -7
- fastlife/templates/H1.jinja +14 -1
- fastlife/templates/H2.jinja +14 -1
- fastlife/templates/H3.jinja +14 -1
- fastlife/templates/H4.jinja +14 -1
- fastlife/templates/H5.jinja +14 -1
- fastlife/templates/H6.jinja +14 -1
- fastlife/templates/Hidden.jinja +3 -3
- fastlife/templates/Input.jinja +21 -8
- fastlife/templates/Label.jinja +18 -2
- fastlife/templates/Option.jinja +14 -2
- fastlife/templates/P.jinja +14 -2
- fastlife/templates/Radio.jinja +34 -12
- fastlife/templates/Select.jinja +15 -4
- fastlife/templates/Summary.jinja +13 -2
- fastlife/templates/Table.jinja +12 -1
- fastlife/templates/Tbody.jinja +11 -1
- fastlife/templates/Td.jinja +12 -1
- fastlife/templates/Textarea.jinja +18 -0
- fastlife/templates/Tfoot.jinja +11 -1
- fastlife/templates/Th.jinja +12 -1
- fastlife/templates/Thead.jinja +11 -1
- fastlife/templates/Tr.jinja +11 -1
- fastlife/templates/pydantic_form/Boolean.jinja +3 -2
- fastlife/templates/pydantic_form/Checklist.jinja +3 -5
- fastlife/templates/pydantic_form/Dropdown.jinja +3 -2
- fastlife/templates/pydantic_form/Error.jinja +4 -3
- fastlife/templates/pydantic_form/Hidden.jinja +2 -1
- fastlife/templates/pydantic_form/Hint.jinja +2 -1
- fastlife/templates/pydantic_form/Model.jinja +16 -3
- fastlife/templates/pydantic_form/Sequence.jinja +15 -6
- fastlife/templates/pydantic_form/Text.jinja +2 -2
- fastlife/templates/pydantic_form/Textarea.jinja +32 -0
- fastlife/templates/pydantic_form/Union.jinja +7 -1
- fastlife/templates/pydantic_form/Widget.jinja +6 -3
- fastlife/templating/binding.py +18 -4
- fastlife/templating/renderer/__init__.py +3 -1
- fastlife/templating/renderer/abstract.py +21 -8
- fastlife/templating/renderer/constants.py +82 -0
- fastlife/templating/renderer/jinjax.py +269 -6
- fastlife/templating/renderer/widgets/base.py +43 -7
- fastlife/templating/renderer/widgets/boolean.py +21 -0
- fastlife/templating/renderer/widgets/checklist.py +23 -0
- fastlife/templating/renderer/widgets/dropdown.py +22 -2
- fastlife/templating/renderer/widgets/factory.py +100 -29
- fastlife/templating/renderer/widgets/hidden.py +16 -0
- fastlife/templating/renderer/widgets/model.py +7 -1
- fastlife/templating/renderer/widgets/sequence.py +8 -6
- fastlife/templating/renderer/widgets/text.py +80 -4
- fastlife/templating/renderer/widgets/union.py +25 -2
- fastlife/testing/testclient.py +3 -3
- fastlife/views/pydantic_form.py +2 -2
- {fastlifeweb-0.9.7.dist-info → fastlifeweb-0.11.0.dist-info}/METADATA +4 -9
- {fastlifeweb-0.9.7.dist-info → fastlifeweb-0.11.0.dist-info}/RECORD +86 -84
- fastlife/configurator/__init__.py +0 -4
- fastlife/configurator/base.py +0 -9
- fastlife/configurator/route_handler.py +0 -29
- fastlife/templates/__init__.py +0 -0
- {fastlifeweb-0.9.7.dist-info → fastlifeweb-0.11.0.dist-info}/LICENSE +0 -0
- {fastlifeweb-0.9.7.dist-info → fastlifeweb-0.11.0.dist-info}/WHEEL +0 -0
@@ -1,3 +1,7 @@
|
|
1
|
+
"""
|
2
|
+
Widget for field of type Set.
|
3
|
+
"""
|
4
|
+
|
1
5
|
from typing import Sequence
|
2
6
|
|
3
7
|
from pydantic import BaseModel, Field
|
@@ -6,6 +10,8 @@ from .base import Widget
|
|
6
10
|
|
7
11
|
|
8
12
|
class Checkable(BaseModel):
|
13
|
+
"""A checkable field from a checklist."""
|
14
|
+
|
9
15
|
label: str
|
10
16
|
name: str
|
11
17
|
value: str
|
@@ -24,11 +30,26 @@ class Checkable(BaseModel):
|
|
24
30
|
|
25
31
|
|
26
32
|
class ChecklistWidget(Widget[Sequence[Checkable]]):
|
33
|
+
"""
|
34
|
+
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
|
+
"""
|
45
|
+
|
27
46
|
def __init__(
|
28
47
|
self,
|
29
48
|
name: str,
|
30
49
|
*,
|
31
50
|
title: str | None,
|
51
|
+
hint: str | None = None,
|
52
|
+
aria_label: str | None = None,
|
32
53
|
value: Sequence[Checkable],
|
33
54
|
error: str | None = None,
|
34
55
|
token: str,
|
@@ -40,6 +61,8 @@ class ChecklistWidget(Widget[Sequence[Checkable]]):
|
|
40
61
|
error=error,
|
41
62
|
token=token,
|
42
63
|
title=title,
|
64
|
+
hint=hint,
|
65
|
+
aria_label=aria_label,
|
43
66
|
removable=removable,
|
44
67
|
)
|
45
68
|
|
@@ -1,20 +1,39 @@
|
|
1
|
+
"""
|
2
|
+
Widget for field of type Enum or Literal.
|
3
|
+
"""
|
4
|
+
|
1
5
|
from typing import Optional, Sequence, Tuple
|
2
6
|
|
3
7
|
from .base import Widget
|
4
8
|
|
5
9
|
|
6
10
|
class DropDownWidget(Widget[str]):
|
11
|
+
"""
|
12
|
+
Widget for field of type Enum or Literal.
|
13
|
+
|
14
|
+
:param name: field name.
|
15
|
+
:param title: title for the widget.
|
16
|
+
:param hint: hint for human.
|
17
|
+
:param aria_label: html input aria-label value.
|
18
|
+
:param value: current value.
|
19
|
+
:param error: error of the value if any.
|
20
|
+
:param options: List of possible values.
|
21
|
+
:param removable: display a button to remove the widget for optional fields.
|
22
|
+
:param token: token used to get unique id on the form.
|
23
|
+
"""
|
24
|
+
|
7
25
|
def __init__(
|
8
26
|
self,
|
9
27
|
name: str,
|
10
28
|
*,
|
11
29
|
title: Optional[str],
|
30
|
+
hint: Optional[str] = None,
|
31
|
+
aria_label: Optional[str] = None,
|
12
32
|
value: Optional[str] = None,
|
13
33
|
error: str | None = None,
|
14
34
|
options: Sequence[Tuple[str, str]] | Sequence[str],
|
15
35
|
removable: bool = False,
|
16
36
|
token: Optional[str] = None,
|
17
|
-
hint: Optional[str] = None,
|
18
37
|
) -> None:
|
19
38
|
super().__init__(
|
20
39
|
name,
|
@@ -23,6 +42,8 @@ class DropDownWidget(Widget[str]):
|
|
23
42
|
title=title,
|
24
43
|
token=token,
|
25
44
|
removable=removable,
|
45
|
+
hint=hint,
|
46
|
+
aria_label=aria_label,
|
26
47
|
)
|
27
48
|
self.options: list[dict[str, str]] = []
|
28
49
|
for opt in options:
|
@@ -30,7 +51,6 @@ class DropDownWidget(Widget[str]):
|
|
30
51
|
self.options.append({"value": opt[0], "text": opt[1]})
|
31
52
|
else:
|
32
53
|
self.options.append({"value": opt, "text": opt})
|
33
|
-
self.hint = hint
|
34
54
|
|
35
55
|
def get_template(self) -> str:
|
36
56
|
return "pydantic_form.Dropdown"
|
@@ -1,39 +1,49 @@
|
|
1
|
+
"""
|
2
|
+
Transform.
|
3
|
+
"""
|
4
|
+
|
1
5
|
import secrets
|
2
6
|
from collections.abc import MutableSequence, Sequence
|
3
7
|
from decimal import Decimal
|
4
8
|
from enum import Enum
|
5
9
|
from inspect import isclass
|
6
10
|
from types import NoneType
|
7
|
-
from typing import Any, Literal, Mapping,
|
11
|
+
from typing import Any, Literal, Mapping, Type, cast, get_origin
|
8
12
|
from uuid import UUID
|
9
13
|
|
10
14
|
from markupsafe import Markup
|
11
15
|
from pydantic import BaseModel, EmailStr, SecretStr, ValidationError
|
12
16
|
from pydantic.fields import FieldInfo
|
13
17
|
|
14
|
-
from fastlife.request.
|
18
|
+
from fastlife.request.form import FormModel
|
15
19
|
from fastlife.shared_utils.infer import is_complex_type, is_union
|
16
20
|
from fastlife.templating.renderer.abstract import AbstractTemplateRenderer
|
21
|
+
from fastlife.templating.renderer.widgets.base import Widget
|
17
22
|
from fastlife.templating.renderer.widgets.boolean import BooleanWidget
|
18
23
|
from fastlife.templating.renderer.widgets.checklist import Checkable, ChecklistWidget
|
19
24
|
from fastlife.templating.renderer.widgets.dropdown import DropDownWidget
|
20
25
|
from fastlife.templating.renderer.widgets.hidden import HiddenWidget
|
26
|
+
from fastlife.templating.renderer.widgets.model import ModelWidget
|
21
27
|
from fastlife.templating.renderer.widgets.sequence import SequenceWidget
|
22
|
-
|
23
|
-
from .
|
24
|
-
from .model import ModelWidget
|
25
|
-
from .text import TextWidget
|
26
|
-
from .union import UnionWidget
|
28
|
+
from fastlife.templating.renderer.widgets.text import TextWidget
|
29
|
+
from fastlife.templating.renderer.widgets.union import UnionWidget
|
27
30
|
|
28
31
|
|
29
32
|
class WidgetFactory:
|
30
|
-
|
33
|
+
"""
|
34
|
+
Form builder for pydantic model.
|
35
|
+
|
36
|
+
:param renderer: template engine to render widget.
|
37
|
+
:param token: reuse a token.
|
38
|
+
"""
|
39
|
+
|
40
|
+
def __init__(self, renderer: AbstractTemplateRenderer, token: str | None = None):
|
31
41
|
self.renderer = renderer
|
32
42
|
self.token = token or secrets.token_urlsafe(4).replace("_", "-")
|
33
43
|
|
34
44
|
def get_markup(
|
35
45
|
self,
|
36
|
-
model:
|
46
|
+
model: FormModel[Any],
|
37
47
|
*,
|
38
48
|
removable: bool = False,
|
39
49
|
field: FieldInfo | None = None,
|
@@ -86,7 +96,12 @@ class WidgetFactory:
|
|
86
96
|
value=value,
|
87
97
|
removable=removable,
|
88
98
|
title=field.title if field else "",
|
89
|
-
|
99
|
+
hint=field.description if field else None,
|
100
|
+
aria_label=(
|
101
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
102
|
+
if field and field.json_schema_extra
|
103
|
+
else None
|
104
|
+
),
|
90
105
|
token=self.token,
|
91
106
|
error=form_errors.get(name),
|
92
107
|
),
|
@@ -148,24 +163,24 @@ class WidgetFactory:
|
|
148
163
|
self,
|
149
164
|
field_name: str,
|
150
165
|
typ: Type[BaseModel],
|
151
|
-
field:
|
166
|
+
field: FieldInfo | None,
|
152
167
|
value: Mapping[str, Any],
|
153
168
|
form_errors: Mapping[str, Any],
|
154
169
|
removable: bool,
|
155
170
|
) -> Widget[Any]:
|
156
171
|
ret: dict[str, Any] = {}
|
157
|
-
for key,
|
172
|
+
for key, child_field in typ.model_fields.items():
|
158
173
|
child_key = f"{field_name}.{key}" if field_name else key
|
159
|
-
if
|
174
|
+
if child_field.exclude:
|
160
175
|
continue
|
161
|
-
if
|
176
|
+
if child_field.annotation is None:
|
162
177
|
raise ValueError( # coverage: ignore
|
163
|
-
f"Missing annotation for {
|
178
|
+
f"Missing annotation for {child_field} in {child_key}"
|
164
179
|
)
|
165
180
|
ret[key] = self.build(
|
166
|
-
|
181
|
+
child_field.annotation,
|
167
182
|
name=child_key,
|
168
|
-
field=
|
183
|
+
field=child_field,
|
169
184
|
value=value.get(key),
|
170
185
|
form_errors=form_errors,
|
171
186
|
removable=False,
|
@@ -174,16 +189,23 @@ class WidgetFactory:
|
|
174
189
|
field_name,
|
175
190
|
value=list(ret.values()),
|
176
191
|
removable=removable,
|
177
|
-
title=
|
192
|
+
title=field.title if field and field.title else "",
|
193
|
+
hint=field.description if field else None,
|
194
|
+
aria_label=(
|
195
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
196
|
+
if field and field.json_schema_extra
|
197
|
+
else None
|
198
|
+
),
|
178
199
|
token=self.token,
|
179
200
|
error=form_errors.get(field_name),
|
201
|
+
nested=field is not None,
|
180
202
|
)
|
181
203
|
|
182
204
|
def build_union(
|
183
205
|
self,
|
184
206
|
field_name: str,
|
185
207
|
field_type: Type[Any],
|
186
|
-
field:
|
208
|
+
field: FieldInfo | None,
|
187
209
|
value: Any,
|
188
210
|
form_errors: Mapping[str, Any],
|
189
211
|
removable: bool,
|
@@ -233,6 +255,12 @@ class WidgetFactory:
|
|
233
255
|
value=child,
|
234
256
|
children_types=types, # type: ignore
|
235
257
|
title=field.title if field else "",
|
258
|
+
hint=field.description if field else None,
|
259
|
+
aria_label=(
|
260
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
261
|
+
if field and field.json_schema_extra
|
262
|
+
else None
|
263
|
+
),
|
236
264
|
token=self.token,
|
237
265
|
removable=removable,
|
238
266
|
error=form_errors.get(field_name),
|
@@ -244,8 +272,8 @@ class WidgetFactory:
|
|
244
272
|
self,
|
245
273
|
field_name: str,
|
246
274
|
field_type: Type[Any],
|
247
|
-
field:
|
248
|
-
value:
|
275
|
+
field: FieldInfo | None,
|
276
|
+
value: Sequence[Any] | None,
|
249
277
|
form_errors: Mapping[str, Any],
|
250
278
|
removable: bool,
|
251
279
|
) -> Widget[Any]:
|
@@ -264,8 +292,13 @@ class WidgetFactory:
|
|
264
292
|
]
|
265
293
|
return SequenceWidget(
|
266
294
|
field_name,
|
267
|
-
hint=field.description if field else "",
|
268
295
|
title=field.title if field else "",
|
296
|
+
hint=field.description if field else None,
|
297
|
+
aria_label=(
|
298
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
299
|
+
if field and field.json_schema_extra
|
300
|
+
else None
|
301
|
+
),
|
269
302
|
value=items,
|
270
303
|
item_type=typ, # type: ignore
|
271
304
|
token=self.token,
|
@@ -277,8 +310,8 @@ class WidgetFactory:
|
|
277
310
|
self,
|
278
311
|
field_name: str,
|
279
312
|
field_type: Type[Any],
|
280
|
-
field:
|
281
|
-
value:
|
313
|
+
field: FieldInfo | None,
|
314
|
+
value: Sequence[Any] | None,
|
282
315
|
form_errors: Mapping[str, Any],
|
283
316
|
removable: bool,
|
284
317
|
) -> Widget[Any]:
|
@@ -320,6 +353,12 @@ class WidgetFactory:
|
|
320
353
|
return ChecklistWidget(
|
321
354
|
field_name,
|
322
355
|
title=field.title if field else "",
|
356
|
+
hint=field.description if field else None,
|
357
|
+
aria_label=(
|
358
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
359
|
+
if field and field.json_schema_extra
|
360
|
+
else None
|
361
|
+
),
|
323
362
|
token=self.token,
|
324
363
|
value=choices,
|
325
364
|
removable=removable,
|
@@ -339,6 +378,12 @@ class WidgetFactory:
|
|
339
378
|
field_name,
|
340
379
|
removable=removable,
|
341
380
|
title=field.title if field else "",
|
381
|
+
hint=field.description if field else None,
|
382
|
+
aria_label=(
|
383
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
384
|
+
if field and field.json_schema_extra
|
385
|
+
else None
|
386
|
+
),
|
342
387
|
token=self.token,
|
343
388
|
value=value,
|
344
389
|
error=form_errors.get(field_name),
|
@@ -355,11 +400,16 @@ class WidgetFactory:
|
|
355
400
|
) -> Widget[Any]:
|
356
401
|
return TextWidget(
|
357
402
|
field_name,
|
358
|
-
hint=field.description if field else "",
|
359
403
|
input_type="email",
|
360
404
|
placeholder=str(field.examples[0]) if field and field.examples else None,
|
361
405
|
removable=removable,
|
362
406
|
title=field.title if field else "",
|
407
|
+
hint=field.description if field else None,
|
408
|
+
aria_label=(
|
409
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
410
|
+
if field and field.json_schema_extra
|
411
|
+
else None
|
412
|
+
),
|
363
413
|
token=self.token,
|
364
414
|
value=str(value),
|
365
415
|
error=form_errors.get(field_name),
|
@@ -376,11 +426,16 @@ class WidgetFactory:
|
|
376
426
|
) -> Widget[Any]:
|
377
427
|
return TextWidget(
|
378
428
|
field_name,
|
379
|
-
hint=field.description if field else "",
|
380
429
|
input_type="password",
|
381
430
|
placeholder=str(field.examples[0]) if field and field.examples else None,
|
382
431
|
removable=removable,
|
383
432
|
title=field.title if field else "",
|
433
|
+
hint=field.description if field else None,
|
434
|
+
aria_label=(
|
435
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
436
|
+
if field and field.json_schema_extra
|
437
|
+
else None
|
438
|
+
),
|
384
439
|
token=self.token,
|
385
440
|
value=value.get_secret_value() if isinstance(value, SecretStr) else value,
|
386
441
|
error=form_errors.get(field_name),
|
@@ -407,6 +462,12 @@ class WidgetFactory:
|
|
407
462
|
options=choices,
|
408
463
|
removable=removable,
|
409
464
|
title=field.title if field else "",
|
465
|
+
hint=field.description if field else None,
|
466
|
+
aria_label=(
|
467
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
468
|
+
if field and field.json_schema_extra
|
469
|
+
else None
|
470
|
+
),
|
410
471
|
token=self.token,
|
411
472
|
value=str(value),
|
412
473
|
error=form_errors.get(field_name),
|
@@ -427,6 +488,12 @@ class WidgetFactory:
|
|
427
488
|
options=options, # type: ignore
|
428
489
|
removable=removable,
|
429
490
|
title=field.title if field else "",
|
491
|
+
hint=field.description if field else None,
|
492
|
+
aria_label=(
|
493
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
494
|
+
if field and field.json_schema_extra
|
495
|
+
else None
|
496
|
+
),
|
430
497
|
token=self.token,
|
431
498
|
value=str(value),
|
432
499
|
error=form_errors.get(field_name),
|
@@ -443,11 +510,15 @@ class WidgetFactory:
|
|
443
510
|
) -> Widget[Any]:
|
444
511
|
return TextWidget(
|
445
512
|
field_name,
|
446
|
-
hint=field.description if field else None,
|
447
513
|
placeholder=str(field.examples[0]) if field and field.examples else None,
|
448
|
-
aria_label=field.description if field else None,
|
449
|
-
removable=removable,
|
450
514
|
title=field.title if field else "",
|
515
|
+
hint=field.description if field else None,
|
516
|
+
aria_label=(
|
517
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
518
|
+
if field and field.json_schema_extra
|
519
|
+
else None
|
520
|
+
),
|
521
|
+
removable=removable,
|
451
522
|
token=self.token,
|
452
523
|
value=str(value),
|
453
524
|
error=form_errors.get(field_name),
|
@@ -1,13 +1,29 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
1
3
|
from .base import Widget
|
2
4
|
|
3
5
|
|
4
6
|
class HiddenWidget(Widget[str]):
|
7
|
+
'''
|
8
|
+
Widget to annotate to display a field as an hidden field.
|
9
|
+
|
10
|
+
::
|
11
|
+
from pydantic import BaseModel
|
12
|
+
from fastlife.templating.renderer.widgets.hidden import HiddenWidget
|
13
|
+
|
14
|
+
class MyForm(BaseModel):
|
15
|
+
id: Annotated[str, HiddenWidget] = Field(...)
|
16
|
+
"""Identifier in the database."""
|
17
|
+
|
18
|
+
'''
|
19
|
+
|
5
20
|
def __init__(
|
6
21
|
self,
|
7
22
|
name: str,
|
8
23
|
*,
|
9
24
|
value: str,
|
10
25
|
token: str,
|
26
|
+
**kwargs: Any,
|
11
27
|
) -> None:
|
12
28
|
super().__init__(name, value=value, token=token)
|
13
29
|
|
@@ -16,22 +16,28 @@ class ModelWidget(Widget[Sequence[Widget[Any]]]):
|
|
16
16
|
error: str | None = None,
|
17
17
|
removable: bool,
|
18
18
|
title: str,
|
19
|
+
hint: str | None = None,
|
20
|
+
aria_label: str | None = None,
|
19
21
|
token: str,
|
22
|
+
nested: bool,
|
20
23
|
):
|
21
24
|
super().__init__(
|
22
25
|
name,
|
23
26
|
title=title,
|
27
|
+
hint=hint,
|
28
|
+
aria_label=aria_label,
|
24
29
|
value=value,
|
25
30
|
error=error,
|
26
31
|
removable=removable,
|
27
32
|
token=token,
|
28
33
|
)
|
34
|
+
self.nested = nested
|
29
35
|
|
30
36
|
def get_template(self) -> str:
|
31
37
|
return "pydantic_form.Model"
|
32
38
|
|
33
39
|
def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
|
34
|
-
"""Return the html version"""
|
40
|
+
"""Return the html version."""
|
35
41
|
children_widget = [child.to_html(renderer) for child in self.value or []]
|
36
42
|
kwargs = {
|
37
43
|
"widget": self,
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import Any,
|
1
|
+
from typing import Any, Sequence, Type
|
2
2
|
|
3
3
|
from markupsafe import Markup
|
4
4
|
|
@@ -12,9 +12,10 @@ class SequenceWidget(Widget[Sequence[Widget[Any]]]):
|
|
12
12
|
self,
|
13
13
|
name: str,
|
14
14
|
*,
|
15
|
-
title:
|
16
|
-
hint:
|
17
|
-
|
15
|
+
title: str | None,
|
16
|
+
hint: str | None = None,
|
17
|
+
aria_label: str | None = None,
|
18
|
+
value: Sequence[Widget[Any]] | None,
|
18
19
|
error: str | None = None,
|
19
20
|
item_type: Type[Any],
|
20
21
|
token: str,
|
@@ -25,11 +26,12 @@ class SequenceWidget(Widget[Sequence[Widget[Any]]]):
|
|
25
26
|
value=value,
|
26
27
|
error=error,
|
27
28
|
title=title,
|
29
|
+
hint=hint,
|
30
|
+
aria_label=aria_label,
|
28
31
|
token=token,
|
29
32
|
removable=removable,
|
30
33
|
)
|
31
34
|
self.item_type = item_type
|
32
|
-
self.hint = hint
|
33
35
|
|
34
36
|
def get_template(self) -> str:
|
35
37
|
return "pydantic_form/Sequence"
|
@@ -38,7 +40,7 @@ class SequenceWidget(Widget[Sequence[Widget[Any]]]):
|
|
38
40
|
return TypeWrapper(self.item_type, route_prefix, self.name, self.token)
|
39
41
|
|
40
42
|
def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
|
41
|
-
"""Return the html version"""
|
43
|
+
"""Return the html version."""
|
42
44
|
children = [Markup(item.to_html(renderer)) for item in self.value or []]
|
43
45
|
return Markup(
|
44
46
|
renderer.render_template(
|
@@ -4,32 +4,108 @@ from .base import Widget
|
|
4
4
|
|
5
5
|
|
6
6
|
class TextWidget(Widget[str]):
|
7
|
+
"""
|
8
|
+
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
|
+
"""
|
20
|
+
|
7
21
|
def __init__(
|
8
22
|
self,
|
9
23
|
name: str,
|
10
24
|
*,
|
11
25
|
title: Optional[str],
|
26
|
+
hint: Optional[str] = None,
|
12
27
|
aria_label: Optional[str] = None,
|
13
28
|
placeholder: Optional[str] = None,
|
14
29
|
error: str | None = None,
|
15
|
-
removable: bool = False,
|
16
30
|
value: str = "",
|
17
|
-
token: Optional[str] = None,
|
18
|
-
hint: Optional[str] = None,
|
19
31
|
input_type: str = "text",
|
32
|
+
removable: bool = False,
|
33
|
+
token: str,
|
20
34
|
) -> None:
|
21
35
|
super().__init__(
|
22
36
|
name,
|
23
37
|
value=value,
|
24
38
|
title=title,
|
39
|
+
hint=hint,
|
25
40
|
aria_label=aria_label,
|
26
41
|
token=token,
|
27
42
|
error=error,
|
28
43
|
removable=removable,
|
29
44
|
)
|
30
45
|
self.placeholder = placeholder or ""
|
31
|
-
self.hint = hint
|
32
46
|
self.input_type = input_type
|
33
47
|
|
34
48
|
def get_template(self) -> str:
|
35
49
|
return "pydantic_form.Text"
|
50
|
+
|
51
|
+
|
52
|
+
class TextareaWidget(Widget[str]):
|
53
|
+
"""
|
54
|
+
Render a Textearea for a string or event a sequence of string.
|
55
|
+
|
56
|
+
```
|
57
|
+
from fastlife.templating.renderer.widgets.text import TextareaWidget
|
58
|
+
from pydantic import BaseModel, Field, field_validator
|
59
|
+
|
60
|
+
class TaggedParagraphForm(BaseModel):
|
61
|
+
paragraph: Annotated[str, TextareaWidget] = Field(...)
|
62
|
+
tags: Annotated[Sequence[str], TextareaWidget] = Field(
|
63
|
+
default_factory=list,
|
64
|
+
title="Tags",
|
65
|
+
description="One tag per line",
|
66
|
+
)
|
67
|
+
|
68
|
+
@field_validator("tags", mode="before")
|
69
|
+
def split(cls, s: Any) -> Sequence[str]:
|
70
|
+
return s.split() if s else []
|
71
|
+
```
|
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
|
+
"""
|
84
|
+
|
85
|
+
def __init__(
|
86
|
+
self,
|
87
|
+
name: str,
|
88
|
+
*,
|
89
|
+
title: Optional[str],
|
90
|
+
hint: Optional[str] = None,
|
91
|
+
aria_label: Optional[str] = None,
|
92
|
+
placeholder: Optional[str] = None,
|
93
|
+
error: str | None = None,
|
94
|
+
value: str = "",
|
95
|
+
removable: bool = False,
|
96
|
+
token: str,
|
97
|
+
) -> None:
|
98
|
+
super().__init__(
|
99
|
+
name,
|
100
|
+
value=value,
|
101
|
+
title=title,
|
102
|
+
hint=hint,
|
103
|
+
aria_label=aria_label,
|
104
|
+
token=token,
|
105
|
+
error=error,
|
106
|
+
removable=removable,
|
107
|
+
)
|
108
|
+
self.placeholder = placeholder or ""
|
109
|
+
|
110
|
+
def get_template(self) -> str:
|
111
|
+
return "pydantic_form.Textarea"
|
@@ -1,3 +1,6 @@
|
|
1
|
+
"""
|
2
|
+
Widget for field of type Union.
|
3
|
+
"""
|
1
4
|
from typing import Any, Optional, Sequence, Type, Union
|
2
5
|
|
3
6
|
from markupsafe import Markup
|
@@ -9,22 +12,41 @@ from .base import TypeWrapper, Widget
|
|
9
12
|
|
10
13
|
|
11
14
|
class UnionWidget(Widget[Widget[Any]]):
|
15
|
+
"""
|
16
|
+
Widget for union types.
|
17
|
+
|
18
|
+
:param name: input name.
|
19
|
+
:param title: title for the widget.
|
20
|
+
:param hint: hint for human.
|
21
|
+
:param aria_label: html input aria-label value.
|
22
|
+
:param value: current value.
|
23
|
+
:param error: error of the value if any.
|
24
|
+
:param children_types: childrens types list.
|
25
|
+
:param removable: display a button to remove the widget for optional fields.
|
26
|
+
:param token: token used to get unique id on the form.
|
27
|
+
|
28
|
+
"""
|
29
|
+
|
12
30
|
def __init__(
|
13
31
|
self,
|
14
32
|
name: str,
|
15
33
|
*,
|
16
34
|
title: Optional[str],
|
35
|
+
hint: Optional[str] = None,
|
36
|
+
aria_label: Optional[str] = None,
|
17
37
|
value: Optional[Widget[Any]],
|
18
38
|
error: str | None = None,
|
19
39
|
children_types: Sequence[Type[BaseModel]],
|
40
|
+
removable: bool = False,
|
20
41
|
token: str,
|
21
|
-
removable: bool,
|
22
42
|
):
|
23
43
|
super().__init__(
|
24
44
|
name,
|
25
45
|
value=value,
|
26
46
|
error=error,
|
27
47
|
title=title,
|
48
|
+
hint=hint,
|
49
|
+
aria_label=aria_label,
|
28
50
|
token=token,
|
29
51
|
removable=removable,
|
30
52
|
)
|
@@ -32,6 +54,7 @@ class UnionWidget(Widget[Widget[Any]]):
|
|
32
54
|
self.parent_name = name
|
33
55
|
|
34
56
|
def build_types(self, route_prefix: str) -> Sequence[TypeWrapper]:
|
57
|
+
"""Wrap types in the union in order to get the in their own widgets."""
|
35
58
|
return [
|
36
59
|
TypeWrapper(typ, route_prefix, self.name, self.token)
|
37
60
|
for typ in self.children_types
|
@@ -41,7 +64,7 @@ class UnionWidget(Widget[Widget[Any]]):
|
|
41
64
|
return "pydantic_form.Union"
|
42
65
|
|
43
66
|
def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
|
44
|
-
"""Return the html version"""
|
67
|
+
"""Return the html version."""
|
45
68
|
child = Markup(self.value.to_html(renderer)) if self.value else ""
|
46
69
|
return Markup(
|
47
70
|
renderer.render_template(
|