#2 Tarea periódica para enviar reportes

Fechado
wilitp quer mesclar 55 commits de wilitp/send-programmed-report-task em wilitp/main

+ 11 - 0
.env.dev

@@ -0,0 +1,11 @@
+#  Programmed Reports:
+PROGRAMMED_REPORTS_SERVICE_AUTH_TOKEN=juwiV9MjP4r3NTsvd9qRsPQ2FMdk5soJjxksYKhruza3wskG23h4cygTEPiFAGo2
+DB_NAME=programmed-reports
+DB_USER=user-preports
+DB_PASSWORD=pass-preports
+CLIMA_URL=http://clima:8000
+# Postgres:
+TZ=GMT+3
+POSTGRES_DB=programmed-reports
+POSTGRES_USER=user-preports
+POSTGRES_PASSWORD=pass-preports

+ 4 - 1
Dockerfile

@@ -1,4 +1,4 @@
-FROM python:3.9-buster
+FROM python:3.9-alpine
 # don't write .pyc files on import
 ENV PYTHONDONTWRITEBYTECODE 1
 # force stdout and stderr to be unbuffered
@@ -8,4 +8,7 @@ EXPOSE 8000
 WORKDIR /app
 COPY ./app .
 RUN pip install -r /app/requirements.txt
+COPY ./entrypoint.sh /
+RUN chmod +x /entrypoint.sh
+ENTRYPOINT [ "/entrypoint.sh" ]
 CMD python manage.py runserver 0.0.0.0:8000

+ 5 - 2
app/api/admin.py

@@ -1,5 +1,8 @@
 from django.contrib import admin
-from .models import Preport
+from .models import ProgrammedReport
+
+class PreportAdmin(admin.ModelAdmin):
+    list_display = ("name",)
 
 # Register your models here.
-admin.site.register(Preport)
+admin.site.register(ProgrammedReport, PreportAdmin)

+ 14 - 0
app/api/client.py

@@ -0,0 +1,14 @@
+import requests
+from preports.settings import CLIMA_URL
+
+
+def get_user_id(token: str) -> requests.Response:
+    try:
+        response = requests.get(
+            f"{CLIMA_URL}/api/get_user_id/",
+            # Authorization debe tener el prefijo 'Token '
+            headers={"Authorization": "Token " + token},
+        )
+        return response
+    except:
+        return None

+ 9 - 0
app/api/config.py

@@ -0,0 +1,9 @@
+from dateutil.relativedelta import relativedelta as reldelta
+from django.utils.timezone import timedelta as delta
+
+CALENDAR_FREQUENCIES = {
+    "daily": delta(days=1),
+    "weekly": reldelta(weeks=1),
+    "monthly": reldelta(months=1),
+    "yearly": reldelta(years=1)
+}

+ 20 - 0
app/api/middleware.py

@@ -0,0 +1,20 @@
+from django.http import HttpResponse
+
+class CorsMiddleware:
+    def __init__(self, get_response):
+        self.get_response = get_response
+
+    def __call__(self, request):
+
+        if(request.method == "OPTIONS"):
+            response = HttpResponse(status="200")
+            response.headers['Access-Control-Allow-Origin'] = "*"
+            response.headers['Access-Control-Allow-Headers'] = "*"
+            response.headers['Access-Control-Allow-Methods'] = "*"
+            return  response
+
+        response = self.get_response(request)
+
+        response["Access-Control-Allow-Origin"] = "*"
+
+        return response

+ 12 - 5
app/api/migrations/0001_initial.py

@@ -1,7 +1,9 @@
-# Generated by Django 4.0.4 on 2022-05-13 22:53
+# Generated by Django 4.0.4 on 2022-09-24 23:16
 
 import api.validators
+import datetime
 from django.db import migrations, models
+import django.utils.timezone
 
 
 class Migration(migrations.Migration):
