Merge dev: feat app general con LogService
This commit is contained in:
@@ -56,6 +56,7 @@ INSTALLED_APPS = [
|
|||||||
'corsheaders',
|
'corsheaders',
|
||||||
|
|
||||||
# Tus Apps (Asegúrate de que el path sea correcto)
|
# Tus Apps (Asegúrate de que el path sea correcto)
|
||||||
|
'general',
|
||||||
'promociones',
|
'promociones',
|
||||||
'backend_admin',
|
'backend_admin',
|
||||||
'common',
|
'common',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from django.urls import path, include
|
|||||||
from backend_admin import views as admin_views
|
from backend_admin import views as admin_views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path('general/', include('general.urls')),
|
||||||
path('admin/', include('backend_admin.urls')),
|
path('admin/', include('backend_admin.urls')),
|
||||||
path('promociones/', include('promociones.urls')),
|
path('promociones/', include('promociones.urls')),
|
||||||
path('api/token/', admin_views.api_token, name='token_obtain_pair'),
|
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.permissions import IsAuthenticated
|
||||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from backend_admin.models import Log
|
|
||||||
|
from general.utilidades.acciones import LogService
|
||||||
from .acciones import getData
|
from .acciones import getData
|
||||||
from django.utils import timezone # Esta es la forma correcta
|
|
||||||
|
|
||||||
class PromocionObtener(APIView):
|
class PromocionObtener(APIView):
|
||||||
authentication_classes = [JWTAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
# --- BLOQUE 1: LOG INITIATION ---
|
path = '/promociones/obtener/'
|
||||||
log_entry = Log.objects.create(
|
|
||||||
user=request.user.username,
|
|
||||||
path='promociones/obtener/',
|
|
||||||
method='POST',
|
|
||||||
status_code='0'
|
|
||||||
)
|
|
||||||
|
|
||||||
print('llega a despues de log entry')
|
# --- LOG: inicio de la petición ---
|
||||||
|
log_id = LogService.gestionar_log(self, request, path=path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# --- BLOQUE 2: DATA CLEANING ---
|
# --- BLOQUE 2: limpieza y validación del body ---
|
||||||
data = request.data
|
data = request.data
|
||||||
log_entry.request = data
|
status = 100
|
||||||
log_entry.save()
|
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')}
|
params = {'id': data.get('id'), 'activo': data.get('activo')}
|
||||||
|
|
||||||
# --- BLOQUE 3: ACTION CALL ---
|
except Exception as error:
|
||||||
# Aquí ya recibimos las fechas como strings gracias al paso 1
|
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)
|
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 ---
|
except Exception as error:
|
||||||
log_entry.response = {"count": len(resultado)} # No guardes todo el JSON si es muy grande
|
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
|
||||||
log_entry.status_code = '200'
|
status = 500
|
||||||
log_entry.save()
|
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||||
|
return JsonResponse(response, status=status, safe=False)
|
||||||
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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user