codeforms 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- {codeforms-0.1.0 → codeforms-0.2.0}/.github/workflows/tests.yml +4 -1
- {codeforms-0.1.0 → codeforms-0.2.0}/.gitignore +5 -2
- codeforms-0.2.0/.python-version +1 -0
- {codeforms-0.1.0 → codeforms-0.2.0}/PKG-INFO +65 -3
- {codeforms-0.1.0 → codeforms-0.2.0}/README.md +63 -1
- codeforms-0.2.0/examples/conditional_visibility.py +103 -0
- codeforms-0.2.0/examples/custom_fields.py +184 -0
- codeforms-0.2.0/examples/dependent_options.py +73 -0
- codeforms-0.2.0/examples/wizard_form.py +129 -0
- {codeforms-0.1.0 → codeforms-0.2.0}/pyproject.toml +2 -2
- codeforms-0.2.0/src/codeforms/__init__.py +86 -0
- {codeforms-0.1.0 → codeforms-0.2.0}/src/codeforms/export.py +81 -12
- {codeforms-0.1.0 → codeforms-0.2.0}/src/codeforms/fields.py +102 -23
- {codeforms-0.1.0 → codeforms-0.2.0}/src/codeforms/forms.py +315 -17
- {codeforms-0.1.0 → codeforms-0.2.0}/src/codeforms/i18n.py +18 -0
- codeforms-0.2.0/src/codeforms/registry.py +194 -0
- codeforms-0.2.0/tests/test_dependent_options.py +118 -0
- codeforms-0.2.0/tests/test_dynamic_visibility.py +243 -0
- codeforms-0.2.0/tests/test_phase2_compat.py +311 -0
- codeforms-0.2.0/tests/test_registry.py +409 -0
- codeforms-0.2.0/tests/test_wizard_steps.py +382 -0
- codeforms-0.2.0/uv.lock +416 -0
- codeforms-0.1.0/.python-version +0 -1
- codeforms-0.1.0/src/codeforms/__init__.py +0 -30
- codeforms-0.1.0/uv.lock +0 -232
- {codeforms-0.1.0 → codeforms-0.2.0}/.github/workflows/python-publish.yml +0 -0
- {codeforms-0.1.0 → codeforms-0.2.0}/LICENSE +0 -0
- {codeforms-0.1.0 → codeforms-0.2.0}/examples/basic_usage.py +0 -0
- {codeforms-0.1.0 → codeforms-0.2.0}/examples/i18n_usage.py +0 -0
- {codeforms-0.1.0 → codeforms-0.2.0}/tests/__init__.py +0 -0
- {codeforms-0.1.0 → codeforms-0.2.0}/tests/conftest.py +0 -0
- {codeforms-0.1.0 → codeforms-0.2.0}/tests/test_basic_usage.py +0 -0
- {codeforms-0.1.0 → codeforms-0.2.0}/tests/test_i18n.py +0 -0
|
@@ -9,6 +9,9 @@ on:
|
|
|
9
9
|
jobs:
|
|
10
10
|
test:
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12"]
|
|
12
15
|
|
|
13
16
|
steps:
|
|
14
17
|
- name: Checkout
|
|
@@ -17,7 +20,7 @@ jobs:
|
|
|
17
20
|
- name: Set up Python
|
|
18
21
|
uses: actions/setup-python@v5
|
|
19
22
|
with:
|
|
20
|
-
python-version:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
21
24
|
|
|
22
25
|
- name: Install dependencies
|
|
23
26
|
run: |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeforms
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python library for creating, validating, and rendering web forms using Pydantic
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
8
|
Requires-Dist: pydantic[email]>=2.0
|
|
9
9
|
Provides-Extra: dev
|
|
10
10
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
@@ -26,7 +26,7 @@ Or with [uv](https://docs.astral.sh/uv/):
|
|
|
26
26
|
uv add codeforms
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Requires Python 3.
|
|
29
|
+
Requires Python 3.9+.
|
|
30
30
|
|
|
31
31
|
## Quick Start
|
|
32
32
|
|
|
@@ -258,6 +258,68 @@ print(result["errors"][0]["message"]) # "El campo name es requerido"
|
|
|
258
258
|
|
|
259
259
|
See [`examples/i18n_usage.py`](examples/i18n_usage.py) for a full working example.
|
|
260
260
|
|
|
261
|
+
## Custom Field Types
|
|
262
|
+
|
|
263
|
+
You can create your own field types by subclassing `FormFieldBase` and registering them with `register_field_type()`. Custom fields integrate seamlessly with forms, JSON serialization, validation, and HTML export.
|
|
264
|
+
|
|
265
|
+
### Defining a Custom Field
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from codeforms import FormFieldBase, register_field_type
|
|
269
|
+
|
|
270
|
+
class PhoneField(FormFieldBase):
|
|
271
|
+
field_type: str = "phone" # unique string identifier
|
|
272
|
+
country_code: str = "+1"
|
|
273
|
+
|
|
274
|
+
class RatingField(FormFieldBase):
|
|
275
|
+
field_type: str = "rating"
|
|
276
|
+
min_rating: int = 1
|
|
277
|
+
max_rating: int = 5
|
|
278
|
+
|
|
279
|
+
register_field_type(PhoneField)
|
|
280
|
+
register_field_type(RatingField)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Using Custom Fields in Forms
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
from codeforms import Form, TextField
|
|
287
|
+
|
|
288
|
+
form = Form(
|
|
289
|
+
name="feedback",
|
|
290
|
+
fields=[
|
|
291
|
+
TextField(name="name", label="Name", required=True),
|
|
292
|
+
PhoneField(name="phone", label="Phone", country_code="+54"),
|
|
293
|
+
RatingField(name="score", label="Score", max_rating=10),
|
|
294
|
+
],
|
|
295
|
+
)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### JSON Roundtrip
|
|
299
|
+
|
|
300
|
+
Custom fields serialize and deserialize automatically (as long as the field type is registered before deserialization):
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
import json
|
|
304
|
+
|
|
305
|
+
json_str = form.to_json()
|
|
306
|
+
restored = Form.loads(json_str)
|
|
307
|
+
|
|
308
|
+
assert isinstance(restored.fields[1], PhoneField)
|
|
309
|
+
assert restored.fields[1].country_code == "+54"
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Listing Registered Types
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from codeforms import get_registered_field_types
|
|
316
|
+
|
|
317
|
+
for name, classes in sorted(get_registered_field_types().items()):
|
|
318
|
+
print(f"{name}: {[c.__name__ for c in classes]}")
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
See [`examples/custom_fields.py`](examples/custom_fields.py) for a full working example.
|
|
322
|
+
|
|
261
323
|
## License
|
|
262
324
|
|
|
263
325
|
MIT
|
|
@@ -14,7 +14,7 @@ Or with [uv](https://docs.astral.sh/uv/):
|
|
|
14
14
|
uv add codeforms
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Requires Python 3.
|
|
17
|
+
Requires Python 3.9+.
|
|
18
18
|
|
|
19
19
|
## Quick Start
|
|
20
20
|
|
|
@@ -246,6 +246,68 @@ print(result["errors"][0]["message"]) # "El campo name es requerido"
|
|
|
246
246
|
|
|
247
247
|
See [`examples/i18n_usage.py`](examples/i18n_usage.py) for a full working example.
|
|
248
248
|
|
|
249
|
+
## Custom Field Types
|
|
250
|
+
|
|
251
|
+
You can create your own field types by subclassing `FormFieldBase` and registering them with `register_field_type()`. Custom fields integrate seamlessly with forms, JSON serialization, validation, and HTML export.
|
|
252
|
+
|
|
253
|
+
### Defining a Custom Field
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from codeforms import FormFieldBase, register_field_type
|
|
257
|
+
|
|
258
|
+
class PhoneField(FormFieldBase):
|
|
259
|
+
field_type: str = "phone" # unique string identifier
|
|
260
|
+
country_code: str = "+1"
|
|
261
|
+
|
|
262
|
+
class RatingField(FormFieldBase):
|
|
263
|
+
field_type: str = "rating"
|
|
264
|
+
min_rating: int = 1
|
|
265
|
+
max_rating: int = 5
|
|
266
|
+
|
|
267
|
+
register_field_type(PhoneField)
|
|
268
|
+
register_field_type(RatingField)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Using Custom Fields in Forms
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
from codeforms import Form, TextField
|
|
275
|
+
|
|
276
|
+
form = Form(
|
|
277
|
+
name="feedback",
|
|
278
|
+
fields=[
|
|
279
|
+
TextField(name="name", label="Name", required=True),
|
|
280
|
+
PhoneField(name="phone", label="Phone", country_code="+54"),
|
|
281
|
+
RatingField(name="score", label="Score", max_rating=10),
|
|
282
|
+
],
|
|
283
|
+
)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### JSON Roundtrip
|
|
287
|
+
|
|
288
|
+
Custom fields serialize and deserialize automatically (as long as the field type is registered before deserialization):
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
import json
|
|
292
|
+
|
|
293
|
+
json_str = form.to_json()
|
|
294
|
+
restored = Form.loads(json_str)
|
|
295
|
+
|
|
296
|
+
assert isinstance(restored.fields[1], PhoneField)
|
|
297
|
+
assert restored.fields[1].country_code == "+54"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Listing Registered Types
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
from codeforms import get_registered_field_types
|
|
304
|
+
|
|
305
|
+
for name, classes in sorted(get_registered_field_types().items()):
|
|
306
|
+
print(f"{name}: {[c.__name__ for c in classes]}")
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
See [`examples/custom_fields.py`](examples/custom_fields.py) for a full working example.
|
|
310
|
+
|
|
249
311
|
## License
|
|
250
312
|
|
|
251
313
|
MIT
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ejemplo de visibilidad condicional con visible_when.
|
|
3
|
+
|
|
4
|
+
Demuestra cómo ocultar/mostrar campos según el valor de otros campos,
|
|
5
|
+
y la diferencia entre validación legacy y dinámica.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from codeforms import (
|
|
9
|
+
Form,
|
|
10
|
+
TextField,
|
|
11
|
+
SelectField,
|
|
12
|
+
SelectOption,
|
|
13
|
+
VisibilityRule,
|
|
14
|
+
validate_form_data,
|
|
15
|
+
validate_form_data_dynamic,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_address_form() -> Form:
|
|
20
|
+
"""Crea un formulario de dirección con campos condicionales."""
|
|
21
|
+
return Form(
|
|
22
|
+
name="address_form",
|
|
23
|
+
fields=[
|
|
24
|
+
SelectField(
|
|
25
|
+
name="country",
|
|
26
|
+
label="Country",
|
|
27
|
+
required=True,
|
|
28
|
+
options=[
|
|
29
|
+
SelectOption(value="US", label="United States"),
|
|
30
|
+
SelectOption(value="AR", label="Argentina"),
|
|
31
|
+
SelectOption(value="UK", label="United Kingdom"),
|
|
32
|
+
],
|
|
33
|
+
),
|
|
34
|
+
# Solo visible cuando country == "US"
|
|
35
|
+
TextField(
|
|
36
|
+
name="state",
|
|
37
|
+
label="State",
|
|
38
|
+
required=True,
|
|
39
|
+
visible_when=[
|
|
40
|
+
VisibilityRule(field="country", operator="equals", value="US"),
|
|
41
|
+
],
|
|
42
|
+
),
|
|
43
|
+
# Solo visible cuando country == "AR"
|
|
44
|
+
TextField(
|
|
45
|
+
name="province",
|
|
46
|
+
label="Province",
|
|
47
|
+
required=True,
|
|
48
|
+
visible_when=[
|
|
49
|
+
VisibilityRule(field="country", operator="equals", value="AR"),
|
|
50
|
+
],
|
|
51
|
+
),
|
|
52
|
+
# Solo visible cuando country == "UK"
|
|
53
|
+
TextField(
|
|
54
|
+
name="county",
|
|
55
|
+
label="County",
|
|
56
|
+
required=True,
|
|
57
|
+
visible_when=[
|
|
58
|
+
VisibilityRule(field="country", operator="equals", value="UK"),
|
|
59
|
+
],
|
|
60
|
+
),
|
|
61
|
+
# Siempre visible
|
|
62
|
+
TextField(
|
|
63
|
+
name="city",
|
|
64
|
+
label="City",
|
|
65
|
+
required=True,
|
|
66
|
+
),
|
|
67
|
+
],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
form = create_address_form()
|
|
73
|
+
data_us = {"country": "US", "state": "California", "city": "Los Angeles"}
|
|
74
|
+
data_ar = {"country": "AR", "province": "Buenos Aires", "city": "CABA"}
|
|
75
|
+
|
|
76
|
+
# --- Legacy validation: ignores visible_when ---
|
|
77
|
+
print("=== Legacy validate_form_data (ignores visible_when) ===")
|
|
78
|
+
result = validate_form_data(form, data_us)
|
|
79
|
+
print(f"US data: success={result['success']}")
|
|
80
|
+
if not result["success"]:
|
|
81
|
+
print(f" Errors: {result['errors']}")
|
|
82
|
+
print(" (Province and County are required but missing — legacy doesn't know they're hidden)")
|
|
83
|
+
|
|
84
|
+
# --- Dynamic validation: respects visible_when ---
|
|
85
|
+
print("\n=== Dynamic validate_form_data_dynamic (respects visible_when) ===")
|
|
86
|
+
result = validate_form_data_dynamic(form, data_us, respect_visibility=True)
|
|
87
|
+
print(f"US data: success={result['success']}")
|
|
88
|
+
if result["success"]:
|
|
89
|
+
print(f" Validated data: {result['data']}")
|
|
90
|
+
print(" (Province and County are hidden, so not validated)")
|
|
91
|
+
|
|
92
|
+
result = validate_form_data_dynamic(form, data_ar, respect_visibility=True)
|
|
93
|
+
print(f"AR data: success={result['success']}")
|
|
94
|
+
if result["success"]:
|
|
95
|
+
print(f" Validated data: {result['data']}")
|
|
96
|
+
|
|
97
|
+
# --- Visible fields helper ---
|
|
98
|
+
print("\n=== get_visible_fields ===")
|
|
99
|
+
visible = form.get_visible_fields(data_us)
|
|
100
|
+
print(f"Visible fields for US: {[f.name for f in visible]}")
|
|
101
|
+
|
|
102
|
+
visible = form.get_visible_fields(data_ar)
|
|
103
|
+
print(f"Visible fields for AR: {[f.name for f in visible]}")
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom field types example for codeforms.
|
|
3
|
+
|
|
4
|
+
Demonstrates how to create and register custom field types,
|
|
5
|
+
use them in forms, and perform JSON roundtrip serialization.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import field_validator
|
|
12
|
+
|
|
13
|
+
from codeforms import (
|
|
14
|
+
Form,
|
|
15
|
+
FormFieldBase,
|
|
16
|
+
FieldGroup,
|
|
17
|
+
TextField,
|
|
18
|
+
EmailField,
|
|
19
|
+
register_field_type,
|
|
20
|
+
get_registered_field_types,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# 1. Define custom field types
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
class PhoneField(FormFieldBase):
|
|
29
|
+
"""A phone number field with an optional country code."""
|
|
30
|
+
field_type: str = "phone"
|
|
31
|
+
country_code: str = "+1"
|
|
32
|
+
placeholder: Optional[str] = "e.g. +1-555-0100"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RatingField(FormFieldBase):
|
|
36
|
+
"""A numeric rating field with configurable range."""
|
|
37
|
+
field_type: str = "rating"
|
|
38
|
+
min_rating: int = 1
|
|
39
|
+
max_rating: int = 5
|
|
40
|
+
|
|
41
|
+
@field_validator("max_rating")
|
|
42
|
+
@classmethod
|
|
43
|
+
def max_above_min(cls, v, info):
|
|
44
|
+
min_r = info.data.get("min_rating", 1)
|
|
45
|
+
if v <= min_r:
|
|
46
|
+
raise ValueError("max_rating must be greater than min_rating")
|
|
47
|
+
return v
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ColorField(FormFieldBase):
|
|
51
|
+
"""A colour picker field."""
|
|
52
|
+
field_type: str = "color"
|
|
53
|
+
color_format: str = "hex" # hex | rgb | hsl
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# 2. Register them
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
register_field_type(PhoneField)
|
|
61
|
+
register_field_type(RatingField)
|
|
62
|
+
register_field_type(ColorField)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def show_registered_types():
|
|
66
|
+
"""Print all registered field types."""
|
|
67
|
+
print("=" * 60)
|
|
68
|
+
print("Registered field types")
|
|
69
|
+
print("=" * 60)
|
|
70
|
+
for key, classes in sorted(get_registered_field_types().items()):
|
|
71
|
+
names = ", ".join(c.__name__ for c in classes)
|
|
72
|
+
print(f" {key:12s} → {names}")
|
|
73
|
+
print()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def create_form_with_custom_fields():
|
|
77
|
+
"""Build a form that mixes built-in and custom field types."""
|
|
78
|
+
print("=" * 60)
|
|
79
|
+
print("Form with custom fields")
|
|
80
|
+
print("=" * 60)
|
|
81
|
+
|
|
82
|
+
form = Form(
|
|
83
|
+
name="event_feedback",
|
|
84
|
+
content=[
|
|
85
|
+
FieldGroup(
|
|
86
|
+
title="Contact",
|
|
87
|
+
fields=[
|
|
88
|
+
TextField(name="name", label="Full name", required=True),
|
|
89
|
+
EmailField(name="email", label="Email"),
|
|
90
|
+
PhoneField(name="phone", label="Phone", country_code="+54"),
|
|
91
|
+
],
|
|
92
|
+
),
|
|
93
|
+
FieldGroup(
|
|
94
|
+
title="Feedback",
|
|
95
|
+
fields=[
|
|
96
|
+
RatingField(
|
|
97
|
+
name="overall_rating",
|
|
98
|
+
label="Overall rating",
|
|
99
|
+
max_rating=10,
|
|
100
|
+
),
|
|
101
|
+
ColorField(
|
|
102
|
+
name="fav_color",
|
|
103
|
+
label="Favourite colour",
|
|
104
|
+
color_format="rgb",
|
|
105
|
+
),
|
|
106
|
+
],
|
|
107
|
+
),
|
|
108
|
+
],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
print(f"Form: {form.name}")
|
|
112
|
+
print(f"Total fields: {len(form.fields)}")
|
|
113
|
+
for f in form.fields:
|
|
114
|
+
print(f" - {f.name} ({f.field_type_value})")
|
|
115
|
+
print()
|
|
116
|
+
return form
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def json_roundtrip(form: Form):
|
|
120
|
+
"""Serialize a form to JSON and back, preserving custom field data."""
|
|
121
|
+
print("=" * 60)
|
|
122
|
+
print("JSON roundtrip")
|
|
123
|
+
print("=" * 60)
|
|
124
|
+
|
|
125
|
+
json_str = form.to_json()
|
|
126
|
+
print("Serialized JSON (pretty):")
|
|
127
|
+
print(json.dumps(json.loads(json_str), indent=2))
|
|
128
|
+
print()
|
|
129
|
+
|
|
130
|
+
restored = Form.loads(json_str)
|
|
131
|
+
print("Restored form fields:")
|
|
132
|
+
for f in restored.fields:
|
|
133
|
+
extra = ""
|
|
134
|
+
if isinstance(f, PhoneField):
|
|
135
|
+
extra = f" (country_code={f.country_code})"
|
|
136
|
+
elif isinstance(f, RatingField):
|
|
137
|
+
extra = f" (max_rating={f.max_rating})"
|
|
138
|
+
elif isinstance(f, ColorField):
|
|
139
|
+
extra = f" (color_format={f.color_format})"
|
|
140
|
+
print(f" - {f.name}: {type(f).__name__}{extra}")
|
|
141
|
+
print()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def validate_custom_form(form: Form):
|
|
145
|
+
"""Validate user data against a form with custom fields."""
|
|
146
|
+
print("=" * 60)
|
|
147
|
+
print("Data validation")
|
|
148
|
+
print("=" * 60)
|
|
149
|
+
|
|
150
|
+
data = {
|
|
151
|
+
"name": "Juan",
|
|
152
|
+
"email": "juan@example.com",
|
|
153
|
+
"phone": "+54-11-5555-0100",
|
|
154
|
+
"overall_rating": "8",
|
|
155
|
+
"fav_color": "#3498db",
|
|
156
|
+
}
|
|
157
|
+
result = form.validate_data(data)
|
|
158
|
+
print(f"Input: {data}")
|
|
159
|
+
print(f"Result: success={result['success']}")
|
|
160
|
+
if result.get("errors"):
|
|
161
|
+
print(f"Errors: {result['errors']}")
|
|
162
|
+
print()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def export_html(form: Form):
|
|
166
|
+
"""Export the form to plain HTML."""
|
|
167
|
+
print("=" * 60)
|
|
168
|
+
print("HTML export")
|
|
169
|
+
print("=" * 60)
|
|
170
|
+
export = form.export("html")
|
|
171
|
+
print(export["output"][:500], "...")
|
|
172
|
+
print()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
# Run all examples
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
show_registered_types()
|
|
181
|
+
form = create_form_with_custom_fields()
|
|
182
|
+
json_roundtrip(form)
|
|
183
|
+
validate_custom_form(form)
|
|
184
|
+
export_html(form)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ejemplo de opciones dependientes con DependentOptionsConfig.
|
|
3
|
+
|
|
4
|
+
Demuestra cómo definir campos cuyos opciones cambian según el valor
|
|
5
|
+
de otro campo (por ejemplo, país → ciudades).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from codeforms import (
|
|
9
|
+
Form,
|
|
10
|
+
SelectField,
|
|
11
|
+
SelectOption,
|
|
12
|
+
DependentOptionsConfig,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_location_form() -> Form:
|
|
17
|
+
"""Crea un formulario con ciudades dependientes del país seleccionado."""
|
|
18
|
+
return Form(
|
|
19
|
+
name="location_form",
|
|
20
|
+
fields=[
|
|
21
|
+
SelectField(
|
|
22
|
+
name="country",
|
|
23
|
+
label="Country",
|
|
24
|
+
required=True,
|
|
25
|
+
options=[
|
|
26
|
+
SelectOption(value="US", label="United States"),
|
|
27
|
+
SelectOption(value="AR", label="Argentina"),
|
|
28
|
+
],
|
|
29
|
+
),
|
|
30
|
+
SelectField(
|
|
31
|
+
name="city",
|
|
32
|
+
label="City",
|
|
33
|
+
required=True,
|
|
34
|
+
# Opciones estáticas (todas las ciudades posibles para HTML rendering)
|
|
35
|
+
options=[
|
|
36
|
+
SelectOption(value="nyc", label="New York City"),
|
|
37
|
+
SelectOption(value="la", label="Los Angeles"),
|
|
38
|
+
SelectOption(value="bsas", label="Buenos Aires"),
|
|
39
|
+
SelectOption(value="cor", label="Córdoba"),
|
|
40
|
+
],
|
|
41
|
+
# Metadata de dependencia (para lógica dinámica en frontend o backend)
|
|
42
|
+
dependent_options=DependentOptionsConfig(
|
|
43
|
+
depends_on="country",
|
|
44
|
+
options_map={
|
|
45
|
+
"US": [
|
|
46
|
+
SelectOption(value="nyc", label="New York City"),
|
|
47
|
+
SelectOption(value="la", label="Los Angeles"),
|
|
48
|
+
],
|
|
49
|
+
"AR": [
|
|
50
|
+
SelectOption(value="bsas", label="Buenos Aires"),
|
|
51
|
+
SelectOption(value="cor", label="Córdoba"),
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
form = create_location_form()
|
|
62
|
+
|
|
63
|
+
# La metadata de dependencia se serializa a JSON
|
|
64
|
+
import json
|
|
65
|
+
data = json.loads(form.model_dump_json(exclude_none=True))
|
|
66
|
+
city_field = data["content"][1]
|
|
67
|
+
print("City field dependent_options:")
|
|
68
|
+
print(json.dumps(city_field["dependent_options"], indent=2))
|
|
69
|
+
|
|
70
|
+
# Las opciones específicas para cada país
|
|
71
|
+
dep = form.fields[1].dependent_options
|
|
72
|
+
print(f"\nOptions for US: {[o.label for o in dep.options_map['US']]}")
|
|
73
|
+
print(f"Options for AR: {[o.label for o in dep.options_map['AR']]}")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ejemplo de formulario multi-paso (wizard) con FormStep.
|
|
3
|
+
|
|
4
|
+
Demuestra cómo crear un formulario wizard con validación por paso
|
|
5
|
+
y validación global.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from codeforms import (
|
|
9
|
+
Form,
|
|
10
|
+
FormStep,
|
|
11
|
+
TextField,
|
|
12
|
+
EmailField,
|
|
13
|
+
NumberField,
|
|
14
|
+
SelectField,
|
|
15
|
+
SelectOption,
|
|
16
|
+
CheckboxField,
|
|
17
|
+
FieldGroup,
|
|
18
|
+
validate_form_data_dynamic,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_registration_wizard() -> Form:
|
|
23
|
+
"""Crea un formulario wizard de registro de usuario en 3 pasos."""
|
|
24
|
+
return Form(
|
|
25
|
+
name="registration_wizard",
|
|
26
|
+
content=[
|
|
27
|
+
# Paso 1: Información personal
|
|
28
|
+
FormStep(
|
|
29
|
+
title="Personal Information",
|
|
30
|
+
description="Tell us about yourself",
|
|
31
|
+
content=[
|
|
32
|
+
TextField(name="first_name", label="First Name", required=True),
|
|
33
|
+
TextField(name="last_name", label="Last Name", required=True),
|
|
34
|
+
EmailField(name="email", label="Email", required=True),
|
|
35
|
+
],
|
|
36
|
+
),
|
|
37
|
+
# Paso 2: Preferencias
|
|
38
|
+
FormStep(
|
|
39
|
+
title="Preferences",
|
|
40
|
+
description="Choose your plan and preferences",
|
|
41
|
+
content=[
|
|
42
|
+
SelectField(
|
|
43
|
+
name="plan",
|
|
44
|
+
label="Plan",
|
|
45
|
+
required=True,
|
|
46
|
+
options=[
|
|
47
|
+
SelectOption(value="free", label="Free"),
|
|
48
|
+
SelectOption(value="pro", label="Professional"),
|
|
49
|
+
SelectOption(value="enterprise", label="Enterprise"),
|
|
50
|
+
],
|
|
51
|
+
),
|
|
52
|
+
NumberField(
|
|
53
|
+
name="team_size",
|
|
54
|
+
label="Team Size",
|
|
55
|
+
min_value=1,
|
|
56
|
+
max_value=1000,
|
|
57
|
+
),
|
|
58
|
+
],
|
|
59
|
+
),
|
|
60
|
+
# Paso 3: Confirmación
|
|
61
|
+
FormStep(
|
|
62
|
+
title="Confirmation",
|
|
63
|
+
description="Review and accept the terms",
|
|
64
|
+
content=[
|
|
65
|
+
CheckboxField(
|
|
66
|
+
name="terms",
|
|
67
|
+
label="I accept the terms and conditions",
|
|
68
|
+
required=True,
|
|
69
|
+
),
|
|
70
|
+
],
|
|
71
|
+
validation_mode="on_submit",
|
|
72
|
+
),
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
form = create_registration_wizard()
|
|
79
|
+
|
|
80
|
+
# Verificar estructura
|
|
81
|
+
print(f"Form: {form.name}")
|
|
82
|
+
print(f"Steps: {len(form.get_steps())}")
|
|
83
|
+
print(f"Total fields: {len(form.fields)}")
|
|
84
|
+
|
|
85
|
+
for i, step in enumerate(form.get_steps()):
|
|
86
|
+
print(f"\n Step {i + 1}: {step.title}")
|
|
87
|
+
for field in step.fields:
|
|
88
|
+
print(f" - {field.name} ({'required' if field.required else 'optional'})")
|
|
89
|
+
|
|
90
|
+
# Validar paso 1
|
|
91
|
+
print("\n--- Validating Step 1 ---")
|
|
92
|
+
result = form.validate_step(0, {
|
|
93
|
+
"first_name": "John",
|
|
94
|
+
"last_name": "Doe",
|
|
95
|
+
"email": "john@example.com",
|
|
96
|
+
})
|
|
97
|
+
print(f"Step 1 valid: {result['success']}")
|
|
98
|
+
|
|
99
|
+
# Validar paso 2
|
|
100
|
+
print("\n--- Validating Step 2 ---")
|
|
101
|
+
result = form.validate_step(1, {
|
|
102
|
+
"plan": "pro",
|
|
103
|
+
"team_size": 5,
|
|
104
|
+
})
|
|
105
|
+
print(f"Step 2 valid: {result['success']}")
|
|
106
|
+
|
|
107
|
+
# Validar todos los pasos
|
|
108
|
+
print("\n--- Validating All Steps ---")
|
|
109
|
+
result = form.validate_all_steps({
|
|
110
|
+
"first_name": "John",
|
|
111
|
+
"last_name": "Doe",
|
|
112
|
+
"email": "john@example.com",
|
|
113
|
+
"plan": "pro",
|
|
114
|
+
"team_size": 5,
|
|
115
|
+
"terms": True,
|
|
116
|
+
})
|
|
117
|
+
print(f"All steps valid: {result['success']}")
|
|
118
|
+
|
|
119
|
+
# Exportar HTML
|
|
120
|
+
print("\n--- HTML Export ---")
|
|
121
|
+
export = form.export("html_bootstrap5")
|
|
122
|
+
print(export["output"][:300] + "...")
|
|
123
|
+
|
|
124
|
+
# JSON roundtrip
|
|
125
|
+
print("\n--- JSON Roundtrip ---")
|
|
126
|
+
json_str = form.model_dump_json()
|
|
127
|
+
restored = Form.model_validate_json(json_str)
|
|
128
|
+
print(f"Restored steps: {len(restored.get_steps())}")
|
|
129
|
+
print(f"Restored fields: {len(restored.fields)}")
|