From a573f214b4ac363d3de381a803dacb16cfe56b4e Mon Sep 17 00:00:00 2001 From: RafaUC Date: Fri, 16 Aug 2024 15:05:09 -0600 Subject: [PATCH] Implementada y optimizada la funcion get de RespuestaForulario de formularios dinamicos --- cosiap_api/common/utils.py | 12 +- cosiap_api/cosiap_api/.env | 5 +- cosiap_api/cosiap_api/settings.py | 64 ++++ cosiap_api/dynamic_forms/admin.py | 4 +- .../migrations/0008_update_RDocumento.py | 23 ++ ...reign_key_and_added_FormularioRespuesta.py | 38 ++ ...reign_key_and_added_FormularioRespuesta.py | 34 ++ .../migrations/0011_refactor_Registros.py | 27 ++ cosiap_api/dynamic_forms/models.py | 76 ++-- cosiap_api/dynamic_forms/serializers.py | 234 +++++++++++-- cosiap_api/dynamic_forms/tests.py | 331 ++++++++++++++---- cosiap_api/dynamic_forms/urls.py | 6 +- cosiap_api/dynamic_forms/views.py | 87 +++-- ...reign_key_and_added_FormularioRespuesta.py | 20 ++ .../migrations/0005_refactor_Registros.py | 25 ++ ...006_sanche_solicitud_pk_and_solicitud_n.py | 25 ++ .../migrations/0007_regenerate_solicitudes.py | 57 +++ .../migrations/0008_regenerate_solicitudes.py | 76 ++++ cosiap_api/solicitudes/models.py | 8 +- ....timestamp-1723477813488-b5222855a1ef9.mjs | 20 ++ 20 files changed, 1022 insertions(+), 150 deletions(-) create mode 100644 cosiap_api/dynamic_forms/migrations/0008_update_RDocumento.py create mode 100644 cosiap_api/dynamic_forms/migrations/0009_remove_generic_foreign_key_and_added_FormularioRespuesta.py create mode 100644 cosiap_api/dynamic_forms/migrations/0010_remove_generic_foreign_key_and_added_FormularioRespuesta.py create mode 100644 cosiap_api/dynamic_forms/migrations/0011_refactor_Registros.py create mode 100644 cosiap_api/solicitudes/migrations/0004_remove_generic_foreign_key_and_added_FormularioRespuesta.py create mode 100644 cosiap_api/solicitudes/migrations/0005_refactor_Registros.py create mode 100644 cosiap_api/solicitudes/migrations/0006_sanche_solicitud_pk_and_solicitud_n.py create mode 100644 cosiap_api/solicitudes/migrations/0007_regenerate_solicitudes.py create mode 100644 cosiap_api/solicitudes/migrations/0008_regenerate_solicitudes.py create mode 100644 cosiap_frontend/vite.config.js.timestamp-1723477813488-b5222855a1ef9.mjs diff --git a/cosiap_api/common/utils.py b/cosiap_api/common/utils.py index 37ce590..52530ff 100644 --- a/cosiap_api/common/utils.py +++ b/cosiap_api/common/utils.py @@ -1,5 +1,13 @@ +import sqlparse import json -def printDict(diccionario): +def print_dict(diccionario): print(json.dumps(diccionario, indent=2, separators=(",", ": "), ensure_ascii=False)) - pass \ No newline at end of file + pass + +def print_captured_queries(captured_queries): + for i, query in enumerate(captured_queries, start=1): + print(f'QUERY {i}:') + formatted_sql = sqlparse.format(query["sql"], reindent=True, keyword_case='upper') + print(formatted_sql) + print(f'TIME: {query["time"]}s\n') \ No newline at end of file diff --git a/cosiap_api/cosiap_api/.env b/cosiap_api/cosiap_api/.env index b225e67..1b8c473 100644 --- a/cosiap_api/cosiap_api/.env +++ b/cosiap_api/cosiap_api/.env @@ -1,4 +1,7 @@ DEBUG=True +DEBUG_SQL=False +DEBUG_SQL_TRACEBACK=False + SECRET_KEY="django-insecure-*8(5essozyzi_b&fz((8j5xxpjra!$_jj$=ic3tw&x^sa8" BASE_URL="http://localhost:8000/" @@ -18,4 +21,4 @@ EMAIL_USE_TLS=True CORS_ALLOWED_ORIGINS="http://localhost:5173" -CHANNEL_LAYERS_CONFIG_HOSTS="[('redis-cosiap', 6379)]" \ No newline at end of file +CHANNEL_LAYERS_CONFIG_HOSTS="[('redis-cosiap', 6379)]" diff --git a/cosiap_api/cosiap_api/settings.py b/cosiap_api/cosiap_api/settings.py index 969e309..6776ca3 100644 --- a/cosiap_api/cosiap_api/settings.py +++ b/cosiap_api/cosiap_api/settings.py @@ -223,3 +223,67 @@ SPECTACULAR_SETTINGS = { CORS_ALLOW_CREDENTIALS = True CORS_ALLOWED_ORIGINS = [origin.strip() for origin in env("CORS_ALLOWED_ORIGINS").split(",")] + + +#debugging queries +DEBUG_SQL = env.bool('DEBUG_SQL', default=False) +DEBUG_SQL_TRACEBACK = env.bool('DEBUG_SQL_TRACEBACK', default=False) + +if DEBUG_SQL or DEBUG_SQL_TRACEBACK: + + LOGGING = { + 'version': 1, # Especifica la versión del formato del diccionario + 'handlers': { + 'console': { # Define el manejador de consola + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'simple', + }, + }, + 'formatters': { + 'simple': { # Define un formateador simple + 'format': '%(levelname)s %(message)s' + }, + }, + 'loggers': {} + } + LOGGING['loggers']['django.db.backends'] = { + 'level': 'DEBUG', + 'handlers': ['console'], + 'propagate': False, + } + +if DEBUG_SQL_TRACEBACK: + import traceback + import logging + import django.db.backends.utils as bakutils + import sqlparse + + logger = logging.getLogger('django.db.backends') + cursor_debug_wrapper_orig = bakutils.CursorDebugWrapper + + def print_stack_in_project(sql): + stack = traceback.extract_stack() + for path, lineno, func, line in stack: + if 'lib/python' in path or 'settings.py' in path: + continue + logger.debug(f'TB File "{path}", line {lineno}, in {func}') + logger.debug(f'TB {line}') + formatted_sql = sqlparse.format(sql, reindent=True, keyword_case='upper') + logger.debug(F'SQL ######\n{formatted_sql}\n') + + class CursorDebugWrapperLoud(cursor_debug_wrapper_orig): + def execute(self, sql, params=None): + try: + return super().execute(sql, params) + finally: + sql = self.db.ops.last_executed_query(self.cursor, sql, params) + print_stack_in_project(sql) + + def executemany(self, sql, param_list): + try: + return super().executemany(sql, param_list) + finally: + print_stack_in_project(sql) + + bakutils.CursorDebugWrapper = CursorDebugWrapperLoud \ No newline at end of file diff --git a/cosiap_api/dynamic_forms/admin.py b/cosiap_api/dynamic_forms/admin.py index 715f11a..a3ce620 100644 --- a/cosiap_api/dynamic_forms/admin.py +++ b/cosiap_api/dynamic_forms/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from dynamic_forms.models import DynamicForm, DynamicFormsSecciones, \ Seccion, SeccionesElementos, Elemento, ElementosOpciones, Opcion, \ - Registro, Respuesta, RTextoCorto, RTextoParrafo, RNumerico, \ + RegistroSeccion, Respuesta, RTextoCorto, RTextoParrafo, RNumerico, \ RHora, RFecha, ROpcionMultiple, RDesplegable, RCasillas, RDocumento # Register your models here. @@ -12,7 +12,7 @@ admin.site.register(SeccionesElementos) admin.site.register(Elemento) admin.site.register(ElementosOpciones) admin.site.register(Opcion) -admin.site.register(Registro) +admin.site.register(RegistroSeccion) admin.site.register(Respuesta) admin.site.register(RTextoCorto) admin.site.register(RTextoParrafo) diff --git a/cosiap_api/dynamic_forms/migrations/0008_update_RDocumento.py b/cosiap_api/dynamic_forms/migrations/0008_update_RDocumento.py new file mode 100644 index 0000000..298e631 --- /dev/null +++ b/cosiap_api/dynamic_forms/migrations/0008_update_RDocumento.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.7 on 2024-08-12 20:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamic_forms', '0007_crear_modelo_registro'), + ] + + operations = [ + migrations.AddField( + model_name='rdocumento', + name='observacion', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Observacion'), + ), + migrations.AddField( + model_name='rdocumento', + name='status', + field=models.CharField(choices=[('revisando', 'En revisión'), ('valido', 'Válido'), ('invalido', 'Inválido')], default='revisando', max_length=255, verbose_name='Status'), + ), + ] diff --git a/cosiap_api/dynamic_forms/migrations/0009_remove_generic_foreign_key_and_added_FormularioRespuesta.py b/cosiap_api/dynamic_forms/migrations/0009_remove_generic_foreign_key_and_added_FormularioRespuesta.py new file mode 100644 index 0000000..cb9a996 --- /dev/null +++ b/cosiap_api/dynamic_forms/migrations/0009_remove_generic_foreign_key_and_added_FormularioRespuesta.py @@ -0,0 +1,38 @@ +# Generated by Django 5.0.7 on 2024-08-14 17:46 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamic_forms', '0008_update_RDocumento'), + ] + + operations = [ + migrations.CreateModel( + name='FormularioRespuesta', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.RemoveIndex( + model_name='registro', + name='dynamic_for_owner_c_70b1a6_idx', + ), + migrations.RemoveField( + model_name='registro', + name='owner_content_type', + ), + migrations.RemoveField( + model_name='registro', + name='owner_id', + ), + migrations.AddField( + model_name='registro', + name='formulario_respuesta', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='dynamic_forms.formulariorespuesta'), + preserve_default=False, + ), + ] diff --git a/cosiap_api/dynamic_forms/migrations/0010_remove_generic_foreign_key_and_added_FormularioRespuesta.py b/cosiap_api/dynamic_forms/migrations/0010_remove_generic_foreign_key_and_added_FormularioRespuesta.py new file mode 100644 index 0000000..8628216 --- /dev/null +++ b/cosiap_api/dynamic_forms/migrations/0010_remove_generic_foreign_key_and_added_FormularioRespuesta.py @@ -0,0 +1,34 @@ +# Generated by Django 5.0.7 on 2024-08-14 19:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamic_forms', '0009_remove_generic_foreign_key_and_added_FormularioRespuesta'), + ('solicitudes', '0003_merge_migrations'), + ] + + operations = [ + migrations.RenameModel( + old_name='FormularioRespuesta', + new_name='RegistroFormulario', + ), + migrations.RenameModel( + old_name='Registro', + new_name='RegistroSeccion', + ), + migrations.AlterModelOptions( + name='dynamicformssecciones', + options={'ordering': ['orden'], 'verbose_name_plural': '02. Rel ormulario - Secciones'}, + ), + migrations.AlterModelOptions( + name='elementosopciones', + options={'ordering': ['orden'], 'verbose_name_plural': '06. Rel Elemento - Opciones'}, + ), + migrations.AlterModelOptions( + name='seccioneselementos', + options={'ordering': ['orden'], 'verbose_name_plural': '04. Rel Secciones - Elementos'}, + ), + ] diff --git a/cosiap_api/dynamic_forms/migrations/0011_refactor_Registros.py b/cosiap_api/dynamic_forms/migrations/0011_refactor_Registros.py new file mode 100644 index 0000000..2367347 --- /dev/null +++ b/cosiap_api/dynamic_forms/migrations/0011_refactor_Registros.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.7 on 2024-08-14 20:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamic_forms', '0010_remove_generic_foreign_key_and_added_FormularioRespuesta'), + ] + + operations = [ + migrations.RenameField( + model_name='registroseccion', + old_name='formulario_respuesta', + new_name='registro_formulario', + ), + migrations.RenameField( + model_name='respuesta', + old_name='registro', + new_name='registro_seccion', + ), + migrations.AlterUniqueTogether( + name='respuesta', + unique_together={('registro_seccion', 'elemento')}, + ), + ] diff --git a/cosiap_api/dynamic_forms/models.py b/cosiap_api/dynamic_forms/models.py index 1609460..81abdf7 100644 --- a/cosiap_api/dynamic_forms/models.py +++ b/cosiap_api/dynamic_forms/models.py @@ -1,5 +1,4 @@ from django.db import models -from solicitudes.models import Solicitud from model_utils.managers import InheritanceManager from django.db import IntegrityError from dynamic_formats.models import DynamicFormat @@ -72,6 +71,7 @@ class ElementosOpciones(models.Model): opcion = models.ForeignKey(Opcion, on_delete=models.CASCADE, verbose_name='Opción') orden = models.IntegerField(verbose_name='Orden', default=0) class Meta: + ordering = ['orden'] unique_together = ('elemento', 'opcion') verbose_name_plural = '06. Rel Elemento - Opciones' @@ -108,6 +108,7 @@ class SeccionesElementos(models.Model): elemento = models.ForeignKey(Elemento, on_delete=models.CASCADE, verbose_name='Elemento') orden = models.IntegerField(verbose_name='Orden', default=0) class Meta: + ordering = ['orden'] unique_together = ('seccion', 'elemento') verbose_name_plural = '04. Rel Secciones - Elementos' @@ -137,26 +138,25 @@ class DynamicFormsSecciones(models.Model): seccion = models.ForeignKey(Seccion, on_delete=models.CASCADE, verbose_name='Sección') orden = models.IntegerField(verbose_name='Orden', default=0) class Meta: + ordering = ['orden'] unique_together = ('dynamic_form', 'seccion') verbose_name_plural = '02. Rel ormulario - Secciones' +class RegistroFormulario(models.Model): + #Formulario que se encarga de agrupar todos los registros de un Formulario + pass -class Registro(models.Model): +class RegistroSeccion(models.Model): ''' Modelo que define el id de una lista de respuestas de una seccion Campos: -id (id) has ''' - owner_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - owner_id = models.PositiveIntegerField() - owner = GenericForeignKey('owner_content_type', 'owner_id') - seccion = models.ForeignKey(Seccion, on_delete=models.CASCADE, null=False, blank=False) + registro_formulario = models.ForeignKey(RegistroFormulario, on_delete=models.CASCADE) + seccion = models.ForeignKey(Seccion, on_delete=models.CASCADE, null=False, blank=False) class Meta: - verbose_name_plural = '08. Rel Respuestas Agrecacion' - indexes = [ - models.Index(fields=['owner_content_type', 'owner_id']), - ] + verbose_name_plural = '08. Rel Respuestas Agrecacion' class Respuesta(models.Model): ''' @@ -167,8 +167,10 @@ class Respuesta(models.Model): - solicitud (Solicitud, solicitud a la que pertenece la respuesta.) - elemento (Elemento, elemento al que pertenece la respuesta.) - otro (None, Campo donde se almacena la opcion otro, no definido en esta clase) - ''' - registro = models.ForeignKey(Registro, on_delete=models.CASCADE, null=False, blank=False) + ''' + RESPUESTA_TYPES = None + + registro_seccion = models.ForeignKey(RegistroSeccion, on_delete=models.CASCADE, null=False, blank=False) elemento = models.ForeignKey(Elemento, on_delete=models.CASCADE, null=False, blank=False) valor = None otro = None @@ -178,9 +180,9 @@ class Respuesta(models.Model): class Meta: verbose_name = 'Respuesta' verbose_name_plural = '09. Respuestas' - unique_together = ('registro', 'elemento') + unique_together = ('registro_seccion', 'elemento') - + ''' def __str__(self): return f"Respuesta {type(self)} - Elemento: {self.elemento} - Solicitante: {self.solicitud.solicitante_id}" @@ -190,7 +192,7 @@ class Respuesta(models.Model): if Respuesta.objects.filter(elemento=self.elemento, solicitud=self.solicitud).exists(): raise IntegrityError('Ya existe una respuesta para este elemento y solicitud') super().save(*args, **kwargs) - + ''' def getStringValue(self): return 'Respuesta no Implementado' @@ -206,23 +208,23 @@ class RNumerico(Respuesta): else : return str(self.valor) class Meta: - verbose_name_plural = '10. R Numericos' + verbose_name_plural = '10. R Numericos' - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # Accede a la instancia de elemento y obtén los valores numMin y numMax + def get_validators(self): + validators = [] elemento = self.elemento if self.pk 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._meta.get_field('valor').validators.append(MinLengthValidator(numMin)) - if numMax is not None: - self._meta.get_field('valor').validators.append(MaxLengthValidator(numMax)) - + if elemento: + if elemento.min_digits is not None: + validators.append(MinLengthValidator(elemento.min_digits)) + if elemento.max_digits is not None: + validators.append(MaxLengthValidator(elemento.max_digits)) + return validators def clean(self): super().clean() + for validator in self.get_validators(): + validator(self.valor) + obligatorio = self.elemento.obligatorio if (not self.valor or not self.valor.strip()) and obligatorio: raise ValidationError("Este campo es Obligatorio.") @@ -400,7 +402,14 @@ class RDesplegable(Respuesta): raise ValidationError("Si eliges 'otro', debes proporcionar más detalles en el campo 'otro'.") class RDocumento(Respuesta): + class Status(models.TextChoices): + REVISANDO = 'revisando', 'En revisión' + VALIDO = 'valido', 'Válido' + INVALIDO = 'invalido', 'Inválido' + valor = models.FileField(verbose_name='Subir Documento', upload_to=nombre_archivo_respuesta_doc , null=True, blank=True) + status = models.CharField(verbose_name='Status', max_length=255, choices=Status.choices, default=Status.REVISANDO) + observacion = models.CharField(verbose_name='Observacion', max_length=255, null=True, blank=True) def getStringValue(self): return self.valor.name if self.valor else '-----' @@ -413,3 +422,16 @@ class RDocumento(Respuesta): obligatorio = self.elemento.obligatorio if (not respuesta) and obligatorio: raise ValidationError("Este campo es Obligatorio.") + + +Respuesta.RESPUESTA_TYPES = { + Elemento.Tipo.NUMERICO: RNumerico, + Elemento.Tipo.TEXTO_CORTO: RTextoCorto, + Elemento.Tipo.TEXT_PARRAFO: RTextoParrafo, + Elemento.Tipo.HORA: RHora, + Elemento.Tipo.FECHA: RFecha, + Elemento.Tipo.OPCION_MULTIPLE: ROpcionMultiple, + Elemento.Tipo.CASILLAS: RCasillas, + Elemento.Tipo.DESPLEGABLE: RDesplegable, + Elemento.Tipo.DOCUMENTO: RDocumento, + } \ No newline at end of file diff --git a/cosiap_api/dynamic_forms/serializers.py b/cosiap_api/dynamic_forms/serializers.py index 65e56de..67c80d2 100644 --- a/cosiap_api/dynamic_forms/serializers.py +++ b/cosiap_api/dynamic_forms/serializers.py @@ -2,12 +2,14 @@ from rest_framework import serializers from django.db import models from dynamic_forms.models import ( Opcion, Elemento, ElementosOpciones, Seccion, SeccionesElementos, - DynamicForm, DynamicFormsSecciones, Registro, Respuesta, RNumerico, + DynamicForm, DynamicFormsSecciones, RegistroSeccion, Respuesta, RNumerico, RTextoCorto, RTextoParrafo, RHora, RFecha, ROpcionMultiple, RCasillas, - RDesplegable, RDocumento + RDesplegable, RDocumento, RegistroFormulario ) from django.core.validators import MinLengthValidator, MaxLengthValidator from django.contrib.contenttypes.models import ContentType +from django.db.models import Prefetch +from django.db.models import OneToOneField class BaseDynamicFormSerializer(serializers.ModelSerializer): def __str__(self): @@ -37,8 +39,9 @@ class ElementosOpcionesSerializer(BaseDynamicFormSerializer): def to_representation(self, instance): representation = super().to_representation(instance) - representation['opcion'] = OpcionSerializer(instance.opcion).data - return representation + combined_representation = OpcionSerializer(instance.opcion).data + combined_representation['orden'] = representation['orden'] + return combined_representation class ElementoSerializer(BaseDynamicFormSerializer): @@ -66,8 +69,9 @@ class SeccionesElementosSerializer(BaseDynamicFormSerializer): def to_representation(self, instance): representation = super().to_representation(instance) - representation['elemento'] = ElementoSerializer(instance.elemento).data - return representation + combined_representation = ElementoSerializer(instance.elemento).data + combined_representation['orden'] = representation['orden'] + return combined_representation # Serializer para Seccion class SeccionSerializer(BaseDynamicFormSerializer): @@ -94,8 +98,9 @@ class DynamicFormsSeccionesSerializer(BaseDynamicFormSerializer): def to_representation(self, instance): representation = super().to_representation(instance) - representation['seccion'] = SeccionSerializer(instance.seccion).data - return representation + combined_representation = SeccionSerializer(instance.seccion).data + combined_representation['orden'] = representation['orden'] + return combined_representation # Serializer para DynamicForm class DynamicFormSerializer(BaseDynamicFormSerializer): @@ -111,10 +116,22 @@ class DynamicFormSerializer(BaseDynamicFormSerializer): class DynamicModelSerializer(serializers.ModelSerializer): def __init__(self, *args, **kwargs): - # Expecting 'model' to be passed in kwargs - model = kwargs.pop('model_class', None) - if model: - self.Meta.model = model + # Obtener la instancia o queryset y el model_class + instance_or_queryset = args[0] if args else None + if isinstance(instance_or_queryset, models.QuerySet): + model_class = instance_or_queryset.model + elif instance_or_queryset is not None: + model_class = type(instance_or_queryset) + else: + # Si no hay ni queryset ni instancia, intentar obtener model_class de kwargs + model_class = kwargs.pop('model_class', None) + # Verificar si model_class está definido + if not model_class: + raise ValueError("El argumento 'model_class' es obligatorio y no fue proporcionado ni se puede obtener de la instancia.") + + # Establecer el modelo en Meta.model + self.Meta.model = model_class + # Llamar al constructor de la clase base super().__init__(*args, **kwargs) class Meta: @@ -134,23 +151,186 @@ RESPUESTA_MODELOS = { 'RDocumento': RDocumento, } -class RespuestaFormularioSerializer(DynamicModelSerializer): +class RespuestaFormularioSerializer(DynamicModelSerializer): + ''' + Argumentos: + - dynamic_form_source (lookup de donde se obtendra la referencia al formulario dinamico en base a la instancia) - def to_representation(self, instance): - # Obtiene el content_type del modelo proporcionado - content_type = ContentType.objects.get_for_model(self.model_class) + Maneja representaciones con la estructura: + Instancia o lista de diccionarios que representan un formulario: + data(formulario) = { + id: # + secciones: [ + { + id: # + ...atributos del elemento: # + elementos: [ + { + id: # + ...atributos del elemento: # + respuesta: { + id: # + registro_seccion (id): # + valor: # + } + } + ] + registros: [ #Solo si la seccion es de tipo "Lista" se generara este atributo con la losta de registros y sus repuestas + id: # (id registro_seccion) + respuesta: { + id: # + registro_seccion (id): # + valor: # + } + ] + } + ] + } + ''' + forms_prefetch_string = 'dynamicformssecciones_set__seccion__seccioneselementos_set__elemento__elementosopciones_set__opcion' + + def __init__(self, *args, **kwargs): + # Verificar si el argumento 'dynamic_form_source' está presente en kwargs + if 'dynamic_form_source' not in kwargs: + raise ValueError("El argumento 'dynamic_form_source' es obligatorio y no fue proporcionado.") + self.dynamic_form_source = kwargs.pop('dynamic_form_source') + # Llamar al constructor de la clase base + super().__init__(*args, **kwargs) + self.inicializar_cache() + + def inicializar_cache(self): + #generar el cache de la configuracion de los formularios que se uzaran, en base a la instancia + self.forms_cache = None + self.registros_respuestas_cache = None + #si la instancia es un queryset: + + one_to_one_field_name = None + owner_one_to_one_field_name = None + for field in self.Meta.model._meta.get_fields(): + if isinstance(field, OneToOneField) and field.related_model == RegistroFormulario: + one_to_one_field_name = getattr(field, 'related_name', None) + owner_one_to_one_field_name = getattr(field, 'name', None) + if not one_to_one_field_name: + one_to_one_field_name = self.Meta.model._meta.model_name + break + + if one_to_one_field_name is None: + raise ValueError("No se encontró un campo OneToOneField relacionado con RegistroFormulario") + if owner_one_to_one_field_name is None: + raise ValueError("No se pudo obtener el nombre de un campo OneToOneField relacionado con RegistroFormulario") + + self.one_to_one_field_name = one_to_one_field_name + self.owner_one_to_one_field_name = owner_one_to_one_field_name + + if isinstance(self.instance, models.QuerySet): + form_ids = self.instance.values_list(self.dynamic_form_source, flat=True) + form_filter_kwargs = {'pk__in':form_ids} + + owners = self.instance.values_list('pk', flat=True) + respuesta_filter_kwargs = {f"registro_seccion__registro_formulario__{one_to_one_field_name}__in": owners} + + + elif self.instance is not None: + form_id = self.get_attr_with_lookup(self.instance, self.dynamic_form_source).pk + form_filter_kwargs = {'pk':form_id} + + owner = self.instance.pk + respuesta_filter_kwargs = {f"registro_seccion__registro_formulario__{one_to_one_field_name}": owner} + + qs = DynamicForm.objects.filter(**form_filter_kwargs).prefetch_related(self.forms_prefetch_string) + self.forms_cache = {form.id: form for form in qs} + + # Prefetch para las respuestas + qs = Respuesta.objects.filter(**respuesta_filter_kwargs).exclude(elemento__tipo=Elemento.Tipo.CASILLAS).select_subclasses().select_related( + 'elemento', + 'registro_seccion', + 'registro_seccion__seccion', + 'registro_seccion__registro_formulario', + f'registro_seccion__registro_formulario__{self.one_to_one_field_name}' + ) + qs_casillas = Respuesta.objects.filter(**respuesta_filter_kwargs, elemento__tipo=Elemento.Tipo.CASILLAS).select_subclasses().select_related( + 'elemento', + 'registro_seccion', + 'registro_seccion__seccion', + 'registro_seccion__registro_formulario', + f'registro_seccion__registro_formulario__{self.one_to_one_field_name}' + ).prefetch_related('valor') + qs = list(qs) + list(qs_casillas) + rs_cache = {} + for respuesta in qs: + #(respuesta = INstancia de RNUmerico) + # Acceder a las claves necesarias y asignar los valores de los diccionarios + registro_seccion = respuesta.registro_seccion + registro_formulario = registro_seccion.registro_formulario + owner = getattr(registro_formulario, self.one_to_one_field_name, None) + seccion_tipo = registro_seccion.seccion.tipo + seccion_id = registro_seccion.seccion_id + elemento_id = respuesta.elemento_id + if owner.pk not in rs_cache: + rs_cache[owner.pk] = {'instance': registro_formulario} + if seccion_tipo == Seccion.Tipo.LISTA: + if seccion_id not in rs_cache[owner.pk]: + rs_cache[owner.pk][seccion_id] = {} + if registro_seccion.pk not in rs_cache[owner.pk][seccion_id]: + rs_cache[owner.pk][seccion_id][registro_seccion.pk] = {'instance': registro_seccion} + rs_cache[owner.pk][seccion_id][registro_seccion.pk][elemento_id] = respuesta + else: + if seccion_id not in rs_cache[owner.pk]: + rs_cache[owner.pk][seccion_id] = {'instance': registro_seccion} + rs_cache[owner.pk][seccion_id][elemento_id] = respuesta + self.registros_respuestas_cache = rs_cache + + @staticmethod + def get_attr_with_lookup(instance, lookup): + attr = instance + for field in lookup.split('__'): + attr = getattr(attr, field, None) + if attr is None: + break + return attr + + + def to_representation(self, instance): + ''' + instance (instancia o queryset del owner de las respuestas) + ''' - if self.context.get('many', False): - # Si es many=True, obtiene todos los registros cuyo owner sea del tipo de modelo dado - registros = Registro.objects.filter(owner_content_type=content_type) - # Mapea los registros a sus respuestas correspondientes - respuestas = Respuesta.objects.filter(registro__in=registros) - return super().to_representation(respuestas, many=True) - else: - # Si es una instancia única, obtiene el registro correspondiente al owner específico - registro = Registro.objects.get(owner_content_type=content_type, owner_id=instance.id) - respuesta = Respuesta.objects.get(registro=registro) - return super().to_representation(respuesta) + #obtenemos el content_type modelo del owner + + #obtenemos la estructura del formulario + form_id = self.get_attr_with_lookup(instance, self.dynamic_form_source).pk + form_data = {} + #''' + form_data = DynamicFormSerializer(self.forms_cache[form_id]).data + #agregamos las representaciones de las respuestas a estos. + for seccion in form_data['secciones']: + r_forulario_dict = self.registros_respuestas_cache.get(instance.pk, {}) + r_seccion_dict = r_forulario_dict.get(seccion['id'], {}) + if seccion['tipo'] == Seccion.Tipo.LISTA: + r_secciones = r_forulario_dict.get(seccion['id'], {}) + seccion['registros'] = [] + for r_seccion_dict in r_secciones.values(): + r_s_id = r_seccion_dict.get('instance', None) + r_s_id = r_s_id.pk if r_s_id else None + registro = { + 'id': r_s_id, + 'respuestas': [ + DynamicModelSerializer(r_seccion_dict.get(elemento['id'], None)).data + if r_seccion_dict and r_seccion_dict.get(elemento['id'], None) else {} + for elemento in seccion['elementos'] + ] + } + seccion['registros'].append(registro) + + r_seccion_dict = None + for elemento in seccion['elementos']: + respuesta = r_seccion_dict.get(elemento['id'], None) if r_seccion_dict else None + if respuesta: + elemento['respuesta'] = DynamicModelSerializer(respuesta).data + else: + elemento['respuesta'] = {} + #''' + return form_data def to_internal_value(self, data): # Implementación del mapeo inverso según sea necesario diff --git a/cosiap_api/dynamic_forms/tests.py b/cosiap_api/dynamic_forms/tests.py index 097493b..c2a8676 100644 --- a/cosiap_api/dynamic_forms/tests.py +++ b/cosiap_api/dynamic_forms/tests.py @@ -2,7 +2,7 @@ from rest_framework import status from common import custom_tests as c_tests from common.custom_tests import BasePerUserTestCase from django.core.files.uploadedfile import SimpleUploadedFile -from common.utils import printDict +from common.utils import print_dict, print_captured_queries from PIL import Image import io import os @@ -11,8 +11,16 @@ from dynamic_forms.models import ( DynamicForm, DynamicFormsSecciones, Respuesta ) -from django.db import connection from django.test.utils import CaptureQueriesContext +from django.db import connection + + +from users.models import Solicitante +from solicitudes.models import Solicitud +from modalidades.models import Modalidad +from dynamic_forms.models import ( + RegistroFormulario ,RegistroSeccion, Respuesta, RCasillas, RDesplegable, RDocumento, RFecha + , RHora, RNumerico, ROpcionMultiple, RTextoCorto, RTextoParrafo) @@ -83,7 +91,7 @@ class OpcionTests(BasePerUserTestCase): with CaptureQueriesContext(connection) as ctx: print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:opciones', token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['data']), self.opcion_count) print(f'\nRENDIMIENTO QUERYS: {len(ctx.captured_queries)}') @@ -93,7 +101,7 @@ class OpcionTests(BasePerUserTestCase): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:opciones', token=self.user_token, user=self.user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertFalse('data' in response.data) @@ -102,7 +110,7 @@ class OpcionTests(BasePerUserTestCase): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:opciones', query_params={'q': 'an'}, token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['data']), 2) @@ -111,7 +119,7 @@ class OpcionTests(BasePerUserTestCase): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:opciones_pk', url_kwargs={'pk': self.opcion1.pk}, token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['data']['nombre'], 'Opcion prueba 1 piña') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -121,7 +129,7 @@ class OpcionTests(BasePerUserTestCase): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:opciones_pk', url_kwargs={'pk': 99999999999}, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) subtest_name = 'test_post_opciones' @@ -132,7 +140,7 @@ class OpcionTests(BasePerUserTestCase): 'nombre': 'Nueva opcion recien creada' } response = self.perform_request('post', url_name='dynamic_forms:opciones', data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(Opcion.objects.count(), self.opcion_count+1) @@ -144,7 +152,7 @@ class OpcionTests(BasePerUserTestCase): } response = self.perform_request('post', url_name='dynamic_forms:opciones', data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(Opcion.objects.count(), self.opcion_count) @@ -162,7 +170,7 @@ class OpcionTests(BasePerUserTestCase): dict_nuevo = Opcion.objects.get(id=dict_original['id']).__dict__.copy() dict_nuevo.pop('_state', None) print(dict_nuevo) - printDict(response.data) + print_dict(response.data) self.assertNotEqual(dict_nuevo, dict_original) self.assertEqual(dict_nuevo, response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -182,7 +190,7 @@ class OpcionTests(BasePerUserTestCase): dict_nuevo = Opcion.objects.get(id=dict_original['id']).__dict__.copy() dict_nuevo.pop('_state', None) print(dict_nuevo) - printDict(response.data) + print_dict(response.data) self.assertEqual(dict_nuevo, dict_original) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(Opcion.objects.count(), self.opcion_count) @@ -192,7 +200,7 @@ class OpcionTests(BasePerUserTestCase): self.reset() print(subtest_name) response = self.perform_request('put', url_name='dynamic_forms:opciones_pk', url_kwargs={'pk': 99999999999}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) subtest_name = 'test_delete_opciones_pk' @@ -200,7 +208,7 @@ class OpcionTests(BasePerUserTestCase): self.reset() print(subtest_name) response = self.perform_request('delete', url_name='dynamic_forms:opciones_pk', url_kwargs={'pk': self.opcion1.pk}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(Opcion.objects.count(), self.opcion_count-1) @@ -209,7 +217,7 @@ class OpcionTests(BasePerUserTestCase): self.reset() print(subtest_name) response = self.perform_request('delete', url_name='dynamic_forms:opciones_pk', url_kwargs={'pk': 99999999999}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) @@ -280,7 +288,7 @@ class ElementoTests(OpcionTests): with CaptureQueriesContext(connection) as ctx: print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:elementos', token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['data']), self.elemento_count) @@ -291,7 +299,7 @@ class ElementoTests(OpcionTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:elementos', token=self.user_token, user=self.user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertFalse('data' in response.data) @@ -300,7 +308,7 @@ class ElementoTests(OpcionTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:elementos', query_params={'q': 'prueba 2'}, token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['data']), 1) @@ -309,7 +317,7 @@ class ElementoTests(OpcionTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:elementos_pk', url_kwargs={'pk': self.elemento1.pk}, token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['data']['nombre'], 'Elemento prueba 1') @@ -318,7 +326,7 @@ class ElementoTests(OpcionTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:elementos_pk', url_kwargs={'pk': 99999999999}, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) subtest_name = 'test_post_elementos' @@ -330,7 +338,7 @@ class ElementoTests(OpcionTests): 'tipo': Elemento.Tipo.TEXTO_CORTO } response = self.perform_request('post', url_name='dynamic_forms:elementos', data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(Elemento.objects.count(), self.elemento_count+1) @@ -340,7 +348,7 @@ class ElementoTests(OpcionTests): print(subtest_name) data = {} response = self.perform_request('post', url_name='dynamic_forms:elementos', data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(Elemento.objects.count(), self.elemento_count) @@ -355,7 +363,7 @@ class ElementoTests(OpcionTests): 'nombre': 'Nombre modificado' } response = self.perform_request('put', url_name='dynamic_forms:elementos_pk', url_kwargs={'pk': dict_original['id']}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(Elemento.objects.get(pk=self.elemento1.pk).nombre, 'Nombre modificado') @@ -370,7 +378,7 @@ class ElementoTests(OpcionTests): 'nombre': '' } response = self.perform_request('put', url_name='dynamic_forms:elementos_pk', url_kwargs={'pk': dict_original['id']}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(Elemento.objects.get(pk=self.elemento1.pk).nombre, 'Elemento prueba 1') @@ -379,7 +387,7 @@ class ElementoTests(OpcionTests): self.reset() print(subtest_name) response = self.perform_request('delete', url_name='dynamic_forms:elementos_pk', url_kwargs={'pk': self.elemento1.pk}, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(Elemento.objects.count(), self.elemento_count-1) @@ -450,7 +458,7 @@ class ElementosOpcionesTests(ElementoTests): with CaptureQueriesContext(connection) as ctx: print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:elementos_opciones', url_kwargs={'elemento': self.elemento1.pk, 'opcion': self.opcion1.pk}, token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['data']), 2) #esto representa unicamente una opcion: la opcion y su orden @@ -461,7 +469,7 @@ class ElementosOpcionesTests(ElementoTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:elementos_opciones', url_kwargs={'elemento': self.elemento1.pk, 'opcion': self.opcion1.pk}, token=self.user_token, user=self.user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertFalse('data' in response.data) @@ -474,7 +482,7 @@ class ElementosOpcionesTests(ElementoTests): 'opcion': self.opcion3.pk } response = self.perform_request('post', url_name='dynamic_forms:elementos_opciones', url_kwargs={'elemento': self.elemento1.pk, 'opcion': self.opcion3.pk} , data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(ElementosOpciones.objects.count(), self.elementos_opciones_count+1) @@ -484,7 +492,7 @@ class ElementosOpcionesTests(ElementoTests): print(subtest_name) data = {} response = self.perform_request('post', url_name='dynamic_forms:elementos_opciones', url_kwargs={'elemento': self.elemento1.pk, 'opcion': 999999999}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(ElementosOpciones.objects.count(), self.elementos_opciones_count) @@ -499,7 +507,7 @@ class ElementosOpcionesTests(ElementoTests): 'orden': 22 } response = self.perform_request('put', url_name='dynamic_forms:elementos_opciones', url_kwargs={'elemento': dict_original['elemento_id'], 'opcion': dict_original['opcion_id']}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(ElementosOpciones.objects.get(pk=self.elementos_opciones1.pk).orden, 22) @@ -513,7 +521,7 @@ class ElementosOpcionesTests(ElementoTests): data = { } response = self.perform_request('put', url_name='dynamic_forms:elementos_opciones', url_kwargs={'elemento': dict_original['elemento_id'], 'opcion': dict_original['opcion_id']}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) subtest_name = 'test_delete_elementos_opciones' @@ -521,7 +529,7 @@ class ElementosOpcionesTests(ElementoTests): self.reset() print(subtest_name) response = self.perform_request('delete', url_name='dynamic_forms:elementos_opciones', url_kwargs={'elemento': self.elemento1.pk, 'opcion': self.opcion1.pk}, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(ElementosOpciones.objects.count(), self.elementos_opciones_count-1) @@ -593,7 +601,7 @@ class SeccionTests(ElementoTests): with CaptureQueriesContext(connection) as ctx: print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:secciones', token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['data']), self.seccion_count) print(f'\nRENDIMIENTO QUERYS: {len(ctx.captured_queries)}') @@ -603,7 +611,7 @@ class SeccionTests(ElementoTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:secciones', token=self.user_token, user=self.user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertFalse('data' in response.data) @@ -612,7 +620,7 @@ class SeccionTests(ElementoTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:secciones', query_params={'q': 'prueba 2'}, token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['data']), 1) @@ -621,7 +629,7 @@ class SeccionTests(ElementoTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:secciones_pk', url_kwargs={'pk': self.seccion1.pk}, token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['data']['nombre'], 'Sección prueba 1') @@ -630,7 +638,7 @@ class SeccionTests(ElementoTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:secciones_pk', url_kwargs={'pk': 99999999999}, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) subtest_name = 'test_post_secciones' @@ -642,7 +650,7 @@ class SeccionTests(ElementoTests): 'tipo': Seccion.Tipo.UNICO } response = self.perform_request('post', url_name='dynamic_forms:secciones', data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(Seccion.objects.count(), self.seccion_count+1) @@ -652,7 +660,7 @@ class SeccionTests(ElementoTests): print(subtest_name) data = {} response = self.perform_request('post', url_name='dynamic_forms:secciones', data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(Seccion.objects.count(), self.seccion_count) @@ -667,7 +675,7 @@ class SeccionTests(ElementoTests): 'nombre': 'Nombre modificado' } response = self.perform_request('put', url_name='dynamic_forms:secciones_pk', url_kwargs={'pk': dict_original['id']}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(Seccion.objects.get(pk=self.seccion1.pk).nombre, 'Nombre modificado') @@ -682,7 +690,7 @@ class SeccionTests(ElementoTests): 'nombre': '' } response = self.perform_request('put', url_name='dynamic_forms:secciones_pk', url_kwargs={'pk': dict_original['id']}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(Seccion.objects.get(pk=self.seccion1.pk).nombre, 'Sección prueba 1') @@ -691,7 +699,7 @@ class SeccionTests(ElementoTests): self.reset() print(subtest_name) response = self.perform_request('delete', url_name='dynamic_forms:secciones_pk', url_kwargs={'pk': self.seccion1.pk}, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(Seccion.objects.count(), self.seccion_count-1) @@ -756,7 +764,7 @@ class SeccionesElementosTests(SeccionTests): with CaptureQueriesContext(connection) as ctx: print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:secciones_elementos', url_kwargs={'seccion': self.seccion1.pk, 'elemento': self.elemento1.pk}, token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['data']), 2) @@ -767,7 +775,7 @@ class SeccionesElementosTests(SeccionTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:secciones_elementos', url_kwargs={'seccion': self.seccion1.pk, 'elemento': self.elemento1.pk}, token=self.user_token, user=self.user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertFalse('data' in response.data) @@ -779,7 +787,7 @@ class SeccionesElementosTests(SeccionTests): 'orden': 44 } response = self.perform_request('post', url_name='dynamic_forms:secciones_elementos', url_kwargs={'seccion': self.seccion2.pk, 'elemento': self.elemento2.pk} , data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(SeccionesElementos.objects.count(), self.secciones_elementos_count+1) @@ -789,7 +797,7 @@ class SeccionesElementosTests(SeccionTests): print(subtest_name) data = {} response = self.perform_request('post', url_name='dynamic_forms:secciones_elementos', url_kwargs={'seccion': self.seccion2.pk, 'elemento': 999999999}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(SeccionesElementos.objects.count(), self.secciones_elementos_count) @@ -804,7 +812,7 @@ class SeccionesElementosTests(SeccionTests): 'orden': 22 } response = self.perform_request('put', url_name='dynamic_forms:secciones_elementos', url_kwargs={'seccion': dict_original['seccion_id'], 'elemento': dict_original['elemento_id']}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(SeccionesElementos.objects.get(pk=self.secciones_elementos1.pk).orden, 22) @@ -818,7 +826,7 @@ class SeccionesElementosTests(SeccionTests): data = { } response = self.perform_request('put', url_name='dynamic_forms:secciones_elementos', url_kwargs={'seccion': dict_original['seccion_id'], 'elemento': dict_original['elemento_id']}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) subtest_name = 'test_delete_secciones_elementos' @@ -826,7 +834,7 @@ class SeccionesElementosTests(SeccionTests): self.reset() print(subtest_name) response = self.perform_request('delete', url_name='dynamic_forms:secciones_elementos', url_kwargs={'seccion': self.seccion1.pk, 'elemento': self.elemento1.pk}, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(SeccionesElementos.objects.count(), self.secciones_elementos_count-1) @@ -901,17 +909,18 @@ class DynamicFormTests(SeccionTests): with CaptureQueriesContext(connection) as ctx: print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:dynamic_forms', token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['data']), self.dynamic_form_count) + #print_captured_queries(ctx.captured_queries) print(f'\nRENDIMIENTO QUERYS: {len(ctx.captured_queries)}') - + subtest_name = 'test_get_forms_incorrect_user' with self.subTest(subtest_name): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:dynamic_forms', token=self.user_token, user=self.user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertFalse('data' in response.data) @@ -920,7 +929,7 @@ class DynamicFormTests(SeccionTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:dynamic_forms', query_params={'q': 'prueba 1'}, token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['data']), 1) @@ -929,7 +938,7 @@ class DynamicFormTests(SeccionTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:dynamic_forms_pk', url_kwargs={'pk': self.dynamic_form1.pk}, token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['data']['nombre'], 'Form prueba 1') @@ -938,7 +947,7 @@ class DynamicFormTests(SeccionTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:dynamic_forms_pk', url_kwargs={'pk': 99999999999}, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) subtest_name = 'test_post_form' @@ -949,7 +958,7 @@ class DynamicFormTests(SeccionTests): 'nombre': 'Nuevo form', } response = self.perform_request('post', url_name='dynamic_forms:dynamic_forms', data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(DynamicForm.objects.count(), self.dynamic_form_count+1) @@ -959,7 +968,7 @@ class DynamicFormTests(SeccionTests): print(subtest_name) data = {} response = self.perform_request('post', url_name='dynamic_forms:dynamic_forms', data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(DynamicForm.objects.count(), self.dynamic_form_count) @@ -974,7 +983,7 @@ class DynamicFormTests(SeccionTests): 'nombre': 'Form modificado' } response = self.perform_request('put', url_name='dynamic_forms:dynamic_forms_pk', url_kwargs={'pk': dict_original['id']}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(DynamicForm.objects.get(pk=self.dynamic_form1.pk).nombre, 'Form modificado') @@ -989,7 +998,7 @@ class DynamicFormTests(SeccionTests): 'nombre': '' } response = self.perform_request('put', url_name='dynamic_forms:dynamic_forms_pk', url_kwargs={'pk': dict_original['id']}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(DynamicForm.objects.get(pk=self.dynamic_form1.pk).nombre, 'Form prueba 1') @@ -998,7 +1007,7 @@ class DynamicFormTests(SeccionTests): self.reset() print(subtest_name) response = self.perform_request('delete', url_name='dynamic_forms:dynamic_forms_pk', url_kwargs={'pk': self.dynamic_form1.pk}, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(DynamicForm.objects.count(), self.dynamic_form_count-1) @@ -1063,7 +1072,7 @@ class DynamicFormsSeccionesTests(DynamicFormTests): with CaptureQueriesContext(connection) as ctx: print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:dynamic_forms_secciones', url_kwargs={'formulario': self.formulario1.pk, 'seccion': self.seccion1.pk}, token=self.solicitante_token, user=self.solicitante_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['data']), 2) @@ -1074,7 +1083,7 @@ class DynamicFormsSeccionesTests(DynamicFormTests): self.reset() print(subtest_name) response = self.perform_request('get', url_name='dynamic_forms:dynamic_forms_secciones', url_kwargs={'formulario': self.formulario1.pk, 'seccion': self.seccion1.pk}, token=self.user_token, user=self.user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertFalse('data' in response.data) @@ -1087,7 +1096,7 @@ class DynamicFormsSeccionesTests(DynamicFormTests): 'orden': 66 } response = self.perform_request('post', url_name='dynamic_forms:dynamic_forms_secciones', url_kwargs={'formulario': self.formulario2.pk, 'seccion': self.seccion2.pk}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(DynamicFormsSecciones.objects.count(), self.forms_secciones_count + 1) @@ -1097,7 +1106,7 @@ class DynamicFormsSeccionesTests(DynamicFormTests): print(subtest_name) data = {} response = self.perform_request('post', url_name='dynamic_forms:dynamic_forms_secciones', url_kwargs={'formulario': self.formulario2.pk, 'seccion': 999999999}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(DynamicFormsSecciones.objects.count(), self.forms_secciones_count) @@ -1112,7 +1121,7 @@ class DynamicFormsSeccionesTests(DynamicFormTests): 'orden': 22 } response = self.perform_request('put', url_name='dynamic_forms:dynamic_forms_secciones', url_kwargs={'formulario': dict_original['dynamic_form_id'], 'seccion': dict_original['seccion_id']}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(DynamicFormsSecciones.objects.get(pk=self.forms_secciones1.pk).orden, 22) @@ -1125,7 +1134,7 @@ class DynamicFormsSeccionesTests(DynamicFormTests): print(dict_original) data = {} response = self.perform_request('put', url_name='dynamic_forms:dynamic_forms_secciones', url_kwargs={'formulario': dict_original['dynamic_form_id'], 'seccion': dict_original['seccion_id']}, data=data, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) subtest_name = 'test_delete_dynamic_forms_secciones' @@ -1133,6 +1142,194 @@ class DynamicFormsSeccionesTests(DynamicFormTests): self.reset() print(subtest_name) response = self.perform_request('delete', url_name='dynamic_forms:dynamic_forms_secciones', url_kwargs={'formulario': self.formulario1.pk, 'seccion': self.seccion1.pk}, token=self.admin_token, user=self.admin_user) - printDict(response.data) + print_dict(response.data) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(DynamicFormsSecciones.objects.count(), self.forms_secciones_count - 1) \ No newline at end of file + self.assertEqual(DynamicFormsSecciones.objects.count(), self.forms_secciones_count - 1) + + +class FormularioRespuestaTests(BasePerUserTestCase): + def setUp(self): + super().setUp() + + self.solicitante_user2 = Solicitante.objects.create( + curp='solicitanteuser2', + password='solicitantepassword', + email='solicitante22@gmail.com', + nombre='Solicitante 222', + ap_paterno='Marqueawdz', + telefono='0000000001', + RFC='1234567890124', # Ajustar tipo de dato + direccion='Calle sin Nombre', + codigo_postal='89890', # Ajustar tipo de dato + municipio_id=2, + poblacion=5, + INE='veeveve' + ) + + self.solicitante_token2 = self.get_tokens_for_user(self.solicitante_user) + + # Crear opciones para los elementos de tipo opción múltiple, casillas, y desplegable + self.opcion_1 = Opcion.objects.create(nombre='Opción 1') + self.opcion_2 = Opcion.objects.create(nombre='Opción 2') + self.opcion_3 = Opcion.objects.create(nombre='Opción 3') + self.opcion_4 = Opcion.objects.create(nombre='Opción 4') + + # Crear un elemento para cada tipo del choices + self.elemento_separador = Elemento.objects.create(nombre='Separador', tipo=Elemento.Tipo.SEPARADOR) + self.elemento_numerico = Elemento.objects.create(nombre='Elemento Numérico', tipo=Elemento.Tipo.NUMERICO, min_digits=1, max_digits=10) + self.elemento_texto_corto = Elemento.objects.create(nombre='Elemento Texto Corto', tipo=Elemento.Tipo.TEXTO_CORTO, obligatorio=True) + self.elemento_texto_parrafo = Elemento.objects.create(nombre='Elemento Texto Párrafo', tipo=Elemento.Tipo.TEXT_PARRAFO) + self.elemento_hora = Elemento.objects.create(nombre='Elemento Hora', tipo=Elemento.Tipo.HORA) + self.elemento_fecha = Elemento.objects.create(nombre='Elemento Fecha', tipo=Elemento.Tipo.FECHA) + self.elemento_opcion_multiple = Elemento.objects.create(nombre='Elemento Opción Múltiple', tipo=Elemento.Tipo.OPCION_MULTIPLE, opcion_otro=True) + self.elemento_casillas = Elemento.objects.create(nombre='Elemento Casillas', tipo=Elemento.Tipo.CASILLAS) + self.elemento_desplegable = Elemento.objects.create(nombre='Elemento Desplegable', tipo=Elemento.Tipo.DESPLEGABLE) + self.elemento_documento = Elemento.objects.create(nombre='Elemento Documento', tipo=Elemento.Tipo.DOCUMENTO) + + # Asignar opciones a los elementos correspondientes + ElementosOpciones.objects.create(elemento=self.elemento_opcion_multiple, opcion=self.opcion_1, orden=1) + ElementosOpciones.objects.create(elemento=self.elemento_opcion_multiple, opcion=self.opcion_2, orden=2) + ElementosOpciones.objects.create(elemento=self.elemento_casillas, opcion=self.opcion_3, orden=1) + ElementosOpciones.objects.create(elemento=self.elemento_casillas, opcion=self.opcion_4, orden=2) + ElementosOpciones.objects.create(elemento=self.elemento_desplegable, opcion=self.opcion_1, orden=1) + ElementosOpciones.objects.create(elemento=self.elemento_desplegable, opcion=self.opcion_4, orden=2) + + # Crear secciones y asignarles elementos + self.seccion_all = Seccion.objects.create(nombre='Sección Todo', tipo=Seccion.Tipo.UNICO) + self.seccion_1 = Seccion.objects.create(nombre='Sección 1', tipo=Seccion.Tipo.UNICO) + self.seccion_2 = Seccion.objects.create(nombre='Sección 2', tipo=Seccion.Tipo.LISTA) + self.seccion_3 = Seccion.objects.create(nombre='Sección 3', tipo=Seccion.Tipo.LISTA) + self.seccion_4 = Seccion.objects.create(nombre='Sección 4', tipo=Seccion.Tipo.LISTA) + self.seccion_5 = Seccion.objects.create(nombre='Sección 5', tipo=Seccion.Tipo.UNICO) + + SeccionesElementos.objects.create(seccion=self.seccion_all, elemento=self.elemento_separador, orden=1) + SeccionesElementos.objects.create(seccion=self.seccion_all, elemento=self.elemento_numerico, orden=2) + SeccionesElementos.objects.create(seccion=self.seccion_all, elemento=self.elemento_texto_corto, orden=3) + SeccionesElementos.objects.create(seccion=self.seccion_all, elemento=self.elemento_texto_parrafo, orden=4) + SeccionesElementos.objects.create(seccion=self.seccion_all, elemento=self.elemento_hora, orden=4) + SeccionesElementos.objects.create(seccion=self.seccion_all, elemento=self.elemento_fecha, orden=5) + SeccionesElementos.objects.create(seccion=self.seccion_all, elemento=self.elemento_opcion_multiple, orden=6) + SeccionesElementos.objects.create(seccion=self.seccion_all, elemento=self.elemento_casillas, orden=7) + SeccionesElementos.objects.create(seccion=self.seccion_all, elemento=self.elemento_desplegable, orden=8) + SeccionesElementos.objects.create(seccion=self.seccion_all, elemento=self.elemento_documento, orden=9) + + SeccionesElementos.objects.create(seccion=self.seccion_1, elemento=self.elemento_separador, orden=0) + SeccionesElementos.objects.create(seccion=self.seccion_1, elemento=self.elemento_numerico, orden=1) + SeccionesElementos.objects.create(seccion=self.seccion_1, elemento=self.elemento_texto_corto, orden=2) + SeccionesElementos.objects.create(seccion=self.seccion_1, elemento=self.elemento_fecha, orden=3) + + SeccionesElementos.objects.create(seccion=self.seccion_2, elemento=self.elemento_texto_parrafo, orden=1) + SeccionesElementos.objects.create(seccion=self.seccion_2, elemento=self.elemento_hora, orden=2) + SeccionesElementos.objects.create(seccion=self.seccion_2, elemento=self.elemento_desplegable, orden=3) + + SeccionesElementos.objects.create(seccion=self.seccion_3, elemento=self.elemento_numerico, orden=1) + SeccionesElementos.objects.create(seccion=self.seccion_3, elemento=self.elemento_opcion_multiple, orden=2) + + SeccionesElementos.objects.create(seccion=self.seccion_4, elemento=self.elemento_casillas, orden=1) + SeccionesElementos.objects.create(seccion=self.seccion_4, elemento=self.elemento_documento, orden=2) + + SeccionesElementos.objects.create(seccion=self.seccion_5, elemento=self.elemento_desplegable, orden=1) + + # Crear dos formularios dinámicos y asignarles secciones + self.formulario_all = DynamicForm.objects.create(nombre='Formulario Todo') + self.formulario_1 = DynamicForm.objects.create(nombre='Formulario 1') + self.formulario_2 = DynamicForm.objects.create(nombre='Formulario 2') + + DynamicFormsSecciones.objects.create(dynamic_form=self.formulario_all, seccion=self.seccion_all, orden=1) + + DynamicFormsSecciones.objects.create(dynamic_form=self.formulario_1, seccion=self.seccion_1, orden=1) + DynamicFormsSecciones.objects.create(dynamic_form=self.formulario_1, seccion=self.seccion_3, orden=2) + DynamicFormsSecciones.objects.create(dynamic_form=self.formulario_1, seccion=self.seccion_5, orden=3) + + DynamicFormsSecciones.objects.create(dynamic_form=self.formulario_2, seccion=self.seccion_2, orden=1) + DynamicFormsSecciones.objects.create(dynamic_form=self.formulario_2, seccion=self.seccion_4, orden=2) + DynamicFormsSecciones.objects.create(dynamic_form=self.formulario_2, seccion=self.seccion_5, orden=3) + + self.modalidad_1 = Modalidad.objects.create(dynamic_form=self.formulario_1, nombre='Modalidad Prueba 1', descripcion='Descripción 1', mostrar=True, archivado=False) + self.modalidad_2 = Modalidad.objects.create(dynamic_form=self.formulario_2, nombre='Modalidad Prueba 2', descripcion='Descripción 2', mostrar=True, archivado=False) + self.modalidad_3 = Modalidad.objects.create(dynamic_form=self.formulario_all, nombre='Modalidad Prueba 3', descripcion='Descripción 3', mostrar=False, archivado=False) + + #creando registros completados para el usuario 1 + #registros de formulario 1 (modalidad 1 ) + self.registro_formulario_1 = RegistroFormulario.objects.create() + + self.registro_seccion_f1_s1 = RegistroSeccion.objects.create(registro_formulario=self.registro_formulario_1, seccion=self.seccion_1) + self.registro_seccion_f1_s3_1 = RegistroSeccion.objects.create(registro_formulario=self.registro_formulario_1, seccion=self.seccion_3) + self.registro_seccion_f1_s5_1 = RegistroSeccion.objects.create(registro_formulario=self.registro_formulario_1, seccion=self.seccion_5) + self.registro_seccion_f1_s3_2 = RegistroSeccion.objects.create(registro_formulario=self.registro_formulario_1, seccion=self.seccion_3) + self.registro_seccion_f1_s5_2 = RegistroSeccion.objects.create(registro_formulario=self.registro_formulario_1, seccion=self.seccion_5) + + + self.respuesta_separador_r1 = RNumerico.objects. create(registro_seccion=self.registro_seccion_f1_s1, elemento=self.elemento_numerico, valor=999) + self.respuesta_texto_corto_r1 = RTextoCorto.objects. create(registro_seccion=self.registro_seccion_f1_s1, elemento=self.elemento_texto_corto, valor='Quinientas Sandias') + self.respuesta_fecha_r1 = RFecha.objects. create(registro_seccion=self.registro_seccion_f1_s1, elemento=self.elemento_fecha, valor='2024-08-14') + + self.respuesta_numerico_r1_1 = RNumerico.objects. create(registro_seccion=self.registro_seccion_f1_s3_1, elemento=self.elemento_numerico,valor=44) + self.respuesta_opcion_multiple_r1_1 = ROpcionMultiple.objects. create(registro_seccion=self.registro_seccion_f1_s3_1, elemento=self.elemento_opcion_multiple,valor=self.opcion_2) + self.respuesta_desplegable_r1_1 = RDesplegable.objects. create(registro_seccion=self.registro_seccion_f1_s5_1, elemento=self.elemento_desplegable, valor=self.opcion_3) + self.respuesta_numerico_r1_2 = RNumerico.objects. create(registro_seccion=self.registro_seccion_f1_s3_2, elemento=self.elemento_numerico,valor=44) + self.respuesta_opcion_multiple_r1_2 = ROpcionMultiple.objects. create(registro_seccion=self.registro_seccion_f1_s3_2, elemento=self.elemento_opcion_multiple,valor=self.opcion_2) + self.respuesta_desplegable_r1_2 = RDesplegable.objects. create(registro_seccion=self.registro_seccion_f1_s5_2, elemento=self.elemento_desplegable, valor=self.opcion_3) + +############# + self.registro_seccion_f1_s3_2 = RegistroSeccion.objects.create(registro_formulario=self.registro_formulario_1, seccion=self.seccion_3) + self.registro_seccion_f1_s5_2 = RegistroSeccion.objects.create(registro_formulario=self.registro_formulario_1, seccion=self.seccion_5) + self.respuesta_numerico_r1_2 = RNumerico.objects. create(registro_seccion=self.registro_seccion_f1_s3_2, elemento=self.elemento_numerico,valor=44) + self.respuesta_opcion_multiple_r1_2 = ROpcionMultiple.objects. create(registro_seccion=self.registro_seccion_f1_s3_2, elemento=self.elemento_opcion_multiple,valor=self.opcion_2) + self.respuesta_desplegable_r1_2 = RDesplegable.objects. create(registro_seccion=self.registro_seccion_f1_s5_2, elemento=self.elemento_desplegable, valor=self.opcion_3) + + self.registro_seccion_f1_s3_2 = RegistroSeccion.objects.create(registro_formulario=self.registro_formulario_1, seccion=self.seccion_3) + self.registro_seccion_f1_s5_2 = RegistroSeccion.objects.create(registro_formulario=self.registro_formulario_1, seccion=self.seccion_5) + self.respuesta_numerico_r1_2 = RNumerico.objects. create(registro_seccion=self.registro_seccion_f1_s3_2, elemento=self.elemento_numerico,valor=44) + self.respuesta_opcion_multiple_r1_2 = ROpcionMultiple.objects. create(registro_seccion=self.registro_seccion_f1_s3_2, elemento=self.elemento_opcion_multiple,valor=self.opcion_2) + self.respuesta_desplegable_r1_2 = RDesplegable.objects. create(registro_seccion=self.registro_seccion_f1_s5_2, elemento=self.elemento_desplegable, valor=self.opcion_3) + + self.registro_seccion_f1_s3_2 = RegistroSeccion.objects.create(registro_formulario=self.registro_formulario_1, seccion=self.seccion_3) + self.registro_seccion_f1_s5_2 = RegistroSeccion.objects.create(registro_formulario=self.registro_formulario_1, seccion=self.seccion_5) + self.respuesta_numerico_r1_2 = RNumerico.objects. create(registro_seccion=self.registro_seccion_f1_s3_2, elemento=self.elemento_numerico,valor=44) + self.respuesta_opcion_multiple_r1_2 = ROpcionMultiple.objects. create(registro_seccion=self.registro_seccion_f1_s3_2, elemento=self.elemento_opcion_multiple,valor=self.opcion_2) + self.respuesta_desplegable_r1_2 = RDesplegable.objects. create(registro_seccion=self.registro_seccion_f1_s5_2, elemento=self.elemento_desplegable, valor=self.opcion_3) +############ + + + self.registro_formulario_all = RegistroFormulario.objects.create() + self.registro_seccion_all = RegistroSeccion.objects.create(registro_formulario=self.registro_formulario_all, seccion=self.seccion_all) + self.respuesta_numerico_rall = RNumerico.objects.create(registro_seccion=self.registro_seccion_all, elemento=self.elemento_numerico, valor=55) + self.respuesta_texto_corto_rall = RTextoCorto.objects.create(registro_seccion=self.registro_seccion_all, elemento=self.elemento_texto_corto, valor='cortoo') + self.respuesta_texto_parrafo_rall = RTextoParrafo.objects.create(registro_seccion=self.registro_seccion_all, elemento=self.elemento_texto_parrafo, valor='largoo') + self.respuesta_hora_rall = RHora.objects.create(registro_seccion=self.registro_seccion_all, elemento=self.elemento_hora, valor='6:20') + self.respuesta_fecha_rall = RFecha.objects.create(registro_seccion=self.registro_seccion_all, elemento=self.elemento_fecha, valor='2024-08-14') + self.respuesta_opcion_multiple_rall = ROpcionMultiple.objects.create(registro_seccion=self.registro_seccion_all, elemento=self.elemento_opcion_multiple, valor=self.opcion_1) + self.respuesta_casillas_rall = RCasillas.objects.create(registro_seccion=self.registro_seccion_all, elemento=self.elemento_casillas, ) + self.respuesta_casillas_rall.valor.set([self.opcion_2, self.opcion_3]) + self.respuesta_desplegable_rall = RDesplegable.objects.create(registro_seccion=self.registro_seccion_all, elemento=self.elemento_desplegable, valor=self.opcion_4) + self.respuesta_documento_rall = RDocumento.objects.create(registro_seccion=self.registro_seccion_all, elemento=self.elemento_documento, ) + + + self.solicitud_1 = Solicitud.objects.create(modalidad=self.modalidad_1 , registro_formulario=self.registro_formulario_1) + self.solicitud_2 = Solicitud.objects.create(modalidad=self.modalidad_2) + self.solicitud_4 = Solicitud.objects.create(modalidad=self.modalidad_1) + + self.solicitud_all = Solicitud.objects.create(modalidad=self.modalidad_3 ,registro_formulario=self.registro_formulario_all) + + #REPETIDO################# + + + + def reset(self): + print('') + + def tests(self): + subtest_name = 'test_get_registro_formulario' + with self.subTest(subtest_name): + self.reset() + # Conectar la señal al manejador de depuración + + with CaptureQueriesContext(connection) as ctx: + print(subtest_name) + response = self.perform_request('get', url_name='dynamic_forms:solcitud_respuestas', token=self.solicitante_token, user=self.solicitante_user) + print_dict(response.data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + #print_captured_queries(ctx.captured_queries) + print(f'\nRENDIMIENTO QUERYS: {len(ctx.captured_queries)}') + diff --git a/cosiap_api/dynamic_forms/urls.py b/cosiap_api/dynamic_forms/urls.py index bcb6010..1a91c23 100644 --- a/cosiap_api/dynamic_forms/urls.py +++ b/cosiap_api/dynamic_forms/urls.py @@ -1,6 +1,6 @@ from django.urls import path from dynamic_forms.views import ( - OpcionAPIView, ElementoAPIView, SeccionAPIView, DynamicFormAPIView, RespuestaAPIView, + OpcionAPIView, ElementoAPIView, SeccionAPIView, DynamicFormAPIView, FormularioRespuestaAPIView, RespuestasFormularioSolicitudesAPIView, DynamicFormsSeccionesAPIView, ElementosOpcionesAPIView, SeccionesElementosAPIView, ) @@ -24,6 +24,6 @@ urlpatterns = [ path('', DynamicFormAPIView.as_view(), name='dynamic_forms'), path('/', DynamicFormAPIView.as_view(), name='dynamic_forms_pk'), - path('respuestas/', RespuestaAPIView.as_view(), name='respuestas'), #FormularioRecord - path('respuestas//', RespuestaAPIView.as_view(), name='respuestas_pk'), + path('respuestas/', RespuestasFormularioSolicitudesAPIView.as_view(), name='solcitud_respuestas'), #FormularioRecord + path('respuestas//', RespuestasFormularioSolicitudesAPIView.as_view(), name='solicitud_respuestas_pk'), ] diff --git a/cosiap_api/dynamic_forms/views.py b/cosiap_api/dynamic_forms/views.py index 0d61e0f..ed244e0 100644 --- a/cosiap_api/dynamic_forms/views.py +++ b/cosiap_api/dynamic_forms/views.py @@ -14,6 +14,7 @@ from .serializers import ( ) from rest_framework.permissions import IsAuthenticated from users.permisos import es_admin, primer_login +from solicitudes.models import Solicitud @@ -25,6 +26,13 @@ class BaseFormAPIView(BasePermissionAPIView): str_plural = None model_queryset = None + required_attributes = [ + 'model_class', + 'serializer_class', + 'str_simple', + 'str_plural', + ] + permission_classes_create = [IsAuthenticated, es_admin] permission_classes_delete = [IsAuthenticated, es_admin] permission_classes_list = [IsAuthenticated, primer_login] @@ -45,7 +53,16 @@ class BaseFormAPIView(BasePermissionAPIView): self.articulo_indefinido = "una" self.desinencia_singular = "a" self.desinencia_plural = "as" - super().__init__() + self._check_attributes() + super().__init__() + + def _check_attributes(self): + # Lista de atributos que deben ser verificados + + for attr in self.required_attributes: + value = getattr(self, attr, None) + if value is None: + raise ValueError(f'{attr} must be initialized') def get(self, request, pk=None, *args, **kwargs): if pk: @@ -212,38 +229,68 @@ class DynamicFormAPIView(BaseFormAPIView): str_plural = 'Formularios' # APIView para Respuesta -class RespuestaAPIView(BasePermissionAPIView): +class FormularioRespuestaAPIView(BaseFormAPIView): permission_classes_create = [IsAuthenticated, primer_login] permission_classes_delete = [IsAuthenticated, primer_login] permission_classes_list = [IsAuthenticated, primer_login] - permission_classes_update = [IsAuthenticated, primer_login] + permission_classes_update = [IsAuthenticated, primer_login] + + required_attributes = BaseFormAPIView.required_attributes + ['dynamic_form_source'] + + serializer_class = RespuestaFormularioSerializer + dynamic_form_source = None + genero_gramatical = False #False masculino, True Femenino + str_simple = 'Formulario' + str_plural = 'Formularios' - def get(self, request, pk=None): + def get(self, request, pk=None, *args, **kwargs): + #form_instance = get_object_or_404(DynamicForm.objects.prefetch_related( + # 'dynamicformssecciones_set__seccion__seccioneselementos_set__elemento__elementosopciones_set__opcion' + # ), pk=formulario) if pk: - respuesta = Respuesta.objects.get(pk=pk) - serializer = RespuestaFormularioSerializer(respuesta) + owner = get_object_or_404(self.model_class, pk=pk) + serializer = self.serializer_class(owner, dynamic_form_source=self.dynamic_form_source) else: - respuestas = Respuesta.objects.all() - serializer = RespuestaFormularioSerializer(respuestas, many=True) - response_data = {'data': serializer.data} + owners = self.model_queryset + serializer = self.serializer_class(owners, dynamic_form_source=self.dynamic_form_source, many=True) + + response_data = {'data': serializer.data} return Response(response_data, status=status.HTTP_200_OK) def post(self, request): - serializer = RespuestaFormularioSerializer(data=request.data) + serializer = self.serializer_class(data=request.data, dynamic_form_source=self.dynamic_form_source) if serializer.is_valid(): serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + response_data = {'data': serializer.data} + Mensaje.success(response_data, f'{self.str_simple.capitalize()} cread{self.desinencia_singular} con éxito.') + return Response(response_data, status=status.HTTP_201_CREATED) + response_data = {'errors': serializer.errors} + Mensaje.warning(response_data, f'No se pudo guardar {self.articulo_definido} {self.str_simple.lower()}.') + Mensaje.error(response_data, serializer.errors) + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) def put(self, request, pk): - respuesta = Respuesta.objects.get(pk=pk) - serializer = RespuestaFormularioSerializer(respuesta, data=request.data) + owner = get_object_or_404(self.model_class, pk=pk) + serializer = self.serializer_class(owner, data=request.data, dynamic_form_source=self.dynamic_form_source) if serializer.is_valid(): serializer.save() - return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + response_data = {'data': serializer.data} + Mensaje.success(response_data, f'{self.str_simple.capitalize()} actualizad{self.desinencia_singular} con éxito.') + return Response(serializer.data, status=status.HTTP_200_OK) + response_data = {'errors': serializer.errors} + Mensaje.warning(response_data, f'No se pudo actualizar {self.articulo_definido} {self.str_simple.lower()}.') + Mensaje.error(response_data, serializer.errors) + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, pk): + owner = get_object_or_404(self.owner, pk=pk) + response_data = {} + Mensaje.success(response_data, f'{self.str_simple.capitalize()} eliminad{self.desinencia_singular} con éxito') + return Response(response_data, status=status.HTTP_204_NO_CONTENT) + - def delete(self, request, pk): - respuesta = Respuesta.objects.get(pk=pk) - respuesta.delete() - return Response(status=status.HTTP_204_NO_CONTENT) +class RespuestasFormularioSolicitudesAPIView(FormularioRespuestaAPIView): + model_class = Solicitud + model_queryset = Solicitud.objects.all().prefetch_related('modalidad__dynamic_form') + dynamic_form_source = 'modalidad__dynamic_form' + \ No newline at end of file diff --git a/cosiap_api/solicitudes/migrations/0004_remove_generic_foreign_key_and_added_FormularioRespuesta.py b/cosiap_api/solicitudes/migrations/0004_remove_generic_foreign_key_and_added_FormularioRespuesta.py new file mode 100644 index 0000000..8c40813 --- /dev/null +++ b/cosiap_api/solicitudes/migrations/0004_remove_generic_foreign_key_and_added_FormularioRespuesta.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.7 on 2024-08-14 19:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamic_forms', '0010_remove_generic_foreign_key_and_added_FormularioRespuesta'), + ('solicitudes', '0003_merge_migrations'), + ] + + operations = [ + migrations.AddField( + model_name='solicitud', + name='respuesta_formulario', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dynamic_forms.registroformulario'), + ), + ] diff --git a/cosiap_api/solicitudes/migrations/0005_refactor_Registros.py b/cosiap_api/solicitudes/migrations/0005_refactor_Registros.py new file mode 100644 index 0000000..db718f7 --- /dev/null +++ b/cosiap_api/solicitudes/migrations/0005_refactor_Registros.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.7 on 2024-08-15 01:19 + +import django.db.models.deletion +import solicitudes.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamic_forms', '0011_refactor_Registros'), + ('solicitudes', '0004_remove_generic_foreign_key_and_added_FormularioRespuesta'), + ] + + operations = [ + migrations.RemoveField( + model_name='solicitud', + name='respuesta_formulario', + ), + migrations.AddField( + model_name='solicitud', + name='registro_formulario', + field=models.OneToOneField(default=solicitudes.models.create_default_registro_formulario, on_delete=django.db.models.deletion.SET_DEFAULT, to='dynamic_forms.registroformulario'), + ), + ] diff --git a/cosiap_api/solicitudes/migrations/0006_sanche_solicitud_pk_and_solicitud_n.py b/cosiap_api/solicitudes/migrations/0006_sanche_solicitud_pk_and_solicitud_n.py new file mode 100644 index 0000000..8f1b51e --- /dev/null +++ b/cosiap_api/solicitudes/migrations/0006_sanche_solicitud_pk_and_solicitud_n.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.7 on 2024-08-15 19:06 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('solicitudes', '0005_refactor_Registros'), + ] + + operations = [ + migrations.AlterField( + model_name='solicitud', + name='solicitud_n', + field=models.IntegerField(blank=True, null=True, unique=True, verbose_name='Num. Solicitud'), + ), + migrations.AddField( + model_name='solicitud', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + preserve_default=False, + ), + ] diff --git a/cosiap_api/solicitudes/migrations/0007_regenerate_solicitudes.py b/cosiap_api/solicitudes/migrations/0007_regenerate_solicitudes.py new file mode 100644 index 0000000..8529900 --- /dev/null +++ b/cosiap_api/solicitudes/migrations/0007_regenerate_solicitudes.py @@ -0,0 +1,57 @@ +# Generated by Django 5.0.7 on 2024-08-15 19:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('solicitudes', '0006_sanche_solicitud_pk_and_solicitud_n'), + ] + + operations = [ + migrations.RemoveField( + model_name='solicitud', + name='convenio', + ), + migrations.RemoveField( + model_name='solicitud', + name='minuta', + ), + migrations.RemoveField( + model_name='solicitud', + name='modalidad', + ), + migrations.RemoveField( + model_name='solicitud', + name='monto_aprobado', + ), + migrations.RemoveField( + model_name='solicitud', + name='monto_solicitado', + ), + migrations.RemoveField( + model_name='solicitud', + name='observacion', + ), + migrations.RemoveField( + model_name='solicitud', + name='registro_formulario', + ), + migrations.RemoveField( + model_name='solicitud', + name='solicitante', + ), + migrations.RemoveField( + model_name='solicitud', + name='solicitud_n', + ), + migrations.RemoveField( + model_name='solicitud', + name='status', + ), + migrations.RemoveField( + model_name='solicitud', + name='timestamp', + ), + ] diff --git a/cosiap_api/solicitudes/migrations/0008_regenerate_solicitudes.py b/cosiap_api/solicitudes/migrations/0008_regenerate_solicitudes.py new file mode 100644 index 0000000..7489057 --- /dev/null +++ b/cosiap_api/solicitudes/migrations/0008_regenerate_solicitudes.py @@ -0,0 +1,76 @@ +# Generated by Django 5.0.7 on 2024-08-15 19:23 + +import django.db.models.deletion +import django.utils.timezone +import solicitudes.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamic_forms', '0011_refactor_Registros'), + ('modalidades', '0003_merge_migrations'), + ('solicitudes', '0007_regenerate_solicitudes'), + ('users', '0012_llenando_catalogos_municipios_estados'), + ] + + operations = [ + migrations.AddField( + model_name='solicitud', + name='convenio', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='solicitudes.convenio', verbose_name='Convenio'), + ), + migrations.AddField( + model_name='solicitud', + name='minuta', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='solicitudes.minuta', verbose_name='Minuta'), + ), + migrations.AddField( + model_name='solicitud', + name='modalidad', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='modalidades.modalidad', verbose_name='Modalidad'), + ), + migrations.AddField( + model_name='solicitud', + name='monto_aprobado', + field=models.FloatField(default=0.0, verbose_name='Monto Aprobado'), + ), + migrations.AddField( + model_name='solicitud', + name='monto_solicitado', + field=models.FloatField(default=0.0, verbose_name='Monto Solicitado'), + ), + migrations.AddField( + model_name='solicitud', + name='observacion', + field=models.TextField(blank=True, null=True, verbose_name='Observación'), + ), + migrations.AddField( + model_name='solicitud', + name='registro_formulario', + field=models.OneToOneField(default=solicitudes.models.create_default_registro_formulario, on_delete=django.db.models.deletion.SET_DEFAULT, to='dynamic_forms.registroformulario'), + ), + migrations.AddField( + model_name='solicitud', + name='solicitante', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.solicitante', verbose_name='Solicitante'), + ), + migrations.AddField( + model_name='solicitud', + name='solicitud_n', + field=models.IntegerField(blank=True, null=True, unique=True, verbose_name='Num. Solicitud'), + ), + migrations.AddField( + model_name='solicitud', + name='status', + field=models.CharField(default=1, max_length=255, verbose_name='Status'), + preserve_default=False, + ), + migrations.AddField( + model_name='solicitud', + name='timestamp', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Timestamp'), + preserve_default=False, + ), + ] diff --git a/cosiap_api/solicitudes/models.py b/cosiap_api/solicitudes/models.py index aed171f..a4e90cd 100644 --- a/cosiap_api/solicitudes/models.py +++ b/cosiap_api/solicitudes/models.py @@ -4,6 +4,7 @@ from common.validadores_campos import validador_archivo_1MB, validador_pdf from dynamic_formats.models import DynamicFormat from users.models import Solicitante from modalidades.models import Modalidad +from dynamic_forms.models import RegistroFormulario class Minuta(models.Model): ''' @@ -89,6 +90,9 @@ class Convenio(models.Model): ordering = ['pk'] +def create_default_registro_formulario(): + return RegistroFormulario.objects.create().pk + class Solicitud(models.Model): """ Modelo que contiene la información de referencia de una solicitud. @@ -107,7 +111,7 @@ class Solicitud(models.Model): """ status = models.CharField(verbose_name='Status', max_length=255) - solicitud_n = models.AutoField(verbose_name='Num. Solicitud', primary_key=True) + solicitud_n = models.IntegerField(verbose_name='Num. Solicitud', null=True, blank=True, unique=True) minuta = models.ForeignKey(Minuta, verbose_name='Minuta', on_delete=models.SET_NULL, null=True, blank=True) convenio = models.OneToOneField(Convenio, verbose_name='Convenio', on_delete=models.SET_NULL, null=True, blank=True) monto_solicitado = models.FloatField(verbose_name='Monto Solicitado', default=0.0) @@ -116,3 +120,5 @@ class Solicitud(models.Model): timestamp = models.DateTimeField(verbose_name='Timestamp', auto_now_add=True) observacion = models.TextField(verbose_name='Observación', null=True, blank=True) solicitante = models.ForeignKey(Solicitante, verbose_name='Solicitante', on_delete=models.SET_NULL, null=True, blank=True) + registro_formulario = models.OneToOneField(RegistroFormulario, on_delete=models.SET_DEFAULT, default=create_default_registro_formulario) + diff --git a/cosiap_frontend/vite.config.js.timestamp-1723477813488-b5222855a1ef9.mjs b/cosiap_frontend/vite.config.js.timestamp-1723477813488-b5222855a1ef9.mjs new file mode 100644 index 0000000..a6bbd4a --- /dev/null +++ b/cosiap_frontend/vite.config.js.timestamp-1723477813488-b5222855a1ef9.mjs @@ -0,0 +1,20 @@ +// vite.config.js +import { defineConfig } from "file:///app/node_modules/vite/dist/node/index.js"; +import react from "file:///app/node_modules/@vitejs/plugin-react-swc/index.mjs"; +import path from "path"; +var __vite_injected_original_dirname = "/app"; +var vite_config_default = defineConfig({ + plugins: [react()], + server: { + host: "0.0.0.0" + }, + resolve: { + alias: { + "@": path.resolve(__vite_injected_original_dirname, "./src") + } + } +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvYXBwXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvYXBwL3ZpdGUuY29uZmlnLmpzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9hcHAvdml0ZS5jb25maWcuanNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJztcclxuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0LXN3Yyc7XHJcbmltcG9ydCBwYXRoIGZyb20gJ3BhdGgnO1xyXG5cclxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cclxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcclxuICBwbHVnaW5zOiBbcmVhY3QoKV0sXHJcbiAgc2VydmVyOiB7XHJcbiAgICBob3N0OiAnMC4wLjAuMCdcclxuICB9LFxyXG4gIHJlc29sdmU6IHtcclxuICAgICAgYWxpYXM6IHtcclxuICAgICAgXCJAXCI6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsIFwiLi9zcmNcIiksXHJcbiAgICB9LFxyXG4gIH0sXHJcbn0pXHJcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBOEwsU0FBUyxvQkFBb0I7QUFDM04sT0FBTyxXQUFXO0FBQ2xCLE9BQU8sVUFBVTtBQUZqQixJQUFNLG1DQUFtQztBQUt6QyxJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTLENBQUMsTUFBTSxDQUFDO0FBQUEsRUFDakIsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFBQSxFQUNBLFNBQVM7QUFBQSxJQUNMLE9BQU87QUFBQSxNQUNQLEtBQUssS0FBSyxRQUFRLGtDQUFXLE9BQU87QUFBQSxJQUN0QztBQUFBLEVBQ0Y7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo= -- GitLab