Merge pull request 'dev' (#21) from dev into master
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
10
.env
10
.env
@@ -1,10 +0,0 @@
|
|||||||
# Seguridad
|
|
||||||
DEBUG=True
|
|
||||||
SECRET_KEY=una-clave-muy-secreta-y-larga-123456
|
|
||||||
|
|
||||||
# Base de Datos (Conectando al PostgreSQL que instalamos)
|
|
||||||
DB_NAME=gitea
|
|
||||||
DB_USER=gitea
|
|
||||||
DB_PASSWORD=gitea
|
|
||||||
DB_HOST=gitea-db
|
|
||||||
DB_PORT=5432
|
|
||||||
10
.env.example
10
.env.example
@@ -1,10 +0,0 @@
|
|||||||
# Seguridad
|
|
||||||
DEBUG=True
|
|
||||||
SECRET_KEY=una-clave-muy-secreta-y-larga-123456
|
|
||||||
|
|
||||||
# Base de Datos (Conectando al PostgreSQL que instalamos)
|
|
||||||
DB_NAME=gitea
|
|
||||||
DB_USER=gitea
|
|
||||||
DB_PASSWORD=gitea
|
|
||||||
DB_HOST=gitea-db
|
|
||||||
DB_PORT=5432
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -10,3 +10,8 @@ deployments/docker-compose.yml
|
|||||||
postgres_data/
|
postgres_data/
|
||||||
local_postgres_data/
|
local_postgres_data/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
apps/backend_admin/migrations/0001_initial.py
|
||||||
|
# Bloquear todos los .env en cualquier carpeta
|
||||||
|
.env
|
||||||
|
**/core/.env
|
||||||
|
**/deployments/.env
|
||||||
67
README.md
67
README.md
@@ -2,5 +2,72 @@
|
|||||||
|
|
||||||
// V-Encore Lab: Sistema Automatizado v1.0.4
|
// V-Encore Lab: Sistema Automatizado v1.0.4
|
||||||
|
|
||||||
|
## 🚀 Inicio Rápido (Desarrollo Local)
|
||||||
|
|
||||||
|
### 1. Clonar y Configurar
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
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
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
# Opcional: Crear superusuario
|
||||||
|
python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Correr el Servidor
|
||||||
|
```bash
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
Abrir http://localhost:8000
|
||||||
|
|
||||||
|
## 🐳 Docker (Producción/Desarrollo)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Acceder a:
|
||||||
|
- App: http://localhost:8000
|
||||||
|
- Admin: http://localhost:8000/admin/
|
||||||
|
- DB: localhost:5432 (Postgres)
|
||||||
|
|
||||||
|
## 📋 Comandos Django Comunes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verificar configuración
|
||||||
|
python manage.py check
|
||||||
|
|
||||||
|
# Recopilar static files
|
||||||
|
python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
# Test
|
||||||
|
python manage.py test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Estructura del Proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
├── apps/ # Aplicaciones Django
|
||||||
|
│ ├── backend_admin/
|
||||||
|
│ ├── common/
|
||||||
|
│ └── promociones/
|
||||||
|
├── core/ # Configuración principal
|
||||||
|
├── deployments/ # Docker, requirements prod
|
||||||
|
└── manage.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## .env Variables
|
||||||
|
Ver `.env.example` para configuración.
|
||||||
@@ -1,13 +1,34 @@
|
|||||||
|
from django.contrib.auth import authenticate
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
|
||||||
class Admin:
|
class Admin:
|
||||||
def get_status_action(self):
|
def get_status_action(self):
|
||||||
|
# Tu lógica de status que ya tenías
|
||||||
|
return {"status": "ok", "service": "Admin Infrastructure"}
|
||||||
|
|
||||||
|
|
||||||
|
def obtener_token_action(self, params):
|
||||||
"""
|
"""
|
||||||
Lógica para comprobar la salud del sistema.
|
Capa Action: Valida credenciales y genera un par de tokens JWT.
|
||||||
Devuelve el estado básico del entorno.
|
|
||||||
"""
|
"""
|
||||||
# En el futuro, podrías usar get_parameterized aquí si quisieras
|
username = params.get('username')
|
||||||
# consultar estados en la base de datos.
|
password = params.get('password')
|
||||||
|
|
||||||
|
# 1. Autenticación
|
||||||
|
user = authenticate(username=username, password=password)
|
||||||
|
|
||||||
|
if user is not None:
|
||||||
|
# 2. Generación de JWT (Access & Refresh)
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "ok",
|
'refresh': str(refresh),
|
||||||
"message": "V-Encore API System is active",
|
'access': str(refresh.access_token),
|
||||||
"environment": "dev" # Esto podría venir de una variable de entorno
|
'user': user.username,
|
||||||
|
'status': 'success'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
1
apps/backend_admin/migrations/__init__.py
Normal file
1
apps/backend_admin/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Archivo para marcar esta carpeta como paquete de migraciones
|
||||||
25
apps/backend_admin/models.py
Normal file
25
apps/backend_admin/models.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
class Log(models.Model):
|
||||||
|
# Usamos BigAutoField para el BIGINT id de tu tabla
|
||||||
|
id = models.BigAutoField(primary_key=True)
|
||||||
|
user_id = models.IntegerField(default=0)
|
||||||
|
user = models.CharField(max_length=255, default='anonimo')
|
||||||
|
app_id = models.IntegerField(default=0)
|
||||||
|
# GenericIPAddressField para el tipo INET de Postgres
|
||||||
|
remote_address = models.GenericIPAddressField(null=True, blank=True)
|
||||||
|
request = models.JSONField(null=True, blank=True) # Para JSONB
|
||||||
|
response = models.JSONField(null=True, blank=True) # Para JSONB
|
||||||
|
status_code = models.CharField(max_length=10, default='0')
|
||||||
|
path = models.CharField(max_length=255)
|
||||||
|
method = models.CharField(max_length=10)
|
||||||
|
createdAt = models.DateTimeField(default=timezone.now)
|
||||||
|
updatedAt = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'audit_logs'
|
||||||
|
managed = False # Al estar la tabla ya creada, Django no intentará modificarla
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.method} {self.path} ({self.status_code})"
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from .actions import Admin
|
from .actions import Admin
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
from .models import Log
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -26,3 +32,64 @@ def status_view(request):
|
|||||||
# BLOQUE 4: Log de cierre y retorno
|
# BLOQUE 4: Log de cierre y retorno
|
||||||
logger.info(f"FIN - Health Check completado. Status: {status_code}")
|
logger.info(f"FIN - Health Check completado. Status: {status_code}")
|
||||||
return JsonResponse(response_data, status=status_code)
|
return JsonResponse(response_data, status=status_code)
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@staticmethod
|
||||||
|
def api_token(request):
|
||||||
|
"""
|
||||||
|
Endpoint: api/token/
|
||||||
|
Patrón: 4 bloques con persistencia en Log DB.
|
||||||
|
"""
|
||||||
|
# --- BLOQUE 1: LOG INITIATION ---
|
||||||
|
logger.info("INICIO - Petición de JWT (api/token/)")
|
||||||
|
|
||||||
|
# Iniciamos el registro en la base de datos (Estándar compañeros)
|
||||||
|
log_entry = Log.objects.create(
|
||||||
|
user='anonimo',
|
||||||
|
path='api/token/',
|
||||||
|
method='POST',
|
||||||
|
createdAt=timezone.now(),
|
||||||
|
status_code='0'
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if request.method == 'POST':
|
||||||
|
# --- BLOQUE 2: DATA CLEANING ---
|
||||||
|
body_data = json.loads(request.body)
|
||||||
|
log_entry.request = body_data # Guardamos lo que entró
|
||||||
|
log_entry.save()
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'username': body_data.get('username'),
|
||||||
|
'password': body_data.get('password')
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- BLOQUE 3: ACTION CALL ---
|
||||||
|
admin_logic = Admin()
|
||||||
|
resultado = admin_logic.obtener_token_action(params)
|
||||||
|
|
||||||
|
# --- BLOQUE 4: LOG CLOSURE & RESPONSE ---
|
||||||
|
if resultado:
|
||||||
|
status = 200
|
||||||
|
log_entry.user = resultado['user']
|
||||||
|
log_entry.response = resultado
|
||||||
|
log_entry.status_code = str(status)
|
||||||
|
log_entry.updatedAt = timezone.now()
|
||||||
|
log_entry.save()
|
||||||
|
|
||||||
|
logger.info(f"FIN - JWT generado para: {log_entry.user}")
|
||||||
|
return JsonResponse(resultado, status=status)
|
||||||
|
else:
|
||||||
|
status = 401
|
||||||
|
response_error = {"error": "Credenciales inválidas"}
|
||||||
|
log_entry.status_code = str(status)
|
||||||
|
log_entry.response = response_error
|
||||||
|
log_entry.save()
|
||||||
|
return JsonResponse(response_error, status=status)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"ERROR CRÍTICO en api_token: {str(e)}")
|
||||||
|
log_entry.status_code = '500'
|
||||||
|
log_entry.response = {'error': str(e)}
|
||||||
|
log_entry.save()
|
||||||
|
return JsonResponse({'error': 'Error interno'}, status=500)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
# apps/common/apps.py
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
class CommonConfig(AppConfig):
|
class CommonConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'apps.common'
|
name = 'apps.common' # <--- ESTO ES LO QUE DEBE PONER
|
||||||
1
apps/common/migrations/__init__.py
Normal file
1
apps/common/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Archivo para marcar esta carpeta como paquete de migraciones
|
||||||
35
apps/promociones/fixtures/semillas.json
Normal file
35
apps/promociones/fixtures/semillas.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "promociones.promocion",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"nombre": "Oferta de Bienvenida",
|
||||||
|
"fecha_inicio": "2026-04-01",
|
||||||
|
"descripcion": "Descuento para nuevos usuarios",
|
||||||
|
"activo": true,
|
||||||
|
"categoria_id": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "promociones.promocion",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"nombre": "Promo Primavera",
|
||||||
|
"fecha_inicio": "2026-05-01",
|
||||||
|
"descripcion": "Todo al 20% de descuento",
|
||||||
|
"activo": true,
|
||||||
|
"categoria_id": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "promociones.promocion",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"nombre": "Liquidación Stock",
|
||||||
|
"fecha_inicio": "2026-04-14",
|
||||||
|
"descripcion": "Últimas unidades",
|
||||||
|
"activo": false,
|
||||||
|
"categoria_id": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
1
apps/promociones/migrations/__init__.py
Normal file
1
apps/promociones/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Archivo para marcar esta carpeta como paquete de migraciones
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .views import get_promocion_view, status_view
|
from .views import PromocionObtener
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Capa 1: Definición del endpoint
|
path('obtener/', PromocionObtener.as_view(), name='obtener_promocion'),
|
||||||
path('obtener/', get_promocion_view, name='get_promocion'),
|
|
||||||
|
|
||||||
]
|
]
|
||||||
@@ -1,73 +1,46 @@
|
|||||||
import logging
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
from apps.backend_admin.models import Log
|
||||||
from .actions import getData
|
from .actions import getData
|
||||||
|
from django.utils import timezone # Esta es la forma correcta
|
||||||
|
class PromocionObtener(APIView):
|
||||||
|
authentication_classes = [JWTAuthentication]
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
from django.http import JsonResponse
|
def post(self, request):
|
||||||
from .actions import get_status_action
|
# --- BLOQUE 1: LOG INITIATION ---
|
||||||
|
log_entry = Log.objects.create(
|
||||||
|
user=request.user.username,
|
||||||
|
path='promociones/obtener/',
|
||||||
|
method='POST',
|
||||||
|
status_code='0'
|
||||||
|
)
|
||||||
|
|
||||||
|
print('llega a despues de log entry')
|
||||||
# Configuración del logger para rastrear la ejecución
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
def get_promocion_view(request):
|
|
||||||
"""
|
|
||||||
Vista estandarizada para la obtención de una promoción.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# BLOQUE 1: Log de iniciación
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
logger.info("[START] Iniciando ejecución de get_promocion_view")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# ---------------------------------------------------------
|
# --- BLOQUE 2: DATA CLEANING ---
|
||||||
# BLOQUE 2: Limpieza de datos (Data Cleaning)
|
data = request.data
|
||||||
# ---------------------------------------------------------
|
log_entry.request = data
|
||||||
# Extraemos los parámetros del request y preparamos el diccionario
|
log_entry.save()
|
||||||
raw_data = request.GET.dict()
|
|
||||||
|
|
||||||
# Aquí es donde ella aplicaría validaciones adicionales si fuera necesario
|
params = {'id': data.get('id'), 'activo': data.get('activo')}
|
||||||
clean_params = {
|
|
||||||
'id': raw_data.get('id'),
|
|
||||||
'activo': raw_data.get('activo', True) # Valor por defecto
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# --- BLOQUE 3: ACTION CALL ---
|
||||||
# BLOQUE 3: Llamada a la Action (Execution)
|
# Aquí ya recibimos las fechas como strings gracias al paso 1
|
||||||
# ---------------------------------------------------------
|
resultado = getData(params)
|
||||||
# La lógica de SQL y parametrización vive dentro de esta llamada
|
|
||||||
resultado_db = getData(clean_params)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# --- BLOQUE 4: LOG CLOSURE & RESPONSE ---
|
||||||
# BLOQUE 4: Log de cierre y respuesta (Closure)
|
log_entry.response = {"count": len(resultado)} # No guardes todo el JSON si es muy grande
|
||||||
# ---------------------------------------------------------
|
log_entry.status_code = '200'
|
||||||
logger.info(f"[SUCCESS] get_promocion_view finalizada. Registros encontrados: {len(resultado_db)}")
|
log_entry.save()
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse(resultado, safe=False, status=200)
|
||||||
'status': 'success',
|
|
||||||
'data': resultado_db
|
|
||||||
}, status=200)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Log de error detallado en caso de fallo
|
log_entry.status_code = '500'
|
||||||
logger.error(f"[ERROR] Fallo crítico en get_promocion_view: {str(e)}")
|
log_entry.response = {'error': str(e)}
|
||||||
|
log_entry.save()
|
||||||
return JsonResponse({
|
return JsonResponse({'error': str(e)}, status=500)
|
||||||
'status': 'error',
|
|
||||||
'message': 'Error interno del servidor'
|
|
||||||
}, status=500)
|
|
||||||
|
|
||||||
# En views.py
|
|
||||||
def status_view(request):
|
|
||||||
# BLOQUE 1: Log de iniciación
|
|
||||||
logger.info("Iniciando petición de status...")
|
|
||||||
|
|
||||||
# BLOQUE 2: Limpieza y validación de datos (En este caso no hay datos de entrada)
|
|
||||||
data_cleaned = {}
|
|
||||||
|
|
||||||
# BLOQUE 3: Llamada a la acción
|
|
||||||
response_data = get_status_action()
|
|
||||||
|
|
||||||
# BLOQUE 4: Log de cierre y retorno
|
|
||||||
logger.info("Status enviado correctamente.")
|
|
||||||
return JsonResponse(response_data, status=200)
|
|
||||||
3
core/.env.example
Normal file
3
core/.env.example
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# core/.env
|
||||||
|
APP_CUSTOM_SETTING="Este es un valor privado de la app"
|
||||||
|
EXTERNAL_SERVICE_API_KEY="sk_test_12345"
|
||||||
121
core/settings.py
121
core/settings.py
@@ -1,26 +1,47 @@
|
|||||||
from dotenv import load_dotenv
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
# Cargar variables desde el archivo .env
|
SIMPLE_JWT = {
|
||||||
load_dotenv()
|
# Cambiamos el tiempo de acceso a 3 horas
|
||||||
|
'ACCESS_TOKEN_LIFETIME': timedelta(hours=3),
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# El tiempo del refresh token suele ser mayor (por ejemplo, 1 día)
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
|
||||||
|
|
||||||
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-change-me-for-production')
|
# Otras configuraciones que ya tengas...
|
||||||
DEBUG = os.getenv('DEBUG', 'True').lower() == 'true'
|
'ALGORITHM': 'HS256',
|
||||||
|
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. RUTA DEL SETTINGS Y CARGA DEL .ENV
|
||||||
|
# Obtenemos la ruta de la carpeta donde está este archivo (core/)
|
||||||
|
CURRENT_DIR = Path(__file__).resolve().parent
|
||||||
|
BASE_DIR = CURRENT_DIR.parent
|
||||||
|
|
||||||
|
# Cargamos el .env específico de esta carpeta (core/.env)
|
||||||
|
load_dotenv(dotenv_path=CURRENT_DIR / '.env')
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-default-key-change-it')
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
# Mejoramos el parseo de DEBUG para que no falle si viene como string
|
||||||
|
DEBUG = os.getenv('DEBUG', 'True').lower() in ('true', '1', 't')
|
||||||
|
|
||||||
|
# 2. ALLOWED HOSTS
|
||||||
|
# Limpiamos y centralizamos los hosts permitidos
|
||||||
ALLOWED_HOSTS = [
|
ALLOWED_HOSTS = [
|
||||||
'v-encore-lab.com',
|
'v-encore-lab.com',
|
||||||
'dev.v-encore-lab.com',
|
'dev.v-encore-lab.com',
|
||||||
'185.187.169.109', # Añade la IP aquí
|
'185.187.169.109',
|
||||||
'localhost',
|
'localhost',
|
||||||
'127.0.0.1',
|
'127.0.0.1',
|
||||||
'django_app_dev',
|
os.getenv('APP_CONTAINER_NAME', 'django_app_dev'), # Dinámico para Docker
|
||||||
'django_app_master'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Application definition
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
@@ -28,11 +49,20 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
|
||||||
|
# Plugins
|
||||||
|
'rest_framework',
|
||||||
|
'rest_framework.authtoken',
|
||||||
|
'corsheaders',
|
||||||
|
|
||||||
|
# Tus Apps (Asegúrate de que el path sea correcto)
|
||||||
'apps.promociones',
|
'apps.promociones',
|
||||||
|
'apps.backend_admin',
|
||||||
'apps.common',
|
'apps.common',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
@@ -62,26 +92,18 @@ TEMPLATES = [
|
|||||||
|
|
||||||
WSGI_APPLICATION = 'core.wsgi.application'
|
WSGI_APPLICATION = 'core.wsgi.application'
|
||||||
|
|
||||||
# Para desarrollo local: SQLite
|
# 3. DATABASE
|
||||||
if DEBUG:
|
# Extraemos con fallback por si el .env falla
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
# Producción: PostgreSQL desde .env
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.postgresql',
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
'NAME': os.getenv('DB_NAME'),
|
'NAME': os.getenv('DB_NAME', 'postgres'),
|
||||||
'USER': os.getenv('DB_USER'),
|
'USER': os.getenv('DB_USER', 'postgres'),
|
||||||
'PASSWORD': os.getenv('DB_PASSWORD'),
|
'PASSWORD': os.getenv('DB_PASSWORD', ''),
|
||||||
'HOST': os.getenv('DB_HOST'),
|
'HOST': os.getenv('DB_HOST', 'localhost'),
|
||||||
'PORT': os.getenv('DB_PORT'),
|
'PORT': os.getenv('DB_PORT', '5432'),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
LANGUAGE_CODE = 'es-es'
|
LANGUAGE_CODE = 'es-es'
|
||||||
@@ -95,19 +117,58 @@ STATIC_ROOT = BASE_DIR / 'staticfiles'
|
|||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
# Logging básico
|
# REST Framework
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
"ACCESS_TOKEN_LIFETIME": timedelta(hours=1),
|
||||||
|
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
|
||||||
|
}
|
||||||
|
|
||||||
|
# CORS & CSRF
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = DEBUG # Solo permitir todo en modo DEBUG
|
||||||
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
|
'https://v-encore-lab.com',
|
||||||
|
'http://localhost:8000',
|
||||||
|
'http://127.0.0.1:8000',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Logging simplificado
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
'disable_existing_loggers': False,
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'verbose': {
|
||||||
|
'format': '{levelname} {asctime} {module} {message}',
|
||||||
|
'style': '{',
|
||||||
|
},
|
||||||
|
},
|
||||||
'handlers': {
|
'handlers': {
|
||||||
'console': {
|
'console': {
|
||||||
'class': 'logging.StreamHandler',
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'verbose',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'loggers': {
|
'loggers': {
|
||||||
'': {
|
'': {
|
||||||
'handlers': ['console'],
|
'handlers': ['console'],
|
||||||
'level': 'INFO',
|
'level': os.getenv('LOG_LEVEL', 'INFO'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- CONFIGURACIONES PERSONALIZADAS DE LA APP ---
|
||||||
|
|
||||||
|
# Leemos la variable del .env (cargado previamente con load_dotenv)
|
||||||
|
# Ponemos un valor por defecto por si se nos olvida ponerlo en el .env
|
||||||
|
APP_CUSTOM_SETTING = os.getenv('APP_CUSTOM_SETTING', 'valor_por_defecto_seguro')
|
||||||
|
|
||||||
|
# Ejemplo de otra variable de API
|
||||||
|
EXTERNAL_SERVICE_API_KEY = os.getenv('EXTERNAL_SERVICE_API_KEY', None)
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
from django.urls import path, include
|
||||||
|
from apps.backend_admin import views as admin_views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Redirigimos todas las peticiones de /api/promociones/ a nuestra app
|
path('admin/', include('apps.backend_admin.urls')),
|
||||||
path('api/promociones/', include('apps.promociones.urls')),
|
# AÑADE ESTA LÍNEA AQUÍ PARA QUE SEA UNA RUTA PRINCIPAL:
|
||||||
path('admin/', include('apps.backend_admin.urls')), # El tuyo
|
path('promociones/', include('apps.promociones.urls')),
|
||||||
|
path('api/token/', admin_views.api_token, name='token_obtain_pair'),
|
||||||
]
|
]
|
||||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
34
deployments/.env.example
Normal file
34
deployments/.env.example
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# =================================================================
|
||||||
|
# 🏗️ INFRAESTRUCTURA (Docker & Jenkins)
|
||||||
|
# =================================================================
|
||||||
|
# Nombres que tomarán los contenedores en Docker
|
||||||
|
APP_CONTAINER_NAME=
|
||||||
|
DB_CONTAINER_NAME=
|
||||||
|
|
||||||
|
# Nombre del proyecto para Docker Compose (usado en Jenkins)
|
||||||
|
PROJECT_NAME=
|
||||||
|
|
||||||
|
# Puertos que se abrirán al exterior (Host)
|
||||||
|
PORT=
|
||||||
|
DATABASE_EXPOSE_PORT=
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# 🐍 CONFIGURACIÓN DE DJANGO
|
||||||
|
# =================================================================
|
||||||
|
# True para desarrollo, False para producción
|
||||||
|
DEBUG=
|
||||||
|
# Genera una clave segura: https://djecrety.ir/
|
||||||
|
SECRET_KEY=
|
||||||
|
# Lista separada por comas: localhost,127.0.0.1,tudominio.com
|
||||||
|
ALLOWED_HOSTS=
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# 🗄️ BASE DE DATOS (PostgreSQL)
|
||||||
|
# =================================================================
|
||||||
|
DB_NAME=
|
||||||
|
DB_USER=
|
||||||
|
DB_PASSWORD=
|
||||||
|
|
||||||
|
# El HOST debe ser "db" cuando se usa Docker Compose
|
||||||
|
DB_HOST=
|
||||||
|
DB_PORT=
|
||||||
@@ -1,29 +1,36 @@
|
|||||||
# Usamos una imagen ligera de Python
|
# 1. Usamos una imagen ligera de Python
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
# Evitar que Python genere archivos .pyc y que el buffer se sature
|
# 2. Evitar archivos .pyc y saturación del buffer
|
||||||
ENV PYTHONDONTWRITEBYTECODE 1
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
ENV TZ=Europe/Madrid
|
||||||
|
|
||||||
# Directorio de trabajo
|
# 3. Directorio de trabajo interno del contenedor
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Instalar dependencias del sistema necesarias
|
# 4. Instalar dependencias del sistema
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
gcc \
|
gcc \
|
||||||
gettext \
|
gettext \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Instalar dependencias de Python
|
# 5. COPIAR REQUISITOS
|
||||||
COPY requirements.txt /app/
|
# OJO: Ahora el contexto es la RAÍZ, así que el archivo está en deployments/
|
||||||
|
COPY deployments/requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copiar el resto del código
|
# 6. COPIAR EL CÓDIGO
|
||||||
COPY . /app/
|
# Copiamos todo el contenido de la raíz (.) al directorio de trabajo (/app)
|
||||||
|
# Esto incluirá apps/, core/, manage.py, etc.
|
||||||
|
COPY . .
|
||||||
|
|
||||||
# Exponer el puerto de Django
|
# 7. EXPOSICIÓN Y SCRIPT DE ENTRADA
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Comando por defecto para arrancar (usaremos manage.py en dev y gunicorn en prod)
|
# El entrypoint también está en deployments/
|
||||||
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
COPY deployments/entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
42
deployments/Jenkinsfile
vendored
42
deployments/Jenkinsfile
vendored
@@ -5,37 +5,55 @@ pipeline {
|
|||||||
stage('Configurar Entorno') {
|
stage('Configurar Entorno') {
|
||||||
steps {
|
steps {
|
||||||
script {
|
script {
|
||||||
|
// Seleccionamos ID de credencial y config según la rama
|
||||||
if (env.BRANCH_NAME == 'master') {
|
if (env.BRANCH_NAME == 'master') {
|
||||||
env.PROJECT_NAME = "django_master"
|
env.PROJECT_NAME = "django_master"
|
||||||
env.CONTAINER_NAME = "django_app_master"
|
env.APP_CONTAINER_NAME = "django_app_master"
|
||||||
env.PORT = "8001"
|
env.PORT = "8001"
|
||||||
env.DEBUG_MODE = "0"
|
env.DEBUG_MODE = "0"
|
||||||
|
env.ENV_CREDENTIAL_ID = "2" // Tu ID para master
|
||||||
} else {
|
} else {
|
||||||
env.PROJECT_NAME = "django_dev"
|
env.PROJECT_NAME = "django_dev"
|
||||||
env.CONTAINER_NAME = "django_app_dev"
|
env.APP_CONTAINER_NAME = "django_app_dev"
|
||||||
env.PORT = "8000"
|
env.PORT = "8000"
|
||||||
env.DEBUG_MODE = "1"
|
env.DEBUG_MODE = "1"
|
||||||
|
env.ENV_CREDENTIAL_ID = "1" // Tu ID para dev
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Despliegue') {
|
stage('Fase Final: Containerización') {
|
||||||
when { anyOf { branch 'dev'; branch 'master' } }
|
when { anyOf { branch 'dev'; branch 'master' } }
|
||||||
steps {
|
steps {
|
||||||
echo "DESPLEGANDO: ${env.CONTAINER_NAME} en el puerto ${env.PORT}"
|
// Bloque mágico: extrae el archivo .env de la bóveda de Jenkins
|
||||||
|
withCredentials([file(credentialsId: env.ENV_CREDENTIAL_ID, variable: 'SECRET_ENV')]) {
|
||||||
// CAMBIAMOS docker-compose (guion) por docker compose (espacio)
|
|
||||||
sh """
|
sh """
|
||||||
CONTAINER_NAME=${env.CONTAINER_NAME} \
|
echo "Copiando configuración segura..."
|
||||||
PORT=${env.PORT} \
|
cp \$SECRET_ENV deployments/.env
|
||||||
DEBUG_MODE=${env.DEBUG_MODE} \
|
|
||||||
|
echo "🚀 DESPLEGANDO: ${env.APP_CONTAINER_NAME} en puerto ${env.PORT}"
|
||||||
|
|
||||||
|
export APP_CONTAINER_NAME=${env.APP_CONTAINER_NAME}
|
||||||
|
export PORT=${env.PORT}
|
||||||
|
export DEBUG_MODE=${env.DEBUG_MODE}
|
||||||
|
|
||||||
docker compose -p ${env.PROJECT_NAME} -f deployments/docker-compose.yml up -d --build web
|
docker compose -p ${env.PROJECT_NAME} -f deployments/docker-compose.yml up -d --build web
|
||||||
"""
|
"""
|
||||||
|
|
||||||
echo "Ejecutando migraciones en ${env.CONTAINER_NAME}..."
|
|
||||||
sh "docker exec ${env.CONTAINER_NAME} python manage.py migrate --noinput"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
echo "✅ Despliegue completado con éxito."
|
||||||
|
// Limpieza preventiva: borramos el .env físico después del despliegue
|
||||||
|
sh "rm -f deployments/.env"
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo "❌ Error en el despliegue. Revisa los logs."
|
||||||
|
sh "rm -f deployments/.env"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,45 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
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
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${DB_NAME:-gitea}
|
||||||
|
POSTGRES_USER: ${DB_USER:-gitea}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-gitea}
|
||||||
|
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}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: deployments/Dockerfile
|
dockerfile: deployments/Dockerfile
|
||||||
container_name: ${CONTAINER_NAME}
|
container_name: ${APP_CONTAINER_NAME:-django_app_dev}
|
||||||
restart: always
|
restart: always
|
||||||
working_dir: /app
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- DEBUG=${DEBUG_MODE}
|
- DEBUG=${DEBUG_MODE:-1}
|
||||||
- PYTHONPATH=/app
|
# IMPORTANTE: Este nombre debe coincidir con el nombre del servicio arriba (gitea-db)
|
||||||
- DB_NAME=gitea
|
- DB_HOST=gitea-db
|
||||||
- DB_USER=gitea
|
|
||||||
- DB_PASSWORD=gitea # <-- Asegúrate de que esta sea la que pusiste en la web de Gitea
|
|
||||||
- DB_HOST=gitea-db # <-- IMPORTANTE: Nombre del servicio, sin el "-1"
|
|
||||||
- DB_PORT=5432
|
- DB_PORT=5432
|
||||||
networks:
|
|
||||||
- gitea_bridge # <-- Conectamos el servicio a nuestro puente
|
|
||||||
ports:
|
ports:
|
||||||
- "${PORT}:8000"
|
- "${PORT:-8000}:8000"
|
||||||
|
depends_on:
|
||||||
|
gitea-db:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
networks:
|
volumes:
|
||||||
gitea_bridge: # <-- Definimos el puente
|
postgres_data:
|
||||||
external: true
|
|
||||||
name: root_gitea_gitea # <-- ESTE es el nombre real que sale en tu 'docker network ls'
|
|
||||||
18
deployments/entrypoint.sh
Normal file
18
deployments/entrypoint.sh
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Salir inmediatamente si un comando falla
|
||||||
|
set -e
|
||||||
|
|
||||||
|
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 "--> 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
|
||||||
@@ -1,2 +1,7 @@
|
|||||||
Django==5.0.3
|
Django==5.0.3
|
||||||
psycopg2-binary==2.
|
psycopg2-binary==2.9.9
|
||||||
|
gunicorn==21.2.0
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
djangorestframework
|
||||||
|
django-cors-headers
|
||||||
|
djangorestframework-simplejwt
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
Django==5.0.3
|
|
||||||
psycopg2-binary==2.9.9
|
|
||||||
gunicorn==21.2.0
|
|
||||||
python-dotenv==1.0.1
|
|
||||||
Reference in New Issue
Block a user