diff --git a/becas_cozcyt/settings.py b/becas_cozcyt/settings.py index 5ec2c38f3322299354f529bcc8cb9760ccf4106d..19d4570f93f42bc50c6b4e47f2dba7d18b26a1b4 100644 --- a/becas_cozcyt/settings.py +++ b/becas_cozcyt/settings.py @@ -28,6 +28,11 @@ SECRET_KEY = 'django-insecure-%l2!-7+%fqz_(bmjzm=@($a_ys5oq=-t#v%f2=^tt0(8s%1a0& # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True +if False: #if DEBUG: # + import socket # only if you haven't already imported this + hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) + INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + ["127.0.0.1", "10.0.2.2"] + ALLOWED_HOSTS = [] @@ -45,17 +50,19 @@ INSTALLED_APPS = [ 'modalidades', 'solicitudes', 'estudio_socio_economico', + 'debug_toolbar', ] MIDDLEWARE = [ + 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - "django_htmx.middleware.HtmxMiddleware", + "django_htmx.middleware.HtmxMiddleware", ] ROOT_URLCONF = 'becas_cozcyt.urls' @@ -160,4 +167,6 @@ EMAIL_HOST_PASSWORD = 'osrruvryhaqpmegi' EMAIL_PORT = 587 EMAIL_USE_TLS = True -PASSWORD_RESET_TIMEOUT = 86400 #El usuario cuenta con 24 horas para confirmar su cuenta. \ No newline at end of file +PASSWORD_RESET_TIMEOUT = 86400 #El usuario cuenta con 24 horas para confirmar su cuenta. + +DATA_UPLOAD_MAX_NUMBER_FIELDS = 9000 \ No newline at end of file diff --git a/becas_cozcyt/urls.py b/becas_cozcyt/urls.py index 871add6094b938e54654eec0a721596ecfc866a6..22ba03f5d82f1e4ebb933118e136687259f19db6 100644 --- a/becas_cozcyt/urls.py +++ b/becas_cozcyt/urls.py @@ -23,9 +23,9 @@ urlpatterns = [ path('',include('usuarios.urls')), path('',include('modalidades.urls')), path('',include('solicitudes.urls')), - path('',include('estudio_socio_economico.urls')), + path('',include('estudio_socio_economico.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -#if settings.DEBUG: -# urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file +if False: # if settings.DEBUG:# + urlpatterns += path("__debug__/", include("debug_toolbar.urls")), \ No newline at end of file diff --git a/estudioSE.sql b/estudioSE.sql new file mode 100644 index 0000000000000000000000000000000000000000..6c9ac19f6e7c92b9b23c45694e6b29c3d243f673 --- /dev/null +++ b/estudioSE.sql @@ -0,0 +1,57 @@ +-- MySQL dump 10.13 Distrib 8.1.0, for Win64 (x86_64) +-- +-- Host: localhost Database: db-becas +-- ------------------------------------------------------ +-- Server version 5.5.5-10.11.2-MariaDB-1:10.11.2+maria~ubu2204 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Dumping data for table `estudio_socio_economico_elemento` +-- + +LOCK TABLES `estudio_socio_economico_elemento` WRITE; +/*!40000 ALTER TABLE `estudio_socio_economico_elemento` DISABLE KEYS */; +INSERT INTO `estudio_socio_economico_elemento` VALUES (58,'Ocupación',1,1,'texto_corto',43,2,1,10,0),(59,'Teléfono de trabajo',0,2,'numerico',43,2,1,10,10),(60,'Hora de entrada',0,4,'hora',43,2,1,10,0),(61,'Hora de salida',0,5,'hora',43,2,1,10,0),(62,'Sueldo mensual',0,3,'numerico',43,2,1,10,0),(63,'Actualmente Vives con:',1,1,'opcion_multiple',43,6,0,10,0),(64,'Años viviendo ahí',1,2,'numerico',43,6,1,10,0),(65,'Personas viviendo contigo',1,3,'numerico',43,6,1,10,0),(66,'Vivienda y transporte',1,1,'separador',43,4,1,10,0),(67,'La casa donde vives es:',1,1,'opcion_multiple',43,8,1,10,0),(68,'El material del piso es:',1,2,'opcion_multiple',43,8,1,10,0),(72,'¿Cuántas recámaras tiene?',1,3,'numerico',43,8,1,10,0),(73,'¿Cuántos baños tiene?',1,1,'numerico',43,10,1,10,0),(74,'¿Tiene sala?',1,2,'opcion_multiple',43,10,0,10,0),(75,'¿Tiene cocina independiente?',1,3,'opcion_multiple',43,10,0,10,0),(76,'¿Cuántos autos tiene?',1,4,'numerico',43,10,1,10,0),(77,'¿con que servicios Cuenta?',1,1,'casillas',43,12,0,10,0),(78,'En tu casa cuentas con:',1,1,'casillas',43,14,0,10,0),(79,'¿Cuentas con seguro de gastos Médicos?',1,1,'opcion_multiple',43,16,0,10,0),(80,'¿Qué transporte utilizas?',1,2,'opcion_multiple',43,16,1,10,0),(81,'Nombre completo',1,1,'texto_corto',44,2,1,10,0),(82,'Parentesco',1,2,'desplegable',44,2,1,10,0),(83,'Estado civil',1,3,'desplegable',44,2,0,10,0),(84,'Edad (años)',1,1,'numerico',44,4,1,3,0),(85,'sexo',1,2,'opcion_multiple',44,4,0,10,0),(86,'Escolaridad',1,3,'desplegable',44,4,0,10,0),(87,'¿Termino la carrera?',1,4,'opcion_multiple',44,4,0,10,0),(88,'¿Percibe algún ingreso?',1,1,'opcion_multiple',44,6,0,10,0),(89,'Ocupación',0,2,'desplegable',44,6,0,10,0),(90,'Lugar de Trabajo',0,3,'texto_corto',44,6,1,10,0),(91,'Ingreso mensual',0,4,'numerico',44,6,1,10,0); +/*!40000 ALTER TABLE `estudio_socio_economico_elemento` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `estudio_socio_economico_opcion` +-- + +LOCK TABLES `estudio_socio_economico_opcion` WRITE; +/*!40000 ALTER TABLE `estudio_socio_economico_opcion` DISABLE KEYS */; +INSERT INTO `estudio_socio_economico_opcion` VALUES (1,'Otro',Null,1),(26,'Padres',63,2),(27,'Amigos',63,4),(28,'Familiares',63,3),(29,'Esposo(a)',63,5),(30,'Propia',67,100000),(31,'Rentada',67,100000),(32,'Casa de huéspedes',67,100000),(33,'Tierra',68,100000),(34,'Alfombra',68,100000),(35,'Madera',68,100000),(36,'Duela',68,100000),(37,'Cemento',68,100000),(39,'Mosaico',68,100000),(48,'Si',74,100000),(49,'No',74,100000),(50,'Si',75,100000),(51,'No',75,100000),(52,'Agua',77,100000),(53,'Luz',77,100000),(54,'Drenaje',77,100000),(55,'Pavimiento',77,100000),(56,'Télefono',77,100000),(57,'Gas',77,100000),(58,'TV por cable',77,100000),(59,'Internet',77,100000),(60,'DVD',78,100000),(61,'Televisión',78,100000),(62,'Estufa',78,100000),(63,'Licuadora',78,100000),(64,'Lavadora',78,100000),(65,'Estéreo',78,100000),(66,'Microondas',78,100000),(67,'Computadora',78,100000),(68,'Si',79,100000),(69,'No',79,100000),(70,'Auto propio',80,100000),(71,'Auto familiar',80,100000),(72,'Motocicleta',80,100000),(73,'Camión',80,100000),(74,'Taxi',80,100000),(75,'Caminando',80,100000),(76,'Padre',82,100000),(77,'Madre',82,100000),(78,'Hermano(a)',82,100000),(79,'Hijo(a)',82,100000),(80,'Abuelo(a)',82,100000),(81,'Tío(a)',82,100000),(82,'Tutor(a)',82,100000),(83,'Esposo(a)',82,100000),(84,'Soltero(a)',83,100000),(85,'Casado(a)',83,100000),(86,'Divorciado(a)',83,100000),(87,'Viudo(a)',83,100000),(88,'Hombre',85,100000),(89,'Mujer',85,100000),(90,'Ninguno',86,100000),(91,'Primaria',86,100000),(92,'Secuandaria',86,100000),(93,'Preparatoria',86,100000),(94,'Carrera técnica',86,100000),(95,'Licenciatura',86,100000),(96,'Maestria',86,100000),(97,'Posgrado',86,100000),(98,'Si',87,100000),(99,'No',87,100000),(100,'Si',88,100000),(101,'No',88,100000),(102,'Estudiante',89,100000),(103,'Hogar',89,100000),(104,'Comerciante',89,100000),(105,'Jubilado / Pensionado',89,100000),(106,'Obrero',89,100000),(107,'Técnico',89,100000),(108,'Profesionista',89,100000),(109,'Empleado',89,100000); +/*!40000 ALTER TABLE `estudio_socio_economico_opcion` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `estudio_socio_economico_seccion` +-- + +LOCK TABLES `estudio_socio_economico_seccion` WRITE; +/*!40000 ALTER TABLE `estudio_socio_economico_seccion` DISABLE KEYS */; +INSERT INTO `estudio_socio_economico_seccion` VALUES (43,'Datos Socioeconómicos','unico',2),(44,'Datos familiares (Deben ser todos con los que vives)','agregacion',100000); +/*!40000 ALTER TABLE `estudio_socio_economico_seccion` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2023-08-27 20:34:27 diff --git a/estudio_socio_economico/admin.py b/estudio_socio_economico/admin.py index f5d3b9a4f9b68cbac665ccc9968cfe3174d21546..d6e12f78bfca2de0e1dbd8790cf3a65e822d97e1 100644 --- a/estudio_socio_economico/admin.py +++ b/estudio_socio_economico/admin.py @@ -1,16 +1,22 @@ from django.contrib import admin -from .models import Seccion, Opcion, Respuesta, RTextoCorto, RTextoParrafo, ROpcionMultiple, RCasillas, RDesplegable, Elemento +from .models import Seccion, Opcion, RAgregacion, 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(RAgregacion) admin.site.register(Respuesta) admin.site.register(RTextoCorto) 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 b1d1d47d88f567cb135a15d7a3712cd8ac5f3699..bcb4ffbfa4018799b8a33fb50a0e3c5359ac90b1 100644 --- a/estudio_socio_economico/forms.py +++ b/estudio_socio_economico/forms.py @@ -1,7 +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, Respuesta +from django.core.validators import MinLengthValidator, MaxLengthValidator class SeccionForm(forms.ModelForm): @@ -10,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 = { @@ -27,7 +28,7 @@ class OpcionForm(forms.ModelForm): fields = ['elemento', 'nombre', 'orden'] widgets = { 'elemento': forms.Select(attrs={'class': 'form-control'}), - 'nombre': forms.TextInput(attrs={'class': 'opcion-text form-control border-3 p-0 m-0', 'onkeyup': 'updateOpcionInputSize(this)', 'onkeydown': 'updateOpcionInputSize(this)'}), + 'nombre': forms.TextInput(attrs={'class': 'opcion-text form-control border-3 p-0 m-0', 'onkeyup': 'updateOpcionInputSize(this)', 'onkeydown': 'updateOpcionInputSize(this)', 'oninput': 'detectarTerminarEscritura(this)'}), 'orden': forms.NumberInput(attrs={'class': 'form-control', 'type': 'hidden'}), } labels = { @@ -41,12 +42,14 @@ class OpcionForm(forms.ModelForm): class ElementoForm(forms.ModelForm): class Meta: model = Elemento - fields = ['seccion', 'nombre', 'obligatorio', 'opcionOtro', 'row', 'col', 'tipo',] + fields = ['seccion', 'nombre', 'obligatorio', 'opcionOtro', 'numMin', 'numMax', 'row', 'col', 'tipo',] widgets = { 'seccion': forms.Select(attrs={'class': 'form-control form-control-sm border-3', 'placeholder': 'Selecciona una sección'}), 'nombre': forms.TextInput(attrs={'class': 'form-control form-control-sm font-semi-bold border-3 ', 'placeholder': 'Nombre de la pregunta'}), 'obligatorio': forms.CheckboxInput(attrs={'class': 'checkbox-principal form-check-input'}), 'opcionOtro': forms.CheckboxInput(attrs={'class': 'checkbox-principal form-check-input', 'title': 'Habilitar Opción Otro', 'data-bs-original-title': 'Habilitar Opción Otro'}), + 'numMin': forms.NumberInput(attrs={'class': 'opcion-digit form-control form-control-sm border-3' }), + 'numMax': forms.NumberInput(attrs={'class': 'opcion-digit form-control form-control-sm border-3' }), 'row': forms.NumberInput(attrs={'class': 'form-control form-control-sm border-3', 'type': 'hidden'}), 'col': forms.NumberInput(attrs={'class': 'form-control form-control-sm border-3', 'type': 'hidden'}), 'tipo': forms.Select(attrs={'class': 'form-select form-select-sm border-3 elem-tipo-select', 'onchange': 'toggleElementoOpciones(this)', 'onload': 'toggleElementoOpciones(this)', 'placeholder': 'Tipo de pregunta'}), @@ -56,6 +59,8 @@ class ElementoForm(forms.ModelForm): 'nombre': 'Nombre de la Pregunta', 'obligatorio': 'Obligatorio', 'opcionOtro': 'Opc. Otro', + 'numMin': 'Min. Dígitos', + 'numMax': 'Max. Dígitos', 'row': 'row', 'col': 'col', 'tipo': 'Tipo de Pregunta', @@ -65,3 +70,220 @@ SeccionFormSet = modelformset_factory(Seccion, form=SeccionForm, extra=1, can_de ElementoFormSet = inlineformset_factory(Seccion, Elemento, form=ElementoForm, extra=1, can_delete=True) 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", None) + solicitante = kwargs.pop("solicitante", None) + super().__init__(*args, **kwargs) + 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 + + '''# Recorre los campos del formulario + for field_name, field in self.fields.items(): + # Verifica si el campo es una ManyToManyField o ForeignKey + if isinstance(field, forms.ModelMultipleChoiceField) or isinstance(field, forms.ModelChoiceField): + # Establece las opciones como una lista vacía + field.choices = [] #''' + + + """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 = ['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: + model = RTextoCorto + fields = ['texto'] + widgets = {'texto': forms.TextInput(attrs={'class': 'form-control border-3'}),} + 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 + fields = ['texto'] + widgets = {'texto': forms.Textarea(attrs={'class': 'form-control border-3'}),} + 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 border-3', '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(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.RadioSelect(attrs={'class': 'form-check-input checkbox-principal', 'onchange': 'toggleOtroCampo(this)'}), + 'otro': forms.TextInput(attrs={'class': 'form-control border-3', 'placeholder': 'Otro'}),} + 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-input checkbox-principal form-check-inline-custom', 'style': 'display: inline-block;', 'onchange': 'toggleOtroCampo(this)'}), + 'otro': forms.TextInput(attrs={'class': 'form-control border-3', 'placeholder': 'Otro'}),} + 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 form-select border-3', 'onchange': 'toggleOtroCampo(this)'}), + 'otro': forms.TextInput(attrs={'class': 'form-control border-3', 'placeholder': 'Otro'}),} + 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 8285d4fd24dda04c767990e53e642237356c5ac6..417226774b10d4c617bafc89b3aeb2ba23483fe3 100644 --- a/estudio_socio_economico/models.py +++ b/estudio_socio_economico/models.py @@ -1,4 +1,4 @@ -from django.db import models +from django.db import models, IntegrityError from django.contrib.auth.models import User from django.core.validators import RegexValidator from django.core.validators import MinLengthValidator, MaxLengthValidator @@ -6,11 +6,12 @@ from django.db import models from model_utils.managers import InheritanceManager from usuarios.models import Solicitante import uuid +from django.core.exceptions import ValidationError class Seccion(models.Model): TIPOS_CHOICES = ( - ('único', 'Único'), - ('agregación', 'Agregación'), + ('unico', 'Único'), + ('agregacion', 'Agregación'), ) nombre = models.CharField(max_length=255) @@ -25,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) @@ -40,7 +41,25 @@ class Opcion(models.Model): #----------Respuestas------------- +class CustomIntegerField(models.IntegerField): + def __init__(self, *args, min_digits=None, max_digits=None, **kwargs): + self.min_digits = min_digits + self.max_digits = max_digits + super().__init__(*args, **kwargs) + + def validate(self, value, model_instance): + super().validate(value, model_instance) + if self.min_digits is not None and len(str(value)) < self.min_digits: + raise ValidationError(f"El valor debe tener almenos {self.min_digits} digitos.") + if self.max_digits is not None and len(str(value)) > self.max_digits: + raise ValidationError(f"El valor debe tener a lo mucho {self.max_digits} digitos.") + +class RAgregacion(models.Model): + # El modelo solo contiene el campo de identificación (ID) de forma predeterminada + pass + class Respuesta(models.Model): + rAgregacion = models.ForeignKey(RAgregacion, on_delete=models.CASCADE, null=True, blank=True) elemento = models.ForeignKey('Elemento', on_delete=models.CASCADE) solicitante = models.ForeignKey(Solicitante, on_delete=models.CASCADE) otro = None @@ -48,46 +67,108 @@ class Respuesta(models.Model): objects = InheritanceManager() def __str__(self): - return f"Respuesta - Elemento: {self.elemento} - Solicitante: {self.solicitante}" + return f"Respuesta {type(self)} - Elemento: {self.elemento} - Solicitante: {self.solicitante_id}" + + def save(self, *args, **kwargs): + if self._state.adding: + # Solo realiza la verificación si estás creando una respuesta nueva + if self.elemento.seccion.tipo == 'unico': + # Verificar si ya existe una respuesta para esta combinación + if Respuesta.objects.filter(elemento=self.elemento, solicitante=self.solicitante).exists(): + raise IntegrityError('Ya existe una respuesta para este elemento y solicitante') + super().save(*args, **kwargs) - class Meta: + class Meta: verbose_name = 'Respuesta' - verbose_name_plural = 'Respuestas' - unique_together = ['elemento', 'solicitante'] + verbose_name_plural = 'Respuestas' + + def getStringValue(self): + return 'Respuesta no Implementado' + + +class RNumerico(Respuesta): + valor = models.CharField(max_length=255, null=True, blank=True) + + def getStringValue(self): + return str(self.valor) class RTextoCorto(Respuesta): - texto = models.CharField(max_length=255) + texto = models.CharField(max_length=255, null=True, blank=True) + + def getStringValue(self): + return str(self.texto) class RTextoParrafo(Respuesta): - texto = models.TextField() + texto = models.TextField(null=True, blank=True) + def getStringValue(self): + return str(self.texto) -class ROpcionMultiple(Respuesta): - respuesta = models.ForeignKey(Opcion, on_delete=models.CASCADE) - otro = models.CharField(max_length=255, verbose_name="Otro", null=True, blank=False) +class RHora(Respuesta): + hora = models.TimeField(null=True, blank=True) -class RCasillas(Respuesta): - respuestas = models.ManyToManyField(Opcion) - otro = models.CharField(max_length=255, verbose_name="Otro", null=True, blank=False) + def getStringValue(self): + return str(self.hora) -class RDesplegable(Respuesta): - respuesta = models.ForeignKey(Opcion, on_delete=models.CASCADE) - otro = models.CharField(max_length=255, verbose_name="Otro", null=True, blank=False) +class RFecha(Respuesta): + fecha = models.DateField(null=True, blank=True) + def getStringValue(self): + return str(self.fecha) + +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=True) + def getStringValue(self): + if self.respuesta and self.respuesta.nombre == 'Otro': + return str(self.respuesta)+': '+str(self.otro) + else : + return str(self.respuesta) + +class RCasillas(Respuesta): + respuesta = models.ManyToManyField(Opcion, blank=True) + otro = models.CharField(max_length=255, verbose_name="Otro", null=True, blank=True) + + def getStringValue(self): + string = '' + objs = self.respuesta.all() + for i, obj in enumerate(objs): + string += str(obj) + if obj.nombre == 'Otro': + string += ': '+ str(self.otro) + if i < len(objs) - 1: + string += ', ' + + return string + + +class RDesplegable(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=True) + + def getStringValue(self): + if self.respuesta and self.respuesta.nombre == 'Otro': + return str(self.respuesta)+': '+str(self.otro) + else : + return str(self.respuesta) + #---------Elementos----------- class Elemento(models.Model): TIPO_CHOICES = ( ('separador', 'Separador'), + ('numerico', 'Numérico'), ('texto_corto', 'Texto Corto'), ('texto_parrafo', 'Texto Párrafo'), + ('hora', 'Hora'), + ('fecha', 'Fecha'), ('opcion_multiple', 'Opción Múltiple'), ('casillas', 'Casillas'), ('desplegable', 'Desplegable'), @@ -97,6 +178,8 @@ class Elemento(models.Model): nombre = models.CharField(max_length=255, verbose_name='Nombre') obligatorio = models.BooleanField(default=True, verbose_name='Obligatorio') opcionOtro = models.BooleanField(default=True, verbose_name='Opcion Otro') + numMin = models.PositiveIntegerField(verbose_name='numMin', default=0) + numMax = models.PositiveIntegerField(verbose_name='numMax', default=10) row = models.IntegerField(verbose_name='row', default=100_000) col = models.IntegerField(verbose_name='col', default=100_000) tipo = models.CharField(max_length=20, choices=TIPO_CHOICES, verbose_name='Tipo') @@ -115,9 +198,22 @@ class Elemento(models.Model): return RCasillas elif self.tipo == 'desplegable': return RDesplegable + elif self.tipo == 'numerico': + return RNumerico + elif self.tipo == 'hora': + return RHora + elif self.tipo == 'fecha': + return RFecha else: return None + def crearRespuesta(self, solicitante): + respuesta_model = self.getRespuestaModel() + if respuesta_model: + return respuesta_model.objects.create(elemento=self, solicitante=solicitante) + else : + return None + class Meta: verbose_name = 'Elemento' verbose_name_plural = 'Elementos' diff --git a/estudio_socio_economico/templates/admin/config_estudioSE.html b/estudio_socio_economico/templates/admin/config_estudioSE.html index a8c1fbecd8b85c75a2f6dcb13b4b330972b362bf..536527a5cf42cc57ea41df4c26bbbc3130ca4f30 100644 --- a/estudio_socio_economico/templates/admin/config_estudioSE.html +++ b/estudio_socio_economico/templates/admin/config_estudioSE.html @@ -6,831 +6,56 @@ {% block body %} {% load custom_filters %} - + {% include 'admin/config_nav.html' %}
Modificar Estudio Socioeconomico
-