Compare commits

61 Commits

Author SHA1 Message Date
minguezsanzjuanjose
1a53cd3918 feat: add postrequest script to save access_token in active environment
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 14:25:24 +02:00
minguezsanzjuanjose
35e1d38859 fix: use native Postman auth field and declare collection variables
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 14:11:39 +02:00
minguezsanzjuanjose
a064895689 fix: add missing initial migration for promociones app
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:53:14 +02:00
minguezsanzjuanjose
05e95f8880 chore: remove legacy postman_environments.json (replaced by environments/ folder)
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:43:35 +02:00
minguezsanzjuanjose
af38d88876 fix: add Postman-compatible environment files (split by env, correct format)
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:35:57 +02:00
minguezsanzjuanjose
02fa6247f1 fix: replace external saas_network with named network to auto-create on compose up
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:27:06 +02:00
minguezsanzjuanjose
428b745700 chore: add Makefile to auto-create saas_network before docker compose up
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:25:18 +02:00
minguezsanzjuanjose
f6892b2166 feat: expand Request_API with all engine endpoints
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Adds get_dataComplex, set_data2, set_data_batch methods for all 3 specialized APIs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 02:30:48 +02:00
minguezsanzjuanjose
d2c91d5196 feat: add Request_API dispatcher and internal API URLs
Replaces urllib http_client with proper Request_API class in general/request_api.py
using the requests library. Adds API_BACKOFFICE, API_COMUNICACIONES, API_DOCUMENTACION
settings pointing to Docker service names. Adds requests==2.32.3 to requirements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 02:24:46 +02:00
minguezsanzjuanjose
d735d73322 feat: add saas_network to docker-compose and http_client for internal API calls
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 02:05:08 +02:00
minguezsanzjuanjose
197cff011f docs: add createsuperuser step for Docker setup
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 00:10:09 +02:00
minguezsanzjuanjose
590e18e994 fix: forzar LF en entrypoint.sh + environments postman + limpiar variables coleccion
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
- .gitattributes: añadir '*.sh text eol=lf' para evitar CRLF en Windows
- deployments/entrypoint.sh: renormalizado a LF
- postman_environments.json: entornos LOCAL/DEV/PROD en un único fichero
- postman_collection.json: eliminadas variables por defecto (vienen del entorno)
2026-04-17 23:01:36 +02:00
juanjo
c692ce2a61 sync
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-17 01:15:05 +02:00
juanjo
152e9c14ef Merge: docs flujo ramas 2026-04-17 01:15:05 +02:00
juanjo
99de5f06b5 docs: corregir flujo de ramas en CLAUDE.md (pre-dev → dev → master)
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 01:15:04 +02:00
juanjo
d7a84a4dfa Merge master: fix api_hub_dispatcher → api_config
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-17 01:13:54 +02:00
juanjo
fbc5f0f6c4 fix: reemplazar referencias a api_hub_dispatcher por api_config
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
ROOT_URLCONF, WSGI_APPLICATION, DJANGO_SETTINGS_MODULE apuntaban al
módulo renombrado — causaba ModuleNotFoundError al arrancar en Docker.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 01:13:52 +02:00
juanjo
cac00a4f8c Merge master: fix env_file docker-compose
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-17 01:06:31 +02:00
juanjo
a17f00bad2 fix: env_file .env (relativo a deployments/) en docker-compose
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Jenkins copia el secret a deployments/.env — el path debe ser .env
relativo al docker-compose.yml, no ../.env a la raíz del workspace.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 01:06:28 +02:00
juanjo
b8c3e03348 fix: corregir env_file path en docker-compose (../.env → .env)
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Jenkins copia el secret a deployments/.env — el path debe ser relativo
al docker-compose.yml, no a la raíz del workspace.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 01:05:37 +02:00
juanjo
252e176e9d Merge master: gunicorn + wait-for-db + healthcheck
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-17 00:39:27 +02:00
juanjo
41fc2a0aa0 fix: gunicorn + wait-for-db + healthcheck en docker-compose
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
- entrypoint.sh: sustituye runserver por gunicorn (workers=2, timeout=120)
- entrypoint.sh: espera a PostgreSQL antes de migrar cuando DB_HOST está definido
- docker-compose.yml: unifica nombre de servicio db, añade healthcheck robusto,
  corrige env_file path a ../.env

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:39:21 +02:00
juanjo
001bf13d26 chore: añadir CLAUDE.md con convenciones del ecosistema
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:12:18 +02:00
juanjo
47f23a73e1 docs: README completo con despliegue, BD y mantenimiento
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:02:21 +02:00
juanjo
04c37f669c revert: restaurar nombre api_config (revertir renombrado erróneo)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:02:21 +02:00
juanjo
92a00ec75f refactor: renombrar proyecto principal api_config → api_hub_dispatcher
- Renombrar carpeta app/api_config/ → app/api_hub_dispatcher/
- Actualizar DJANGO_SETTINGS_MODULE en asgi.py, wsgi.py, manage.py
- Actualizar ROOT_URLCONF y WSGI_APPLICATION en settings.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:02:21 +02:00
juanjo
30c540b6f9 revert: restaurar nombre api_config (revertir renombrado erróneo)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 23:31:08 +02:00
juanjo
778753afd6 refactor: renombrar proyecto principal api_config → api_hub_dispatcher
- Renombrar carpeta app/api_config/ → app/api_hub_dispatcher/
- Actualizar DJANGO_SETTINGS_MODULE en asgi.py, wsgi.py, manage.py
- Actualizar ROOT_URLCONF y WSGI_APPLICATION en settings.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 23:28:43 +02:00
b91f5d09e5 Merge pull request 'dev' (#43) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #43
2026-04-16 17:32:46 +00:00
1e1348bc1a Merge pull request 'dev' (#41) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #41
2026-04-16 15:34:48 +00:00
juanjo
384f47df5e Merge branch 'master' of https://git.v-encore-lab.com/Proyecto-SaaS/django-core-base
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 17:29:18 +02:00
juanjo
2684e251f7 Merge dev: fix serialización LogService 2026-04-16 17:28:48 +02:00
juanjo
94faedecae Merge pre-dev: fix serialización LogService 2026-04-16 17:28:48 +02:00
3f95e92318 Merge pull request 'dev' (#39) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #39
2026-04-16 15:11:49 +00:00
juanjo
03663aacb4 Merge dev: fix ruta data/ y LogService resiliente 2026-04-16 17:07:23 +02:00
juanjo
0c18ffc2f9 Merge pre-dev: fix ruta data/ y LogService resiliente 2026-04-16 17:07:23 +02:00
77b722d739 Merge pull request 'dev' (#37) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #37
2026-04-16 14:57:37 +00:00
juanjo
156b5ad77d Merge dev: feat app general con LogService 2026-04-16 16:51:00 +02:00
juanjo
5d2a6469aa Merge pre-dev: feat app general con LogService 2026-04-16 16:51:00 +02:00
e2ae400889 Merge pull request 'dev' (#35) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #35
2026-04-16 14:37:32 +00:00
juanjo
4425141cb3 Merge dev: refactor mover BD SQLite a carpeta data/
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 16:34:16 +02:00
juanjo
29db0eb0a2 Merge pre-dev: refactor mover BD SQLite a carpeta data/ 2026-04-16 16:34:00 +02:00
0a73b91e12 Merge pull request 'fix: corregir name en common/apps.py' (#33) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 14:13:02 +00:00
508f3f028d Merge pull request 'refactor: reorganizar estructura app/api_config' (#31) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 14:02:19 +00:00
bc82249a29 Merge pull request 'Merge dev into master' (#29) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 13:51:32 +00:00
f3514d399e Merge pull request 'dev' (#27) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #27
2026-04-14 23:44:35 +00:00
0ac0a859b9 Merge pull request 'dev' (#24) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #24
2026-04-14 21:52:57 +00:00
7a9a1686fa Merge pull request 'dev' (#21) from dev into master
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
Reviewed-on: #21
2026-04-14 21:28:08 +00:00
5a7209badb Merge pull request 'dev' (#16) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #16
2026-04-12 20:57:46 +00:00
32e3184b59 Merge pull request 'dev' (#11) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #11
2026-04-12 13:42:46 +00:00
083375c5f0 Merge pull request 'dev' (#9) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #9
2026-04-12 12:10:27 +00:00
6f84db00cd Merge pull request 'dev' (#7) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #7
2026-04-12 02:53:09 +00:00
b60ecccad4 Merge pull request 'dev' (#28) from dev into master
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
Reviewed-on: #28
2026-04-11 23:02:30 +02:00
c4e8675fe8 Merge pull request 'dev' (#23) from dev into master
Reviewed-on: #23
2026-04-11 20:48:19 +02:00
04fb83447f Merge pull request 'dev' (#16) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/16
2026-04-11 18:39:48 +02:00
bdda074fa1 Merge pull request 'dev' (#14) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/14
2026-04-11 18:23:51 +02:00
64afc3aedb Merge pull request 'dev' (#11) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/11
2026-04-11 18:04:09 +02:00
156e1aa27c Merge pull request 'dev' (#9) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/9
2026-04-11 15:21:01 +02:00
16b7f956b3 Merge pull request 'dev' (#7) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/7
2026-04-11 15:13:03 +02:00
34faf2157e Merge pull request 'dev' (#5) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/5
2026-04-11 15:07:07 +02:00
2a6723aedf Merge pull request 'dev' (#3) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/3
2026-04-11 14:57:47 +02:00
17 changed files with 752 additions and 109 deletions

3
.gitattributes vendored
View File

@@ -1,2 +1,5 @@
# Auto detect text files and perform LF normalization
* text=auto
# Shell scripts: forzar LF siempre (evita CRLF en Windows que rompe Docker)
*.sh text eol=lf

110
CLAUDE.md Normal file
View File

@@ -0,0 +1,110 @@
# Convenciones del Ecosistema V-Encore Lab
## Ecosistema de microservicios
| Repo | Rol | Puerto |
|------|-----|--------|
| `django-core-base` | Hub orquestador principal | 8000 |
| `api_backoffice` | Consulta y gestión de BD | 8001 |
| `api_comunicaciones` | Emails, notificaciones, webhooks | 8002 |
| `api_documentacion` | Generación y gestión de documentos | 8003 |
| `web_interno` | Panel de gestión (React + Ant Design) | 3000 |
## Stack Django
- Django 5.0 + DRF + SimpleJWT
- Apps bajo `app/`. Prefijo `/api/` en todas las URLs salvo `admin/`.
- Patrón 3 capas: **URL → View → Action**
- SQL con `connection.cursor()` y placeholders (`%s`). Nunca concatenar strings.
## Patrón de vistas — 4 bloques obligatorios
```python
from general.utilidades.acciones import LogService
class MiVista(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/mi-app/mi-endpoint/'
# Bloque 1 — Inicio Log
log_id = LogService.gestionar_log(self, request, path=path)
try:
# Bloque 2 — Data Cleaning
data = request.data
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_request=data, status_code=100)
params = {'campo': data.get('campo')}
except Exception as error:
response = {'error': str(error)}
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=response, status_code=400)
return JsonResponse(response, status=400)
try:
# Bloque 3 — Action Call
resultado = mi_accion(params)
# Bloque 4 — Cierre Log
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=resultado, status_code=200)
return JsonResponse(resultado, status=200)
except Exception as error:
response = {'error': str(error)}
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=response, status_code=500)
return JsonResponse(response, status=500)
```
## LogService — reglas
- Siempre llamar como `LogService.gestionar_log(self, request, ...)` — pasar `self` de la vista.
- Primera llamada (sin `log_id`): crea el registro, devuelve el `log_id`.
- Llamadas siguientes: actualizar con `log_id=log_id`.
- `body_request` y `body_response` se serializan con `DjangoJSONEncoder` — soporta `datetime.date`, `Decimal`, etc.
- `status_code` como entero. Usar `is not None` para comprobar (0 es válido).
## Base de datos
- `DB_HOST` definido → PostgreSQL
- `DB_HOST` no definido → SQLite en `app/data/db.sqlite3`
- Migraciones siempre desde `app/`: `cd app && python manage.py migrate`
- SQL INSERT con psycopg2: usar `INSERT ... RETURNING id` + `cursor.fetchone()`. **Nunca `cursor.lastrowid`**.
- Booleans en psycopg2: pasar `True`/`False`, no `1`/`0`.
## Flujo de ramas
```
pre-dev → dev → master
```
- `pre-dev`: desarrollo activo — aquí entran todos los cambios
- `dev`: validación previa a producción
- `master`: producción estable
- Merges siempre con `--no-ff`
- **NUNCA** propagar en sentido inverso (master → dev o dev → pre-dev)
- Secuencia correcta:
```bash
git checkout pre-dev && git merge <mi-cambio> --no-ff
git checkout dev && git merge pre-dev --no-ff && git push origin dev
git checkout master && git merge dev --no-ff && git push origin master
```
## Estructura de app Django
```
app/
├── api_config/ # settings.py, urls.py, wsgi.py
├── general/ # LogService, utils — NO tiene modelos propios
├── backend_admin/ # Modelo Log (audit_logs), admin panel
├── common/ # Modelos y utilidades compartidas entre apps
├── <feature_app>/ # Una app por dominio de negocio
├── data/ # SQLite (gitignored, solo .gitkeep versionado)
└── manage.py
```
## Fixtures
- `loaddata` ignora `auto_now_add=True` — incluir fechas explícitas en el JSON o falla con NOT NULL en PostgreSQL.

341
README.md
View File

@@ -1,73 +1,324 @@
# django-core-base
# django-core-base · API Hub Orquestador
// V-Encore Lab: Sistema Automatizado v1.0.4
> V-Encore Lab — Microservicio principal. Actúa como orquestador y punto de entrada central del ecosistema SaaS.
> Puerto por defecto: **8000**
## 🚀 Inicio Rápido (Desarrollo Local)
---
## Tabla de contenidos
1. [Requisitos previos](#requisitos-previos)
2. [Inicio rápido — Desarrollo local](#inicio-rápido--desarrollo-local)
3. [Variables de entorno](#variables-de-entorno)
4. [Base de datos](#base-de-datos)
5. [Docker — Producción](#docker--producción)
6. [Estructura del proyecto](#estructura-del-proyecto)
7. [Endpoints principales](#endpoints-principales)
8. [Patrón LogService](#patrón-logservice)
9. [Flujo de ramas](#flujo-de-ramas)
10. [Mantenimiento](#mantenimiento)
---
## Requisitos previos
| Herramienta | Versión mínima |
|-------------|---------------|
| Python | 3.11 |
| pip | 23+ |
| Docker | 24+ |
| Docker Compose | v2 |
| Git | 2.40+ |
---
## Inicio rápido — Desarrollo local
### 1. Clonar y Configurar
```bash
# 1. Clonar
git clone https://git.v-encore-lab.com/Proyecto-SaaS/django-core-base.git
cd django-core-base
cp .env.example .env
```
### 2. Instalar Dependencias
```bash
# 2. Entorno virtual
python -m venv .venv
source .venv/bin/activate # Linux/Mac
.venv\Scripts\activate # Windows
# 3. Dependencias
pip install -r deployments/requirements.txt
```
### 3. Migraciones de Base de Datos
```bash
# Crear y aplicar migraciones para todos los modelos
python manage.py makemigrations
# 4. Variables de entorno
cp .env.example .env
# Editar .env según el entorno (ver sección Variables de entorno)
# 5. Migraciones (SQLite en local)
cd app
python manage.py migrate
# Opcional: Crear superusuario
# 6. Superusuario (primera vez)
python manage.py createsuperuser
# 7. Servidor de desarrollo
python manage.py runserver 0.0.0.0:8000
```
Accesos:
- API: http://localhost:8000/api/
- Admin: http://localhost:8000/admin/
---
## Variables de entorno
Copia `.env.example` a `.env` y ajusta los valores:
| Variable | Descripción | Ejemplo / Default |
|----------------|--------------------------------------------------|----------------------------|
| `SECRET_KEY` | Clave secreta Django | `django-insecure-...` |
| `DEBUG` | Modo debug | `True` (dev) / `False` (prod) |
| `ALLOWED_HOSTS`| Hosts permitidos (separados por coma) | `localhost,127.0.0.1` |
| `DB_HOST` | Host PostgreSQL. **Si no se define → SQLite** | `localhost` / vacío |
| `DB_PORT` | Puerto PostgreSQL | `5432` |
| `DB_NAME` | Nombre de la base de datos | `vencorelab` |
| `DB_USER` | Usuario PostgreSQL | `postgres` |
| `DB_PASSWORD` | Contraseña PostgreSQL | `postgres` |
---
## Base de datos
### Desarrollo local — SQLite
Si `DB_HOST` **no** está definido en `.env`, Django usa automáticamente SQLite:
```
app/data/db.sqlite3
```
```bash
cd app
python manage.py migrate # crea app/data/db.sqlite3
python manage.py createsuperuser
```
### 4. Correr el Servidor
```bash
python manage.py runserver
La carpeta `app/data/` está en `.gitignore` (solo se versiona `.gitkeep`).
### Producción — PostgreSQL
Define `DB_HOST` en `.env` para activar el driver PostgreSQL:
```env
DB_HOST=localhost
DB_PORT=5432
DB_NAME=vencorelab
DB_USER=postgres
DB_PASSWORD=supersecret
```
Abrir http://localhost:8000
## 🐳 Docker (Producción/Desarrollo)
```bash
docker-compose up --build
cd app
python manage.py migrate
```
Acceder a:
- App: http://localhost:8000
- Admin: http://localhost:8000/admin/
- DB: localhost:5432 (Postgres)
## 📋 Comandos Django Comunes
### Comandos útiles de migración
```bash
# Verificar configuración
python manage.py check
cd app
# Recopilar static files
python manage.py collectstatic --noinput
# Ver migraciones pendientes
python manage.py showmigrations
# Test
python manage.py test
# Crear migraciones de una app
python manage.py makemigrations <app_name>
# Aplicar todas las migraciones
python manage.py migrate
# Revertir migraciones de una app al estado inicial
python manage.py migrate <app_name> zero
```
## 🔧 Estructura del Proyecto
---
```
├── apps/ # Aplicaciones Django
│ ├── backend_admin/
│ ├── common/
│ └── promociones/
├── core/ # Configuración principal
├── deployments/ # Docker, requirements prod
└── manage.py
## Docker — Local y Producción
```bash
cd deployments
# Construir e iniciar
docker-compose up --build -d
# Ver logs
docker-compose logs -f
# Parar
docker-compose down
# Parar y eliminar volúmenes (¡cuidado en producción!)
docker-compose down -v
```
## .env Variables
Ver `.env.example` para configuración.
El contenedor expone el puerto **8000**.
### Crear superusuario (primera vez con Docker)
Una vez los contenedores estén corriendo:
```bash
docker-compose exec web sh -c "cd /app/app && python manage.py createsuperuser"
```
Introduce usuario, email y contraseña cuando lo pida. Estas credenciales son las que usarás para entrar en el panel web (`web_interno`) y en `/admin/`.
Para producción con PostgreSQL, asegúrate de que `.env` tenga `DB_HOST` apuntando al host correcto (puede ser el nombre del servicio en la red Docker).
---
## Estructura del proyecto
```
django-core-base/
├── app/
│ ├── api_config/ # Configuración Django (settings, urls, wsgi)
│ ├── general/ # App transversal
│ │ └── utilidades/
│ │ ├── acciones.py # LogService — auditoría centralizada
│ │ └── utils.py # Utilidades HTTP (get_client_ip, etc.)
│ ├── backend_admin/ # App admin: modelo Log, endpoints de gestión
│ ├── common/ # Modelos y utilidades compartidas
│ ├── promociones/ # App de ejemplo
│ ├── automatizados/ # Endpoints para Jenkins/automatizaciones
│ ├── data/ # Directorio de la BD SQLite (gitignored)
│ │ └── .gitkeep
│ └── manage.py
├── deployments/
│ ├── requirements.txt
│ └── Dockerfile
├── docker-compose.yml
├── .env.example
└── README.md
```
---
## Endpoints principales
| Método | Ruta | Descripción | Auth |
|--------|-------------------------------|-----------------------------------|--------------|
| POST | `/api/token/` | Obtener JWT (login) | No |
| POST | `/api/token/refresh/` | Renovar access token | No |
| GET | `/admin/` | Panel de administración Django | Session |
| POST | `/api/promociones/obtener/` | Consultar promociones | JWT Bearer |
| GET | `/api/general/health/` | Health check | No |
Autenticación: `Authorization: Bearer <access_token>`
---
## Patrón LogService
Todas las vistas deben usar `LogService` para auditoría:
```python
from general.utilidades.acciones import LogService
class MiVista(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/mi-app/mi-endpoint/'
# Bloque 1 — Inicio log
log_id = LogService.gestionar_log(self, request, path=path)
try:
# Bloque 2 — Limpieza de datos
data = request.data
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_request=data, status_code=100)
params = {'campo': data.get('campo')}
except Exception as error:
response = {'error': str(error)}
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=response, status_code=400)
return JsonResponse(response, status=400)
try:
# Bloque 3 — Acción
resultado = mi_accion(params)
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=resultado, status_code=200)
return JsonResponse(resultado, status=200)
except Exception as error:
response = {'error': str(error)}
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=response, status_code=500)
return JsonResponse(response, status=500)
```
`LogService` registra automáticamente en la tabla `audit_logs` el usuario, IP, path, request/response y status code.
---
## Flujo de ramas
```
pre-dev → dev → master
```
- `pre-dev`: desarrollo activo, integración de features
- `dev`: validación previa a producción
- `master`: rama de producción estable
```bash
# Crear feature
git checkout pre-dev
git checkout -b feature/mi-feature
# Mergear a pre-dev
git checkout pre-dev
git merge feature/mi-feature --no-ff
# Promover a dev
git checkout dev
git merge pre-dev --no-ff
# Promover a master
git checkout master
git merge dev --no-ff
```
---
## Mantenimiento
```bash
# Limpiar sesiones expiradas
cd app && python manage.py clearsessions
# Ver estado de la BD
cd app && python manage.py dbshell
# Backup SQLite (desarrollo)
cp app/data/db.sqlite3 app/data/db.sqlite3.bak
# Backup PostgreSQL (producción)
pg_dump -U postgres vencorelab > backup_$(date +%Y%m%d).sql
# Actualizar dependencias
pip list --outdated
pip install -U <paquete>
pip freeze > deployments/requirements.txt
# Reiniciar contenedor Docker
docker-compose restart web
# Ver logs del contenedor
docker-compose logs -f web --tail=100
```
---
> **V-Encore Lab** — Sistema Automatizado v1.0
> Repositorio: `Proyecto-SaaS/django-core-base`

View File

@@ -175,6 +175,11 @@ LOGGING = {
},
}
# --- URLs de APIs internas (via red Docker saas_network) ---
API_BACKOFFICE = os.getenv('API_BACKOFFICE', 'http://api_backoffice:8001')
API_COMUNICACIONES = os.getenv('API_COMUNICACIONES', 'http://api_comunicaciones:8002')
API_DOCUMENTACION = os.getenv('API_DOCUMENTACION', 'http://api_documentacion:8003')
# --- CONFIGURACIONES PERSONALIZADAS DE LA APP ---
# Leemos la variable del .env (cargado previamente con load_dotenv)

60
app/common/http_client.py Normal file
View File

@@ -0,0 +1,60 @@
import urllib.request
import json
import os
BACKOFFICE_URL = os.getenv('BACKOFFICE_URL', 'http://django_app_backoffice:8000')
COMUNICACIONES_URL = os.getenv('COMUNICACIONES_URL', 'http://django_app_comunicaciones:8000')
DOCUMENTACION_URL = os.getenv('DOCUMENTACION_URL', 'http://django_app_documentacion:8000')
def _post(url, payload):
data = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(
url, data=data,
headers={'Content-Type': 'application/json'},
method='POST'
)
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read())
# --- Backoffice ---
def backoffice_obtener_usuario(params):
return _post(f'{BACKOFFICE_URL}/api/usuarios/obtener/', params)
def backoffice_crear_usuario(params):
return _post(f'{BACKOFFICE_URL}/api/usuarios/guardar/', params)
def backoffice_obtener_cliente(params):
return _post(f'{BACKOFFICE_URL}/api/clientes/obtener/', params)
def backoffice_crear_cliente(params):
return _post(f'{BACKOFFICE_URL}/api/clientes/guardar/', params)
def backoffice_obtener_contrato(params):
return _post(f'{BACKOFFICE_URL}/api/contratos/obtener/', params)
def backoffice_crear_contrato(params):
return _post(f'{BACKOFFICE_URL}/api/contratos/guardar/', params)
# --- Comunicaciones ---
def comunicaciones_enviar_email(params):
return _post(f'{COMUNICACIONES_URL}/api/email/enviar/', params)
def comunicaciones_enviar_sms(params):
return _post(f'{COMUNICACIONES_URL}/api/sms/enviar/', params)
def comunicaciones_registrar_webhook(params):
return _post(f'{COMUNICACIONES_URL}/api/webhooks/registrar/', params)
# --- Documentacion ---
def documentacion_generar_pdf(params):
return _post(f'{DOCUMENTACION_URL}/api/generation/pdf/', params)
def documentacion_obtener_template(params):
return _post(f'{DOCUMENTACION_URL}/api/templates/obtener/', params)
def documentacion_guardar_documento(params):
return _post(f'{DOCUMENTACION_URL}/api/storage/guardar/', params)

100
app/general/request_api.py Normal file
View File

@@ -0,0 +1,100 @@
import json
import requests
from django.conf import settings
class Request_API:
"""Dispatcher de llamadas HTTP internas hacia las APIs especializadas."""
def _post(self, base_url, url_path, data):
url = base_url.rstrip('/') + '/' + url_path.lstrip('/')
headers = {'Content-Type': 'application/json'}
response = requests.post(url, data=json.dumps(data), headers=headers, timeout=30)
return response.json(), response.status_code
# ------------------------------------------------------------------ #
# api_backoffice #
# ------------------------------------------------------------------ #
def backoffice_get_parameterized(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/get_parameterized/', data)
def backoffice_get_dataComplex(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/get_dataComplex/', data)
def backoffice_set_parameterized(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/set_parameterized/', data)
def backoffice_set_data2(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/set_data2/', data)
def backoffice_set_data_batch(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/set_data_batch/', data)
def backoffice_get_BBDD(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/get_BBDD/', data)
def backoffice_set_BBDD(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/set_BBDD/', data)
# ------------------------------------------------------------------ #
# api_comunicaciones #
# ------------------------------------------------------------------ #
def comunicaciones_get_parameterized(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/get_parameterized/', data)
def comunicaciones_get_dataComplex(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/get_dataComplex/', data)
def comunicaciones_set_parameterized(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/set_parameterized/', data)
def comunicaciones_set_data2(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/set_data2/', data)
def comunicaciones_set_data_batch(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/set_data_batch/', data)
def comunicaciones_get_BBDD(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/get_BBDD/', data)
def comunicaciones_set_BBDD(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/set_BBDD/', data)
def comunicaciones_enviar_email(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/email/enviar/', data)
def comunicaciones_enviar_sms(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/sms/enviar/', data)
# ------------------------------------------------------------------ #
# api_documentacion #
# ------------------------------------------------------------------ #
def documentacion_get_parameterized(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/get_parameterized/', data)
def documentacion_get_dataComplex(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/get_dataComplex/', data)
def documentacion_set_parameterized(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/set_parameterized/', data)
def documentacion_set_data2(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/set_data2/', data)
def documentacion_set_data_batch(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/set_data_batch/', data)
def documentacion_get_BBDD(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/get_BBDD/', data)
def documentacion_set_BBDD(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/set_BBDD/', data)
def documentacion_generar_pdf(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/generation/pdf/', data)
def documentacion_guardar(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/storage/guardar/', data)

View File

@@ -0,0 +1,26 @@
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='Promocion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=255)),
('fecha_inicio', models.DateField(blank=True, null=True)),
('fecha_modificacion', models.DateField(blank=True, null=True)),
('descripcion', models.TextField(blank=True, null=True)),
('activo', models.BooleanField(default=True)),
('categoria_id', models.IntegerField(blank=True, null=True)),
],
options={
'db_table': 'promociones',
},
),
]

View File

@@ -5,3 +5,4 @@ python-dotenv==1.0.1
djangorestframework
django-cors-headers
djangorestframework-simplejwt
requests==2.32.3

15
deployments/Makefile Normal file
View File

@@ -0,0 +1,15 @@
NETWORK = saas_network
.PHONY: network up down logs
network:
docker network inspect $(NETWORK) >/dev/null 2>&1 || docker network create $(NETWORK)
up: network
docker compose up --build -d
down:
docker compose down
logs:
docker compose logs -f

View File

@@ -1,45 +1,48 @@
version: '3.8'
services:
gitea-db:
image: postgres:15
# Usará el nombre de tu .env (django_db_local)
container_name: ${DB_CONTAINER_NAME:-django_db_dev}
restart: always
db:
image: postgres:15-alpine
container_name: ${DB_CONTAINER_NAME:-django_core_db}
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-gitea}
POSTGRES_USER: ${DB_USER:-gitea}
POSTGRES_PASSWORD: ${DB_PASSWORD:-gitea}
POSTGRES_DB: ${DB_NAME:-django_core_db}
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
volumes:
- postgres_data:/var/lib/postgresql/data
# --- ESTO ES LO QUE FALTA ---
ports:
- "${DATABASE_EXPOSE_PORT:-5432}:5432"
# ----------------------------
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-gitea} -d ${DB_NAME:-gitea}"]
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-django_core_db}"]
interval: 5s
timeout: 5s
retries: 5
retries: 10
start_period: 10s
web:
build:
context: ..
dockerfile: deployments/Dockerfile
container_name: ${APP_CONTAINER_NAME:-django_app_dev}
restart: always
container_name: ${APP_CONTAINER_NAME:-django_core_app}
restart: unless-stopped
env_file:
- .env
environment:
- DEBUG=${DEBUG_MODE:-1}
# IMPORTANTE: Este nombre debe coincidir con el nombre del servicio arriba (gitea-db)
- DB_HOST=gitea-db
- DB_HOST=db
- DB_PORT=5432
ports:
- "${PORT:-8000}:8000"
depends_on:
gitea-db:
db:
condition: service_healthy
networks:
- default
- saas_network
volumes:
postgres_data:
networks:
saas_network:
name: saas_network

View File

@@ -3,16 +3,39 @@
# Salir inmediatamente si un comando falla
set -e
# --- Esperar a PostgreSQL si estamos en modo BD remota ---
if [ -n "$DB_HOST" ]; then
echo "--> Esperando a PostgreSQL en $DB_HOST:${DB_PORT:-5432}..."
until python -c "
import sys, psycopg2, os
try:
psycopg2.connect(
host=os.environ['DB_HOST'],
port=os.environ.get('DB_PORT', 5432),
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD'],
dbname=os.environ['DB_NAME']
)
sys.exit(0)
except Exception:
sys.exit(1)
" 2>/dev/null; do
echo " PostgreSQL no disponible, reintentando en 2s..."
sleep 2
done
echo "--> PostgreSQL listo."
fi
echo "--> Ejecutando migraciones..."
# Esto asegura que si hay cambios en models.py, se generen y apliquen las tablas
python manage.py makemigrations --noinput
python manage.py migrate --noinput
echo "--> Cargando datos de prueba..."
# Este comando busca archivos JSON en las carpetas 'fixtures' de tus apps
# Usamos || true para que si el archivo no existe o ya están cargados, el contenedor no se detenga
python manage.py loaddata semillas || echo "Aviso: No se pudieron cargar las semillas (fichero no encontrado o error de formato)."
echo "--> Cargando semillas (si existen)..."
python manage.py loaddata semillas 2>/dev/null || echo " Sin semillas, continuando."
echo "--> Arrancando el servidor Django..."
# Usamos exec para que Django sea el proceso principal (PID 1) y reciba señales de Docker
exec python manage.py runserver 0.0.0.0:8000
echo "--> Arrancando servidor con Gunicorn..."
exec gunicorn api_config.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 2 \
--timeout 120 \
--access-logfile - \
--error-logfile -

View File

@@ -5,3 +5,4 @@ python-dotenv==1.0.1
djangorestframework
django-cors-headers
djangorestframework-simplejwt
requests==2.32.3

View File

@@ -0,0 +1,11 @@
{
"id": "x8jh10qysfo",
"name": "DEV",
"values": [
{ "key": "base_url", "value": "https://dev.v-encore-lab.com", "type": "default", "enabled": true },
{ "key": "username", "value": "admin", "type": "default", "enabled": true },
{ "key": "password", "value": "admin", "type": "default", "enabled": true },
{ "key": "access_token", "value": "", "type": "default", "enabled": true }
],
"_postman_variable_scope": "environment"
}

View File

@@ -0,0 +1,11 @@
{
"id": "ftt19d5vxuj",
"name": "LOCAL",
"values": [
{ "key": "base_url", "value": "http://localhost:8000", "type": "default", "enabled": true },
{ "key": "username", "value": "admin", "type": "default", "enabled": true },
{ "key": "password", "value": "admin", "type": "default", "enabled": true },
{ "key": "access_token", "value": "", "type": "default", "enabled": true }
],
"_postman_variable_scope": "environment"
}

View File

@@ -0,0 +1,11 @@
{
"id": "3ckrklvkf27",
"name": "PROD",
"values": [
{ "key": "base_url", "value": "https://v-encore-lab.com", "type": "default", "enabled": true },
{ "key": "username", "value": "admin", "type": "default", "enabled": true },
{ "key": "password", "value": "admin", "type": "default", "enabled": true },
{ "key": "access_token", "value": "", "type": "default", "enabled": true }
],
"_postman_variable_scope": "environment"
}

View File

@@ -6,31 +6,8 @@
"_postman_id": "django-core-base-collection"
},
"variable": [
{
"key": "base_url",
"value": "http://localhost:8000",
"type": "string"
},
{
"key": "access_token",
"value": "",
"type": "string"
},
{
"key": "refresh_token",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "admin",
"type": "string"
},
{
"key": "password",
"value": "admin",
"type": "string"
}
{ "key": "access_token", "value": "", "type": "string" },
{ "key": "refresh_token", "value": "", "type": "string" }
],
"item": [
{
@@ -51,6 +28,29 @@
"}"
]
}
},
{
"listen": "postrequest",
"script": {
"type": "text/javascript",
"exec": [
"// Comprobamos si la respuesta es exitosa",
"if (pw.response.status === 200) {",
" // Si el body ya es un objeto no hace falta JSON.parse",
" const body = typeof pw.response.body === 'string'",
" ? JSON.parse(pw.response.body)",
" : pw.response.body;",
"",
" if (body.access) {",
" // Guardamos el token en el entorno actual",
" pw.env.set(\"access_token\", body.access);",
" console.log(\"✅ Token guardado correctamente\");",
" } else {",
" console.log(\"❌ No se encontró el campo 'access' en la respuesta\");",
" }",
"}"
]
}
}
],
"request": {
@@ -77,10 +77,13 @@
{
"name": "Obtener Promoción",
"request": {
"auth": {
"type": "bearer",
"bearer": [{ "key": "token", "value": "{{access_token}}", "type": "string" }]
},
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Authorization", "value": "Bearer {{access_token}}" }
{ "key": "Content-Type", "value": "application/json" }
],
"body": {
"mode": "raw",
@@ -101,10 +104,13 @@
{
"name": "Ejecutar Automatizaciones",
"request": {
"auth": {
"type": "bearer",
"bearer": [{ "key": "token", "value": "{{access_token}}", "type": "string" }]
},
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Authorization", "value": "Bearer {{access_token}}" }
{ "key": "Content-Type", "value": "application/json" }
],
"body": {
"mode": "raw",
@@ -120,10 +126,13 @@
{
"name": "Historial de Ejecuciones",
"request": {
"auth": {
"type": "bearer",
"bearer": [{ "key": "token", "value": "{{access_token}}", "type": "string" }]
},
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Authorization", "value": "Bearer {{access_token}}" }
{ "key": "Content-Type", "value": "application/json" }
],
"body": {
"mode": "raw",
@@ -139,10 +148,13 @@
{
"name": "Estado del Módulo",
"request": {
"auth": {
"type": "bearer",
"bearer": [{ "key": "token", "value": "{{access_token}}", "type": "string" }]
},
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Authorization", "value": "Bearer {{access_token}}" }
{ "key": "Content-Type", "value": "application/json" }
],
"body": {
"mode": "raw",