feat(campaigns): add accountable work assignments
Module Package Release / publish-packages (push) Successful in 14s

This commit is contained in:
2026-08-22 00:50:03 +02:00
parent c2f083e5f6
commit 2630498026
23 changed files with 3011 additions and 15 deletions
+119 -1
View File
@@ -4,7 +4,7 @@ from collections import Counter
from collections.abc import Mapping, Sequence
from datetime import datetime, timezone
from sqlalchemy import Text, cast, func, or_
from sqlalchemy import Text, and_, cast, func, or_
from sqlalchemy.orm import Session
from govoplan_campaign.backend.db.models import (
@@ -18,6 +18,8 @@ from govoplan_campaign.backend.db.models import (
CampaignSchedule,
CampaignShare,
CampaignVersion,
CampaignWorkAssignment,
CampaignWorkAssignmentEvent,
ImapAppendAttempt,
PostboxDeliveryAttempt,
PrintOutputAttempt,
@@ -279,6 +281,7 @@ class CampaignDsarProvider:
append=append,
tenant_id=tenant_id,
subject_user_id=subject_user_id,
subject_account_id=subject.account_id,
campaign_ids=evidence_campaign_ids,
version_ids=version_ids,
)
@@ -643,9 +646,124 @@ class CampaignDsarProvider:
append: object,
tenant_id: str,
subject_user_id: str | None,
subject_account_id: str | None,
campaign_ids: set[str],
version_ids: set[str],
) -> None:
assignment_filters = []
if subject_user_id is not None:
assignment_filters.append(
CampaignWorkAssignment.assigned_by_user_id == subject_user_id
)
if subject_account_id is not None:
assignment_filters.append(
and_(
CampaignWorkAssignment.assignee_type == "account",
CampaignWorkAssignment.assignee_id == subject_account_id,
)
)
assignment_ids: set[str] = set()
if assignment_filters:
assignment_rows = _bounded_rows(
db.query(CampaignWorkAssignment)
.filter(
CampaignWorkAssignment.tenant_id == tenant_id,
or_(*assignment_filters),
)
.order_by(CampaignWorkAssignment.id)
)
assignment_ids = {item.id for item in assignment_rows}
for assignment in assignment_rows:
match_fields = []
if assignment.assigned_by_user_id == subject_user_id:
match_fields.append("assigned_by_user_id")
if (
assignment.assignee_type == "account"
and assignment.assignee_id == subject_account_id
):
match_fields.append("assignee_id")
append( # type: ignore[operator]
_record(
"campaign_work_assignment",
assignment.id,
"campaign_work_accountability",
"Campaign work assignment",
{
"match_fields": match_fields,
"campaign_id": assignment.campaign_id,
"campaign_version_id": assignment.campaign_version_id,
"purpose": assignment.purpose,
"status": assignment.status,
"due_at": _iso(assignment.due_at),
"assignee_type": assignment.assignee_type,
"assignee_id": assignment.assignee_id,
"assignee_label_snapshot": assignment.assignee_label_snapshot,
"assignee_resolution_state": assignment.assignee_resolution_state,
"reference_kind": assignment.reference_kind,
"reference_id": assignment.reference_id,
"completed_at": _iso(assignment.completed_at),
"cancelled_at": _iso(assignment.cancelled_at),
},
observed_at=assignment.updated_at,
source_path=f"/campaigns/{assignment.campaign_id}/work",
)
)
event_filters = []
if subject_user_id is not None:
event_filters.append(
CampaignWorkAssignmentEvent.actor_user_id == subject_user_id
)
if assignment_ids:
event_filters.append(
CampaignWorkAssignmentEvent.assignment_id.in_(assignment_ids)
)
if subject_account_id is not None:
event_filters.extend(
(
and_(
CampaignWorkAssignmentEvent.assignee_type_snapshot == "account",
CampaignWorkAssignmentEvent.assignee_id_snapshot
== subject_account_id,
),
cast(CampaignWorkAssignmentEvent.details, Text).contains(
subject_account_id
),
)
)
if event_filters:
event_rows = _bounded_rows(
db.query(CampaignWorkAssignmentEvent)
.filter(
CampaignWorkAssignmentEvent.tenant_id == tenant_id,
or_(*event_filters),
)
.order_by(CampaignWorkAssignmentEvent.id)
)
for event in event_rows:
append( # type: ignore[operator]
_record(
"campaign_work_assignment_event",
event.id,
"campaign_work_accountability_evidence",
"Campaign work assignment event",
{
"campaign_id": event.campaign_id,
"assignment_id": event.assignment_id,
"event_kind": event.event_kind,
"status": event.status_snapshot,
"assignee_type": event.assignee_type_snapshot,
"assignee_id": event.assignee_id_snapshot,
"assignee_label_snapshot": event.assignee_label_snapshot,
"resolution_state": event.resolution_state_snapshot,
},
observed_at=event.created_at,
immutable=True,
retention_reason="Assignment lifecycle events retain accountable institutional work history.",
source_path=f"/campaigns/{event.campaign_id}/work",
)
)
if subject_user_id is not None:
collaboration_rows = _bounded_rows(
db.query(CampaignCollaborationEntry)