Skip to content

⚙️ 47823 backend [ tasks ] Update "require completion by all" logic for groups#257

Open
pneumojoseph wants to merge 13 commits into
masterfrom
backend/tasks/47823__update_rcba
Open

⚙️ 47823 backend [ tasks ] Update "require completion by all" logic for groups#257
pneumojoseph wants to merge 13 commits into
masterfrom
backend/tasks/47823__update_rcba

Conversation

@pneumojoseph

@pneumojoseph pneumojoseph commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Problem

When a task is set up so that every assignee must complete it individually, and the task is assigned to a user group, the system did not reliably track whether each group member had done their part. A task could stay open even after all active members had finished, or move forward before everyone had acted. Assignees who were later removed from the task could still be counted as outstanding. In addition, internal completion records for individual group members were incorrectly exposed in task details and activity history, making the performer list confusing for users.

Fix / Solution

  • Rework Task.can_be_completed() to correctly evaluate completion when require_completion_by_all is enabled, including group members, removed performers, inactive users, and deleted groups.

Release notes

  • Tasks with Require completion by all now correctly require every member of an assigned group to complete the task individually. Removed or inactive assignees are no longer counted as outstanding.
  • A completion event is recorded for each user who finishes their part. Internal group-member completion records are no longer shown in task performer lists or workflow events.

Changes

API

Tasks (/v2/tasks/)

  • GET /v2/tasks/:idperformers list includes only user and group types; group_user records are excluded.
  • GET /v2/tasks/:id/eventstask.performers in event payloads excludes group_user records.
  • POST /v2/tasks/:id/complete — when a group member completes a task with require_completion_by_all=true, a hidden group_user performer is created/updated, a task_complete event is emitted for that user, and the task advances only after all group members (and other performers) have completed; removed/deleted performers are ignored in completion checks.
  • POST /v2/tasks/:id/commenttask.performers in the response excludes group_user records.

Workflows (/workflows/)

  • GET /workflows/:id/eventstask.performers in event payloads excludes group_user records.
  • POST /workflows/:id/commenttask.performers in the response excludes group_user records.

Web-client

No frontend changes in this branch.

Test cases

API

POST /v2/tasks/:id/complete

Authorization Test case Expected result
Task performer (group member), valid token Send POST with only required fields (no fields_values) for a group-assigned task with require_completion_by_all=true where other group members have not yet completed 200; group_user record created for the current user with is_completed=true; task_complete event created for the user; task remains active (is_completed=false); workflow does not advance
Task performer (group member), valid token Send POST with all fields (fields_values included) for a group-assigned task with require_completion_by_all=true where all group members have now completed 200; response reflects completed task state; workflow advances to the next step
Task performer (direct user), valid token Send POST with only required fields for a user-assigned task with require_completion_by_all=false 200; task marked completed; default values applied for omitted optional fields
Task performer (direct user), valid token Send POST with all fields for a user-assigned task 200; response contains submitted field values
Task performer (group member), valid token Send POST for a group-assigned task where the user's group_user record is already is_completed=true 400; validation error body indicates the user has already completed the task
Task performer (group member), valid token Send POST for a group-assigned task where the group performer has directly_status=deleted 400; validation error — performer not found / cannot complete
Task performer (direct user), valid token Send POST for a user-assigned task where the user performer has directly_status=deleted 400; validation error — performer not found / cannot complete
Task performer (group member), valid token Send POST for a group-assigned task with an incomplete checklist 400; ChecklistIncompleted validation error in response body
Task performer (group member), valid token Send POST with invalid fields_values (wrong type or missing required field) 400; DRF validation error in response body
No authentication Send POST without auth token 401
Authenticated user who is not a task performer and not account owner Send POST 403
Guest token for a different task Send POST 403
Account owner (not a performer), valid token Send POST to force-complete a task 200; task completed regardless of individual performer states

GET /v2/tasks/:id

