feat(logs): add workspace activity log api

This commit is contained in:
2026-04-28 16:42:37 +03:30
parent c8a118788b
commit 71924ce6fb
32 changed files with 1118 additions and 122 deletions

View File

@@ -0,0 +1,12 @@
from rest_framework.exceptions import PermissionDenied
from apps.logs.services import WORKSPACE_LOGS_VIEW
from apps.workspaces.services import has_workspace_capability
def enforce_workspace_log_access(user, workspace) -> None:
if not user or not user.is_authenticated:
raise PermissionDenied("Authentication credentials were not provided.")
if not has_workspace_capability(user, workspace, WORKSPACE_LOGS_VIEW):
raise PermissionDenied("You do not have permission to view workspace logs.")

View File

@@ -0,0 +1,32 @@
from rest_framework import serializers
from apps.logs.services import LOG_EVENTS, LOG_SECTIONS
from apps.logs.services.query import (
serialize_workspace_log_detail,
serialize_workspace_log_list_item,
)
class WorkspaceLogQuerySerializer(serializers.Serializer):
workspace = serializers.UUIDField()
section = serializers.ChoiceField(choices=LOG_SECTIONS, required=False)
actor = serializers.UUIDField(required=False)
event = serializers.ChoiceField(choices=LOG_EVENTS, required=False)
search = serializers.CharField(required=False, allow_blank=True, trim_whitespace=True)
from_date = serializers.CharField(required=False, allow_blank=True, source="from")
to_date = serializers.CharField(required=False, allow_blank=True, source="to")
ordering = serializers.ChoiceField(
choices=("timestamp", "-timestamp"),
required=False,
default="-timestamp",
)
class WorkspaceLogListSerializer(serializers.Serializer):
def to_representation(self, instance):
return serialize_workspace_log_list_item(instance)
class WorkspaceLogDetailSerializer(serializers.Serializer):
def to_representation(self, instance):
return serialize_workspace_log_detail(instance)

12
apps/logs/api/urls.py Normal file
View File

@@ -0,0 +1,12 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from apps.logs.api.views import WorkspaceLogViewSet
router = DefaultRouter()
router.register(r"", WorkspaceLogViewSet, basename="workspace-logs")
urlpatterns = [
path("", include(router.urls)),
]

80
apps/logs/api/views.py Normal file
View File

@@ -0,0 +1,80 @@
from auditlog.models import LogEntry
from django.shortcuts import get_object_or_404
from rest_framework import status, viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from apps.logs.api.permissions import enforce_workspace_log_access
from apps.logs.api.serializers import (
WorkspaceLogDetailSerializer,
WorkspaceLogListSerializer,
WorkspaceLogQuerySerializer,
)
from apps.logs.services import (
WorkspaceLogFilters,
filter_workspace_logs,
get_log_workspace_id,
is_visible_workspace_log,
parse_filter_datetime,
)
from apps.workspaces.models import Workspace
from core.paginations.limit_offset import CustomLimitOffsetPagination
class WorkspaceLogViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated]
pagination_class = CustomLimitOffsetPagination
def get_queryset(self):
return LogEntry.objects.select_related("actor", "content_type").order_by("-timestamp")
def get_serializer_class(self):
if self.action == "retrieve":
return WorkspaceLogDetailSerializer
return WorkspaceLogListSerializer
def list(self, request, *args, **kwargs):
query_data = request.query_params.copy()
if "from" in query_data:
query_data["from_date"] = query_data.get("from")
if "to" in query_data:
query_data["to_date"] = query_data.get("to")
query_serializer = WorkspaceLogQuerySerializer(data=query_data)
query_serializer.is_valid(raise_exception=True)
filters_data = query_serializer.validated_data
workspace = get_object_or_404(
Workspace,
id=filters_data["workspace"],
is_deleted=False,
)
enforce_workspace_log_access(request.user, workspace)
filters = WorkspaceLogFilters(
workspace_id=str(workspace.id),
section=filters_data.get("section"),
actor_id=str(filters_data["actor"]) if filters_data.get("actor") else None,
event=filters_data.get("event"),
search=filters_data.get("search") or None,
from_value=parse_filter_datetime(query_data.get("from")),
to_value=parse_filter_datetime(query_data.get("to"), is_end=True),
ordering=filters_data.get("ordering", "-timestamp"),
)
queryset = filter_workspace_logs(self.get_queryset(), filters)
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
def retrieve(self, request, *args, **kwargs):
entry = self.get_object()
workspace_id = get_log_workspace_id(entry)
if not workspace_id:
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
workspace = get_object_or_404(Workspace, id=workspace_id, is_deleted=False)
enforce_workspace_log_access(request.user, workspace)
if not is_visible_workspace_log(entry):
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
serializer = self.get_serializer(entry)
return Response(serializer.data)