258 lines
8.5 KiB
Python
258 lines
8.5 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from govoplan_campaign.backend.schemas import (
|
|
ReportEmailRequest,
|
|
ReportEmailResponse,
|
|
)
|
|
from govoplan_core.auth import ApiPrincipal, has_scope, require_scope
|
|
from govoplan_core.audit.logging import audit_from_principal
|
|
from govoplan_campaign.backend.db.models import (
|
|
CampaignVersion,
|
|
)
|
|
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.reports.emailing import (
|
|
CampaignReportEmailError,
|
|
send_campaign_report_email,
|
|
)
|
|
from govoplan_campaign.backend.integrations import (
|
|
MailDeliveryCommandError,
|
|
MailProfileError,
|
|
SmtpConfigurationError,
|
|
SmtpSendError,
|
|
mail_integration,
|
|
)
|
|
|
|
|
|
from govoplan_campaign.backend.route_support import (
|
|
_get_campaign_for_principal,
|
|
_require_mail_profile_use_if_needed,
|
|
_require_permission,
|
|
)
|
|
|
|
router = APIRouter(prefix="/campaigns", tags=["campaigns"])
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _enqueue_mail_command() -> None:
|
|
try:
|
|
from govoplan_core.celery_app import celery
|
|
from govoplan_core.settings import settings
|
|
|
|
if settings.celery_enabled:
|
|
celery.send_task(
|
|
"govoplan.mail.dispatch_outbox",
|
|
args=[None, 25],
|
|
queue="mail",
|
|
)
|
|
except Exception:
|
|
logger.warning(
|
|
"Mail delivery command is durable but immediate worker wake-up failed",
|
|
exc_info=True,
|
|
)
|
|
|
|
|
|
@router.get("/{campaign_id}/summary")
|
|
def campaign_summary(
|
|
campaign_id: str,
|
|
version_id: str | None = None,
|
|
include_jobs: bool = False,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:read")),
|
|
):
|
|
"""Return dashboard-friendly campaign status counters and summaries."""
|
|
_get_campaign_for_principal(session, campaign_id, principal)
|
|
if include_jobs:
|
|
_require_permission(principal, "campaigns:recipient:read")
|
|
|
|
try:
|
|
return generate_campaign_report(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=version_id,
|
|
include_jobs=include_jobs,
|
|
include_recent_failures=include_jobs,
|
|
include_diagnostics=has_scope(principal, "campaigns:diagnostic:read"),
|
|
)
|
|
except CampaignReportError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
|
) from exc
|
|
|
|
|
|
@router.get("/{campaign_id}/report")
|
|
def campaign_report(
|
|
campaign_id: str,
|
|
version_id: str | None = None,
|
|
include_jobs: bool = False,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:report:read")),
|
|
):
|
|
"""Return the recipient-level JSON report for one campaign."""
|
|
_get_campaign_for_principal(session, campaign_id, principal)
|
|
_require_permission(principal, "campaigns:recipient:read")
|
|
|
|
try:
|
|
return generate_campaign_report(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=version_id,
|
|
include_jobs=include_jobs,
|
|
include_recent_failures=include_jobs,
|
|
include_diagnostics=has_scope(principal, "campaigns:diagnostic:read"),
|
|
)
|
|
except CampaignReportError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
|
) from exc
|
|
|
|
|
|
@router.get("/{campaign_id}/report/jobs.csv")
|
|
def campaign_jobs_csv(
|
|
campaign_id: str,
|
|
version_id: str | None = None,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:report:export")),
|
|
):
|
|
"""Export per-job campaign status as CSV."""
|
|
_get_campaign_for_principal(session, campaign_id, principal)
|
|
_require_permission(principal, "campaigns:recipient:export")
|
|
|
|
try:
|
|
csv_text = generate_jobs_csv(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=version_id,
|
|
include_diagnostics=has_scope(principal, "campaigns:diagnostic:read"),
|
|
)
|
|
except CampaignReportError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
|
) from exc
|
|
return Response(
|
|
content=csv_text,
|
|
media_type="text/csv; charset=utf-8",
|
|
headers={
|
|
"Content-Disposition": f'attachment; filename="campaign-{campaign_id}-jobs.csv"'
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/{campaign_id}/report/email", response_model=ReportEmailResponse)
|
|
def email_campaign_report(
|
|
campaign_id: str,
|
|
payload: ReportEmailRequest,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:report:send")),
|
|
):
|
|
"""Generate a campaign report and send it to one or more email addresses."""
|
|
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:
|
|
_require_mail_profile_use_if_needed(
|
|
principal,
|
|
selected_version.raw_json
|
|
if isinstance(selected_version.raw_json, dict)
|
|
else {},
|
|
)
|
|
try:
|
|
result = send_campaign_report_email(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=payload.version_id,
|
|
to=payload.to,
|
|
include_jobs=payload.include_jobs,
|
|
attach_jobs_csv=payload.attach_jobs_csv,
|
|
attach_report_json=payload.attach_report_json,
|
|
dry_run=payload.dry_run,
|
|
idempotency_key=payload.idempotency_key,
|
|
created_by_user_id=principal.user.id,
|
|
)
|
|
audit_from_principal(
|
|
session,
|
|
principal,
|
|
action="report.email_requested"
|
|
if not payload.dry_run
|
|
else "report.email_dry_run",
|
|
object_type="campaign",
|
|
object_id=campaign_id,
|
|
details=result.audit_dict(),
|
|
commit=True,
|
|
)
|
|
if not payload.dry_run:
|
|
_enqueue_mail_command()
|
|
return ReportEmailResponse(result=result.as_dict())
|
|
except CampaignReportError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
|
) from exc
|
|
except (
|
|
CampaignReportEmailError,
|
|
MailProfileError,
|
|
MailDeliveryCommandError,
|
|
SmtpConfigurationError,
|
|
SmtpSendError,
|
|
) as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)
|
|
) from exc
|
|
except Exception as exc:
|
|
logger.error("Campaign report email failed with an unexpected internal error")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Campaign report email could not be completed.",
|
|
) from exc
|
|
|
|
|
|
@router.get(
|
|
"/{campaign_id}/report/email/{command_id}",
|
|
response_model=ReportEmailResponse,
|
|
)
|
|
def campaign_report_email_status(
|
|
campaign_id: str,
|
|
command_id: str,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:report:read")),
|
|
):
|
|
_get_campaign_for_principal(session, campaign_id, principal)
|
|
try:
|
|
result = mail_integration().delivery_command_summary(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
command_id=command_id,
|
|
)
|
|
except MailDeliveryCommandError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=str(exc),
|
|
) from exc
|
|
if (
|
|
result.get("source_module") != "campaigns"
|
|
or result.get("source_resource_type") != "campaign"
|
|
or result.get("source_resource_id") != campaign_id
|
|
):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Campaign report delivery not found",
|
|
)
|
|
return ReportEmailResponse(result=result)
|