feat(reports): add localized workspace reports and exports

This commit is contained in:
2026-04-27 16:15:41 +03:30
parent fadf898486
commit e26263e93f
22 changed files with 2029 additions and 8 deletions

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,39 @@
from rest_framework import serializers
from apps.reports.models import ReportExportJob
from apps.reports.services.aggregation import ALLOWED_PERIODS
class ReportExportCreateSerializer(serializers.Serializer):
workspace = serializers.UUIDField()
period = serializers.ChoiceField(choices=sorted(ALLOWED_PERIODS))
from_date = serializers.DateField(required=False, allow_null=True)
to_date = serializers.DateField(required=False, allow_null=True)
user = serializers.UUIDField(required=False, allow_null=True)
client = serializers.UUIDField(required=False, allow_null=True)
project = serializers.UUIDField(required=False, allow_null=True)
tags = serializers.ListField(
child=serializers.UUIDField(),
required=False,
allow_empty=True,
)
language = serializers.ChoiceField(choices=("en", "fa"), required=False, default="en")
export_type = serializers.ChoiceField(choices=ReportExportJob.ExportType.choices)
class ReportExportJobSerializer(serializers.ModelSerializer):
class Meta:
model = ReportExportJob
fields = (
"id",
"workspace",
"export_type",
"status",
"filters",
"file_name",
"error_message",
"expires_at",
"completed_at",
"created_at",
)
read_only_fields = fields

20
apps/reports/api/urls.py Normal file
View File

@@ -0,0 +1,20 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from apps.reports.api.views import (
ReportChartView,
ReportDayDetailsView,
ReportExportJobViewSet,
ReportTableView,
)
router = DefaultRouter()
router.register(r"exports", ReportExportJobViewSet, basename="report-export-job")
urlpatterns = [
path("chart/", ReportChartView.as_view(), name="report-chart"),
path("table/", ReportTableView.as_view(), name="report-table"),
path("day-details/", ReportDayDetailsView.as_view(), name="report-day-details"),
path("", include(router.urls)),
]

110
apps/reports/api/views.py Normal file
View File

@@ -0,0 +1,110 @@
from __future__ import annotations
from django.conf import settings
from django.http import FileResponse, Http404
from django.urls import reverse
from django.utils import timezone
from drf_spectacular.utils import extend_schema
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.reports.api.serializers import (
ReportExportCreateSerializer,
ReportExportJobSerializer,
)
from apps.reports.models import ReportExportJob
from apps.reports.services import (
build_chart_report,
build_day_details_report,
build_table_report,
load_report_filters,
)
from apps.reports.tasks import generate_report_export_task
class ReportChartView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(responses=dict)
def get(self, request):
return Response(build_chart_report(request.user, request.query_params))
class ReportTableView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(responses=dict)
def get(self, request):
return Response(build_table_report(request.user, request.query_params))
class ReportDayDetailsView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(responses=dict)
def get(self, request):
return Response(build_day_details_report(request.user, request.query_params))
class ReportExportJobViewSet(
mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet,
):
permission_classes = [IsAuthenticated]
serializer_class = ReportExportJobSerializer
def get_queryset(self):
return ReportExportJob.objects.filter(requesting_user=self.request.user, is_deleted=False)
@extend_schema(request=ReportExportCreateSerializer, responses=ReportExportJobSerializer)
def create(self, request, *args, **kwargs):
serializer = ReportExportCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
filters = load_report_filters(request.user, serializer.validated_data)
job = ReportExportJob.objects.create(
requesting_user=request.user,
workspace=filters.workspace,
export_type=serializer.validated_data["export_type"],
filters={
"workspace": str(filters.workspace.id),
"period": filters.period,
"from_date": filters.from_date.isoformat(),
"to_date": filters.to_date.isoformat(),
"user": filters.user_id,
"client": filters.client_id,
"project": filters.project_id,
"tags": filters.tag_ids,
"language": serializer.validated_data.get("language", "en"),
},
status=ReportExportJob.Status.PENDING,
)
generate_report_export_task.delay(str(job.id))
output = ReportExportJobSerializer(job)
return Response(output.data, status=status.HTTP_202_ACCEPTED)
@action(detail=True, methods=["get"], url_path="download")
def download(self, request, pk=None):
job = self.get_object()
if job.status != ReportExportJob.Status.COMPLETED or not job.file:
raise Http404("Export file is not available.")
if job.expires_at and job.expires_at <= timezone.now():
raise Http404("Export file has expired.")
response = FileResponse(
job.file.open("rb"),
as_attachment=True,
filename=job.file_name or job.file.name.split("/")[-1],
)
return response
def build_export_action_url(job: ReportExportJob) -> str:
path = reverse("report-export-job-download", kwargs={"pk": job.id})
if settings.BASE_URL:
return f"{settings.BASE_URL.rstrip('/')}{path}"
return path