codeforms 0.2.1__tar.gz → 0.2.2__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.2.1 → codeforms-0.2.2}/.gitignore +1 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/PKG-INFO +93 -1
- {codeforms-0.2.1 → codeforms-0.2.2}/README.md +92 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/pyproject.toml +1 -1
- {codeforms-0.2.1 → codeforms-0.2.2}/src/codeforms/__init__.py +2 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/src/codeforms/export.py +29 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/src/codeforms/fields.py +28 -1
- {codeforms-0.2.1 → codeforms-0.2.2}/src/codeforms/forms.py +171 -266
- {codeforms-0.2.1 → codeforms-0.2.2}/src/codeforms/registry.py +2 -0
- codeforms-0.2.2/tests/test_object_list_field.py +166 -0
- codeforms-0.2.1/uv.lock +0 -416
- {codeforms-0.2.1 → codeforms-0.2.2}/.github/workflows/python-publish.yml +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/.github/workflows/tests.yml +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/.python-version +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/LICENSE +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/examples/basic_usage.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/examples/conditional_visibility.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/examples/custom_fields.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/examples/dependent_options.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/examples/i18n_usage.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/examples/wizard_form.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/src/codeforms/i18n.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/tests/__init__.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/tests/conftest.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_basic_usage.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_dependent_options.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_dynamic_visibility.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_i18n.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_json_schema_export.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_phase2_compat.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_registry.py +0 -0
- {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_wizard_steps.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeforms
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Python library for creating, validating, and rendering web forms using Pydantic
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -100,6 +100,69 @@ All fields inherit from `FormFieldBase` and share these common attributes:
|
|
|
100
100
|
- `accept`: Accepted file types (e.g. `"image/*,.pdf"`).
|
|
101
101
|
- `multiple`: Allow multiple file uploads.
|
|
102
102
|
- **`HiddenField`** — Hidden field (`<input type="hidden">`).
|
|
103
|
+
- **`ListField`** — Array of primitive values.
|
|
104
|
+
- `item_type`: Primitive type for each item (`text`, `number`, `email`, `url`, `date`).
|
|
105
|
+
- `min_items`, `max_items`: List size limits.
|
|
106
|
+
- **`ObjectListField`** — Array of homogeneous objects validated against nested subfields.
|
|
107
|
+
- `fields`: List of subfields that define each object shape.
|
|
108
|
+
- `min_items`, `max_items`: List size limits.
|
|
109
|
+
|
|
110
|
+
### `ObjectListField`
|
|
111
|
+
|
|
112
|
+
Use `ObjectListField` when a form needs a repeatable list of structured rows, such as parallel approvers, attendees with roles, or line items.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from codeforms import Form, ObjectListField, TextField, CheckboxField
|
|
116
|
+
|
|
117
|
+
form = Form(
|
|
118
|
+
name="parallel_approvers",
|
|
119
|
+
fields=[
|
|
120
|
+
ObjectListField(
|
|
121
|
+
name="parallel_approvals",
|
|
122
|
+
label="Aprobadores",
|
|
123
|
+
required=True,
|
|
124
|
+
min_items=1,
|
|
125
|
+
max_items=5,
|
|
126
|
+
fields=[
|
|
127
|
+
TextField(name="approver_email", label="Email", required=True),
|
|
128
|
+
TextField(name="label", label="Etiqueta", required=True),
|
|
129
|
+
CheckboxField(name="required", label="Obligatorio"),
|
|
130
|
+
],
|
|
131
|
+
)
|
|
132
|
+
],
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Expected submitted value:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"parallel_approvals": [
|
|
141
|
+
{
|
|
142
|
+
"approver_email": "ana@empresa.com",
|
|
143
|
+
"label": "Compras",
|
|
144
|
+
"required": true
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"approver_email": "luis@empresa.com",
|
|
148
|
+
"label": "Finanzas"
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Validation behavior:
|
|
155
|
+
|
|
156
|
+
- The top-level field must be a JSON array.
|
|
157
|
+
- Each item must be a JSON object.
|
|
158
|
+
- Unknown keys inside items are rejected.
|
|
159
|
+
- Required nested subfields are enforced.
|
|
160
|
+
- Validation errors include nested paths like `parallel_approvals[0].label`.
|
|
161
|
+
|
|
162
|
+
Current limitation:
|
|
163
|
+
|
|
164
|
+
- `ObjectListField` is fully supported in backend validation and JSON Schema export.
|
|
165
|
+
- Rich repeatable HTML UI generation is not implemented yet. If you need an interactive editor, prefer consuming the exported `json_schema` from your frontend.
|
|
103
166
|
|
|
104
167
|
## Data Validation
|
|
105
168
|
|
|
@@ -284,9 +347,38 @@ Output:
|
|
|
284
347
|
| `UrlField` | `string` (`format: "uri"`) | `minLength`, `maxLength` |
|
|
285
348
|
| `TextareaField` | `string` | `minLength`, `maxLength` |
|
|
286
349
|
| `ListField` | `array` | `minItems`, `maxItems` |
|
|
350
|
+
| `ObjectListField` | `array` of `object` | nested `properties`, nested `required`, `minItems`, `maxItems` |
|
|
287
351
|
|
|
288
352
|
Field annotations like `label`, `help_text`, `default_value`, and `readonly` map to the JSON Schema keywords `title`, `description`, `default`, and `readOnly` respectively.
|
|
289
353
|
|
|
354
|
+
Example `ObjectListField` schema:
|
|
355
|
+
|
|
356
|
+
```json
|
|
357
|
+
{
|
|
358
|
+
"type": "array",
|
|
359
|
+
"minItems": 1,
|
|
360
|
+
"items": {
|
|
361
|
+
"type": "object",
|
|
362
|
+
"properties": {
|
|
363
|
+
"approver_email": {
|
|
364
|
+
"type": "string",
|
|
365
|
+
"title": "Email"
|
|
366
|
+
},
|
|
367
|
+
"label": {
|
|
368
|
+
"type": "string",
|
|
369
|
+
"title": "Etiqueta"
|
|
370
|
+
},
|
|
371
|
+
"required": {
|
|
372
|
+
"type": "boolean",
|
|
373
|
+
"title": "Obligatorio"
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
"required": ["approver_email", "label"],
|
|
377
|
+
"additionalProperties": false
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
290
382
|
Fields inside `FieldGroup` and `FormStep` containers are flattened into the top-level `properties` automatically.
|
|
291
383
|
|
|
292
384
|
## Internationalization (i18n)
|
|
@@ -87,6 +87,69 @@ All fields inherit from `FormFieldBase` and share these common attributes:
|
|
|
87
87
|
- `accept`: Accepted file types (e.g. `"image/*,.pdf"`).
|
|
88
88
|
- `multiple`: Allow multiple file uploads.
|
|
89
89
|
- **`HiddenField`** — Hidden field (`<input type="hidden">`).
|
|
90
|
+
- **`ListField`** — Array of primitive values.
|
|
91
|
+
- `item_type`: Primitive type for each item (`text`, `number`, `email`, `url`, `date`).
|
|
92
|
+
- `min_items`, `max_items`: List size limits.
|
|
93
|
+
- **`ObjectListField`** — Array of homogeneous objects validated against nested subfields.
|
|
94
|
+
- `fields`: List of subfields that define each object shape.
|
|
95
|
+
- `min_items`, `max_items`: List size limits.
|
|
96
|
+
|
|
97
|
+
### `ObjectListField`
|
|
98
|
+
|
|
99
|
+
Use `ObjectListField` when a form needs a repeatable list of structured rows, such as parallel approvers, attendees with roles, or line items.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from codeforms import Form, ObjectListField, TextField, CheckboxField
|
|
103
|
+
|
|
104
|
+
form = Form(
|
|
105
|
+
name="parallel_approvers",
|
|
106
|
+
fields=[
|
|
107
|
+
ObjectListField(
|
|
108
|
+
name="parallel_approvals",
|
|
109
|
+
label="Aprobadores",
|
|
110
|
+
required=True,
|
|
111
|
+
min_items=1,
|
|
112
|
+
max_items=5,
|
|
113
|
+
fields=[
|
|
114
|
+
TextField(name="approver_email", label="Email", required=True),
|
|
115
|
+
TextField(name="label", label="Etiqueta", required=True),
|
|
116
|
+
CheckboxField(name="required", label="Obligatorio"),
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
],
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Expected submitted value:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"parallel_approvals": [
|
|
128
|
+
{
|
|
129
|
+
"approver_email": "ana@empresa.com",
|
|
130
|
+
"label": "Compras",
|
|
131
|
+
"required": true
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"approver_email": "luis@empresa.com",
|
|
135
|
+
"label": "Finanzas"
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Validation behavior:
|
|
142
|
+
|
|
143
|
+
- The top-level field must be a JSON array.
|
|
144
|
+
- Each item must be a JSON object.
|
|
145
|
+
- Unknown keys inside items are rejected.
|
|
146
|
+
- Required nested subfields are enforced.
|
|
147
|
+
- Validation errors include nested paths like `parallel_approvals[0].label`.
|
|
148
|
+
|
|
149
|
+
Current limitation:
|
|
150
|
+
|
|
151
|
+
- `ObjectListField` is fully supported in backend validation and JSON Schema export.
|
|
152
|
+
- Rich repeatable HTML UI generation is not implemented yet. If you need an interactive editor, prefer consuming the exported `json_schema` from your frontend.
|
|
90
153
|
|
|
91
154
|
## Data Validation
|
|
92
155
|
|
|
@@ -271,9 +334,38 @@ Output:
|
|
|
271
334
|
| `UrlField` | `string` (`format: "uri"`) | `minLength`, `maxLength` |
|
|
272
335
|
| `TextareaField` | `string` | `minLength`, `maxLength` |
|
|
273
336
|
| `ListField` | `array` | `minItems`, `maxItems` |
|
|
337
|
+
| `ObjectListField` | `array` of `object` | nested `properties`, nested `required`, `minItems`, `maxItems` |
|
|
274
338
|
|
|
275
339
|
Field annotations like `label`, `help_text`, `default_value`, and `readonly` map to the JSON Schema keywords `title`, `description`, `default`, and `readOnly` respectively.
|
|
276
340
|
|
|
341
|
+
Example `ObjectListField` schema:
|
|
342
|
+
|
|
343
|
+
```json
|
|
344
|
+
{
|
|
345
|
+
"type": "array",
|
|
346
|
+
"minItems": 1,
|
|
347
|
+
"items": {
|
|
348
|
+
"type": "object",
|
|
349
|
+
"properties": {
|
|
350
|
+
"approver_email": {
|
|
351
|
+
"type": "string",
|
|
352
|
+
"title": "Email"
|
|
353
|
+
},
|
|
354
|
+
"label": {
|
|
355
|
+
"type": "string",
|
|
356
|
+
"title": "Etiqueta"
|
|
357
|
+
},
|
|
358
|
+
"required": {
|
|
359
|
+
"type": "boolean",
|
|
360
|
+
"title": "Obligatorio"
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
"required": ["approver_email", "label"],
|
|
364
|
+
"additionalProperties": false
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
277
369
|
Fields inside `FieldGroup` and `FormStep` containers are flattened into the top-level `properties` automatically.
|
|
278
370
|
|
|
279
371
|
## Internationalization (i18n)
|
|
@@ -12,6 +12,7 @@ from codeforms.fields import (
|
|
|
12
12
|
FormStep,
|
|
13
13
|
HiddenField,
|
|
14
14
|
ListField,
|
|
15
|
+
ObjectListField,
|
|
15
16
|
NumberField,
|
|
16
17
|
RadioField,
|
|
17
18
|
SelectField,
|
|
@@ -63,6 +64,7 @@ __all__ = [
|
|
|
63
64
|
"UrlField",
|
|
64
65
|
"TextareaField",
|
|
65
66
|
"ListField",
|
|
67
|
+
"ObjectListField",
|
|
66
68
|
"FieldGroup",
|
|
67
69
|
"FormStep",
|
|
68
70
|
# Form
|
|
@@ -12,6 +12,7 @@ from codeforms.fields import (
|
|
|
12
12
|
FormStep,
|
|
13
13
|
HiddenField,
|
|
14
14
|
ListField,
|
|
15
|
+
ObjectListField,
|
|
15
16
|
NumberField,
|
|
16
17
|
RadioField,
|
|
17
18
|
SelectField,
|
|
@@ -485,6 +486,26 @@ _LIST_ITEM_TYPE_MAP: Dict[str, str] = {
|
|
|
485
486
|
}
|
|
486
487
|
|
|
487
488
|
|
|
489
|
+
def _object_list_item_schema(field: ObjectListField) -> Dict[str, Any]:
|
|
490
|
+
properties: Dict[str, Any] = {}
|
|
491
|
+
required = []
|
|
492
|
+
|
|
493
|
+
for subfield in field.fields:
|
|
494
|
+
properties[subfield.name] = _field_to_json_schema_property(subfield)
|
|
495
|
+
if subfield.required:
|
|
496
|
+
required.append(subfield.name)
|
|
497
|
+
|
|
498
|
+
item_schema: Dict[str, Any] = {
|
|
499
|
+
"type": "object",
|
|
500
|
+
"properties": properties,
|
|
501
|
+
"additionalProperties": False,
|
|
502
|
+
}
|
|
503
|
+
if required:
|
|
504
|
+
item_schema["required"] = required
|
|
505
|
+
|
|
506
|
+
return item_schema
|
|
507
|
+
|
|
508
|
+
|
|
488
509
|
def _field_to_json_schema_property(field: FormFieldBase) -> Dict[str, Any]:
|
|
489
510
|
"""Convert a single form field to a JSON Schema property definition."""
|
|
490
511
|
prop: Dict[str, Any] = {}
|
|
@@ -570,6 +591,14 @@ def _field_to_json_schema_property(field: FormFieldBase) -> Dict[str, Any]:
|
|
|
570
591
|
if field.max_items is not None:
|
|
571
592
|
prop["maxItems"] = field.max_items
|
|
572
593
|
|
|
594
|
+
elif isinstance(field, ObjectListField):
|
|
595
|
+
prop["type"] = "array"
|
|
596
|
+
prop["items"] = _object_list_item_schema(field)
|
|
597
|
+
if field.min_items is not None:
|
|
598
|
+
prop["minItems"] = field.min_items
|
|
599
|
+
if field.max_items is not None:
|
|
600
|
+
prop["maxItems"] = field.max_items
|
|
601
|
+
|
|
573
602
|
elif isinstance(field, TextField):
|
|
574
603
|
prop["type"] = "string"
|
|
575
604
|
if field.minlength is not None:
|
|
@@ -30,6 +30,7 @@ class FieldType(str, Enum):
|
|
|
30
30
|
HIDDEN = "hidden"
|
|
31
31
|
URL = "url"
|
|
32
32
|
LIST = "list"
|
|
33
|
+
OBJECT_LIST = "object-list"
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class ValidationRule(BaseModel):
|
|
@@ -279,7 +280,7 @@ class TextareaField(FormFieldBase):
|
|
|
279
280
|
|
|
280
281
|
|
|
281
282
|
class ListField(FormFieldBase):
|
|
282
|
-
"""Campo para listas de valores (ej: lista de participantes)"""
|
|
283
|
+
"""Campo para listas de valores primitivos (ej: lista de participantes)."""
|
|
283
284
|
|
|
284
285
|
field_type: FieldType = FieldType.LIST
|
|
285
286
|
min_items: Optional[int] = None
|
|
@@ -287,6 +288,32 @@ class ListField(FormFieldBase):
|
|
|
287
288
|
item_type: str = "text" # Tipo de cada item en la lista
|
|
288
289
|
|
|
289
290
|
|
|
291
|
+
class ObjectListField(FormFieldBase):
|
|
292
|
+
"""Campo para listas de objetos homogéneos validados por subcampos."""
|
|
293
|
+
|
|
294
|
+
field_type: FieldType = FieldType.OBJECT_LIST
|
|
295
|
+
min_items: Optional[int] = None
|
|
296
|
+
max_items: Optional[int] = None
|
|
297
|
+
fields: List[Any] = Field(default_factory=list)
|
|
298
|
+
|
|
299
|
+
@model_validator(mode="before")
|
|
300
|
+
@classmethod
|
|
301
|
+
def resolve_object_fields(cls, data: Any) -> Any:
|
|
302
|
+
if isinstance(data, dict) and "fields" in data:
|
|
303
|
+
from codeforms.registry import resolve_content_item
|
|
304
|
+
|
|
305
|
+
data = data.copy()
|
|
306
|
+
data["fields"] = [resolve_content_item(item) for item in data["fields"]]
|
|
307
|
+
return data
|
|
308
|
+
|
|
309
|
+
@model_validator(mode="after")
|
|
310
|
+
def validate_object_fields(self) -> "ObjectListField":
|
|
311
|
+
names = [field.name for field in self.fields]
|
|
312
|
+
if len(names) != len(set(names)):
|
|
313
|
+
raise ValueError(t("form.unique_field_names_in_group", title=self.label or self.name))
|
|
314
|
+
return self
|
|
315
|
+
|
|
316
|
+
|
|
290
317
|
class FieldGroup(BaseModel):
|
|
291
318
|
"""Representa un grupo de campos en un formulario para organización en secciones"""
|
|
292
319
|
|