@@ -13,14 +15,19 @@ class Migration(migrations.Migration):
 
     operations = [
         migrations.CreateModel(
-            name='Preport',
+            name='ProgrammedReport',
             fields=[
                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=100)),
                 ('user_id', models.CharField(max_length=2048)),
-                ('report_type', models.CharField(max_length=50)),
-                ('modules', models.JSONField(validators=[api.validators.json_not_empty_string_array])),
+                ('frequency', models.DurationField(default=datetime.timedelta(days=1))),
                 ('notified_emails', models.JSONField(validators=[api.validators.json_not_empty_string_array, api.validators.json_email_array])),
-                ('last_report_date', models.DateField(blank=True, default=None, null=True)),
+                ('last_report_date', models.DateTimeField(blank=True, default=None, null=True)),
+                ('start_report_date', models.DateTimeField(default=django.utils.timezone.now)),
+                ('format', models.CharField(choices=[('excel', 'Excel'), ('csv', 'CSV')], max_length=5)),
+                ('option', models.CharField(max_length=50)),
+                ('stations', models.JSONField(validators=[api.validators.json_not_empty_string_array])),
+                ('modules', models.JSONField(validators=[api.validators.json_not_empty_string_array])),
             ],
         ),
     ]

+ 18 - 0
app/api/migrations/0002_alter_programmedreport_option.py

@@ -0,0 +1,18 @@
+# Generated by Django 4.0.4 on 2022-09-27 12:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('api', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='programmedreport',
+            name='option',
+            field=models.CharField(default='all', max_length=50),
+        ),
+    ]

+ 18 - 0
app/api/migrations/0003_programmedreport_due_date.py

@@ -0,0 +1,18 @@
+# Generated by Django 4.0.4 on 2022-10-17 01:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('api', '0002_alter_programmedreport_option'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='programmedreport',
+            name='due_date',
+            field=models.DateTimeField(blank=True, default=None, null=True),
+        ),
+    ]

+ 19 - 0
app/api/migrations/0004_alter_programmedreport_frequency.py

@@ -0,0 +1,19 @@
+# Generated by Django 4.0.4 on 2022-10-19 23:41
+
+import api.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('api', '0003_programmedreport_due_date'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='programmedreport',
+            name='frequency',
+            field=models.CharField(default='Mensual', max_length=20, validators=[api.validators.valid_frequency_format]),
+        ),
+    ]

+ 19 - 0
app/api/migrations/0005_alter_programmedreport_frequency.py

@@ -0,0 +1,19 @@
+# Generated by Django 4.0.4 on 2023-01-16 22:15
+
+import api.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('api', '0004_alter_programmedreport_frequency'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='programmedreport',
+            name='frequency',
+            field=models.CharField(default='monthly', max_length=20, validators=[api.validators.valid_frequency_format]),
+        ),
+    ]

+ 17 - 0
app/api/migrations/0006_remove_programmedreport_last_report_date.py

@@ -0,0 +1,17 @@
+# Generated by Django 4.0.4 on 2023-01-17 17:34
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('api', '0005_alter_programmedreport_frequency'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='programmedreport',
+            name='last_report_date',
+        ),
+    ]

+ 108 - 6
app/api/models.py

@@ -1,12 +1,114 @@
 from django.db import models
-from .validators import json_email_array, json_not_empty_string_array
+from .validators import json_email_array, json_not_empty_string_array, valid_frequency_format
+from django.utils import timezone
+from django.conf import settings
+from django.utils.timezone import timedelta as delta
+from dateutil.relativedelta import relativedelta as reldelta
+from pytz import timezone as tz
+import requests
+import logging
+logger = logging.getLogger('django')
 
 
-class Preport(models.Model):
+class ProgrammedReport(models.Model):
+    REPORT_FORMATS = [
+        ("excel", "Excel"),
+        ("csv", "CSV")
+    ]
+
+    name = models.CharField(max_length=100)
     user_id = models.CharField(max_length=2048)
-    report_type = models.CharField(max_length=50)
-    modules = models.JSONField(
-        validators=[json_not_empty_string_array])
+    frequency = models.CharField(max_length=20, default="monthly", validators=[valid_frequency_format])
+    start_report_date = models.DateTimeField(
+        default=timezone.now)
+    due_date = models.DateTimeField(blank=True, null=True, default=None)
+
+    # Valores que configuran al reporte:
+    format = models.CharField(
+        max_length=5, choices=REPORT_FORMATS)
+    option = models.CharField(max_length=50, default="all")
+    stations = models.JSONField(validators=[json_not_empty_string_array])
+    modules = models.JSONField(validators=[json_not_empty_string_array])
     notified_emails = models.JSONField(
         validators=[json_not_empty_string_array, json_email_array])
