refactor: reorganizar estructura del proyecto al estándar app/api_config
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
- core/ → app/api_config/ - apps/backend_admin/ → app/backend_admin/ - apps/common/ → app/common/ - apps/promociones/ → app/promociones/ - manage.py → app/manage.py - Añadir app/requirements.txt - Actualizar todos los imports y referencias (DJANGO_SETTINGS_MODULE, ROOT_URLCONF, WSGI_APPLICATION, INSTALLED_APPS) - Actualizar Dockerfile con nuevo WORKDIR Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
3
app/api_config/.env.example
Normal file
3
app/api_config/.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"
|
||||
0
app/api_config/__init__.py
Normal file
0
app/api_config/__init__.py
Normal file
4
app/api_config/asgi.py
Normal file
4
app/api_config/asgi.py
Normal file
@@ -0,0 +1,4 @@
|
||||
import os
|
||||
from django.core.asgi import get_asgi_application
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings')
|
||||
application = get_asgi_application()
|
||||
174
app/api_config/settings.py
Normal file
174
app/api_config/settings.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from datetime import timedelta
|
||||
|
||||
SIMPLE_JWT = {
|
||||
# Cambiamos el tiempo de acceso a 3 horas
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(hours=3),
|
||||
|
||||
# El tiempo del refresh token suele ser mayor (por ejemplo, 1 día)
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
|
||||
|
||||
# Otras configuraciones que ya tengas...
|
||||
'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 = [
|
||||
'v-encore-lab.com',
|
||||
'dev.v-encore-lab.com',
|
||||
'185.187.169.109',
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
os.getenv('APP_CONTAINER_NAME', 'django_app_dev'), # Dinámico para Docker
|
||||
]
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
# Plugins
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'corsheaders',
|
||||
|
||||
# Tus Apps (Asegúrate de que el path sea correcto)
|
||||
'promociones',
|
||||
'backend_admin',
|
||||
'common',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'api_config.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'api_config.wsgi.application'
|
||||
|
||||
# 3. DATABASE
|
||||
# Extraemos con fallback por si el .env falla
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.getenv('DB_NAME', 'postgres'),
|
||||
'USER': os.getenv('DB_USER', 'postgres'),
|
||||
'PASSWORD': os.getenv('DB_PASSWORD', ''),
|
||||
'HOST': os.getenv('DB_HOST', 'localhost'),
|
||||
'PORT': os.getenv('DB_PORT', '5432'),
|
||||
}
|
||||
}
|
||||
|
||||
# Internationalization
|
||||
LANGUAGE_CODE = 'es-es'
|
||||
TIME_ZONE = 'Europe/Madrid'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# 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 = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': '{levelname} {asctime} {module} {message}',
|
||||
'style': '{',
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'': {
|
||||
'handlers': ['console'],
|
||||
'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)
|
||||
8
app/api_config/urls.py
Normal file
8
app/api_config/urls.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.urls import path, include
|
||||
from backend_admin import views as admin_views
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', include('backend_admin.urls')),
|
||||
path('promociones/', include('promociones.urls')),
|
||||
path('api/token/', admin_views.api_token, name='token_obtain_pair'),
|
||||
]
|
||||
7
app/api_config/wsgi.py
Normal file
7
app/api_config/wsgi.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
# Este es el enlace con tus configuraciones
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
34
app/backend_admin/acciones.py
Normal file
34
app/backend_admin/acciones.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from django.contrib.auth import authenticate
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
|
||||
class Admin:
|
||||
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):
|
||||
"""
|
||||
Capa Action: Valida credenciales y genera un par de tokens JWT.
|
||||
"""
|
||||
username = params.get('username')
|
||||
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 {
|
||||
'refresh': str(refresh),
|
||||
'access': str(refresh.access_token),
|
||||
'user': user.username,
|
||||
'status': 'success'
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
35
app/backend_admin/migrations/0001_initial.py
Normal file
35
app/backend_admin/migrations/0001_initial.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 5.0.3 on 2026-04-14 23:15
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Log',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('user_id', models.IntegerField(default=0)),
|
||||
('user', models.CharField(default='anonimo', max_length=255)),
|
||||
('app_id', models.IntegerField(default=0)),
|
||||
('remote_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('request', models.JSONField(blank=True, null=True)),
|
||||
('response', models.JSONField(blank=True, null=True)),
|
||||
('status_code', models.CharField(default='0', max_length=10)),
|
||||
('path', models.CharField(max_length=255)),
|
||||
('method', models.CharField(max_length=10)),
|
||||
('createdAt', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('updatedAt', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'audit_logs',
|
||||
},
|
||||
),
|
||||
]
|
||||
1
app/backend_admin/migrations/__init__.py
Normal file
1
app/backend_admin/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Archivo para marcar esta carpeta como paquete de migraciones
|
||||
26
app/backend_admin/models.py
Normal file
26
app/backend_admin/models.py
Normal file
@@ -0,0 +1,26 @@
|
||||
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 = True <-- Asegúrate de que no esté en False
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.method} {self.path} ({self.status_code})"
|
||||
7
app/backend_admin/urls.py
Normal file
7
app/backend_admin/urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
from .views import status_view
|
||||
|
||||
urlpatterns = [
|
||||
# Ruta final: /admin/status/
|
||||
path('status/', status_view, name='admin_status'),
|
||||
]
|
||||
95
app/backend_admin/views.py
Normal file
95
app/backend_admin/views.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from django.http import JsonResponse
|
||||
from .acciones import Admin
|
||||
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__)
|
||||
|
||||
def status_view(request):
|
||||
# BLOQUE 1: Log de iniciación
|
||||
logger.info("INICIO - Ejecutando Health Check de Administración.")
|
||||
|
||||
# BLOQUE 2: Limpieza y validación de datos
|
||||
# Para un status simple, el diccionario de limpieza está vacío
|
||||
data_cleaned = {}
|
||||
|
||||
# BLOQUE 3: Llamada a la acción
|
||||
try:
|
||||
# Instanciamos la clase Admin y llamamos al método
|
||||
admin_logic = Admin()
|
||||
response_data = admin_logic.get_status_action()
|
||||
status_code = 200
|
||||
except Exception as e:
|
||||
logger.error(f"ERROR - Fallo en get_status_action: {str(e)}")
|
||||
response_data = {"status": "error", "message": "Internal Server Error"}
|
||||
status_code = 500
|
||||
|
||||
# BLOQUE 4: Log de cierre y retorno
|
||||
logger.info(f"FIN - Health Check completado. 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)
|
||||
6
app/common/apps.py
Normal file
6
app/common/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# apps/common/apps.py
|
||||
from django.apps import AppConfig
|
||||
|
||||
class CommonConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.common' # <--- ESTO ES LO QUE DEBE PONER
|
||||
1
app/common/migrations/__init__.py
Normal file
1
app/common/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Archivo para marcar esta carpeta como paquete de migraciones
|
||||
11
app/common/utils.py
Normal file
11
app/common/utils.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# Ejemplo de limpieza de tipos para SQL
|
||||
def clean_sql_string(value):
|
||||
if value is None:
|
||||
return ""
|
||||
return str(value).strip().replace("'", "''")
|
||||
|
||||
def clean_sql_int(value):
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
21
app/manage.py
Normal file
21
app/manage.py
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
# Apuntamos a la configuración dentro de la carpeta 'core'
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
64
app/promociones/acciones.py
Normal file
64
app/promociones/acciones.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from django.db import connection
|
||||
from common.utils import clean_sql_string, clean_sql_int
|
||||
|
||||
|
||||
def get_status_action():
|
||||
"""
|
||||
Acción simple para comprobar que la lógica de la API responde.
|
||||
"""
|
||||
return {"status": "ok", "message": "API is running"}
|
||||
|
||||
def getData(params):
|
||||
"""
|
||||
Función estándar para obtener datos de promociones.
|
||||
"""
|
||||
# 1. Definimos la query con placeholders (%s)
|
||||
query = """
|
||||
SELECT id, nombre, fecha_inicio, descripcion
|
||||
FROM promociones
|
||||
WHERE id = %s AND activo = %s
|
||||
"""
|
||||
|
||||
|
||||
# 2. Preparamos el diccionario de parámetros (tu estándar get_parameterized)
|
||||
# Limpiamos los datos antes de enviarlos a la base de datos
|
||||
id_promocion = clean_sql_int(params.get('id'))
|
||||
is_active = True if params.get('activo') else False
|
||||
|
||||
parameter_dict = [id_promocion, is_active]
|
||||
|
||||
# 3. Ejecución parametrizada
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(query, parameter_dict)
|
||||
columns = [col[0] for col in cursor.description]
|
||||
result = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||
|
||||
return result
|
||||
|
||||
def setData(params):
|
||||
"""
|
||||
Función estándar para insertar o actualizar (set_parameterized).
|
||||
"""
|
||||
# Ejemplo de UPDATE con JOIN (como solicitaste en tu estándar)
|
||||
query = """
|
||||
UPDATE promociones p
|
||||
SET p.nombre = %s, p.fecha_modificacion = %s
|
||||
FROM categorias c
|
||||
WHERE p.categoria_id = c.id AND p.id = %s
|
||||
"""
|
||||
|
||||
# Fecha en formato YYYY-MM-DD para SQL
|
||||
import datetime
|
||||
fecha_hoy = datetime.datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
parameter_dict = [
|
||||
clean_sql_string(params.get('nombre')),
|
||||
fecha_hoy,
|
||||
clean_sql_int(params.get('id'))
|
||||
]
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(query, parameter_dict)
|
||||
affected_rows = cursor.rowcount
|
||||
|
||||
return affected_rows
|
||||
5
app/promociones/apps.py
Normal file
5
app/promociones/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class PromocionesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'promociones'
|
||||
35
app/promociones/fixtures/semillas.json
Normal file
35
app/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
app/promociones/migrations/__init__.py
Normal file
1
app/promociones/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Archivo para marcar esta carpeta como paquete de migraciones
|
||||
20
app/promociones/models.py
Normal file
20
app/promociones/models.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from django.db import models
|
||||
|
||||
class Promocion(models.Model):
|
||||
# Campos detectados en tu getData y setData
|
||||
nombre = models.CharField(max_length=255)
|
||||
fecha_inicio = models.DateField(null=True, blank=True)
|
||||
fecha_modificacion = models.DateField(null=True, blank=True)
|
||||
descripcion = models.TextField(null=True, blank=True)
|
||||
activo = models.BooleanField(default=True)
|
||||
|
||||
# Campo usado en tu JOIN de setData
|
||||
categoria_id = models.IntegerField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
# ¡CRÍTICO! Esto le dice a Django que la tabla se llame exactamente
|
||||
# como la has escrito en tu SQL manual
|
||||
db_table = 'promociones'
|
||||
|
||||
def __str__(self):
|
||||
return self.nombre
|
||||
6
app/promociones/urls.py
Normal file
6
app/promociones/urls.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.urls import path
|
||||
from .views import PromocionObtener
|
||||
|
||||
urlpatterns = [
|
||||
path('obtener/', PromocionObtener.as_view(), name='obtener_promocion'),
|
||||
]
|
||||
46
app/promociones/views.py
Normal file
46
app/promociones/views.py
Normal file
@@ -0,0 +1,46 @@
|
||||
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 backend_admin.models import Log
|
||||
from .acciones import getData
|
||||
from django.utils import timezone # Esta es la forma correcta
|
||||
class PromocionObtener(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
# --- 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')
|
||||
|
||||
try:
|
||||
# --- BLOQUE 2: DATA CLEANING ---
|
||||
data = request.data
|
||||
log_entry.request = data
|
||||
log_entry.save()
|
||||
|
||||
params = {'id': data.get('id'), 'activo': data.get('activo')}
|
||||
|
||||
# --- BLOQUE 3: ACTION CALL ---
|
||||
# Aquí ya recibimos las fechas como strings gracias al paso 1
|
||||
resultado = getData(params)
|
||||
|
||||
# --- BLOQUE 4: LOG CLOSURE & RESPONSE ---
|
||||
log_entry.response = {"count": len(resultado)} # No guardes todo el JSON si es muy grande
|
||||
log_entry.status_code = '200'
|
||||
log_entry.save()
|
||||
|
||||
return JsonResponse(resultado, safe=False, status=200)
|
||||
|
||||
except Exception as e:
|
||||
log_entry.status_code = '500'
|
||||
log_entry.response = {'error': str(e)}
|
||||
log_entry.save()
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
7
app/requirements.txt
Normal file
7
app/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Django==5.0.3
|
||||
psycopg2-binary==2.9.9
|
||||
gunicorn==21.2.0
|
||||
python-dotenv==1.0.1
|
||||
djangorestframework
|
||||
django-cors-headers
|
||||
djangorestframework-simplejwt
|
||||
Reference in New Issue
Block a user