Authorization Test case Expected result
Workflow member or account owner, valid token GET task that has both group and group_user performers 200; performers contains only the group entry (one item); no group_user entries
Workflow member, valid token GET task where a performer was directly deleted; only the remaining active performer should appear 200; performers lists only non-deleted performers
Workflow member, valid token GET task with require_completion_by_all=true where current user has completed their part but task is still active 200; current user's performer entry shows is_completed=true
Workflow member, valid token GET completed task 200; task status and performer completion states are correct
No authentication GET 401
Admin not a workflow member GET 403
Guest from another task GET 403

GET /v2/tasks/:id/events

Authorization Test case Expected result
Workflow member, valid token GET events for a task with group and group_user performers after a task_complete event 200; task.performers in each event contains only group type, no group_user
Workflow member, valid token GET events with limit and offset query parameters 200; response size and offset match pagination parameters
Workflow member, valid token GET events with no matching events 200; empty list
No authentication GET 401
Guest from another task GET 403

GET /workflows/:id/events

Authorization Test case Expected result
Workflow member, valid token GET events for a workflow whose task has group and group_user performers 200; task.performers in events contains only group type
Workflow member, valid token GET events with limit and offset query parameters 200; pagination parameters respected
No authentication GET 401
User not a workflow member GET 403
Expired subscription GET 403

POST /v2/tasks/:id/comment

Authorization Test case Expected result
Workflow member, valid token POST comment with only required field (text) on a task with group_user performers 200; response task.performers contains only group type
Workflow member, valid token POST comment with all fields 200; comment created; performers list excludes group_user
No authentication POST 401
User without comment permission POST 403

POST /workflows/:id/comment

Authorization Test case Expected result
Workflow member, valid token POST comment on a workflow whose current task has group_user performers 200; response task.performers contains only group type
No authentication POST 401
User without comment permission POST 403

Web-client

No direct UI changes in this branch. Regression checks below cover API-driven behavior visible in the client.

Browser Device Authorization Test scenario Expected result
Chrome Desktop browser Group member Open a task assigned to a group with Require completion by all enabled; complete the task as the first group member while others have not Task remains in the active list for other members; performer list shows the group (not individual hidden records); a completion event appears in the activity feed for the completing user
Chrome Desktop browser Group member (last to complete) Complete the task as the final group member Task moves to completed state; workflow advances; all members see updated status
Chrome Desktop browser Workflow member Open task details for a group-assigned task where some members have already completed individually Performers section shows only the group entry, not duplicate individual entries
Chrome Desktop browser Workflow member Open the activity/events panel for a task with group performers Events show group in performers list; no group_user entries visible
Chrome Desktop browser Account owner Force-complete a group task with RCBA before all members have acted Task completes and workflow advances
Chrome Desktop browser Non-performer user Attempt to complete a task not assigned to them Access denied error shown
Chrome Desktop browser Group member Attempt to complete the same task twice Error message indicating the task was already completed by this user
Chrome Desktop browser Workflow member / account owner On a group task with RCBA, remove from the group the last member who has not yet completed the task Task completes automatically; workflow advances; remaining members no longer see the task as active
Chrome Desktop browser Account owner / group admin Add a new user to a group while a group task with RCBA is in progress and other members have already completed New group member sees the task as active and must complete it; task does not advance until the new member completes
Chrome Desktop browser Template editor (account owner / admin) Edit the template and change performer settings on the current active task (e.g. toggle RCBA, change group); save template changes applied to the running workflow Current task reflects updated performer settings; if all remaining required performers have already completed under the new rules, the task auto-completes and the workflow advances
Chrome Desktop browser Workflow member Change group membership, then revert the workflow to the previous step where that group is the performer Reverted task shows the group as performer with RCBA rules applied to the current group membership; each current group member must complete (or re-complete) as required; task advances only when all current members have completed
Chrome Desktop browser User assigned both directly and via group Open and complete a task with RCBA where the same user is a direct performer and a member of an assigned group User can complete the task once; task completion logic treats the user correctly; task advances when all required performers (including other group members) have completed
Chrome Desktop browser Account owner Remove a group performer from the task, then add the same group back as performer; group members complete the task Task can be completed normally by group members after re-adding; RCBA tracking resets appropriately; no stale completion state blocks progress