-    last_report_date = models.DateField(blank=True, null=True, default=None)
+
+    # Calculamos el rango del reporte y lo guardamos en UTC en la base de datos.
+    def save(self, *args, **kwargs):
+        range = self.get_report_range()
+        self.start_report_date = range[0].astimezone(tz('UTC'))
+        self.due_date = range[1].astimezone(tz('UTC'))
+        super().save(*args, **kwargs)
+
+    def get_report_range(self):
+        # Por defecto frequencia Diaria:
+        start_date = self.start_report_date.astimezone(tz('America/Argentina/Cordoba')).replace(hour=0, minute=0, second=0, microsecond=0)
+        end_date = self.start_report_date.astimezone(tz('America/Argentina/Cordoba')).replace(hour=23, minute=59, second=59, microsecond=0)
+        if self.frequency == "weekly":
+            start_date -= delta(days=start_date.weekday())
+            end_date += delta(days=(6 - end_date.weekday())) # Lunes de la siguiente semana
+        elif self.frequency == "monthly":
+            start_date = start_date.replace(day=1)
+            end_date = (end_date.replace(day=1) + reldelta(months=1)).replace(day=1) - delta(days=1)
+        elif self.frequency == "yearly":
+            start_date = start_date.replace(day=1, month=1)
+            end_date = (end_date.replace(day=1, month=1) + reldelta(years=1)) - delta(days=1)
+        elif self.frequency != 'daily':
+            return None
+        return start_date, end_date
+
+    # Metodo que envía la petición a Clima para enviar el reporte.
+    def send_report(self):
+        # Calculamos el formato en el que Clima requiere recibir los rangos.
+        start_date = self.start_report_date.astimezone(tz('America/Argentina/Cordoba')).strftime("%d/%m/%Y %H:%M")
+        end_date = self.due_date.astimezone(tz('America/Argentina/Cordoba')).strftime("%d/%m/%Y %H:%M")
+        
+        # Enviamos la request:
+        response = requests.post(settings.CLIMA_URL + "/async_report_handler", {
+            "id": self.id,
+            "name": self.name,
+            "user_id": self.user_id,
+            "format": self.format,
+            "stations": self.stations,
+            "modules": self.modules,
+            "option": self.option,
+            "start_date": start_date,
+            "end_date": end_date,
+            "notified_emails": self.notified_emails
+        }, headers={"AUTHORIZATION": settings.PROGRAMMED_REPORTS_SERVICE_AUTH_TOKEN})
+
+        if response.status_code != 200:
+            logger.critical(
+                "No se pudo enviar el reporte asincrono: %s\n Reason: %s", self, response.reason)
+            # TODO! Seria ideal enviar un email al administrador para ver que paso.
+            return False
+
+        self.start_report_date = self.due_date + delta(seconds=1) # Pass to next cycle
+        self.save()
+        return True
+
+    def send_report_now(self):
+        # Calculamos el formato en el que Clima requiere recibir los rangos.
+        start_date = self.start_report_date.astimezone(tz('America/Argentina/Cordoba')).strftime("%d/%m/%Y %H:%M")
+        end_date = timezone.datetime.now().strftime("%d/%m/%Y %H:%M")
+
+        # Enviamos la request:
+        response = requests.post(settings.CLIMA_URL + "/async_report_handler", {
+            "id": self.id,
+            "name": self.name,
+            "user_id": self.user_id,
+            "format": self.format,
+            "stations": self.stations,
+            "modules": self.modules,
+            "option": self.option,
+            "start_date": start_date,
+            "end_date": end_date,
+            "notified_emails": self.notified_emails
+        }, headers={"AUTHORIZATION": settings.PROGRAMMED_REPORTS_SERVICE_AUTH_TOKEN})
+
+        if response.status_code != 200:
+            logger.critical(
+                "No se pudo enviar el reporte asincrono: %s\n Reason: %s", self, response.reason)
+            # TODO! Seria ideal enviar un email al administrador para ver que paso.
+            return False
+
+        return True

+ 17 - 0
app/api/permissions.py

@@ -0,0 +1,17 @@
+from rest_framework import permissions
+from .client import get_user_id
+
+
+class IsOmixomUser(permissions.BasePermission):
+    message = 'El token no pertenece a un usuario de Omixom'
+
+    # Determina el usuario al que pertenece el Token de Autorizacion.
+    # Solo da permisos si el token es valido y en ese caso setea el user_id asociado a ese token.
+    def has_permission(self, request, view):
+        token = request.META.get('HTTP_AUTHORIZATION')
+        response = get_user_id(token)
+        if response:
+            request.data["user_id"] = response.json()["user_id"]
+            return response.status_code == 200
+        else:
+            return False

