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.
Files changed (93) hide show
  1. CHANGELOG.md +18 -1
  2. fastlife/__init__.py +45 -13
  3. fastlife/adapters/__init__.py +1 -1
  4. fastlife/adapters/fastapi/__init__.py +9 -0
  5. fastlife/adapters/fastapi/form.py +26 -0
  6. fastlife/{request → adapters/fastapi}/form_data.py +1 -1
  7. fastlife/{request → adapters/fastapi}/localizer.py +4 -2
  8. fastlife/adapters/fastapi/request.py +33 -0
  9. fastlife/{routing → adapters/fastapi/routing}/route.py +3 -3
  10. fastlife/{routing → adapters/fastapi/routing}/router.py +1 -1
  11. fastlife/adapters/itsdangerous/__init__.py +3 -0
  12. fastlife/adapters/itsdangerous/session.py +50 -0
  13. fastlife/adapters/jinjax/jinjax_ext/inspectable_component.py +7 -7
  14. fastlife/adapters/jinjax/jinjax_ext/jinjax_doc.py +1 -1
  15. fastlife/adapters/jinjax/renderer.py +9 -57
  16. fastlife/adapters/jinjax/widget_factory/bool_builder.py +2 -2
  17. fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +5 -4
  18. fastlife/adapters/jinjax/widget_factory/enum_builder.py +2 -2
  19. fastlife/adapters/jinjax/widget_factory/factory.py +32 -23
  20. fastlife/adapters/jinjax/widget_factory/literal_builder.py +7 -6
  21. fastlife/adapters/jinjax/widget_factory/model_builder.py +3 -3
  22. fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +2 -2
  23. fastlife/adapters/jinjax/widget_factory/sequence_builder.py +3 -3
  24. fastlife/adapters/jinjax/widget_factory/set_builder.py +2 -2
  25. fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +7 -8
  26. fastlife/adapters/jinjax/widget_factory/union_builder.py +3 -3
  27. fastlife/adapters/jinjax/widgets/base.py +36 -36
  28. fastlife/adapters/jinjax/widgets/boolean.py +13 -34
  29. fastlife/adapters/jinjax/widgets/checklist.py +36 -42
  30. fastlife/adapters/jinjax/widgets/dropdown.py +32 -38
  31. fastlife/adapters/jinjax/widgets/hidden.py +7 -15
  32. fastlife/adapters/jinjax/widgets/model.py +36 -43
  33. fastlife/adapters/jinjax/widgets/sequence.py +63 -42
  34. fastlife/adapters/jinjax/widgets/text.py +39 -78
  35. fastlife/adapters/jinjax/widgets/union.py +51 -58
  36. fastlife/components/CsrfToken.jinja +1 -1
  37. fastlife/components/Form.jinja +1 -1
  38. fastlife/components/pydantic_form/FatalError.jinja +8 -0
  39. fastlife/components/pydantic_form/Widget.jinja +4 -3
  40. fastlife/config/__init__.py +3 -6
  41. fastlife/config/configurator.py +80 -32
  42. fastlife/config/exceptions.py +0 -2
  43. fastlife/config/resources.py +1 -2
  44. fastlife/config/views.py +2 -4
  45. fastlife/domain/__init__.py +1 -0
  46. fastlife/domain/model/__init__.py +1 -0
  47. fastlife/domain/model/asgi.py +3 -0
  48. fastlife/domain/model/csrf.py +19 -0
  49. fastlife/{request → domain/model}/form.py +13 -22
  50. fastlife/{request → domain/model}/request.py +26 -30
  51. fastlife/domain/model/security_policy.py +105 -0
  52. fastlife/{templates/inline.py → domain/model/template.py} +8 -0
  53. fastlife/domain/model/types.py +17 -0
  54. fastlife/middlewares/base.py +1 -1
  55. fastlife/middlewares/reverse_proxy/x_forwarded.py +1 -2
  56. fastlife/middlewares/session/__init__.py +2 -2
  57. fastlife/middlewares/session/middleware.py +4 -3
  58. fastlife/middlewares/session/serializer.py +0 -44
  59. fastlife/{services/policy.py → service/check_permission.py} +1 -1
  60. fastlife/{security → service}/csrf.py +5 -15
  61. fastlife/{services → service}/locale_negociator.py +5 -8
  62. fastlife/{config → service}/registry.py +13 -7
  63. fastlife/service/security_policy.py +100 -0
  64. fastlife/{services → service}/templates.py +10 -48
  65. fastlife/{services → service}/translations.py +15 -0
  66. fastlife/{config/settings.py → settings.py} +6 -12
  67. fastlife/shared_utils/infer.py +24 -1
  68. fastlife/{templates/constants.py → template_globals.py} +2 -2
  69. fastlife/testing/testclient.py +2 -2
  70. fastlife/views/__init__.py +1 -0
  71. fastlife/views/pydantic_form.py +6 -0
  72. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/METADATA +1 -1
  73. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/RECORD +79 -80
  74. tailwind.config.js +1 -1
  75. fastlife/components/pydantic_form/Boolean.jinja +0 -13
  76. fastlife/components/pydantic_form/Checklist.jinja +0 -21
  77. fastlife/components/pydantic_form/Dropdown.jinja +0 -18
  78. fastlife/components/pydantic_form/Hidden.jinja +0 -3
  79. fastlife/components/pydantic_form/Model.jinja +0 -30
  80. fastlife/components/pydantic_form/Sequence.jinja +0 -47
  81. fastlife/components/pydantic_form/Text.jinja +0 -11
  82. fastlife/components/pydantic_form/Textarea.jinja +0 -38
  83. fastlife/components/pydantic_form/Union.jinja +0 -34
  84. fastlife/request/__init__.py +0 -5
  85. fastlife/security/__init__.py +0 -1
  86. fastlife/security/policy.py +0 -188
  87. fastlife/templates/__init__.py +0 -12
  88. fastlife/templates/binding.py +0 -52
  89. /fastlife/{routing → adapters/fastapi/routing}/__init__.py +0 -0
  90. /fastlife/{services → service}/__init__.py +0 -0
  91. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/WHEEL +0 -0
  92. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/entry_points.txt +0 -0
  93. {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[str | int | str | float | Decimal | UUID]):
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, int | str | float | Decimal | UUID)
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: int | str | float | Decimal | UUID | None,
27
+ value: Builtins | None,
29
28
  form_errors: Mapping[str, Any],
30
29
  removable: bool,
31
- ) -> Widget[int | str | float | Decimal | UUID]:
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.services.templates import AbstractTemplateRenderer
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(abc.ABC, Generic[T]):
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
- value: T | None
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
- aria_label: str
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
- def __init__(
56
- self,
57
- name: str,
58
- *,
59
- value: T | None = None,
60
- error: str | None = None,
61
- title: str | None = None,
62
- hint: str | None = None,
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.get_template(), widget=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
- def __init__(
23
- self,
24
- name: str,
25
- *,
26
- title: str | None,
27
- hint: str | None = None,
28
- aria_label: str | None = None,
29
- value: bool = False,
30
- error: str | None = None,
31
- removable: bool = False,
32
- token: str,
33
- ) -> None:
34
- super().__init__(
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
- @property
23
- def id(self) -> str:
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
- @property
28
- def field_name(self) -> str:
29
- return f"{self.name}[]"
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
- def __init__(
47
- self,
48
- name: str,
49
- *,
50
- title: str | None,
51
- hint: str | None = None,
52
- aria_label: str | None = None,
53
- value: Sequence[Checkable],
54
- error: str | None = None,
55
- token: str,
56
- removable: bool,
57
- ) -> None:
58
- super().__init__(
59
- name,
60
- value=value,
61
- error=error,
62
- token=token,
63
- title=title,
64
- hint=hint,
65
- aria_label=aria_label,
66
- removable=removable,
67
- )
68
-
69
- def get_template(self) -> str:
70
- return "pydantic_form.Checklist.jinja"
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
- :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.
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
- def __init__(
26
- self,
27
- name: str,
28
- *,
29
- title: str | None,
30
- hint: str | None = None,
31
- aria_label: str | None = None,
32
- value: str | None = None,
33
- error: str | None = None,
34
- options: Sequence[tuple[str, str]] | Sequence[str],
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
- self.options.append({"value": opt[0], "text": opt[1]})
47
+ ret.append({"value": opt[0], "text": opt[1]})
52
48
  else:
53
- self.options.append({"value": opt, "text": opt})
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 typing import Any
3
+ from fastlife.domain.model.types import Builtins
4
4
 
5
5
  from .base import Widget
6
6
 
7
7
 
8
- class HiddenWidget(Widget[str]):
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
- def __init__(
23
- self,
24
- name: str,
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
- from fastlife.services.templates import AbstractTemplateRenderer
9
-
10
- from .base import Widget
11
-
12
-
13
- class ModelWidget(Widget[Sequence[Widget[Any]]]):
14
- def __init__(
15
- self,
16
- name: str,
17
- *,
18
- value: Sequence[Widget[Any]],
19
- error: str | None = None,
20
- removable: bool,
21
- title: str,
22
- hint: str | None = None,
23
- aria_label: str | None = None,
24
- token: str,
25
- nested: bool,
26
- ):
27
- super().__init__(
28
- name,
29
- title=title,
30
- hint=hint,
31
- aria_label=aria_label,
32
- value=value,
33
- error=error,
34
- removable=removable,
35
- token=token,
36
- )
37
- self.nested = nested
38
-
39
- def get_template(self) -> str:
40
- return "pydantic_form.Model.jinja"
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
- children_widget = [child.to_html(renderer) for child in self.value or []]
45
- kwargs = {
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))