Martín Miranda

Merge branch 'feature/#67_agregar_auditorias' into 'develop'

Feature/#67 agregar auditorias



See merge request !63
@@ -4,6 +4,21 @@ from rest_framework.decorators import api_view @@ -4,6 +4,21 @@ from rest_framework.decorators import api_view
4 4
5 from django.conf import settings 5 from django.conf import settings
6 6
  7 +import datetime
  8 +
  9 +from actstream.models import actor_stream
  10 +from actstream.models import Action
  11 +from django.http import Http404
  12 +from django_filters.rest_framework import DjangoFilterBackend
  13 +from rest_framework import filters
  14 +from rest_framework.generics import get_object_or_404
  15 +from rest_framework.permissions import IsAuthenticated
  16 +from rest_framework_json_api.views import ReadOnlyModelViewSet
  17 +
  18 +from core.permissions import CustomModelPermissions
  19 +from core.serializers import ActionSerializer
  20 +from usuario.models import Usuario
  21 +
7 22
8 @api_view(['POST']) 23 @api_view(['POST'])
9 def recaptcha(request): 24 def recaptcha(request):
@@ -16,3 +31,39 @@ def recaptcha(request): @@ -16,3 +31,39 @@ def recaptcha(request):
16 ) 31 )
17 32
18 return Response({'captcha': r.json()}) 33 return Response({'captcha': r.json()})
  34 +
  35 +
  36 +class AuditoriaViewSet(ReadOnlyModelViewSet):
  37 + queryset = Action.objects.all()
  38 + permission_classes = (IsAuthenticated, CustomModelPermissions)
  39 + serializer_class = ActionSerializer
  40 + filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
  41 + ordering = '-timestamp'
  42 +
  43 + def get_queryset(self):
  44 + queryset = super().get_queryset()
  45 +
  46 + if not self.action == 'list':
  47 + return queryset
  48 +
  49 + # en el caso de que la accion sea listar, controlar que se filtre por fecha obligatoriamente
  50 + usuario_id = self.request.GET.get('usuario_id', None)
  51 + fecha_desde = self.request.GET.get('fecha_desde', None)
  52 + fecha_hasta = self.request.GET.get('fecha_hasta', None)
  53 +
  54 + if not fecha_desde:
  55 + return queryset.none()
  56 +
  57 + if not fecha_hasta or fecha_hasta < fecha_desde:
  58 + fecha_hasta = datetime.datetime.now()
  59 +
  60 + if usuario_id:
  61 + try:
  62 + usuario = get_object_or_404(Usuario, id=usuario_id)
  63 + queryset = usuario.actor_actions.public(timestamp__date__range=(fecha_desde, fecha_hasta))
  64 + except Http404:
  65 + return queryset.none()
  66 + else:
  67 + queryset = Action.objects.public(timestamp__date__range=(fecha_desde, fecha_hasta))
  68 +
  69 + return queryset
  1 +# IMAGEN_AVATAR_DEFECTO = 'img/perfil-avatar.jpg'
  2 +TASK_TIME_LIMIT = 60 * 60
  3 +TASK_RETRY_DELAY = 60
  4 +
  5 +# Verbos para auditoria
  6 +iniciado = 'iniciado'
  7 +
  1 +from typing import Optional
  2 +from actstream import action
  3 +from actstream.models import Action
  4 +from django.contrib.contenttypes.models import ContentType
  5 +from rest_framework.response import Response
  6 +from rest_framework import status
  7 +
  8 +from core.constants import iniciado
  9 +from core.serializers import ActionSerializer
  10 +
  11 +
  12 +class FiltroObligatorioMixin(object):
  13 + nombre_filtro_obligatorio: Optional[str] = None
  14 +
  15 + def get_queryset(self):
  16 + queryset = super().get_queryset()
  17 +
  18 + if not self.action == 'list':
  19 + return queryset
  20 +
  21 + # en el caso de que la accion sea listar, controlar que vengan los datos del filtro indicado
  22 + filtro = self.request.GET.get(self.get_nombre_filtro_obligatorio(), None)
  23 + if filtro:
  24 + return queryset
  25 +
  26 + return queryset.none()
  27 +
  28 + def get_nombre_filtro_obligatorio(self):
  29 + assert self.nombre_filtro_obligatorio is not None, (
  30 + "Debe definir el atributo nombre_filtro_obligatorio a la clase '%s'"
  31 + % self.__class__.__name__
  32 + )
  33 +
  34 + return self.nombre_filtro_obligatorio
  35 +
  36 +
  37 +class AuditoriaMixin:
  38 +
  39 + def create(self, request, *args, **kwargs):
  40 + serializer = self.get_serializer(data=request.data)
  41 + serializer.is_valid(raise_exception=True)
  42 + self.perform_create(serializer)
  43 + action_object = serializer.instance
  44 +
  45 + self.registrar_accion(usuario=self.request.user, verb=iniciado, instance=serializer.instance,
  46 + data=serializer.data)
  47 + headers = self.get_success_headers(serializer.data)
  48 + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
  49 +
  50 + def perform_update(self, serializer):
  51 + serializer_modificado = self.get_serializer(data=self.request.data)
  52 + serializer_modificado.is_valid()
  53 + serializer_accion = self.get_serializer(instance=serializer.instance)
  54 +
  55 + data = {'data_inicial': serializer_accion.data, 'data_modificada': serializer_modificado.data}
  56 +
  57 + self.registrar_accion(usuario=self.request.user, verb=serializer_modificado.data["estado"], instance=serializer.instance,
  58 + data=data)
  59 + serializer.save()
  60 +
  61 + @staticmethod
  62 + def registrar_accion(usuario, verb, instance, data=None, target=None):
  63 + action.send(usuario, verb=verb, action_object=instance, target=target, data=data)
  64 +
  65 + @staticmethod
  66 + def obtener_historial_acciones(content_type, action_objects_id, request):
  67 + content_type = ContentType.objects.get(model=content_type)
  68 + historial_actividades = Action.objects.filter(
  69 + action_object_object_id__in=action_objects_id,
  70 + action_object_content_type_id=content_type.id
  71 + )
  72 + serializer = ActionSerializer(historial_actividades, many=True, context={'request': request})
  73 + return serializer
  1 +from actstream.models import Action
  2 +from rest_framework import serializers
  3 +
  4 +from usuario.serializers import UsuarioSerializer
  5 +
  6 +from usuario.models import Usuario
  7 +
  8 +
  9 +class ActivityGenericRelatedField(serializers.Field):
  10 + """
  11 + DRF Serializer field that serializers GenericForeignKey fields on the :class:`~activity.models.Action`
  12 + of known model types to their respective ActionSerializer implementation.
  13 + """
  14 +
  15 + def to_representation(self, value):
  16 + from edicto.serializers import EdictoSerializer, PrecioSerializer
  17 +
  18 + MAPEO_SERIALIZADORES_POR_MODELO = {
  19 + "Usuario": UsuarioSerializer,
  20 + "Edicto": EdictoSerializer,
  21 + "Precio": PrecioSerializer
  22 + }
  23 +
  24 + nombre_modelo = type(value).__name__
  25 + serializer_cls = MAPEO_SERIALIZADORES_POR_MODELO.get(nombre_modelo, None)
  26 +
  27 + if serializer_cls:
  28 + # se genera un nuevo diccionario donde se le suma otro diccionario conteniendo el elemento "type"
  29 + data = {**{'type': nombre_modelo}, **{'id': value.id}, **serializer_cls(value, context=self.context).data}
  30 + else:
  31 + data = str(value)
  32 +
  33 + return data
  34 +
  35 +
  36 +class ActorSerializer(serializers.ModelSerializer):
  37 + class Meta:
  38 + model = Usuario
  39 + fields = (
  40 + 'username',
  41 + 'first_name',
  42 + 'last_name',
  43 + 'cuil'
  44 + )
  45 +
  46 +
  47 +class ActionSerializer(serializers.Serializer):
  48 + """
  49 + DRF serializer for :class:`~activity.models.Action`.
  50 + """
  51 + actor = ActorSerializer(read_only=True)
  52 + verb = serializers.CharField(read_only=True)
  53 + action_object = ActivityGenericRelatedField(read_only=True)
  54 + target = ActivityGenericRelatedField(read_only=True)
  55 + timestamp = serializers.DateTimeField(read_only=True)
  56 + data = serializers.DictField(read_only=True)
  57 +
  58 + class Meta:
  59 + model = Action
  60 + fields = ('id', 'actor', 'verb', 'action_object', 'target', 'timestamp', 'data')