+ 5 - 5
app/api/serializers.py

@@ -1,9 +1,9 @@
 from rest_framework import serializers
-from .models import Preport
+from .models import ProgrammedReport
 
 
-class PreportSerializer(serializers.ModelSerializer):
+class ProgrammedReportSerializer(serializers.ModelSerializer):
     class Meta:
-        model = Preport
-        fields = ["id", "user_id", "report_type", "modules",
-                  "notified_emails", "last_report_date"]
+        model = ProgrammedReport
+        fields = ["id", "name", "user_id", "frequency", "notified_emails", "start_report_date", "due_date",
+                  "format", "option", "stations", "modules"]

+ 21 - 0
app/api/tasks.py

@@ -0,0 +1,21 @@
+from .models import ProgrammedReport
+from django.utils import timezone
+import logging
+logger = logging.getLogger('django')
+
+# Task que determina que reportes deben ser enviados:
+# Ejecutado por Cron (Ver settings.py)
+def send_programmed_reports_task():
+    timenow = timezone.now()
+    for preport in ProgrammedReport.objects.all():
+        try: # Este try-except evita que si haya errores en el envio de algun reporte se cancele el envio de los que siguen.
+            if timenow > preport.due_date:
+                sent = preport.send_report()
+                if sent:
+                    logger.info("Se envio el Programmed Report ID: {}".format(preport.id))
+                else:
+                    logger.info("No se pudo enviar el Programmed Report ID: {}".format(preport.id))
+        except Exception:
+            logger.exception("Error enviando Reporte Programado: {}".format(preport.id))
+                
+                

+ 0 - 3
app/api/tests.py

@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.

+ 7 - 5
app/api/urls.py

@@ -1,8 +1,10 @@
+from django.urls import path
 from rest_framework.routers import SimpleRouter
-from .views import PreportViewSet
-
+from .views import ProgrammedReportViewSet, SendReportNow
 
 router = SimpleRouter()
-router.register(r'preports', PreportViewSet, "preport")
-
-urlpatterns = router.urls
+router.register(r'preports', ProgrammedReportViewSet, "preport")
+urlpatterns = [
+    # path(r'preports-confirmation', ProgrammedReportSentConfirmation.as_view()),
+    path('preports-send-now/<int:pk>/', SendReportNow.as_view())
+] + router.urls

+ 10 - 4
app/api/validators.py

@@ -1,11 +1,17 @@
-import json
-import re
+from tokenize import String
 from typing import List
 from django.core.exceptions import ValidationError
 from django.core.validators import validate_email
+from api.config import CALENDAR_FREQUENCIES
 
-from django.db.models.lookups import Regex
 
