Sintoma: /accounts/password_change/ renderizaba la pantalla del admin
de Django (cabecera "Administracion de Django", breadcrumb "Inicio >
Cambio de contrasena") en vez de la plantilla de la app.
Causa: con APP_DIRS=True (settings.py) y sin DIRS explicito, Django
busca plantillas recorriendo INSTALLED_APPS en orden y usa la PRIMERA
coincidencia por ruta relativa. django.contrib.admin trae sus propias
registration/password_change_form.html y
registration/password_change_done.html. Como 'django.contrib.admin'
aparecia antes que 'expenses' en la lista, sus plantillas ganaban
siempre a las de nuestra app, que ni siquiera llegaban a evaluarse.
Verificado con el shell de Django (get_template(...).origin.name):
Antes del cambio:
registration/password_change_form.html -> .../django/contrib/admin/templates/registration/password_change_form.html
registration/password_change_done.html -> .../django/contrib/admin/templates/registration/password_change_done.html
Despues del cambio:
registration/password_change_form.html -> .../expenses/templates/registration/password_change_form.html
registration/password_change_done.html -> .../expenses/templates/registration/password_change_done.html
Esto tambien explica por que el bug de las plantillas rotas (extendian
"base.html", que no existe; ver commit anterior) llevaba tanto tiempo
sin dar error: al estar sombreadas, esas plantillas nunca se
renderizaban, asi que su TemplateDoesNotExist nunca saltaba. Y explica
por que el login SI funcionaba pese a la colision de app: el admin
publica su login en "admin/login.html", no en "registration/login.html",
asi que ahi no habia conflicto.
Efecto secundario esperado y comprobado (no es una regresion, pero hay
que saberlo): /admin/password_change/, que Django resuelve tambien por
la ruta "registration/password_change_form.html" cuando un admin
cambia su propia contrasena, ahora renderiza con el layout completo de
la app (menu de navegacion, etc.) en vez del look propio del admin.
Probado con un superusuario contra /admin/, /admin/login/,
/admin/password_change/ y /admin/password_change/done/: los cuatro
responden 200 y el formulario funciona (AdminPasswordChangeForm hereda
los mismos campos old_password/new_password1/new_password2 de
PasswordChangeForm, asi que el parcial generico de campos los pinta
sin problema). El resto del admin (listados, cambio de contrasena de
otros usuarios, etc.) no usa ninguna plantilla bajo registration/ ni
admin/ que la app sobrescriba, asi que no le afecta.
No se reordena alfabeticamente esta lista en el futuro sin tener en
cuenta esto: hay un comentario en el propio settings.py explicandolo.
|
||
|---|---|---|
| expenses_manager | ||
| .gitignore | ||
| LICENSE | ||
| README.md | ||
expenses_manager
Aplicación web de gestión de finanzas personales (gastos, ingresos, cuentas, categorías, objetivos y repostajes de vehículo), construida con Django y desplegada en un NAS Synology mediante Docker + PostgreSQL.
Características
- Registro de gastos e ingresos, con categorías jerárquicas y etiquetas.
- Cuentas con cálculo de saldo (soft-delete para preservar históricos).
- Dashboard con gráficos por categoría, evolución de saldo y comparativas de periodo.
- Objetivos de tres tipos: pago/deuda, presupuesto (mensual/anual) y ahorro.
- Módulo de repostajes con cálculo de consumo, precio por litro y km recorridos.
Stack
- Python 3.11 · Django 5.2
- PostgreSQL (producción) / SQLite (desarrollo local por defecto)
- Gunicorn + WhiteNoise
- Docker (despliegue) · Jenkins (CI)
Desarrollo local
Por defecto, en local la aplicación usa SQLite, así que no necesitas levantar ninguna base de datos para empezar.
Requisitos
- Python 3.11 (estos pasos asumen un entorno conda, pero sirve cualquier entorno virtual).
Pasos
-
Clona el repositorio:
git clone https://gitea.kuijper.es/jkuijperm/expenses_manager.git cd expenses_manager -
Crea y activa el entorno conda:
conda create -n gastos python=3.11 conda activate gastos -
Instala las dependencias:
pip install -r expenses_manager/requirements.txt -
Crea el archivo
.env(ver Variables de entorno). Para desarrollo basta con:DEBUG=True SECRET_KEY=cualquier-clave-para-desarrolloCon
DEBUG=Truey sin variablesDB_*, se usa SQLite automáticamente. -
Aplica las migraciones y arranca el servidor:
cd expenses_manager python manage.py migrate python manage.py runserver -
Abre
http://127.0.0.1:8000/en el navegador.
Datos de demostración (opcional): con
DEBUG=Truepuedes poblar la base de datos con un usuario y datos de ejemplo:python manage.py seed_demoEl comando está protegido para no ejecutarse si
DEBUG=False.
Variables de entorno
La configuración se lee de un archivo .env (mediante python-dotenv) situado
en expenses_manager/ (junto a manage.py). Este archivo no se versiona.
| Variable | Obligatoria | Por defecto | Descripción |
|---|---|---|---|
SECRET_KEY |
Sí en producción | — | Clave secreta de Django. Con DEBUG=False es obligatoria (si falta, la app no arranca). |
DEBUG |
No | False |
True en desarrollo. Activa SQLite por defecto y desactiva el hardening. |
ALLOWED_HOSTS |
No | localhost,127.0.0.1 |
Lista separada por comas. |
CSRF_TRUSTED_ORIGINS |
No | (vacío) | Lista separada por comas (p. ej. https://midominio.com). |
DB_ENGINE |
No | sqlite3 |
postgresql para usar Postgres; cualquier otro valor usa SQLite. |
DB_NAME |
Con Postgres | — | Nombre de la base de datos. |
DB_USER |
Con Postgres | — | Usuario. |
DB_PASSWORD |
Con Postgres | — | Contraseña. |
DB_HOST |
No | localhost |
Host de la base de datos (db en Docker Compose). |
DB_PORT |
No | 5432 |
Puerto. |
En el repositorio hay un archivo .env.example con estas variables como
plantilla. Cópialo a .env y rellena los valores.
Despliegue con Docker
El despliegue en producción usa Docker con dos servicios: la aplicación Django (servida por Gunicorn) y una base de datos PostgreSQL.
Dockerfile
El Dockerfile está en el repositorio (app/Dockerfile en el árbol de
despliegue) y hace lo esencial: parte de python:3.11-slim, instala las
dependencias de requirements.txt y copia el código. A grandes rasgos:
FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
WORKDIR /app/expenses_manager
COPY expenses_manager/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
docker-compose (no versionado)
El docker-compose.yml no está en el repositorio: es específico de la
infraestructura del NAS y vive fuera del proyecto. A continuación se documenta
qué debe contener, para poder reproducirlo. Ningún secreto va escrito en el
archivo: todos los valores sensibles se leen del .env.
services:
db:
image: postgres:15
restart: always
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- ./postgres:/var/lib/postgresql/data
web:
build: ./app
container_name: finanzas_web
command: gunicorn expenses_manager.wsgi:application --bind 0.0.0.0:8000
volumes:
- ./app:/app
ports:
- "8001:8000"
depends_on:
- db
env_file:
- ./app/expenses_manager/.env
Notas:
- Las credenciales de Postgres (
DB_NAME,DB_USER,DB_PASSWORD) las toma Docker Compose de un.envsituado junto aldocker-compose.yml. Es un archivo distinto del.envde Django (que va dentro del proyecto y lo carga la propia app víaenv_file). - El servicio
webno repite las variables de base de datos en un bloqueenvironment: las hereda del.envde Django a través deenv_file.
Puesta en marcha y actualizaciones
Desde el directorio que contiene el docker-compose.yml:
git -C app pull # traer los últimos cambios al repo montado
docker-compose down
docker-compose up -d --build
docker-compose exec web python manage.py migrate
docker-compose exec web python manage.py collectstatic --noinput
migratesolo es necesario si hay migraciones nuevas.collectstaticsolo si han cambiado archivos estáticos (CSS/JS).- No ejecutes
makemigrationsen producción: las migraciones se generan en desarrollo y llegan versionadas con elgit pull.
Tests
La suite usa pytest + pytest-django:
cd expenses_manager
pytest
La integración continua (Jenkins) ejecuta los tests en cada cambio de la rama
main.
Ramas
main— solo recibe cambios ya probados (vía merge desdedev).dev— rama de trabajo activa.