diff --git a/cosiap_api/cosiap_api/.env b/cosiap_api/cosiap_api/.env index b5782c3b6c4483a807a432bc056791012ac6108f..b225e678368c09ffbd9c5e9aebe1764baaaddbfc 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/cosiap_api/settings.py b/cosiap_api/cosiap_api/settings.py index f3b8a9cb6ae2d55857892e64601978c6509d5567..969e30928793b4bf0d99d87cd3455f1a7cc8fe26 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 a439c3ea0acd26fd0abed04391a37e56a6d8c18a..80fd960c3eb36fccd4b45557848bbe172204ce8a 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 03a55928e43893b8a9349980caec719aca030eac..b4a5d3d1cc8984c74675d941a4f91af92fd21da1 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$|^dynamic_form$', re.IGNORECASE) + class DynamicTable(serializers.ModelSerializer): ''' Clase equivalente a un serializer con la lógica del manejo de las tablas dinámicas @@ -33,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 @@ -141,7 +146,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 +272,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 +361,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/DynamicTableDynamicForm.py b/cosiap_api/dynamic_tables/DynamicTableDynamicForm.py new file mode 100644 index 0000000000000000000000000000000000000000..94503d7f890bbb3fa5616532e09787f8419cf79e --- /dev/null +++ b/cosiap_api/dynamic_tables/DynamicTableDynamicForm.py @@ -0,0 +1,20 @@ +from .DynamicTable import DynamicTable + +class DynamicTableDynamicForm(DynamicTable): + + 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 dad5a5cce0dc4cbafa4f9a0dc1678e9dbb540774..6ba1f1de9f8f95a530cec7098d1989193306f47e 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 1fc66327207fcb6348fd36306f6b7bf0fac5cc5f..67f6b1c01fca6080e10fdbdbfec7ceac31858cc8 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): @@ -83,21 +84,24 @@ 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) + 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) - 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) def put(self, request, pk, *args, **kwargs): 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 0000000000000000000000000000000000000000..a8fd08838fd23cc952a8694356dc85df578e10f2 --- /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 f50cdef6fe34942811b18644d90e52bc9baba122..b36adc01c627a223b6e9ab0b07f7f89f59773553 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/serializer.py b/cosiap_api/solicitudes/serializer.py new file mode 100644 index 0000000000000000000000000000000000000000..7723d4173b35bc6e1a086afab8096093b94382d7 --- /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.py b/cosiap_api/solicitudes/tests.py index 1c5644e2934eb2be9678004f7fc782d221530ca2..a36e951b32d9b27faa4771c8c0db7c3795c00b3b 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, @@ -24,12 +28,14 @@ class PermisosSolicitudTests(c_tests.PermissionTestCase): 'anonymous': status.HTTP_401_UNAUTHORIZED } } +''' 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 +66,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( @@ -326,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/tests_historial.py b/cosiap_api/solicitudes/tests_historial.py new file mode 100644 index 0000000000000000000000000000000000000000..3156fe4d14e10ac416c82cdd75ef14bd623b5111 --- /dev/null +++ b/cosiap_api/solicitudes/tests_historial.py @@ -0,0 +1,146 @@ +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 +from rest_framework_simplejwt.tokens import RefreshToken + + +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) + + 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 53e9b00d392e7f248067062bedfce9a53781f3fa..9506e8aa257b85fbd3fe234445d17b8148041c7c 100644 --- a/cosiap_api/solicitudes/urls.py +++ b/cosiap_api/solicitudes/urls.py @@ -7,4 +7,8 @@ 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('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 2b38431c61a0f0eb02d13ca1eaa5d6f008b73997..6cecdef214efc2e106e9ebce71135f691ae53d73 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -7,11 +7,23 @@ 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 +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): ''' Clase para el manejo de la lista de solicitudes y la aplicación de sus filtros + ''' model_class = Solicitud @@ -23,4 +35,62 @@ 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, *args, **kwargs ): + ''' + Método get para obtener la lista de solicitudes realizadas + ''' + + response_data = {} + + 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: + 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) + + +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) + + diff --git a/cosiap_api/users/admin_views.py b/cosiap_api/users/admin_views.py index 5a93d66a8551d93e80aa12ce51d86912b48d9219..2fcbbd5ed7853521ea03e2e46c6c26370a190ef8 100644 --- a/cosiap_api/users/admin_views.py +++ b/cosiap_api/users/admin_views.py @@ -4,61 +4,56 @@ 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 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.'}} - return Response(response_data, status = status.HTTP_400_BAD_REQUEST) - - + Mensaje.error(response_data, 'Este email ya esta en uso por otro usuario.') + 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 3d6b01c2069ba9432795b14e04e5bf9ffe2eb797..0e90249a82ab521a22623d8c3519345059da37bb 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 @@ -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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..57c1a138d90aae2d4fdc2b0b897a2755853b7e6c 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 d58e6e480d968bfae1dd3e533da8a9d83d27be2e..4582f5fe1e9cf54ed7f8f35ceec865147ec9413d 100644 --- a/cosiap_api/users/urls.py +++ b/cosiap_api/users/urls.py @@ -10,10 +10,10 @@ 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'), diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index f3cb8348ae8abf48882c417042c05d85a91ca785..b70c089c8fc711b1564a4850e635682cc71235b2 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,9 +60,7 @@ class CustomTokenRefreshView(TokenRefreshView): return response - -# Funcionalidad para crear un usuario en el sistema, ver sus datos o eliminarlo -class UsuarioAPIView(BasePermissionAPIView): +class UsuarioAPIView(DynamicTableAPIView): """ Clase Usuario para manejar las solicitudes de los usuarios básicos @@ -73,87 +72,40 @@ 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] - - # 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: - 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) - - # Función para la creación del usuario + permission_classes_create = [AllowAny] + + model_class = Usuario + model_name = 'Usuario' + columns = '__all__' + 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) - # Función para la eliminación del usuario - def delete( self, request, *args, **kwargs ): - # 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 - return Response({"message": {'success': 'Eliminación exitosa'}}, status=status.HTTP_204_NO_CONTENT) - - -class SolicitanteAPIView(BasePermissionAPIView): +class SolicitanteAPIView(DynamicTableAPIView): """ Clase Solicitante @@ -165,37 +117,19 @@ 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_create = [IsAuthenticated] 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 - 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 + model_class = Solicitante + model_name = 'Solicitante' + columns = '__all__' + def post(self, request, *args, **kwargs): - # obtenemos al usuario del request + ''' + Método post para la creación de un nuevo solicitante + ''' + response_data = {} usuario = request.user - # creamos el solicitante solicitante, created = Solicitante.objects.get_or_create( id=usuario.id, defaults={ @@ -206,42 +140,16 @@ 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 - return Response({"message": {'success': 'Acceso permitido.'}}, 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) - # Si hay errores en los datos, enviamos un bad request + 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_400_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): - # 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) - 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 - return Response({"message": {'success': 'Datos actualizados exitosamente.'}}, 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) class ResetPassword(APIView): @@ -251,29 +159,21 @@ 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): - # extraemos el email de la solicitud + response_data = {} 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 - return Response({"message": {'success': 'Correo de restablecimiento enviado.'}}, status=status.HTTP_200_OK) - # si el usuario no existe + Mensaje.success(response_data, 'Correo de restablecimiento enviado.') + return Response(response_data, status=status.HTTP_200_OK) 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): @@ -284,7 +184,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 @@ -294,29 +194,21 @@ class NuevaPassword(APIView): - uidb64 (el id del usuario que restablece su contraseña encriptado en base64) - token (el token de reestablecimiento de contraseña) """ - # intentamos extraer los datos del usuario + response_data = {} 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 - 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): @@ -328,15 +220,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) @@ -349,15 +236,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): @@ -370,23 +252,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 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