From ff259fbd9820fdfb111eaf5b87f8ce2b1033fb53 Mon Sep 17 00:00:00 2001 From: RafaUC <35164744@uaz.edu.mx> Date: Sat, 2 Sep 2023 02:33:58 -0600 Subject: [PATCH] Se implementaron las validaciones personalizadas de los formularios y el algoritmo de ordenamiento de los elementos en el template --- estudio_socio_economico/admin.py | 7 +- estudio_socio_economico/forms.py | 175 ++++++++++++++++-- estudio_socio_economico/models.py | 52 ++---- .../templates/solicitante/estudioSE.html | 79 +++++++- estudio_socio_economico/views.py | 88 ++++++--- static/css/main.css | 4 + 6 files changed, 318 insertions(+), 87 deletions(-) diff --git a/estudio_socio_economico/admin.py b/estudio_socio_economico/admin.py index f5d3b9a..ed65a63 100644 --- a/estudio_socio_economico/admin.py +++ b/estudio_socio_economico/admin.py @@ -1,10 +1,12 @@ from django.contrib import admin -from .models import Seccion, Opcion, Respuesta, RTextoCorto, RTextoParrafo, ROpcionMultiple, RCasillas, RDesplegable, Elemento +from .models import Seccion, Opcion, Respuesta, RTextoCorto, RTextoParrafo, ROpcionMultiple, RCasillas, RDesplegable, RNumerico, RHora, RFecha , Elemento # Register your models here. class ElementoAdmin(admin.ModelAdmin): list_display = ('nombre', 'tipo', 'row', 'col') + + admin.site.register(Seccion) admin.site.register(Opcion) admin.site.register(Respuesta) @@ -13,4 +15,7 @@ admin.site.register(RTextoParrafo) admin.site.register(ROpcionMultiple) admin.site.register(RCasillas) admin.site.register(RDesplegable) +admin.site.register(RNumerico) +admin.site.register(RHora) +admin.site.register(RFecha) admin.site.register(Elemento, ElementoAdmin) \ No newline at end of file diff --git a/estudio_socio_economico/forms.py b/estudio_socio_economico/forms.py index a4ec287..305b336 100644 --- a/estudio_socio_economico/forms.py +++ b/estudio_socio_economico/forms.py @@ -1,8 +1,8 @@ from django import forms from .models import Seccion, Opcion, Elemento from django.forms import inlineformset_factory, modelformset_factory -from .models import RNumerico, RTextoCorto, RTextoParrafo, RHora, RFecha, ROpcionMultiple, RCasillas, RDesplegable - +from .models import RNumerico, RTextoCorto, RTextoParrafo, RHora, RFecha, ROpcionMultiple, RCasillas, RDesplegable, Respuesta +from django.core.validators import MinLengthValidator, MaxLengthValidator class SeccionForm(forms.ModelForm): @@ -11,7 +11,7 @@ class SeccionForm(forms.ModelForm): fields = ['nombre', 'tipo', 'orden'] widgets = { 'nombre': forms.TextInput(attrs={'class': 'form-control form-control-lg font-semi-bold border-3'}), - 'tipo': forms.RadioSelect(attrs={'class': 'ms-3'}), + 'tipo': forms.RadioSelect(attrs={'class': 'form-check-input checkbox-principal'}), 'orden': forms.NumberInput(attrs={'class': 'form-control', 'type': 'hidden'}), } labels = { @@ -71,19 +71,59 @@ ElementoFormSet = inlineformset_factory(Seccion, Elemento, form=ElementoForm, ex OpcionFormSet = inlineformset_factory(Elemento, Opcion, form=OpcionForm, extra=1, can_delete=True) class RespuestaForm(forms.ModelForm): + class Meta: + model = Respuesta + fields = [] + def __init__(self, *args, **kwargs): - elemento = kwargs.pop("elemento") - solicitante = kwargs.pop("solicitante") + elemento = kwargs.pop("elemento", None) + solicitante = kwargs.pop("solicitante", None) super(RespuestaForm, self).__init__(*args, **kwargs) - self.instance.solicitante = solicitante - self.instance.elemento = elemento + if elemento and solicitante: + self.instance.solicitante = solicitante + self.instance.elemento = elemento + + if self.instance and hasattr(self.instance, 'elemento') and self.fields: + elemento_nombre = self.instance.elemento.nombre + first_field_name = list(self.fields.keys())[0] + self.fields[first_field_name].widget.attrs['placeholder'] = elemento_nombre + + """def add_prefix(self, field_name): + field_name = super(RespuestaForm, self).add_prefix(field_name) + return self.prefix """ + class RNumericoForm(RespuestaForm): class Meta: model = RNumerico - fields = ['texto'] - widgets = {'texto': forms.NumberInput(attrs={'class': 'form-control'}),} - labels = {'texto': 'Respuesta numérica'} + fields = ['valor'] + widgets = {'valor': forms.TextInput(attrs={'class': 'form-control border-3', 'onkeypress': "return isNumberPuntKey(event)"}),} + labels = {'valor': 'Respuesta numérica'} + + def __init__(self, *args, **kwargs): + super(RNumericoForm, self).__init__(*args, **kwargs) + + # Accede a la instancia de elemento y obtén los valores numMin y numMax + elemento = self.instance.elemento if self.instance else None + numMin = elemento.numMin if elemento else None + numMax = elemento.numMax if elemento else None + + # Configura validadores de longitud mínima y máxima en el campo 'valor' + if numMin is not None: + self.fields['valor'].validators.append(MinLengthValidator(numMin)) + self.fields['valor'].widget.attrs['minlength'] = numMin + if numMax is not None: + self.fields['valor'].validators.append(MaxLengthValidator(numMax)) + self.fields['valor'].widget.attrs['maxlength'] = numMax + + + def clean_valor(self): + obligatorio = self.instance.elemento.obligatorio + r = self.cleaned_data.get('valor') + if (not r or not r.strip()) and obligatorio: + raise forms.ValidationError("Este campo es Obligatorio.") + return r + class RTextoCortoForm(RespuestaForm): class Meta: @@ -92,6 +132,14 @@ class RTextoCortoForm(RespuestaForm): widgets = {'texto': forms.TextInput(attrs={'class': 'form-control'}),} labels = {'texto': 'Respuesta texto corto'} + def clean_texto(self): + obligatorio = self.instance.elemento.obligatorio + r = self.cleaned_data.get('texto') + if (not r or not r.strip()) and obligatorio: + raise forms.ValidationError("Este campo es Obligatorio.") + return r + + class RTextoParrafoForm(RespuestaForm): class Meta: model = RTextoParrafo @@ -99,40 +147,135 @@ class RTextoParrafoForm(RespuestaForm): widgets = {'texto': forms.Textarea(attrs={'class': 'form-control'}),} labels = {'texto': 'Respuesta párrafo'} + def clean_texto(self): + obligatorio = self.instance.elemento.obligatorio + r = self.cleaned_data.get('texto') + if (not r or not r.strip()) and obligatorio: + raise forms.ValidationError("Este campo es Obligatorio.") + return r + + class RHoraForm(RespuestaForm): class Meta: model = RHora fields = ['hora'] - widgets = {'hora': forms.TimeInput(attrs={'class': 'form-control'}),} + widgets = {'hora': forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}),} labels = {'hora': 'Hora'} + def clean_hora(self): + obligatorio = self.instance.elemento.obligatorio + r = self.cleaned_data.get('hora') + if (not r) and obligatorio: + raise forms.ValidationError("Este campo es Obligatorio.") + return r + + class RFechaForm(RespuestaForm): class Meta: model = RFecha fields = ['fecha'] - widgets = {'fecha': forms.DateInput(attrs={'class': 'form-control'}),} + widgets = {'fecha': forms.DateInput(format=('%Y-%m-%d'), attrs={'class': 'form-control border-3', 'type': 'date'}),} labels = {'fecha': 'Fecha'} + def clean_fecha(self): + obligatorio = self.instance.elemento.obligatorio + r = self.cleaned_data.get('fecha') + if (not r) and obligatorio: + raise forms.ValidationError("Este campo es Obligatorio.") + return r + + class ROpcionMultipleForm(RespuestaForm): class Meta: model = ROpcionMultiple fields = ['respuesta', 'otro'] - widgets = {'respuesta': forms.Select(attrs={'class': 'form-control'}), + widgets = {'respuesta': forms.RadioSelect(attrs={'class': 'form-check-input checkbox-principal'}), 'otro': forms.TextInput(attrs={'class': 'form-control'}),} labels = {'respuesta': 'Respuesta de opción múltiple', 'otro': 'Otro'} + def clean(self): + cleaned_data = super().clean() + obligatorio = self.instance.elemento.obligatorio + opcOtro = self.instance.elemento.opcionOtro + respuesta = cleaned_data.get('respuesta') + otro = cleaned_data.get('otro') + if obligatorio: + if not ((opcOtro and (otro and otro.strip())) or not opcOtro) and (respuesta and respuesta.nombre == 'Otro'): + raise forms.ValidationError("Este campo es obligatorio") + if (otro and otro.strip()) and (respuesta and not respuesta.nombre == 'Otro'): + raise forms.ValidationError("No esta seleccionada opcion Otro") + if respuesta and (respuesta.nombre == 'Otro' and not( otro and otro.strip())): + raise forms.ValidationError("Si eliges 'otro', debes proporcionar más detalles en el campo 'otro'.") + return cleaned_data + + + def clean_respuesta(self): + respuesta = self.cleaned_data.get('respuesta') + obligatorio = self.instance.elemento.obligatorio + if (not respuesta) and obligatorio: + raise forms.ValidationError("Este campo es Obligatorio.") + return respuesta + + class RCasillasForm(RespuestaForm): class Meta: model = RCasillas fields = ['respuesta', 'otro'] - widgets = {'respuesta': forms.CheckboxSelectMultiple(attrs={'class': 'form-check'}), + widgets = {'respuesta': forms.CheckboxSelectMultiple(attrs={'class': 'form-check-input checkbox-principal form-check-inline-custom', 'style': 'display: inline-block;'}), 'otro': forms.TextInput(attrs={'class': 'form-control'}),} labels = {'respuestas': 'Casillas', 'otro': 'Otro'} + def clean(self): + cleaned_data = super().clean() + obligatorio = self.instance.elemento.obligatorio + opcOtro = self.instance.elemento.opcionOtro + respuesta = cleaned_data.get('respuesta') + otro = cleaned_data.get('otro') + if obligatorio: + if not ((opcOtro and (otro and otro.strip())) or not opcOtro) and (respuesta and respuesta.filter(nombre='Otro').exists()): + raise forms.ValidationError("Este campo es obligatorio") + if (otro and otro.strip()) and (respuesta and not respuesta.filter(nombre='Otro').exists()): + raise forms.ValidationError("No esta seleccionada opcion Otro") + if respuesta and (respuesta.filter(nombre='Otro').exists() and not( otro and otro.strip())): + raise forms.ValidationError("Si eliges 'otro', debes proporcionar más detalles en el campo 'otro'.") + return cleaned_data + + def clean_respuesta(self): + respuesta = self.cleaned_data.get('respuesta') + noRespuesta = respuesta.count() == 0 + obligatorio = self.instance.elemento.obligatorio + if (noRespuesta) and obligatorio: + raise forms.ValidationError("Este campo es Obligatorio.") + return respuesta + + class RDesplegableForm(RespuestaForm): class Meta: model = RDesplegable fields = ['respuesta', 'otro'] - widgets = {'respuesta': forms.Select(attrs={'class': 'form-control'}), + widgets = {'respuesta': forms.Select(attrs={'class': 'form-control form-select border-3'}), 'otro': forms.TextInput(attrs={'class': 'form-control'}),} - labels = {'respuesta': 'Respuesta de desplegable', 'otro': 'Otro'} \ No newline at end of file + labels = {'respuesta': 'Respuesta de desplegable', 'otro': 'Otro'} + + def clean(self): + cleaned_data = super().clean() + obligatorio = self.instance.elemento.obligatorio + opcOtro = self.instance.elemento.opcionOtro + respuesta = cleaned_data.get('respuesta') + otro = cleaned_data.get('otro') + if obligatorio: + if not ((opcOtro and (otro and otro.strip())) or not opcOtro) and (respuesta and respuesta.nombre == 'Otro'): + raise forms.ValidationError("Este campo es obligatorio") + if (otro and otro.strip()) and (respuesta and not respuesta.nombre == 'Otro'): + raise forms.ValidationError("No esta seleccionada opcion Otro") + if respuesta and (respuesta.nombre == 'Otro' and not( otro and otro.strip())): + raise forms.ValidationError("Si eliges 'otro', debes proporcionar más detalles en el campo 'otro'.") + return cleaned_data + + + def clean_respuesta(self): + respuesta = self.cleaned_data.get('respuesta') + obligatorio = self.instance.elemento.obligatorio + if (not respuesta) and obligatorio: + raise forms.ValidationError("Este campo es Obligatorio.") + return respuesta diff --git a/estudio_socio_economico/models.py b/estudio_socio_economico/models.py index 56f75d4..bd6ebfa 100644 --- a/estudio_socio_economico/models.py +++ b/estudio_socio_economico/models.py @@ -26,7 +26,7 @@ class Seccion(models.Model): class Opcion(models.Model): - elemento = models.ForeignKey('Elemento', on_delete=models.CASCADE) + elemento = models.ForeignKey('Elemento', on_delete=models.CASCADE, null=True, blank=True) nombre = models.CharField(max_length=255, verbose_name='Nombre Opción') orden = models.IntegerField(verbose_name='orden', default=100_000) @@ -69,65 +69,39 @@ class Respuesta(models.Model): verbose_name_plural = 'Respuestas' unique_together = ['elemento', 'solicitante'] - def clean(self): - if self.elemento.obligatorio and self.is_blank(): - raise ValidationError(f"La respuesta para '{self.elemento}' es obligatoria.") - super().clean() - - def is_blank(self): - raise NotImplementedError("Subclases de Respuesta deben implementar este método.") - class RNumerico(Respuesta): - texto = models.CharField(max_length=255) + valor = models.CharField(max_length=255, null=True, blank=True) + - def is_blank(self): - return not self.texto.strip() class RTextoCorto(Respuesta): - texto = models.CharField(max_length=255) - - def is_blank(self): - return not self.texto.strip() + texto = models.CharField(max_length=255, null=True, blank=True) + class RTextoParrafo(Respuesta): - texto = models.TextField() + texto = models.TextField(null=True, blank=True) - def is_blank(self): - return not self.texto.strip() class RHora(Respuesta): - hora = models.TimeField() + hora = models.TimeField(null=True, blank=True) - def is_blank(self): - return self.hora is None class RFecha(Respuesta): - fecha = models.DateField() + fecha = models.DateField(null=True, blank=True) - def is_blank(self): - return self.fecha is None class ROpcionMultiple(Respuesta): respuesta = models.ForeignKey(Opcion, on_delete=models.CASCADE, null=True, blank=True) - otro = models.CharField(max_length=255, verbose_name="Otro", null=True, blank=False) - - def is_blank(self): - return self.respuesta is None and not ((self.elemento.opcionOtro and self.otro.strip()) or not self.elemento.opcionOtro ) + otro = models.CharField(max_length=255, verbose_name="Otro", null=True, blank=True) class RCasillas(Respuesta): - respuesta = models.ManyToManyField(Opcion) - otro = models.CharField(max_length=255, verbose_name="Otro", null=True, blank=False) - - def is_blank(self): - return self.respuesta is None and not ((self.elemento.opcionOtro and self.otro.strip()) or not self.elemento.opcionOtro ) + respuesta = models.ManyToManyField(Opcion, blank=True) + otro = models.CharField(max_length=255, verbose_name="Otro", null=True, blank=True) class RDesplegable(Respuesta): - respuesta = models.ForeignKey(Opcion, on_delete=models.CASCADE) - otro = models.CharField(max_length=255, verbose_name="Otro", null=True, blank=False) - - def is_blank(self): - return self.respuesta is None and not ((self.elemento.opcionOtro and self.otro.strip()) or not self.elemento.opcionOtro ) + respuesta = models.ForeignKey(Opcion, on_delete=models.CASCADE, null=True, blank=True) + otro = models.CharField(max_length=255, verbose_name="Otro", null=True, blank=True) #---------Elementos----------- diff --git a/estudio_socio_economico/templates/solicitante/estudioSE.html b/estudio_socio_economico/templates/solicitante/estudioSE.html index 561ee30..46bc7e9 100644 --- a/estudio_socio_economico/templates/solicitante/estudioSE.html +++ b/estudio_socio_economico/templates/solicitante/estudioSE.html @@ -1,14 +1,83 @@ {% extends 'base.html' %} +{% load custom_filters %} {% block body %} -

