From 25554feac53705137268fbbfc7403e7de0521533 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Mon, 29 Jun 2026 07:50:27 +0545 Subject: [PATCH] feat(page_views): add automation rule management UI Add support for creating, toggling and deleting automation rules to streamline workflow and reduce manual effort --- src/dashboard/board_views/actions.py | 1 + src/dashboard/board_views/board.py | 10 +- src/dashboard/page_views/__init__.py | 1 + src/dashboard/page_views/automation_views.py | 97 ++++++++++++++++++++ src/dashboard/page_views/project_views.py | 14 ++- src/dashboard/page_views/ticket_views.py | 1 + 6 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 src/dashboard/page_views/automation_views.py diff --git a/src/dashboard/board_views/actions.py b/src/dashboard/board_views/actions.py index dbe0ad1..b94e2e6 100644 --- a/src/dashboard/board_views/actions.py +++ b/src/dashboard/board_views/actions.py @@ -122,6 +122,7 @@ def move_ticket(request, project_id, ticket_id): ) ticket.status = status + ticket._updater = request.user ticket.save(update_fields=["status", "updated_at"]) TicketHistory.objects.create( diff --git a/src/dashboard/board_views/board.py b/src/dashboard/board_views/board.py index 39ed51a..a8319bd 100644 --- a/src/dashboard/board_views/board.py +++ b/src/dashboard/board_views/board.py @@ -8,7 +8,7 @@ from django.views.decorators.http import require_http_methods from src.accounts.models import ADMIN_ROLES, Role from src.epics.models import Epic -from src.projects.models import BoardRolePermission, Project +from src.projects.models import AutomationRule, BoardRolePermission, Project from src.tickets.models import ( Ticket, TicketPriority, @@ -145,6 +145,13 @@ def project_board(request, project_id=None): if selected_project else [] ) + automation_rules = ( + AutomationRule.objects.filter(project=selected_project) + .select_related("trigger_status") + .order_by("-created_at") + if selected_project + else [] + ) role_choices = Role.objects.all() return render( @@ -177,6 +184,7 @@ def project_board(request, project_id=None): "first_status_id": first_status_id, "first_status_name": first_status_name, "permissions": permissions, + "automation_rules": automation_rules, "role_choices": role_choices, "admin_roles": ADMIN_ROLES, }, diff --git a/src/dashboard/page_views/__init__.py b/src/dashboard/page_views/__init__.py index 43bcee1..7c5a481 100644 --- a/src/dashboard/page_views/__init__.py +++ b/src/dashboard/page_views/__init__.py @@ -1,3 +1,4 @@ +from .automation_views import * from .board_permission_views import * from .epic_views import * from .external_views import * diff --git a/src/dashboard/page_views/automation_views.py b/src/dashboard/page_views/automation_views.py new file mode 100644 index 0000000..beab5c2 --- /dev/null +++ b/src/dashboard/page_views/automation_views.py @@ -0,0 +1,97 @@ +import json + +from django.contrib import messages +from django.http import JsonResponse +from django.shortcuts import get_object_or_404, redirect +from django.views.decorators.http import require_http_methods + +from src.projects.models import AutomationRule, Project + +VALID_ACTION_TYPES = frozenset( + { + "assign_user", + "change_status", + "notify_in_app", + "send_email", + } +) + + +def _validate_actions(actions): + if not isinstance(actions, list): + return False, "Actions must be a list." + for i, action in enumerate(actions): + if not isinstance(action, dict): + return False, f"Action at index {i} must be an object." + atype = action.get("type") + if atype not in VALID_ACTION_TYPES: + return False, f"Action at index {i}: unknown type '{atype}'." + return True, None + + +@require_http_methods(["GET", "POST"]) +def automation_rules(request, project_id): + project = get_object_or_404(Project, pk=project_id) + + if not request.user.is_admin: + messages.error(request, "Permission denied.") + return redirect("project-board-project", project_id=project_id) + + if request.method == "POST": + name = request.POST.get("name", "").strip() + trigger_event = request.POST.get("trigger_event", "status_changed") + trigger_status_id = request.POST.get("trigger_status") or None + actions_raw = request.POST.get("actions", "[]") + + if not name: + messages.error(request, "Rule name is required.") + return redirect("project-board-project", project_id=project_id) + + try: + actions = json.loads(actions_raw) + except json.JSONDecodeError: + messages.error(request, "Invalid actions JSON.") + return redirect("project-board-project", project_id=project_id) + + valid, error = _validate_actions(actions) + if not valid: + messages.error(request, error) + return redirect("project-board-project", project_id=project_id) + + AutomationRule.objects.create( + project=project, + name=name, + trigger_event=trigger_event, + trigger_status_id=trigger_status_id, + actions=actions, + created_by=request.user, + ) + messages.success(request, f"Automation rule '{name}' created.") + + return redirect("project-board-project", project_id=project_id) + + +@require_http_methods(["POST"]) +def automation_rule_toggle(request, project_id, rule_id): + project = get_object_or_404(Project, pk=project_id) + if not request.user.is_admin: + return JsonResponse({"error": "Permission denied"}, status=403) + + rule = get_object_or_404(AutomationRule, pk=rule_id, project=project) + rule.is_active = not rule.is_active + rule.save(update_fields=["is_active"]) + return JsonResponse({"is_active": rule.is_active}) + + +@require_http_methods(["POST"]) +def automation_rule_delete(request, project_id, rule_id): + project = get_object_or_404(Project, pk=project_id) + + if not request.user.is_admin: + messages.error(request, "Permission denied.") + return redirect("project-board-project", project_id=project_id) + + rule = get_object_or_404(AutomationRule, pk=rule_id, project=project) + rule.delete() + messages.success(request, f"Automation rule '{rule.name}' deleted.") + return redirect("project-board-project", project_id=project_id) diff --git a/src/dashboard/page_views/project_views.py b/src/dashboard/page_views/project_views.py index e01155d..81def48 100644 --- a/src/dashboard/page_views/project_views.py +++ b/src/dashboard/page_views/project_views.py @@ -13,7 +13,12 @@ from django.views.decorators.http import require_http_methods from src.accounts.models import ADMIN_ROLES, Role from src.epics.models import Epic from src.notes.models import ProjectNote -from src.projects.models import BoardRolePermission, Project, ProjectMember +from src.projects.models import ( + AutomationRule, + BoardRolePermission, + Project, + ProjectMember, +) from src.tickets.models import ( Ticket, TicketPriority, @@ -297,6 +302,13 @@ def project_detail(request, project_id): .order_by("role"), "role_choices": Role.objects.all(), "admin_roles": ADMIN_ROLES, + "columns": [ + {"status": s} + for s in project.ticket_statuses.all().order_by("sort_order") + ], + "automation_rules": AutomationRule.objects.filter(project=project) + .select_related("trigger_status") + .order_by("-created_at"), }, ) diff --git a/src/dashboard/page_views/ticket_views.py b/src/dashboard/page_views/ticket_views.py index ab7abf6..51b6c2f 100644 --- a/src/dashboard/page_views/ticket_views.py +++ b/src/dashboard/page_views/ticket_views.py @@ -792,6 +792,7 @@ def tickets_bulk_status(request): for ticket in tickets: old_name = ticket.status.name ticket.status = status + ticket._updater = request.user ticket.save(update_fields=["status", "updated_at"]) TicketHistory.objects.create(