feat: govern attachment exceptions and ownership transfers
This commit is contained in:
@@ -478,7 +478,91 @@ def _recent_failures(jobs: list[CampaignJob], *, limit: int = 20) -> list[dict[s
|
||||
]
|
||||
|
||||
|
||||
def _job_row(job: CampaignJob, *, include_diagnostics: bool = False) -> dict[str, Any]:
|
||||
def _review_decisions_by_job(
|
||||
version: CampaignVersion | None,
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
if version is None:
|
||||
return {}
|
||||
editor_state = (
|
||||
getattr(version, "editor_state", None)
|
||||
if isinstance(getattr(version, "editor_state", None), dict)
|
||||
else {}
|
||||
)
|
||||
review_state = editor_state.get("review_send")
|
||||
build_summary = (
|
||||
getattr(version, "build_summary", None)
|
||||
if isinstance(getattr(version, "build_summary", None), dict)
|
||||
else {}
|
||||
)
|
||||
build_token = str(
|
||||
build_summary.get("build_token")
|
||||
or build_summary.get("built_at")
|
||||
or ""
|
||||
)
|
||||
if (
|
||||
not isinstance(review_state, dict)
|
||||
or review_state.get("inspection_complete") is not True
|
||||
or not build_token
|
||||
or str(review_state.get("build_token") or "") != build_token
|
||||
):
|
||||
return {}
|
||||
decisions: dict[str, dict[str, Any]] = {}
|
||||
for item in review_state.get("issue_decisions") or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
job_id = str(item.get("job_id") or "").strip()
|
||||
if job_id and job_id not in decisions:
|
||||
decisions[job_id] = item
|
||||
return decisions
|
||||
|
||||
|
||||
def _public_review_decision(
|
||||
decision: dict[str, Any] | None,
|
||||
*,
|
||||
include_diagnostics: bool,
|
||||
) -> dict[str, Any] | None:
|
||||
if not decision:
|
||||
return None
|
||||
result = {
|
||||
"decision": decision.get("decision"),
|
||||
"reason": decision.get("reason"),
|
||||
"actor_user_id": decision.get("actor_user_id"),
|
||||
"decided_at": decision.get("decided_at"),
|
||||
"issue_codes": list(decision.get("issue_codes") or []),
|
||||
}
|
||||
if include_diagnostics:
|
||||
result.update(
|
||||
{
|
||||
"review_key": decision.get("review_key"),
|
||||
"message_sha256": decision.get("message_sha256"),
|
||||
"issue_fingerprint": decision.get("issue_fingerprint"),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _review_decision_summary(
|
||||
decisions: dict[str, dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
issue_codes: Counter[str] = Counter()
|
||||
for decision in decisions.values():
|
||||
issue_codes.update(
|
||||
str(code)
|
||||
for code in decision.get("issue_codes") or []
|
||||
if code
|
||||
)
|
||||
return {
|
||||
"exception_decision_count": len(decisions),
|
||||
"by_issue_code": dict(issue_codes),
|
||||
}
|
||||
|
||||
|
||||
def _job_row(
|
||||
job: CampaignJob,
|
||||
*,
|
||||
include_diagnostics: bool = False,
|
||||
review_decision: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
row = {
|
||||
"job_id": job.id,
|
||||
"entry_index": job.entry_index,
|
||||
@@ -519,6 +603,10 @@ def _job_row(job: CampaignJob, *, include_diagnostics: bool = False) -> dict[str
|
||||
"issues_count": len(job.issues_snapshot or []),
|
||||
"attachment_config_count": len(job.resolved_attachments or []),
|
||||
"matched_file_count": sum(len(item.get("matches") or []) for item in (job.resolved_attachments or []) if isinstance(item, dict)),
|
||||
"review_decision": _public_review_decision(
|
||||
review_decision,
|
||||
include_diagnostics=include_diagnostics,
|
||||
),
|
||||
}
|
||||
if include_diagnostics:
|
||||
row.update(
|
||||
@@ -592,8 +680,14 @@ def _job_evidence_row(
|
||||
latest_imap: ImapAppendAttempt | None = None,
|
||||
latest_message_action: CampaignMessageAction | None = None,
|
||||
include_diagnostics: bool = False,
|
||||
review_decision: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
row = _job_row(job, include_diagnostics=include_diagnostics)
|
||||
row = _job_row(
|
||||
job,
|
||||
include_diagnostics=include_diagnostics,
|
||||
review_decision=review_decision,
|
||||
)
|
||||
public_decision = row.pop("review_decision") or {}
|
||||
recipients = job.resolved_recipients or {}
|
||||
row.update({
|
||||
"campaign_id": job.campaign_id,
|
||||
@@ -617,7 +711,27 @@ def _job_evidence_row(
|
||||
if latest_message_action
|
||||
else None
|
||||
),
|
||||
"review_decision": public_decision.get("decision"),
|
||||
"review_reason": public_decision.get("reason"),
|
||||
"review_actor_user_id": public_decision.get("actor_user_id"),
|
||||
"review_decided_at": public_decision.get("decided_at"),
|
||||
"review_issue_codes": "; ".join(
|
||||
str(code)
|
||||
for code in public_decision.get("issue_codes") or []
|
||||
),
|
||||
})
|
||||
if include_diagnostics:
|
||||
row.update(
|
||||
{
|
||||
"review_key": public_decision.get("review_key"),
|
||||
"review_message_sha256": public_decision.get(
|
||||
"message_sha256"
|
||||
),
|
||||
"review_issue_fingerprint": public_decision.get(
|
||||
"issue_fingerprint"
|
||||
),
|
||||
}
|
||||
)
|
||||
return row
|
||||
|
||||
|
||||
@@ -684,6 +798,7 @@ def generate_campaign_report(
|
||||
)
|
||||
aggregate = _JobReportAggregate()
|
||||
job_rows: list[dict[str, Any]] = []
|
||||
review_decisions = _review_decisions_by_job(version)
|
||||
retry_max_attempts = _retry_max_attempts(version)
|
||||
iterator = jobs.yield_per(500) if hasattr(jobs, "yield_per") else jobs
|
||||
for job in iterator:
|
||||
@@ -700,7 +815,11 @@ def generate_campaign_report(
|
||||
"or the paginated recipient table."
|
||||
)
|
||||
job_rows.append(
|
||||
_job_row(job, include_diagnostics=include_diagnostics)
|
||||
_job_row(
|
||||
job,
|
||||
include_diagnostics=include_diagnostics,
|
||||
review_decision=review_decisions.get(job.id),
|
||||
)
|
||||
)
|
||||
report = _campaign_report_payload(
|
||||
session,
|
||||
@@ -710,6 +829,7 @@ def generate_campaign_report(
|
||||
tenant_id=tenant_id,
|
||||
include_recent_failures=include_recent_failures,
|
||||
include_diagnostics=include_diagnostics,
|
||||
review_decisions=review_decisions,
|
||||
)
|
||||
if include_jobs:
|
||||
report["jobs"] = job_rows
|
||||
@@ -743,6 +863,7 @@ def _campaign_report_payload(
|
||||
tenant_id: str,
|
||||
include_recent_failures: bool,
|
||||
include_diagnostics: bool,
|
||||
review_decisions: dict[str, dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
report = {
|
||||
"generated_at": _utcnow_iso(),
|
||||
@@ -775,6 +896,7 @@ def _campaign_report_payload(
|
||||
"by_status": dict(aggregate.attachment_status),
|
||||
"by_behavior": dict(aggregate.attachment_behavior),
|
||||
},
|
||||
"review": _review_decision_summary(review_decisions or {}),
|
||||
"attempts": _campaign_report_attempt_counts(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
@@ -1199,6 +1321,7 @@ def generate_jobs_csv(
|
||||
session,
|
||||
job_ids,
|
||||
)
|
||||
review_decisions = _review_decisions_by_job(version)
|
||||
rows = [
|
||||
_job_evidence_row(
|
||||
job,
|
||||
@@ -1206,6 +1329,7 @@ def generate_jobs_csv(
|
||||
latest_imap=latest_imap.get(job.id),
|
||||
latest_message_action=latest_message_actions.get(job.id),
|
||||
include_diagnostics=include_diagnostics,
|
||||
review_decision=review_decisions.get(job.id),
|
||||
)
|
||||
for job in jobs
|
||||
]
|
||||
@@ -1258,10 +1382,22 @@ def generate_jobs_csv(
|
||||
"latest_message_action_status",
|
||||
"latest_message_action_id",
|
||||
"latest_message_action_at",
|
||||
"review_decision",
|
||||
"review_reason",
|
||||
"review_actor_user_id",
|
||||
"review_decided_at",
|
||||
"review_issue_codes",
|
||||
]
|
||||
if include_diagnostics:
|
||||
sent_at_index = fieldnames.index("outcome_unknown_at")
|
||||
fieldnames[sent_at_index:sent_at_index] = ["claimed_at", "smtp_started_at"]
|
||||
fieldnames.extend(
|
||||
[
|
||||
"review_key",
|
||||
"review_message_sha256",
|
||||
"review_issue_fingerprint",
|
||||
]
|
||||
)
|
||||
buffer = io.StringIO()
|
||||
writer = csv.DictWriter(buffer, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
|
||||
Reference in New Issue
Block a user