codeforms 0.2.0__py3-none-any.whl → 0.2.2__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.
- codeforms/__init__.py +25 -22
- codeforms/export.py +354 -101
- codeforms/fields.py +86 -38
- codeforms/forms.py +343 -318
- codeforms/i18n.py +1 -30
- codeforms/registry.py +43 -18
- codeforms-0.2.2.dist-info/METADATA +675 -0
- codeforms-0.2.2.dist-info/RECORD +10 -0
- {codeforms-0.2.0.dist-info → codeforms-0.2.2.dist-info}/WHEEL +1 -1
- codeforms-0.2.0.dist-info/METADATA +0 -325
- codeforms-0.2.0.dist-info/RECORD +0 -10
- {codeforms-0.2.0.dist-info → codeforms-0.2.2.dist-info}/licenses/LICENSE +0 -0
codeforms/export.py
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from codeforms.fields import (
|
|
5
|
+
CheckboxField,
|
|
6
|
+
CheckboxGroupField,
|
|
7
|
+
DateField,
|
|
8
|
+
EmailField,
|
|
9
|
+
FieldGroup,
|
|
10
|
+
FileField,
|
|
11
|
+
FormFieldBase,
|
|
12
|
+
FormStep,
|
|
13
|
+
HiddenField,
|
|
14
|
+
ListField,
|
|
15
|
+
ObjectListField,
|
|
16
|
+
NumberField,
|
|
17
|
+
RadioField,
|
|
18
|
+
SelectField,
|
|
19
|
+
TextareaField,
|
|
20
|
+
TextField,
|
|
21
|
+
UrlField,
|
|
22
|
+
)
|
|
2
23
|
from codeforms.forms import Form
|
|
3
|
-
from codeforms.fields import FormFieldBase, FieldGroup, FormStep
|
|
4
24
|
from codeforms.i18n import t
|
|
5
25
|
|
|
6
26
|
|
|
7
27
|
class ExportFormat(Enum):
|
|
8
|
-
HTML =
|
|
9
|
-
BOOTSTRAP4 =
|
|
10
|
-
BOOTSTRAP5 =
|
|
28
|
+
HTML = "html"
|
|
29
|
+
BOOTSTRAP4 = "html_bootstrap4"
|
|
30
|
+
BOOTSTRAP5 = "html_bootstrap5"
|
|
31
|
+
JSON_SCHEMA = "json_schema"
|
|
11
32
|
|
|
12
33
|
|
|
13
34
|
def generate_validation_code(form, output_format: str) -> str:
|
|
14
35
|
"""Genera el código de validación en Javascript"""
|
|
15
|
-
if output_format ==
|
|
36
|
+
if output_format == "html":
|
|
16
37
|
validation_code = f"""
|
|
17
38
|
<script>
|
|
18
39
|
function validate_{form.name}(form) {{
|
|
@@ -97,67 +118,75 @@ def js_generate_field_validations(form) -> str:
|
|
|
97
118
|
|
|
98
119
|
return "\n".join(validations)
|
|
99
120
|
|
|
121
|
+
|
|
100
122
|
def field_exporter(field: FormFieldBase, output_format: str, **kwargs) -> str:
|
|
101
123
|
"""Exporta un campo individual al formato especificado"""
|
|
102
|
-
if output_format ==
|
|
124
|
+
if output_format == "html":
|
|
103
125
|
return field_to_html(field, kwargs=kwargs)
|
|
104
126
|
return ""
|
|
105
127
|
|
|
106
128
|
|
|
107
129
|
def group_exporter(group, output_format: str, **kwargs) -> str:
|
|
108
130
|
"""Exporta un grupo de campos al formato especificado"""
|
|
109
|
-
if output_format ==
|
|
131
|
+
if output_format == "html":
|
|
110
132
|
return group_to_html(group, kwargs=kwargs)
|
|
111
133
|
return ""
|
|
112
134
|
|
|
113
135
|
|
|
114
136
|
def step_exporter(step, output_format: str, **kwargs) -> str:
|
|
115
137
|
"""Exporta un paso de formulario (wizard) al formato especificado"""
|
|
116
|
-
if output_format in (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
if output_format in (
|
|
139
|
+
"html",
|
|
140
|
+
ExportFormat.HTML.value,
|
|
141
|
+
ExportFormat.BOOTSTRAP4.value,
|
|
142
|
+
ExportFormat.BOOTSTRAP5.value,
|
|
143
|
+
):
|
|
144
|
+
actual_kwargs = kwargs.get("kwargs", kwargs)
|
|
145
|
+
actual_kwargs["output_format"] = output_format
|
|
121
146
|
return step_to_html(step, **actual_kwargs)
|
|
122
147
|
return ""
|
|
123
148
|
|
|
124
149
|
|
|
125
150
|
def group_to_html(group, **kwargs) -> str:
|
|
126
151
|
"""Genera la representación HTML del grupo de campos usando fieldset y legend"""
|
|
127
|
-
output_format = kwargs.get(
|
|
128
|
-
is_bootstrap = output_format in [
|
|
129
|
-
|
|
152
|
+
output_format = kwargs.get("output_format", ExportFormat.HTML.value)
|
|
153
|
+
is_bootstrap = output_format in [
|
|
154
|
+
ExportFormat.BOOTSTRAP4.value,
|
|
155
|
+
ExportFormat.BOOTSTRAP5.value,
|
|
156
|
+
]
|
|
157
|
+
|
|
130
158
|
# Clases CSS para el fieldset
|
|
131
|
-
fieldset_class =
|
|
159
|
+
fieldset_class = (
|
|
160
|
+
f"mb-4 {group.css_classes or ''}".strip()
|
|
161
|
+
if is_bootstrap
|
|
162
|
+
else group.css_classes or ""
|
|
163
|
+
)
|
|
132
164
|
legend_class = "h5 mb-3" if is_bootstrap else ""
|
|
133
|
-
|
|
165
|
+
|
|
134
166
|
# Atributos del fieldset
|
|
135
|
-
fieldset_attrs = {
|
|
136
|
-
|
|
137
|
-
"class": fieldset_class
|
|
138
|
-
}
|
|
139
|
-
|
|
167
|
+
fieldset_attrs = {"id": f"group_{group.id}", "class": fieldset_class}
|
|
168
|
+
|
|
140
169
|
# Agregar atributos personalizados del grupo
|
|
141
170
|
fieldset_attrs.update(group.attributes)
|
|
142
|
-
|
|
171
|
+
|
|
143
172
|
# Generar string de atributos
|
|
144
173
|
attrs_str = " ".join(f'{k}="{v}"' for k, v in fieldset_attrs.items() if v)
|
|
145
|
-
|
|
174
|
+
|
|
146
175
|
# Generar HTML de los campos dentro del grupo
|
|
147
176
|
fields_html = "\n".join(field_to_html(field, **kwargs) for field in group.fields)
|
|
148
|
-
|
|
177
|
+
|
|
149
178
|
# Generar descripción si existe
|
|
150
179
|
description_html = ""
|
|
151
180
|
if group.description:
|
|
152
181
|
desc_class = "text-muted small mb-3" if is_bootstrap else "group-description"
|
|
153
182
|
description_html = f'<p class="{desc_class}">{group.description}</p>'
|
|
154
|
-
|
|
183
|
+
|
|
155
184
|
# Construir el HTML del fieldset
|
|
156
|
-
html = f
|
|
185
|
+
html = f"<fieldset {attrs_str}>"
|
|
157
186
|
html += f'<legend class="{legend_class}">{group.title}</legend>'
|
|
158
187
|
html += description_html
|
|
159
188
|
html += fields_html
|
|
160
|
-
html +=
|
|
189
|
+
html += "</fieldset>"
|
|
161
190
|
|
|
162
191
|
return html
|
|
163
192
|
|
|
@@ -167,11 +196,18 @@ def step_to_html(step, **kwargs) -> str:
|
|
|
167
196
|
|
|
168
197
|
Diferente de FieldGroup (que usa <fieldset>) para distinguir semánticamente.
|
|
169
198
|
"""
|
|
170
|
-
output_format = kwargs.get(
|
|
171
|
-
is_bootstrap = output_format in [
|
|
199
|
+
output_format = kwargs.get("output_format", ExportFormat.HTML.value)
|
|
200
|
+
is_bootstrap = output_format in [
|
|
201
|
+
ExportFormat.BOOTSTRAP4.value,
|
|
202
|
+
ExportFormat.BOOTSTRAP5.value,
|
|
203
|
+
]
|
|
172
204
|
|
|
173
205
|
# Clases CSS para el section
|
|
174
|
-
step_class =
|
|
206
|
+
step_class = (
|
|
207
|
+
f"form-step mb-4 {step.css_classes or ''}".strip()
|
|
208
|
+
if is_bootstrap
|
|
209
|
+
else f"form-step {step.css_classes or ''}".strip()
|
|
210
|
+
)
|
|
175
211
|
title_class = "h4 mb-3" if is_bootstrap else "step-title"
|
|
176
212
|
|
|
177
213
|
# Atributos del section
|
|
@@ -180,7 +216,7 @@ def step_to_html(step, **kwargs) -> str:
|
|
|
180
216
|
"class": step_class,
|
|
181
217
|
"data-step": "true",
|
|
182
218
|
"data-validation-mode": step.validation_mode,
|
|
183
|
-
"data-skippable": str(step.skippable).lower()
|
|
219
|
+
"data-skippable": str(step.skippable).lower(),
|
|
184
220
|
}
|
|
185
221
|
step_attrs.update(step.attributes)
|
|
186
222
|
|
|
@@ -201,19 +237,24 @@ def step_to_html(step, **kwargs) -> str:
|
|
|
201
237
|
desc_class = "text-muted mb-3" if is_bootstrap else "step-description"
|
|
202
238
|
description_html = f'<p class="{desc_class}">{step.description}</p>'
|
|
203
239
|
|
|
204
|
-
html = f
|
|
240
|
+
html = f"<section {attrs_str}>"
|
|
205
241
|
html += f'<h2 class="{title_class}">{step.title}</h2>'
|
|
206
242
|
html += description_html
|
|
207
243
|
html += content_html
|
|
208
|
-
html +=
|
|
244
|
+
html += "</section>"
|
|
209
245
|
|
|
210
246
|
return html
|
|
211
247
|
|
|
212
248
|
|
|
213
249
|
def form_to_html(form: Form, **kwargs) -> str:
|
|
214
250
|
"""Genera el HTML completo del formulario"""
|
|
215
|
-
output_format = kwargs.get(
|
|
216
|
-
form_class =
|
|
251
|
+
output_format = kwargs.get("output_format", ExportFormat.HTML.value)
|
|
252
|
+
form_class = (
|
|
253
|
+
"needs-validation"
|
|
254
|
+
if output_format
|
|
255
|
+
in [ExportFormat.BOOTSTRAP4.value, ExportFormat.BOOTSTRAP5.value]
|
|
256
|
+
else ""
|
|
257
|
+
)
|
|
217
258
|
|
|
218
259
|
# Detectar si es wizard
|
|
219
260
|
is_wizard = any(isinstance(item, FormStep) for item in form.content)
|
|
@@ -221,10 +262,10 @@ def form_to_html(form: Form, **kwargs) -> str:
|
|
|
221
262
|
form_class = f"{form_class} form-wizard".strip()
|
|
222
263
|
|
|
223
264
|
attributes = {
|
|
224
|
-
"id": kwargs.get(
|
|
265
|
+
"id": kwargs.get("id") or str(form.id),
|
|
225
266
|
"name": form.name,
|
|
226
267
|
"class": f"{form_class} {form.css_classes or ''}".strip(),
|
|
227
|
-
"enctype": kwargs.get(
|
|
268
|
+
"enctype": kwargs.get("enctype") or "application/x-www-form-urlencoded",
|
|
228
269
|
}
|
|
229
270
|
if is_wizard:
|
|
230
271
|
attributes["data-wizard"] = "true"
|
|
@@ -240,11 +281,16 @@ def form_to_html(form: Form, **kwargs) -> str:
|
|
|
240
281
|
content_html_parts.append(group_to_html(item, **kwargs))
|
|
241
282
|
else: # Es un campo individual
|
|
242
283
|
content_html_parts.append(field_to_html(item, **kwargs))
|
|
243
|
-
|
|
284
|
+
|
|
244
285
|
content_html = "\n".join(content_html_parts)
|
|
245
|
-
|
|
246
|
-
if kwargs.get(
|
|
247
|
-
submit_class =
|
|
286
|
+
|
|
287
|
+
if kwargs.get("submit"):
|
|
288
|
+
submit_class = (
|
|
289
|
+
"btn btn-primary"
|
|
290
|
+
if output_format
|
|
291
|
+
in [ExportFormat.BOOTSTRAP4.value, ExportFormat.BOOTSTRAP5.value]
|
|
292
|
+
else ""
|
|
293
|
+
)
|
|
248
294
|
submit_html = f'<button type="submit" class="{submit_class}">{t("export.submit")}</button>'
|
|
249
295
|
else:
|
|
250
296
|
submit_html = ""
|
|
@@ -252,89 +298,104 @@ def form_to_html(form: Form, **kwargs) -> str:
|
|
|
252
298
|
html = f"<form {attrs_str}>"
|
|
253
299
|
html += f"\t{content_html}"
|
|
254
300
|
html += f"\t{submit_html}"
|
|
255
|
-
html +=
|
|
301
|
+
html += "</form>"
|
|
256
302
|
return html
|
|
257
303
|
|
|
258
304
|
|
|
259
305
|
def field_to_html(field: FormFieldBase, **kwargs) -> str:
|
|
260
306
|
"""Genera la representación HTML del campo"""
|
|
261
|
-
output_format = kwargs.get(
|
|
262
|
-
is_bootstrap = output_format in [
|
|
263
|
-
|
|
307
|
+
output_format = kwargs.get("output_format", ExportFormat.HTML.value)
|
|
308
|
+
is_bootstrap = output_format in [
|
|
309
|
+
ExportFormat.BOOTSTRAP4.value,
|
|
310
|
+
ExportFormat.BOOTSTRAP5.value,
|
|
311
|
+
]
|
|
312
|
+
|
|
264
313
|
# Clases base para Bootstrap 4/5
|
|
265
314
|
if is_bootstrap:
|
|
266
315
|
base_input_class = "form-control"
|
|
267
|
-
form_group_class =
|
|
316
|
+
form_group_class = (
|
|
317
|
+
"mb-3" if output_format == ExportFormat.BOOTSTRAP5.value else "form-group"
|
|
318
|
+
)
|
|
268
319
|
help_text_class = "form-text" # Bootstrap 5 removed text-muted
|
|
269
320
|
else:
|
|
270
321
|
base_input_class = ""
|
|
271
322
|
form_group_class = "form-field"
|
|
272
323
|
help_text_class = "help-text"
|
|
273
324
|
|
|
274
|
-
skip_label = [
|
|
275
|
-
|
|
276
|
-
label_html =
|
|
325
|
+
skip_label = ["hidden"]
|
|
326
|
+
|
|
327
|
+
label_html = ""
|
|
277
328
|
if field.field_type_value not in skip_label:
|
|
278
329
|
label_class = "form-label" if is_bootstrap else ""
|
|
279
|
-
label_html =
|
|
280
|
-
|
|
281
|
-
|
|
330
|
+
label_html = (
|
|
331
|
+
f'<label class="{label_class}" for="{field.id}">{field.label}</label>'
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
help_html = (
|
|
335
|
+
f'<small class="{help_text_class}">{field.help_text}</small>'
|
|
336
|
+
if field.help_text
|
|
337
|
+
else ""
|
|
338
|
+
)
|
|
282
339
|
|
|
283
340
|
# Manejar campos SELECT de manera especial
|
|
284
|
-
if field.field_type_value ==
|
|
341
|
+
if field.field_type_value == "select":
|
|
285
342
|
select_attrs = {
|
|
286
343
|
"id": str(field.id),
|
|
287
344
|
"name": field.name,
|
|
288
345
|
"class": f"{base_input_class} {field.css_classes or ''}".strip(),
|
|
289
346
|
}
|
|
290
|
-
|
|
347
|
+
|
|
291
348
|
if field.required:
|
|
292
349
|
select_attrs["required"] = "required"
|
|
293
|
-
|
|
294
|
-
if hasattr(field,
|
|
350
|
+
|
|
351
|
+
if hasattr(field, "multiple") and field.multiple:
|
|
295
352
|
select_attrs["multiple"] = "multiple"
|
|
296
|
-
|
|
353
|
+
|
|
297
354
|
# Agregar atributos personalizados
|
|
298
355
|
select_attrs.update(field.attributes)
|
|
299
|
-
|
|
356
|
+
|
|
300
357
|
attrs_str = " ".join(f'{k}="{v}"' for k, v in select_attrs.items() if v)
|
|
301
|
-
|
|
358
|
+
|
|
302
359
|
# Generar opciones
|
|
303
360
|
options_html = ""
|
|
304
|
-
if hasattr(field,
|
|
361
|
+
if hasattr(field, "options"):
|
|
305
362
|
for option in field.options:
|
|
306
363
|
selected = 'selected="selected"' if option.selected else ""
|
|
307
|
-
options_html +=
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
364
|
+
options_html += (
|
|
365
|
+
f'<option value="{option.value}" {selected}>{option.label}</option>'
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
input_html = f"<select {attrs_str}>{options_html}</select>"
|
|
369
|
+
|
|
311
370
|
# Manejar campos RADIO de manera especial
|
|
312
|
-
elif field.field_type_value ==
|
|
371
|
+
elif field.field_type_value == "radio":
|
|
313
372
|
radio_html_parts = []
|
|
314
|
-
if hasattr(field,
|
|
373
|
+
if hasattr(field, "options"):
|
|
315
374
|
for option in field.options:
|
|
316
375
|
radio_attrs = {
|
|
317
376
|
"id": f"{field.id}_{option.value}",
|
|
318
377
|
"name": field.name,
|
|
319
378
|
"type": "radio",
|
|
320
379
|
"value": option.value,
|
|
321
|
-
"class": field.css_classes or ""
|
|
380
|
+
"class": field.css_classes or "",
|
|
322
381
|
}
|
|
323
|
-
|
|
382
|
+
|
|
324
383
|
if option.selected:
|
|
325
384
|
radio_attrs["checked"] = "checked"
|
|
326
|
-
|
|
385
|
+
|
|
327
386
|
if field.required:
|
|
328
387
|
radio_attrs["required"] = "required"
|
|
329
|
-
|
|
388
|
+
|
|
330
389
|
attrs_str = " ".join(f'{k}="{v}"' for k, v in radio_attrs.items() if v)
|
|
331
|
-
radio_label =
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
390
|
+
radio_label = (
|
|
391
|
+
f'<label for="{field.id}_{option.value}">{option.label}</label>'
|
|
392
|
+
)
|
|
393
|
+
radio_html_parts.append(f"<input {attrs_str}>{radio_label}")
|
|
394
|
+
|
|
395
|
+
input_html = '<div class="radio-group">' + "".join(radio_html_parts) + "</div>"
|
|
396
|
+
|
|
336
397
|
# Manejar campos CHECKBOX con opciones múltiples
|
|
337
|
-
elif field.field_type_value ==
|
|
398
|
+
elif field.field_type_value == "checkbox" and hasattr(field, "options"):
|
|
338
399
|
checkbox_html_parts = []
|
|
339
400
|
for option in field.options:
|
|
340
401
|
checkbox_attrs = {
|
|
@@ -342,21 +403,25 @@ def field_to_html(field: FormFieldBase, **kwargs) -> str:
|
|
|
342
403
|
"name": field.name,
|
|
343
404
|
"type": "checkbox",
|
|
344
405
|
"value": option.value,
|
|
345
|
-
"class": field.css_classes or ""
|
|
406
|
+
"class": field.css_classes or "",
|
|
346
407
|
}
|
|
347
|
-
|
|
408
|
+
|
|
348
409
|
if option.selected:
|
|
349
410
|
checkbox_attrs["checked"] = "checked"
|
|
350
|
-
|
|
411
|
+
|
|
351
412
|
if field.required:
|
|
352
413
|
checkbox_attrs["required"] = "required"
|
|
353
|
-
|
|
414
|
+
|
|
354
415
|
attrs_str = " ".join(f'{k}="{v}"' for k, v in checkbox_attrs.items() if v)
|
|
355
|
-
checkbox_label =
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
416
|
+
checkbox_label = (
|
|
417
|
+
f'<label for="{field.id}_{option.value}">{option.label}</label>'
|
|
418
|
+
)
|
|
419
|
+
checkbox_html_parts.append(f"<input {attrs_str}>{checkbox_label}")
|
|
420
|
+
|
|
421
|
+
input_html = (
|
|
422
|
+
'<div class="checkbox-group">' + "".join(checkbox_html_parts) + "</div>"
|
|
423
|
+
)
|
|
424
|
+
|
|
360
425
|
# Manejar campos normales (input)
|
|
361
426
|
else:
|
|
362
427
|
attributes = {
|
|
@@ -366,18 +431,22 @@ def field_to_html(field: FormFieldBase, **kwargs) -> str:
|
|
|
366
431
|
"class": f"{base_input_class} {field.css_classes or ''}".strip(),
|
|
367
432
|
"placeholder": field.placeholder or "",
|
|
368
433
|
}
|
|
369
|
-
|
|
370
|
-
if hasattr(field,
|
|
371
|
-
attributes["value"] =
|
|
434
|
+
|
|
435
|
+
if hasattr(field, "value"):
|
|
436
|
+
attributes["value"] = field.value
|
|
372
437
|
|
|
373
438
|
if field.required:
|
|
374
439
|
attributes["required"] = "required"
|
|
375
440
|
|
|
376
441
|
if field.default_value is not None:
|
|
377
442
|
attributes["value"] = str(field.default_value)
|
|
378
|
-
|
|
443
|
+
|
|
379
444
|
# Para checkbox simple, manejar el atributo checked
|
|
380
|
-
if
|
|
445
|
+
if (
|
|
446
|
+
field.field_type_value == "checkbox"
|
|
447
|
+
and hasattr(field, "checked")
|
|
448
|
+
and field.checked
|
|
449
|
+
):
|
|
381
450
|
attributes["checked"] = "checked"
|
|
382
451
|
|
|
383
452
|
# Agregar atributos personalizados
|
|
@@ -385,16 +454,200 @@ def field_to_html(field: FormFieldBase, **kwargs) -> str:
|
|
|
385
454
|
|
|
386
455
|
# Convertir atributos a string
|
|
387
456
|
attrs_str = " ".join(f'{k}="{v}"' for k, v in attributes.items() if v)
|
|
388
|
-
input_html = f
|
|
457
|
+
input_html = f"<input {attrs_str}>"
|
|
458
|
+
|
|
459
|
+
return (
|
|
460
|
+
f"""<div class="{form_group_class}">{label_html}{input_html}{help_html}</div>"""
|
|
461
|
+
)
|
|
389
462
|
|
|
390
|
-
return f"""<div class="{form_group_class}">{label_html}{input_html}{help_html}</div>"""
|
|
391
463
|
|
|
392
464
|
def exporter(form: Form, output_format: str, **kwargs) -> dict:
|
|
393
|
-
export_result = {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
465
|
+
export_result = {"format": output_format}
|
|
466
|
+
|
|
467
|
+
if output_format == ExportFormat.JSON_SCHEMA.value:
|
|
468
|
+
export_result["output"] = form_to_json_schema(form)
|
|
469
|
+
elif output_format in [fmt.value for fmt in ExportFormat]:
|
|
470
|
+
actual_kwargs = kwargs.get("kwargs", kwargs)
|
|
471
|
+
actual_kwargs["output_format"] = output_format
|
|
472
|
+
export_result["output"] = form_to_html(form, **actual_kwargs)
|
|
473
|
+
export_result["javascript_validation_code"] = generate_validation_code(
|
|
474
|
+
form, output_format
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
return export_result
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
_LIST_ITEM_TYPE_MAP: Dict[str, str] = {
|
|
481
|
+
"text": "string",
|
|
482
|
+
"number": "number",
|
|
483
|
+
"email": "string",
|
|
484
|
+
"url": "string",
|
|
485
|
+
"date": "string",
|
|
486
|
+
}
|
|
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
|
+
|
|
509
|
+
def _field_to_json_schema_property(field: FormFieldBase) -> Dict[str, Any]:
|
|
510
|
+
"""Convert a single form field to a JSON Schema property definition."""
|
|
511
|
+
prop: Dict[str, Any] = {}
|
|
512
|
+
|
|
513
|
+
if isinstance(field, SelectField):
|
|
514
|
+
enum_values = [opt.value for opt in field.options]
|
|
515
|
+
if field.multiple:
|
|
516
|
+
prop["type"] = "array"
|
|
517
|
+
prop["items"] = {"type": "string", "enum": enum_values}
|
|
518
|
+
prop["uniqueItems"] = True
|
|
519
|
+
if field.min_selected is not None:
|
|
520
|
+
prop["minItems"] = field.min_selected
|
|
521
|
+
if field.max_selected is not None:
|
|
522
|
+
prop["maxItems"] = field.max_selected
|
|
523
|
+
else:
|
|
524
|
+
prop["type"] = "string"
|
|
525
|
+
prop["enum"] = enum_values
|
|
526
|
+
|
|
527
|
+
elif isinstance(field, RadioField):
|
|
528
|
+
prop["type"] = "string"
|
|
529
|
+
prop["enum"] = [opt.value for opt in field.options]
|
|
530
|
+
|
|
531
|
+
elif isinstance(field, CheckboxGroupField):
|
|
532
|
+
prop["type"] = "array"
|
|
533
|
+
prop["items"] = {
|
|
534
|
+
"type": "string",
|
|
535
|
+
"enum": [opt.value for opt in field.options],
|
|
536
|
+
}
|
|
537
|
+
prop["uniqueItems"] = True
|
|
538
|
+
|
|
539
|
+
elif isinstance(field, CheckboxField):
|
|
540
|
+
prop["type"] = "boolean"
|
|
541
|
+
|
|
542
|
+
elif isinstance(field, EmailField):
|
|
543
|
+
prop["type"] = "string"
|
|
544
|
+
prop["format"] = "email"
|
|
545
|
+
|
|
546
|
+
elif isinstance(field, NumberField):
|
|
547
|
+
prop["type"] = "number"
|
|
548
|
+
if field.min_value is not None:
|
|
549
|
+
prop["minimum"] = field.min_value
|
|
550
|
+
if field.max_value is not None:
|
|
551
|
+
prop["maximum"] = field.max_value
|
|
552
|
+
if field.step is not None:
|
|
553
|
+
prop["multipleOf"] = field.step
|
|
554
|
+
|
|
555
|
+
elif isinstance(field, DateField):
|
|
556
|
+
prop["type"] = "string"
|
|
557
|
+
prop["format"] = "date"
|
|
558
|
+
|
|
559
|
+
elif isinstance(field, FileField):
|
|
560
|
+
if field.multiple:
|
|
561
|
+
prop["type"] = "array"
|
|
562
|
+
prop["items"] = {"type": "string", "contentEncoding": "base64"}
|
|
563
|
+
else:
|
|
564
|
+
prop["type"] = "string"
|
|
565
|
+
prop["contentEncoding"] = "base64"
|
|
566
|
+
|
|
567
|
+
elif isinstance(field, HiddenField):
|
|
568
|
+
prop["type"] = "string"
|
|
569
|
+
|
|
570
|
+
elif isinstance(field, UrlField):
|
|
571
|
+
prop["type"] = "string"
|
|
572
|
+
prop["format"] = "uri"
|
|
573
|
+
if field.minlength is not None:
|
|
574
|
+
prop["minLength"] = field.minlength
|
|
575
|
+
if field.maxlength is not None:
|
|
576
|
+
prop["maxLength"] = field.maxlength
|
|
577
|
+
|
|
578
|
+
elif isinstance(field, TextareaField):
|
|
579
|
+
prop["type"] = "string"
|
|
580
|
+
if field.minlength is not None:
|
|
581
|
+
prop["minLength"] = field.minlength
|
|
582
|
+
if field.maxlength is not None:
|
|
583
|
+
prop["maxLength"] = field.maxlength
|
|
584
|
+
|
|
585
|
+
elif isinstance(field, ListField):
|
|
586
|
+
item_type = _LIST_ITEM_TYPE_MAP.get(field.item_type, "string")
|
|
587
|
+
prop["type"] = "array"
|
|
588
|
+
prop["items"] = {"type": item_type}
|
|
589
|
+
if field.min_items is not None:
|
|
590
|
+
prop["minItems"] = field.min_items
|
|
591
|
+
if field.max_items is not None:
|
|
592
|
+
prop["maxItems"] = field.max_items
|
|
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
|
+
|
|
602
|
+
elif isinstance(field, TextField):
|
|
603
|
+
prop["type"] = "string"
|
|
604
|
+
if field.minlength is not None:
|
|
605
|
+
prop["minLength"] = field.minlength
|
|
606
|
+
if field.maxlength is not None:
|
|
607
|
+
prop["maxLength"] = field.maxlength
|
|
608
|
+
if field.pattern is not None:
|
|
609
|
+
prop["pattern"] = field.pattern
|
|
610
|
+
|
|
611
|
+
else:
|
|
612
|
+
prop["type"] = "string"
|
|
613
|
+
|
|
614
|
+
if field.label:
|
|
615
|
+
prop["title"] = field.label
|
|
616
|
+
if field.help_text:
|
|
617
|
+
prop["description"] = field.help_text
|
|
618
|
+
if field.default_value is not None:
|
|
619
|
+
prop["default"] = field.default_value
|
|
620
|
+
if field.readonly:
|
|
621
|
+
prop["readOnly"] = True
|
|
622
|
+
|
|
623
|
+
return prop
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def form_to_json_schema(form: Form) -> Dict[str, Any]:
|
|
627
|
+
"""Convert a Form definition to a JSON Schema (draft-07) object.
|
|
628
|
+
|
|
629
|
+
All fields (including those inside FieldGroups and FormSteps) are
|
|
630
|
+
flattened into a single ``properties`` mapping via ``form.fields``.
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
A Python dict representing the JSON Schema.
|
|
634
|
+
"""
|
|
635
|
+
properties: Dict[str, Any] = {}
|
|
636
|
+
required = []
|
|
637
|
+
|
|
638
|
+
for field in form.fields:
|
|
639
|
+
properties[field.name] = _field_to_json_schema_property(field)
|
|
640
|
+
if field.required:
|
|
641
|
+
required.append(field.name)
|
|
642
|
+
|
|
643
|
+
schema: Dict[str, Any] = {
|
|
644
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
645
|
+
"type": "object",
|
|
646
|
+
"title": form.name,
|
|
647
|
+
"properties": properties,
|
|
648
|
+
"additionalProperties": False,
|
|
649
|
+
}
|
|
650
|
+
if required:
|
|
651
|
+
schema["required"] = required
|
|
652
|
+
|
|
653
|
+
return schema
|