Compare commits

...

2 Commits

Author SHA1 Message Date
816cc276f8 Merge pull request 'fix' (#17) from pre-dev into dev
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
Reviewed-on: #17
2026-04-13 23:01:41 +00:00
minguezsanzjuanjose
9dd97b34f2 fix
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-14 01:00:37 +02:00
16 changed files with 339 additions and 137 deletions

8
.env
View File

@@ -3,8 +3,8 @@ DEBUG=True
SECRET_KEY=una-clave-muy-secreta-y-larga-123456 SECRET_KEY=una-clave-muy-secreta-y-larga-123456
# Base de Datos (Conectando al PostgreSQL que instalamos) # Base de Datos (Conectando al PostgreSQL que instalamos)
DB_NAME=gitea DB_NAME=django_test
DB_USER=gitea DB_USER=django_user
DB_PASSWORD=gitea DB_PASSWORD=django_password
DB_HOST=gitea-db DB_HOST=db
DB_PORT=5432 DB_PORT=5432

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@ deployments/docker-compose.yml
postgres_data/ postgres_data/
local_postgres_data/ local_postgres_data/
*.pyc *.pyc
apps/backend_admin/migrations/0001_initial.py

View File

@@ -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.

View File

@@ -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')
return {
"status": "ok", # 1. Autenticación
"message": "V-Encore API System is active", user = authenticate(username=username, password=password)
"environment": "dev" # Esto podría venir de una variable de entorno
} if user is not None:
# 2. Generación de JWT (Access & Refresh)
refresh = RefreshToken.for_user(user)
return {
'refresh': str(refresh),
'access': str(refresh.access_token),
'user': user.username,
'status': 'success'
}
return None

View 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})"

View File

@@ -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__)
@@ -25,4 +31,65 @@ 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)

View File

@@ -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

View File

@@ -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'),
] ]

View File

