Compare commits
108 Commits
0fc5392bd2
...
pre-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a53cd3918 | ||
|
|
35e1d38859 | ||
|
|
a064895689 | ||
|
|
05e95f8880 | ||
|
|
af38d88876 | ||
|
|
02fa6247f1 | ||
|
|
428b745700 | ||
|
|
f6892b2166 | ||
|
|
d2c91d5196 | ||
|
|
d735d73322 | ||
|
|
197cff011f | ||
|
|
590e18e994 | ||
|
|
c692ce2a61 | ||
|
|
152e9c14ef | ||
|
|
99de5f06b5 | ||
|
|
d7a84a4dfa | ||
|
|
fbc5f0f6c4 | ||
|
|
cac00a4f8c | ||
|
|
a17f00bad2 | ||
|
|
b8c3e03348 | ||
|
|
252e176e9d | ||
|
|
41fc2a0aa0 | ||
|
|
001bf13d26 | ||
|
|
47f23a73e1 | ||
|
|
04c37f669c | ||
|
|
92a00ec75f | ||
|
|
30c540b6f9 | ||
|
|
778753afd6 | ||
| b91f5d09e5 | |||
| aee34d797d | |||
| 1e1348bc1a | |||
| f68029edc1 | |||
|
|
384f47df5e | ||
|
|
2684e251f7 | ||
|
|
94faedecae | ||
| 3f95e92318 | |||
| aed8661331 | |||
|
|
03663aacb4 | ||
|
|
0c18ffc2f9 | ||
| 77b722d739 | |||
| 3c7d47782b | |||
|
|
156b5ad77d | ||
|
|
5d2a6469aa | ||
| e2ae400889 | |||
| e597a05f08 | |||
|
|
4425141cb3 | ||
|
|
29db0eb0a2 | ||
| 0a73b91e12 | |||
| d444021a00 | |||
| 508f3f028d | |||
| 7a151a4768 | |||
| bc82249a29 | |||
| 56e7d77d63 | |||
| f3514d399e | |||
| 1e72aa3e44 | |||
| b08e74f459 | |||
| 0ac0a859b9 | |||
| 02aca6a2a6 | |||
| 7a9a1686fa | |||
| b4ab653c95 | |||
| 049764b79c | |||
| c60c140a97 | |||
| f610eb24d4 | |||
| 816cc276f8 | |||
| 5a7209badb | |||
| d39e078fb6 | |||
| ac1c024bee | |||
| 147a1d49cc | |||
| 8579af3f21 | |||
| 32e3184b59 | |||
| 5e04933708 | |||
| 083375c5f0 | |||
| d9bba25437 | |||
| 6f84db00cd | |||
| b37b581a78 | |||
| c54e1e9b9e | |||
| 15f2a21a2c | |||
| 264c66c7c0 | |||
| 16cbd85e7f | |||
| 14e2036bd0 | |||
| b60ecccad4 | |||
| 1752b88bc3 | |||
| 9b447a21ce | |||
| 0af35ee5db | |||
| f77f05e105 | |||
| c4e8675fe8 | |||
| 3fecd8c7fb | |||
| 7b0fcf26ff | |||
| f5f53d03c9 | |||
| e304a5c35f | |||
| 93164e4f2c | |||
| ce7a1414a2 | |||
| 04fb83447f | |||
| 52f11e7174 | |||
| bdda074fa1 | |||
| 6f2c383db4 | |||
| 8b3a1b032f | |||
| 64afc3aedb | |||
| 289bd6f510 | |||
| 156e1aa27c | |||
| 2d528b1e4d | |||
| 16b7f956b3 | |||
| 29af52add4 | |||
| 34faf2157e | |||
| 069a29bc27 | |||
| 2a6723aedf | |||
| 03edd2c544 | |||
| 15050ae93d |
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -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
110
CLAUDE.md
Normal 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
341
README.md
@@ -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`
|
||||
|
||||
@@ -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
60
app/common/http_client.py
Normal 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
100
app/general/request_api.py
Normal 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)
|
||||
26
app/promociones/migrations/0001_initial.py
Normal file
26
app/promociones/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -5,3 +5,4 @@ python-dotenv==1.0.1
|
||||
djangorestframework
|
||||
django-cors-headers
|
||||
djangorestframework-simplejwt
|
||||
requests==2.32.3
|
||||
15
deployments/Makefile
Normal file
15
deployments/Makefile
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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 -
|
||||
|
||||
@@ -5,3 +5,4 @@ python-dotenv==1.0.1
|
||||
djangorestframework
|
||||
django-cors-headers
|
||||
djangorestframework-simplejwt
|
||||
requests==2.32.3
|
||||
11
environments/DEV.postman_environment.json
Normal file
11
environments/DEV.postman_environment.json
Normal 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"
|
||||
}
|
||||
11
environments/LOCAL.postman_environment.json
Normal file
11
environments/LOCAL.postman_environment.json
Normal 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"
|
||||
}
|
||||
11
environments/PROD.postman_environment.json
Normal file
11
environments/PROD.postman_environment.json
Normal 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"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user