+def valid_frequency_format(freq: String):
+    '''
+    Ve que el valor sea alguna de las frequencias calendario definidad en CALENDAR_FREQUENCIES
+    '''
+
+    if freq not in CALENDAR_FREQUENCIES:
+        raise ValidationError("Debe ser una frequencia calendario.")
 
 def json_email_array(val: List):
     '''
@@ -32,7 +38,7 @@ def json_not_empty_string_array(val):
     valid = True
 
     # Ser una lista no vacia
-    if(not (isinstance(val, list) and len(val) > 0)):
+    if (not (isinstance(val, list) and len(val) > 0)):
         valid = False
 
     # Ser una lista de strings

+ 69 - 10
app/api/views.py

@@ -1,16 +1,75 @@
-from rest_framework import viewsets
-from rest_framework.permissions import AllowAny
-from .models import Preport
-from .serializers import PreportSerializer
+from django.db.models import ObjectDoesNotExist
+from django.urls.exceptions import Http404
+from rest_framework import viewsets, views
+from rest_framework.exceptions import PermissionDenied
+from rest_framework.response import Response
+from .models import ProgrammedReport
+from .serializers import ProgrammedReportSerializer
+from .permissions import IsOmixomUser
+from rest_framework import status
 
 
-class PreportViewSet(viewsets.ModelViewSet):
-    # TODO: Mostrar solo los reportes del usuario
+# API de Programmed Reports:
+# Requisitos:
+#   - Especificar el TOKEN del usuario en el HTTP Header "AUTHORIZATION".
+#   - Las requests deben ser JSON.
+#   - No es necesario enviar el field user_id, esto es seteado automaticamente al recibir el TOKEN.
+# Endpoints: (BASE_URL: /api/preports)
+#   - (List) GET /                   -> Obtener la lista de todos los Programmed Report
+#   - (Create) POST /                -> Crear un nuevo Programmed Report
+#   - (Retrieve) GET /{id}/          -> Obtener la informacion de un Programmed Report
+#   - (Update) PUT /{id}/            -> Actualizar un Programmed Report (requiere todos los fields)
+#   - (Partial_Update) PATCH /{id}/  -> Actualizar algunos fields de un Programmed Report
+#   - (Delete) DELETE /{id}/         -> Elimina un Programmed Report
+
+class SendReportNow(views.APIView):
+    permission_classes = [IsOmixomUser]
+    def get(self, request, pk):
+        """
+        Send report right now, regardless of if it should be sent.
+        """
+        user_id = request.data["user_id"]
+        try:
+            preport: ProgrammedReport = ProgrammedReport.objects.get(pk=pk)
+        except ObjectDoesNotExist:
+            raise Http404
+
+        if(int(preport.user_id) != int(user_id)):
+            raise PermissionDenied(f"Su usuario no es dueño de este reporte({preport.user_id} != {user_id})")
+        sent = preport.send_report_now()
+        if sent:
+            print("Se envio el Programmed Report ID:", preport.id)
+        else:
+            print("No se pudo enviar el Programmed Report ID:", preport.id)
+        if not sent:
+            return Response("Error al comunicarse con clima", status.HTTP_500_INTERNAL_SERVER_ERROR)
+        return Response("Ok", status.HTTP_200_OK)
+
+
+
+
+class ProgrammedReportViewSet(viewsets.ModelViewSet):
+    # El ViewSet solo es valido si el Token enviado es de un usuario de Omixom.
+    permission_classes = [IsOmixomUser]
+    serializer_class = ProgrammedReportSerializer
+
+    # Solo se devuelven los modelos para el user_id asociado al token. (Ver IsOmixomUser)
     def get_queryset(self):
-        qs = Preport.objects.all()
+        user_id = self.request.data["user_id"]
+        qs = ProgrammedReport.objects.filter(user_id=user_id)
         return qs
 
-    serializer_class = PreportSerializer
+# Endpoint para la confirmacion de envio de un reporte programado por parte de Clima:
+# Por el momento no es usado:
+# class ProgrammedReportSentConfirmation(views.APIView):
+#     parser_classes = [JSONParser]
 
-    # TODO: Implementar la permission class para checkear el token del usuario y vea que clases
-    permission_classes = [AllowAny]
+#     def post(self, request, format=None):
+#         if(settings.PROGRAMMED_REPORTS_SERVICE_AUTH_TOKEN != request.META.get("HTTP_AUTHORIZATION")):
+#             return Response("Invalid Access Credentials.", status.HTTP_403_FORBIDDEN)
+#         report = ProgrammedReport.objects.filter(id=request.data.get("id")).first()
+#         if report is None:
+#             return Response("Invalid Report.", status.HTTP_400_BAD_REQUEST)
+#         report.last_report_date = timezone.now()
+#         report.save()
+#         return Response("Ok", status.HTTP_200_OK)

+ 56 - 6
app/preports/settings.py

@@ -26,7 +26,7 @@ SECRET_KEY = 'django-insecure-f779!gl7b8__izv_p!x2ssyce2y6g7yc$k_u20wl8!x+877ng^
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = True
 
-ALLOWED_HOSTS = []
+ALLOWED_HOSTS = ["*"]
 
 
 # Application definition
@@ -38,6 +38,7 @@ INSTALLED_APPS = [
     'django.contrib.sessions',
     'django.contrib.messages',
     'django.contrib.staticfiles',
+    'django_crontab',
     'rest_framework',
     'api'
 ]
@@ -50,8 +51,11 @@ MIDDLEWARE = [
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'api.middleware.CorsMiddleware'
 ]
 
+SESSION_COOKIE_NAME = 'preportssessionid'
+
 ROOT_URLCONF = 'preports.urls'
 
 TEMPLATES = [
@@ -78,15 +82,14 @@ WSGI_APPLICATION = 'preports.wsgi.application'
 
 DATABASES = {
     'default': {
-        'NAME': 'p-reports',
+        'NAME': os.getenv("DB_NAME"),
         'ENGINE': 'django.db.backends.postgresql',
-        'USER': 'p-reports',
+        'USER': os.getenv("DB_USER"),
         'PASSWORD': os.getenv("DB_PASSWORD"),
-        'HOST': 'db'
+        'HOST': 'preports-db'
     },
 }
 
-
 # Password validation
 # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
 
@@ -111,7 +114,7 @@ AUTH_PASSWORD_VALIDATORS = [
 
 LANGUAGE_CODE = 'en-us'
 
-TIME_ZONE = 'UTC'
+TIME_ZONE = 'America/Argentina/Cordoba'
 
 USE_I18N = True
 
@@ -127,3 +130,50 @@ STATIC_URL = 'static/'
 # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
 
 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+
+REST_FRAMEWORK = {
+    'DEFAULT_AUTHENTICATION_CLASSES': [
+        'rest_framework.authentication.BasicAuthentication',
+    ]
+}
+
+CLIMA_URL = os.getenv("CLIMA_URL")
+
+CRONJOBS = [
+    ('2 */2 * * *', 'api.tasks.send_programmed_reports_task')
+]
+
+PROGRAMMED_REPORTS_SERVICE_AUTH_TOKEN = os.getenv("PROGRAMMED_REPORTS_SERVICE_AUTH_TOKEN")
+
+LOGGING = {
+    "version": 1,
+    "disable_existing_loggers": False,
+    "formatters": {
+        "verbose": {
+            "format": "%(lineno)d: %(levelname)s %(asctime)s %(module)s %(message)s"
+        },
+        "simple": {"format": "%(asctime)s: %(message)s"},
+    },
+    "handlers": {
+        "django-log-file": {
+            "level": "WARNING",
+            "class": "logging.handlers.RotatingFileHandler",
+            "filename": "/logs/django.log",
+            "maxBytes": 1024 * 1024 * 128,  # Files of 128 MB
+            "backupCount": 2,
+            "formatter": "verbose",
+        },
+        "console": {
+            "level": "DEBUG",
+            "class": "logging.StreamHandler",
+            "formatter": "verbose",
+        },
+    },
+    "loggers": {
+        "django": {
+            "handlers": ["django-log-file", "console"],
+            "propagate": True,
+        },
+    },
+}

+ 9 - 1
app/requirements.txt

@@ -1,10 +1,18 @@
 asgiref==3.5.1
+certifi==2022.5.18.1
+charset-normalizer==2.0.12
 Django==4.0.4
 django-filter==21.1
 djangorestframework==3.13.1
+idna==3.3
 importlib-metadata==4.11.3
 Markdown==3.3.7
 psycopg2-binary==2.9.3
-pytz==2022.1
+requests==2.27.1
 sqlparse==0.4.2
+urllib3==1.26.9
 zipp==3.8.0
+django-crontab==0.7.1
+python-dateutil==2.8.2
+pytz==2022.7.1
+

+ 19 - 8
docker-compose.yml

@@ -1,19 +1,30 @@
-version: '3'
+version: "3"
 
 services:
-  app:
+  preports-app:
+    container_name: preports
+    restart: unless-stopped
     build:
       context: ./
       dockerfile: ./Dockerfile
+    env_file: ./.env.dev
+    networks:
+      clima-services:
+        aliases: 
+          - preports
+      default:
     ports:
       - "8080:8000"
-    env_file:
-      - app.env
-    volumes: 
+    volumes:
       - ./app:/app
-  db:
+      - ./logs:/logs
+  preports-db:
+    container_name: preports-db
+    restart: unless-stopped
     image: postgres
-    env_file:
-      - postgres.env
+    env_file: ./.env.dev
     volumes:
       - ./data/postgres_data:/var/lib/postgresql/data/
+networks:
+  clima-services:
+    external: true

+ 10 - 0
entrypoint.sh

@@ -0,0 +1,10 @@
+#!/bin/sh
+
+# Add crontabs from Django:
+python manage.py crontab add
+
+# Launch the Cron daemon:
+crond -f &
+
+# Launch Docker Command:
+exec "$@"