feat(campaign): add privacy-safe aggregate reports
This commit is contained in:
@@ -106,6 +106,14 @@ from govoplan_campaign.backend.db.models import (
|
||||
)
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_campaign.backend.reports.campaigns import CampaignReportError, generate_campaign_report, generate_jobs_csv
|
||||
from govoplan_campaign.backend.report_privacy_policy import CampaignReportPrivacyPolicyError
|
||||
from govoplan_campaign.backend.reports.aggregate import (
|
||||
AggregateCampaignReport,
|
||||
AggregateCampaignReportError,
|
||||
AggregateReportCampaignList,
|
||||
aggregate_report_campaign_item,
|
||||
generate_aggregate_campaign_report,
|
||||
)
|
||||
from govoplan_campaign.backend.response_security import (
|
||||
public_campaign_payload,
|
||||
public_delivery_result_message,
|
||||
@@ -962,6 +970,52 @@ def delete_recipient_import_mapping_profile(
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.get("/aggregate-reports", response_model=AggregateReportCampaignList)
|
||||
def list_aggregate_campaign_reports(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:report:read")),
|
||||
):
|
||||
"""List only the business metadata needed to select an aggregate report."""
|
||||
|
||||
campaigns = (
|
||||
_campaign_query_for_principal(session, principal)
|
||||
.order_by(Campaign.updated_at.desc())
|
||||
.all()
|
||||
)
|
||||
return AggregateReportCampaignList(
|
||||
campaigns=[aggregate_report_campaign_item(campaign) for campaign in campaigns]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/aggregate-reports/{campaign_id}", response_model=AggregateCampaignReport)
|
||||
def aggregate_campaign_report(
|
||||
campaign_id: str,
|
||||
version_id: str | None = None,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:report:read")),
|
||||
):
|
||||
"""Return the privacy-safe aggregate projection without recipient detail."""
|
||||
|
||||
_get_campaign_for_principal(session, campaign_id, principal)
|
||||
try:
|
||||
return generate_aggregate_campaign_report(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
version_id=version_id,
|
||||
)
|
||||
except AggregateCampaignReportError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Campaign report not found",
|
||||
) from exc
|
||||
except CampaignReportPrivacyPolicyError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Campaign report privacy policy is invalid",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get("/{campaign_id}/address-lookup", response_model=CampaignAddressLookupResponse)
|
||||
def lookup_campaign_addresses(
|
||||
campaign_id: str,
|
||||
@@ -2937,6 +2991,7 @@ def get_job_diagnostics(
|
||||
"""Return infrastructure details only to campaign operators/admins."""
|
||||
|
||||
_get_campaign_for_principal(session, campaign_id, principal)
|
||||
_require_permission(principal, "campaigns:recipient:read")
|
||||
campaign = _get_campaign_for_tenant(session, campaign_id, principal.tenant_id)
|
||||
job = session.get(CampaignJob, job_id)
|
||||
if not job or job.campaign_id != campaign.id or job.tenant_id != principal.tenant_id:
|
||||
@@ -2992,8 +3047,7 @@ def campaign_report(
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:report:read")),
|
||||
):
|
||||
_get_campaign_for_principal(session, campaign_id, principal)
|
||||
if include_jobs:
|
||||
_require_permission(principal, "campaigns:recipient:read")
|
||||
_require_permission(principal, "campaigns:recipient:read")
|
||||
"""Return the full JSON report for one campaign."""
|
||||
|
||||
try:
|
||||
@@ -3048,6 +3102,7 @@ def email_campaign_report(
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:report:send")),
|
||||
):
|
||||
campaign = _get_campaign_for_principal(session, campaign_id, principal)
|
||||
_require_permission(principal, "campaigns:recipient:export")
|
||||
selected_version_id = payload.version_id or campaign.current_version_id
|
||||
selected_version = session.get(CampaignVersion, selected_version_id) if selected_version_id else None
|
||||
if selected_version is not None and selected_version.campaign_id == campaign.id:
|
||||
@@ -3055,8 +3110,6 @@ def email_campaign_report(
|
||||
principal,
|
||||
selected_version.raw_json if isinstance(selected_version.raw_json, dict) else {},
|
||||
)
|
||||
if payload.include_jobs or payload.attach_jobs_csv:
|
||||
_require_permission(principal, "campaigns:recipient:export")
|
||||
"""Generate a campaign report and send it to one or more email addresses."""
|
||||
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user