Integrate Distribution Lists with Campaign recipients
This commit is contained in:
@@ -21,6 +21,10 @@ from govoplan_campaign.backend.schemas import (
|
||||
CampaignRecipientAddressSourceSnapshotResponse,
|
||||
CampaignRecipientSnapshotExcludedItem,
|
||||
CampaignRecipientSnapshotItem,
|
||||
CampaignDistributionListExpansionRequest,
|
||||
CampaignDistributionListExpansionResponse,
|
||||
CampaignDistributionListSource,
|
||||
CampaignDistributionListSourcesResponse,
|
||||
RecipientImportMappingProfileListResponse,
|
||||
RecipientImportMappingProfilePayload,
|
||||
RecipientImportMappingProfileResponse,
|
||||
@@ -63,6 +67,11 @@ from govoplan_campaign.backend.integrations import (
|
||||
postbox_integration,
|
||||
)
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_core.core.distribution_lists import (
|
||||
CAPABILITY_DISTRIBUTION_LIST_EXPAND,
|
||||
CAPABILITY_DISTRIBUTION_LIST_SOURCE,
|
||||
DistributionExpansionRequest,
|
||||
)
|
||||
from govoplan_campaign.backend.reports.campaigns import (
|
||||
CampaignReportError,
|
||||
generate_campaign_report,
|
||||
@@ -809,6 +818,144 @@ def snapshot_campaign_recipient_address_source(
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{campaign_id}/recipient-distribution-lists",
|
||||
response_model=CampaignDistributionListSourcesResponse,
|
||||
)
|
||||
def list_campaign_recipient_distribution_lists(
|
||||
campaign_id: str,
|
||||
query: str = Query(default="", min_length=0, max_length=200),
|
||||
limit: int = Query(default=100, ge=1, le=250),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:recipient:read")),
|
||||
):
|
||||
_get_campaign_for_principal(session, campaign_id, principal)
|
||||
source_capability = _registry_capability(CAPABILITY_DISTRIBUTION_LIST_SOURCE)
|
||||
expand_capability = _registry_capability(CAPABILITY_DISTRIBUTION_LIST_EXPAND)
|
||||
if source_capability is None or not hasattr(source_capability, "list_sources"):
|
||||
return CampaignDistributionListSourcesResponse(available=False)
|
||||
sources = getattr(source_capability, "list_sources")(
|
||||
session,
|
||||
principal,
|
||||
query=query,
|
||||
limit=limit,
|
||||
)
|
||||
return CampaignDistributionListSourcesResponse(
|
||||
available=True,
|
||||
expand_available=bool(
|
||||
expand_capability is not None and hasattr(expand_capability, "expand")
|
||||
),
|
||||
sources=[
|
||||
CampaignDistributionListSource.model_validate(
|
||||
_capability_payload(source)
|
||||
)
|
||||
for source in sources
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{campaign_id}/recipient-distribution-lists/preview",
|
||||
response_model=CampaignDistributionListExpansionResponse,
|
||||
)
|
||||
def preview_campaign_recipient_distribution_list(
|
||||
campaign_id: str,
|
||||
payload: CampaignDistributionListExpansionRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:recipient:read")),
|
||||
):
|
||||
_get_campaign_for_principal(session, campaign_id, principal)
|
||||
return _expand_campaign_distribution_list(
|
||||
session,
|
||||
principal,
|
||||
payload=payload,
|
||||
freeze=False,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{campaign_id}/recipient-distribution-lists/snapshot",
|
||||
response_model=CampaignDistributionListExpansionResponse,
|
||||
)
|
||||
def snapshot_campaign_recipient_distribution_list(
|
||||
campaign_id: str,
|
||||
payload: CampaignDistributionListExpansionRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:recipient:import")),
|
||||
):
|
||||
_get_campaign_for_principal(session, campaign_id, principal, write=True)
|
||||
if not payload.idempotency_key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail="Freezing a distribution list requires an idempotency key",
|
||||
)
|
||||
result = _expand_campaign_distribution_list(
|
||||
session,
|
||||
principal,
|
||||
payload=payload,
|
||||
freeze=True,
|
||||
)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="campaign.distribution_list_snapshot_frozen",
|
||||
object_type="campaign",
|
||||
object_id=campaign_id,
|
||||
details={
|
||||
"list_id": result.source.id,
|
||||
"list_revision": result.source.revision,
|
||||
"snapshot_id": result.snapshot_id,
|
||||
"expansion_hash": result.expansion_hash,
|
||||
"recipient_count": len(result.recipients),
|
||||
"excluded_count": len(result.excluded),
|
||||
"stale": result.stale,
|
||||
"truncated": result.truncated,
|
||||
},
|
||||
commit=True,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _expand_campaign_distribution_list(
|
||||
session: Session,
|
||||
principal: ApiPrincipal,
|
||||
*,
|
||||
payload: CampaignDistributionListExpansionRequest,
|
||||
freeze: bool,
|
||||
) -> CampaignDistributionListExpansionResponse:
|
||||
capability = _registry_capability(CAPABILITY_DISTRIBUTION_LIST_EXPAND)
|
||||
if capability is None or not hasattr(capability, "expand"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Distribution-list expansion capability is not available",
|
||||
)
|
||||
request = DistributionExpansionRequest(
|
||||
list_id=payload.list_id,
|
||||
revision=payload.revision,
|
||||
effective_at=payload.effective_at,
|
||||
purpose=payload.purpose,
|
||||
requested_channels=tuple(payload.requested_channels),
|
||||
parameters=payload.parameters,
|
||||
preview=not freeze,
|
||||
freeze=freeze,
|
||||
idempotency_key=payload.idempotency_key if freeze else None,
|
||||
)
|
||||
try:
|
||||
result = getattr(capability, "expand")(
|
||||
session,
|
||||
principal,
|
||||
request=request,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
return CampaignDistributionListExpansionResponse.model_validate(
|
||||
_capability_payload(result)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{campaign_id}", response_model=CampaignResponse)
|
||||
def get_campaign(
|
||||
campaign_id: str,
|
||||
|
||||
Reference in New Issue
Block a user