diff --git a/cosiap_api/common/models.py b/cosiap_api/common/models.py index 71a836239075aa6e6e4ecb700e9c42c95c022d91..63e0c5ad8dc44b9185759656880c8487de1ed569 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 3fa5e3c57bb3352d232344d03c47a86141fb691b..73df4228a1f882fb5933b8256222ea05f78cc004 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 0000000000000000000000000000000000000000..e4bd9ec46251c05c54d6b4edba5b4d69cd32c81b --- /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 0000000000000000000000000000000000000000..b9648d5b2fdfec51632689fb9a54f9904cd69c19 --- /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 aa384da052d74d282362ac62ab7a0556dc5e2c5f..3341e2c6470c2adc1d3056f4f29d1f97a06dbf37 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 809e51c33dcb02f0ffca9b7e6c65c6c5beb3c190..05a5542dae05966e956f15768873dcaa74cdadbd 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 0000000000000000000000000000000000000000..6a5758361a1f4e837ba08b41a84d83db128a2f14 --- /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 71a836239075aa6e6e4ecb700e9c42c95c022d91..222fae05bbf7f29f84baf0b3959491bb4086f9e8 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 7f6735d621f03a35472d945b3691e1370447ee8b..914602aab7bcc9dca8f29022e89a359e0b954ce5 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 0000000000000000000000000000000000000000..8728738a8a8f5028fc9ba283620d0a9ba50aba5f --- /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 71a836239075aa6e6e4ecb700e9c42c95c022d91..d43d3fd2e86f3c3d36f7e5f33e24209056b48b5b 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 0000000000000000000000000000000000000000..26b5b72d58f5e57dfb896c0c415c1968771d58f1 --- /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 0000000000000000000000000000000000000000..67c313bdbdb620c4ae8c718582c8e82d5c844a7d --- /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 71a836239075aa6e6e4ecb700e9c42c95c022d91..c8f173b94b751ac616787b6b1d8270e0c22b7185 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 0000000000000000000000000000000000000000..e6b15acc4a1bf76cad58244b1caf03fec5e91f6d --- /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 71a836239075aa6e6e4ecb700e9c42c95c022d91..10c663715101cf71bcb7bd22a56acb8f8bc40914 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 0000000000000000000000000000000000000000..a9c4e435199a60f2ea8f1469aee9452c74dafed8 --- /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 6382d666f53b91037fd8c9f4ddc803d7cad95dc9..b49e885314eb442eec18e1696504d2bbee5c1b0f 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 aa289e7f467019da67ebab48b1902a48a9a5a31d..de7faaf4485ce87898916f145695e27daa3d90ea 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 # serializar para el administrador @@ -90,7 +90,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 @@ -102,7 +102,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 27a7d06d4be3907854f223a76cdf194e6d058fba..4b90e44bc223354a3ee3419ae6a4ec0a1acdc359 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 @@ -195,7 +195,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) @@ -204,7 +204,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 @@ -215,7 +215,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,