@@ -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 try:
logger = logging.getLogger(__name__) # --- BLOQUE 2: DATA CLEANING ---
data = request.data
log_entry.request = data
log_entry.save()
def get_promocion_view(request): params = {'id': data.get('id'), 'activo': data.get('activo')}
"""
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: # --- BLOQUE 3: ACTION CALL ---
# --------------------------------------------------------- # Aquí ya recibimos las fechas como strings gracias al paso 1
# BLOQUE 2: Limpieza de datos (Data Cleaning) resultado = getData(params)
# ---------------------------------------------------------
# Extraemos los parámetros del request y preparamos el diccionario
raw_data = request.GET.dict()
# Aquí es donde ella aplicaría validaciones adicionales si fuera necesario
clean_params = {
'id': raw_data.get('id'),
'activo': raw_data.get('activo', True) # Valor por defecto
}
# --------------------------------------------------------- # --- BLOQUE 4: LOG CLOSURE & RESPONSE ---
# BLOQUE 3: Llamada a la Action (Execution) log_entry.response = {"count": len(resultado)} # No guardes todo el JSON si es muy grande
# --------------------------------------------------------- log_entry.status_code = '200'
# La lógica de SQL y parametrización vive dentro de esta llamada log_entry.save()
resultado_db = getData(clean_params)
# --------------------------------------------------------- return JsonResponse(resultado, safe=False, status=200)
# BLOQUE 4: Log de cierre y respuesta (Closure)
# ---------------------------------------------------------
logger.info(f"[SUCCESS] get_promocion_view finalizada. Registros encontrados: {len(resultado_db)}")
return JsonResponse({
'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)

View File

@@ -1,6 +1,7 @@
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 # Cargar variables desde el archivo .env
load_dotenv() load_dotenv()
@@ -8,9 +9,19 @@ load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-change-me-for-production') SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-change-me-for-production')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', 'True').lower() == 'true' DEBUG = os.getenv('DEBUG', 'True').lower() == 'true'
# Constants from .env
DB_NAME = os.getenv('DB_NAME')
DB_USER = os.getenv('DB_USER')
DB_PASSWORD = os.getenv('DB_PASSWORD')
DB_HOST = os.getenv('DB_HOST')
DB_PORT = os.getenv('DB_PORT')
ALLOWED_HOSTS = [ ALLOWED_HOSTS = [
'v-encore-lab.com', 'v-encore-lab.com',
'dev.v-encore-lab.com', 'dev.v-encore-lab.com',
@@ -18,9 +29,12 @@ ALLOWED_HOSTS = [
'localhost', 'localhost',
'127.0.0.1', '127.0.0.1',
'django_app_dev', 'django_app_dev',
'django_app_master' 'django_app_master',
'rest_framework',
'rest_framework.authtoken'
] ]
# Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
@@ -28,11 +42,16 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'apps.promociones', 'apps.promociones',
'apps.backend_admin',
'apps.common', 'apps.common',
'corsheaders',
] ]
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 +81,17 @@ TEMPLATES = [
WSGI_APPLICATION = 'core.wsgi.application' WSGI_APPLICATION = 'core.wsgi.application'
# Para desarrollo local: SQLite # Database
if DEBUG: DATABASES = {
DATABASES = { 'default': {
'default': { 'ENGINE': 'django.db.backends.postgresql',
'ENGINE': 'django.db.backends.sqlite3', 'NAME': DB_NAME,
'NAME': BASE_DIR / 'db.sqlite3', 'USER': DB_USER,
} 'PASSWORD': DB_PASSWORD,
} 'HOST': DB_HOST,
else: 'PORT': DB_PORT,
# Producción: PostgreSQL desde .env
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER'),
'PASSWORD': os.getenv('DB_PASSWORD'),
'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT'),
}
} }
}
# Internationalization # Internationalization
LANGUAGE_CODE = 'es-es' LANGUAGE_CODE = 'es-es'
@@ -89,13 +99,36 @@ TIME_ZONE = 'Europe/Madrid'
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = True
# Static files # Static files (CSS, JavaScript, Images)
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles' 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
CORS_ALLOW_ALL_ORIGINS = True
CSRF_TRUSTED_ORIGINS = [
'chrome-extension://amknoiejhlmhancpahfcfcfhllgkpbld',
'http://localhost:8000',
'http://127.0.0.1:8000',
]
# Logging
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,

View File

@@ -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'),
]

View File

@@ -4,7 +4,8 @@ FROM python:3.12-slim
# Evitar que Python genere archivos .pyc y que el buffer se sature # Evitar que Python genere archivos .pyc y que el buffer se sature
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
# Definimos la zona horaria como variable de entorno
ENV TZ=Europe/Madrid
# Directorio de trabajo # Directorio de trabajo
WORKDIR /app WORKDIR /app

View File

@@ -1,25 +1,36 @@
services: services:
web: db:
build: image: postgres:15
context: .. container_name: django_db_local
dockerfile: deployments/Dockerfile # Cargamos el archivo directamente
container_name: ${CONTAINER_NAME} env_file:
restart: always - ../.env
working_dir: /app
environment: environment:
- DEBUG=${DEBUG_MODE} # POSTGRES_DB espera estas variables exactas,
- PYTHONPATH=/app # así que las mapeamos a lo que tienes en tu .env
- DB_NAME=gitea - POSTGRES_DB=${DB_NAME}
- DB_USER=gitea - POSTGRES_USER=${DB_USER}
- DB_PASSWORD=gitea # <-- Asegúrate de que esta sea la que pusiste en la web de Gitea - POSTGRES_PASSWORD=${DB_PASSWORD}
- DB_HOST=gitea-db # <-- IMPORTANTE: Nombre del servicio, sin el "-1"
- DB_PORT=5432
networks:
- gitea_bridge # <-- Conectamos el servicio a nuestro puente
ports: ports:
- "${PORT}:8000" - "5432:5432"
volumes:
- local_postgres_data:/var/lib/postgresql/data
networks: web:
gitea_bridge: # <-- Definimos el puente build: .
external: true container_name: django_app_dev
name: root_gitea_gitea # <-- ESTE es el nombre real que sale en tu 'docker network ls' volumes:
- ..:/app
# Cargamos el archivo directamente aquí también
env_file:
- ../.env
environment:
- DB_HOST=db
- DB_PORT=5432
ports:
- "8000:8000"
depends_on:
- db
volumes:
local_postgres_data:

View File

@@ -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

View File

@@ -1,4 +0,0 @@
Django==5.0.3
psycopg2-binary==2.9.9
gunicorn==21.2.0
python-dotenv==1.0.1