Note

High Risk
Changes core task completion and workflow advancement for RCBA + groups, and triggers async auto-complete on membership changes—incorrect SQL or edge-case handling could complete tasks early or leave them stuck.

Overview
Fixes require completion by all for tasks assigned to groups by tracking each active member’s completion separately and only advancing the workflow when everyone who still counts has finished.

Completion logic: Task.can_be_completed() now uses GetIncompletedTaskPerformersQuery for RCBA cases (direct users, group members via GROUP_USER completion rows, ignoring inactive users, soft-deleted groups, directly-deleted performers, and stale deleted GROUP_USER rows). Group members completing via complete_task_for_user get or create a hidden group_user TaskPerformer; analytics and task_complete events run in that path, and full task completion uses can_be_completed(by_user=...). API serializers expose only user and group performers.

Data model: TaskPerformer uniqueness becomes (task, user, type) for non-deleted rows (migration included).

Side effects: Celery check_and_complete_tasks runs after user deactivation and when users leave or a group is deleted, auto-completing eligible active group/user tasks as the account owner.

Reviewed by Cursor Bugbot for commit 249a04b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Update 'require completion by all' logic to support group performers in task completion

  • Introduces PerformerType.GROUP_USER to track individual group member completions; when a group performer completes a task, a GROUP_USER TaskPerformer record is get-or-created for that user.
  • Replaces WorkflowActionService._task_can_be_completed and Task.exclude_directly_deleted_taskperformer_set with Task.can_be_completed(by_user=...), which uses a new raw SQL query (GetIncompletedTaskPerformersQuery) to resolve incomplete performers across both direct and group memberships.
  • Moves analytics and workflow event emission from complete_task into complete_task_for_user; complete_task no longer accepts by_user or emits events.
  • Adds a check_and_complete_tasks Celery task, triggered when a user is deactivated or removed/deleted from a group, to asynchronously complete any affected tasks that become completable as a result.
  • Performer serialization and prefetching now filters to USER and GROUP types only, excluding GROUP_USER from API responses.
  • Risk: TaskPerformer uniqueness constraint changes from (user, task) to a conditional (task, user, type) where is_deleted=False, requiring a schema migration and allowing multiple rows per user/task across types.

Macroscope summarized 249a04b.

@pneumojoseph pneumojoseph self-assigned this Jul 7, 2026
@pneumojoseph pneumojoseph added the Backend API changes request label Jul 7, 2026
Comment thread backend/src/processes/services/workflow_action.py
Comment thread backend/src/processes/models/workflows/task.py
Comment thread backend/src/processes/services/workflow_action.py
Comment thread backend/src/processes/services/workflow_action.py
Comment thread backend/src/processes/queries.py
Comment thread backend/src/processes/queries.py
Comment thread backend/src/processes/services/workflow_action.py
Comment thread backend/src/processes/models/workflows/task.py
Comment thread backend/src/processes/models/mixins.py
Comment thread backend/src/processes/models/mixins.py
Comment thread backend/src/processes/migrations/0255_add_fieldsets.py Outdated
Comment thread backend/src/processes/migrations/0255_add_fieldsets.py Outdated
super().delete_raw_performers()
self._delete_orphaned_performers()

def can_be_completed(self) -> bool:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphan cleanup drops GROUP_USER rows

High Severity

_delete_orphaned_performers builds a queryset of performers not linked from raw performers, which always includes GROUP_USER rows. When any orphan USER or GROUP performer is found, it deletes the entire queryset, wiping per-member GROUP_USER completion records needed for require-completion-by-all.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08e729a. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium accounts/validators.py:12

