feat: añadir app general con LogService centralizado
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/pr-dev This commit looks good
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good

- Crear app/general con estructura estándar del proyecto:
  · utilidades/acciones.py → LogService.gestionar_log() (única fuente de logs)
  · utilidades/utils.py → get_client_ip()
  · utilidades/custom_errors.py → ValidationError, ExternalServiceError, NotFoundError
  · exception.py, request.py, serializers.py, validaciones/
- Registrar 'general' en INSTALLED_APPS y añadir general/ a urls.py
- Refactorizar promociones/views.py para usar LogService en lugar de Log directo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
juanjo
2026-04-16 16:50:53 +02:00
parent 91fc6900eb
commit 2f6564d9a6
19 changed files with 230 additions and 27 deletions

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'})