@@ -2,13 +2,18 @@ from django_filters.rest_framework import DjangoFilterBackend @@ -2,13 +2,18 @@ from django_filters.rest_framework import DjangoFilterBackend
2 from rest_framework import viewsets, filters, mixins 2 from rest_framework import viewsets, filters, mixins
3 from rest_framework.permissions import IsAuthenticated 3 from rest_framework.permissions import IsAuthenticated
4 4
  5 +from core.mixins import AuditoriaMixin
  6 +from rest_framework.decorators import action
  7 +from core.serializers import ActionSerializer
  8 +from rest_framework.response import Response
  9 +
5 from .filters import EdictoFilter, PrecioFilter 10 from .filters import EdictoFilter, PrecioFilter
6 from .models import Edicto, Precio 11 from .models import Edicto, Precio
7 from .permissions import IsAdminOrAuthorized 12 from .permissions import IsAdminOrAuthorized
8 -from .serializer import EdictoSerializer, PrecioSerializer 13 +from .serializers import EdictoSerializer, PrecioSerializer
9 14
10 15
11 -class EdictoViewSet(mixins.CreateModelMixin, 16 +class EdictoViewSet(AuditoriaMixin, mixins.CreateModelMixin,
12 mixins.RetrieveModelMixin, 17 mixins.RetrieveModelMixin,
13 mixins.UpdateModelMixin, 18 mixins.UpdateModelMixin,
14 mixins.ListModelMixin, 19 mixins.ListModelMixin,
@@ -23,8 +28,23 @@ class EdictoViewSet(mixins.CreateModelMixin, @@ -23,8 +28,23 @@ class EdictoViewSet(mixins.CreateModelMixin,
23 ordering = ('fecha_publicacion',) 28 ordering = ('fecha_publicacion',)
24 lookup_field = 'uuid' 29 lookup_field = 'uuid'
25 30
  31 + @action(
  32 + methods=['GET'],
  33 + detail=True,
  34 + url_path='obtener-historial',
  35 + serializer_class=ActionSerializer
  36 + )
  37 + def obtener_historial(self, request, uuid):
  38 + action_objects_id = list(Edicto.objects.filter(uuid=uuid).values_list('id', flat=True))
  39 + serializer = self.obtener_historial_acciones(
  40 + content_type="edicto",
  41 + action_objects_id=action_objects_id,
  42 + request=request
  43 + )
  44 + return Response(serializer.data)
  45 +
26 46
27 -class PrecioViewSet(viewsets.ReadOnlyModelViewSet): 47 +class PrecioViewSet(AuditoriaMixin, viewsets.ReadOnlyModelViewSet):
28 serializer_class = PrecioSerializer 48 serializer_class = PrecioSerializer
29 permission_classes = [IsAuthenticated, IsAdminOrAuthorized] 49 permission_classes = [IsAuthenticated, IsAdminOrAuthorized]
30 filter_backends = (DjangoFilterBackend, filters.OrderingFilter) 50 filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
@@ -4,3 +4,13 @@ from django.apps import AppConfig @@ -4,3 +4,13 @@ from django.apps import AppConfig
4 class EdictoConfig(AppConfig): 4 class EdictoConfig(AppConfig):
5 default_auto_field = 'django.db.models.BigAutoField' 5 default_auto_field = 'django.db.models.BigAutoField'
6 name = 'edicto' 6 name = 'edicto'
  7 +
  8 + def ready(self):
  9 + from actstream import registry
  10 + registry.register(
  11 + self.get_model('Edicto'),
  12 + self.get_model('Precio'),
  13 + )
  14 +
  15 +
  16 +default_app_config = 'edicto.apps.EdictoConfig'
@@ -4,3 +4,10 @@ from django.apps import AppConfig @@ -4,3 +4,10 @@ from django.apps import AppConfig
4 class UsuarioConfig(AppConfig): 4 class UsuarioConfig(AppConfig):
5 default_auto_field = 'django.db.models.BigAutoField' 5 default_auto_field = 'django.db.models.BigAutoField'
6 name = 'usuario' 6 name = 'usuario'
  7 +
  8 + def ready(self):
  9 + from actstream import registry
  10 + registry.register(self.get_model('Usuario'))
  11 +
  12 +
  13 +default_app_config = 'usuario.apps.UsuarioConfig'
@@ -4,6 +4,8 @@ from organismo import api as organismo_api @@ -4,6 +4,8 @@ from organismo import api as organismo_api
4 from usuario import api as usuario_api 4 from usuario import api as usuario_api
5 from edicto.api import EdictoViewSet 5 from edicto.api import EdictoViewSet
6 from edicto import api as edicto_api 6 from edicto import api as edicto_api
  7 +from core import api as core_api
  8 +
7 # Define routes 9 # Define routes
8 router = routers.DefaultRouter() 10 router = routers.DefaultRouter()
9 11
@@ -11,5 +13,4 @@ router.register(prefix='usuario', viewset=usuario_api.UsuarioViewSet) @@ -11,5 +13,4 @@ router.register(prefix='usuario', viewset=usuario_api.UsuarioViewSet)
11 router.register(prefix='organismo', viewset=organismo_api.OrganismoViewSet) 13 router.register(prefix='organismo', viewset=organismo_api.OrganismoViewSet)
12 router.register(r'edicto', EdictoViewSet, basename='edicto') 14 router.register(r'edicto', EdictoViewSet, basename='edicto')
13 router.register(prefix='precio', viewset=edicto_api.PrecioViewSet) 15 router.register(prefix='precio', viewset=edicto_api.PrecioViewSet)
14 -  
15 - 16 +router.register(prefix='auditoria', viewset=core_api.AuditoriaViewSet)
@@ -52,6 +52,7 @@ THIRD_PARTY_APPS = ( @@ -52,6 +52,7 @@ THIRD_PARTY_APPS = (
52 'django_filters', 52 'django_filters',
53 'corsheaders', 53 'corsheaders',
54 'oauth2_provider', 54 'oauth2_provider',
  55 + 'actstream',
55 ) 56 )
56 57
57 PROJECT_APPS = ( 58 PROJECT_APPS = (
@@ -61,6 +62,8 @@ PROJECT_APPS = ( @@ -61,6 +62,8 @@ PROJECT_APPS = (
61 'edicto', 62 'edicto',
62 ) 63 )
63 64
  65 +SITE_ID = 1
  66 +
64 INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS 67 INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS
65 68
66 MIDDLEWARE = ( 69 MIDDLEWARE = (
@@ -192,3 +195,7 @@ AUTHENTICATION_BACKENDS = ( @@ -192,3 +195,7 @@ AUTHENTICATION_BACKENDS = (
192 195
193 # Secret Key para Captcha. 196 # Secret Key para Captcha.
194 SECRET_KEY_CAPTCHA = env.str('SECRET_KEY_CAPTCHA', default="") 197 SECRET_KEY_CAPTCHA = env.str('SECRET_KEY_CAPTCHA', default="")
  198 +
  199 +ACTSTREAM_SETTINGS = {
  200 + 'USE_JSONFIELD': True,
  201 +}
@@ -19,3 +19,6 @@ pyOpenSSL==22.0.0 @@ -19,3 +19,6 @@ pyOpenSSL==22.0.0
19 # database 19 # database
20 psycopg2==2.9.1 20 psycopg2==2.9.1
21 psycopg2-binary==2.9.1 21 psycopg2-binary==2.9.1
  22 +
  23 +# Django Activity
  24 +django-activity-stream==1.4.2