codeforms 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
codeforms/forms.py ADDED
@@ -0,0 +1,530 @@
1
+ from codeforms.fields import *
2
+ from codeforms.i18n import t
3
+ import json
4
+ import re
5
+ from typing import Any
6
+
7
+ class Form(BaseModel):
8
+ id: UUID = Field(default_factory=uuid4)
9
+ name: str
10
+ content: List[Union[
11
+ TextField,
12
+ EmailField,
13
+ NumberField,
14
+ DateField,
15
+ SelectField,
16
+ RadioField,
17
+ CheckboxField,
18
+ CheckboxGroupField,
19
+ FileField,
20
+ HiddenField,
21
+ 'FieldGroup'
22
+ ]]
23
+ css_classes: Optional[str] = None
24
+ version: int = 1
25
+ attributes: Dict[str, str] = Field(default_factory=dict)
26
+ action: Optional[str] = None
27
+
28
+ @model_validator(mode='before')
29
+ @classmethod
30
+ def convert_fields_to_content(cls, data: Any) -> Any:
31
+ """
32
+ Validador para mantener retrocompatibilidad.
33
+ Convierte automáticamente 'fields' a 'content' si está presente.
34
+ """
35
+ if isinstance(data, dict):
36
+ # Si tiene 'fields' pero no 'content', convertir
37
+ if 'fields' in data and 'content' not in data:
38
+ data = data.copy()
39
+ data['content'] = data.pop('fields')
40
+ # Si tiene ambos, 'content' tiene prioridad
41
+ elif 'fields' in data and 'content' in data:
42
+ data = data.copy()
43
+ data.pop('fields') # Remover 'fields' redundante
44
+ return data
45
+
46
+ @property
47
+ def fields(self) -> List[Union[TextField, EmailField, NumberField, DateField, SelectField, RadioField, CheckboxField, CheckboxGroupField, FileField, HiddenField]]:
48
+ """
49
+ Devuelve una lista plana de todos los campos del formulario,
50
+ independientemente de si están en un grupo o no.
51
+ Mantiene retrocompatibilidad con el código existente.
52
+ """
53
+ all_fields = []
54
+ for item in self.content:
55
+ if hasattr(item, 'fields') and hasattr(item, 'title'): # Es un FieldGroup
56
+ all_fields.extend(item.fields)
57
+ else: # Es un campo individual
58
+ all_fields.append(item)
59
+ return all_fields
60
+
61
+ @staticmethod
62
+ def loads(form: Union[str, dict, bytearray]):
63
+ if isinstance(form, (str, bytearray)):
64
+ return Form.model_validate_json(form)
65
+ else:
66
+ return Form.model_validate(form)
67
+
68
+ @classmethod
69
+ def create_from_fields(cls, name: str, fields: List, **kwargs) -> 'Form':
70
+ """
71
+ Método de conveniencia para crear un formulario usando la estructura anterior
72
+ donde se pasaba directamente una lista de campos.
73
+ Mantiene retrocompatibilidad.
74
+ """
75
+ return cls(name=name, content=fields, **kwargs)
76
+
77
+ def to_dict(self, exclude_none: bool=True) -> Dict[str, Any]:
78
+ return json.loads(self.model_dump_json(exclude_none=exclude_none))
79
+
80
+ @model_validator(mode='after')
81
+ def validate_field_names(self) -> 'Form':
82
+ """Valida que todos los nombres de campos sean únicos en todo el formulario"""
83
+ names = [field.name for field in self.fields]
84
+ if len(names) != len(set(names)):
85
+ raise ValueError(t("form.unique_field_names"))
86
+ return self
87
+
88
+ def validate_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
89
+ """Valida todos los campos del formulario y retorna el resultado"""
90
+ errors = []
91
+ validated_data = {}
92
+
93
+ for field in self.fields:
94
+ field_value = data.get(field.name)
95
+
96
+ # Validar campo requerido
97
+ if field.required and field_value is None:
98
+ errors.append({
99
+ "field": field.name,
100
+ "message": t("field.required_named", name=field.name)
101
+ })
102
+ continue
103
+
104
+ # Validar según el tipo de campo
105
+ if isinstance(field, TextField):
106
+ is_valid, error_msg = field.validate_value(field_value)
107
+ if not is_valid:
108
+ errors.append({"field": field.name, "message": error_msg})
109
+ elif isinstance(field, EmailField):
110
+ if not re.match(r"[^@]+@[^@]+\.[^@]+", field_value):
111
+ errors.append({"field": field.name, "message": t("email.invalid")})
112
+ elif isinstance(field, NumberField):
113
+ try:
114
+ num_value = float(field_value)
115
+ if field.min_value is not None and num_value < field.min_value:
116
+ errors.append({"field": field.name, "message": t("number.min_value", min=field.min_value)})
117
+ if field.max_value is not None and num_value > field.max_value:
118
+ errors.append({"field": field.name, "message": t("number.max_value", max=field.max_value)})
119
+ except (ValueError, TypeError):
120
+ errors.append({"field": field.name, "message": t("number.invalid")})
121
+ elif isinstance(field, DateField):
122
+ try:
123
+ date_value = date.fromisoformat(field_value)
124
+ if field.min_date is not None and date_value < field.min_date:
125
+ errors.append({"field": field.name, "message": t("date.min_date", min=field.min_date)})
126
+ if field.max_date is not None and date_value > field.max_date:
127
+ errors.append({"field": field.name, "message": t("date.max_date", max=field.max_date)})
128
+ except (ValueError, TypeError):
129
+ errors.append({"field": field.name, "message": t("date.invalid_format")})
130
+ elif isinstance(field, SelectField):
131
+ valid_options = [opt.value for opt in field.options]
132
+ if field.multiple:
133
+ if not isinstance(field_value, list) or not all(v in valid_options for v in field_value):
134
+ errors.append({"field": field.name, "message": t("select.invalid_options")})
135
+ elif field_value not in valid_options:
136
+ errors.append({"field": field.name, "message": t("select.invalid_option")})
137
+ elif isinstance(field, RadioField):
138
+ if field_value not in [opt.value for opt in field.options]:
139
+ errors.append({"field": field.name, "message": t("radio.invalid_option")})
140
+ elif isinstance(field, CheckboxField):
141
+ if not isinstance(field_value, bool):
142
+ errors.append({"field": field.name, "message": t("checkbox.must_be_boolean")})
143
+ elif isinstance(field, CheckboxGroupField):
144
+ if not isinstance(field_value, list) or not all(v in [opt.value for opt in field.options] for v in field_value):
145
+ errors.append({"field": field.name, "message": t("checkbox_group.invalid_options")})
146
+
147
+ if not errors:
148
+ validated_data[field.name] = field_value
149
+
150
+ return {
151
+ "success": len(errors) == 0,
152
+ "data": validated_data if not errors else None,
153
+ "errors": errors,
154
+ "message": t("form.validation_success") if not errors else t("form.validation_error")
155
+ }
156
+
157
+ def export(self, output_format: str = 'html', **kwargs) -> dict:
158
+ from codeforms.export import ExportFormat
159
+ export_result = {'format': output_format}
160
+ if output_format in [format.value for format in ExportFormat]:
161
+ from codeforms.export import exporter
162
+ export_result = exporter(self, output_format=output_format, **kwargs)
163
+
164
+ elif output_format == 'dict':
165
+ export_result['output'] = self.to_dict()
166
+
167
+ elif output_format == 'json':
168
+ export_result['output'] = self.model_dump_json()
169
+
170
+ else:
171
+ raise ValueError(f"Unsupported export format: {output_format}")
172
+
173
+ return export_result
174
+
175
+ def to_json(self) -> str:
176
+ return self.export(output_format='json').get('output')
177
+
178
+ def set_default_values(self, data: Dict[str, Any]) -> 'Form':
179
+ """Establece los valores por defecto para los campos del formulario"""
180
+ for field in self.fields:
181
+ field.default_value = data.get(field.name)
182
+ return self
183
+
184
+
185
+ # Modelo para los datos recibidos
186
+ class FormDataModel(BaseModel):
187
+ def __init__(self, form: Form):
188
+ """
189
+ Crea dinámicamente un modelo basado en la estructura del formulario
190
+ """
191
+ fields = {}
192
+ for field in form.fields:
193
+ # Definir el tipo y validaciones basadas en el campo del formulario
194
+ if isinstance(field, EmailField):
195
+ fields[field.name] = (EmailStr, ... if field.required else None)
196
+ elif isinstance(field, CheckboxGroupField):
197
+ fields[field.name] = (List[str], [] if not field.required else ...)
198
+ elif isinstance(field, CheckboxField):
199
+ fields[field.name] = (bool, ... if field.required else False)
200
+ elif isinstance(field, NumberField):
201
+ fields[field.name] = (float, ... if field.required else None)
202
+ elif isinstance(field, DateField):
203
+ fields[field.name] = (date, ... if field.required else None)
204
+ else:
205
+ fields[field.name] = (str, ... if field.required else None)
206
+
207
+ # Crear el modelo dinámicamente
208
+ self.__class__ = type(
209
+ 'DynamicFormData',
210
+ (BaseModel,),
211
+ {
212
+ '__annotations__': fields,
213
+ 'model_config': {
214
+ 'extra': 'forbid' # No permitir campos extra
215
+ }
216
+ }
217
+ )
218
+ super().__init__()
219
+
220
+
221
+ class FormDataValidator:
222
+ @staticmethod
223
+ def create_model(form: Form) -> Type[BaseModel]:
224
+ fields = {}
225
+ annotations = {}
226
+ validations = {}
227
+
228
+ for field in form.fields:
229
+ # Configurar el tipo y las validaciones según el tipo de campo
230
+ if isinstance(field, SelectField):
231
+ valid_values = field.get_valid_values()
232
+ if field.multiple:
233
+ field_type = List[str]
234
+
235
+ # Crear validador para la lista de valores
236
+ def create_validator(valid_values=valid_values,
237
+ min_selected=field.min_selected,
238
+ max_selected=field.max_selected):
239
+ def validate_select_values(v: List[str]) -> List[str]:
240
+ if not v and field.required:
241
+ raise ValueError(t("field.required"))
242
+
243
+ # Validar que todos los valores sean válidos
244
+ invalid_values = set(v) - valid_values
245
+ if invalid_values:
246
+ raise ValueError(
247
+ t("select.invalid_values", values=', '.join(invalid_values))
248
+ )
249
+
250
+ # Validar cantidad mínima de selecciones
251
+ if min_selected is not None and len(v) < min_selected:
252
+ raise ValueError(
253
+ t("select.min_selected", min=min_selected)
254
+ )
255
+
256
+ # Validar cantidad máxima de selecciones
257
+ if max_selected is not None and len(v) > max_selected:
258
+ raise ValueError(
259
+ t("select.max_selected", max=max_selected)
260
+ )
261
+
262
+ return v
263
+
264
+ return validate_select_values
265
+
266
+ validations[field.name] = field_validator(field.name)(
267
+ create_validator()
268
+ )
269
+ else:
270
+ field_type = str
271
+
272
+ # Crear validador para valor único
273
+ def create_validator(valid_values=valid_values):
274
+ def validate_select_value(v: str) -> str:
275
+ if not v and field.required:
276
+ raise ValueError(t("field.required"))
277
+ if v not in valid_values:
278
+ raise ValueError(
279
+ t("select.invalid_value_must_be_one_of", valid=', '.join(valid_values))
280
+ )
281
+ return v
282
+
283
+ return validate_select_value
284
+
285
+ validations[field.name] = field_validator(field.name)(
286
+ create_validator()
287
+ )
288
+ elif isinstance(field, EmailField):
289
+ field_type = EmailStr
290
+ elif isinstance(field, CheckboxGroupField):
291
+ field_type = List[str]
292
+ elif isinstance(field, CheckboxField):
293
+ field_type = bool
294
+ elif isinstance(field, NumberField):
295
+ field_type = float
296
+ elif isinstance(field, DateField):
297
+ field_type = date
298
+ else:
299
+ field_type = str
300
+
301
+ # Definir el campo con sus validaciones
302
+ if field.required:
303
+ fields[field.name] = Field(..., description=field.help_text)
304
+ else:
305
+ default_value = None
306
+ if field_type == List[str]:
307
+ default_value = []
308
+ fields[field.name] = Field(default=default_value, description=field.help_text)
309
+
310
+ annotations[field.name] = field_type
311
+
312
+ # Crear el modelo dinámicamente
313
+ model_name = f"Dynamic{form.name.title()}Data"
314
+ model = type(
315
+ model_name,
316
+ (BaseModel,),
317
+ {
318
+ '__annotations__': annotations,
319
+ **fields,
320
+ **validations,
321
+ 'model_config': ConfigDict(
322
+ arbitrary_types_allowed=True,
323
+ extra='forbid'
324
+ )
325
+ }
326
+ )
327
+
328
+ return model
329
+
330
+
331
+ def validate_form_data(form: Form, data: Dict[str, Any]) -> Dict[str, Any]:
332
+ try:
333
+ validated_data = {}
334
+ for field in form.fields:
335
+ field_value = data.get(field.name)
336
+
337
+ # Si no hay valor, usar el valor por defecto
338
+ if field_value is None:
339
+ field_value = field.default_value
340
+
341
+ # Validar campo requerido después de considerar el valor por defecto
342
+ if field.required and field_value is None:
343
+ return {
344
+ "success": False,
345
+ "errors": [{
346
+ "field": field.name,
347
+ "message": t("field.required_named", name=field.name)
348
+ }],
349
+ "message": t("form.data_validation_error")
350
+ }
351
+
352
+ # Procesar valores específicos por tipo
353
+ if field_value is not None:
354
+ # Select validation (debe ir antes de otros tipos)
355
+ if field.field_type == FieldType.SELECT:
356
+ valid_options = [opt.value for opt in field.options]
357
+ if field.multiple:
358
+ # Asegurar que field_value sea una lista
359
+ if isinstance(field_value, str):
360
+ field_value = [field_value]
361
+ if not isinstance(field_value, list):
362
+ return {
363
+ "success": False,
364
+ "errors": [{
365
+ "field": field.name,
366
+ "message": t("select.value_must_be_list")
367
+ }],
368
+ "message": t("form.data_validation_error")
369
+ }
370
+ # Validar cada valor en la lista
371
+ invalid_values = [v for v in field_value if v not in valid_options]
372
+ if invalid_values:
373
+ return {
374
+ "success": False,
375
+ "errors": [{
376
+ "field": field.name,
377
+ "message": t("select.invalid_values", values=str(invalid_values))
378
+ }],
379
+ "message": t("form.data_validation_error")
380
+ }
381
+ else:
382
+ # Validar valor único
383
+ if field_value not in valid_options:
384
+ return {
385
+ "success": False,
386
+ "errors": [{
387
+ "field": field.name,
388
+ "message": t("select.invalid_option_value", value=field_value, valid=str(valid_options))
389
+ }],
390
+ "message": t("form.data_validation_error")
391
+ }
392
+ validated_data[field.name] = field_value
393
+
394
+ # Email validation
395
+ elif field.field_type == FieldType.EMAIL:
396
+ try:
397
+ if not re.match(r"[^@]+@[^@]+\.[^@]+", field_value):
398
+ raise ValueError("Invalid email format")
399
+ validated_data[field.name] = field_value
400
+ except ValueError:
401
+ return {
402
+ "success": False,
403
+ "errors": [{
404
+ "field": field.name,
405
+ "message": t("email.invalid")
406
+ }],
407
+ "message": t("form.data_validation_error")
408
+ }
409
+
410
+ # Checkbox group validation
411
+ elif field.field_type == FieldType.CHECKBOX and hasattr(field, 'options'):
412
+ valid_options = [opt.value for opt in field.options]
413
+
414
+ # Ensure field_value is a list
415
+ if isinstance(field_value, str):
416
+ field_value = [field_value]
417
+
418
+ if not isinstance(field_value, list):
419
+ return {
420
+ "success": False,
421
+ "errors": [{
422
+ "field": field.name,
423
+ "message": t("select.value_must_be_list")
424
+ }],
425
+ "message": t("form.data_validation_error")
426
+ }
427
+
428
+ # Check if all values are valid options
429
+ invalid_values = [v for v in field_value if v not in valid_options]
430
+ if invalid_values:
431
+ return {
432
+ "success": False,
433
+ "errors": [{
434
+ "field": field.name,
435
+ "message": t("select.invalid_values", values=str(invalid_values))
436
+ }],
437
+ "message": t("form.data_validation_error")
438
+ }
439
+ validated_data[field.name] = field_value
440
+
441
+ # Single checkbox validation
442
+ elif field.field_type == FieldType.CHECKBOX and not hasattr(field, 'options'):
443
+ # Convertir a booleano si es necesario
444
+ validated_data[field.name] = bool(field_value)
445
+
446
+ # Radio validation
447
+ elif field.field_type == FieldType.RADIO:
448
+ valid_options = [opt.value for opt in field.options]
449
+ if field_value not in valid_options:
450
+ return {
451
+ "success": False,
452
+ "errors": [{
453
+ "field": field.name,
454
+ "message": t("radio.invalid_option")
455
+ }],
456
+ "message": t("form.data_validation_error")
457
+ }
458
+ validated_data[field.name] = field_value
459
+
460
+ # Number validation
461
+ elif field.field_type == FieldType.NUMBER:
462
+ try:
463
+ num_value = float(field_value)
464
+ if hasattr(field, 'min_value') and field.min_value is not None:
465
+ if num_value < field.min_value:
466
+ raise ValueError(t("number.min_value", min=field.min_value))
467
+ if hasattr(field, 'max_value') and field.max_value is not None:
468
+ if num_value > field.max_value:
469
+ raise ValueError(t("number.max_value", max=field.max_value))
470
+ validated_data[field.name] = num_value
471
+ except ValueError as e:
472
+ return {
473
+ "success": False,
474
+ "errors": [{
475
+ "field": field.name,
476
+ "message": str(e)
477
+ }],
478
+ "message": t("form.data_validation_error")
479
+ }
480
+
481
+ # Text validation
482
+ elif field.field_type == FieldType.TEXT:
483
+ if not isinstance(field_value, str):
484
+ field_value = str(field_value)
485
+ if hasattr(field, 'minlength') and field.minlength is not None:
486
+ if len(field_value) < field.minlength:
487
+ return {
488
+ "success": False,
489
+ "errors": [{
490
+ "field": field.name,
491
+ "message": t("text.minlength", min=field.minlength)
492
+ }],
493
+ "message": t("form.data_validation_error")
494
+ }
495
+ if hasattr(field, 'maxlength') and field.maxlength is not None:
496
+ if len(field_value) > field.maxlength:
497
+ return {
498
+ "success": False,
499
+ "errors": [{
500
+ "field": field.name,
501
+ "message": t("text.maxlength", max=field.maxlength)
502
+ }],
503
+ "message": t("form.data_validation_error")
504
+ }
505
+ validated_data[field.name] = field_value
506
+
507
+ # Default validation for other types
508
+ else:
509
+ validated_data[field.name] = field_value
510
+
511
+ return {
512
+ "success": True,
513
+ "data": validated_data,
514
+ "message": t("form.validation_success")
515
+ }
516
+
517
+ except Exception as e:
518
+ return {
519
+ "success": False,
520
+ "errors": [{
521
+ "field": "unknown",
522
+ "message": str(e)
523
+ }],
524
+ "message": t("form.data_validation_error")
525
+ }
526
+
527
+
528
+
529
+
530
+