From ee48bf870e4bad5e0d5b73c7ae28a0dc98ed3527 Mon Sep 17 00:00:00 2001 From: lizerk Date: Thu, 6 Aug 2026 15:42:39 +0300 Subject: [PATCH] feat(tasks): allow users to manage automations --- backend/api/v1/tasks/serializers.py | 8 ++- backend/api/v1/tasks/views.py | 17 +++--- .../api/v1/tasks/test_task_automations.py | 26 +++++++++- frontend/src/app/tasks/page.tsx | 52 +++++++++---------- 4 files changed, 67 insertions(+), 36 deletions(-) diff --git a/backend/api/v1/tasks/serializers.py b/backend/api/v1/tasks/serializers.py index a279ead2..86162d16 100644 --- a/backend/api/v1/tasks/serializers.py +++ b/backend/api/v1/tasks/serializers.py @@ -144,6 +144,12 @@ class TaskAutomationRunSerializer(serializers.ModelSerializer): class TaskAutomationSerializer(serializers.ModelSerializer): + """Validate shared automation rules exposed to authenticated task users. + + ``can_manage`` intentionally mirrors the temporary broad API permission. It + should use the dedicated automation permission once that role is introduced. + """ + created_by = EmployeeBriefSerializer(read_only=True) boards = serializers.PrimaryKeyRelatedField( queryset=TaskBoard.objects.filter(is_archived=False), @@ -188,7 +194,7 @@ class TaskAutomationSerializer(serializers.ModelSerializer): def get_can_manage(self, obj): user = getattr(self.context.get("request"), "user", None) - return bool(user and (user.is_staff or user.is_superuser)) + return bool(user and user.is_authenticated) def validate_name(self, value): name = value.strip() diff --git a/backend/api/v1/tasks/views.py b/backend/api/v1/tasks/views.py index dd7f783a..2be28bfd 100644 --- a/backend/api/v1/tasks/views.py +++ b/backend/api/v1/tasks/views.py @@ -32,7 +32,7 @@ from procurement.models import ProcurementRequest from requests_app.models import Request as EmployeeRequest from rest_framework import filters, parsers, status, viewsets from rest_framework.decorators import action -from rest_framework.permissions import IsAdminUser, IsAuthenticated +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from schedule.models import Event @@ -186,17 +186,22 @@ def send_task_checklist_update(task, event, item_id): class TaskAutomationViewSet(viewsets.ModelViewSet): + """Manage shared task automation rules. + + Automation management is temporarily available to every authenticated user. + Rules are system-wide and may target multiple boards, so this broad access must + be replaced with a dedicated permission before permission roles are finalized. + """ + + # TODO(tasks-automation-permissions): Replace broad authenticated access with + # a dedicated permission/role for managing shared automation rules. + permission_classes = [IsAuthenticated] serializer_class = TaskAutomationSerializer filter_backends = [filters.SearchFilter, filters.OrderingFilter] search_fields = ["name", "description"] ordering_fields = ["position", "name", "created_at", "last_run_at"] ordering = ["kind", "position", "name", "id"] - def get_permissions(self): - if self.action in {"catalog", "buttons", "run"}: - return [IsAuthenticated()] - return [IsAdminUser()] - def get_queryset(self): queryset = ( TaskAutomation.objects.select_related("created_by") diff --git a/backend/tests/api/v1/tasks/test_task_automations.py b/backend/tests/api/v1/tasks/test_task_automations.py index 04175162..d19325c1 100644 --- a/backend/tests/api/v1/tasks/test_task_automations.py +++ b/backend/tests/api/v1/tasks/test_task_automations.py @@ -213,7 +213,7 @@ def test_scheduled_automation_runs_relative_to_task_due_date(task_setup): assert run.context["scheduled_for"] == local_run_at.isoformat() -def test_regular_user_can_run_eligible_button_but_cannot_manage_rules( +def test_regular_user_can_manage_rules_and_run_eligible_button( api_client, user_factory, task_setup, @@ -232,6 +232,19 @@ def test_regular_user_can_run_eligible_button_but_cannot_manage_rules( api_client.force_authenticate(employee) list_response = api_client.get(reverse("api:v1:tasks:task-automation-list")) + create_response = api_client.post( + reverse("api:v1:tasks:task-automation-list"), + { + "name": "Правило сотрудника", + "kind": "event", + "trigger": "task_updated", + "conditions": {"operator": "and", "children": []}, + "actions": [{"type": "set_priority", "priority": "medium"}], + "applies_to_all_boards": False, + "boards": [board.id], + }, + format="json", + ) buttons_response = api_client.get( reverse("api:v1:tasks:task-automation-buttons"), {"task": task.id}, @@ -242,7 +255,10 @@ def test_regular_user_can_run_eligible_button_but_cannot_manage_rules( format="json", ) - assert list_response.status_code == status.HTTP_403_FORBIDDEN + assert list_response.status_code == status.HTTP_200_OK + assert create_response.status_code == status.HTTP_201_CREATED + assert create_response.data["created_by"]["id"] == employee.id + assert create_response.data["can_manage"] is True assert buttons_response.status_code == status.HTTP_200_OK assert [item["id"] for item in buttons_response.data] == [button.id] assert run_response.status_code == status.HTTP_200_OK @@ -250,6 +266,12 @@ def test_regular_user_can_run_eligible_button_but_cannot_manage_rules( assert task.priority == "high" +def test_automation_management_requires_authentication(api_client): + response = api_client.get(reverse("api:v1:tasks:task-automation-list")) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + def test_manual_button_rechecks_conditions_on_run(api_client, user_factory, task_setup): owner, board, _, _, task = task_setup employee = user_factory(email="automation-condition@example.com") diff --git a/frontend/src/app/tasks/page.tsx b/frontend/src/app/tasks/page.tsx index fe204022..14125000 100644 --- a/frontend/src/app/tasks/page.tsx +++ b/frontend/src/app/tasks/page.tsx @@ -6640,19 +6640,19 @@ function TasksPageContent() { Добавить колонку - {user?.auth?.is_staff || user?.auth?.is_superuser ? ( - - ) : null} + {/* TODO(tasks-automation-permissions): Gate this action by the + dedicated automation-management capability once introduced. */} + - {user?.auth?.is_staff || user?.auth?.is_superuser ? ( - - ) : null} + ) : null}