From c3adf0972c9a1868dc6b7d84dc8e4ef14dcd537e Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Mon, 29 Jul 2024 12:52:27 -0600 Subject: [PATCH 1/6] Solucion de omision de columnas no indicadas --- cosiap_api/cosiap_api/settings.py | 4 +- cosiap_api/dynamic_forms/models.py | 10 ++--- cosiap_api/dynamic_tables/DynamicTable.py | 17 +++----- cosiap_api/dynamic_tables/views.py | 2 +- .../0002_dynamic_form_modalidades.py | 20 +++++++++ cosiap_api/modalidades/models.py | 6 ++- cosiap_api/solicitudes/tests.py | 42 ++++++++++-------- cosiap_api/users/models.py | 2 +- cosiap_api/users/views.py | 43 +++++++++++-------- 9 files changed, 90 insertions(+), 56 deletions(-) create mode 100644 cosiap_api/modalidades/migrations/0002_dynamic_form_modalidades.py diff --git a/cosiap_api/cosiap_api/settings.py b/cosiap_api/cosiap_api/settings.py index f3b8a9c..969e309 100644 --- a/cosiap_api/cosiap_api/settings.py +++ b/cosiap_api/cosiap_api/settings.py @@ -136,7 +136,7 @@ TIME_ZONE = 'America/Mexico_City' USE_I18N = True -USE_TZ = True +USE_TZ = True #Email settings @@ -148,7 +148,7 @@ EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD') EMAIL_PORT = env('EMAIL_PORT') EMAIL_USE_TLS = env('EMAIL_USE_TLS') - + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.0/howto/static-files/ diff --git a/cosiap_api/dynamic_forms/models.py b/cosiap_api/dynamic_forms/models.py index a439c3e..80fd960 100644 --- a/cosiap_api/dynamic_forms/models.py +++ b/cosiap_api/dynamic_forms/models.py @@ -165,15 +165,13 @@ class Respuesta(models.Model): objects = InheritanceManager() def __str__(self): - return f"Respuesta {type(self)} - Elemento: {self.elemento} - Solicitante: {self.solicitante_id}" + return f"Respuesta {type(self)} - Elemento: {self.elemento} - Solicitante: {self.solicitud.solicitante_id}" def save(self, *args, **kwargs): if self._state.adding: - # Solo realiza la verificación si estás creando una respuesta nueva - if self.elemento.seccion.tipo == 'unico': - # Verificar si ya existe una respuesta para esta combinación - if Respuesta.objects.filter(elemento=self.elemento, solicitante=self.solicitante).exists(): - raise IntegrityError('Ya existe una respuesta para este elemento y solicitante') + if self.elemento.seccioneselementos_set.filter(seccion__tipo='unico').exists(): + 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) class Meta: diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index 03a5592..91110ff 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -13,6 +13,9 @@ import re from django.core.exceptions import ValidationError, FieldDoesNotExist import json + +exclude_pattern = re.compile(r'^id$|^password$|^last_login$|^created_at$|^updated_at$|^usuario_ptr$|^groups$|^user_permissions$', re.IGNORECASE) + class DynamicTable(serializers.ModelSerializer): ''' Clase equivalente a un serializer con la lógica del manejo de las tablas dinámicas @@ -141,7 +144,7 @@ class DynamicTable(serializers.ModelSerializer): if hasattr(field, 'verbose_name'): label = field.verbose_name.capitalize() else: - label = part.capitalize() # Use field name as label if verbose_name is not available + label = part.capitalize() filter_info = { 'campo': column, @@ -267,20 +270,17 @@ class DynamicTable(serializers.ModelSerializer): # Una vez el modelo sea visitado lo guardamos en la lista visited_models.add(model) fields = {} - - # Expresión regular para excluir columnas específicas - exclude_pattern = re.compile(r'id|password|last_login|created_at|updated_at|ptr|groups|permissions', re.IGNORECASE) for field in model._meta.get_fields(): # Verificar si el campo debe ser excluido - if exclude_pattern.search(field.name): + if exclude_pattern.fullmatch(field.name): continue if isinstance(field, ForeignKey): related_model = field.related_model related_fields = DynamicTable.get_model_fields(related_model, visited_models) for related_field_name, related_field_verbose_name in related_fields.items(): - if not exclude_pattern.search(related_field_name): + if not exclude_pattern.fullmatch(related_field_name): fields[f"{field.name}__{related_field_name}"] = f"{related_field_verbose_name}" elif isinstance(field, ManyToManyField): fields[field.name] = field.verbose_name @@ -359,11 +359,8 @@ class DynamicTable(serializers.ModelSerializer): :param data: Diccionario para almacenar los campos y sus valores. """ - # Expresión regular para excluir columnas específicas - exclude_pattern = re.compile(r'id|password|last_login|created_at|updated_at|ptr|groups|permissions', re.IGNORECASE) - for field in instance._meta.get_fields(): - if exclude_pattern.search(field.name) or isinstance(field, ManyToOneRel): + if exclude_pattern.fullmatch(field.name) or isinstance(field, ManyToOneRel): continue try: field_value = getattr(instance, field.name, None) diff --git a/cosiap_api/dynamic_tables/views.py b/cosiap_api/dynamic_tables/views.py index 1fc6632..b89824c 100644 --- a/cosiap_api/dynamic_tables/views.py +++ b/cosiap_api/dynamic_tables/views.py @@ -18,7 +18,7 @@ class DynamicTableAPIView(BasePermissionAPIView): ''' permission_classes_update = [IsAuthenticated, es_admin] - permission_classes_list = [IsAuthenticated, es_admin] + permission_classes_list = [AllowAny] permission_classes_create = [IsAuthenticated, es_admin] permission_classes_delete = [IsAuthenticated, es_admin] diff --git a/cosiap_api/modalidades/migrations/0002_dynamic_form_modalidades.py b/cosiap_api/modalidades/migrations/0002_dynamic_form_modalidades.py new file mode 100644 index 0000000..a8fd088 --- /dev/null +++ b/cosiap_api/modalidades/migrations/0002_dynamic_form_modalidades.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.7 on 2024-07-29 17:17 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamic_forms', '0002_creacion_modelos_formularios_dinamicos_02'), + ('modalidades', '0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes'), + ] + + operations = [ + migrations.AddField( + model_name='modalidad', + name='dynamic_form', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='dynamic_forms.dynamicform', verbose_name='Dynamic Form'), + ), + ] diff --git a/cosiap_api/modalidades/models.py b/cosiap_api/modalidades/models.py index f50cdef..b36adc0 100644 --- a/cosiap_api/modalidades/models.py +++ b/cosiap_api/modalidades/models.py @@ -7,7 +7,11 @@ from django.db.models.signals import pre_save from django.dispatch import receiver + +DynamicForm = 'dynamic_forms.DynamicForm' + class Modalidad(models.Model): + ''' Modelo que contiene la informacion de una modalidad. @@ -24,7 +28,7 @@ class Modalidad(models.Model): descripcion = models.TextField(verbose_name="Descripción", null=False) mostrar = models.BooleanField(default=True) archivado = models.BooleanField(default=False) - dynamic_form = None + dynamic_form = models.ForeignKey(DynamicForm, verbose_name="Dynamic Form", on_delete=models.SET_NULL, null=True) def __str__(self): return f'{self.nombre}' diff --git a/cosiap_api/solicitudes/tests.py b/cosiap_api/solicitudes/tests.py index 1c5644e..34211f0 100644 --- a/cosiap_api/solicitudes/tests.py +++ b/cosiap_api/solicitudes/tests.py @@ -6,10 +6,14 @@ from users.models import Usuario, Solicitante, Municipio from .models import Solicitud from django.utils import timezone from common import custom_tests as c_tests +import json +from django.core.files import File +import os +from django.conf import settings class PermisosSolicitudTests(c_tests.PermissionTestCase): - url_name = 'solicitudes:solicitud-list' + url_name = 'solicitudes:solicitudes' methods_responses = { 'get': { 'user': status.HTTP_403_FORBIDDEN, @@ -30,6 +34,7 @@ class SolicitudTests(TestCase): """ Clase de prueba de la lista de solicitudes usando DynamicTable """ + ine_file_path = os.path.join(settings.MEDIA_ROOT, 'protected_uploads/INE_files', 'test.png') def setUp(self): """Configurar el entorno de prueba""" @@ -60,23 +65,24 @@ class SolicitudTests(TestCase): if refresh_token: self.client.cookies['refresh_token'] = refresh_token.value - # Crear una instancia de Solicitante - self.solicitante = Solicitante.objects.create( - curp="CEVA020423HGRRZDA9", - email="ceva@example.com", - nombre="Adalberto", - ap_paterno="Evans", - ap_materno="Vargas", - telefono="1234567890", - RFC="CEVA0204237E4", - direccion="Calle Falsa 123", - codigo_postal="12345", - municipio= Municipio.objects.get(id=1), - poblacion="Test Poblacion", - datos_bancarios=None, # Asignar datos bancarios si es necesario - INE=None, # Asignar archivo de INE si es necesario - password="password" - ) + # Abre el archivo y asignarlo al campo INE + with open(self.ine_file_path, 'rb') as ine_file: + self.solicitante = Solicitante.objects.create( + curp="CEVA020423HGRRZDA9", + email="ceva@example.com", + nombre="Adalberto", + ap_paterno="Evans", + ap_materno="Vargas", + telefono="1234567890", + RFC="CEVA0204237E4", + direccion="Calle Falsa 123", + codigo_postal="12345", + municipio=Municipio.objects.get(id=1), + poblacion="Test Poblacion", + datos_bancarios=None, # Asignar datos bancarios si es necesario + INE=File(ine_file), # Asignar archivo de INE + password="password" + ) # Crear instancias de Solicitud usando la instancia de Solicitante self.solicitud1 = Solicitud.objects.create( diff --git a/cosiap_api/users/models.py b/cosiap_api/users/models.py index 3d6b01c..e512669 100644 --- a/cosiap_api/users/models.py +++ b/cosiap_api/users/models.py @@ -151,7 +151,7 @@ class Solicitante(Usuario): # campo de relacion uno a uno con los datos bancarios datos_bancarios = models.OneToOneField(DatosBancarios, verbose_name="Datos Bancarios",null=True, blank=True, on_delete=models.CASCADE ) # campo para la identificación oficial - INE = models.FileField(verbose_name='INE', upload_to= nombre_archivo_ine , null=True, blank=True) + INE = models.FileField(verbose_name='INE', upload_to= nombre_archivo_ine , null=True, blank=False) def __str__(self): return self.nombre diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index f3cb834..bd16a04 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -59,8 +59,6 @@ class CustomTokenRefreshView(TokenRefreshView): return response - -# Funcionalidad para crear un usuario en el sistema, ver sus datos o eliminarlo class UsuarioAPIView(BasePermissionAPIView): """ Clase Usuario para manejar las solicitudes de los usuarios básicos @@ -77,7 +75,6 @@ class UsuarioAPIView(BasePermissionAPIView): permission_classes_delete = [IsAuthenticated, es_admin] permission_classes_list = [IsAuthenticated, es_admin] - # Función para la obtención de la lista de usuarios def get( self, request, *args, **kwargs ): # si se quiere observar solo un usuario if 'pk' in kwargs: @@ -98,7 +95,6 @@ class UsuarioAPIView(BasePermissionAPIView): # retornamos la lista de usuarios return Response(serializer.data) - # Función para la creación del usuario def post( self, request, *args, **kwargs ): # obtenemos el email y la curp directamente del request email = request.data.get('email') @@ -140,8 +136,8 @@ class UsuarioAPIView(BasePermissionAPIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - # Función para la eliminación del usuario def delete( self, request, *args, **kwargs ): + response_data = {} # revisamos que el usuario que realizó el request tenga permisos self.check_object_permissions(request, request.user) # obtenemos la intancia del usuario a eliminar @@ -150,7 +146,8 @@ class UsuarioAPIView(BasePermissionAPIView): # eliminamos la intancia instance.delete() # devolvemos una respuesta sin contenido confirmando la eliminación - return Response({"message": {'success': 'Eliminación exitosa'}}, status=status.HTTP_204_NO_CONTENT) + Mensaje.success(response_data, 'Eliminación exitosa') + return Response(response_data, status=status.HTTP_204_NO_CONTENT) class SolicitanteAPIView(BasePermissionAPIView): @@ -193,6 +190,7 @@ class SolicitanteAPIView(BasePermissionAPIView): # Solicitud post, para manejar el registro de datos de solicitante por medio del serializer def post(self, request, *args, **kwargs): + response_data = {} # obtenemos al usuario del request usuario = request.user # creamos el solicitante @@ -214,21 +212,24 @@ class SolicitanteAPIView(BasePermissionAPIView): # Revisamos que los datos estén completos (debido a que son opcionales) if solicitante.datos_completos: # Permitimos el acceso - return Response({"message": {'success': 'Acceso permitido.'}}, status=status.HTTP_200_OK) + Mensaje.success(response_data, 'Acceso permitido.') + return Response(response_data, status=status.HTTP_200_OK) # Si no están completos, se solicita que se completen los datos - return Response({"message": {'error': 'Favor de completar sus datos.'}}, status=status.HTTP_200_OK) + Mensaje.error(response_data, 'Favor de completar sus datos.') + return Response(response_data, status=status.HTTP_200_OK) # Si hay errores en los datos, enviamos un bad request return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # Solicitud put para actualizar los datos del solicitante def put(self, request, *args, **kwargs): + response_data = {} # si se envía un ID, es porque un administrador quiere editar al usuario vía ID if 'pk' in kwargs: # extraemos al solicitante por el ID solicitante = get_object_or_404(Solicitante, pk=kwargs['pk']) if not (request.user.is_staff or (solicitante.id == request.user.id)): - # si el usuario no es admin o el id enviado no pertenece al solicitante, no esta autorizado para la edición por ID - return Response({"message": {'error': 'Usted no tiene permisos para realizar esta acción.'}}, status=status.HTTP_401_UNAUTHORIZED) + Mensaje.error(response_data, 'Usted no tiene permisos para realizar esta acción.') + return Response(response_data, status=status.HTTP_401_UNAUTHORIZED) else: # recuperamos el solicitante de la base de datos, enviando un error si no se encuentra solicitante = get_object_or_404(Solicitante, id=request.user.id) @@ -239,7 +240,8 @@ class SolicitanteAPIView(BasePermissionAPIView): # guardamos los cambios serializer.save() # enviamos el mensaje de la actualización de los datos - return Response({"message": {'success': 'Datos actualizados exitosamente.'}}, status=status.HTTP_200_OK) + Mensaje.success(response_data, 'Datos actualizados exitosamente.') + return Response( response_data, status=status.HTTP_200_OK) # si hay errores en los datos, regresamos un bad request return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -251,11 +253,13 @@ class ResetPassword(APIView): Tipo de solicitud: - GET (Recibe el email del usuario y envia un correo de restablecimiento) """ + # esta view la puede ejecutar cualquiera permission_classes = [AllowAny] # Solicitudes de tipo post en donde se enviará el email. def post(self, request, *args, **kwargs): + response_data = {} # extraemos el email de la solicitud email = request.data.get('email') # intentamos extraer el usuario al que le pertenece el email @@ -269,11 +273,13 @@ class ResetPassword(APIView): # enviamos el correo de restablecimiento enviar_correo_reset_password(email, uid, token) # confirmamos el envio del correo - return Response({"message": {'success': 'Correo de restablecimiento enviado.'}}, status=status.HTTP_200_OK) + Mensaje.success(response_data, 'Correo de restablecimiento enviado.') + return Response(response_data, status=status.HTTP_200_OK) # si el usuario no existe except Usuario.DoesNotExist: # enviamos un mensaje para indicar que el correo no esta registrado - return Response({"message": {'error': 'El correo no está registrado.'}}, status=status.HTTP_400_BAD_REQUEST) + Mensaje.error(response_data, 'El correo no está registrado.') + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) class NuevaPassword(APIView): @@ -294,6 +300,7 @@ class NuevaPassword(APIView): - uidb64 (el id del usuario que restablece su contraseña encriptado en base64) - token (el token de reestablecimiento de contraseña) """ + response_data = {} # intentamos extraer los datos del usuario try: # decodificamos el id recibido @@ -313,10 +320,12 @@ class NuevaPassword(APIView): # guardamos los cambios realizados usuario.save() # si todo salio bien, enviamos la confirmación - return Response({"message": {'success': 'La contraseña ha sido restablecida exitosamente.'}}, status=status.HTTP_200_OK) + Mensaje.success(response_data, 'La contraseña ha sido restablecida exitosamente.') + return Response(response_data, status=status.HTTP_200_OK) else: # si algo salió mal, indicamos que el enlace es inválido - return Response({"message": {'error': 'El enlace no es válido.'}}, status=status.HTTP_400_BAD_REQUEST) + Mensaje.error(response_data, 'El enlace no es válido.') + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) def enviar_correo_reset_password(email, uid, token): @@ -386,7 +395,7 @@ class VerificarCorreo(APIView): # guardamos los cambios usuario.save() # si es necesario, enviamos un mensaje de exito - return Response({"message": {'success': 'Cuenta verificada exitosamente.'}}, status=status.HTTP_200_OK) + return redirect(f'http://localhost:5173/auth?status=success&message=Cuenta verificada exitosamente.') else: # si algo sale mal, indicamos simplemente que el token no es válido - return Response({"message": {'error': 'El token de verificación es inválido o ha expirado.'}}, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file + return redirect(f'http://localhost:5173/auth?status=error&message=El token de verificación es inválido o ha expirado.') \ No newline at end of file -- GitLab From 06ca11a06f083b8c6d061dbaec1daba3c6631e7f Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Tue, 30 Jul 2024 13:27:11 -0600 Subject: [PATCH 2/6] Historial de apoyos del solicitante --- cosiap_api/dynamic_tables/DynamicTable.py | 2 +- .../dynamic_tables/DynamicTableDynamicForm.py | 6 + cosiap_api/dynamic_tables/views.py | 4 +- cosiap_api/solicitudes/serializer.py | 40 ++++++ cosiap_api/solicitudes/tests_historial.py | 125 ++++++++++++++++++ cosiap_api/solicitudes/urls.py | 1 + cosiap_api/solicitudes/views.py | 42 +++++- 7 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 cosiap_api/dynamic_tables/DynamicTableDynamicForm.py create mode 100644 cosiap_api/solicitudes/serializer.py create mode 100644 cosiap_api/solicitudes/tests_historial.py diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index 91110ff..a12804b 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -14,7 +14,7 @@ from django.core.exceptions import ValidationError, FieldDoesNotExist import json -exclude_pattern = re.compile(r'^id$|^password$|^last_login$|^created_at$|^updated_at$|^usuario_ptr$|^groups$|^user_permissions$', re.IGNORECASE) +exclude_pattern = re.compile(r'^id$|^password$|^last_login$|^created_at$|^updated_at$|^usuario_ptr$|^groups$|^user_permissions$|^dynamic_form$', re.IGNORECASE) class DynamicTable(serializers.ModelSerializer): ''' diff --git a/cosiap_api/dynamic_tables/DynamicTableDynamicForm.py b/cosiap_api/dynamic_tables/DynamicTableDynamicForm.py new file mode 100644 index 0000000..857fc91 --- /dev/null +++ b/cosiap_api/dynamic_tables/DynamicTableDynamicForm.py @@ -0,0 +1,6 @@ +from .DynamicTable import DynamicTable + +class DynamicTableDynamicForm(DynamicTable): + + def get_dynamictable_fields(self): + pass \ No newline at end of file diff --git a/cosiap_api/dynamic_tables/views.py b/cosiap_api/dynamic_tables/views.py index b89824c..ae09631 100644 --- a/cosiap_api/dynamic_tables/views.py +++ b/cosiap_api/dynamic_tables/views.py @@ -18,7 +18,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] @@ -83,14 +83,12 @@ class DynamicTableAPIView(BasePermissionAPIView): ''' response_data = {} - if pk is not None: # Recuperar la instancia y extraer los datos instance = get_object_or_404(self.model_class, pk=pk) serializer = DynamicTable() instance_data = serializer.retrieve_instance_data(instance) return Response(instance_data, status=status.HTTP_200_OK) - configuracion_reporte = self.get_configuracion_reporte(request) serializer = DynamicTable(instance=configuracion_reporte) data = serializer.get_data(configuracion_reporte) diff --git a/cosiap_api/solicitudes/serializer.py b/cosiap_api/solicitudes/serializer.py new file mode 100644 index 0000000..7723d41 --- /dev/null +++ b/cosiap_api/solicitudes/serializer.py @@ -0,0 +1,40 @@ +from rest_framework import serializers +from .models import Solicitud, Minuta, Convenio + +class MinutaSerializer(serializers.ModelSerializer): + ''' + Serializer para la minuta de una solicitud + ''' + class Meta: + model = Minuta + fields = ['archivo'] + +class ConvenioSerializer(serializers.ModelSerializer): + ''' + Serializer para el convenio de una solicitud + ''' + class Meta: + model = Convenio + fields = ['archivo'] + +class SolicitudSerializer(serializers.ModelSerializer): + ''' + Serializer para listar las solicitudes + ''' + minuta = MinutaSerializer(read_only=True) + convenio = ConvenioSerializer(read_only=True) + + class Meta: + model = Solicitud + fields = [ + 'status', + 'solicitud_n', + 'minuta', + 'convenio', + 'monto_solicitado', + 'monto_aprobado', + 'modalidad', + 'timestamp', + 'observacion', + 'solicitante' + ] \ No newline at end of file diff --git a/cosiap_api/solicitudes/tests_historial.py b/cosiap_api/solicitudes/tests_historial.py new file mode 100644 index 0000000..a7a2b2b --- /dev/null +++ b/cosiap_api/solicitudes/tests_historial.py @@ -0,0 +1,125 @@ +from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APIClient +from rest_framework import status +from users.models import Usuario, Solicitante, Municipio +from .models import Solicitud +from django.utils import timezone +from common import custom_tests as c_tests +import json +from django.core.files import File +import os +from django.conf import settings + + + +class HistorialAPIVIewTests(TestCase): + ine_file_path = os.path.join(settings.MEDIA_ROOT, 'protected_uploads/INE_files', 'test.png') + + def setUp(self): + """Configurar el entorno de prueba""" + self.client = APIClient() + self.usuario_data = { + 'curp': 'CEVA020423HGRRZDA8', + 'nombre': 'Adalberto', + 'email': 'adalc3488@gmail.com', + 'password': 'testpassword123' + } + self.usuario = Usuario.objects.create_user(**self.usuario_data) + self.usuario.is_active = True + self.usuario.is_staff = False + self.usuario.save() + + # Iniciar sesión + self.login_url = reverse('users:token_obtain') + response = self.client.post(self.login_url, { + 'curp': self.usuario_data['curp'], + 'password': self.usuario_data['password'] + }) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.access_token = response.data['access'] + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.access_token}') + + # Configurar cookie de refresh token + refresh_token = response.cookies.get('refresh_token') + if refresh_token: + self.client.cookies['refresh_token'] = refresh_token.value + + # Crear solicitante + with open(self.ine_file_path, 'rb') as ine_file: + self.solicitante = Solicitante.objects.create( + pk = self.usuario.pk, + password = 'testpassword123', + curp="CEVA020423HGRRZDA9", + email="ceva@example.com", + nombre="Adalberto", + ap_paterno="Evans", + ap_materno="Vargas", + telefono="1234567890", + RFC="CEVA0204237E4", + direccion="Calle Falsa 123", + codigo_postal="12345", + municipio=Municipio.objects.get(id=1), + poblacion="Test Poblacion", + datos_bancarios=None, + INE=File(ine_file), + is_active=True, + ) + self.solicitante.save() + + # Crear solicitudes + self.solicitud1 = Solicitud.objects.create( + solicitante=self.solicitante, + status="Pendiente", + solicitud_n="001", + monto_solicitado=1000, + monto_aprobado=800, + observacion="Observación 1", + ) + + self.solicitud2 = Solicitud.objects.create( + solicitante=self.solicitante, + status="Aprobado", + solicitud_n="002", + monto_solicitado=1500, + monto_aprobado=1200, + observacion="Observación 2", + ) + + + def test_get_historial(self): + """ + Probar que se pueden recuperar todas las solicitudes del historial con una solicitud GET + """ + url = reverse('solicitudes:historial') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) # Verificar que hay 2 solicitudes + + def test_get_historial_sin_solicitudes(self): + """ + Probar el caso donde no hay solicitudes en el historial + """ + Solicitud.objects.all().delete() # Borrar todas las solicitudes + url = reverse('solicitudes:historial') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(response.data, {'messages': {'error': ['No existen solicitudes en el historial.']}}) + + def test_get_historial_usuario_no_solicitante(self): + """ + Probar el caso donde el usuario no es un solicitante + """ + otro_usuario = Usuario.objects.create_user( + curp='ANOT009876HGRRZDA9', + nombre='OtroUsuario', + email='otro@example.com', + password='anotherpassword' + ) + refresh = RefreshToken.for_user(otro_usuario) + access_token = str(refresh.access_token) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {access_token}') + + url = reverse('solicitudes:historial') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) diff --git a/cosiap_api/solicitudes/urls.py b/cosiap_api/solicitudes/urls.py index 53e9b00..4d9efb2 100644 --- a/cosiap_api/solicitudes/urls.py +++ b/cosiap_api/solicitudes/urls.py @@ -7,4 +7,5 @@ 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'), ] \ No newline at end of file diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index 2b38431..922aa55 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -7,11 +7,18 @@ from .models import Solicitud from users.permisos import es_admin from rest_framework.permissions import IsAuthenticated from datetime import timedelta, datetime - +from common.views import BasePermissionAPIView +from users.models import Solicitante +from .models import Solicitud +from notificaciones.mensajes import Mensaje +from rest_framework.response import Response +from rest_framework import status +from .serializer import SolicitudSerializer class SolicitudAPIView(DynamicTableAPIView): ''' Clase para el manejo de la lista de solicitudes y la aplicación de sus filtros + ''' model_class = Solicitud @@ -23,4 +30,35 @@ class SolicitudAPIView(DynamicTableAPIView): 'gte': [(datetime.now() - timedelta(days=5*30)).strftime('%Y-%m-%d')] } } - non_editable_fields = ["status"] \ No newline at end of file + non_editable_fields = ["status"] + + +class HistorialAPIVIew(BasePermissionAPIView): + ''' + APIView con la funcionalidad para ver el historial de apoyos de un solicitante + ''' + + permission_classes_list = [IsAuthenticated] + + def get(self, request): + ''' + Método get para obtener la lista de solicitudes realizadas + ''' + + response_data = {} + + uid = request.user.pk + solicitante = Solicitante.objects.get(pk=uid) + + # obtenemos las solicitudes + + solicitudes = Solicitud.objects.filter(solicitante=solicitante) + + if not solicitudes: + Mensaje.error(response_data, 'No existen solicitudes en el historial.') + return Response(response_data, status=status.HTTP_204_NO_CONTENT) + + serializer = SolicitudSerializer(solicitudes, many=True) + return Response(serializer.data,status=status.HTTP_200_OK) + + -- GitLab From 2569e81cfd5702a9f8912897ff4e5b83015723a0 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Wed, 31 Jul 2024 13:04:04 -0600 Subject: [PATCH 3/6] Ver historial de un solicitante por PK --- cosiap_api/solicitudes/tests_historial.py | 23 +++- cosiap_api/solicitudes/urls.py | 1 + cosiap_api/solicitudes/views.py | 12 +- cosiap_api/users/admin_views.py | 68 +++++++----- cosiap_api/users/urls.py | 4 +- cosiap_api/users/views.py | 127 ++++------------------ 6 files changed, 97 insertions(+), 138 deletions(-) diff --git a/cosiap_api/solicitudes/tests_historial.py b/cosiap_api/solicitudes/tests_historial.py index a7a2b2b..3156fe4 100644 --- a/cosiap_api/solicitudes/tests_historial.py +++ b/cosiap_api/solicitudes/tests_historial.py @@ -10,7 +10,7 @@ import json from django.core.files import File import os from django.conf import settings - +from rest_framework_simplejwt.tokens import RefreshToken class HistorialAPIVIewTests(TestCase): @@ -123,3 +123,24 @@ class HistorialAPIVIewTests(TestCase): url = reverse('solicitudes:historial') response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_get_historial_admin(self): + ''' + Probamos el caso en el que un administrador recupera el historial de un solicitante + ''' + + user_admin = Usuario.objects.create_superuser( + curp='ANOT009876HGRRZDA9', + nombre='OtroUsuario', + email='otro@example.com', + password='anotherpassword', + ) + + refresh = RefreshToken.for_user(user_admin) + access_token = str(refresh.access_token) + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {access_token}') + + url = reverse('solicitudes:historial_pk',args=[self.solicitante.pk]) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) diff --git a/cosiap_api/solicitudes/urls.py b/cosiap_api/solicitudes/urls.py index 4d9efb2..f63713b 100644 --- a/cosiap_api/solicitudes/urls.py +++ b/cosiap_api/solicitudes/urls.py @@ -8,4 +8,5 @@ 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('historial//', views.HistorialAPIVIew.as_view(), name='historial_pk'), ] \ No newline at end of file diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index 922aa55..c3c4f4b 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -14,6 +14,7 @@ from notificaciones.mensajes import Mensaje from rest_framework.response import Response from rest_framework import status from .serializer import SolicitudSerializer +from django.shortcuts import get_object_or_404 class SolicitudAPIView(DynamicTableAPIView): ''' @@ -40,18 +41,21 @@ class HistorialAPIVIew(BasePermissionAPIView): permission_classes_list = [IsAuthenticated] - def get(self, request): + def get(self, request, *args, **kwargs ): ''' Método get para obtener la lista de solicitudes realizadas ''' response_data = {} - uid = request.user.pk - solicitante = Solicitante.objects.get(pk=uid) + if request.user.is_staff: + if 'pk' in kwargs: + solicitante = get_object_or_404(Solicitante, pk = kwargs['pk']) + else: + uid = request.user.pk + solicitante = Solicitante.objects.get(pk=uid) # obtenemos las solicitudes - solicitudes = Solicitud.objects.filter(solicitante=solicitante) if not solicitudes: diff --git a/cosiap_api/users/admin_views.py b/cosiap_api/users/admin_views.py index 5a93d66..eb1b4f5 100644 --- a/cosiap_api/users/admin_views.py +++ b/cosiap_api/users/admin_views.py @@ -4,61 +4,79 @@ from rest_framework.views import APIView from .serializers import AdminSerializer from .permisos import es_admin -from .models import Usuario +from .models import Usuario, Solicitante from .views import enviar_correo_verificacion from rest_framework import status from rest_framework.response import Response from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode from .tokens import account_activation_token from django.utils.encoding import force_bytes +from dynamic_tables.views import DynamicTableAPIView +from common.views import BasePermissionAPIView +from rest_framework.permissions import IsAuthenticated, AllowAny +from notificaciones.mensajes import Mensaje -# clase de APIView con el método post para la creación de el administrador -class AdminAPIView(APIView): - # permitimos que unicamente un administrador pueda crear otras cuentas de admin - permission_classes = [es_admin] - # método get para obtener la lista de administradores +class AdminAPIView(BasePermissionAPIView): + ''' + Clase de APIView para el manejo de los usuarios administradores + ''' + permission_classes_list = [IsAuthenticated, es_admin] + permission_classes_create = [IsAuthenticated, es_admin] + + def get(self, request, *args, **kwargs): - # indicamos el query set de todos los usuarios + ''' + Método get para obtener la lista de los administradores del sistema + ''' queryset = Usuario.objects.filter(is_staff=True) - # indicamos el serializer a utilizar y enviamos el queryset serializer = AdminSerializer(queryset, many=True) - # retornamos la lista de usuarios return Response(serializer.data) - # Método post para realziar el registro del nuevo admin def post(self, request, *args, **kwargs): - # obtenemos el email del request + ''' + Método post para la creación de un nuevo usuario administrador. + ''' + + response_data = {} email = request.data.get("email") - # verificamos que el email no esté repetido try: email_exist = Usuario.objects.get(email=email) except Usuario.DoesNotExist: email_exist = None - # si no hay datos duplicado procedemos con el registro del admin if email_exist is None: - # inicializamos el serializer de admin serializer = AdminSerializer(data=request.data) - # verificamos que los datos sean válidos if serializer.is_valid(): - # creamos la instancia del nuevo usuario como inactivo usuario_nuevo = serializer.save(is_active = False) - # incluimos la contraseña del usuario usuario_nuevo.set_password(request.data.get('password')) - # extraemos el id del usuario y lo decodificacmos a base64 uid = urlsafe_base64_encode(force_bytes(usuario_nuevo.pk)) - # creamos el token asociado al usuario token = account_activation_token.make_token(usuario_nuevo) - # enviamos el correo de verificación con el token enviar_correo_verificacion(usuario_nuevo.email, uid, token) - # retornamos la respuesta de creación - response_data = {'data': serializer.data, 'message': {'success': 'Creación del administrador exitosa.'}} + Mensaje.success(response_data, 'Creación del administrador exitosa.') return Response(response_data, status=status.HTTP_201_CREATED) - # en caso de que los datos sean incorrectos, retornamos un error. return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) else: - # en caso de que algún dato este repetido lo indicamos por un mensaje - response_data = {'message': {'error': 'Este email ya esta en uso por otro usuario.'}} + Mensaje.error(response_data, 'Este email ya esta en uso por otro usuario.') return Response(response_data, status = status.HTTP_400_BAD_REQUEST) + +class UsuarioTableAPIView(DynamicTableAPIView): + ''' + Clase con la funcionalidad de tabla dinámica de usuarios + ''' + + model_class = Usuario + model_name = 'Usuario' + columns = '__all__' + + +class SolicitanteTableAPIView(DynamicTableAPIView): + ''' + Clase con la funcionalidad de tabla dinámica de solicitantes + ''' + + model_class = Solicitante + model_name = 'Solicitante' + columns = '__all__' + \ No newline at end of file diff --git a/cosiap_api/users/urls.py b/cosiap_api/users/urls.py index d58e6e4..2569643 100644 --- a/cosiap_api/users/urls.py +++ b/cosiap_api/users/urls.py @@ -2,7 +2,7 @@ from . import views from django.urls import path from django.contrib.auth import views as auth_views from .views import CustomTokenObtainPairView, CustomTokenRefreshView -from .admin_views import AdminAPIView +from .admin_views import AdminAPIView, UsuarioTableAPIView, SolicitanteTableAPIView app_name = 'users' @@ -18,4 +18,6 @@ urlpatterns = [ path('restablecer-password/', views.ResetPassword.as_view(), name='reset_password'), path('nueva-password///', views.NuevaPassword.as_view(), name='nueva_password'), path('administradores/', AdminAPIView.as_view() , name = 'administrador_list_create'), + path('tabla_usuarios/', UsuarioTableAPIView.as_view() , name = 'usuario_dynamic_table'), + path('tabla_solicitantes/', SolicitanteTableAPIView.as_view() , name = 'solicitante_dynamic_table'), ] \ No newline at end of file diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index bd16a04..7216d5f 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -76,76 +76,52 @@ class UsuarioAPIView(BasePermissionAPIView): permission_classes_list = [IsAuthenticated, es_admin] def get( self, request, *args, **kwargs ): - # si se quiere observar solo un usuario + ''' + Método get para obtener la lista de usuarios o bien obtener a un usuario en específico + ''' if 'pk' in kwargs: - self.check_object_permissions(request, request.user) - # obtenemos la instancia del usario instance = get_object_or_404(Usuario, pk=kwargs['pk']) - self.check_object_permissions(request, instance) - # indicamos el serializer a utilizar y enviamos la instancia serializer = UsuarioSerializer(instance) - # devolvemos los datos del usuario return Response(serializer.data) - # si se desea ver la lista completa else: - # indicamos el query set de todos los usuarios queryset = Usuario.objects.all() - # indicamos el serializer a utilizar y enviamos el queryset serializer = UsuarioSerializer(queryset, many=True) - # retornamos la lista de usuarios return Response(serializer.data) def post( self, request, *args, **kwargs ): - # obtenemos el email y la curp directamente del request + ''' + Método post para la creación de un nuevo usuario + ''' email = request.data.get('email') curp = request.data.get('curp') - # filtramos en la base de datos para obtener al usuario con la misma curp curp_exist = Usuario.objects.filter(curp=curp, is_active=False) - # filtramos en la base de datos para obtener al usuario con el mismo email email_exist = Usuario.objects.filter(email=email, is_active=False).exclude(curp=curp) - # si la curp existe if curp_exist: - # extraemos el usuario asociado a ella curp_exist = Usuario.objects.get(curp=curp) else: - # en caso de que no exista dejamos curp_exist como None para que el serializer inicie una instancia vacia curp_exist = None - # si el email existe if email_exist: - # eliminamos el usuario asociado a el - email_exist = Usuario.objects.get(email=email).delete() - # sobreescribimos el serializer enviando la instancia encontrada + email_exist = Usuario.objects.get(email=email).delete() serializer = UsuarioSerializer(data=request.data, instance=curp_exist) - # verificamos los datos enviados if serializer.is_valid(): - # creamos la instancia del nuevo usuario como inactivo usuario_nuevo = serializer.save(is_active = False) - # incluimos la contraseña del usuario usuario_nuevo.set_password(request.data.get('password')) - # extraemos el id del usuario y lo decodificacmos a base64 uid = urlsafe_base64_encode(force_bytes(usuario_nuevo.pk)) - # creamos el token asociado al usuario token = account_activation_token.make_token(usuario_nuevo) - # enviamos el correo de verificación con el token enviar_correo_verificacion(usuario_nuevo.email, uid, token) - # retornamos la respuesta de creación response_data = {'data': serializer.data} Mensaje.success(response_data, 'Usuario creado exitosamente.') return Response(response_data, status=status.HTTP_201_CREATED) - # en caso de que los datos sean incorrectos, retornamos un error. return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def delete( self, request, *args, **kwargs ): + ''' + Método delete para la eliminación de un usuario + ''' response_data = {} - # revisamos que el usuario que realizó el request tenga permisos - self.check_object_permissions(request, request.user) - # obtenemos la intancia del usuario a eliminar instance = get_object_or_404(Usuario, pk=kwargs['pk']) - self.check_object_permissions(request, instance) - # eliminamos la intancia instance.delete() - # devolvemos una respuesta sin contenido confirmando la eliminación Mensaje.success(response_data, 'Eliminación exitosa') return Response(response_data, status=status.HTTP_204_NO_CONTENT) @@ -162,38 +138,29 @@ class SolicitanteAPIView(BasePermissionAPIView): Herencia: - BasePermissionAPIView (Heréda de la clase con los permisos predefinidos) """ - # Indicamos que solo los usuarios logeados puedan acceder a esta función permission_classes_create = [IsAuthenticated, primer_login] permission_classes_list = [IsAuthenticated, es_admin] permission_classes_update = [IsAuthenticated, primer_login] - # Petición get para listar a los solicitantes def get(self, request, *args, **kwargs): - # si se quiere observar solo un solicitante + ''' + Método get para obtener la lista de solicitantes + ''' if 'pk' in kwargs: - self.check_object_permissions(request, request.user) - # obtenemos la instancia del solicitante instance = get_object_or_404(Solicitante, pk=kwargs['pk']) - self.check_object_permissions(request, instance) - # indicamos el serializer a utilizar y enviamos la instancia serializer = SolicitanteSerializer(instance) - # devolvemos los datos del usuario return Response(serializer.data) - # si se desea ver la lista completa else: - # indicamos el query set de todos los usuarios queryset = Solicitante.objects.all() - # indicamos el serializer a utilizar y enviamos el queryset serializer = SolicitanteSerializer(queryset, many=True) - # retornamos la lista de usuarios return Response(serializer.data) - # Solicitud post, para manejar el registro de datos de solicitante por medio del serializer def post(self, request, *args, **kwargs): + ''' + Método post para la creación de un nuevo solicitante + ''' response_data = {} - # obtenemos al usuario del request usuario = request.user - # creamos el solicitante solicitante, created = Solicitante.objects.get_or_create( id=usuario.id, defaults={ @@ -204,45 +171,33 @@ class SolicitanteAPIView(BasePermissionAPIView): 'password': usuario.password } ) - # Verificar que los datos del solicitante estén completos serializer = SolicitanteSerializer(instance= solicitante, data=request.data) if serializer.is_valid(): - # Guardamos los datos del solicitante serializer.save() - # Revisamos que los datos estén completos (debido a que son opcionales) if solicitante.datos_completos: - # Permitimos el acceso Mensaje.success(response_data, 'Acceso permitido.') return Response(response_data, status=status.HTTP_200_OK) - # Si no están completos, se solicita que se completen los datos Mensaje.error(response_data, 'Favor de completar sus datos.') return Response(response_data, status=status.HTTP_200_OK) - # Si hay errores en los datos, enviamos un bad request return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - # Solicitud put para actualizar los datos del solicitante def put(self, request, *args, **kwargs): + ''' + Método put para la edición de un solicitante. + ''' response_data = {} - # si se envía un ID, es porque un administrador quiere editar al usuario vía ID if 'pk' in kwargs: - # extraemos al solicitante por el ID solicitante = get_object_or_404(Solicitante, pk=kwargs['pk']) if not (request.user.is_staff or (solicitante.id == request.user.id)): Mensaje.error(response_data, 'Usted no tiene permisos para realizar esta acción.') return Response(response_data, status=status.HTTP_401_UNAUTHORIZED) else: - # recuperamos el solicitante de la base de datos, enviando un error si no se encuentra solicitante = get_object_or_404(Solicitante, id=request.user.id) - # inicializamos el serializer con los datos precargados serializer = SolicitanteSerializer(instance=solicitante, data=request.data) - # si el serilizer es valido if serializer.is_valid(): - # guardamos los cambios serializer.save() - # enviamos el mensaje de la actualización de los datos Mensaje.success(response_data, 'Datos actualizados exitosamente.') return Response( response_data, status=status.HTTP_200_OK) - # si hay errores en los datos, regresamos un bad request return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -253,31 +208,19 @@ class ResetPassword(APIView): Tipo de solicitud: - GET (Recibe el email del usuario y envia un correo de restablecimiento) """ - - # esta view la puede ejecutar cualquiera permission_classes = [AllowAny] - # Solicitudes de tipo post en donde se enviará el email. def post(self, request, *args, **kwargs): response_data = {} - # extraemos el email de la solicitud email = request.data.get('email') - # intentamos extraer el usuario al que le pertenece el email try: - # extraemos al ususario por medio del email usuario = Usuario.objects.get(email=email) - # creamos un token para el restablecimiento de la contraseña token = token_generator.make_token(usuario) - # obtenemos el user id y lo codificamos a base64 uid = urlsafe_base64_encode(force_bytes(usuario.pk)) - # enviamos el correo de restablecimiento enviar_correo_reset_password(email, uid, token) - # confirmamos el envio del correo Mensaje.success(response_data, 'Correo de restablecimiento enviado.') return Response(response_data, status=status.HTTP_200_OK) - # si el usuario no existe except Usuario.DoesNotExist: - # enviamos un mensaje para indicar que el correo no esta registrado Mensaje.error(response_data, 'El correo no está registrado.') return Response(response_data, status=status.HTTP_400_BAD_REQUEST) @@ -290,7 +233,7 @@ class NuevaPassword(APIView): - POST (Se envía la nueva contraseña y se le asigna al usuario una vez confirme el restablecimiento por correo) """ permission_classes = [AllowAny] - # método post donde se enviará las nueva password + def post(self, request, uidb64, token): """ Método post para guardar la nueva contraseña @@ -301,29 +244,18 @@ class NuevaPassword(APIView): - token (el token de reestablecimiento de contraseña) """ response_data = {} - # intentamos extraer los datos del usuario try: - # decodificamos el id recibido uid = urlsafe_base64_decode(uidb64).decode() - # extraemos al usuario de la base de datos usuario = Usuario.objects.get(pk=uid) - # en caso de que el usuario no se encuentre except (TypeError, ValueError, OverflowError, Usuario.DoesNotExist): - # se setea a None usuario = None - # verificamos que el usuario exista y que el token enviado pertenezca a el if usuario is not None and token_generator.check_token(usuario, token): - # extraemos la nueva password de la solicitud password = request.data.get('password') - # asignamos la nueva contraseña al usuario usuario.password = make_password(password) - # guardamos los cambios realizados usuario.save() - # si todo salio bien, enviamos la confirmación Mensaje.success(response_data, 'La contraseña ha sido restablecida exitosamente.') return Response(response_data, status=status.HTTP_200_OK) else: - # si algo salió mal, indicamos que el enlace es inválido Mensaje.error(response_data, 'El enlace no es válido.') return Response(response_data, status=status.HTTP_400_BAD_REQUEST) @@ -337,15 +269,10 @@ def enviar_correo_reset_password(email, uid, token): - uid (id del usuario) - token (token de restablecimiento de contraseña) """ - # título del correo subject = 'Restablecer contraseña' - # Cuerpo del correo electrónico message = f'Para restablecer tu contraseña, haz click en confirmar:\n\n{settings.BASE_URL}api/usuarios/nueva-password/{uid}/{token}/' - # correo remitente from_email = settings.EMAIL_HOST_USER - # indicamos el correo o correos destinatarios recipient_list = [email] - # enviamos el email de restablecimiento de contraseña send_mail(subject, message, from_email, recipient_list) @@ -358,15 +285,10 @@ def enviar_correo_verificacion(email, uid, token): - uid (id del usuario) - token (token de verificación de correo) """ - # tema o título del correo subject = 'Verificación de la cuenta' - # cuerpo del correo con el enlace de verificación, que incluye el token y el id del usuario message = f'Para activar tu cuenta, haz clic en el siguiente enlace:\n\n{settings.BASE_URL}api/usuarios/verificar-correo/{uid}/{token}/' - # extraemos el correo remitente desde la configuración del settings from_email = settings.EMAIL_HOST_USER - # indicamos el correo o correos destinatarios recipient_list = [email] - # enviamos el email de verificación send_mail(subject, message, from_email, recipient_list) class VerificarCorreo(APIView): @@ -379,23 +301,14 @@ class VerificarCorreo(APIView): permission_classes = [AllowAny] def get(self, request, uidb64, token ): - # Si el usuario asociado al token existe try: - # extraemos el id del usuario presente en el token uid = force_str(urlsafe_base64_decode(uidb64)) - # extraemos el usuario de la base de datos usuario = Usuario.objects.get(pk=uid) except (TypeError, ValueError, OverflowError, Usuario.DoesNotExist): - # si el usuario no existe lo seteamos a None usuario = None - # Utilizamos la función check_token() para verificar que el token sea correcto y pertenezca al usuario if usuario is not None and account_activation_token.check_token(usuario, token): - # activamos la cuenta del usuario usuario.is_active = True - # guardamos los cambios usuario.save() - # si es necesario, enviamos un mensaje de exito return redirect(f'http://localhost:5173/auth?status=success&message=Cuenta verificada exitosamente.') else: - # si algo sale mal, indicamos simplemente que el token no es válido return redirect(f'http://localhost:5173/auth?status=error&message=El token de verificación es inválido o ha expirado.') \ No newline at end of file -- GitLab From c319fdbadf3337032d8f025636921e29fde80014 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Thu, 1 Aug 2024 13:31:13 -0600 Subject: [PATCH 4/6] =?UTF-8?q?Refactorizaci=C3=B3n=20de=20modulos=20de=20?= =?UTF-8?q?usuarios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/cosiap_api/.env | 4 +- cosiap_api/users/admin_views.py | 25 +--- cosiap_api/users/models.py | 7 +- cosiap_api/users/tests.py | 236 ++++++++++++++++++++++++++++++++ cosiap_api/users/urls.py | 12 +- cosiap_api/users/views.py | 73 ++-------- 6 files changed, 257 insertions(+), 100 deletions(-) diff --git a/cosiap_api/cosiap_api/.env b/cosiap_api/cosiap_api/.env index b5782c3..b225e67 100644 --- a/cosiap_api/cosiap_api/.env +++ b/cosiap_api/cosiap_api/.env @@ -11,8 +11,8 @@ DATABASES_DEFAULT_PORT="3306" EMAIL_HOST="sandbox.smtp.mailtrap.io" EMAIL_FROM="cosiap@example.com" -EMAIL_HOST_USER="2456f598d9da38" -EMAIL_HOST_PASSWORD="2c2051e768ff8c" +EMAIL_HOST_USER="3b48193365f615" +EMAIL_HOST_PASSWORD="37f89fc1d98f48" EMAIL_PORT="2525" EMAIL_USE_TLS=True diff --git a/cosiap_api/users/admin_views.py b/cosiap_api/users/admin_views.py index eb1b4f5..2fcbbd5 100644 --- a/cosiap_api/users/admin_views.py +++ b/cosiap_api/users/admin_views.py @@ -11,7 +11,6 @@ from rest_framework.response import Response from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode from .tokens import account_activation_token from django.utils.encoding import force_bytes -from dynamic_tables.views import DynamicTableAPIView from common.views import BasePermissionAPIView from rest_framework.permissions import IsAuthenticated, AllowAny from notificaciones.mensajes import Mensaje @@ -57,26 +56,4 @@ class AdminAPIView(BasePermissionAPIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) else: Mensaje.error(response_data, 'Este email ya esta en uso por otro usuario.') - return Response(response_data, status = status.HTTP_400_BAD_REQUEST) - - - -class UsuarioTableAPIView(DynamicTableAPIView): - ''' - Clase con la funcionalidad de tabla dinámica de usuarios - ''' - - model_class = Usuario - model_name = 'Usuario' - columns = '__all__' - - -class SolicitanteTableAPIView(DynamicTableAPIView): - ''' - Clase con la funcionalidad de tabla dinámica de solicitantes - ''' - - model_class = Solicitante - model_name = 'Solicitante' - columns = '__all__' - \ No newline at end of file + return Response(response_data, status = status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/cosiap_api/users/models.py b/cosiap_api/users/models.py index e512669..0e90249 100644 --- a/cosiap_api/users/models.py +++ b/cosiap_api/users/models.py @@ -162,9 +162,4 @@ class Solicitante(Usuario): # campos requeridos required_fields = ['ap_paterno', 'telefono', 'RFC', 'direccion', 'codigo_postal', 'municipio', 'poblacion', 'INE'] # verificamos que cada uno de los campos este lleno - for field in required_fields: - if not getattr(self, field): - # si se encuentra un dato vacío se retorna falso - return False - # si todos los datos estan llenos, retornamos un True - return True + return all(getattr(self, field) for field in required_fields) diff --git a/cosiap_api/users/tests.py b/cosiap_api/users/tests.py index e69de29..57c1a13 100644 --- a/cosiap_api/users/tests.py +++ b/cosiap_api/users/tests.py @@ -0,0 +1,236 @@ +from django.test import TestCase +from rest_framework.test import APIClient +from django.urls import reverse +from rest_framework import status +from .models import Usuario, Solicitante, Municipio +from django.core.files.uploadedfile import SimpleUploadedFile +import os +from django.conf import settings +from rest_framework_simplejwt.tokens import RefreshToken + +class UsuarioTest(TestCase): + ''' + Clase para probar la funcionalidad de usuarios de la API. + ''' + + def setUp(self): + ''' + Configuración del entorno de prueba + ''' + + self.client = APIClient() + self.usuario_data = { + 'curp': 'CEVA020423HGRRZDA8', + 'nombre': 'Adalberto', + 'email': 'adalc3488@gmail.com', + 'password': 'testpassword123' + } + self.usuario = Usuario.objects.create_superuser(**self.usuario_data) + self.usuario.is_active = True + self.usuario.is_staff = True + self.usuario.save() + + self.login_url = reverse('users:token_obtain') + response = self.client.post(self.login_url, { + 'curp': self.usuario_data['curp'], + 'password': self.usuario_data['password'] + }) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.access_token = response.data['access'] + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.access_token}') + + refresh_token = response.cookies.get('refresh_token') + if refresh_token: + self.client.cookies['refresh_token'] = refresh_token.value + + def test_crear_usuario(self): + ''' + Probamos que la creación de un usuario se realice de manera correcta. + ''' + + data = { + 'curp':'CEVA020223HGRRZDA8', + 'nombre':'OtroUsuario', + 'email':'34152734@uaz.edu.mx', + 'password':'anotherpassword', + 'confirmar_password':'anotherpassword' + } + + url = reverse('users:usuarios') + + response = self.client.post(url, data, format = 'json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + + def test_get_usuarios(self): + ''' + Probamos la funcion de la obtencion de la tabla de usuarios + ''' + + url = reverse('users:usuarios') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data['data']), 1) + + def test_get_usuario_1(self): + ''' + Probamos la funcionalidad para obtener un usuario por pk + ''' + + url = reverse('users:usuarios_pk', args= [self.usuario.pk]) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['curp'], 'CEVA020423HGRRZDA8') + + def test_update_usuario(self): + ''' + Probamos la edición de un usuario + ''' + + url = reverse('users:usuarios_pk', args= [self.usuario.pk]) + data = { + "field_updates":{ + "nombre": "Adalberto Cerrillo V." + } + } + response = self.client.put(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.usuario.refresh_from_db() + self.assertEqual(self.usuario.nombre, 'Adalberto Cerrillo V.') + + + def test_delete_usuario(self): + ''' + Probamos la eliminación de un usuario + ''' + + url = reverse('users:usuarios_pk', args= [self.usuario.pk]) + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + +class SolicitanteTest(TestCase): + ''' + Clase de prueba para las funcionalidades del solicitante + ''' + def setUp(self): + ''' + Configuración del entorno de prueba + ''' + + + + self.client = APIClient() + self.usuario_data = { + 'curp': 'CEVA020423HGRRZDA8', + 'nombre': 'Adalberto', + 'email': 'adalc3488@gmail.com', + 'password': 'testpassword123' + } + self.usuario = Usuario.objects.create_user(**self.usuario_data) + self.usuario.is_active = True + self.usuario.save() + + self.login_url = reverse('users:token_obtain') + response = self.client.post(self.login_url, { + 'curp': self.usuario_data['curp'], + 'password': self.usuario_data['password'] + }) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.access_token = response.data['access'] + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.access_token}') + + refresh_token = response.cookies.get('refresh_token') + if refresh_token: + self.client.cookies['refresh_token'] = refresh_token.value + + ine_file_path = os.path.join(settings.MEDIA_ROOT, 'protected_uploads/INE_files', 'test.png') + with open(ine_file_path, 'rb') as ine_file: + ine_file_data = ine_file.read() + + self.ine_uploaded_file = SimpleUploadedFile( + name='test.png', + content=ine_file_data, + content_type='image/png' + ) + + + def test_crear_solicitante_datos_completos(self): + ''' + Prueba de la creación correcta de un solicitante + ''' + + data = { + "ap_paterno": "Evans", + "ap_materno": "Vargas", + "telefono": "1234567890", + "RFC": "CEVA0204237E4", + "direccion": "Calle Falsa 123", + "codigo_postal": "12345", + "municipio": Municipio.objects.get(pk=1).pk, + "poblacion": "Test Poblacion", + "INE": self.ine_uploaded_file, + } + + 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.']}}) + + + def test_crear_solicitante_datos_incompletos(self): + ''' + Prueba del comportamiento de la creación en caso de datos incompletos + ''' + + data = { + "ap_paterno": "Evans", + "ap_materno": "Vargas", + "RFC": "CEVA0204237E4", + "codigo_postal": "12345", + "municipio": Municipio.objects.get(pk=1).pk, + "poblacion": "Test Poblacion", + "INE": self.ine_uploaded_file, + } + + 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]) + + + def test_update_solicitante(self): + ''' + Probar la funcionalidad de atualizacion de un solicitante + ''' + + solicitante = Solicitante.objects.create( + pk= self.usuario.pk, + curp=self.usuario.curp, + email=self.usuario.email, + nombre=self.usuario.nombre, + is_active=True, + ap_paterno="Evans", + ap_materno="Vargas", + telefono="1234567890", + RFC="CEVA0204237E4", + direccion="Calle Falsa 123", + codigo_postal="12345", + municipio=Municipio.objects.get(id=1), + poblacion="Test Poblacion", + INE=self.ine_uploaded_file, + password=self.usuario.password + ) + solicitante.save() + + url = reverse('users:solicitantes_pk', args= [solicitante.pk]) + data = { + "field_updates":{ + "nombre": "Adalberto Cerrillo V." + } + } + response = self.client.put(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + solicitante.refresh_from_db() + self.assertEqual(solicitante.nombre, 'Adalberto Cerrillo V.') \ No newline at end of file diff --git a/cosiap_api/users/urls.py b/cosiap_api/users/urls.py index 2569643..4582f5f 100644 --- a/cosiap_api/users/urls.py +++ b/cosiap_api/users/urls.py @@ -2,7 +2,7 @@ from . import views from django.urls import path from django.contrib.auth import views as auth_views from .views import CustomTokenObtainPairView, CustomTokenRefreshView -from .admin_views import AdminAPIView, UsuarioTableAPIView, SolicitanteTableAPIView +from .admin_views import AdminAPIView app_name = 'users' @@ -10,14 +10,12 @@ urlpatterns = [ path('token/', CustomTokenObtainPairView.as_view(), name='token_obtain'), path('token/refresh/', CustomTokenRefreshView.as_view(), name='token_refresh'), - path('', views.UsuarioAPIView.as_view(), name = 'usuario_list_create'), - path('/', views.UsuarioAPIView.as_view(), name = 'usuario_get_delete'), - path('solicitantes/', views.SolicitanteAPIView.as_view(), name = 'solicitante_list_create'), - path('solicitantes/', views.SolicitanteAPIView.as_view(), name = 'solicitante_get'), + path('', views.UsuarioAPIView.as_view(), name = 'usuarios'), + path('/', views.UsuarioAPIView.as_view(), name = 'usuarios_pk'), + path('solicitantes/', views.SolicitanteAPIView.as_view(), name = 'solicitantes'), + path('solicitantes/', views.SolicitanteAPIView.as_view(), name = 'solicitantes_pk'), path('verificar-correo///', views.VerificarCorreo.as_view(), name='verificar_correo'), path('restablecer-password/', views.ResetPassword.as_view(), name='reset_password'), path('nueva-password///', views.NuevaPassword.as_view(), name='nueva_password'), path('administradores/', AdminAPIView.as_view() , name = 'administrador_list_create'), - path('tabla_usuarios/', UsuarioTableAPIView.as_view() , name = 'usuario_dynamic_table'), - path('tabla_solicitantes/', SolicitanteTableAPIView.as_view() , name = 'solicitante_dynamic_table'), ] \ No newline at end of file diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index 7216d5f..b70c089 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -26,6 +26,7 @@ from datetime import datetime, timedelta from .permisos import es_admin, primer_login from notificaciones.mensajes import Mensaje from common.views import BasePermissionAPIView +from dynamic_tables.views import DynamicTableAPIView class CustomTokenObtainPairView(TokenObtainPairView): def post(self, request, *args, **kwargs): @@ -59,7 +60,7 @@ class CustomTokenRefreshView(TokenRefreshView): return response -class UsuarioAPIView(BasePermissionAPIView): +class UsuarioAPIView(DynamicTableAPIView): """ Clase Usuario para manejar las solicitudes de los usuarios básicos @@ -71,22 +72,11 @@ class UsuarioAPIView(BasePermissionAPIView): Herencia: - BasePermissionAPIView (Heréda de la clase con los permisos predefinidos) """ - permission_classes_create = [AllowAny] - permission_classes_delete = [IsAuthenticated, es_admin] - permission_classes_list = [IsAuthenticated, es_admin] + permission_classes_create = [AllowAny] - def get( self, request, *args, **kwargs ): - ''' - Método get para obtener la lista de usuarios o bien obtener a un usuario en específico - ''' - if 'pk' in kwargs: - instance = get_object_or_404(Usuario, pk=kwargs['pk']) - serializer = UsuarioSerializer(instance) - return Response(serializer.data) - else: - queryset = Usuario.objects.all() - serializer = UsuarioSerializer(queryset, many=True) - return Response(serializer.data) + model_class = Usuario + model_name = 'Usuario' + columns = '__all__' def post( self, request, *args, **kwargs ): ''' @@ -115,18 +105,7 @@ class UsuarioAPIView(BasePermissionAPIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def delete( self, request, *args, **kwargs ): - ''' - Método delete para la eliminación de un usuario - ''' - response_data = {} - instance = get_object_or_404(Usuario, pk=kwargs['pk']) - instance.delete() - Mensaje.success(response_data, 'Eliminación exitosa') - return Response(response_data, status=status.HTTP_204_NO_CONTENT) - - -class SolicitanteAPIView(BasePermissionAPIView): +class SolicitanteAPIView(DynamicTableAPIView): """ Clase Solicitante @@ -138,22 +117,12 @@ class SolicitanteAPIView(BasePermissionAPIView): Herencia: - BasePermissionAPIView (Heréda de la clase con los permisos predefinidos) """ - permission_classes_create = [IsAuthenticated, primer_login] - permission_classes_list = [IsAuthenticated, es_admin] + permission_classes_create = [IsAuthenticated] permission_classes_update = [IsAuthenticated, primer_login] - def get(self, request, *args, **kwargs): - ''' - Método get para obtener la lista de solicitantes - ''' - if 'pk' in kwargs: - instance = get_object_or_404(Solicitante, pk=kwargs['pk']) - serializer = SolicitanteSerializer(instance) - return Response(serializer.data) - else: - queryset = Solicitante.objects.all() - serializer = SolicitanteSerializer(queryset, many=True) - return Response(serializer.data) + model_class = Solicitante + model_name = 'Solicitante' + columns = '__all__' def post(self, request, *args, **kwargs): ''' @@ -178,27 +147,9 @@ class SolicitanteAPIView(BasePermissionAPIView): Mensaje.success(response_data, 'Acceso permitido.') return Response(response_data, status=status.HTTP_200_OK) Mensaje.error(response_data, 'Favor de completar sus datos.') - return Response(response_data, status=status.HTTP_200_OK) + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def put(self, request, *args, **kwargs): - ''' - Método put para la edición de un solicitante. - ''' - response_data = {} - if 'pk' in kwargs: - solicitante = get_object_or_404(Solicitante, pk=kwargs['pk']) - if not (request.user.is_staff or (solicitante.id == request.user.id)): - Mensaje.error(response_data, 'Usted no tiene permisos para realizar esta acción.') - return Response(response_data, status=status.HTTP_401_UNAUTHORIZED) - else: - solicitante = get_object_or_404(Solicitante, id=request.user.id) - serializer = SolicitanteSerializer(instance=solicitante, data=request.data) - if serializer.is_valid(): - serializer.save() - Mensaje.success(response_data, 'Datos actualizados exitosamente.') - return Response( response_data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class ResetPassword(APIView): -- GitLab From 2448c840f66d33e87cb5d80fe684fb73f15a676e Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Tue, 6 Aug 2024 12:22:59 -0600 Subject: [PATCH 5/6] =?UTF-8?q?Validaci=C3=B3n=20para=20no=20cambiar=20el?= =?UTF-8?q?=20modelo=20en=20una=20solicitud=20de=20tabla=20din=C3=A1mica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/dynamic_tables/DynamicTable.py | 2 ++ .../dynamic_tables/DynamicTableDynamicForm.py | 16 ++++++++++- cosiap_api/dynamic_tables/tests.py | 2 +- cosiap_api/dynamic_tables/views.py | 26 ++++++++++------- cosiap_api/solicitudes/tests.py | 7 +++-- cosiap_api/solicitudes/urls.py | 2 ++ cosiap_api/solicitudes/views.py | 28 +++++++++++++++++++ 7 files changed, 69 insertions(+), 14 deletions(-) diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index a12804b..b4a5d3d 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -36,6 +36,8 @@ class DynamicTable(serializers.ModelSerializer): representation = super().to_representation(instance) # Eliminar el campo 'data' representation.pop('data', None) + # No retornamos el modelo name + representation.pop('model_name', None) return representation diff --git a/cosiap_api/dynamic_tables/DynamicTableDynamicForm.py b/cosiap_api/dynamic_tables/DynamicTableDynamicForm.py index 857fc91..94503d7 100644 --- a/cosiap_api/dynamic_tables/DynamicTableDynamicForm.py +++ b/cosiap_api/dynamic_tables/DynamicTableDynamicForm.py @@ -2,5 +2,19 @@ from .DynamicTable import DynamicTable class DynamicTableDynamicForm(DynamicTable): - def get_dynamictable_fields(self): + def get_dynamicform_fields(self): + ''' Aquí se realizará la lógica para la obtención de las columnas del dynamic_form''' + pass + + def get_dynamicform_filters(self): + ''' Aquí se realizará la lógica para la obtención de los filtros disponibles de dynamic_form''' + pass + + def apply_dynamicform_filters(self): + ''' Aquí se realizará la lógica para la aplicación de filtros sobre el dynamic_form''' + pass + + + def update_dynamicform_fields(self): + ''' Aquí se realizará la lógica para la actualización del dynamic_form''' pass \ No newline at end of file diff --git a/cosiap_api/dynamic_tables/tests.py b/cosiap_api/dynamic_tables/tests.py index dad5a5c..6ba1f1d 100644 --- a/cosiap_api/dynamic_tables/tests.py +++ b/cosiap_api/dynamic_tables/tests.py @@ -88,7 +88,7 @@ class ReportesTests(TestCase): url = reverse('dynamic-tables:reportes_pk', args=[self.reporte1.pk]) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['model_name'], 'Solicitud') + self.assertEqual(response.data['columns'], ["status", "solicitud_n", "monto_solicitado", "monto_aprobado", "timestamp","solicitante__nombre"]) def test_post_reporte(self): """ diff --git a/cosiap_api/dynamic_tables/views.py b/cosiap_api/dynamic_tables/views.py index ae09631..67f6b1c 100644 --- a/cosiap_api/dynamic_tables/views.py +++ b/cosiap_api/dynamic_tables/views.py @@ -18,7 +18,7 @@ class DynamicTableAPIView(BasePermissionAPIView): ''' permission_classes_update = [IsAuthenticated, es_admin] - permission_classes_list = [IsAuthenticated, es_admin] + permission_classes_list = [IsAuthenticated, es_admin] permission_classes_create = [IsAuthenticated, es_admin] permission_classes_delete = [IsAuthenticated, es_admin] @@ -59,7 +59,7 @@ class DynamicTableAPIView(BasePermissionAPIView): # Verificamos si reporte_data es un diccionario vacío o None, asignamos configuración predeterminada if not reporte_data: reporte_data = { - 'model_name': self.model_name, + #'model_name': self.model_name, 'columns': self.columns, 'filters': self.filters, 'exclude_columns': self.exclude_columns, @@ -68,12 +68,13 @@ class DynamicTableAPIView(BasePermissionAPIView): } # debemos actualizar las variables predeterminadas para el futuro uso de las mismas - self.model_name = reporte_data.get("model_name", self.model_name) + #self.model_name = reporte_data.get("model_name", self.model_name) self.columns = reporte_data.get("columns", self.columns) self.filters = reporte_data.get("filters", self.filters) self.exclude_columns = reporte_data.get("exclude_columns", self.exclude_columns) self.search_query = reporte_data.get("search_query", self.search_query) self.exclude_filters = reporte_data.get("exclude_filters", self.exclude_filters) + reporte_data["model_name"] = self.model_name return DynamicTableReport(**reporte_data) def get(self, request, pk=None): @@ -89,13 +90,18 @@ class DynamicTableAPIView(BasePermissionAPIView): serializer = DynamicTable() instance_data = serializer.retrieve_instance_data(instance) return Response(instance_data, status=status.HTTP_200_OK) - configuracion_reporte = self.get_configuracion_reporte(request) - serializer = DynamicTable(instance=configuracion_reporte) - data = serializer.get_data(configuracion_reporte) - available_filters = serializer.get_available_filters(configuracion_reporte) - available_columns = serializer.get_available_columns(configuracion_reporte) - response_data = {'data': data, 'available_filters': available_filters, 'available_columns': available_columns} - return Response(response_data, status=status.HTTP_200_OK) + try: + configuracion_reporte = self.get_configuracion_reporte(request) + serializer = DynamicTable(instance=configuracion_reporte) + data = serializer.get_data(configuracion_reporte) + available_filters = serializer.get_available_filters(configuracion_reporte) + available_columns = serializer.get_available_columns(configuracion_reporte) + response_data = {'data': data, 'available_filters': available_filters, 'available_columns': available_columns} + return Response(response_data, status=status.HTTP_200_OK) + except Exception as e: + Mensaje.error(response_data, str(e)) + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) + def put(self, request, pk, *args, **kwargs): diff --git a/cosiap_api/solicitudes/tests.py b/cosiap_api/solicitudes/tests.py index 34211f0..a36e951 100644 --- a/cosiap_api/solicitudes/tests.py +++ b/cosiap_api/solicitudes/tests.py @@ -11,7 +11,7 @@ from django.core.files import File import os from django.conf import settings - +''' class PermisosSolicitudTests(c_tests.PermissionTestCase): url_name = 'solicitudes:solicitudes' methods_responses = { @@ -28,6 +28,7 @@ class PermisosSolicitudTests(c_tests.PermissionTestCase): 'anonymous': status.HTTP_401_UNAUTHORIZED } } +''' class SolicitudTests(TestCase): @@ -332,4 +333,6 @@ class SolicitudTests(TestCase): # Verificar los campos dentro del diccionario 'solicitante' solicitante_data = data.get('solicitante', {}) self.assertIn('nombre', solicitante_data) - self.assertEqual(solicitante_data['nombre'], self.solicitante.nombre) \ No newline at end of file + self.assertEqual(solicitante_data['nombre'], self.solicitante.nombre) + + \ No newline at end of file diff --git a/cosiap_api/solicitudes/urls.py b/cosiap_api/solicitudes/urls.py index f63713b..9506e8a 100644 --- a/cosiap_api/solicitudes/urls.py +++ b/cosiap_api/solicitudes/urls.py @@ -9,4 +9,6 @@ urlpatterns = [ path('/', views.SolicitudAPIView.as_view(), name='solicitudes_pk'), 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'), ] \ No newline at end of file diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index c3c4f4b..6cecdef 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -15,6 +15,10 @@ from rest_framework.response import Response from rest_framework import status from .serializer import SolicitudSerializer from django.shortcuts import get_object_or_404 +from dynamic_tables.views import ReporteAPIView +from rest_framework.permissions import AllowAny +from dynamic_tables.models import DynamicTableReport +from dynamic_tables.DynamicTable import DynamicTable class SolicitudAPIView(DynamicTableAPIView): ''' @@ -66,3 +70,27 @@ class HistorialAPIVIew(BasePermissionAPIView): return Response(serializer.data,status=status.HTTP_200_OK) +class ReportesSolicitudesAPIView(ReporteAPIView): + ''' + Clase con herencia de ReporteAPI view, para obtener los reportes exclusivamente del modelo solicitud. + ''' + + def get(self, request, *args, **kwargs): + ''' + Método GET para obtener una lista de reportes de solicitudes + o un reporte en caso de recibir un pk + ''' + + # indicamos el nombre del modelo. + model = 'Solicitud' + + if 'pk' in kwargs: + instance = get_object_or_404(DynamicTableReport, pk=kwargs['pk']) + serializer = DynamicTable(instance) + return Response(serializer.data) + + queryset = DynamicTableReport.objects.filter(model_name= model) + serializer = DynamicTable(queryset, many=True) + return Response(serializer.data) + + -- GitLab From 7af6370ae997fce6b1ddcb2906ba65d14f7981c6 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Thu, 8 Aug 2024 12:56:31 -0600 Subject: [PATCH 6/6] =?UTF-8?q?Edici=C3=B3n=20de=20varios=20registros=20de?= =?UTF-8?q?=20manera=20simultanea?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/dynamic_tables/DynamicTable.py | 34 ++++++++- cosiap_api/dynamic_tables/views.py | 70 ++++++++++++------- .../migrations/0002_modalidad_dynamic_form.py | 20 ++++++ cosiap_api/solicitudes/tests.py | 29 +++++++- 4 files changed, 127 insertions(+), 26 deletions(-) create mode 100644 cosiap_api/modalidades/migrations/0002_modalidad_dynamic_form.py diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index b4a5d3d..4cb3716 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -527,4 +527,36 @@ class DynamicTable(serializers.ModelSerializer): if not success: raise ValidationError(errors) # devolvemos un diccionario de errores surgidos durante las validaciones - return success \ No newline at end of file + return success + + + def update_registers(self, update_registers, model): + ''' + Actualiza múltiples campos de múltiples registros del modelo específicado + + :param update_registers: Diccionario en el formato {"id_registro": {"column1": "new_value", "column2": "new_value"}} + :return: True si todos los registros fueron actualizados de manera exitosa. False de lo contrario + ''' + all_errors = {} + overall_success = True + + for pk, field_updates in update_registers.items(): + try: + instance = model.objects.get(pk=int(pk)) + # Intentar actualizar los campos de la instancia + try: + success = self.update_fields(instance, field_updates) + + except ValidationError as e: + all_errors[pk] = str(e) # Devolvemos el error de validación como string + overall_success = False + + except Exception as e: + all_errors[pk] = str(e) + overall_success = False + + if not overall_success: + raise ValidationError(all_errors) # Devolver un diccionario de errores surgidos durante las actualizaciones + + return overall_success + diff --git a/cosiap_api/dynamic_tables/views.py b/cosiap_api/dynamic_tables/views.py index 67f6b1c..ce82311 100644 --- a/cosiap_api/dynamic_tables/views.py +++ b/cosiap_api/dynamic_tables/views.py @@ -104,43 +104,65 @@ class DynamicTableAPIView(BasePermissionAPIView): - def put(self, request, pk, *args, **kwargs): + def put(self, request, pk=None, *args, **kwargs): ''' Permite a un admin modificar manualmente varias columnas de una fila en la tabla dinámica. ''' response_data = {} + instance = None # Obtener la instancia del modelo por el id proporcionado en la URL o un 404 en caso de no existir - instance = get_object_or_404(self.model_class, pk=pk) + if pk is not None: + instance = get_object_or_404(self.model_class, pk=pk) + + # Obtener datos de actualización del request field_updates = request.data.get('field_updates', {}) + register_updates = request.data.get('register_updates', {}) - # extraemos la configuración, o si no fue enviada asignamos la predeterminada + # Extraer la configuración, o si no fue enviada asignamos la predeterminada configuracion = self.get_configuracion_reporte(request) serializer = DynamicTable(instance=configuracion) - # si tenemos un columns all if self.columns == "__all__": - # debemos recuperar las columnas disponibles según la configuración self.columns = list(serializer.get_available_columns(configuracion).keys()) - - # Verificamos que los campos estén disponibles en la tabla y que sean editables - for column_name in field_updates: - if column_name not in self.columns or column_name in self.non_editable_fields: - Mensaje.error(response_data, f"Modificación no permitida en la columna '{column_name}'.") - return Response(response_data, status=status.HTTP_400_BAD_REQUEST) - try: - serializer = DynamicTable(instance=configuracion) - # Actualizar los valores de los campos y guardar la instancia - success = serializer.update_fields(instance, field_updates) - - if success: - Mensaje.success(response_data, 'Campos actualizados con éxito.') - return Response(response_data, status=status.HTTP_200_OK) - else: - Mensaje.error(response_data,'Ocurrió un error al actualizar algunos campos.') - return Response(response_data, status=status.HTTP_400_BAD_REQUEST) - except ValidationError as e: - return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + if field_updates and instance: + # Caso donde se proporcionan actualizaciones individuales para un registro + for column_name in field_updates: + if column_name not in self.columns or column_name in self.non_editable_fields: + Mensaje.error(response_data, f"Modificación no permitida en la columna '{column_name}'.") + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) + + try: + # Actualizar los valores de los campos del registro y guardar la instancia + success = serializer.update_fields(instance, field_updates) + + if success: + Mensaje.success(response_data, 'Campos actualizados con éxito.') + return Response(response_data, status=status.HTTP_200_OK) + else: + Mensaje.error(response_data, 'Ocurrió un error al actualizar algunos campos.') + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) + except ValidationError as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + elif register_updates: + # Caso donde se proporcionan actualizaciones para múltiples registros + try: + success = serializer.update_registers(register_updates, self.model_class) + if success: + Mensaje.success(response_data, 'Registros actualizados con éxito.') + return Response(response_data, status=status.HTTP_200_OK) + else: + Mensaje.error(response_data, 'Ocurrió un error al actualizar algunos registros.') + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) + except ValidationError as e: + print(str(e)) + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + else: + Mensaje.error(response_data, 'No se proporcionaron datos de actualización.') + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) + def delete(self, request, pk, *args, **kwargs): diff --git a/cosiap_api/modalidades/migrations/0002_modalidad_dynamic_form.py b/cosiap_api/modalidades/migrations/0002_modalidad_dynamic_form.py new file mode 100644 index 0000000..76a4d1e --- /dev/null +++ b/cosiap_api/modalidades/migrations/0002_modalidad_dynamic_form.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.7 on 2024-08-08 16:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamic_forms', '0006_merge_migrations'), + ('modalidades', '0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes'), + ] + + operations = [ + migrations.AddField( + model_name='modalidad', + name='dynamic_form', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='dynamic_forms.dynamicform', verbose_name='Formulario'), + ), + ] diff --git a/cosiap_api/solicitudes/tests.py b/cosiap_api/solicitudes/tests.py index a36e951..8d2ea92 100644 --- a/cosiap_api/solicitudes/tests.py +++ b/cosiap_api/solicitudes/tests.py @@ -335,4 +335,31 @@ class SolicitudTests(TestCase): self.assertIn('nombre', solicitante_data) self.assertEqual(solicitante_data['nombre'], self.solicitante.nombre) - \ No newline at end of file + def test_put_actualizacion_registro_multiple(self): + """ + Probar la actualización de múltiples registros con una solicitud PUT + """ + url = reverse('solicitudes:solicitudes') + + data = { + "register_updates": { + str(self.solicitud1.pk): { + "monto_solicitado": "10000", + "observacion": "Actualización múltiple - Solicitud 1" + }, + str(self.solicitud2.pk): { + "monto_solicitado": "10000", + "observacion": "Actualización múltiple - Solicitud 2" + } + } + } + response = self.client.put(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.solicitud1.refresh_from_db() + self.solicitud2.refresh_from_db() + self.assertEqual(self.solicitud1.monto_solicitado, 10000.0) + self.assertEqual(self.solicitud1.observacion, "Actualización múltiple - Solicitud 1") + self.assertEqual(self.solicitud2.monto_solicitado, 10000.0) + self.assertEqual(self.solicitud2.observacion, "Actualización múltiple - Solicitud 2") + self.assertIn('Registros actualizados con éxito.', response.data['messages']['success']) + -- GitLab