feat(tasks): allow users to manage automations
This commit is contained in:
parent
0d252753a3
commit
ee48bf870e
@ -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()
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -6640,19 +6640,19 @@ function TasksPageContent() {
|
||||
<Plus size={14} className="app-text-muted shrink-0" />
|
||||
<span>Добавить колонку</span>
|
||||
</button>
|
||||
{user?.auth?.is_staff || user?.auth?.is_superuser ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setBoardActionsMenuOpen(false);
|
||||
setAutomationModalOpen(true);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-[var(--foreground)] transition hover:bg-[var(--surface-secondary)]"
|
||||
>
|
||||
<Workflow size={14} className="app-text-muted shrink-0" />
|
||||
<span>Автоматизация</span>
|
||||
</button>
|
||||
) : null}
|
||||
{/* TODO(tasks-automation-permissions): Gate this action by the
|
||||
dedicated automation-management capability once introduced. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setBoardActionsMenuOpen(false);
|
||||
setAutomationModalOpen(true);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-[var(--foreground)] transition hover:bg-[var(--surface-secondary)]"
|
||||
>
|
||||
<Workflow size={14} className="app-text-muted shrink-0" />
|
||||
<span>Автоматизация</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => board && void setDefaultBoard(board)}
|
||||
@ -7600,20 +7600,18 @@ function TasksPageContent() {
|
||||
<UserPlus size={15} className="app-text-muted" />
|
||||
Участники
|
||||
</button>
|
||||
{user?.auth?.is_staff || user?.auth?.is_superuser ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTaskAddMenuOpen(false);
|
||||
closeTaskView();
|
||||
setAutomationModalOpen(true);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm transition hover:bg-[var(--surface-secondary)]"
|
||||
>
|
||||
<Workflow size={15} className="app-text-muted" />
|
||||
Автоматизация
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTaskAddMenuOpen(false);
|
||||
closeTaskView();
|
||||
setAutomationModalOpen(true);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm transition hover:bg-[var(--surface-secondary)]"
|
||||
>
|
||||
<Workflow size={15} className="app-text-muted" />
|
||||
Автоматизация
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user