Showing
10 changed files
with
324 additions
and
7 deletions
1 | -from rest_framework import viewsets | 1 | +import json |
2 | +from rest_framework import viewsets,mixins | ||
2 | from rest_framework.permissions import IsAuthenticated | 3 | from rest_framework.permissions import IsAuthenticated |
4 | +from rest_framework.decorators import action | ||
5 | +from rest_framework import status | ||
6 | +from rest_framework.response import Response | ||
7 | +from rest_framework_api_key.permissions import HasAPIKey | ||
8 | + | ||
9 | +from datetime import datetime | ||
3 | 10 | ||
4 | from .models import Evento | 11 | from .models import Evento |
5 | from .serializers import EventoSerializer | 12 | from .serializers import EventoSerializer |
13 | +from .service import GenerarFechas | ||
6 | 14 | ||
7 | 15 | ||
8 | -class EventoViewSets(viewsets.ReadOnlyModelViewSet): | 16 | +class EventoViewSets( |
17 | + mixins.CreateModelMixin, | ||
18 | + mixins.ListModelMixin, | ||
19 | + mixins.RetrieveModelMixin, | ||
20 | + viewsets.GenericViewSet, | ||
21 | +): | ||
9 | queryset = Evento.objects.all().order_by('id') | 22 | queryset = Evento.objects.all().order_by('id') |
10 | serializer_class = EventoSerializer | 23 | serializer_class = EventoSerializer |
11 | - permission_classes = [IsAuthenticated,] | 24 | + permission_classes = [HasAPIKey | IsAuthenticated] |
12 | lookup_field = 'id' | 25 | lookup_field = 'id' |
13 | 26 | ||
27 | + @action(methods=['POST'], detail=False, url_path='obtener-mes') | ||
28 | + def obtener_mes(self, request): | ||
29 | + mes_query = request.data.get('mes', None) | ||
30 | + | ||
31 | + if mes_query is None: | ||
32 | + return Response( | ||
33 | + data={ | ||
34 | + 'error': | ||
35 | + 'El mes ingresado no es válido. ' | ||
36 | + 'Por favor, ingrese el mes correspondiente a la fecha actual o posterior.' | ||
37 | + }, | ||
38 | + status=status.HTTP_400_BAD_REQUEST | ||
39 | + ) | ||
40 | + | ||
41 | + eventos = Evento.objects.filter(fecha_inicio__month=mes_query) | ||
42 | + evento_json = [] | ||
43 | + for evento in eventos: | ||
44 | + generar_fechas = GenerarFechas(evento=evento, mes=mes_query) | ||
45 | + generar_fechas.procesar_fechas_evento() # Cambiado a 'procesar_fechas_evento()' | ||
46 | + evento_json.append(generar_fechas.obtener_datos_evento()) | ||
47 | + | ||
48 | + return Response(data=evento_json, status=status.HTTP_200_OK) | ||
49 | + |
project/apps/evento/constant.py
0 → 100644
project/apps/evento/migrations/0002_remove_fechaevento_dia_evento_fechaevento_dias_and_more.py
0 → 100644
1 | +# Generated by Django 4.2.9 on 2024-10-08 01:39 | ||
2 | + | ||
3 | +from django.db import migrations, models | ||
4 | + | ||
5 | + | ||
6 | +class Migration(migrations.Migration): | ||
7 | + | ||
8 | + dependencies = [ | ||
9 | + ('evento', '0001_initial'), | ||
10 | + ] | ||
11 | + | ||
12 | + operations = [ | ||
13 | + migrations.RemoveField( | ||
14 | + model_name='fechaevento', | ||
15 | + name='dia_evento', | ||
16 | + ), | ||
17 | + migrations.AddField( | ||
18 | + model_name='fechaevento', | ||
19 | + name='dias', | ||
20 | + field=models.DateField(blank=True, editable=False, null=True), | ||
21 | + ), | ||
22 | + migrations.AddField( | ||
23 | + model_name='fechaevento', | ||
24 | + name='duracion_evento', | ||
25 | + field=models.IntegerField(choices=[(1, 'Lunes'), (2, 'Martes'), (3, 'Miércoles'), (4, 'Jueves'), (5, 'Viernes'), (6, 'Sábado'), (7, 'Domingo')], default=1, verbose_name='Días de la semana'), | ||
26 | + preserve_default=False, | ||
27 | + ), | ||
28 | + ] |
@@ -3,6 +3,7 @@ from django.db import models | @@ -3,6 +3,7 @@ from django.db import models | ||
3 | from django.core.validators import FileExtensionValidator | 3 | from django.core.validators import FileExtensionValidator |
4 | 4 | ||
5 | from organismo.models import Organismo, Dependencia | 5 | from organismo.models import Organismo, Dependencia |
6 | +from .constant import DIAS_SEMANA | ||
6 | 7 | ||
7 | # Create your models here. | 8 | # Create your models here. |
8 | 9 | ||
@@ -62,8 +63,8 @@ class FechaEvento(models.Model): | @@ -62,8 +63,8 @@ class FechaEvento(models.Model): | ||
62 | class Meta: | 63 | class Meta: |
63 | verbose_name = 'Fecha del Eventos' | 64 | verbose_name = 'Fecha del Eventos' |
64 | verbose_name_plural = 'Fechas del Eventos' | 65 | verbose_name_plural = 'Fechas del Eventos' |
65 | - | ||
66 | - dia_evento = models.DateField(verbose_name='Días del evento') | 66 | + duracion_evento = models.IntegerField(choices=DIAS_SEMANA, blank=False, verbose_name='Días de la semana') |
67 | + dias = models.DateField(editable=False, blank=True, null=True) | ||
67 | 68 | ||
68 | def __str__(self): | 69 | def __str__(self): |
69 | - return f'{self.dia_evento}' | ||
70 | + return f'{self.duracion_evento}' |
project/apps/evento/service.py
0 → 100644
1 | +import json | ||
2 | +import calendar | ||
3 | +import pytz | ||
4 | + | ||
5 | +from django.core.serializers.json import DjangoJSONEncoder | ||
6 | +from datetime import datetime, timedelta, time | ||
7 | + | ||
8 | +from .models import Evento, FechaEvento | ||
9 | + | ||
10 | + | ||
11 | +class GenerarFechas: | ||
12 | + def __init__(self, evento: Evento, mes: int): | ||
13 | + self.fechas_generadas = {} | ||
14 | + self.evento = evento | ||
15 | + self.mes = mes | ||
16 | + | ||
17 | + def obtener_rango_del_mes(self): | ||
18 | + """Obtiene el rango de fechas para el mes específico.""" | ||
19 | + year = datetime.now().year | ||
20 | + self.mes = int(self.mes) | ||
21 | + primer_dia, ultimo_dia = calendar.monthrange(year, self.mes) | ||
22 | + fecha_inicio_mes = datetime(year, self.mes, 1) | ||
23 | + fecha_fin_mes = datetime(year, self.mes, ultimo_dia) | ||
24 | + return fecha_inicio_mes, fecha_fin_mes | ||
25 | + | ||
26 | + def ajustar_fechas_a_rango_mes(self): | ||
27 | + """Ajusta las fechas del evento al rango del mes especificado.""" | ||
28 | + zona_horaria = pytz.timezone('America/Argentina/Buenos_Aires') | ||
29 | + | ||
30 | + fecha_inicio_evento = self.combinar_fecha_hora(self.evento.fecha_inicio, self.evento.hora_inicio, zona_horaria) | ||
31 | + fecha_fin_evento = self.combinar_fecha_hora(self.evento.fecha_final, self.evento.hora_fin, zona_horaria) | ||
32 | + | ||
33 | + fecha_inicio_mes, fecha_fin_mes = self.obtener_rango_del_mes() | ||
34 | + fecha_inicio_evento = max(fecha_inicio_evento, zona_horaria.localize(fecha_inicio_mes)) | ||
35 | + fecha_fin_evento = min(fecha_fin_evento, zona_horaria.localize(fecha_fin_mes)) | ||
36 | + | ||
37 | + return fecha_inicio_evento, fecha_fin_evento | ||
38 | + | ||
39 | + @staticmethod | ||
40 | + def combinar_fecha_hora(fecha, hora, zona_horaria): | ||
41 | + """Combina la fecha y hora en un solo objeto datetime.""" | ||
42 | + if fecha is None or hora is None: | ||
43 | + return None | ||
44 | + fecha_hora = datetime.combine(fecha, hora) | ||
45 | + return zona_horaria.localize(fecha_hora) if fecha_hora.tzinfo is None else fecha_hora | ||
46 | + | ||
47 | + def obtener_dias_del_evento(self): | ||
48 | + """Obtiene los días de la semana en los que se realiza el evento (en formato 1 a 7).""" | ||
49 | + dias_evento = [] | ||
50 | + for fecha in self.evento.fechas.all(): | ||
51 | + if fecha.duracion_evento is not None: # Solo considerar fechas con duracion_evento definido | ||
52 | + dias_evento.append(int(fecha.duracion_evento)) # Convertir a entero y agregar a la lista | ||
53 | + return dias_evento | ||
54 | + | ||
55 | + def procesar_fechas_evento(self): | ||
56 | + """Genera las fechas del evento ajustadas al mes especificado.""" | ||
57 | + fecha_inicio, fecha_fin = self.ajustar_fechas_a_rango_mes() | ||
58 | + dias_evento = self.obtener_dias_del_evento() | ||
59 | + fechas_null = self.evento.fechas.filter(dias__isnull=True) | ||
60 | + | ||
61 | + self.asignar_fechas_nulas(fechas_null, dias_evento, fecha_inicio) | ||
62 | + self.generar_fechas_para_dias_especificos(fecha_inicio, fecha_fin, dias_evento) | ||
63 | + | ||
64 | + return self.fechas_generadas | ||
65 | + | ||
66 | + def asignar_fechas_nulas(self, fechas_null, dias_evento, fecha_inicio): | ||
67 | + """Asigna fechas a las entradas que no tienen días especificados.""" | ||
68 | + for fecha_obj in fechas_null: | ||
69 | + dia_semana = fecha_inicio.weekday() + 1 # Ajustar de 0-6 a 1-7 | ||
70 | + if dia_semana in dias_evento: | ||
71 | + fecha_obj.dias = fecha_inicio.date() | ||
72 | + fecha_obj.save(update_fields=['dias']) | ||
73 | + self.fechas_generadas[fecha_inicio.strftime('%Y-%m-%d')] = fecha_obj.pk | ||
74 | + | ||
75 | + def generar_fechas_para_dias_especificos(self, fecha_inicio, fecha_fin, dias_evento): | ||
76 | + """Genera fechas adicionales dentro del rango si coinciden con los días seleccionados.""" | ||
77 | + fecha_actual = fecha_inicio | ||
78 | + while fecha_actual <= fecha_fin: | ||
79 | + dia_semana = fecha_actual.weekday() + 1 # Ajustar de 0-6 a 1-7 | ||
80 | + if dia_semana in dias_evento: | ||
81 | + self.agregar_fecha(fecha_actual, dia_semana) | ||
82 | + fecha_actual += timedelta(days=1) | ||
83 | + | ||
84 | + def agregar_fecha(self, fecha_actual, dia_semana): | ||
85 | + """Agrega una fecha al evento si no existe ya en el sistema.""" | ||
86 | + if not FechaEvento.objects.filter(dias=fecha_actual, duracion_evento=str(dia_semana)).exists(): | ||
87 | + nueva_fecha = FechaEvento.objects.create(dias=fecha_actual, duracion_evento=str(dia_semana)) | ||
88 | + self.evento.fechas.add(nueva_fecha) | ||
89 | + self.fechas_generadas[fecha_actual.strftime('%Y-%m-%d')] = nueva_fecha.pk | ||
90 | + | ||
91 | + def obtener_datos_evento(self): | ||
92 | + """Devuelve los datos del evento en formato serializado.""" | ||
93 | + return { | ||
94 | + 'titulo': self.evento.titulo, | ||
95 | + 'categoria': self.evento.categoria, | ||
96 | + 'descripcion': self.evento.descripcion, | ||
97 | + 'direccion': self.evento.direccion, | ||
98 | + 'fecha_inicio': self.evento.fecha_inicio.isoformat() if self.evento.fecha_inicio else None, | ||
99 | + 'hora_inicio': self.evento.hora_inicio.isoformat() if self.evento.hora_inicio else None, | ||
100 | + 'fecha_final': self.evento.fecha_final.isoformat() if self.evento.fecha_final else None, | ||
101 | + 'hora_fin': self.evento.hora_fin.isoformat() if self.evento.hora_fin else None, | ||
102 | + 'url': self.evento.url, | ||
103 | + 'imagen': self.evento.imagen.url if self.evento.imagen else None, | ||
104 | + 'fechas': list(self.evento.fechas.values('id', 'duracion_evento', 'dias')), | ||
105 | + 'organismo': list(self.evento.organismo.values('short_name')), | ||
106 | + 'dependencia': list(self.evento.dependencia.values('short_name')), | ||
107 | + } | ||
108 | + |
project/apps/evento/tests/__init__.py
0 → 100644
project/apps/evento/tests/factories.py
0 → 100644
1 | +import random | ||
2 | + | ||
3 | +from factory import faker, django, post_generation | ||
4 | + | ||
5 | +from evento.models import Evento, FechaEvento | ||
6 | + | ||
7 | + | ||
8 | +class FechaEventoFactory(django.DjangoModelFactory): | ||
9 | + class Meta: | ||
10 | + model = FechaEvento | ||
11 | + | ||
12 | + duracion_evento = random.randint(1, 7) | ||
13 | + | ||
14 | + | ||
15 | +class EventoFactory(django.DjangoModelFactory): | ||
16 | + class Meta: | ||
17 | + model = Evento | ||
18 | + skip_postgeneration_save = True | ||
19 | + | ||
20 | + titulo = faker.Faker(provider='sentence', nb_words=50) | ||
21 | + categoria = faker.Faker(provider='sentence', nb_words=30) | ||
22 | + direccion = 'https://maps.app.goo.gl/CNwbHBx5zq1VDje57' | ||
23 | + descripcion = faker.Faker(provider='sentence', nb_words=30) | ||
24 | + fecha_inicio = '2024-10-04' | ||
25 | + fecha_fin = '2024-10-04' | ||
26 | + | ||
27 | + @post_generation | ||
28 | + def add_fechas(self, create, extracted, **kwargs): | ||
29 | + if not create: | ||
30 | + return | ||
31 | + | ||
32 | + if extracted: | ||
33 | + for dias in extracted: | ||
34 | + self.fechas.add(dias) |
project/apps/evento/tests/test_evento.py
0 → 100644
1 | +import pytest | ||
2 | +import json | ||
3 | + | ||
4 | +from datetime import time, datetime | ||
5 | +from rest_framework import status | ||
6 | +from django.contrib.auth.models import User | ||
7 | +from django.urls import reverse | ||
8 | +from rest_framework.test import APIClient | ||
9 | + | ||
10 | +from evento.tests.factories import EventoFactory, FechaEventoFactory | ||
11 | +from evento.models import Evento | ||
12 | + | ||
13 | + | ||
14 | +@pytest.mark.django_db | ||
15 | +@pytest.mark.parametrize('mes, fecha_inicio, fecha_final', [ | ||
16 | + (10, '2024-10-07', '2024-10-07'), # Evento en octubre | ||
17 | + (11, '2024-11-04', '2024-11-04') # Evento en noviembre | ||
18 | +]) | ||
19 | +def test_obtener_mes_get(mes, fecha_inicio, fecha_final): | ||
20 | + cliente = APIClient() | ||
21 | + user = User.objects.create_user( | ||
22 | + username='admin', | ||
23 | + email='admin@example.com', | ||
24 | + password='password123', | ||
25 | + is_superuser=True, | ||
26 | + ) | ||
27 | + cliente.force_authenticate(user=user) | ||
28 | + | ||
29 | + calendario = FechaEventoFactory.create(duracion_evento=1) | ||
30 | + | ||
31 | + data = { | ||
32 | + 'titulo': f'Event test for month {mes}', | ||
33 | + 'categoria': 'Cultural', | ||
34 | + 'descripcion': f'Durante el evento test para el mes {mes}', | ||
35 | + 'direccion': 'https://maps.app.goo.gl/RJExHoGFyg8Ska7CA', | ||
36 | + 'fecha_inicio': fecha_inicio, | ||
37 | + 'hora_inicio': str(time(8, 30)), | ||
38 | + 'fecha_final': fecha_final, | ||
39 | + 'hora_fin': str(time(12, 00)), | ||
40 | + 'url': 'google.com', | ||
41 | + 'fechas': json.dumps([ | ||
42 | + { | ||
43 | + 'id': calendario.pk, | ||
44 | + 'duracion_evento': 1, | ||
45 | + 'dias': None, | ||
46 | + } | ||
47 | + ]), | ||
48 | + } | ||
49 | + | ||
50 | + evento = Evento.objects.create( | ||
51 | + titulo=data['titulo'], | ||
52 | + categoria=data['categoria'], | ||
53 | + descripcion=data['descripcion'], | ||
54 | + direccion=data['direccion'], | ||
55 | + fecha_inicio=data['fecha_inicio'], | ||
56 | + hora_inicio=data['hora_inicio'], | ||
57 | + fecha_final=data['fecha_final'], | ||
58 | + hora_fin=data['hora_fin'], | ||
59 | + url=data['url'] | ||
60 | + ) | ||
61 | + evento.fechas.set([calendario]) | ||
62 | + | ||
63 | + url = reverse('evento-obtener-mes') | ||
64 | + | ||
65 | + response = cliente.post(url, {'mes': mes}, format='multipart') | ||
66 | + print("Response JSON:", response.json()) | ||
67 | + | ||
68 | + evento_fechas = evento.fechas.all().values() | ||
69 | + print("Fechas asociadas al evento:", list(evento_fechas)) | ||
70 | + | ||
71 | + if response.status_code == status.HTTP_400_BAD_REQUEST: | ||
72 | + assert response.data['error'] == ( | ||
73 | + 'El mes ingresado no es válido. Por favor, ingrese el mes correspondiente a la fecha actual o posterior.' | ||
74 | + ) | ||
75 | + else: | ||
76 | + assert response.status_code == 200 | ||
77 | + | ||
78 | + # Verificar que solo existe una fecha asociada con el evento | ||
79 | + assert evento.fechas.count() == 1, "Debe existir solo una fecha asociada al evento" | ||
80 | + | ||
81 | + evento_data = response.json()['data'][0] | ||
82 | + assert evento_data['titulo'] == f'Event test for month {mes}' | ||
83 | + assert evento_data['categoria'] == 'Cultural' | ||
84 | + assert evento_data['descripcion'] == f'Durante el evento test para el mes {mes}' | ||
85 | + assert evento_data['direccion'] == 'https://maps.app.goo.gl/RJExHoGFyg8Ska7CA' | ||
86 | + assert evento_data['fecha_inicio'] == fecha_inicio | ||
87 | + assert evento_data['hora_inicio'] == str(time(8, 30)) | ||
88 | + assert evento_data['fecha_final'] == fecha_final | ||
89 | + assert evento_data['hora_fin'] == str(time(12, 0)) | ||
90 | + assert evento_data['url'] == 'google.com' | ||
91 | + assert evento_data['fechas'] == [{ | ||
92 | + 'id': calendario.pk, | ||
93 | + 'duracion_evento': 1, | ||
94 | + 'dias': fecha_inicio, | ||
95 | + }] |
@@ -57,6 +57,7 @@ THIRD_PARTY_APPS = ( | @@ -57,6 +57,7 @@ THIRD_PARTY_APPS = ( | ||
57 | 'corsheaders', | 57 | 'corsheaders', |
58 | 'oauth2_provider', | 58 | 'oauth2_provider', |
59 | 'mozilla_django_oidc', | 59 | 'mozilla_django_oidc', |
60 | + "rest_framework_api_key", | ||
60 | 61 | ||
61 | ) | 62 | ) |
62 | 63 | ||
@@ -86,7 +87,7 @@ ROOT_URLCONF = 'project.urls' | @@ -86,7 +87,7 @@ ROOT_URLCONF = 'project.urls' | ||
86 | WSGI_APPLICATION = 'project.wsgi.application' | 87 | WSGI_APPLICATION = 'project.wsgi.application' |
87 | 88 | ||
88 | LANGUAGE_CODE = 'es-AR' | 89 | LANGUAGE_CODE = 'es-AR' |
89 | -TIME_ZONE = 'America/Argentina/Catamarca' | 90 | +TIME_ZONE = 'America/Argentina/Buenos_Aires' |
90 | USE_I18N = True | 91 | USE_I18N = True |
91 | USE_TZ = True | 92 | USE_TZ = True |
92 | 93 | ||
@@ -158,6 +159,10 @@ REST_FRAMEWORK = { | @@ -158,6 +159,10 @@ REST_FRAMEWORK = { | ||
158 | 'rest_framework.parsers.FormParser', | 159 | 'rest_framework.parsers.FormParser', |
159 | 'rest_framework.parsers.MultiPartParser' | 160 | 'rest_framework.parsers.MultiPartParser' |
160 | ), | 161 | ), |
162 | + "DEFAULT_PERMISSION_CLASSES": [ | ||
163 | + "rest_framework_api_key.permissions.HasAPIKey", | ||
164 | + 'rest_framework.permissions.AllowAny', | ||
165 | + ], | ||
161 | 'DEFAULT_AUTHENTICATION_CLASSES': ( | 166 | 'DEFAULT_AUTHENTICATION_CLASSES': ( |
162 | 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', | 167 | 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', |
163 | 'rest_framework.authentication.BasicAuthentication', | 168 | 'rest_framework.authentication.BasicAuthentication', |
@@ -6,6 +6,7 @@ django-filter==23.3 | @@ -6,6 +6,7 @@ django-filter==23.3 | ||
6 | djangorestframework==3.14.0 | 6 | djangorestframework==3.14.0 |
7 | django-environ==0.11.2 | 7 | django-environ==0.11.2 |
8 | djangorestframework-jsonapi==6.1.0 | 8 | djangorestframework-jsonapi==6.1.0 |
9 | +djangorestframework-api-key==2.3.0 | ||
9 | django-oauth-toolkit==2.3.0 | 10 | django-oauth-toolkit==2.3.0 |
10 | mozilla-django-oidc==3.0.0 | 11 | mozilla-django-oidc==3.0.0 |
11 | 12 |
-
Please register or login to post a comment