Merge pull request 'feat: añadir app general con LogService centralizado' (#36) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good

Reviewed-on: #36
This commit was merged in pull request #36.
This commit is contained in:
2026-04-16 14:57:20 +00:00
19 changed files with 230 additions and 27 deletions

View File

@@ -56,6 +56,7 @@ INSTALLED_APPS = [
'corsheaders',
# Tus Apps (Asegúrate de que el path sea correcto)
'general',
'promociones',
'backend_admin',
'common',

View File

@@ -2,6 +2,7 @@ from django.urls import path, include
from backend_admin import views as admin_views
urlpatterns = [
path('general/', include('general.urls')),
path('admin/', include('backend_admin.urls')),
path('promociones/', include('promociones.urls')),
path('api/token/', admin_views.api_token, name='token_obtain_pair'),

0
app/general/__init__.py Normal file
View File

1
app/general/admin.py Normal file
View File

@@ -0,0 +1 @@
from django.contrib import admin

6
app/general/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class GeneralConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'general'

21
app/general/exception.py Normal file
View File

@@ -0,0 +1,21 @@
from rest_framework.views import exception_handler
from rest_framework.response import Response
def custom_exception_handler(exc, context):
"""
Handler global de excepciones para DRF.
Añadir en settings.py:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'general.exception.custom_exception_handler',
}
"""
response = exception_handler(exc, context)
if response is not None:
response.data = {
'body': {'data': [], 'error': response.data},
'mensaje': str(exc),
}
return response

View File

5
app/general/models.py Normal file
View File

@@ -0,0 +1,5 @@
from django.db import models
# La app general no define modelos propios.
# El modelo Log vive en backend_admin.models y se accede desde
# general.utilidades.acciones.LogService.

24
app/general/request.py Normal file
View File

@@ -0,0 +1,24 @@
import json
from django.http import JsonResponse
def parse_body(request):
"""
Parsea el body de la request como JSON.
Lanza ValueError si el body está vacío o no es JSON válido.
"""
raw = request.body
if not raw:
raise ValueError('El body de la petición está vacío.')
return json.loads(raw)
def build_error_response(message, status=400, data=None):
"""Construye una respuesta de error en el formato estándar del proyecto."""
body = {'data': data if data is not None else [], 'error': message}
return JsonResponse({'body': body, 'mensaje': message}, status=status, safe=False)
def build_success_response(data, status=200):
"""Construye una respuesta de éxito en el formato estándar del proyecto."""
return JsonResponse(data, status=status, safe=False)

View File

@@ -0,0 +1,6 @@
from rest_framework import serializers
class EmptySerializer(serializers.Serializer):
"""Serializer base vacío para endpoints sin body estructurado."""
pass

7
app/general/tests.py Normal file
View File

@@ -0,0 +1,7 @@
from django.test import TestCase
class LogServiceTest(TestCase):
def test_placeholder(self):
"""Placeholder: añadir tests de LogService aquí."""
pass

6
app/general/urls.py Normal file
View File

@@ -0,0 +1,6 @@
from django.urls import path
from .views import status_view
urlpatterns = [
path('status/', status_view, name='general_status'),
]

View File

View File

@@ -0,0 +1,81 @@
from backend_admin.models import Log
from django.utils import timezone
from . import utils
class LogService:
"""
Servicio centralizado de gestión de logs.
Única fuente de inserción en la tabla audit_logs.
Uso en vistas:
log_id = LogService.gestionar_log(self, request, path='/mi/path/')
LogService.gestionar_log(self, request, log_id=log_id, body_request=data, status_code=200)
"""
def gestionar_log(
self,
request,
log_id=None,
path=None,
user=None,
body_request=None,
body_response=None,
status_code=None,
):
# Determinar la app que llama a partir del módulo de la vista
modulo = self.__class__.__module__
app_nombre = modulo.split('.')[0]
tag_header = request.headers.get('tag')
if tag_header:
usuario_final = tag_header
elif app_nombre:
apps_automatizadas = ['automatizados']
if app_nombre.lower() in apps_automatizadas:
usuario_final = 'Jenkins'
else:
usuario_final = app_nombre
else:
usuario_final = user if user else 'orquestador'
if log_id is None:
# --- CREACIÓN: primer registro del ciclo de vida de la petición ---
path_final = path if path else request.path
data_log = {
'user_id': 0,
'user': usuario_final,
'app_id': 0,
'remote_address': utils.get_client_ip(request),
'request': '',
'response': '',
'status_code': status_code if status_code else '0',
'path': path_final,
'method': request.method,
'createdAt': timezone.now(),
}
nuevo_log = Log.objects.create(**data_log)
return nuevo_log.pk
else:
# --- ACTUALIZACIÓN: enriquece el log con datos del procesamiento ---
datos_a_actualizar = {
'updatedAt': timezone.now(),
}
if user:
datos_a_actualizar['user'] = user
if path:
datos_a_actualizar['path'] = path
if body_request is not None:
datos_a_actualizar['request'] = body_request
if body_response is not None:
datos_a_actualizar['response'] = body_response
if status_code:
datos_a_actualizar['status_code'] = str(status_code)
Log.objects.filter(pk=log_id).update(**datos_a_actualizar)
return log_id

View File

@@ -0,0 +1,33 @@
class ValidationError(Exception):
"""Error de validación de datos de entrada."""
def __init__(self, message, field=None):
self.message = message
self.field = field
super().__init__(message)
def to_response(self):
detail = {'error': self.message}
if self.field:
detail['field'] = self.field
return {'body': {'data': [], **detail}, 'mensaje': self.message}
class ExternalServiceError(Exception):
"""Error en la comunicación con un servicio externo."""
def __init__(self, message, service=None):
self.message = message
self.service = service
super().__init__(message)
def to_response(self):
return {'body': {'data': [], 'error': self.message}, 'mensaje': self.message}
class NotFoundError(Exception):
"""Recurso no encontrado."""
def __init__(self, message='Recurso no encontrado'):
self.message = message
super().__init__(message)
def to_response(self):
return {'body': {'data': [], 'error': self.message}, 'mensaje': self.message}

View File

@@ -0,0 +1,6 @@
def get_client_ip(request):
"""Obtiene la IP real del cliente, considerando proxies."""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR')

View File

5
app/general/views.py Normal file
View File

@@ -0,0 +1,5 @@
from django.http import JsonResponse
def status_view(request):
return JsonResponse({'status': 'ok', 'service': 'general'})

View File

@@ -2,45 +2,45 @@ 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 general.utilidades.acciones import LogService
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'
)
path = '/promociones/obtener/'
print('llega a despues de log entry')
# --- LOG: inicio de la petición ---
log_id = LogService.gestionar_log(self, request, path=path)
try:
# --- BLOQUE 2: DATA CLEANING ---
# --- BLOQUE 2: limpieza y validación del body ---
data = request.data
log_entry.request = data
log_entry.save()
status = 100
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
params = {'id': data.get('id'), 'activo': data.get('activo')}
# --- BLOQUE 3: ACTION CALL ---
# Aquí ya recibimos las fechas como strings gracias al paso 1
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
status = 400
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)
try:
# --- BLOQUE 3: llamada a la acción ---
resultado = getData(params)
response = resultado
status = 200
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, safe=False, status=status)
# --- 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)
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
status = 500
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)