From cb482970561730a62a65a497924d355a75806058 Mon Sep 17 00:00:00 2001 From: RafaUC Date: Tue, 9 Jul 2024 12:32:40 -0600 Subject: [PATCH] creacion de los modelos de dynamic_formats, modalidades, minuta y convenio --- cosiap_api/common/models.py | 25 ++++++ cosiap_api/common/nombres_archivos.py | 28 ++++-- cosiap_api/common/validadores_campos.py | 18 ++++ cosiap_api/cosiap_api/consumers.py | 27 ++++++ cosiap_api/cosiap_api/settings.py | 5 +- cosiap_api/cosiap_api/wsgi.py | 22 ++++- ...namic_formats__modalidades__solicitudes.py | 28 ++++++ cosiap_api/dynamic_formats/models.py | 21 ++++- cosiap_api/entrypoint.sh | 3 +- ...namic_formats__modalidades__solicitudes.py | 45 ++++++++++ cosiap_api/modalidades/models.py | 61 ++++++++++++- .../commands/delete_old_notifications.py | 16 ++++ ...namic_formats__modalidades__solicitudes.py | 46 ++++++++++ cosiap_api/notificaciones/models.py | 71 +++++++++++++++- ...namic_formats__modalidades__solicitudes.py | 40 +++++++++ cosiap_api/solicitudes/models.py | 85 ++++++++++++++++++- cosiap_api/solicitudes/signals.py | 4 + cosiap_api/users/permisos.py | 1 + cosiap_api/users/serializers.py | 6 +- cosiap_api/users/views.py | 10 +-- 20 files changed, 541 insertions(+), 21 deletions(-) create mode 100644 cosiap_api/common/validadores_campos.py create mode 100644 cosiap_api/cosiap_api/consumers.py create mode 100644 cosiap_api/dynamic_formats/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py create mode 100644 cosiap_api/modalidades/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py create mode 100644 cosiap_api/notificaciones/commands/delete_old_notifications.py create mode 100644 cosiap_api/notificaciones/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py create mode 100644 cosiap_api/solicitudes/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py create mode 100644 cosiap_api/solicitudes/signals.py diff --git a/cosiap_api/common/models.py b/cosiap_api/common/models.py index 71a8362..63e0c5a 100644 --- a/cosiap_api/common/models.py +++ b/cosiap_api/common/models.py @@ -1,3 +1,28 @@ from django.db import models # Create your models here. +class SingletonModel(models.Model): + ''' + Modelo Singleton para objetos que solo necesitan una instancia + ''' + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + """ + Guarda el objeto en la base de datos y elimina los dema si existen + """ + self.__class__.objects.exclude(id=self.id).delete() + super(SingletonModel, self).save(*args, **kwargs) + + @classmethod + def get_object(cls): + """ + Carga el objeto de la base de datos. Si no existe ningun objeto + se crea uno nuevo sin guardar y lo devuelve. + """ + try: + return cls.objects.get() + except cls.DoesNotExist: + return cls() diff --git a/cosiap_api/common/nombres_archivos.py b/cosiap_api/common/nombres_archivos.py index 3fa5e3c..73df422 100644 --- a/cosiap_api/common/nombres_archivos.py +++ b/cosiap_api/common/nombres_archivos.py @@ -1,11 +1,17 @@ -import uuid +from uuid import uuid4 +from django.conf import settings import os # Función para generar nombre único de archivo -def generar_nombre_archivo(nombre_archivo, path): +def generar_nombre_archivo(nombre_archivo, path, protected=True): + ''' + Genera la ruta de un archivo de media con un nombre unico aleatorio. + ''' ext = nombre_archivo.split('.')[-1] - nombre_unico = f"{uuid.uuid4().hex}.{ext}" - return os.path.join(path, nombre_unico) + nombre_unico = f"{uuid4().hex}.{ext}" + if protected: + os.path.join('protected_uploads/', path) + return os.path.join(settings.MEDIA_ROOT, path, nombre_unico) # Funciones para nombre de archivo específico para cada campo de FileField def nombre_archivo_estado_cuenta(instance, filename): @@ -15,4 +21,16 @@ 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 + return generar_nombre_archivo(filename, 'INE_files/') + +def nombre_archivo_minuta(instance, filename): + return generar_nombre_archivo(filename, 'minutas/',) + +def nombre_archivo_convenio(instance, filename): + return generar_nombre_archivo(filename, 'convenios/',) + +def nombre_archivo_modalidad(instance, filename): + return generar_nombre_archivo(filename, 'modalidades/', protected=False) + +def nombre_archivo_formato(instance, filename): + return generar_nombre_archivo(filename, 'formatos/', protected=False) \ No newline at end of file diff --git a/cosiap_api/common/validadores_campos.py b/cosiap_api/common/validadores_campos.py new file mode 100644 index 0000000..e4bd9ec --- /dev/null +++ b/cosiap_api/common/validadores_campos.py @@ -0,0 +1,18 @@ +import os +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + +def validador_pdf(value): + ''' + Validador para campos de archivo que verifica que su extencion sea un pdf + ''' + ext = os.path.splitext(value.name)[1] # Obtener la extensión del archivo + valid_extensions = ['.pdf'] # Lista de extensiones permitidas + if ext.lower() not in valid_extensions: + raise ValidationError(_('Sólo se permiten archivos en formato PDF.')) + +def validador_archivo_1MB(value): + '''Validador que valida que un archivo subido no sobrepase el tamaño de 1MB''' + filesize = value.size + if filesize > 1048576: # 1MB + raise ValidationError("El archivo es demasiado grande. El tamaño máximo permitido es 1MB.") \ No newline at end of file diff --git a/cosiap_api/cosiap_api/consumers.py b/cosiap_api/cosiap_api/consumers.py new file mode 100644 index 0000000..b9648d5 --- /dev/null +++ b/cosiap_api/cosiap_api/consumers.py @@ -0,0 +1,27 @@ +import json +from channels.generic.websocket import AsyncWebsocketConsumer + +class NotificacionConsumer(AsyncWebsocketConsumer): + async def connect(self): + await self.accept() + # Asociar la conexión WebSocket con el usuario + self.user = self.scope["user"] + await self.channel_layer.group_add( + f"user_{self.user.id}", + self.channel_name + ) + + async def disconnect(self, close_code): + # Eliminar la asociación de la conexión WebSocket cuando se desconecta + await self.channel_layer.group_discard( + f"user_{self.user.id}", + self.channel_name + ) + + async def notificar_notificacion(self, event): + mensaje = event['mensaje'] + + # Envía el mensaje al cliente + await self.send(text_data=json.dumps({ + 'mensaje': mensaje + })) diff --git a/cosiap_api/cosiap_api/settings.py b/cosiap_api/cosiap_api/settings.py index aa384da..3341e2c 100644 --- a/cosiap_api/cosiap_api/settings.py +++ b/cosiap_api/cosiap_api/settings.py @@ -148,7 +148,7 @@ EMAIL_USE_TLS = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.0/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles/') STATICFILES_DIRS = ( @@ -181,7 +181,8 @@ REST_FRAMEWORK = { ), 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated' + 'rest_framework.permissions.IsAuthenticated', + 'users.permisos.primer_login' #'rest_framework.permissions.AllowAny' ], } diff --git a/cosiap_api/cosiap_api/wsgi.py b/cosiap_api/cosiap_api/wsgi.py index 809e51c..05a5542 100644 --- a/cosiap_api/cosiap_api/wsgi.py +++ b/cosiap_api/cosiap_api/wsgi.py @@ -10,7 +10,27 @@ https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ import os from django.core.wsgi import get_wsgi_application +from django.core.asgi import get_asgi_application +from channels.routing import ProtocolTypeRouter, URLRouter +from django.core.asgi import get_asgi_application + +from channels.auth import AuthMiddlewareStack +from django.urls import re_path +from .consumers import NotificacionConsumer os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cosiap_api.settings') -application = get_wsgi_application() +django_asgi_app = get_asgi_application() + +websocket_urlpatterns = [ + re_path(r'ws/notificaciones/$', NotificacionConsumer.as_asgi()), +] + +application = ProtocolTypeRouter({ + "http": django_asgi_app, + "websocket": AuthMiddlewareStack( + URLRouter( + websocket_urlpatterns + ) + ), +}) diff --git a/cosiap_api/dynamic_formats/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py b/cosiap_api/dynamic_formats/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py new file mode 100644 index 0000000..6a57583 --- /dev/null +++ b/cosiap_api/dynamic_formats/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.6 on 2024-07-09 16:29 + +import common.nombres_archivos +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='DynamicFormat', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nombre', models.CharField(max_length=255, verbose_name='Nombre')), + ('template', models.FileField(upload_to=common.nombres_archivos.nombre_archivo_formato, verbose_name='Plantilla')), + ], + options={ + 'verbose_name': 'Formato Dinámico', + 'verbose_name_plural': 'Formatos Dinámicos', + 'ordering': ['nombre'], + }, + ), + ] diff --git a/cosiap_api/dynamic_formats/models.py b/cosiap_api/dynamic_formats/models.py index 71a8362..222fae0 100644 --- a/cosiap_api/dynamic_formats/models.py +++ b/cosiap_api/dynamic_formats/models.py @@ -1,3 +1,22 @@ from django.db import models +from common.nombres_archivos import nombre_archivo_formato -# Create your models here. +class DynamicFormat(models.Model): + ''' + Modelo que contiene la información de los formatos dinámicos. + + Campos: + - **nombre**: Nombre del formato. + - **template**: Archivo de plantilla asociado al formato. + ''' + + nombre = models.CharField(max_length=255, verbose_name="Nombre") + template = models.FileField(upload_to=nombre_archivo_formato, verbose_name="Plantilla") + + def __str__(self): + return self.nombre + + class Meta: + verbose_name = "Formato Dinámico" + verbose_name_plural = "Formatos Dinámicos" + ordering = ['nombre'] diff --git a/cosiap_api/entrypoint.sh b/cosiap_api/entrypoint.sh index 7f6735d..914602a 100644 --- a/cosiap_api/entrypoint.sh +++ b/cosiap_api/entrypoint.sh @@ -16,7 +16,8 @@ done #Django commands -#python3 manage.py makemigrations +>&2 echo "Ejecutando Migraciones" +#python3 manage.py makemigrations --name python3 manage.py migrate #python3 manage.py collectstatic --no-input diff --git a/cosiap_api/modalidades/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py b/cosiap_api/modalidades/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py new file mode 100644 index 0000000..8728738 --- /dev/null +++ b/cosiap_api/modalidades/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py @@ -0,0 +1,45 @@ +# Generated by Django 5.0.6 on 2024-07-09 16:29 + +import common.nombres_archivos +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Modalidad', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nombre', models.CharField(max_length=255, verbose_name='Nombre')), + ('imagen', models.ImageField(upload_to=common.nombres_archivos.nombre_archivo_modalidad, verbose_name='Imagen')), + ('descripcion', models.TextField(verbose_name='Descripción')), + ('mostrar', models.BooleanField(default=True)), + ('archivado', models.BooleanField(default=False)), + ], + options={ + 'ordering': ['nombre'], + }, + ), + migrations.CreateModel( + name='MontoModalidad', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('monto', models.FloatField(default=0.0, verbose_name='Monto')), + ('fecha_inicio', models.DateTimeField(auto_now_add=True, verbose_name='Fecha de Inicio')), + ('fecha_fin', models.DateTimeField(blank=True, null=True, verbose_name='Fecha de Fin')), + ('modalidad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='modalidades.modalidad', verbose_name='Modalidad')), + ], + options={ + 'verbose_name': 'Monto de Modalidad', + 'verbose_name_plural': 'Montos de Modalidades', + 'ordering': ['modalidad', '-fecha_inicio'], + }, + ), + ] diff --git a/cosiap_api/modalidades/models.py b/cosiap_api/modalidades/models.py index 71a8362..d43d3fd 100644 --- a/cosiap_api/modalidades/models.py +++ b/cosiap_api/modalidades/models.py @@ -1,3 +1,62 @@ from django.db import models +from common.validadores_campos import validador_pdf +from common.nombres_archivos import nombre_archivo_modalidad +from django.utils import timezone -# Create your models here. + +class Modalidad(models.Model): + ''' + Modelo que contiene la informacion de una modalidad. + + Campos: + - nombre: Nombre de la modalidad. Es un campo de texto con un máximo de 255 caracteres. + - imagen: Imagen asociada a la modalidad. + - descripcion: Descripción de la modalidad. Es un campo de texto. + - mostrar: Booleano que indica si la modalidad se debe mostrar o no. Por defecto es True. + - archivado: Booleano que indica si la modalidad está archivada. Por defecto es False. + - dynamic_form: Campo reservado para formularios dinámicos (actualmente no se usa). + ''' + nombre = models.CharField(max_length=255, verbose_name="Nombre", null=False) + imagen = models.ImageField(upload_to=nombre_archivo_modalidad, verbose_name="Imagen", null=False) + descripcion = models.TextField(verbose_name="Descripción", null=False) + mostrar = models.BooleanField(default=True) + archivado = models.BooleanField(default=False) + dynamic_form = None + + def __str__(self): + return f'{self.nombre} ({self.tipo})' + + class Meta: + ordering = ['nombre'] + +class MontoModalidad(models.Model): + ''' + Modelo que contiene los valores de los montos de las modalidades a través del tiempo. + + Campos: + - modalidad: Instancia de Modalidad a la que pertenece el monto. + - monto: Número flotante que indica el valor del monto de esa modalidad en ese periodo. Por defecto es 0.0. + - fecha_inicio: Fecha en la que comenzó a estar vigente el monto. Campo autogenerado. + - fecha_fin: Fecha en la que dejó de estar vigente el monto. Por defecto es None. Campo autogenerado: si existe algún MontoModalidad cuya fecha de fin esté sin definir, al momento de crearse un nuevo MontoModalidad para la misma Modalidad, el campo indefinido de ese MontoModalidad se define a la fecha actual. + ''' + modalidad = models.ForeignKey(Modalidad, on_delete=models.CASCADE, verbose_name="Modalidad") + monto = models.FloatField(default=0.0, verbose_name="Monto") + fecha_inicio = models.DateTimeField(auto_now_add=True, verbose_name="Fecha de Inicio") + fecha_fin = models.DateTimeField(null=True, blank=True, verbose_name="Fecha de Fin") + + def save(self, *args, **kwargs): + # Autogenerar la fecha de fin del MontoModalidad anterior + if not self.pk: # Solo aplica para nuevas instancias + ultimo_monto = MontoModalidad.objects.filter(modalidad=self.modalidad, fecha_fin__isnull=True).last() + if ultimo_monto: + ultimo_monto.fecha_fin = timezone.now() + ultimo_monto.save() + super(MontoModalidad, self.save(*args, **kwargs)) + + def __str__(self): + return f'{self.modalidad.nombre} - {self.monto}' + + class Meta: + verbose_name = "Monto de Modalidad" + verbose_name_plural = "Montos de Modalidades" + ordering = ['modalidad', '-fecha_inicio'] \ No newline at end of file diff --git a/cosiap_api/notificaciones/commands/delete_old_notifications.py b/cosiap_api/notificaciones/commands/delete_old_notifications.py new file mode 100644 index 0000000..26b5b72 --- /dev/null +++ b/cosiap_api/notificaciones/commands/delete_old_notifications.py @@ -0,0 +1,16 @@ +from django.core.management.base import BaseCommand +from datetime import timedelta +from django.utils import timezone +from mensajes.models import Notificacion + +class Command(BaseCommand): + help = 'Elimina registros más antiguos de 3 meses' + + def handle(self, *args, **options): + borrar_notificaciones_viejas() + self.stdout.write(self.style.SUCCESS('Registros eliminados con éxito')) + +def borrar_notificaciones_viejas(): + print('Borrando notificaciones mas antiguas a 3 meses') + three_months_ago = timezone.now() - timedelta(days=90) + Notificacion.objects.filter(timestamp__lt=three_months_ago).delete() diff --git a/cosiap_api/notificaciones/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py b/cosiap_api/notificaciones/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py new file mode 100644 index 0000000..67c313b --- /dev/null +++ b/cosiap_api/notificaciones/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py @@ -0,0 +1,46 @@ +# Generated by Django 5.0.6 on 2024-07-09 16:29 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Notificacion', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('leido', models.BooleanField(default=False)), + ('titulo', models.CharField(blank=True, default='Sistema de Apoyos COZCYT', max_length=255, null=True)), + ('mensaje', models.TextField()), + ('urlName', models.CharField(blank=True, max_length=255, null=True)), + ('urlArgs', models.JSONField(blank=True, null=True)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('usuario', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Usuario')), + ], + options={ + 'ordering': ['usuario', '-timestamp'], + }, + ), + migrations.CreateModel( + name='NotifInboxLastOpened', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(auto_now=True, verbose_name='Fecha y hora de última apertura')), + ('usuario', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Usuario')), + ], + options={ + 'verbose_name': 'Última apertura de bandeja de entrada', + 'verbose_name_plural': 'Últimas aperturas de bandeja de entrada', + 'ordering': ['-timestamp'], + }, + ), + ] diff --git a/cosiap_api/notificaciones/models.py b/cosiap_api/notificaciones/models.py index 71a8362..c8f173b 100644 --- a/cosiap_api/notificaciones/models.py +++ b/cosiap_api/notificaciones/models.py @@ -1,3 +1,72 @@ from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver -# Create your models here. +from asgiref.sync import async_to_sync +from channels.layers import get_channel_layer +from django.urls import reverse +from django.conf import settings + + +class NotifInboxLastOpened(models.Model): + ''' + Modelo que registra la última vez que un usuario abrió su bandeja de entrada de notificaciones. + + Campos: + - **usuario**: Usuario al que pertenece el registro. Debe ser único. + - **timestamp**: Fecha y hora en que el usuario abrió su bandeja de entrada por última vez. Se autogenera cuando se crea o actualiza el registro. + ''' + + usuario = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, unique=True, verbose_name="Usuario") + timestamp = models.DateTimeField(auto_now=True, verbose_name="Fecha y hora de última apertura") + + def __str__(self): + return f'Última apertura de {self.usuario.username} en {self.timestamp}' + + class Meta: + verbose_name = "Última apertura de bandeja de entrada" + verbose_name_plural = "Últimas aperturas de bandeja de entrada" + ordering = ['-timestamp'] + +class Notificacion(models.Model): + ''' + Modelo que contiene la información de las notificaciones. + + Campos: + - *usuario*: Usuario al que va dirigida la notificación. + - *leido*: Booleano que indica si el mensaje ya ha sido abierto. Por defecto es False. + - *titulo*: Título de la notificación. Por defecto es "Sistema de Apoyos COZCYT". Puede estar en blanco o ser nulo. + - *mensaje*: Mensaje de la notificación. + - *urlName*: Nombre de la URL asociada a la notificación. Puede estar en blanco o ser nulo. + - *urlArgs*: Argumentos de la URL en formato JSON. Puede estar en blanco o ser nulo. + - *timestamp*: Fecha y hora en que la notificación fue creada. Se autogenera cuando se crea la notificación. + ''' + TITULO_DEFAULT = 'Sistema de Apoyos COZCYT' + + usuario = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="Usuario") + leido = models.BooleanField(default=False) + titulo = models.CharField(max_length=255, blank=True, null=True, default=TITULO_DEFAULT) + mensaje = models.TextField() + urlName = models.CharField(max_length=255, blank=True, null=True) + urlArgs = models.JSONField(blank=True, null=True) + timestamp = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['usuario', '-timestamp'] + + def __str__(self): + return f'Notificación de {self.usuario} - {self.timestamp}' + +@receiver(post_save, sender=Notificacion) +def notificar_nueva_notificacion(sender, instance, created, **kwargs): + if created: + if instance.usuario.is_authenticated: + # Lógica para enviar la notificación al usuario + channel_layer = get_channel_layer() + async_to_sync(channel_layer.group_send)( + f'user_{instance.usuario.id}', + { + 'type': 'notificar_notificacion', + 'mensaje': 'nuevaNotificacion', + } + ) \ No newline at end of file diff --git a/cosiap_api/solicitudes/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py b/cosiap_api/solicitudes/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py new file mode 100644 index 0000000..e6b15ac --- /dev/null +++ b/cosiap_api/solicitudes/migrations/0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes.py @@ -0,0 +1,40 @@ +# Generated by Django 5.0.6 on 2024-07-09 16:29 + +import common.nombres_archivos +import common.validadores_campos +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Convenio', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('archivo', models.FileField(upload_to=common.nombres_archivos.nombre_archivo_convenio, validators=[common.validadores_campos.validador_archivo_1MB, common.validadores_campos.validador_pdf], verbose_name='Archivo')), + ], + options={ + 'verbose_name': 'Minuta', + 'verbose_name_plural': 'Minutas', + 'ordering': ['pk'], + }, + ), + migrations.CreateModel( + name='Minuta', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('archivo', models.FileField(upload_to=common.nombres_archivos.nombre_archivo_minuta, validators=[common.validadores_campos.validador_archivo_1MB, common.validadores_campos.validador_pdf], verbose_name='Archivo')), + ], + options={ + 'verbose_name': 'Minuta', + 'verbose_name_plural': 'Minutas', + 'ordering': ['pk'], + }, + ), + ] diff --git a/cosiap_api/solicitudes/models.py b/cosiap_api/solicitudes/models.py index 71a8362..10c6637 100644 --- a/cosiap_api/solicitudes/models.py +++ b/cosiap_api/solicitudes/models.py @@ -1,3 +1,86 @@ from django.db import models +from common.nombres_archivos import nombre_archivo_minuta, nombre_archivo_convenio +from common.validadores_campos import validador_archivo_1MB, validador_pdf +from dynamic_formats.models import DynamicFormat -# Create your models here. +class Minuta(models.Model): + ''' + Modelo que contiene la información de las minutas. + + Campos: + - *archivo*: Archivo de la minuta. + + Métodos: + - *get_formato()*: Devuelve el formato común de todas las instancias de Minuta. + - *set_formato(value)*: Establece el formato común para todas las instancias de Minuta. + ''' + archivo = models.FileField(upload_to=nombre_archivo_minuta, validators=[validador_archivo_1MB, validador_pdf], verbose_name="Archivo") + # Atributo de clase común a todas las instancias + _formato = None + + @classmethod + def get_formato(cls): + ''' + Devuelve el formato común de todas las instancias de Minuta. + ''' + if cls._formato is None: + formato_minuta = DynamicFormat.objects.filter(nombre="formato_minuta_defult").first() + cls._formato = formato_minuta + return cls._formato + + @classmethod + def set_formato(cls, value): + '''Establece el formato común para todas las instancias de Minuta. + Args: + - value: El nuevo valor del formato. + ''' + cls._formato = value + + def __str__(self): + return f'Minuta {self.pk}' + + class Meta: + verbose_name = "Minuta" + verbose_name_plural = "Minutas" + ordering = ['pk'] + +class Convenio(models.Model): + ''' + Modelo que contiene la información de los Convenios. + + Campos: + - *archivo*: Archivo del convenio. + + Métodos: + - *get_formato()*: Devuelve el formato común de todas las instancias de Minuta. + - *set_formato(value)*: Establece el formato común para todas las instancias de Minuta. + ''' + archivo = models.FileField(upload_to=nombre_archivo_convenio, validators=[validador_archivo_1MB, validador_pdf], verbose_name="Archivo") + # Atributo de clase común a todas las instancias + _formato = None + + @classmethod + def get_formato(cls): + ''' + Devuelve el formato común de todas las instancias de Convenio. + ''' + if cls._formato is None: + formato_convenio = DynamicFormat.objects.filter(nombre="formato_convenio_defult").first() + cls._formato = formato_convenio + return cls._formato + + @classmethod + def set_formato(cls, value): + '''Establece el formato común para todas las instancias de Minuta. + Args: + - value: El nuevo valor del formato. + ''' + cls._formato = value + + def __str__(self): + return f'Minuta {self.pk}' + + class Meta: + verbose_name = "Minuta" + verbose_name_plural = "Minutas" + ordering = ['pk'] \ No newline at end of file diff --git a/cosiap_api/solicitudes/signals.py b/cosiap_api/solicitudes/signals.py new file mode 100644 index 0000000..a9c4e43 --- /dev/null +++ b/cosiap_api/solicitudes/signals.py @@ -0,0 +1,4 @@ +from django.dispatch import receiver +from dynamic_formats.models import DynamicFormat +from solicitudes.models import Minuta, Convenio + diff --git a/cosiap_api/users/permisos.py b/cosiap_api/users/permisos.py index 6382d66..b49e885 100644 --- a/cosiap_api/users/permisos.py +++ b/cosiap_api/users/permisos.py @@ -9,6 +9,7 @@ class es_admin(permissions.BasePermission): # Clase para verificar que el usuario que va a ingresar tenga sus datos completos class primer_login(permissions.BasePermission): + message = 'El usuario requiere completar su información.' # método para determinar si se requiere o no un primer login def has_permission(self, request, view): # obtenemos al usuario de la request diff --git a/cosiap_api/users/serializers.py b/cosiap_api/users/serializers.py index 4b47cac..65375a0 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, Solicitante, DatosBancarios +from .models import Usuario, Usuario, DatosBancarios # serializer para el usuario solicitante class usuario_serializer(serializers.ModelSerializer): @@ -56,7 +56,7 @@ class datos_bancarios_serializer(serializers.ModelSerializer): class solicitante_serializer(serializers.ModelSerializer): class Meta: # indicamos el modelo a utilziar - model = Solicitante + model = Usuario # indicamos los campos que debe ingresar el usuario fields = ['ap_paterno', 'ap_materno', 'telefono', 'RFC', 'direccion', 'codigo_postal', 'municipio', 'poblacion', 'INE'] # Agregamos validadores para asegurar que los campos requeridos no estén vacíos @@ -68,7 +68,7 @@ class solicitante_serializer(serializers.ModelSerializer): # Definimos una función para crear al Solicitante def create(self, validated_data): user = self.context['request'].user - solicitante = Solicitante.objects.create( + solicitante = Usuario.objects.create( curp=user.curp, nombre=user.nombre, email=user.email, diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index d5b7da6..5926cf1 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -7,7 +7,7 @@ from rest_framework import status, permissions from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny, IsAuthenticated -from .models import Usuario, Solicitante +from .models import Usuario, Usuario from django.contrib import messages from .serializers import usuario_serializer, solicitante_serializer from django.contrib.auth import authenticate @@ -190,7 +190,7 @@ class solicitante(BasePermissionAPIView): 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']) + 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 = solicitante_serializer(instance) @@ -199,7 +199,7 @@ class solicitante(BasePermissionAPIView): # si se desea ver la lista completa else: # indicamos el query set de todos los usuarios - queryset = Solicitante.objects.all() + queryset = Usuario.objects.all() # indicamos el serializer a utilizar y enviamos el queryset serializer = solicitante_serializer(queryset, many=True) # retornamos la lista de usuarios @@ -210,7 +210,7 @@ class solicitante(BasePermissionAPIView): # obtenemos al usuario del request usuario = request.user # creamos el solicitante - solicitante, created = Solicitante.objects.get_or_create( + solicitante, created = Usuario.objects.get_or_create( id=usuario.id, defaults={ 'curp': usuario.curp, @@ -237,7 +237,7 @@ class solicitante(BasePermissionAPIView): # Solicitud put para actualizar los datos del solicitante def put(self, request, *args, **kwargs): # 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) + solicitante = get_object_or_404(Usuario, id=request.user.id) # inicializamos el serializer con los datos precargados serializer = solicitante_serializer(instance=solicitante, data=request.data) # si el serilizer es valido -- GitLab