Compare commits

6 Commits

Author SHA1 Message Date
juanjo
abe67a7f20 Merge pre-dev: fix env_file docker-compose
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-17 01:06:34 +02:00
juanjo
5367ba7313 Merge pre-dev: fix env_file path docker-compose
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-17 01:05:40 +02:00
juanjo
23cd4bf8b4 Merge pre-dev: gunicorn + wait-for-db + healthcheck
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
2026-04-17 00:39:30 +02:00
juanjo
b43ca9d919 Merge branch 'dev' of https://git.v-encore-lab.com/Proyecto-SaaS/django-core-base into dev 2026-04-17 00:39:30 +02:00
juanjo
adaf20bd8e Merge pre-dev: revert api_config 2026-04-16 23:31:29 +02:00
juanjo
95e036108d Merge pre-dev: renombrar api_config → api_hub_dispatcher 2026-04-16 23:28:44 +02:00
18 changed files with 42 additions and 324 deletions

3
.gitattributes vendored
View File

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

View File

@@ -80,17 +80,10 @@ class MiVista(APIView):
pre-dev → dev → master pre-dev → dev → master
``` ```
- `pre-dev`: desarrollo activo — aquí entran todos los cambios - `pre-dev`: desarrollo activo
- `dev`: validación previa a producción - `dev`: validación previa a producción
- `master`: producción estable - `master`: producción estable
- Merges siempre con `--no-ff` - 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 ## Estructura de app Django

View File

@@ -140,11 +140,9 @@ python manage.py migrate <app_name> zero
--- ---
## Docker — Local y Producción ## Docker — Producción
```bash ```bash
cd deployments
# Construir e iniciar # Construir e iniciar
docker-compose up --build -d docker-compose up --build -d
@@ -160,16 +158,6 @@ docker-compose down -v
El contenedor expone el puerto **8000**. 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). 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).
--- ---

View File

@@ -1,4 +1,4 @@
import os import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_hub_dispatcher.settings')
application = get_asgi_application() application = get_asgi_application()

View File

@@ -74,7 +74,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
ROOT_URLCONF = 'api_config.urls' ROOT_URLCONF = 'api_hub_dispatcher.urls'
TEMPLATES = [ TEMPLATES = [
{ {
@@ -92,7 +92,7 @@ TEMPLATES = [
}, },
] ]
WSGI_APPLICATION = 'api_config.wsgi.application' WSGI_APPLICATION = 'api_hub_dispatcher.wsgi.application'
# 3. DATABASE # 3. DATABASE
# En producción (cuando DB_HOST está definido) usa PostgreSQL. # En producción (cuando DB_HOST está definido) usa PostgreSQL.
@@ -175,11 +175,6 @@ 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 --- # --- CONFIGURACIONES PERSONALIZADAS DE LA APP ---
# Leemos la variable del .env (cargado previamente con load_dotenv) # Leemos la variable del .env (cargado previamente con load_dotenv)

View File

@@ -2,6 +2,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
# Este es el enlace con tus configuraciones # Este es el enlace con tus configuraciones
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_hub_dispatcher.settings')
application = get_wsgi_application() application = get_wsgi_application()

View File

@@ -18,7 +18,7 @@ class Admin:
# 1. Autenticación # 1. Autenticación
user = authenticate(username=username, password=password) user = authenticate(username=username, password=password)
if user is not None: if user is not None:
# 2. Generación de JWT (Access & Refresh) # 2. Generación de JWT (Access & Refresh)
refresh = RefreshToken.for_user(user) refresh = RefreshToken.for_user(user)

View File

@@ -1,60 +0,0 @@
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)

View File

@@ -1,100 +0,0 @@
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

@@ -1,26 +0,0 @@
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

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

View File

@@ -1,15 +0,0 @@
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

@@ -36,13 +36,6 @@ services:
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
networks:
- default
- saas_network
volumes: volumes:
postgres_data: postgres_data:
networks:
saas_network:
name: saas_network

View File

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

View File

@@ -1,11 +0,0 @@
{
"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

@@ -1,11 +0,0 @@
{
"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

@@ -1,11 +0,0 @@
{
"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,8 +6,31 @@
"_postman_id": "django-core-base-collection" "_postman_id": "django-core-base-collection"
}, },
"variable": [ "variable": [
{ "key": "access_token", "value": "", "type": "string" }, {
{ "key": "refresh_token", "value": "", "type": "string" } "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"
}
], ],
"item": [ "item": [
{ {
@@ -28,29 +51,6 @@
"}" "}"
] ]
} }
},
{
"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": { "request": {
@@ -77,13 +77,10 @@
{ {
"name": "Obtener Promoción", "name": "Obtener Promoción",
"request": { "request": {
"auth": {
"type": "bearer",
"bearer": [{ "key": "token", "value": "{{access_token}}", "type": "string" }]
},
"method": "POST", "method": "POST",
"header": [ "header": [
{ "key": "Content-Type", "value": "application/json" } { "key": "Content-Type", "value": "application/json" },
{ "key": "Authorization", "value": "Bearer {{access_token}}" }
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
@@ -104,13 +101,10 @@
{ {
"name": "Ejecutar Automatizaciones", "name": "Ejecutar Automatizaciones",
"request": { "request": {
"auth": {
"type": "bearer",
"bearer": [{ "key": "token", "value": "{{access_token}}", "type": "string" }]
},
"method": "POST", "method": "POST",
"header": [ "header": [
{ "key": "Content-Type", "value": "application/json" } { "key": "Content-Type", "value": "application/json" },
{ "key": "Authorization", "value": "Bearer {{access_token}}" }
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
@@ -126,13 +120,10 @@
{ {
"name": "Historial de Ejecuciones", "name": "Historial de Ejecuciones",
"request": { "request": {
"auth": {
"type": "bearer",
"bearer": [{ "key": "token", "value": "{{access_token}}", "type": "string" }]
},
"method": "POST", "method": "POST",
"header": [ "header": [
{ "key": "Content-Type", "value": "application/json" } { "key": "Content-Type", "value": "application/json" },
{ "key": "Authorization", "value": "Bearer {{access_token}}" }
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
@@ -148,13 +139,10 @@
{ {
"name": "Estado del Módulo", "name": "Estado del Módulo",
"request": { "request": {
"auth": {
"type": "bearer",
"bearer": [{ "key": "token", "value": "{{access_token}}", "type": "string" }]
},
"method": "POST", "method": "POST",
"header": [ "header": [
{ "key": "Content-Type", "value": "application/json" } { "key": "Content-Type", "value": "application/json" },
{ "key": "Authorization", "value": "Bearer {{access_token}}" }
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",