chore: initial commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
db.sqlite3
|
||||
staticfiles/
|
||||
media/
|
||||
.git/
|
||||
.vscode/
|
||||
.env
|
||||
+35
@@ -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
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Añade",
|
||||
"avistamientos",
|
||||
"reportar"
|
||||
],
|
||||
"files.associations": {
|
||||
"**/templates/**/*.html": "django-html"
|
||||
},
|
||||
"[django-html]": {
|
||||
"editor.formatOnSave": false
|
||||
}
|
||||
}
|
||||
+40
@@ -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"]
|
||||
@@ -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]
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AvistamientosConfig(AppConfig):
|
||||
name = 'avistamientos'
|
||||
@@ -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
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -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'),
|
||||
]
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user