From 3c4a58635f279404baa028fdf1cfa01151aadc1f Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Mon, 1 Jul 2024 12:38:12 -0600 Subject: [PATCH 1/5] Tablas Estado y Municipio creadas, relaciones de solicitante con datos bancarios realizadas --- cosiap_api/users/admin.py | 6 +- .../0005_creacion_tablas_estado_municipio.py | 44 ++++++++++ .../0006_crear_tabla_datosbancarios.py | 26 ++++++ ...07_relacion_banco_municipio_solicitante.py | 24 ++++++ cosiap_api/users/models.py | 85 ++++++++++++++++++- cosiap_api/users/permisos.py | 7 ++ cosiap_api/users/views.py | 7 +- 7 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 cosiap_api/users/migrations/0005_creacion_tablas_estado_municipio.py create mode 100644 cosiap_api/users/migrations/0006_crear_tabla_datosbancarios.py create mode 100644 cosiap_api/users/migrations/0007_relacion_banco_municipio_solicitante.py create mode 100644 cosiap_api/users/permisos.py diff --git a/cosiap_api/users/admin.py b/cosiap_api/users/admin.py index d74e966..a218f14 100644 --- a/cosiap_api/users/admin.py +++ b/cosiap_api/users/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin -from users.models import Usuario, Solicitante +from users.models import Usuario, Solicitante, Municipio, Estado # Register your models here. @@ -31,4 +31,6 @@ class UserAdmin(UserAdmin): list_filter = ('is_staff', 'is_superuser', 'is_active') admin.site.register(Usuario, UserAdmin) -admin.site.register(Solicitante) \ No newline at end of file +admin.site.register(Solicitante) +admin.site.register(Estado) +admin.site.register(Municipio) \ No newline at end of file diff --git a/cosiap_api/users/migrations/0005_creacion_tablas_estado_municipio.py b/cosiap_api/users/migrations/0005_creacion_tablas_estado_municipio.py new file mode 100644 index 0000000..3637f75 --- /dev/null +++ b/cosiap_api/users/migrations/0005_creacion_tablas_estado_municipio.py @@ -0,0 +1,44 @@ +# Generated by Django 5.0.6 on 2024-07-01 17:45 + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0004_creacion_modelo_solicitante'), + ] + + operations = [ + migrations.CreateModel( + name='Estado', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nombre', models.CharField(max_length=191, verbose_name='Nombre Estado')), + ('created_at', models.DateTimeField(auto_now_add=True, null=True, verbose_name='created_at')), + ('updated_at', models.DateTimeField(auto_now=True, null=True, verbose_name='updated_at')), + ], + ), + migrations.AlterField( + model_name='solicitante', + name='RFC', + field=models.CharField(max_length=13, unique=True, validators=[django.core.validators.RegexValidator('^([A-ZÑ&]{3,4}) ?(?:- ?)?(\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])) ?(?:- ?)?([A-Z\\d]{2})([A\\d])$', 'Debe ser un RFC válido.')], verbose_name='RFC'), + ), + migrations.CreateModel( + name='Municipio', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cve_mun', models.PositiveIntegerField(verbose_name='Clave Municipio')), + ('nombre', models.CharField(max_length=191, verbose_name='Nombre Municipio')), + ('created_at', models.DateTimeField(auto_now_add=True, null=True, verbose_name='created_at')), + ('updated_at', models.DateTimeField(auto_now=True, null=True, verbose_name='updated_at')), + ('estado', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.estado', verbose_name='Estado')), + ], + options={ + 'ordering': ['estado', 'nombre'], + 'unique_together': {('estado', 'cve_mun')}, + }, + ), + ] diff --git a/cosiap_api/users/migrations/0006_crear_tabla_datosbancarios.py b/cosiap_api/users/migrations/0006_crear_tabla_datosbancarios.py new file mode 100644 index 0000000..db56c36 --- /dev/null +++ b/cosiap_api/users/migrations/0006_crear_tabla_datosbancarios.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.6 on 2024-07-01 18:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0005_creacion_tablas_estado_municipio'), + ] + + operations = [ + migrations.CreateModel( + name='DatosBancarios', + fields=[ + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('nombre_banco', models.CharField(max_length=20, verbose_name='Nombre banco')), + ('cuenta_bancaria', models.CharField(max_length=10, verbose_name='No. Cuenta')), + ('clabe_bancaria', models.CharField(max_length=16, verbose_name='Clabe bancaria')), + ('doc_estado_cuenta', models.FileField(blank=True, null=True, upload_to='estados_cuenta_files/', verbose_name='Estado de cuenta')), + ('doc_constancia_sat', models.FileField(blank=True, null=True, upload_to='constancias_sat_files/', verbose_name='Constancia Situación Fiscal')), + ('codigo_postal_fiscal', models.CharField(max_length=5, verbose_name='Código Postal Físcal')), + ('regimen', models.CharField(max_length=20)), + ], + ), + ] diff --git a/cosiap_api/users/migrations/0007_relacion_banco_municipio_solicitante.py b/cosiap_api/users/migrations/0007_relacion_banco_municipio_solicitante.py new file mode 100644 index 0000000..5a2f88d --- /dev/null +++ b/cosiap_api/users/migrations/0007_relacion_banco_municipio_solicitante.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.6 on 2024-07-01 18:30 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0006_crear_tabla_datosbancarios'), + ] + + operations = [ + migrations.AddField( + model_name='solicitante', + name='datos_bancarios', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='users.datosbancarios', verbose_name='Datos Bancarios'), + ), + migrations.AddField( + model_name='solicitante', + name='municipio', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='users.municipio', verbose_name='Municipio'), + ), + ] diff --git a/cosiap_api/users/models.py b/cosiap_api/users/models.py index d90e7c6..56b0071 100644 --- a/cosiap_api/users/models.py +++ b/cosiap_api/users/models.py @@ -3,7 +3,67 @@ from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager from django.core.validators import RegexValidator from django.contrib.auth.models import PermissionsMixin from django.core.exceptions import ValidationError -import uuid +import uuid +import os + +# modelo con la información de los estados de la república +class Estado(models.Model): + nombre = models.CharField(verbose_name="Nombre Estado", max_length=191, null=False) + created_at = models.DateTimeField(verbose_name="created_at", auto_now_add=True, null=True) + updated_at = models.DateTimeField(verbose_name="updated_at", auto_now=True, null=True) + + def __str__(self): + return self.nombre + +# modelo con la información de los municipios de un estado +class Municipio(models.Model): + estado = models.ForeignKey(Estado, verbose_name="Estado", on_delete=models.CASCADE) + cve_mun = models.PositiveIntegerField(verbose_name="Clave Municipio", null=False) + nombre = models.CharField(verbose_name="Nombre Municipio", max_length=191, null=False) + created_at = models.DateTimeField(verbose_name="created_at", auto_now_add=True, null=True) + updated_at = models.DateTimeField(verbose_name="updated_at", auto_now=True, null=True) + + def save(self, *args, **kwargs): + # Generar el ID combinando estado y cve_mun como una cadena + self.id = f"{self.estado_id}{self.cve_mun}" + super(Municipio, self).save(*args, **kwargs) + + def __str__(self): + return self.nombre + class Meta: + ordering = ['estado', 'nombre'] + unique_together = ('estado', 'cve_mun') + + +# modelo para el registro de los datos bancarios del solicitante +class DatosBancarios(models.Model): + # identificador del objeto + id = models.BigAutoField(primary_key=True) + # nombre del banco al que pertenece la cuenta + nombre_banco = models.CharField(verbose_name="Nombre banco", max_length=20) + # numero de cuenta + cuenta_bancaria = models.CharField(verbose_name="No. Cuenta", max_length=10) + # clabe interbancaria + clabe_bancaria = models.CharField(verbose_name="Clabe bancaria", max_length=16) + # archivo de el estado de cuenta + doc_estado_cuenta = models.FileField(verbose_name="Estado de cuenta", upload_to= "estados_cuenta_files/", null=True, blank=True) + # archivo de el comporbante de situación fiscal + doc_constancia_sat = models.FileField(verbose_name="Constancia Situación Fiscal", upload_to="constancias_sat_files/", null=True, blank=True) + # código postal fiscal + codigo_postal_fiscal = models.CharField(verbose_name="Código Postal Físcal", max_length=5) + # regimen fiscal al que pertenece + regimen = models.CharField(max_length=20) + + # sobreescribimos el método save para la lógica del nombre del archivo + def save(self, *args, **kwargs): + # Asignar un nombre único al estado de cuenta + if self.doc_estado_cuenta: + self.doc_estado_cuenta.name = generar_nombre_archivo(self.doc_estado_cuenta.name) + # Asignar un nombre único a la constancia sat + if self.doc_constancia_sat: + self.doc_constancia_sat.name = generar_nombre_archivo(self.doc_constancia_sat.name) + super(DatosBancarios, self).save(*args, **kwargs) + class UsuarioManager(BaseUserManager): def create_user(self, email, curp, nombre, password=None, is_admin=False, is_staff=False, is_active=False): @@ -62,7 +122,7 @@ class Usuario(AbstractBaseUser, PermissionsMixin): # clase Solicitante que hereda de Usuario class Solicitante(Usuario): # validador para el formato del RFC - RFC_REGEX = r'^[A-Z&Ñ]{4}\d{6}[A-Z0-9]{3}$' + RFC_REGEX = r'^([A-ZÑ&]{3,4}) ?(?:- ?)?(\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])) ?(?:- ?)?([A-Z\d]{2})([A\d])$' # campos para los apellidos ap_paterno = models.CharField(verbose_name='Apellido Paterno',max_length=50) @@ -76,11 +136,30 @@ class Solicitante(Usuario): direccion = models.CharField(verbose_name='Dirección', max_length=255) # campo para el código postal, longitud 5 codigo_postal = models.CharField(verbose_name='Código Postal', max_length=5) + # relación con el municipio mediante llave foránea + municipio = models.ForeignKey(Municipio, verbose_name="Municipio",null=True, blank=False, on_delete=models.CASCADE) # campo para la poblacion poblacion = models.CharField(verbose_name='Población', max_length=255) + # 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='ine_files/', null=True, blank=True) + def __str__(self): + return self.RFC + + # sobreescribimos el método save para la lógica del nombre del archivo + def save(self, *args, **kwargs): + # Asignar un nombre único al archivo INE + if self.INE: + self.INE.name = generar_nombre_archivo(self.INE.name) + super(Solicitante, self).save(*args, **kwargs) - +# Método para generar un nombre único a los archivos del usuario +def generar_nombre_archivo(nombre_archivo): + # Obtener la extensión del archivo + ext = nombre_archivo.split('.')[-1] + # Generar un nombre único usando uuid4 + nombre_unico= f"{uuid.uuid4().hex}.{ext}" + return os.path.join('ine_files/', nombre_unico) \ No newline at end of file diff --git a/cosiap_api/users/permisos.py b/cosiap_api/users/permisos.py new file mode 100644 index 0000000..9b3dc4f --- /dev/null +++ b/cosiap_api/users/permisos.py @@ -0,0 +1,7 @@ +from rest_framework import permissions + +# Funcionalidad para verificar que el usuario que realiza la eliminación sea un admin +class es_admin(permissions.BasePermission): + # método para determinar que un usuario tiene permisos + def has_permission( self, request, view): + return request.user and request.user.is_superuser \ No newline at end of file diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index 91859f3..cb98268 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -22,12 +22,7 @@ from rest_framework.response import Response from rest_framework import status from django.conf import settings from datetime import datetime, timedelta - -# Funcionalidad para verificar que el usuario que realiza la eliminación sea un admin -class es_admin(permissions.BasePermission): - # método para determinar que un usuario tiene permisos - def has_permission( self, request, view): - return request.user and request.user.is_superuser +from .permisos import es_admin class CustomTokenObtainPairView(TokenObtainPairView): def post(self, request, *args, **kwargs): -- GitLab From a1014d110b4bcf604268368ea6e6461ca9d209ad Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Mon, 1 Jul 2024 13:42:46 -0600 Subject: [PATCH 2/5] =?UTF-8?q?A=C3=B1adiendo=20choices=20de=20regimen=20f?= =?UTF-8?q?iscal=20a=20modelo=20DatosBancarios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0008_agregar_choices_regimenfiscal.py | 18 ++++++++++++++++++ cosiap_api/users/models.py | 17 ++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 cosiap_api/users/migrations/0008_agregar_choices_regimenfiscal.py diff --git a/cosiap_api/users/migrations/0008_agregar_choices_regimenfiscal.py b/cosiap_api/users/migrations/0008_agregar_choices_regimenfiscal.py new file mode 100644 index 0000000..49817a7 --- /dev/null +++ b/cosiap_api/users/migrations/0008_agregar_choices_regimenfiscal.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-07-01 19:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0007_relacion_banco_municipio_solicitante'), + ] + + operations = [ + migrations.AlterField( + model_name='datosbancarios', + name='regimen', + field=models.CharField(choices=[('1', 'Régminen Simplificado de Confianza'), ('2', 'Sueldos y salarios e ingresos asimilados a salarios'), ('3', 'Régimen de Actividades Empresariales y Profesionales'), ('4', 'Régimen de Incorporación Fiscal'), ('5', 'Enajenación de bienes'), ('6', 'Régimen de Actividades Empresariales con ingresos a través de Plataformas Tecnológicas'), ('7', 'Régimen de Arrendamiento'), ('8', 'Intereses'), ('9', 'Obtención de premios'), ('10', 'Dividendos'), ('11', 'Demás Ingresos'), ('12', 'Sin obligaciones fiscales')], max_length=255), + ), + ] diff --git a/cosiap_api/users/models.py b/cosiap_api/users/models.py index 56b0071..ba717fb 100644 --- a/cosiap_api/users/models.py +++ b/cosiap_api/users/models.py @@ -37,6 +37,21 @@ class Municipio(models.Model): # modelo para el registro de los datos bancarios del solicitante class DatosBancarios(models.Model): + # Choices para el campo de régimen fiscal + REGIMEN_CHOICES= [ + ('1', 'Régminen Simplificado de Confianza'), + ('2', 'Sueldos y salarios e ingresos asimilados a salarios'), + ('3', 'Régimen de Actividades Empresariales y Profesionales'), + ('4', 'Régimen de Incorporación Fiscal'), + ('5', 'Enajenación de bienes'), + ('6', 'Régimen de Actividades Empresariales con ingresos a través de Plataformas Tecnológicas'), + ('7', 'Régimen de Arrendamiento'), + ('8', 'Intereses'), + ('9', 'Obtención de premios'), + ('10', 'Dividendos'), + ('11', 'Demás Ingresos'), + ('12', 'Sin obligaciones fiscales') + ] # identificador del objeto id = models.BigAutoField(primary_key=True) # nombre del banco al que pertenece la cuenta @@ -52,7 +67,7 @@ class DatosBancarios(models.Model): # código postal fiscal codigo_postal_fiscal = models.CharField(verbose_name="Código Postal Físcal", max_length=5) # regimen fiscal al que pertenece - regimen = models.CharField(max_length=20) + regimen = models.CharField(max_length=255, choices=REGIMEN_CHOICES) # sobreescribimos el método save para la lógica del nombre del archivo def save(self, *args, **kwargs): -- GitLab From 1b7260a4f7c3cca447072f33b9a1c6daa7ab3fa2 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Mon, 1 Jul 2024 15:22:02 -0600 Subject: [PATCH 3/5] =?UTF-8?q?Creaci=C3=B3n=20del=20serializer=20de=20sol?= =?UTF-8?q?icitante?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/users/serializers.py | 57 +++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/cosiap_api/users/serializers.py b/cosiap_api/users/serializers.py index e2a7d6d..ed90130 100644 --- a/cosiap_api/users/serializers.py +++ b/cosiap_api/users/serializers.py @@ -3,7 +3,7 @@ # Versión: 1.0 from rest_framework import serializers -from .models import Usuario +from .models import Usuario, Solicitante, DatosBancarios # serializer para el usuario solicitante class usuario_serializer(serializers.ModelSerializer): @@ -40,4 +40,57 @@ class usuario_serializer(serializers.ModelSerializer): is_staff=False ) # Retornamos el usuario creado - return user \ No newline at end of file + return user + + +# Serializer para el registro y edición de los datos bancarios del usuario +class datos_bancarios_serializer(serializers.ModelSerializer): + class Meta: + # indicamos el modelo + model = DatosBancarios + # indicamos los fields que el usuario debe ingresar + fields = ['nombre_banco', 'cuenta_bancaria', 'clabe_bancaria', 'doc_estado_cuenta', 'doc_constancia_sat', 'codigo_postal_fiscal', 'regimen'] + + +# Serializer para el registro y actualización de los datos del solicitante +class solicitante_serializer(serializers.ModelSerializer): + class Meta: + # indicamos el modelo a utilziar + model = Solicitante + # indicamos los campos que debe ingresar el usuario + fields = ['ap_paterno', 'ap_materno', 'telefono', 'RFC', 'direccion', 'codigo_postal', 'municipio', 'poblacion', 'INE'] + extra_kwargs = { + 'datos_bancarios': {'allow_null': True, 'required': False} # Permitir que datos_bancarios sea null y no requerido + } + + # Definimos una función para crear al Solicitante + def create(self, validated_data): + # Vamos a extraer al usuario que realizó la solicitud + user = self.context['request'].user + + # En este punto vamos a Crear o bien, Actulizar el solicitante + solicitante, created = Solicitante.objects.update_or_create( + curp=user.curp, + **validated_data + ) + + # Retornamos la instancia del solicitante + return solicitante + + # Definimos una función para la actualización del solicitante, recibiendo una instancia + def update(self, instance, validated_data): + # Actualizar los campos del Solicitante + instance.ap_paterno = validated_data.get('ap_paterno', instance.ap_paterno) + instance.ap_materno = validated_data.get('ap_materno', instance.ap_materno) + instance.telefono = validated_data.get('telefono', instance.telefono) + instance.RFC = validated_data.get('RFC', instance.RFC) + instance.direccion = validated_data.get('direccion', instance.direccion) + instance.codigo_postal = validated_data.get('codigo_postal', instance.codigo_postal) + instance.municipio = validated_data.get('municipio', instance.municipio) + instance.poblacion = validated_data.get('poblacion', instance.poblacion) + instance.INE = validated_data.get('INE', instance.INE) + + # Guardar el objeto Solicitante actualizado + instance.save() + # Retornamos la instancia actualziada + return instance \ No newline at end of file -- GitLab From 934563cf29551ae495b34e67ebb9b6394a548257 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Tue, 2 Jul 2024 13:39:56 -0600 Subject: [PATCH 4/5] =?UTF-8?q?Logica=20de=20primer=20LOGIN,=20creaci?= =?UTF-8?q?=C3=B3n=20del=20solicitante=20y=20edici=C3=B3n=20de=20sus=20dat?= =?UTF-8?q?os?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/users/models.py | 14 +++- cosiap_api/users/serializers.py | 26 +++++--- cosiap_api/users/urls.py | 3 +- cosiap_api/users/views.py | 111 ++++++++++++++++++++++++++++++-- 4 files changed, 139 insertions(+), 15 deletions(-) diff --git a/cosiap_api/users/models.py b/cosiap_api/users/models.py index ba717fb..265f458 100644 --- a/cosiap_api/users/models.py +++ b/cosiap_api/users/models.py @@ -161,7 +161,7 @@ class Solicitante(Usuario): INE = models.FileField(verbose_name='INE', upload_to='ine_files/', null=True, blank=True) def __str__(self): - return self.RFC + return self.nombre # sobreescribimos el método save para la lógica del nombre del archivo @@ -171,6 +171,18 @@ class Solicitante(Usuario): self.INE.name = generar_nombre_archivo(self.INE.name) super(Solicitante, self).save(*args, **kwargs) + # Método para determinar si todos los datos del solicitante estan completos + def datos_completos(self): + # 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 retona falso + return False + # si todos los datos estan llenos, retornamos un True + return True + # Método para generar un nombre único a los archivos del usuario def generar_nombre_archivo(nombre_archivo): # Obtener la extensión del archivo diff --git a/cosiap_api/users/serializers.py b/cosiap_api/users/serializers.py index ed90130..3ed56f2 100644 --- a/cosiap_api/users/serializers.py +++ b/cosiap_api/users/serializers.py @@ -59,22 +59,30 @@ class solicitante_serializer(serializers.ModelSerializer): model = Solicitante # indicamos los campos que debe ingresar el usuario fields = ['ap_paterno', 'ap_materno', 'telefono', 'RFC', 'direccion', 'codigo_postal', 'municipio', 'poblacion', 'INE'] + # De momento todos los campos son opcionales extra_kwargs = { - 'datos_bancarios': {'allow_null': True, 'required': False} # Permitir que datos_bancarios sea null y no requerido - } + 'ap_paterno': {'required':False, 'allow_null': True}, + 'ap_materno': {'required': False, 'allow_null': True}, + 'telefono': {'required': False, 'allow_null': True}, + 'RFC': {'required': False, 'allow_null': True}, + 'direccion': {'required': False, 'allow_null': True}, + 'codigo_postal': {'required': False, 'allow_null': True}, + 'municipio': {'required': False, 'allow_null': True}, + 'poblacion': {'required': False, 'allow_null': True}, + 'INE': {'required': False, 'allow_null': True}, + 'datos_bancarios': {'required': False, 'allow_null': True} + } # Definimos una función para crear al Solicitante def create(self, validated_data): - # Vamos a extraer al usuario que realizó la solicitud user = self.context['request'].user - - # En este punto vamos a Crear o bien, Actulizar el solicitante - solicitante, created = Solicitante.objects.update_or_create( + solicitante = Solicitante.objects.create( curp=user.curp, + nombre=user.nombre, + email=user.email, + is_active=True, **validated_data ) - - # Retornamos la instancia del solicitante return solicitante # Definimos una función para la actualización del solicitante, recibiendo una instancia @@ -92,5 +100,5 @@ class solicitante_serializer(serializers.ModelSerializer): # Guardar el objeto Solicitante actualizado instance.save() - # Retornamos la instancia actualziada + # Retornamos la instancia actualizada return instance \ No newline at end of file diff --git a/cosiap_api/users/urls.py b/cosiap_api/users/urls.py index 5d0247c..4fc205e 100644 --- a/cosiap_api/users/urls.py +++ b/cosiap_api/users/urls.py @@ -26,5 +26,6 @@ urlpatterns = [ path('usuarios/', views.usuario.as_view(), name = 'usuario-lista-crear'), path('usuarios//', views.usuario.as_view(), name = 'ver-eliminar-usuario'), - path('usuarios/verificar///', views.verificar_token, name='verificar_token'), + path('datos-solicitante/', views.solicitante.as_view(), name = 'datos-solicitante'), + path('verificar///', views.verificar_token, name='verificar_token'), ] \ No newline at end of file diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index cb98268..918355f 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -6,10 +6,10 @@ from django.shortcuts import get_object_or_404, redirect from rest_framework import status, permissions from rest_framework.views import APIView from rest_framework.response import Response -from rest_framework.permissions import AllowAny -from .models import Usuario +from rest_framework.permissions import AllowAny, IsAuthenticated +from .models import Usuario, Solicitante from django.contrib import messages -from .serializers import usuario_serializer +from .serializers import usuario_serializer, solicitante_serializer from django.contrib.auth import authenticate from django.core.mail import send_mail from django.conf import settings @@ -132,6 +132,8 @@ class usuario(APIView): 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 @@ -157,12 +159,113 @@ class usuario(APIView): return Response(status=status.HTTP_204_NO_CONTENT) +# Clase para manejar la lógica del solicitante (Creación, edición) +class solicitante(APIView): + # Indicamos que solo los usuarios logeados puedan acceder a esta función + permission_classes = [IsAuthenticated] + + # Petición Get en la que se manejará la lógica del primer login + def get(self, request, *args, **kwargs): + # Extraemos el usuario de la petición + usuario = request.user + # Verificamos que el usuario esté verificado + if not usuario.is_active: + # Si no está activo, negamos el acceso a la plataforma + return Response({"detail": 'Usuario no verificado.'}, status=status.HTTP_403_FORBIDDEN) + try: + # Buscamos al solicitante asociado al usuario + solicitante = Solicitante.objects.get(id=usuario.id) + # En caso de que el solicitante aún no exista + except Solicitante.DoesNotExist: + # Creamos la relación con los datos iniciales del usuario + solicitante = Solicitante.objects.create( + id=usuario.id, + curp=usuario.curp, + nombre=usuario.nombre, + email=usuario.email, + is_active=True, + password=usuario.password + ) + # Solicitamos el llenado del resto de datos del solicitante + return Response({"detail": "Favor de completar sus datos."}, status=status.HTTP_200_OK) + + # Verificamos que los datos del solicitante ya están completos + if not solicitante.datos_completos(): + # Solicitamos al usuario que complete sus datos de solicitante + return Response({"detail": "Favor de completar sus datos."}, status=status.HTTP_200_OK) + + # Si los datos están completos, permitimos el acceso + return redirect('users:usuario-lista-crear') + + # Solicitud post, para manejar el registro de datos de solicitante por medio del serializer + def post(self, request, *args, **kwargs): + # Recuperamos el usuario de la solicitud + usuario = request.user + # Verificamos que el usuario esté verificado + if not usuario.is_active: + # Si no está activo, negamos el acceso a la plataforma + return Response({"detail": 'Usuario no verificado.'}, status=status.HTTP_403_FORBIDDEN) + try: + # Buscamos al solicitante asociado al usuario + solicitante = Solicitante.objects.get(id=usuario.id) + # En caso de que el solicitante aún no exista + except Solicitante.DoesNotExist: + # Creamos la relación con los datos iniciales del usuario + solicitante = Solicitante.objects.create( + id=usuario.id, + curp=usuario.curp, + nombre=usuario.nombre, + email=usuario.email, + is_active=True, + password=usuario.password + ) + # Verificar que los datos del solicitante estén completos + serializer = solicitante_serializer(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 redirect('users:usuario-lista-crear') + # Si no están completos, se solicita que se completen los datos + return Response({"detail": "Favor de completar sus datos."}, 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): + # Recuperamos el usuario de la solicitud + usuario = request.user + # Verificamos que el usuario esté verificado + if not usuario.is_active: + # Si no está activo, negamos el acceso a la plataforma + return Response({"detail": 'Usuario no verificado.'}, status=status.HTTP_403_FORBIDDEN) + + try: + # Buscamos al solicitante asociado al usuario + solicitante = Solicitante.objects.get(id=usuario.id) + except Solicitante.DoesNotExist: + return Response({"detail": "No se encontró al solicitante."}, status=status.HTTP_404_NOT_FOUND) + + # Actualizamos el solicitante con los datos recibidos + serializer = solicitante_serializer(instance=solicitante, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + # Verificamos si los datos están completos después de la actualización + if solicitante.datos_completos(): + return redirect('users:usuario-lista-crear') + return Response({"detail": "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) + + # Función para enviar el correo de confirmación def enviar_correo_verificacion(email, uid, token): # 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}usuarios/verificar/{uid}/{token}/' + message = f'Para activar tu cuenta, haz clic en el siguiente enlace:\n\n{settings.BASE_URL}api/usuarios/verificar/{uid}/{token}/' # extraemos el correo remitente desde la configuración del settings from_email = settings.EMAIL_HOST_USER # indicamos el correo o correos destinatarios -- GitLab From b5061809773504500f45f419e7b59ad8aa57d5fe Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Thu, 4 Jul 2024 14:17:27 -0600 Subject: [PATCH 5/5] Mejoras solicitadas en el modulo de usuarios --- cosiap_api/common/__init__.py | 0 cosiap_api/common/admin.py | 3 + cosiap_api/common/apps.py | 6 + cosiap_api/common/migrations/__init__.py | 0 cosiap_api/common/models.py | 3 + cosiap_api/common/nombres_archivos.py | 18 ++ cosiap_api/common/tests.py | 3 + cosiap_api/common/views.py | 3 + .../0009_solicitante_campos_no_vacios.py | 60 ++++++ cosiap_api/users/models.py | 53 ++---- cosiap_api/users/permisos.py | 28 ++- cosiap_api/users/serializers.py | 17 +- cosiap_api/users/urls.py | 9 +- cosiap_api/users/views.py | 179 ++++++++---------- 14 files changed, 230 insertions(+), 152 deletions(-) create mode 100644 cosiap_api/common/__init__.py create mode 100644 cosiap_api/common/admin.py create mode 100644 cosiap_api/common/apps.py create mode 100644 cosiap_api/common/migrations/__init__.py create mode 100644 cosiap_api/common/models.py create mode 100644 cosiap_api/common/nombres_archivos.py create mode 100644 cosiap_api/common/tests.py create mode 100644 cosiap_api/common/views.py create mode 100644 cosiap_api/users/migrations/0009_solicitante_campos_no_vacios.py diff --git a/cosiap_api/common/__init__.py b/cosiap_api/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cosiap_api/common/admin.py b/cosiap_api/common/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/cosiap_api/common/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/cosiap_api/common/apps.py b/cosiap_api/common/apps.py new file mode 100644 index 0000000..01cca2f --- /dev/null +++ b/cosiap_api/common/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CommonConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'common' diff --git a/cosiap_api/common/migrations/__init__.py b/cosiap_api/common/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cosiap_api/common/models.py b/cosiap_api/common/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/cosiap_api/common/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/cosiap_api/common/nombres_archivos.py b/cosiap_api/common/nombres_archivos.py new file mode 100644 index 0000000..3fa5e3c --- /dev/null +++ b/cosiap_api/common/nombres_archivos.py @@ -0,0 +1,18 @@ +import uuid +import os + +# Función para generar nombre único de archivo +def generar_nombre_archivo(nombre_archivo, path): + ext = nombre_archivo.split('.')[-1] + nombre_unico = f"{uuid.uuid4().hex}.{ext}" + return os.path.join(path, nombre_unico) + +# Funciones para nombre de archivo específico para cada campo de FileField +def nombre_archivo_estado_cuenta(instance, filename): + return generar_nombre_archivo(filename, 'estado_cuenta_files/') + +def nombre_archivo_sat(instance, filename): + return generar_nombre_archivo(filename, 'constancia_sat_files/') + +def nombre_archivo_ine(instance, filename): + return generar_nombre_archivo(filename, 'INE_files/') \ No newline at end of file diff --git a/cosiap_api/common/tests.py b/cosiap_api/common/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/cosiap_api/common/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/cosiap_api/common/views.py b/cosiap_api/common/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/cosiap_api/common/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/cosiap_api/users/migrations/0009_solicitante_campos_no_vacios.py b/cosiap_api/users/migrations/0009_solicitante_campos_no_vacios.py new file mode 100644 index 0000000..0ee3656 --- /dev/null +++ b/cosiap_api/users/migrations/0009_solicitante_campos_no_vacios.py @@ -0,0 +1,60 @@ +# Generated by Django 5.0.6 on 2024-07-04 18:40 + +import common.nombres_archivos +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0008_agregar_choices_regimenfiscal'), + ] + + operations = [ + migrations.AlterField( + model_name='datosbancarios', + name='doc_constancia_sat', + field=models.FileField(blank=True, null=True, upload_to=common.nombres_archivos.nombre_archivo_sat, verbose_name='Constancia Situación Fiscal'), + ), + migrations.AlterField( + model_name='datosbancarios', + name='doc_estado_cuenta', + field=models.FileField(blank=True, null=True, upload_to=common.nombres_archivos.nombre_archivo_estado_cuenta, verbose_name='Estado de cuenta'), + ), + migrations.AlterField( + model_name='solicitante', + name='INE', + field=models.FileField(blank=True, null=True, upload_to=common.nombres_archivos.nombre_archivo_ine, verbose_name='INE'), + ), + migrations.AlterField( + model_name='solicitante', + name='RFC', + field=models.CharField(max_length=13, null=True, unique=True, validators=[django.core.validators.RegexValidator('^([A-ZÑ&]{3,4}) ?(?:- ?)?(\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])) ?(?:- ?)?([A-Z\\d]{2})([A\\d])$', 'Debe ser un RFC válido.')], verbose_name='RFC'), + ), + migrations.AlterField( + model_name='solicitante', + name='ap_paterno', + field=models.CharField(max_length=50, null=True, verbose_name='Apellido Paterno'), + ), + migrations.AlterField( + model_name='solicitante', + name='codigo_postal', + field=models.CharField(max_length=5, null=True, verbose_name='Código Postal'), + ), + migrations.AlterField( + model_name='solicitante', + name='direccion', + field=models.CharField(max_length=255, null=True, verbose_name='Dirección'), + ), + migrations.AlterField( + model_name='solicitante', + name='poblacion', + field=models.CharField(max_length=255, null=True, verbose_name='Población'), + ), + migrations.AlterField( + model_name='solicitante', + name='telefono', + field=models.CharField(max_length=10, null=True, verbose_name='Teléfono'), + ), + ] diff --git a/cosiap_api/users/models.py b/cosiap_api/users/models.py index 265f458..da9d7c8 100644 --- a/cosiap_api/users/models.py +++ b/cosiap_api/users/models.py @@ -2,9 +2,9 @@ from django.db import models from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager from django.core.validators import RegexValidator from django.contrib.auth.models import PermissionsMixin -from django.core.exceptions import ValidationError -import uuid -import os +from django.core.exceptions import ValidationError +from common.nombres_archivos import nombre_archivo_estado_cuenta, nombre_archivo_ine, nombre_archivo_sat + # modelo con la información de los estados de la república class Estado(models.Model): @@ -61,24 +61,14 @@ class DatosBancarios(models.Model): # clabe interbancaria clabe_bancaria = models.CharField(verbose_name="Clabe bancaria", max_length=16) # archivo de el estado de cuenta - doc_estado_cuenta = models.FileField(verbose_name="Estado de cuenta", upload_to= "estados_cuenta_files/", null=True, blank=True) + doc_estado_cuenta = models.FileField(verbose_name="Estado de cuenta", upload_to= nombre_archivo_estado_cuenta, null=True, blank=True) # archivo de el comporbante de situación fiscal - doc_constancia_sat = models.FileField(verbose_name="Constancia Situación Fiscal", upload_to="constancias_sat_files/", null=True, blank=True) + doc_constancia_sat = models.FileField(verbose_name="Constancia Situación Fiscal", upload_to= nombre_archivo_sat , null=True, blank=True) # código postal fiscal codigo_postal_fiscal = models.CharField(verbose_name="Código Postal Físcal", max_length=5) # regimen fiscal al que pertenece regimen = models.CharField(max_length=255, choices=REGIMEN_CHOICES) - # sobreescribimos el método save para la lógica del nombre del archivo - def save(self, *args, **kwargs): - # Asignar un nombre único al estado de cuenta - if self.doc_estado_cuenta: - self.doc_estado_cuenta.name = generar_nombre_archivo(self.doc_estado_cuenta.name) - # Asignar un nombre único a la constancia sat - if self.doc_constancia_sat: - self.doc_constancia_sat.name = generar_nombre_archivo(self.doc_constancia_sat.name) - super(DatosBancarios, self).save(*args, **kwargs) - class UsuarioManager(BaseUserManager): def create_user(self, email, curp, nombre, password=None, is_admin=False, is_staff=False, is_active=False): @@ -140,37 +130,30 @@ class Solicitante(Usuario): RFC_REGEX = r'^([A-ZÑ&]{3,4}) ?(?:- ?)?(\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])) ?(?:- ?)?([A-Z\d]{2})([A\d])$' # campos para los apellidos - ap_paterno = models.CharField(verbose_name='Apellido Paterno',max_length=50) + ap_paterno = models.CharField(verbose_name='Apellido Paterno',max_length=50, null=True, blank=False) # campo de apellido materno es opcional ap_materno = models.CharField(verbose_name='Apellido Materno', max_length=50, null=True, blank=True) # campo para telefono, longitud maxima de 10 - telefono = models.CharField(verbose_name='Teléfono', max_length=10) + telefono = models.CharField(verbose_name='Teléfono', max_length=10, null=True, blank=False) # campo para el RFC, longitud de 13 - RFC = models.CharField(verbose_name='RFC', max_length=13, validators=[RegexValidator(RFC_REGEX,'Debe ser un RFC válido.')], unique=True) + RFC = models.CharField(verbose_name='RFC', max_length=13, validators=[RegexValidator(RFC_REGEX,'Debe ser un RFC válido.')], unique=True, null=True, blank=False) # campo para la dirección - direccion = models.CharField(verbose_name='Dirección', max_length=255) + direccion = models.CharField(verbose_name='Dirección', max_length=255, null=True, blank=False) # campo para el código postal, longitud 5 - codigo_postal = models.CharField(verbose_name='Código Postal', max_length=5) + codigo_postal = models.CharField(verbose_name='Código Postal', max_length=5, null=True, blank=False) # relación con el municipio mediante llave foránea municipio = models.ForeignKey(Municipio, verbose_name="Municipio",null=True, blank=False, on_delete=models.CASCADE) # campo para la poblacion - poblacion = models.CharField(verbose_name='Población', max_length=255) + poblacion = models.CharField(verbose_name='Población', max_length=255, null=True, blank=False) # 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='ine_files/', 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 - - - # sobreescribimos el método save para la lógica del nombre del archivo - def save(self, *args, **kwargs): - # Asignar un nombre único al archivo INE - if self.INE: - self.INE.name = generar_nombre_archivo(self.INE.name) - super(Solicitante, self).save(*args, **kwargs) + @property # Método para determinar si todos los datos del solicitante estan completos def datos_completos(self): # campos requeridos @@ -178,15 +161,7 @@ class Solicitante(Usuario): # 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 retona falso + # si se encuentra un dato vacío se retorna falso return False # si todos los datos estan llenos, retornamos un True return True - -# Método para generar un nombre único a los archivos del usuario -def generar_nombre_archivo(nombre_archivo): - # Obtener la extensión del archivo - ext = nombre_archivo.split('.')[-1] - # Generar un nombre único usando uuid4 - nombre_unico= f"{uuid.uuid4().hex}.{ext}" - return os.path.join('ine_files/', nombre_unico) \ No newline at end of file diff --git a/cosiap_api/users/permisos.py b/cosiap_api/users/permisos.py index 9b3dc4f..6382d66 100644 --- a/cosiap_api/users/permisos.py +++ b/cosiap_api/users/permisos.py @@ -1,7 +1,33 @@ from rest_framework import permissions +from .models import Solicitante # Funcionalidad para verificar que el usuario que realiza la eliminación sea un admin class es_admin(permissions.BasePermission): # método para determinar que un usuario tiene permisos def has_permission( self, request, view): - return request.user and request.user.is_superuser \ No newline at end of file + return request.user and request.user.is_superuser + +# Clase para verificar que el usuario que va a ingresar tenga sus datos completos +class primer_login(permissions.BasePermission): + # método para determinar si se requiere o no un primer login + def has_permission(self, request, view): + # obtenemos al usuario de la request + usuario = request.user + # verificamos que el usuario no sea un administrador + if not usuario.is_staff: + # tratamos de extraer su objeto de solicitante + try: + solicitante = Solicitante.objects.get(id=usuario.id) + # si no se encontró + except Solicitante.DoesNotExist: + # No se da acceso a la plataforma, por tanto se requiere el primer login + return False + # en caso de que se encuentre al solicitante, si tiene sus datos completos, tendrá acceso + # si no los tiene completos, se requiere primer login + return solicitante.datos_completos + else: + # Si es admin, tiene acceso + return True + + + diff --git a/cosiap_api/users/serializers.py b/cosiap_api/users/serializers.py index 3ed56f2..4b47cac 100644 --- a/cosiap_api/users/serializers.py +++ b/cosiap_api/users/serializers.py @@ -59,18 +59,10 @@ class solicitante_serializer(serializers.ModelSerializer): model = Solicitante # indicamos los campos que debe ingresar el usuario fields = ['ap_paterno', 'ap_materno', 'telefono', 'RFC', 'direccion', 'codigo_postal', 'municipio', 'poblacion', 'INE'] - # De momento todos los campos son opcionales - extra_kwargs = { - 'ap_paterno': {'required':False, 'allow_null': True}, - 'ap_materno': {'required': False, 'allow_null': True}, - 'telefono': {'required': False, 'allow_null': True}, - 'RFC': {'required': False, 'allow_null': True}, - 'direccion': {'required': False, 'allow_null': True}, - 'codigo_postal': {'required': False, 'allow_null': True}, - 'municipio': {'required': False, 'allow_null': True}, - 'poblacion': {'required': False, 'allow_null': True}, - 'INE': {'required': False, 'allow_null': True}, - 'datos_bancarios': {'required': False, 'allow_null': True} + # Agregamos validadores para asegurar que los campos requeridos no estén vacíos + extra_kwargs = {'ap_paterno': {'required': True}, 'ap_materno': {'required': False},'telefono': {'required': True},'RFC': {'required': True}, + 'direccion': {'required': True},'codigo_postal': {'required': True},'municipio': {'required': True},'poblacion': {'required': True}, + 'INE': {'required': True} } # Definimos una función para crear al Solicitante @@ -88,6 +80,7 @@ class solicitante_serializer(serializers.ModelSerializer): # Definimos una función para la actualización del solicitante, recibiendo una instancia def update(self, instance, validated_data): # Actualizar los campos del Solicitante + instance.nombre = validated_data.get('nombre', instance.nombre) instance.ap_paterno = validated_data.get('ap_paterno', instance.ap_paterno) instance.ap_materno = validated_data.get('ap_materno', instance.ap_materno) instance.telefono = validated_data.get('telefono', instance.telefono) diff --git a/cosiap_api/users/urls.py b/cosiap_api/users/urls.py index 4fc205e..ff9b857 100644 --- a/cosiap_api/users/urls.py +++ b/cosiap_api/users/urls.py @@ -24,8 +24,9 @@ urlpatterns = [ path('token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'), path('token/refresh/', CustomTokenRefreshView.as_view(), name='token_refresh'), - path('usuarios/', views.usuario.as_view(), name = 'usuario-lista-crear'), - path('usuarios//', views.usuario.as_view(), name = 'ver-eliminar-usuario'), - path('datos-solicitante/', views.solicitante.as_view(), name = 'datos-solicitante'), - path('verificar///', views.verificar_token, name='verificar_token'), + path('', views.usuario.as_view(), name = 'usuario-lista-crear'), + path('/', views.usuario.as_view(), name = 'ver-eliminar-usuario'), + path('solicitantes/', views.solicitante.as_view(), name = 'solicitante-list'), + path('solicitantes/', views.solicitante.as_view(), name = 'solicitante-detail'), + path('verificar-correo///', views.verificar_token, name='verificar_token'), ] \ No newline at end of file diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index 918355f..62af2ed 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -22,7 +22,7 @@ from rest_framework.response import Response from rest_framework import status from django.conf import settings from datetime import datetime, timedelta -from .permisos import es_admin +from .permisos import es_admin, primer_login class CustomTokenObtainPairView(TokenObtainPairView): def post(self, request, *args, **kwargs): @@ -52,39 +52,55 @@ class CustomTokenRefreshView(TokenRefreshView): response.delete_cookie('refresh_token') return response -# Funcionalidad para crear un usuario en el sistema, ver sus datos o eliminarlo -class usuario(APIView): - # permitimos que cualquier persona que haga una solicitud pueda crear un usuario + +# Clase abstracta para la verificación de los permisos de usuario +class BasePermissionAPIView(APIView): + # permisos para la creación de nuevos usuarios permission_classes_create = [AllowAny] - # permitimos solo a administradores eliminar un usuario + # permisos para eliminar usuarios permission_classes_delete = [es_admin] - # permitimos solo administradores vean la lista de usuarios + # permisos para listar o ver detalles de usuarios permission_classes_list = [es_admin] + # permisos para edición de usuarios + permission_classes_update = [IsAuthenticated] - # función para verificar los permisos de la solicitud - def check_permissions( self, request ): - # verificamos si la solicitud fue hecha mediante delete + # Método para verificar los permisos de ususario + def check_permissions(self, request): + # Si se envió una solicitud de tipo DELETE if request.method == 'DELETE': - # recorremos la lista de permisos de eliminación predefinada + # recorremos los permisos de la lista de permisos de eliminación for permission in self.permission_classes_delete: - # si el usuario no cuenta con permisos + # si no existe el permiso adecuado if not permission().has_permission(request, self): - # denegamos los permisos para la eliminación - self.permission_denied( - request, - message=getattr(permission, 'message', None) - ) + # negamos el acceso a la view + self.permission_denied(request, message=getattr(permission, 'message', None)) + # Si se envió una solicitud de tipo GET elif request.method == 'GET': - # recorremos la lista de permisos de visualización predefinada + # recorremos los permisos de la lista de permisos de visualización for permission in self.permission_classes_list: - # si el usuario no cuenta con permisos + # si no existe el permiso adecuado + if not permission().has_permission(request, self): + # negamos el acceso a la view + self.permission_denied(request, message=getattr(permission, 'message', None)) + # Si se envió una solicitud de tipo POST + elif request.method == 'POST': + # recorremos los permisos de la lista de permisos de creación + for permission in self.permission_classes_create: + # si no existe el permiso adecuado + if not permission().has_permission(request, self): + # negamos el acceso a la view + self.permission_denied(request, message=getattr(permission, 'message', None)) + # Si se envió una solicitud de tipo PUT + elif request.method == 'PUT': + # recorremos los permisos de la lista de permisos de edición + for permission in self.permission_classes_update: + # si no existe el permiso adecuado if not permission().has_permission(request, self): - # denegamos los permisos para la eliminación - self.permission_denied( - request, - message=getattr(permission, 'message', None) - ) + # negamos el acceso a la view + self.permission_denied(request, message=getattr(permission, 'message', None)) +# Funcionalidad para crear un usuario en el sistema, ver sus datos o eliminarlo +class usuario(BasePermissionAPIView): # Función para la obtención de la lista de usuarios def get( self, request, *args, **kwargs ): # si se quiere observar solo un usuario @@ -160,74 +176,56 @@ class usuario(APIView): # Clase para manejar la lógica del solicitante (Creación, edición) -class solicitante(APIView): +class solicitante(BasePermissionAPIView): # Indicamos que solo los usuarios logeados puedan acceder a esta función - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated, primer_login] + permission_classes_create = [IsAuthenticated] - # Petición Get en la que se manejará la lógica del primer login + # Petición get para listar a los solicitantes def get(self, request, *args, **kwargs): - # Extraemos el usuario de la petición - usuario = request.user - # Verificamos que el usuario esté verificado - if not usuario.is_active: - # Si no está activo, negamos el acceso a la plataforma - return Response({"detail": 'Usuario no verificado.'}, status=status.HTTP_403_FORBIDDEN) - try: - # Buscamos al solicitante asociado al usuario - solicitante = Solicitante.objects.get(id=usuario.id) - # En caso de que el solicitante aún no exista - except Solicitante.DoesNotExist: - # Creamos la relación con los datos iniciales del usuario - solicitante = Solicitante.objects.create( - id=usuario.id, - curp=usuario.curp, - nombre=usuario.nombre, - email=usuario.email, - is_active=True, - password=usuario.password - ) - # Solicitamos el llenado del resto de datos del solicitante - return Response({"detail": "Favor de completar sus datos."}, status=status.HTTP_200_OK) - - # Verificamos que los datos del solicitante ya están completos - if not solicitante.datos_completos(): - # Solicitamos al usuario que complete sus datos de solicitante - return Response({"detail": "Favor de completar sus datos."}, status=status.HTTP_200_OK) - - # Si los datos están completos, permitimos el acceso - return redirect('users:usuario-lista-crear') + # 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 = solicitante_serializer(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 = solicitante_serializer(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): - # Recuperamos el usuario de la solicitud + # obtenemos al usuario del request usuario = request.user - # Verificamos que el usuario esté verificado - if not usuario.is_active: - # Si no está activo, negamos el acceso a la plataforma - return Response({"detail": 'Usuario no verificado.'}, status=status.HTTP_403_FORBIDDEN) - try: - # Buscamos al solicitante asociado al usuario - solicitante = Solicitante.objects.get(id=usuario.id) - # En caso de que el solicitante aún no exista - except Solicitante.DoesNotExist: - # Creamos la relación con los datos iniciales del usuario - solicitante = Solicitante.objects.create( - id=usuario.id, - curp=usuario.curp, - nombre=usuario.nombre, - email=usuario.email, - is_active=True, - password=usuario.password - ) + # creamos el solicitante + solicitante, created = Solicitante.objects.get_or_create( + id=usuario.id, + defaults={ + 'curp': usuario.curp, + 'nombre': usuario.nombre, + 'email': usuario.email, + 'is_active': True, + 'password': usuario.password + } + ) # Verificar que los datos del solicitante estén completos serializer = solicitante_serializer(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(): + if solicitante.datos_completos: # Permitimos el acceso - return redirect('users:usuario-lista-crear') + return Response({"detail": "Acesso permitido."}, status=status.HTTP_200_OK) # Si no están completos, se solicita que se completen los datos return Response({"detail": "Favor de completar sus datos."}, status=status.HTTP_200_OK) # Si hay errores en los datos, enviamos un bad request @@ -235,26 +233,15 @@ class solicitante(APIView): # Solicitud put para actualizar los datos del solicitante def put(self, request, *args, **kwargs): - # Recuperamos el usuario de la solicitud - usuario = request.user - # Verificamos que el usuario esté verificado - if not usuario.is_active: - # Si no está activo, negamos el acceso a la plataforma - return Response({"detail": 'Usuario no verificado.'}, status=status.HTTP_403_FORBIDDEN) - - try: - # Buscamos al solicitante asociado al usuario - solicitante = Solicitante.objects.get(id=usuario.id) - except Solicitante.DoesNotExist: - return Response({"detail": "No se encontró al solicitante."}, status=status.HTTP_404_NOT_FOUND) - - # Actualizamos el solicitante con los datos recibidos - serializer = solicitante_serializer(instance=solicitante, data=request.data, partial=True) + # 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 = solicitante_serializer(instance=solicitante, data=request.data) + # si el serilizer es valido if serializer.is_valid(): + # guardamos los cambios serializer.save() - # Verificamos si los datos están completos después de la actualización - if solicitante.datos_completos(): - return redirect('users:usuario-lista-crear') + # enviamos el mensaje de la actualización de los datos return Response({"detail": "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) @@ -265,7 +252,7 @@ def enviar_correo_verificacion(email, uid, token): # 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/{uid}/{token}/' + 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 -- GitLab