Pagina Estudio Socio Economico (implementacion pendiente)

+

Estudio Socioeconómico

- {% for form_name, form in forms.items %} -

{{ form_name }}

- {% csrf_token %} - {{ form.as_p }} + {% csrf_token %} + {% for seccion in preguntasEstudio %} +
+

{{ seccion.nombre }}

+
+ {% for elemento in seccion.elemento_set.all %} + {% with forms|hashD:elemento.id as form %} + +
+
+ {{ elemento.nombre }} + {% if elemento.obligatorio %} + * + {% endif %} +
+ {% for field in form %} + + {% if elemento.opcionOtro and field.label == 'Otro' %} +
{{ elemento.id }}
+ {{ field.label }}{{ field }} + {% elif field.label == 'Otro' %} + + {% else %} + {% if elemento.tipo == 'opcion_multiple' or elemento.tipo == 'casillas' %} +
{{ elemento.id }}
+
+
+ {% for choice in field %} + {{ choice }} + {% endfor %} +
+
+ + + {% else %} +
{{ elemento.id }}
+ {{ field.label }}{{ field }} + {% endif %} + {% endif %} + +
+ {% if field.errors %} + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% endif %} +
+ + {% endfor %} + +
+ {% for error in form.non_field_errors %} + {{ error }} + {% endfor %} +
+
+ + {% endwith %} + + {% with c=seccion.elemento_set.all|hashD:forloop.counter %} + {% with cb=elemento %} + {% if c.row != cb.row and c is not None %} +
+
+ {% endif %} + {% endwith %} + {% endwith %} + {% endfor %} +
+
{% endfor %} + +
diff --git a/estudio_socio_economico/views.py b/estudio_socio_economico/views.py index 5724375..b61e42c 100644 --- a/estudio_socio_economico/views.py +++ b/estudio_socio_economico/views.py @@ -21,45 +21,81 @@ def estudioSE(request): solicitante = get_object_or_404(Solicitante, pk=request.user.id) #obtener los formularios del estudio SE preguntasEstudio = Seccion.objects.prefetch_related('elemento_set__opcion_set').all() - + forms = {} + opcOtro = get_object_or_404(Opcion, nombre = "Otro") #se obtienen e indexan las respuestas existentes del usuario respuestas = {} - respuestasExistentes = Respuesta.objects.filter(solicitante = solicitante) + respuestasExistentes = Respuesta.objects.filter(solicitante = solicitante).select_subclasses() for respuesta in respuestasExistentes: respuestas[respuesta.elemento_id] = respuesta #generando Forms #se genera un diccionario con los tipos de forms formModels = {} - formModelsConOpcion = [ROpcionMultipleForm, RCasillasForm, RDesplegableForm] - forms = {} + formModelsConOpcion = [ROpcionMultipleForm, RCasillasForm, RDesplegableForm] for formtype in [RNumericoForm, RTextoCortoForm, RTextoParrafoForm, RHoraForm, RFechaForm, ROpcionMultipleForm, RCasillasForm, RDesplegableForm]: formModels[formtype.Meta.model] = formtype - for seccion in preguntasEstudio: - for elemento in seccion.elemento_set.all(): - #si la pregunta no es de una opcion de las que se tiene formulario se salta esta iteracion - if elemento.getRespuestaModel() is None: - continue - formModel = formModels[elemento.getRespuestaModel()] - #si ya existe una respuesta de esta pregunta - if elemento.id in respuestas: - forms[elemento.id] = formModel(instance=respuestas[elemento.id]) - #si no existe una respuesta de esta pregunta - else: - forms[elemento.id] = formModel(elemento=elemento, solicitante=solicitante) - #si son formularios de opcion se asignan las opciones - if formModel in formModelsConOpcion: - choices = [(opcion.id, opcion.nombre) for opcion in elemento.opcion_set.all()] - if isinstance(forms[elemento.id].fields['respuesta'].widget, FForms.Select): - # Agrega la opción en blanco al comienzo solo si el campo es un 'select' - choices.insert(0, ("", "---------")) - forms[elemento.id].fields['respuesta'].choices = choices - #forms[elemento.id].fields['respuesta'].queryset = elemento.opcion_set.all() - + + if request.method == 'GET': + for seccion in preguntasEstudio: + for elemento in seccion.elemento_set.all(): + #si la pregunta no es de una opcion de las que se tiene formulario se salta esta iteracion + if elemento.getRespuestaModel() is None: + continue + formModel = formModels[elemento.getRespuestaModel()] + #si ya existe una respuesta de esta pregunta + if elemento.id in respuestas: + forms[elemento.id] = formModel(instance=respuestas[elemento.id], prefix=f'respuesta_{elemento.id}') + #si no existe una respuesta de esta pregunta + else: + forms[elemento.id] = formModel(elemento=elemento, solicitante=solicitante, prefix=f'respuesta_{elemento.id}') + #si son formularios de opcion se asignan las opciones + if formModel in formModelsConOpcion: + choices = [(opcion.id, opcion.nombre) for opcion in elemento.opcion_set.all()] + if isinstance(forms[elemento.id].fields['respuesta'].widget, FForms.Select): + # Agrega la opción en blanco al comienzo solo si el campo es un 'select' + choices.insert(0, ("", "---------")) + if elemento.opcionOtro : + choices.append((opcOtro.id, opcOtro.nombre) ) + forms[elemento.id].fields['respuesta'].choices = choices + #forms[elemento.id].fields['respuesta'].queryset = elemento.opcion_set.all() + + if request.method == 'POST': + print(request.POST) + for seccion in preguntasEstudio: + for elemento in seccion.elemento_set.all(): + #si la pregunta no es de una opcion de las que se tiene formulario se salta esta iteracion + if elemento.getRespuestaModel() is None: + continue + formModel = formModels[elemento.getRespuestaModel()] + #si ya existe una respuesta de esta pregunta + if elemento.id in respuestas: + forms[elemento.id] = formModel(request.POST, instance=respuestas[elemento.id], prefix=f'respuesta_{elemento.id}') + #si no existe una respuesta de esta pregunta + else: + forms[elemento.id] = formModel(request.POST, elemento=elemento, solicitante=solicitante, prefix=f'respuesta_{elemento.id}') + #si son formularios de opcion se asignan las opciones + if formModel in formModelsConOpcion: + choices = [(opcion.id, opcion.nombre) for opcion in elemento.opcion_set.all()] + if isinstance(forms[elemento.id].fields['respuesta'].widget, FForms.Select): + # Agrega la opción en blanco al comienzo solo si el campo es un 'select' + choices.insert(0, ("", "---------")) + if elemento.opcionOtro : + choices.append((opcOtro.id, opcOtro.nombre) ) + forms[elemento.id].fields['respuesta'].choices = choices + #forms[elemento.id].fields['respuesta'].queryset = elemento.opcion_set.all() + + for form in forms.values(): + form.is_valid() + print(form.instance.elemento_id) + print(form.errors) + print('aa') + - context = { + context = { 'forms': forms, + 'preguntasEstudio': preguntasEstudio, } diff --git a/static/css/main.css b/static/css/main.css index b281012..c55e67e 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -486,4 +486,8 @@ th { padding: 20px; border-radius: 5px; text-align: center; +} + +.form-check-inline-custom{ + display: inline-block; } \ No newline at end of file -- GitLab