user_is_last_performer() returns True (blocking deactivation) for a user whose own TaskPerformer row is soft-deleted, as long as the task has some other non-deleted performer. The chained filters .filter(taskperformer__is_deleted=False) and .filter(raw_performers__is_deleted=False) are applied as separate joins from on_performer(user.id), so each predicate can match a different related row — the user_id filter hits the user's row while the is_deleted=False filter is satisfied by a different performer. Use a single .filter(taskperformer__user_id=user.id, taskperformer__is_deleted=False) call (or Q object) so both conditions apply to the same TaskPerformer row. Apply the same fix for raw_performers.

    if (
        Task.objects.filter(
            taskperformer__user_id=user.id,
            taskperformer__is_deleted=False,
            taskperformer__directly_status__ne=DirectlyStatus.DELETED,
            raw_performers__is_deleted=False,
        )
        .raw_performers_count(1)
        .filter(
            status__in=(
                TaskStatus.PENDING,
                TaskStatus.DELAYED,
                TaskStatus.ACTIVE,
            ),
        ).exists()
    ):
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @backend/src/accounts/validators.py around lines 12-25:

`user_is_last_performer()` returns `True` (blocking deactivation) for a user whose own `TaskPerformer` row is soft-deleted, as long as the task has some other non-deleted performer. The chained filters `.filter(taskperformer__is_deleted=False)` and `.filter(raw_performers__is_deleted=False)` are applied as separate joins from `on_performer(user.id)`, so each predicate can match a different related row — the `user_id` filter hits the user's row while the `is_deleted=False` filter is satisfied by a different performer. Use a single `.filter(taskperformer__user_id=user.id, taskperformer__is_deleted=False)` call (or `Q` object) so both conditions apply to the same `TaskPerformer` row. Apply the same fix for `raw_performers`.

