diff --git a/cosiap_api/dynamic_forms/migrations/0003_ajustes_tipos_respuestas.py b/cosiap_api/dynamic_forms/migrations/0003_ajustes_tipos_respuestas.py new file mode 100644 index 0000000000000000000000000000000000000000..64a725068bb7ea62fc08d56be12d6e89d4f7a2cf --- /dev/null +++ b/cosiap_api/dynamic_forms/migrations/0003_ajustes_tipos_respuestas.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.7 on 2024-08-28 17:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamic_forms', '0002_OneToOneRelation_RegistroFormulario'), + ] + + operations = [ + migrations.AlterField( + model_name='rnumerico', + name='valor', + field=models.FloatField(default=1), + preserve_default=False, + ), + ] diff --git a/cosiap_api/dynamic_forms/migrations/0004_registro_formulario_foreignkey.py b/cosiap_api/dynamic_forms/migrations/0004_registro_formulario_foreignkey.py new file mode 100644 index 0000000000000000000000000000000000000000..ecd7ea9a4a14fd6c35ee60bd6796c9a428eefcb2 --- /dev/null +++ b/cosiap_api/dynamic_forms/migrations/0004_registro_formulario_foreignkey.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.7 on 2024-08-28 17:24 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamic_forms', '0003_ajustes_tipos_respuestas'), + ] + + operations = [ + migrations.AlterField( + model_name='registroseccion', + name='registro_formulario', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dynamic_forms.registroformulario'), + ), + ] diff --git a/cosiap_api/dynamic_forms/models.py b/cosiap_api/dynamic_forms/models.py index 82b9643bfeb568f24caaf85dc85242d8597eb0a7..def86c1f9f17b03cca47a2360b17aa2152660e60 100644 --- a/cosiap_api/dynamic_forms/models.py +++ b/cosiap_api/dynamic_forms/models.py @@ -152,7 +152,7 @@ class RegistroSeccion(models.Model): Campos: -id (id) has ''' - registro_formulario = models.OneToOneField(RegistroFormulario, on_delete=models.CASCADE) + registro_formulario = models.ForeignKey(RegistroFormulario, on_delete=models.CASCADE) seccion = models.ForeignKey(Seccion, on_delete=models.CASCADE, null=False, blank=False) class Meta: @@ -183,21 +183,12 @@ class Respuesta(models.Model): unique_together = ('registro_seccion', 'elemento') @classmethod - def create_respuesta(cls, registro_seccion, elemento, valor=None, otro=None): - """ - Crea una nueva respuesta basada en el tipo de elemento. - """ - respuesta_class = cls.RESPUESTA_TYPES.get(elemento.tipo) + def create_respuesta(cls, **kwargs): + respuesta_class = cls.RESPUESTA_TYPES.get(kwargs['elemento'].tipo) if not respuesta_class: - raise ValidationError(f"No se puede encontrar un tipo de respuesta para el tipo de elemento {elemento.tipo}") - - respuesta = respuesta_class( - registro_seccion=registro_seccion, - elemento=elemento, - valor=valor, - otro=otro - ) + raise ValidationError(f"No se puede encontrar un tipo de respuesta para el tipo de elemento {kwargs['elemento'].tipo}") + respuesta = respuesta_class(**kwargs) respuesta.clean() respuesta.save() return respuesta @@ -206,12 +197,21 @@ class Respuesta(models.Model): """ Actualiza una respuesta existente con los nuevos valores proporcionados. """ - self.valor = valor - self.otro = otro - - self.clean() - self.save() - return self + # Asegurarte de que estás trabajando con la subclase correcta + instancia = self.__class__.objects.get_subclass(pk=self.pk) + + if hasattr(instancia, 'valor'): + print(f"Actualizando 'valor' de {instancia.valor} a {valor}") + instancia.valor = valor + + if hasattr(instancia, 'otro') and otro is not None: + print(f"Actualizando 'otro' de {instancia.otro} a {otro}") + instancia.otro = otro + + instancia.clean() + instancia.save() + return instancia + def clean(self): """ @@ -225,7 +225,7 @@ class Respuesta(models.Model): class RNumerico(Respuesta): - valor = models.CharField(max_length=255, null=True, blank=True) + valor = models.FloatField() def getStringValue(self): if self.valor is None: diff --git a/cosiap_api/dynamic_tables/views.py b/cosiap_api/dynamic_tables/views.py index 85895604267bf51ea476ce4fe1de87ad1a44f309..8dac577efd5adc78a2f80e130314a12b0b43e580 100644 --- a/cosiap_api/dynamic_tables/views.py +++ b/cosiap_api/dynamic_tables/views.py @@ -22,7 +22,7 @@ class DynamicTableAPIView(BasePermissionAPIView): ''' permission_classes_update = [IsAuthenticated, es_admin] - permission_classes_list = [AllowAny] + permission_classes_list = [IsAuthenticated, es_admin] permission_classes_create = [IsAuthenticated, es_admin] permission_classes_delete = [IsAuthenticated, es_admin] @@ -96,7 +96,7 @@ class DynamicTableAPIView(BasePermissionAPIView): if self.dynamic_form_exist: serializer = DynamicTableDynamicForm(model_class=self.model_class) else: - serializer = DynamicTable() + serializer = DynamicTable(model_class=self.model_class) instance_data = serializer.retrieve_instance_data(instance) return Response(instance_data, status=status.HTTP_200_OK) try: diff --git a/cosiap_api/solicitudes/respuestas_serializer.py b/cosiap_api/solicitudes/respuestas_serializer.py new file mode 100644 index 0000000000000000000000000000000000000000..d177d3c7c9bb7c4af6a077a1cbe98565e9459707 --- /dev/null +++ b/cosiap_api/solicitudes/respuestas_serializer.py @@ -0,0 +1,116 @@ +from rest_framework import serializers +from dynamic_forms.models import Respuesta, Elemento, RegistroFormulario, RegistroSeccion, RNumerico, RCasillas, RDesplegable, RDocumento, RFecha, RHora, ROpcionMultiple, RTextoParrafo, RTextoCorto + +class RespuestaSerializer(serializers.Serializer): + seccion_id = serializers.IntegerField() + elemento_id = serializers.IntegerField() + valor = serializers.CharField(max_length=255, required=False, allow_blank=True) + otro = serializers.CharField(max_length=255, required=False, allow_blank=True) + + def validate(self, data): + registro_formulario = self.context['registro_formulario'] + seccion_id = data.get('seccion_id') + elemento_id = data.get('elemento_id') + + try: + elemento = Elemento.objects.get(id=elemento_id) + except Elemento.DoesNotExist: + raise serializers.ValidationError("El elemento especificado no existe.") + + registro_seccion = RegistroSeccion.objects.filter( + registro_formulario=registro_formulario, + seccion_id=seccion_id + ).first() + + if not registro_seccion: + registro_seccion = RegistroSeccion.objects.create( + registro_formulario=registro_formulario, + seccion_id=seccion_id + ) + + data['registro_seccion'] = registro_seccion + data['elemento'] = elemento + + # Validación de los campos `valor` y `otro` + respuesta_class = Respuesta.RESPUESTA_TYPES.get(elemento.tipo) + if respuesta_class: + respuesta_instance = respuesta_class() + if hasattr(respuesta_instance, 'valor'): + if not self._validate_value(data.get('valor'), respuesta_instance): + raise serializers.ValidationError("El valor no cumple con el formato esperado.") + + if hasattr(respuesta_instance, 'otro'): + if not self._validate_value(data.get('otro'), respuesta_instance, field_name='otro'): + raise serializers.ValidationError("El campo 'otro' no cumple con el formato esperado.") + + return data + + def _validate_value(self, value, respuesta_instance, field_name='valor'): + """ + Valida el valor del campo según el tipo de campo en la instancia del modelo. + """ + if value is None or value == '': + return True + + if isinstance(respuesta_instance, RNumerico): + try: + value = float(value) + except ValueError: + return False + return isinstance(value, (int, float)) + + if isinstance(respuesta_instance, RTextoCorto) or isinstance(respuesta_instance, RTextoParrafo) or isinstance(respuesta_instance, RDesplegable): + return isinstance(value, str) + + if isinstance(respuesta_instance, RHora) or isinstance(respuesta_instance, RFecha): + return isinstance(value, str) + + if isinstance(respuesta_instance, ROpcionMultiple): + try: + value = json.loads(value) + return isinstance(value, list) + except (ValueError, json.JSONDecodeError): + return False + + if isinstance(respuesta_instance, RDocumento): + return hasattr(value, 'read') + return True + + + def create(self, validated_data): + registro_seccion = validated_data.get('registro_seccion') + elemento = validated_data.get('elemento') + valor = validated_data.get('valor', '') + otro = validated_data.get('otro', '') + + if hasattr(Respuesta, 'otro'): + otro = otro if elemento.opcion_otro else None + else: + otro = None + + try: + respuesta = Respuesta.objects.get(registro_seccion=registro_seccion, elemento=elemento) + respuesta.update_respuesta(valor=valor, otro=otro) + except Respuesta.DoesNotExist: + respuesta_data = { + 'registro_seccion': registro_seccion, + 'elemento': elemento, + 'valor': valor + } + if otro is not None: + respuesta_data['otro'] = otro + + respuesta = Respuesta.create_respuesta(**respuesta_data) + + return respuesta + + def update(self, instance, validated_data): + valor = validated_data.get('valor', instance.valor) + otro = validated_data.get('otro', instance.otro) + + if hasattr(instance, 'otro'): + instance.update_respuesta(valor=valor, otro=otro) + else: + instance.update_respuesta(valor=valor) + + return instance diff --git a/cosiap_api/solicitudes/serializer.py b/cosiap_api/solicitudes/serializer.py index 7723d4173b35bc6e1a086afab8096093b94382d7..749f74277b920f702c0e7d16f4bd28e8627a4fe8 100644 --- a/cosiap_api/solicitudes/serializer.py +++ b/cosiap_api/solicitudes/serializer.py @@ -27,6 +27,7 @@ class SolicitudSerializer(serializers.ModelSerializer): class Meta: model = Solicitud fields = [ + 'id', 'status', 'solicitud_n', 'minuta', diff --git a/cosiap_api/solicitudes/urls.py b/cosiap_api/solicitudes/urls.py index adeb962d8f6f27fc570aefbe7b42e7b2dc661d04..0d1eae7d6fa95ea684196ae318bfafdcdcf56fd1 100644 --- a/cosiap_api/solicitudes/urls.py +++ b/cosiap_api/solicitudes/urls.py @@ -5,9 +5,11 @@ from django.contrib.auth import views as auth_views app_name = 'solicitudes' urlpatterns = [ - path('', views.SolicitudAPIView.as_view(), name='solicitudes'), - path('/', views.SolicitudAPIView.as_view(), name='solicitudes_pk'), - path('historial/', views.HistorialAPIVIew.as_view(), name='historial'), + path('', views.SolicitudAPIView.as_view(), name='solicitudes'), # lista de solicitudes para el admin + path('/', views.SolicitudAPIView.as_view(), name='solicitudes_pk'), # detalle de solicitud del admin o edicion para el admin + path('solicitar/', views.SolicitarAPIView.as_view(), name='solicitar'), # crear nueva solicitud + path('solicitar//', views.SolicitarAPIView.as_view(), name='ver_editar_solicitud'), # se envía el pk de la solicitud a ver o a editar. + path('historial/', views.HistorialAPIVIew.as_view(), name='historial'), path('historial//', views.HistorialAPIVIew.as_view(), name='historial_pk'), path('reportes/', views.ReportesSolicitudesAPIView.as_view(), name='reportes_solicitudes'), path('reportes//', views.ReportesSolicitudesAPIView.as_view(), name='reportes_solicitudes_pk'), diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index 58193421a2c9f69f15ddd0abec1d02c51a15c086..f7d3d225fb021781cfddfa7c80446cbf804ad7e8 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -20,6 +20,12 @@ from rest_framework.permissions import AllowAny from dynamic_tables.models import DynamicTableReport from dynamic_tables.DynamicTable import DynamicTable from dynamic_tables.views import Exportar_CSV +from modalidades.models import Modalidad +from django.db import transaction +from .respuestas_serializer import RespuestaSerializer +from django.utils import timezone +from dynamic_forms.serializers import RespuestaFormularioSerializer +from dynamic_forms.models import Elemento, RegistroFormulario, RegistroSeccion, Respuesta class SolicitudAPIView(DynamicTableAPIView): ''' @@ -27,6 +33,8 @@ class SolicitudAPIView(DynamicTableAPIView): ''' + permission_classes_list = [AllowAny] + model_class = Solicitud model_name = 'Solicitud' columns = '__all__' @@ -36,9 +44,180 @@ class SolicitudAPIView(DynamicTableAPIView): 'gte': [(datetime.now() - timedelta(days=5*30)).strftime('%Y-%m-%d')] } } - non_editable_fields = ["status"] + non_editable_fields = ["id"] dynamic_form_exist = True +class SolicitarAPIView(BasePermissionAPIView): + ''' + Clase para manejar la lógica de la creación y edición de solicitudes + ''' + + permission_classes_create = [IsAuthenticated] + permission_classes_update = [IsAuthenticated] + permission_classes_list = [IsAuthenticated] + + + def get(self, request, *args, **kwargs): + ''' + Metodo GET para obtener el formulario a contestar de la modalidad + o la vista detallada de la solicitud (para solicitante) + ''' + + data = {} + + try: + solicitante = Solicitante.objects.get(id=request.user.id) + + if 'pk' in kwargs: + solicitud_id = kwargs['pk'] + try: + solicitud = Solicitud.objects.get(id=solicitud_id, solicitante=solicitante) + data["monto_solicitado"] = solicitud.monto_solicitado + form = RespuestaFormularioSerializer(solicitud, dynamic_form_source='modalidad__dynamic_form') + data["formulario"] = form.data + return Response(data, status=status.HTTP_200_OK) + + except Solicitud.DoesNotExist: + Mensaje.error(data, 'No tienes permiso para ver esta solicitud o no existe.') + return Response(data, status=status.HTTP_404_NOT_FOUND) + else: + modalidad_id = request.query_params.get('modalidad_id', None) + modalidad = Modalidad.objects.get(id=modalidad_id) + + # Crear una solicitud temporal + solicitud_temporal = Solicitud( + solicitante=solicitante, + modalidad=modalidad, + ) + + # Serializar el formulario basado en la modalidad + form = RespuestaFormularioSerializer(solicitud_temporal, dynamic_form_source='modalidad__dynamic_form') + form_data = form.data + return Response(form_data, status=status.HTTP_200_OK) + + except Modalidad.DoesNotExist: + Mensaje.error(data, 'Modalidad no encontrada.') + return Response(data, status=status.HTTP_404_NOT_FOUND) + + except Solicitante.DoesNotExist: + Mensaje.error(data, 'Solicitante no encontrado.') + return Response(data, status=status.HTTP_404_NOT_FOUND) + + except Exception as e: + Mensaje.error(data, str(e)) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + + + + def post(self, request, *args, **kwargs): + ''' + Método POST para la creación de una nueva solicitud contestando el formulario correspondiente + ''' + data = {} + + try: + user = request.user + solicitante = Solicitante.objects.get(id=user.id) + monto_solicitado = request.data.get("monto_solicitado") + modalidad = Modalidad.objects.get(id = request.data.get('modalidad_id')) + + año_actual = timezone.now().year + # Contar cuántas solicitudes ha hecho el solicitante en el año actual + solicitudes_del_año = Solicitud.objects.filter(solicitante=solicitante, timestamp__year=año_actual).count() + + if solicitudes_del_año >= 2: + Mensaje.error(data, 'No puedes registrar más de dos solicitudes en el mismo año.') + return Response(data, status=status.HTTP_400_BAD_REQUEST) + + with transaction.atomic(): + nueva_solicitud = Solicitud.objects.create( + solicitante=solicitante, + monto_solicitado=monto_solicitado, + modalidad=modalidad, + status='Pendiente' + ) + nueva_solicitud.solicitud_n = nueva_solicitud.id + nueva_solicitud.save() + registro_formulario = nueva_solicitud.registro_formulario + + # Procesar las respuestas enviadas + respuestas = request.data.get('respuestas', []) + for respuesta in respuestas: + serializer = RespuestaSerializer(data=respuesta, context={'registro_formulario': registro_formulario}) + if serializer.is_valid(raise_exception=True): + serializer.save() + + # Respuesta exitosa + Mensaje.success(data, 'Solicitud y respuestas registradas exitosamente.') + return Response(data, status=status.HTTP_201_CREATED) + + except Exception as e: + Mensaje.error(data, str(e)) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + + + def put(self, request, *args, **kwargs): + ''' + Método PUT para la edición de una solicitud existente + ''' + data = {} + + try: + user = request.user + solicitante = Solicitante.objects.get(id=user.id) + solicitud_id = kwargs['pk'] + monto_solicitado = request.data.get("monto_solicitado") + + try: + solicitud = Solicitud.objects.get(id=solicitud_id, solicitante=solicitante) + except Solicitud.DoesNotExist: + Mensaje.error(data, 'La solicitud no existe o no tienes permisos para editarla.') + return Response(data, status=status.HTTP_404_NOT_FOUND) + + if solicitud.status in ['Aprobado', 'Rechazado']: + Mensaje.error(data, f'No puedes realizar cambios en esta solicitud dado que su estatus es {solicitud.status}') + return Response(data, status=status.HTTP_400_BAD_REQUEST) + + with transaction.atomic(): + solicitud.monto_solicitado = monto_solicitado + solicitud.status = 'Pendiente' + solicitud.save() + + registro_formulario = solicitud.registro_formulario + + # Procesar las respuestas enviadas + respuestas = request.data.get('respuestas', []) + for respuesta in respuestas: + serializer = RespuestaSerializer(data=respuesta, context={'registro_formulario': registro_formulario}) + + if serializer.is_valid(raise_exception=True): + # Verificar si la respuesta ya existe + elemento_id = respuesta.get('elemento_id') + registro_seccion_id = respuesta.get('seccion_id') + try: + respuesta_instance = Respuesta.objects.get( + registro_seccion__registro_formulario=registro_formulario, + registro_seccion__seccion_id=registro_seccion_id, + elemento_id=elemento_id + ) + # Llamar al método `update` del serializer para actualizar la respuesta existente + serializer.update(respuesta_instance, serializer.validated_data) + except Respuesta.DoesNotExist: + # Llamar al método `create` del serializer para crear una nueva respuesta + serializer.save() + + # Respuesta exitosa + Mensaje.success(data, 'Solicitud actualizada exitosamente.') + return Response(data, status=status.HTTP_200_OK) + + except Solicitante.DoesNotExist: + Mensaje.error(data, 'Solicitante no encontrado.') + return Response(data, status=status.HTTP_404_NOT_FOUND) + + except Exception as e: + Mensaje.error(data, str(e)) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + class HistorialAPIVIew(BasePermissionAPIView): ''' diff --git a/cosiap_api/users/tests.py b/cosiap_api/users/tests.py index 57c1a138d90aae2d4fdc2b0b897a2755853b7e6c..ba72976566f25ff743b3513edc0f1bffc4f54f00 100644 --- a/cosiap_api/users/tests.py +++ b/cosiap_api/users/tests.py @@ -174,8 +174,7 @@ class SolicitanteTest(TestCase): url = reverse('users:solicitantes') response = self.client.post(url, data, format='multipart') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'messages': {'success': ['Acceso permitido.']}}) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_crear_solicitante_datos_incompletos(self): @@ -195,9 +194,7 @@ class SolicitanteTest(TestCase): url = reverse('users:solicitantes') response = self.client.post(url, data, format='multipart') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertIn('Este campo es requerido.', response.data['telefono'][0]) - self.assertIn('Este campo es requerido.', response.data['direccion'][0]) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_update_solicitante(self):