Feature: Allow monitoring access to tasks summary (#12624)

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
Trenton H
2026-04-22 13:48:54 -07:00
committed by GitHub
co-authored by shamoon
parent 2a20cc29a6
commit 0c25c2dac5
14 changed files with 133 additions and 21 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ def has_system_status_permission(user: User | None) -> bool:
return (
getattr(user, "is_superuser", False)
or getattr(user, "is_staff", False)
or user.has_perm("paperless.view_system_status")
or user.has_perm("paperless.view_system_monitoring")
)
+8
View File
@@ -100,6 +100,14 @@ class TestTasksSummarySchema:
"summary items must have 'total_count' (TaskSummarySerializer)"
)
def test_summary_days_parameter_constraints(self, api_schema: SchemaGenerator):
op = api_schema["paths"]["/api/tasks/summary/"]["get"]
params = {p["name"]: p for p in op.get("parameters", [])}
assert "days" in params, "days query parameter must be declared"
schema = params["days"]["schema"]
assert schema.get("minimum") == 1, "days must have minimum: 1"
assert schema.get("maximum") == 365, "days must have maximum: 365"
class TestTasksActiveSchema:
"""tasks_active_retrieve: response must be an array of TaskSerializerV10."""
+1 -1
View File
@@ -102,7 +102,7 @@ class TestSystemStatus(APITestCase):
user = User.objects.create_user(username="status_user")
user.user_permissions.add(
Permission.objects.get(codename="view_system_status"),
Permission.objects.get(codename="view_system_monitoring"),
)
self.client.force_login(user)
+94
View File
@@ -578,6 +578,100 @@ class TestSummary:
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "days" in response.data
def test_days_capped_at_365(self, admin_client: APIClient) -> None:
"""?days= above 365 is silently clamped to 365 so tasks older than a year are excluded."""
old_task = PaperlessTaskFactory(task_type=PaperlessTask.TaskType.CONSUME_FILE)
PaperlessTask.objects.filter(pk=old_task.pk).update(
date_created=timezone.now() - timedelta(days=400),
)
response = admin_client.get(ENDPOINT + "summary/", {"days": 10000})
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 0
@pytest.mark.django_db()
class TestSummaryPermissions:
def test_monitoring_user_can_access_summary(
self,
user_client: APIClient,
regular_user,
) -> None:
"""A user with view_system_monitoring but no document permissions can access summary/."""
regular_user.user_permissions.add(
Permission.objects.get(codename="view_system_monitoring"),
)
response = user_client.get(ENDPOINT + "summary/")
assert response.status_code == status.HTTP_200_OK
def test_monitoring_user_sees_all_tasks(
self,
user_client: APIClient,
regular_user,
admin_user,
) -> None:
"""Monitoring user sees aggregate data for all tasks, not just unowned ones."""
regular_user.user_permissions.add(
Permission.objects.get(codename="view_system_monitoring"),
)
PaperlessTaskFactory(
owner=admin_user,
task_type=PaperlessTask.TaskType.CONSUME_FILE,
status=PaperlessTask.Status.SUCCESS,
)
response = user_client.get(ENDPOINT + "summary/")
assert response.status_code == status.HTTP_200_OK
total = sum(item["total_count"] for item in response.data)
assert total == 1
def test_regular_user_summary_scoped_to_own_and_unowned_tasks(
self,
user_client: APIClient,
regular_user: User,
admin_user: User,
) -> None:
"""A regular user with view_paperlesstask but not view_system_monitoring sees only
their own tasks and unowned tasks in the summary, not other users' tasks."""
regular_user.user_permissions.add(
Permission.objects.get(codename="view_paperlesstask"),
)
PaperlessTaskFactory(
owner=regular_user,
task_type=PaperlessTask.TaskType.CONSUME_FILE,
status=PaperlessTask.Status.SUCCESS,
)
PaperlessTaskFactory(
owner=None,
task_type=PaperlessTask.TaskType.CONSUME_FILE,
status=PaperlessTask.Status.SUCCESS,
)
PaperlessTaskFactory( # other user's task — must not appear
owner=admin_user,
task_type=PaperlessTask.TaskType.CONSUME_FILE,
status=PaperlessTask.Status.SUCCESS,
)
response = user_client.get(ENDPOINT + "summary/")
assert response.status_code == status.HTTP_200_OK
total = sum(item["total_count"] for item in response.data)
assert total == 2
def test_unauthenticated_cannot_access_summary(
self,
rest_api_client: APIClient,
) -> None:
"""Unauthenticated requests to summary/ return 401."""
response = rest_api_client.get(ENDPOINT + "summary/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.django_db()
class TestActive:
+14 -4
View File
@@ -3844,10 +3844,10 @@ class RemoteVersionView(GenericAPIView[Any]):
parameters=[
OpenApiParameter(
name="days",
type=int,
type={"type": "integer", "minimum": 1, "maximum": 365, "default": 30},
location=OpenApiParameter.QUERY,
required=False,
description="Number of days to include in aggregation (default 30)",
description="Number of days to include in aggregation (default 30, min 1, max 365)",
),
],
),
@@ -3949,18 +3949,28 @@ class TasksViewSet(ReadOnlyModelViewSet[PaperlessTask]):
count = tasks.update(acknowledged=True)
return Response({"result": count})
def get_permissions(self):
if self.action == "summary" and has_system_status_permission(
getattr(self.request, "user", None),
):
return [IsAuthenticated()]
return super().get_permissions()
@action(methods=["get"], detail=False)
def summary(self, request):
"""Aggregated task statistics per task_type over the last N days (default 30)."""
try:
days = max(1, int(request.query_params.get("days", 30)))
days = min(365, max(1, int(request.query_params.get("days", 30))))
except (TypeError, ValueError):
return Response(
{"days": "Must be a positive integer."},
status=status.HTTP_400_BAD_REQUEST,
)
cutoff = timezone.now() - timedelta(days=days)
queryset = self.get_queryset().filter(date_created__gte=cutoff)
if has_system_status_permission(request.user):
queryset = PaperlessTask.objects.filter(date_created__gte=cutoff)
else:
queryset = self.get_queryset().filter(date_created__gte=cutoff)
data = queryset.values("task_type").annotate(
total_count=Count("id"),