chore: initial commit

This commit is contained in:
2026-09-10 13:22:33 +02:00
commit f219128ffa
27 changed files with 790 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
venv/
__pycache__/
*.pyc
db.sqlite3
staticfiles/
media/
.git/
.vscode/
.env
+35
View File
@@ -0,0 +1,35 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.so
# Virtual environments
venv/
env/
.venv/
ENV/
# Django
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
media/
staticfiles/
# Environment variables
.env
.env.local
.env.*.local
# macOS
.DS_Store
# IDE
.idea/
*.swp
*.swo
# Docker overrides (keep Dockerfile)
docker-compose.override.yml
+13
View File
@@ -0,0 +1,13 @@
{
"cSpell.words": [
"Añade",
"avistamientos",
"reportar"
],
"files.associations": {
"**/templates/**/*.html": "django-html"
},
"[django-html]": {
"editor.formatOnSave": false
}
}
+40
View File
@@ -0,0 +1,40 @@
# 1. Imagen base ligera de Python
FROM python:3.12-slim
# 2. Variables para optimizar Python en contenedores
# PYTHONDONTWRITEBYTECODE: Evita generar archivos .pyc innecesarios en el contenedor
# PYTHONUNBUFFERED: Hace que los logs se impriman directamente en la consola de Coolify
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# 3. Directorio de trabajo
WORKDIR /app
# 4. Instalar dependencias del sistema necesarias para compilar Pillow y psycopg2 si hiciera falta
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
libjpeg-dev \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
# 5. Instalar dependencias de Python
COPY requirements.txt /app/
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# 6. Copiar el código del proyecto
COPY . /app/
# 7. Ejecutar collectstatic durante el build
# Pasamos una SECRET_KEY ficticia porque Django la exige para compilar estáticos
RUN python manage.py collectstatic --noinput --settings=config.settings
# 8. Asegurar permisos de ejecución para el entrypoint
RUN chmod +x /app/entrypoint.sh
# 9. Puerto interno que expondrá la aplicación
EXPOSE 8000
# 10. Comando de inicio
ENTRYPOINT ["/app/entrypoint.sh"]
View File
+18
View File
@@ -0,0 +1,18 @@
from django.contrib import admin
from .models import Avistamiento
@admin.action(description="Marcar seleccionados como Validados")
def marcar_como_validado(modeladmin, request, queryset):
queryset.update(estado=Avistamiento.Estado.VALIDADO)
@admin.action(description="Marcar seleccionados como Rechazados")
def marcar_como_rechazado(modeladmin, request, queryset):
queryset.update(estado=Avistamiento.Estado.RECHAZADO)
@admin.register(Avistamiento)
class AvistamientoAdmin(admin.ModelAdmin):
list_display = ('fecha_avistamiento', 'estado', 'fecha_reporte')
list_filter = ('estado',)
search_fields = ('descripcion', 'contacto', 'nota_validacion')
readonly_fields = ('fecha_reporte',)
actions = [marcar_como_validado, marcar_como_rechazado]
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AvistamientosConfig(AppConfig):
name = 'avistamientos'
+38
View File
@@ -0,0 +1,38 @@
# avistamientos/forms.py
from django import forms
from .models import Avistamiento
class AvistamientoForm(forms.ModelForm):
class Meta:
model = Avistamiento
exclude = ('estado', 'nota_validacion')
widgets = {
'fecha_avistamiento': forms.DateInput(attrs={'type': 'date'}),
'latitud': forms.HiddenInput(),
'longitud': forms.HiddenInput(),
'descripcion': forms.Textarea(attrs={'rows': 4, 'placeholder': 'Describe brevemente lo que has observado...'}),
'contacto': forms.EmailInput(attrs={'placeholder': 'tu@email.com (opcional)'}),
}
labels = {
'fecha_avistamiento': 'Fecha del avistamiento',
'descripcion': 'Descripción',
'foto': 'Fotografía',
'contacto': 'Email de contacto',
}
def clean(self):
cleaned_data = super().clean()
latitud = cleaned_data.get('latitud')
longitud = cleaned_data.get('longitud')
# 1. Comprobar que realmente se ha marcado un punto
if latitud is None or longitud is None:
raise forms.ValidationError("Es obligatorio marcar un punto en el mapa.")
# 2. Comprobar que está en el área de estudio (Baleares aprox.)
if not (38.0 <= latitud <= 40.5 and 1.0 <= longitud <= 4.5):
raise forms.ValidationError(
"La ubicación seleccionada queda fuera del ámbito de actuación (Islas Baleares)."
)
return cleaned_data
+29
View File
@@ -0,0 +1,29 @@
# Generated by Django 6.1.1 on 2026-09-08 11:16
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Avistamiento',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('latitud', models.FloatField()),
('longitud', models.FloatField()),
('fecha_avistamiento', models.DateField()),
('fecha_reporte', models.DateTimeField(auto_now_add=True)),
('descripcion', models.TextField()),
('foto', models.ImageField(blank=True, null=True, upload_to='avistamientos/')),
('contacto', models.EmailField(blank=True, max_length=254, null=True)),
('estado', models.CharField(choices=[('pendiente', 'Pendiente'), ('validado', 'Validado'), ('rechazado', 'Rechazado')], default='pendiente', max_length=20)),
('nota_validacion', models.TextField(blank=True)),
],
),
]
+24
View File
@@ -0,0 +1,24 @@
from django.db import models
class Avistamiento(models.Model):
class Estado(models.TextChoices):
PENDIENTE = 'pendiente', 'Pendiente'
VALIDADO = 'validado', 'Validado'
RECHAZADO = 'rechazado', 'Rechazado'
latitud = models.FloatField()
longitud = models.FloatField()
fecha_avistamiento = models.DateField()
fecha_reporte = models.DateTimeField(auto_now_add=True)
descripcion = models.TextField()
foto = models.ImageField(upload_to='avistamientos/', blank=True, null=True)
contacto = models.EmailField(blank=True, null=True)
estado = models.CharField(
max_length=20,
choices=Estado.choices,
default=Estado.PENDIENTE,
)
nota_validacion = models.TextField(blank=True)
def __str__(self):
return f"Avistamiento {self.id} ({self.fecha_avistamiento}) - {self.estado}"
+16
View File
@@ -0,0 +1,16 @@
from rest_framework import serializers
from .models import Avistamiento
class AvistamientoPublicoSerializer(serializers.ModelSerializer):
class Meta:
model = Avistamiento
# Solo los campos seguros y útiles para el mapa
fields = [
'id',
'latitud',
'longitud',
'fecha_avistamiento',
'descripcion',
'foto',
]
read_only_fields = fields
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}GEN-GOB | Posidonia{% endblock %}</title>
<!-- CSS base para todo el proyecto -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 0; color: #333; }
header { padding: 1rem 1.5rem; background: #0f3d3e; color: white; display: flex; justify-content: space-between; align-items: center; }
header a { color: white; text-decoration: none; margin-left: 1rem; }
.nav-btn { background: #198754; padding: 0.5rem 1rem; border-radius: 4px; font-weight: bold; }
.container { max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
</style>
<!-- Hueco opcional para CSS específico de una página -->
{% block extra_css %}{% endblock %}
</head>
<body>
<header>
<a href="{% url 'mapa_publico' %}" style="margin: 0; font-size: 1.2rem; font-weight: bold;">
🌿 Posidonia Alerta (GEN-GOB)
</a>
<nav>
<a href="{% url 'mapa_publico' %}">Mapa</a>
<a href="{% url 'reportar_avistamiento' %}" class="nav-btn">+ Reportar</a>
</nav>
</header>
<main>
<!-- Hueco obligatorio donde cada página pondrá su contenido -->
{% block content %}
{% endblock %}
</main>
<!-- Leaflet JS compartido -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Hueco opcional para JS específico de cada página -->
{% block extra_js %}{% endblock %}
</body>
</html>
@@ -0,0 +1,13 @@
{% extends "avistamientos/base.html" %}
{% block title %}Reporte Recibido - GEN-GOB{% endblock %}
{% block content %}
<div class="container" style="text-align: center;">
<div style="background: #f8f9fa; border-radius: 8px; padding: 2.5rem 1.5rem; border: 1px solid #e9ecef;">
<h1 style="color: #198754;">¡Gracias por tu colaboración!</h1>
<p>Tu reporte ha sido registrado correctamente y será revisado por el equipo técnico.</p>
<a href="{% url 'reportar_avistamiento' %}">← Enviar otro reporte</a>
</div>
</div>
{% endblock %}
@@ -0,0 +1,54 @@
{% extends "avistamientos/base.html" %}
{% block title %}Mapa fondeos ilegales - GEN-GOB{% endblock %}
{% block extra_css %}
<style>
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 0; color: #333; }
header { padding: 1rem 1.5rem; background: #0f3d3e; color: white; display: flex; justify-content: space-between; align-items: center; }
header h1 { margin: 0; font-size: 1.3rem; }
header a { color: white; background: #198754; padding: 0.5rem 1rem; border-radius: 4px; text-decoration: none; font-weight: bold; }
header a:hover { background: #157347; }
#mapa-completo { height: calc(100vh - 65px); width: 100vw; }
.popup-foto { width: 100%; max-height: 140px; object-fit: cover; border-radius: 4px; margin-top: 0.5rem; }
</style>
{% endblock %}
{% block content %}
<div id="mapa-completo"></div>
<!-- Django inyecta los datos como JSON seguro -->
{{ puntos|json_script:"puntos-data" }}
{% endblock %}
{% block extra_js %}
<script>
const mapa = L.map('mapa-completo').setView([39.0, 1.5], 9);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(mapa);
// Leemos el JSON inyectado por Django
const avistamientos = JSON.parse(document.getElementById('puntos-data').textContent);
avistamientos.forEach(item => {
let contenidoPopup = `
<div style="font-size: 0.95rem;">
<strong>Fecha:</strong> ${item.fecha}<br>
<p style="margin: 0.5rem 0 0;">${item.descripcion}</p>
`;
if (item.foto_url) {
contenidoPopup += `<img src="${item.foto_url}" class="popup-foto" alt="Evidencia">`;
}
contenidoPopup += `</div>`;
L.marker([item.lat, item.lng])
.addTo(mapa)
.bindPopup(contenidoPopup);
});
</script>
{% endblock %}
@@ -0,0 +1,136 @@
{% extends "avistamientos/base.html" %}
{% block title %}Reportar Fondeo - GEN-GOB{% endblock %}
{% block extra_css %}
<style>
#mapa {
height: 320px;
width: 100%;
border-radius: 8px;
margin-bottom: 0.5rem;
border: 1px solid #ccc;
}
.campo { margin-bottom: 1.2rem; }
label { display: block; font-weight: bold; margin-bottom: 0.3rem; }
input[type="text"], input[type="email"], input[type="date"], textarea {
width: 100%;
padding: 0.6rem;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
.ayuda-mapa { font-size: 0.9rem; color: #666; margin-bottom: 1.2rem; }
.coordenadas-indicador {
background: #f0f4f8;
padding: 0.5rem 0.8rem;
border-radius: 4px;
font-family: monospace;
display: inline-block;
}
button[type="submit"] {
background-color: #0d6efd;
color: white;
border: none;
padding: 0.75rem 1.5rem;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
}
button[type="submit"]:hover { background-color: #0b5ed7; }
.errores { color: #dc3545; font-size: 0.9rem; margin-top: 0.25rem; }
</style>
{% endblock %}
{% block content %}
<div class="container">
<h1>Reportar Fondeo</h1>
<p>Haz clic en el mapa para marcar la ubicación exacta del avistamiento.</p>
<div id="mapa"></div>
<div class="ayuda-mapa">
Punto seleccionado:
<span id="coord-texto" class="coordenadas-indicador">Ninguno (haz clic en el mapa)</span>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% if form.non_field_errors %}
<div style="background-color: #f8d7da; color: #842029; padding: 0.8rem; border-radius: 4px; margin-bottom: 1.2rem; border: 1px solid #f5c2c7;">
{{ form.non_field_errors }}
</div>
{% endif %}
{{ form.latitud }}
{{ form.longitud }}
{% if form.latitud.errors or form.longitud.errors %}
<div class="errores">Debes seleccionar un punto en el mapa.</div>
{% endif %}
<div class="campo">
<label for="{{ form.fecha_avistamiento.id_for_label }}">{{ form.fecha_avistamiento.label }} *</label>
{{ form.fecha_avistamiento }}
{% if form.fecha_avistamiento.errors %}
<div class="errores">{{ form.fecha_avistamiento.errors }}</div>
{% endif %}
</div>
<div class="campo">
<label for="{{ form.descripcion.id_for_label }}">{{ form.descripcion.label }} *</label>
{{ form.descripcion }}
{% if form.descripcion.errors %}
<div class="errores">{{ form.descripcion.errors }}</div>
{% endif %}
</div>
<div class="campo">
<label for="{{ form.foto.id_for_label }}">{{ form.foto.label }} (opcional)</label>
{{ form.foto }}
{% if form.foto.errors %}
<div class="errores">{{ form.foto.errors }}</div>
{% endif %}
</div>
<div class="campo">
<label for="{{ form.contacto.id_for_label }}">{{ form.contacto.label }}</label>
{{ form.contacto }}
{% if form.contacto.errors %}
<div class="errores">{{ form.contacto.errors }}</div>
{% endif %}
</div>
<button type="submit">Enviar reporte</button>
</form>
</div>
{% endblock %}
{% block extra_js %}
<script>
const mapa = L.map("mapa").setView([38.9, 1.43], 9);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: "© OpenStreetMap",
}).addTo(mapa);
let marcador;
const inputLat = document.getElementById("id_latitud");
const inputLng = document.getElementById("id_longitud");
const coordTexto = document.getElementById("coord-texto");
mapa.on('click', function(e) {
const { lat, lng } = e.latlng;
inputLat.value = lat;
inputLng.value = lng;
coordTexto.textContent = `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
if (marcador) {
marcador.setLatLng(e.latlng);
} else {
marcador = L.marker(e.latlng).addTo(mapa);
}
});
</script>
{% endblock %}
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+10
View File
@@ -0,0 +1,10 @@
# avistamientos/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.mapa_publico, name='mapa_publico'),
path('reportar/', views.reportar_avistamiento,name='reportar_avistamiento'),
path('gracias/', views.gracias, name='gracias'),
path('api/avistamientos/', views.AvistamientoListAPIView.as_view(), name='api_avistamientos'),
]
+44
View File
@@ -0,0 +1,44 @@
from django.shortcuts import render, redirect
from .forms import AvistamientoForm
from .models import Avistamiento
from rest_framework.generics import ListAPIView
from .serializers import AvistamientoPublicoSerializer
def reportar_avistamiento(request):
if request.method == 'POST':
form = AvistamientoForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return redirect('gracias')
else:
# Añade esta línea para ver en la terminal el motivo exacto:
print("ERRORES DEL FORMULARIO:", form.errors)
else:
form = AvistamientoForm()
return render(request, 'avistamientos/reportar.html', {'form': form})
def gracias(request):
return render(request, 'avistamientos/gracias.html')
def mapa_publico(request):
# 1. Filtramos solo los validados
avistamientos = Avistamiento.objects.filter(estado=Avistamiento.Estado.VALIDADO)
# 2. Proyectamos únicamente los datos públicos
puntos = []
for a in avistamientos:
puntos.append({
'lat': a.latitud,
'lng': a.longitud,
'fecha': a.fecha_avistamiento.strftime('%d/%m/%Y'),
'descripcion': a.descripcion,
'foto_url': a.foto.url if a.foto else None,
})
return render(request, 'avistamientos/mapa.html', {'puntos': puntos})
class AvistamientoListAPIView(ListAPIView):
serializer_class = AvistamientoPublicoSerializer
def get_queryset(self):
# Filtramos en base de datos antes de pasar al serializador
return Avistamiento.objects.filter(estado=Avistamiento.Estado.VALIDADO).order_by('-fecha_avistamiento')
View File
+16
View File
@@ -0,0 +1,16 @@
"""
ASGI config for config project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_asgi_application()
+155
View File
@@ -0,0 +1,155 @@
"""
Django settings for config project.
Generated by 'django-admin startproject' using Django 6.1.1.
For more information on this file, see
https://docs.djangoproject.com/en/6.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.1/ref/settings/
"""
import os
from pathlib import Path
import dj_database_url
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('SECRET_KEY','django-insecure-clave-de-desarrollo-local')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 't')
# Lista de dominios separados por coma: 'avistamientos.eivissaenvironmentalwatch.es,localhost,127.0.0.1'
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
# Si usas HTTPS detrás de Traefik/Coolify, añade esto para que Django reconozca el proxy seguro:
CSRF_TRUSTED_ORIGINS = os.environ.get('CSRF_TRUSTED_ORIGINS', 'http://localhost,http://127.0.0.1').split(',')
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'avistamientos',
'rest_framework'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # <-- AQUÍ
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
# Database
# https://docs.djangoproject.com/en/6.1/ref/settings/#databases
# Si existe DATABASE_URL usa Postgres; si no, recurre a SQLite
DATABASE_URL = os.environ.get('DATABASE_URL')
if DATABASE_URL:
DATABASES = {
'default': dj_database_url.config(default=DATABASE_URL, conn_max_age=600)
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Email
# https://docs.djangoproject.com/en/6.1/topics/email/#topic-email-configuration
MAILERS = {
'default': {
'BACKEND': 'django.core.mail.backends.console.EmailBackend',
},
}
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.1/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
# Almacenamiento optimizado de WhiteNoise (comprime y añade hashes para caché)
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
+28
View File
@@ -0,0 +1,28 @@
"""
URL configuration for config project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/6.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('avistamientos.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+16
View File
@@ -0,0 +1,16 @@
"""
WSGI config for config project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_wsgi_application()
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
set -e
echo "==> Aplicando migraciones de base de datos..."
python manage.py migrate --noinput
echo "==> Iniciando Gunicorn..."
exec gunicorn config.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 3 \
--timeout 120
Executable
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
+9
View File
@@ -0,0 +1,9 @@
asgiref==3.12.1
dj-database-url==3.1.2
Django==6.1.1
djangorestframework==3.18.1
gunicorn==26.2.0
pillow==12.3.0
psycopg2-binary==2.9.12
sqlparse==0.6.0
whitenoise==6.12.0