@pneumojoseph pneumojoseph changed the title ⚙️ 47823 backend [ tasks ] Update "require completion by all" logic for ⚙️ 47823 backend [ tasks ] Update "require completion by all" logic for groups Jul 8, 2026
- after remove user from a group
- after remove group
- after deactivate user

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 249a04b. Configure here.

)
if group_performer_tasks_ids:
check_and_complete_tasks.delay(
task_ids=group_performer_tasks_ids,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task IDs list tuple shape

High Severity

_check_and_complete_tasks builds task ID lists with values_list('id') without flat=True, producing one-tuples instead of integers. check_and_complete_tasks then filters with id__in on those values, so removed or deactivated users may not trigger auto-completion of RCBA group tasks.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 249a04b. Configure here.

completed_performers = task_performers.filter(
Q(is_completed=True)
| Q(is_completed=False, user_id=self.user.id)
| Q(is_completed=False, group__users=self.user.id),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dual performer completion deadlock

High Severity

For RCBA tasks where the same user is both a direct user performer and a group member, by_user_or_group().first() can pick the group row first. After they finish via a group_user record, a later complete attempt raises UserAlreadyCompleteTask while the direct user performer row may still be incomplete, blocking task completion.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 249a04b. Configure here.

Comment on lines +67 to +68
for task in tasks:
if task.can_be_completed():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High tasks/tasks.py:67

check_and_complete_tasks can force a task into COMPLETED status even after it has transitioned to PENDING or DELAYED, corrupting workflow state. The task is reloaded only by id and then completed if task.can_be_completed() returns true, but neither that method nor the loop checks task.is_active. Because this runs in a Celery task, the status can change between enqueue time and worker execution, so a task that is no longer active still gets force-completed. Consider guarding the loop with an task.is_active check before calling service.complete_task(task).

    for task in tasks:
-        if task.can_be_completed():
+        if task.is_active and task.can_be_completed():
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @backend/src/processes/tasks/tasks.py around lines 67-68:

`check_and_complete_tasks` can force a task into `COMPLETED` status even after it has transitioned to `PENDING` or `DELAYED`, corrupting workflow state. The task is reloaded only by `id` and then completed if `task.can_be_completed()` returns true, but neither that method nor the loop checks `task.is_active`. Because this runs in a Celery task, the status can change between enqueue time and worker execution, so a task that is no longer active still gets force-completed. Consider guarding the loop with an `task.is_active` check before calling `service.complete_task(task)`.

Comment on lines 397 to 399
service.update_users_counts()
self._check_and_complete_tasks()
self.identify(user)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High services/user.py:397

_deactivate() enqueues check_and_complete_tasks inside the transaction.atomic() block, so the Celery worker can run before the deactivation commits. task.can_be_completed() then still sees the user as active and returns False, so tasks are skipped with no retry — the new auto-completion path silently does nothing. Move the self._check_and_complete_tasks() call after the atomic block, or defer it with transaction.on_commit, so the worker only runs once the user's status = INACTIVE is persisted.

            service.update_users_counts()
-            self._check_and_complete_tasks()
            self.identify(user)

        self._check_and_complete_tasks()
Also found in 1 other location(s)

backend/src/accounts/services/group.py:258

delete() now calls _check_and_complete_tasks() at line 258 before self.instance.delete() soft-deletes the group and cascades the related TaskPerformer rows. _check_and_complete_tasks() immediately enqueues check_and_complete_tasks, so the worker can run against the still-existing group performer records, see the task as not completable, and exit. Because there is no second recheck after the delete finishes, tasks that should auto-complete after group deletion can remain stuck active.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @backend/src/accounts/services/user.py around lines 397-399:

`_deactivate()` enqueues `check_and_complete_tasks` inside the `transaction.atomic()` block, so the Celery worker can run before the deactivation commits. `task.can_be_completed()` then still sees the user as active and returns `False`, so tasks are skipped with no retry — the new auto-completion path silently does nothing. Move the `self._check_and_complete_tasks()` call after the `atomic` block, or defer it with `transaction.on_commit`, so the worker only runs once the user's `status = INACTIVE` is persisted.

Also found in 1 other location(s):
- backend/src/accounts/services/group.py:258 -- `delete()` now calls `_check_and_complete_tasks()` at line 258 before `self.instance.delete()` soft-deletes the group and cascades the related `TaskPerformer` rows. `_check_and_complete_tasks()` immediately enqueues `check_and_complete_tasks`, so the worker can run against the still-existing group performer records, see the task as not completable, and exit. Because there is no second recheck after the delete finishes, tasks that should auto-complete after group deletion can remain stuck active.

""" Check if it is possible to complete tasks
where the performer group is """

group_performer_tasks_ids = list(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High services/group.py:164

_check_and_complete_tasks can enqueue and auto-complete tasks belonging to unrelated groups. The query Task.objects.active().active_for_group(self.instance.id) uses active_for_group, whose filter is Q(taskperformer__group_id=group_id) | Q(taskperformer__type=PerformerType.GROUP). The second branch matches any active task that has any group performer at all, regardless of which group. So removing users from one group can trigger check_and_complete_tasks on tasks whose only group performer is a different group, and those tasks may be auto-completed if they are otherwise completable. Consider using a query scoped to this group only (e.g. taskperformer__group_id=self.instance.id) instead of reusing the broad active_for_group queryset.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @backend/src/accounts/services/group.py around line 164:

`_check_and_complete_tasks` can enqueue and auto-complete tasks belonging to unrelated groups. The query `Task.objects.active().active_for_group(self.instance.id)` uses `active_for_group`, whose filter is `Q(taskperformer__group_id=group_id) | Q(taskperformer__type=PerformerType.GROUP)`. The second branch matches any active task that has *any* group performer at all, regardless of which group. So removing users from one group can trigger `check_and_complete_tasks` on tasks whose only group performer is a different group, and those tasks may be auto-completed if they are otherwise completable. Consider using a query scoped to this group only (e.g. `taskperformer__group_id=self.instance.id`) instead of reusing the broad `active_for_group` queryset.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend API changes request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant