fastlifeweb 0.2.3__py3-none-any.whl → 0.3.1__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 +5 -0
- fastlife/configurator/configurator.py +2 -2
- fastlife/configurator/registry.py +7 -3
- fastlife/configurator/settings.py +1 -1
- fastlife/request/form_data.py +2 -7
- fastlife/security/csrf.py +1 -1
- fastlife/session/middleware.py +0 -1
- fastlife/shared_utils/__init__.py +0 -0
- fastlife/shared_utils/resolver.py +5 -3
- fastlife/templates/A.jinja +5 -0
- fastlife/templates/Button.jinja +27 -0
- fastlife/templates/Checkbox.jinja +2 -0
- fastlife/templates/CsrfToken.jinja +2 -0
- fastlife/templates/Form.jinja +5 -0
- fastlife/templates/H1.jinja +4 -0
- fastlife/templates/H2.jinja +4 -0
- fastlife/templates/Hidden.jinja +6 -0
- fastlife/templates/Input.jinja +8 -0
- fastlife/templates/Label.jinja +3 -0
- fastlife/templates/Option.jinja +2 -0
- fastlife/templates/Radio.jinja +8 -0
- fastlife/templates/Select.jinja +8 -0
- fastlife/templates/__init__.py +0 -0
- fastlife/templates/pydantic_form/Boolean.jinja +7 -0
- fastlife/templates/pydantic_form/Dropdown.jinja +17 -0
- fastlife/templates/pydantic_form/Hidden.jinja +2 -0
- fastlife/templates/pydantic_form/Model.jinja +16 -0
- fastlife/templates/pydantic_form/Sequence.jinja +37 -0
- fastlife/templates/pydantic_form/Text.jinja +12 -0
- fastlife/templates/pydantic_form/Union.jinja +26 -0
- fastlife/templates/pydantic_form/Widget.jinja +10 -0
- fastlife/templating/__init__.py +2 -2
- fastlife/templating/binding.py +6 -7
- fastlife/templating/renderer/__init__.py +2 -2
- fastlife/templating/renderer/abstract.py +9 -7
- fastlife/templating/renderer/jinjax.py +73 -0
- fastlife/templating/renderer/widgets/base.py +14 -13
- fastlife/templating/renderer/widgets/boolean.py +1 -1
- fastlife/templating/renderer/widgets/dropdown.py +7 -6
- fastlife/templating/renderer/widgets/factory.py +14 -7
- fastlife/templating/renderer/widgets/hidden.py +1 -1
- fastlife/templating/renderer/widgets/model.py +8 -10
- fastlife/templating/renderer/widgets/sequence.py +4 -4
- fastlife/templating/renderer/widgets/text.py +1 -1
- fastlife/templating/renderer/widgets/union.py +5 -4
- fastlife/testing/testclient.py +115 -10
- fastlife/views/pydantic_form.py +8 -3
- {fastlifeweb-0.2.3.dist-info → fastlifeweb-0.3.1.dist-info}/METADATA +3 -3
- fastlifeweb-0.3.1.dist-info/RECORD +63 -0
- {fastlifeweb-0.2.3.dist-info → fastlifeweb-0.3.1.dist-info}/WHEEL +1 -1
- fastlife/templates/base.jinja2 +0 -2
- fastlife/templates/globals.jinja2 +0 -83
- fastlife/templates/pydantic_form/boolean.jinja2 +0 -8
- fastlife/templates/pydantic_form/dropdown.jinja2 +0 -18
- fastlife/templates/pydantic_form/hidden.jinja2 +0 -1
- fastlife/templates/pydantic_form/model.jinja2 +0 -16
- fastlife/templates/pydantic_form/sequence.jinja2 +0 -41
- fastlife/templates/pydantic_form/text.jinja2 +0 -16
- fastlife/templates/pydantic_form/union.jinja2 +0 -40
- fastlife/templates/pydantic_form/widget.jinja2 +0 -10
- fastlife/templating/renderer/jinja2.py +0 -110
- fastlifeweb-0.2.3.dist-info/RECORD +0 -50
- {fastlifeweb-0.2.3.dist-info → fastlifeweb-0.3.1.dist-info}/LICENSE +0 -0
fastlife/__init__.py
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
from .configurator import Configurator, configure
|
2
|
+
from .configurator.registry import Registry
|
3
|
+
from .request.form_data import model
|
2
4
|
from .templating import Template, template
|
3
5
|
|
4
6
|
__all__ = [
|
@@ -7,4 +9,7 @@ __all__ = [
|
|
7
9
|
"Configurator",
|
8
10
|
"template",
|
9
11
|
"Template",
|
12
|
+
"Registry",
|
13
|
+
# Model
|
14
|
+
"model",
|
10
15
|
]
|
@@ -2,8 +2,8 @@
|
|
2
2
|
The configurator is here to register routes in a fastapi app,
|
3
3
|
with dependency injection.
|
4
4
|
"""
|
5
|
-
import inspect
|
6
5
|
import importlib
|
6
|
+
import inspect
|
7
7
|
import logging
|
8
8
|
from enum import Enum
|
9
9
|
from pathlib import Path
|
@@ -31,7 +31,7 @@ from fastlife.security.csrf import check_csrf
|
|
31
31
|
from .settings import Settings
|
32
32
|
|
33
33
|
if TYPE_CHECKING:
|
34
|
-
from .registry import AppRegistry
|
34
|
+
from .registry import AppRegistry # coverage: ignore
|
35
35
|
|
36
36
|
log = logging.getLogger(__name__)
|
37
37
|
VENUSIAN_CATEGORY = "fastlife"
|
@@ -1,17 +1,21 @@
|
|
1
|
-
from typing import Annotated
|
1
|
+
from typing import TYPE_CHECKING, Annotated
|
2
2
|
|
3
3
|
from fastapi import Depends
|
4
4
|
|
5
5
|
from fastlife.security.policy import CheckPermission
|
6
6
|
from fastlife.shared_utils.resolver import resolve
|
7
|
-
|
7
|
+
|
8
|
+
if TYPE_CHECKING:
|
9
|
+
from fastlife.templating.renderer import ( # coverage: ignore
|
10
|
+
AbstractTemplateRenderer, # coverage: ignore
|
11
|
+
) # coverage: ignore
|
8
12
|
|
9
13
|
from .settings import Settings
|
10
14
|
|
11
15
|
|
12
16
|
class AppRegistry:
|
13
17
|
settings: Settings
|
14
|
-
renderer: AbstractTemplateRenderer
|
18
|
+
renderer: "AbstractTemplateRenderer"
|
15
19
|
check_permission: CheckPermission
|
16
20
|
|
17
21
|
def __init__(self, settings: Settings) -> None:
|
@@ -12,7 +12,7 @@ class Settings(BaseSettings):
|
|
12
12
|
template_search_path: str = Field(default="fastlife:templates")
|
13
13
|
registry_class: str = Field(default="fastlife.configurator.registry:AppRegistry")
|
14
14
|
template_renderer_class: str = Field(
|
15
|
-
default="fastlife.templating.renderer
|
15
|
+
default="fastlife.templating.renderer:JinjaxTemplateRenderer"
|
16
16
|
)
|
17
17
|
form_data_model_prefix: str = Field(default="payload")
|
18
18
|
csrf_token_name: str = Field(default="csrf_token")
|
fastlife/request/form_data.py
CHANGED
@@ -26,7 +26,6 @@ def unflatten_struct(
|
|
26
26
|
) -> Mapping[str, Any] | Sequence[Any]:
|
27
27
|
# we sort to ensure that list index are ordered
|
28
28
|
formkeys = sorted(flatten_input.keys())
|
29
|
-
|
30
29
|
for key in formkeys:
|
31
30
|
if csrf_token_name is not None and key == csrf_token_name:
|
32
31
|
continue
|
@@ -39,15 +38,11 @@ def unflatten_struct(
|
|
39
38
|
raise ValueError(f"{flatten_input}: Not a list")
|
40
39
|
while int(key) != len(unflattened_output):
|
41
40
|
unflattened_output.append(None)
|
42
|
-
if not int(key) == len(unflattened_output):
|
43
|
-
raise ValueError(
|
44
|
-
f"{flatten_input}: Missing index {len(unflattened_output)}"
|
45
|
-
)
|
46
41
|
unflattened_output.append(flatten_input[key])
|
47
42
|
elif isinstance(unflattened_output, dict):
|
48
43
|
unflattened_output[key] = flatten_input[key]
|
49
44
|
else:
|
50
|
-
raise
|
45
|
+
raise TypeError(type(unflattened_output))
|
51
46
|
continue
|
52
47
|
|
53
48
|
child_is_list = rest.partition(".")[0].isdigit()
|
@@ -74,7 +69,7 @@ def unflatten_struct(
|
|
74
69
|
level + 1,
|
75
70
|
)
|
76
71
|
else:
|
77
|
-
raise
|
72
|
+
raise TypeError(type(unflattened_output))
|
78
73
|
|
79
74
|
return unflattened_output
|
80
75
|
|
fastlife/security/csrf.py
CHANGED
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Callable, Coroutine
|
|
4
4
|
from fastapi import Request
|
5
5
|
|
6
6
|
if TYPE_CHECKING:
|
7
|
-
from fastlife.configurator.registry import Registry
|
7
|
+
from fastlife.configurator.registry import Registry # coverage: ignore
|
8
8
|
|
9
9
|
|
10
10
|
class CSRFAttack(Exception):
|
fastlife/session/middleware.py
CHANGED
@@ -39,7 +39,6 @@ class SessionMiddleware(AbstractMiddleware):
|
|
39
39
|
if scope["type"] not in ("http", "websocket"): # pragma: no cover
|
40
40
|
await self.app(scope, receive, send)
|
41
41
|
return
|
42
|
-
|
43
42
|
connection = HTTPConnection(scope)
|
44
43
|
existing_session = self.cookie_name in connection.cookies
|
45
44
|
if existing_session:
|
File without changes
|
@@ -8,16 +8,18 @@ def resolve(value: str) -> Any:
|
|
8
8
|
module_name, attr_name = value.split(":", 2)
|
9
9
|
spec = importlib.util.find_spec(module_name)
|
10
10
|
if spec is None:
|
11
|
-
raise ValueError(f"Module {module_name} not found
|
11
|
+
raise ValueError(f"Module {module_name} not found")
|
12
12
|
module = importlib.util.module_from_spec(spec)
|
13
13
|
if spec.loader is None:
|
14
|
-
|
14
|
+
# I can't figure out when the spec.loader is None,
|
15
|
+
# just relying on typing here
|
16
|
+
raise ValueError("No loader on spec") # coverage: ignore
|
15
17
|
spec.loader.exec_module(module)
|
16
18
|
|
17
19
|
try:
|
18
20
|
attr = getattr(module, attr_name)
|
19
21
|
except AttributeError:
|
20
|
-
raise ValueError(f"Attribute {attr_name} not found in module {module_name}
|
22
|
+
raise ValueError(f"Attribute {attr_name} not found in module {module_name}")
|
21
23
|
|
22
24
|
return attr
|
23
25
|
|
@@ -0,0 +1,5 @@
|
|
1
|
+
{# def href, target="#maincontent", swap="innerHTML show:body:top", push_url=true #}
|
2
|
+
<a href="{{href}}" hx-get="{{href}}" hx-target="{{target}}" hx-swap="{{swap}}" {% if push_url %}hx-push-url="true" {%- endif %}
|
3
|
+
class="text-neutral-900 bg-neutral-200 hover:bg-neutral-50 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-neutral-400 dark:hover:bg-neutral-300 dark:focus:ring-neutral-100">
|
4
|
+
{{ content }}
|
5
|
+
</a>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
{# def
|
2
|
+
type="submit",
|
3
|
+
id="",
|
4
|
+
name="action",
|
5
|
+
value="submit",
|
6
|
+
onclick="",
|
7
|
+
target="",
|
8
|
+
swap="",
|
9
|
+
get="",
|
10
|
+
select="",
|
11
|
+
after_request="",
|
12
|
+
vals="",
|
13
|
+
full_width=false,
|
14
|
+
hidden=false,
|
15
|
+
aria_label=""
|
16
|
+
#}
|
17
|
+
{% set css = "text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300
|
18
|
+
font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700
|
19
|
+
dark:focus:ring-primary-800" %}
|
20
|
+
<button type="{{type}}" {%if id %}id="{{id}}" {%endif%} name="{{name}}" value="{{value}}" {% if
|
21
|
+
target%}hx-target="{{target}}" {% endif %} {% if swap %}hx-swap="{{swap}}" {% endif %} {% if select
|
22
|
+
%}hx-select="{{select}}" {% endif %} {% if get %}hx-get="{{get}}" {% endif %} {% if onclick %}onclick="{{onclick}}" {%
|
23
|
+
endif %}{% if after_request %}hx-on::after-request="{{after_request}}" {% endif %} {% if vals
|
24
|
+
%}hx-vals='{{vals|safe}}' {% endif %} {% if aria_label %}aria-label="{{aria_label}}" {% endif %}
|
25
|
+
class="{% if full_width %}w-full {% endif %}{{css}}" {% if hidden %}hidden{% endif %}>
|
26
|
+
{{ content}}
|
27
|
+
</button>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
{# def name, value="", input_type="text", id="", aria_label="", placeholder="" #}
|
2
|
+
{% set css = "bg-neutral-50 border border-neutral-300 text-neutral-900 text-base rounded-lg focus:ring-primary-500
|
3
|
+
focus:border-primary-500 block w-full p-2.5 dark:bg-neutral-700 dark:border-neutral-600 dark:placeholder-neutral-400
|
4
|
+
dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" %}
|
5
|
+
|
6
|
+
<input name="{{name}}" value="{{value}}" type="{{input_type}}" id="{{id}}" {% if aria_label
|
7
|
+
%}aria-label="{{aria_label}}" {% endif %} {% if placeholder %}placeholder="{{placeholder}}" {% endif %}
|
8
|
+
class="{{css}}" />
|
@@ -0,0 +1,8 @@
|
|
1
|
+
{# def label name id value checked=false disabled=false onclick="" #}
|
2
|
+
<div class="flex items-center mb-4">
|
3
|
+
<input type="radio" name="{{name}}" id="{{id}}" {% if value %}value="{{value}}" {% endif %}{% if checked %}checked{%
|
4
|
+
endif %}
|
5
|
+
class="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-neutral-800 focus:ring-2 dark:bg-neutral-700 dark:border-neutral-600"
|
6
|
+
{% if onclick %}onclick="{{onclick}}" {% endif %} {%if disabled%}disabled{%endif%}>
|
7
|
+
<label for="{{id}}" class="ms-2 text-sm font-medium text-neutral-900 dark:text-neutral-300">{{label}}</label>
|
8
|
+
</div>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
{# def name, id #}
|
2
|
+
{% set css = "bg-neutral-50 border border-neutral-300 text-neutral-900 text-base rounded-lg focus:ring-primary-500
|
3
|
+
focus:border-primary-500 block w-full p-2.5 dark:bg-neutral-700 dark:border-neutral-600 dark:placeholder-neutral-400
|
4
|
+
dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" %}
|
5
|
+
|
6
|
+
<select name="{{name}}" id="{{id}}" class="{{css}}">
|
7
|
+
{{content}}
|
8
|
+
</select>
|
File without changes
|
@@ -0,0 +1,7 @@
|
|
1
|
+
{# def widget #}
|
2
|
+
<pydantic_form.Widget widget={widget} removable={widget.removable}>
|
3
|
+
<div class="pt-4">
|
4
|
+
<Label for={widget.id}>{{widget.title}}</Label>
|
5
|
+
<Checkbox name={widget.name} value={widget.value} type="checkbox" id={widget.id} />
|
6
|
+
</div>
|
7
|
+
</pydantic_form.Widget>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
{# def widget #}
|
2
|
+
|
3
|
+
<pydantic_form.Widget widget={widget} removable={widget.removable}>
|
4
|
+
<div class="pt-4">
|
5
|
+
<Label for={widget.id}>{{widget.title}}</Label>
|
6
|
+
<Select name={widget.name} id={widget.id}>
|
7
|
+
{%- for opt in widget.options -%}
|
8
|
+
<Option value={opt.value}>
|
9
|
+
{{ opt.text }}
|
10
|
+
</Option>
|
11
|
+
{%- endfor -%}
|
12
|
+
</Select>
|
13
|
+
{%- if widget.help_text -%}
|
14
|
+
<span class="mt-2 text-sm text-neutral-500 dark:text-neutral-400">{{widget.help_text}}</span>
|
15
|
+
{%- endif %}
|
16
|
+
</div>
|
17
|
+
</pydantic_form.Widget>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
{# def widget, children_widget #}
|
2
|
+
|
3
|
+
<pydantic_form.Widget widget={widget} removable={widget.removable}>
|
4
|
+
<div id="{{widget.id}}" class="m-4">
|
5
|
+
<details open>
|
6
|
+
<summary class="justify-between items-center font-medium cursor-pointer">
|
7
|
+
<h4 class="inline font-sans text-3xl font-bold">{{widget.title}}</h4>
|
8
|
+
</summary>
|
9
|
+
<div>
|
10
|
+
{% for child in children_widget %}
|
11
|
+
{{ child }}
|
12
|
+
{% endfor %}
|
13
|
+
</div>
|
14
|
+
</details>
|
15
|
+
</div>
|
16
|
+
</pydantic_form.Widget>
|
@@ -0,0 +1,37 @@
|
|
1
|
+
{# def widget, children_widgets, type #}
|
2
|
+
|
3
|
+
<pydantic_form.Widget widget={widget} removable={widget.removable}>
|
4
|
+
<details id="{{widget.id}}" open>
|
5
|
+
<summary class="justify-between items-center font-medium cursor-pointer">
|
6
|
+
<h4 class="inline font-sans text-3xl font-bold">{{widget.title}}</h4>
|
7
|
+
</summary>
|
8
|
+
<div>
|
9
|
+
{% set fnGetName = "get" + widget.id.replace("-", "_") %}
|
10
|
+
<script>
|
11
|
+
function {{fnGetName}}() {
|
12
|
+
const el = document.getElementById("{{widget.id}}-content");
|
13
|
+
const len = el.dataset.length;
|
14
|
+
el.dataset.length = parseInt(len) + 1;
|
15
|
+
return "{{type.name}}." + len;
|
16
|
+
}
|
17
|
+
</script>
|
18
|
+
|
19
|
+
<div id="{{widget.id}}-content" class="m-4" data-length="{{children_widgets|length|string}}">
|
20
|
+
{% for child in children_widgets %}
|
21
|
+
{% set container_id = widget.id + "-container" %}
|
22
|
+
<div id="{{container_id}}">
|
23
|
+
{{ child }}
|
24
|
+
</div>
|
25
|
+
{% endfor%}
|
26
|
+
</div>
|
27
|
+
<div>
|
28
|
+
{% set container_id = "#" + widget.id + "-container" %}
|
29
|
+
{% set add_id = widget.id + "-add" %}
|
30
|
+
{% set vals = 'js:{"name": '+ fnGetName + '(), "token": "' + type.token + '", "removable": true}' %}
|
31
|
+
<Button type="button" target={container_id} swap="beforeend" id={add_id} vals={vals} get={type.url}>
|
32
|
+
Add
|
33
|
+
</Button>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
</details>
|
37
|
+
</pydantic_form.Widget>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
{# def widget #}
|
2
|
+
|
3
|
+
<pydantic_form.Widget widget={widget} removable={widget.removable}>
|
4
|
+
<div class="pt-4">
|
5
|
+
<Label for={widget.id}>{{widget.title}}</Label>
|
6
|
+
<Input name={widget.name} value={widget.value} type={widget.input_type} id={widget.id}
|
7
|
+
aria-label={widget.aria_label} placeholder={widget.placeholder} />
|
8
|
+
{%- if widget.help_text -%}
|
9
|
+
<span class="mt-2 text-sm text-neutral-500 dark:text-neutral-400">{{widget.help_text}}</span>
|
10
|
+
{%- endif %}
|
11
|
+
</div>
|
12
|
+
</pydantic_form.Widget>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
{# def widget, child, types, parent_type #}
|
2
|
+
|
3
|
+
<pydantic_form.Widget widget={widget}>
|
4
|
+
<div id="{{widget.id}}">
|
5
|
+
<details open>
|
6
|
+
<summary class="justify-between items-center font-medium cursor-pointer">
|
7
|
+
<h4 class="inline font-sans text-3xl font-bold">{{widget.title}}</h4>
|
8
|
+
</summary>
|
9
|
+
<div hx-sync="this" id="{{widget.id}}-child">
|
10
|
+
{% if child %}
|
11
|
+
{{ child }}
|
12
|
+
{% else %}
|
13
|
+
{% for typ in types %}
|
14
|
+
<Button type="button" target="closest div" get={typ.url} vals={typ.params|tojson}
|
15
|
+
id={typ.fullname + "-" + widget.token}
|
16
|
+
onclick={"document.getElementById('" + widget.id +"-remove-btn').hidden=false"}>{{typ.title}}</Button>
|
17
|
+
{% endfor %}
|
18
|
+
{% endif %}
|
19
|
+
</div>
|
20
|
+
<Button type="button" id={widget.id + "-remove-btn" } target={"#" + widget.id} vals={parent_type.params|tojson}
|
21
|
+
get={parent_type.url} hidden={not child}>
|
22
|
+
Remove
|
23
|
+
</Button>
|
24
|
+
</details>
|
25
|
+
</div>
|
26
|
+
</pydantic_form.Widget>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
{# def widget, removable=false #}
|
2
|
+
{% set container_id = widget.id + "-container" %}
|
3
|
+
<div id="{{container_id}}">
|
4
|
+
{{ content }}
|
5
|
+
{% if removable %}
|
6
|
+
<Button type="button" onclick={"document.getElementById('" + container_id + "').remove()"}>
|
7
|
+
Remove
|
8
|
+
</Button>
|
9
|
+
{% endif %}
|
10
|
+
</div>
|
fastlife/templating/__init__.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
from .binding import Template, template
|
2
|
-
from .renderer import AbstractTemplateRenderer,
|
2
|
+
from .renderer import AbstractTemplateRenderer, JinjaxTemplateRenderer
|
3
3
|
|
4
4
|
__all__ = [
|
5
5
|
"AbstractTemplateRenderer",
|
6
|
-
"
|
6
|
+
"JinjaxTemplateRenderer",
|
7
7
|
"Template",
|
8
8
|
"template",
|
9
9
|
]
|
fastlife/templating/binding.py
CHANGED
@@ -1,12 +1,11 @@
|
|
1
|
-
from typing import Any, Callable
|
1
|
+
from typing import Any, Callable
|
2
2
|
|
3
3
|
from fastapi import Depends, Request, Response
|
4
4
|
|
5
5
|
from fastlife.configurator.registry import Registry
|
6
6
|
from fastlife.security.csrf import create_csrf_token
|
7
7
|
|
8
|
-
|
9
|
-
Template = Callable[..., TemplateEngineHandler]
|
8
|
+
Template = Callable[..., Response]
|
10
9
|
TemplateEngine = Callable[["Registry", Request], Template]
|
11
10
|
|
12
11
|
|
@@ -19,12 +18,12 @@ def get_page_template(
|
|
19
18
|
*,
|
20
19
|
_create_csrf_token: Callable[..., str] = create_csrf_token,
|
21
20
|
) -> Template:
|
22
|
-
|
21
|
+
def parametrizer(**kwargs: Any) -> Response:
|
23
22
|
request.scope[reg.settings.csrf_token_name] = (
|
24
23
|
request.cookies.get(reg.settings.csrf_token_name)
|
25
24
|
or _create_csrf_token()
|
26
25
|
)
|
27
|
-
data =
|
26
|
+
data = reg.renderer.render_page(request, template, **kwargs)
|
28
27
|
resp = Response(data, headers={"Content-Type": content_type})
|
29
28
|
resp.set_cookie(
|
30
29
|
reg.settings.csrf_token_name,
|
@@ -44,8 +43,8 @@ def get_template(
|
|
44
43
|
template: str, *, content_type: str = "text/html"
|
45
44
|
) -> Callable[["Registry"], Template]:
|
46
45
|
def render_template(reg: "Registry") -> Template:
|
47
|
-
|
48
|
-
data =
|
46
|
+
def parametrizer(**kwargs: Any) -> Response:
|
47
|
+
data = reg.renderer.render_template(template, **kwargs)
|
49
48
|
resp = Response(data, headers={"Content-Type": content_type})
|
50
49
|
return resp
|
51
50
|
|
@@ -1,4 +1,4 @@
|
|
1
1
|
from .abstract import AbstractTemplateRenderer
|
2
|
-
from .
|
2
|
+
from .jinjax import JinjaxTemplateRenderer
|
3
3
|
|
4
|
-
__all__ = ["AbstractTemplateRenderer", "
|
4
|
+
__all__ = ["AbstractTemplateRenderer", "JinjaxTemplateRenderer"]
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import abc
|
2
|
-
from typing import Any, Mapping,
|
2
|
+
from typing import Any, Mapping, Type
|
3
3
|
|
4
4
|
from fastapi import Request
|
5
5
|
from markupsafe import Markup
|
6
|
+
from pydantic.fields import FieldInfo
|
6
7
|
|
7
8
|
|
8
9
|
class AbstractTemplateRenderer(abc.ABC):
|
@@ -10,20 +11,21 @@ class AbstractTemplateRenderer(abc.ABC):
|
|
10
11
|
"""Used to prefix url to fetch fast life widgets."""
|
11
12
|
|
12
13
|
@abc.abstractmethod
|
13
|
-
|
14
|
+
def render_page(self, request: Request, template: str, **params: Any) -> str:
|
14
15
|
...
|
15
16
|
|
16
17
|
@abc.abstractmethod
|
17
|
-
|
18
|
+
def render_template(self, template: str, **params: Any) -> str:
|
18
19
|
...
|
19
20
|
|
20
21
|
@abc.abstractmethod
|
21
|
-
|
22
|
+
def pydantic_form(
|
22
23
|
self,
|
23
24
|
model: Type[Any],
|
24
|
-
form_data:
|
25
|
-
name:
|
26
|
-
token:
|
25
|
+
form_data: Mapping[str, Any] | None = None,
|
26
|
+
name: str | None = None,
|
27
|
+
token: str | None = None,
|
27
28
|
removable: bool = False,
|
29
|
+
field: FieldInfo | None = None,
|
28
30
|
) -> Markup:
|
29
31
|
...
|
@@ -0,0 +1,73 @@
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence, Type
|
2
|
+
|
3
|
+
from fastapi import Request
|
4
|
+
from jinjax.catalog import Catalog
|
5
|
+
from markupsafe import Markup
|
6
|
+
from pydantic.fields import FieldInfo
|
7
|
+
|
8
|
+
from fastlife.templating.renderer.widgets.factory import WidgetFactory
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from fastlife.configurator.settings import Settings # coverage: ignore
|
12
|
+
|
13
|
+
from fastlife.shared_utils.resolver import resolve_path
|
14
|
+
|
15
|
+
from .abstract import AbstractTemplateRenderer
|
16
|
+
|
17
|
+
|
18
|
+
def build_searchpath(template_search_path: str) -> Sequence[str]:
|
19
|
+
searchpath: list[str] = []
|
20
|
+
paths = template_search_path.split(",")
|
21
|
+
|
22
|
+
for path in paths:
|
23
|
+
if ":" in path:
|
24
|
+
searchpath.append(resolve_path(path))
|
25
|
+
else:
|
26
|
+
searchpath.append(path)
|
27
|
+
return searchpath
|
28
|
+
|
29
|
+
|
30
|
+
class JinjaxTemplateRenderer(AbstractTemplateRenderer):
|
31
|
+
route_prefix: str
|
32
|
+
"""Used to prefix url to fetch fast life widgets."""
|
33
|
+
|
34
|
+
def __init__(self, settings: "Settings") -> None:
|
35
|
+
self.route_prefix = settings.fastlife_route_prefix
|
36
|
+
self.form_data_model_prefix = settings.form_data_model_prefix
|
37
|
+
self.csrf_token_name = settings.csrf_token_name
|
38
|
+
|
39
|
+
self.catalog = Catalog()
|
40
|
+
for path in build_searchpath(settings.template_search_path):
|
41
|
+
self.catalog.add_folder(path)
|
42
|
+
|
43
|
+
def render_page(self, request: Request, template: str, **params: Any) -> str:
|
44
|
+
return self.catalog.render( # type:ignore
|
45
|
+
template,
|
46
|
+
request=request,
|
47
|
+
csrf_token={
|
48
|
+
"name": self.csrf_token_name,
|
49
|
+
"value": request.scope.get(self.csrf_token_name, ""),
|
50
|
+
},
|
51
|
+
pydantic_form=self.pydantic_form,
|
52
|
+
**params
|
53
|
+
)
|
54
|
+
|
55
|
+
def render_template(self, template: str, **params: Any) -> str:
|
56
|
+
return self.catalog.render(template, **params) # type:ignore
|
57
|
+
|
58
|
+
def pydantic_form(
|
59
|
+
self,
|
60
|
+
model: Type[Any],
|
61
|
+
form_data: Optional[Mapping[str, Any]] = None,
|
62
|
+
name: Optional[str] = None,
|
63
|
+
token: Optional[str] = None,
|
64
|
+
removable: bool = False,
|
65
|
+
field: FieldInfo | None = None,
|
66
|
+
) -> Markup:
|
67
|
+
return WidgetFactory(self, token).get_markup(
|
68
|
+
model,
|
69
|
+
form_data or {},
|
70
|
+
prefix=(name or self.form_data_model_prefix),
|
71
|
+
removable=removable,
|
72
|
+
field=field,
|
73
|
+
)
|
@@ -42,18 +42,15 @@ class Widget(abc.ABC):
|
|
42
42
|
self.aria_label = aria_label or ""
|
43
43
|
self.token = token or secrets.token_urlsafe(4).replace("_", "-")
|
44
44
|
self.removable = removable
|
45
|
-
|
46
|
-
@property
|
47
|
-
def id(self) -> str:
|
48
|
-
return f"{self.name}-{self.token}".replace("_", "-").replace(".", "-")
|
45
|
+
self.id = f"{self.name}-{self.token}".replace("_", "-").replace(".", "-")
|
49
46
|
|
50
47
|
@abc.abstractmethod
|
51
48
|
def get_template(self) -> str:
|
52
49
|
...
|
53
50
|
|
54
|
-
|
51
|
+
def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
|
55
52
|
"""Return the html version"""
|
56
|
-
return Markup(
|
53
|
+
return Markup(renderer.render_template(self.get_template(), widget=self))
|
57
54
|
|
58
55
|
|
59
56
|
def _get_fullname(typ: Type[Any]) -> str:
|
@@ -64,11 +61,18 @@ def _get_fullname(typ: Type[Any]) -> str:
|
|
64
61
|
|
65
62
|
|
66
63
|
class TypeWrapper:
|
67
|
-
def __init__(
|
64
|
+
def __init__(
|
65
|
+
self,
|
66
|
+
typ: Type[Any],
|
67
|
+
route_prefix: str,
|
68
|
+
name: str,
|
69
|
+
token: str,
|
70
|
+
title: str | None = None,
|
71
|
+
):
|
68
72
|
self.typ = typ
|
69
73
|
self.route_prefix = route_prefix
|
70
74
|
self.name = name
|
71
|
-
self.title = get_title(typ)
|
75
|
+
self.title = title or get_title(typ)
|
72
76
|
self.token = token
|
73
77
|
|
74
78
|
@property
|
@@ -77,12 +81,9 @@ class TypeWrapper:
|
|
77
81
|
|
78
82
|
@property
|
79
83
|
def params(self) -> Mapping[str, str]:
|
80
|
-
return {"name": self.name, "token": self.token}
|
84
|
+
return {"name": self.name, "token": self.token, "title": self.title}
|
81
85
|
|
82
86
|
@property
|
83
87
|
def url(self) -> str:
|
84
|
-
ret =
|
85
|
-
f"{self.route_prefix}/pydantic-form/widgets/{self.fullname}"
|
86
|
-
# f"?name={self.name}&token={self.token}"
|
87
|
-
)
|
88
|
+
ret = f"{self.route_prefix}/pydantic-form/widgets/{self.fullname}"
|
88
89
|
return ret
|