Merge dev: feat app general con LogService
This commit is contained in:
@@ -56,6 +56,7 @@ INSTALLED_APPS = [
|
||||
'corsheaders',
|
||||
|
||||
# Tus Apps (Asegúrate de que el path sea correcto)
|
||||
'general',
|
||||
'promociones',
|
||||
'backend_admin',
|
||||
'common',
|
||||
|
||||
@@ -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
0
app/general/__init__.py
Normal file
1
app/general/admin.py
Normal file
1
app/general/admin.py
Normal file
@@ -0,0 +1 @@
|
||||
from django.contrib import admin
|
||||
6
app/general/apps.py
Normal file
6
app/general/apps.py
Normal 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
21
app/general/exception.py
Normal 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
|
||||
0
app/general/migrations/__init__.py
Normal file
0
app/general/migrations/__init__.py
Normal file
5
app/general/models.py
Normal file
5
app/general/models.py
Normal 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
24
app/general/request.py
Normal 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)
|
||||
6
app/general/serializers.py
Normal file
6
app/general/serializers.py
Normal 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
7
app/general/tests.py
Normal 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
6
app/general/urls.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.urls import path
|
||||
from .views import status_view
|
||||
|
||||
urlpatterns = [
|
||||
path('status/', status_view, name='general_status'),
|
||||
]
|
||||
0
app/general/utilidades/__init__.py
Normal file
0
app/general/utilidades/__init__.py
Normal file
81
app/general/utilidades/acciones.py
Normal file
81
app/general/utilidades/acciones.py
Normal 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
|
||||
33
app/general/utilidades/custom_errors.py
Normal file
33
app/general/utilidades/custom_errors.py
Normal 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}
|
||||
6
app/general/utilidades/utils.py
Normal file
6
app/general/utilidades/utils.py
Normal 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')
|
||||
0
app/general/validaciones/__init__.py
Normal file
0
app/general/validaciones/__init__.py
Normal file
5
app/general/views.py
Normal file
5
app/general/views.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.http import JsonResponse
|
||||
|
||||
|
||||
def status_view(request):
|
||||
return JsonResponse({'status': 'ok', 'service': 'general'})
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user