Release v0.1.4
This commit is contained in:
@@ -19,6 +19,9 @@ from govoplan_campaign.backend.schemas import (
|
||||
CampaignShareTargetsResponse,
|
||||
CampaignShareUpsertRequest,
|
||||
CampaignOwnerUpdateRequest,
|
||||
RecipientImportMappingProfileListResponse,
|
||||
RecipientImportMappingProfilePayload,
|
||||
RecipientImportMappingProfileResponse,
|
||||
CampaignJobsResponse,
|
||||
CampaignJobDetailResponse,
|
||||
CampaignRetryJobsRequest,
|
||||
@@ -40,7 +43,7 @@ from govoplan_campaign.backend.schemas import (
|
||||
)
|
||||
from govoplan_core.auth.dependencies import ApiPrincipal, has_scope, require_scope
|
||||
from govoplan_core.audit.logging import audit_from_principal
|
||||
from govoplan_campaign.backend.db.models import Campaign, CampaignJob, CampaignShare, CampaignVersion, Group, ImapAppendAttempt, SendAttempt, User, UserGroupMembership
|
||||
from govoplan_campaign.backend.db.models import Campaign, CampaignJob, CampaignShare, CampaignVersion, Group, ImapAppendAttempt, RecipientImportMappingProfile, SendAttempt, User, UserGroupMembership
|
||||
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
|
||||
@@ -181,6 +184,27 @@ def _require_permission(principal: ApiPrincipal, scope: str) -> None:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Missing scope: {scope}")
|
||||
|
||||
|
||||
def _get_recipient_import_profile_for_principal(session: Session, profile_id: str, principal: ApiPrincipal) -> RecipientImportMappingProfile:
|
||||
profile = session.get(RecipientImportMappingProfile, profile_id)
|
||||
if not profile or profile.tenant_id != principal.tenant_id or profile.owner_user_id != principal.user.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Recipient import mapping profile not found")
|
||||
return profile
|
||||
|
||||
|
||||
def _apply_recipient_import_profile_payload(profile: RecipientImportMappingProfile, payload: RecipientImportMappingProfilePayload) -> None:
|
||||
profile.name = payload.name.strip()
|
||||
profile.column_count = payload.column_count
|
||||
profile.headers = list(payload.headers)
|
||||
profile.normalized_headers = list(payload.normalized_headers)
|
||||
profile.ordered_header_fingerprint = payload.ordered_header_fingerprint
|
||||
profile.unordered_header_fingerprint = payload.unordered_header_fingerprint
|
||||
profile.delimiter = payload.delimiter
|
||||
profile.header_rows = payload.header_rows
|
||||
profile.quoted = payload.quoted
|
||||
profile.value_separators = payload.value_separators
|
||||
profile.mappings = [mapping.model_dump(mode="json") for mapping in payload.mappings]
|
||||
|
||||
|
||||
def _recipient_sections_changed(current: dict[str, object] | None, proposed: dict[str, object] | None) -> bool:
|
||||
if proposed is None:
|
||||
return False
|
||||
@@ -346,6 +370,109 @@ def list_campaigns(
|
||||
return CampaignListResponse(campaigns=[CampaignResponse.model_validate(item) for item in campaigns])
|
||||
|
||||
|
||||
@router.get("/recipient-import/mapping-profiles", response_model=RecipientImportMappingProfileListResponse)
|
||||
def list_recipient_import_mapping_profiles(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:recipient:import")),
|
||||
):
|
||||
profiles = (
|
||||
session.query(RecipientImportMappingProfile)
|
||||
.filter(
|
||||
RecipientImportMappingProfile.tenant_id == principal.tenant_id,
|
||||
RecipientImportMappingProfile.owner_user_id == principal.user.id,
|
||||
)
|
||||
.order_by(RecipientImportMappingProfile.updated_at.desc(), RecipientImportMappingProfile.name.asc())
|
||||
.all()
|
||||
)
|
||||
return RecipientImportMappingProfileListResponse(
|
||||
profiles=[RecipientImportMappingProfileResponse.model_validate(profile) for profile in profiles]
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/recipient-import/mapping-profiles",
|
||||
response_model=RecipientImportMappingProfileResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def create_recipient_import_mapping_profile(
|
||||
payload: RecipientImportMappingProfilePayload,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:recipient:import")),
|
||||
):
|
||||
profile = RecipientImportMappingProfile(
|
||||
tenant_id=principal.tenant_id,
|
||||
owner_user_id=principal.user.id,
|
||||
name=payload.name.strip(),
|
||||
column_count=payload.column_count,
|
||||
headers=list(payload.headers),
|
||||
normalized_headers=list(payload.normalized_headers),
|
||||
ordered_header_fingerprint=payload.ordered_header_fingerprint,
|
||||
unordered_header_fingerprint=payload.unordered_header_fingerprint,
|
||||
delimiter=payload.delimiter,
|
||||
header_rows=payload.header_rows,
|
||||
quoted=payload.quoted,
|
||||
value_separators=payload.value_separators,
|
||||
mappings=[mapping.model_dump(mode="json") for mapping in payload.mappings],
|
||||
)
|
||||
session.add(profile)
|
||||
session.flush()
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="campaign.recipient_import_mapping_profile_created",
|
||||
object_type="recipient_import_mapping_profile",
|
||||
object_id=profile.id,
|
||||
details={"name": profile.name, "ordered_header_fingerprint": profile.ordered_header_fingerprint},
|
||||
commit=True,
|
||||
)
|
||||
session.refresh(profile)
|
||||
return RecipientImportMappingProfileResponse.model_validate(profile)
|
||||
|
||||
|
||||
@router.put("/recipient-import/mapping-profiles/{profile_id}", response_model=RecipientImportMappingProfileResponse)
|
||||
def update_recipient_import_mapping_profile(
|
||||
profile_id: str,
|
||||
payload: RecipientImportMappingProfilePayload,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:recipient:import")),
|
||||
):
|
||||
profile = _get_recipient_import_profile_for_principal(session, profile_id, principal)
|
||||
_apply_recipient_import_profile_payload(profile, payload)
|
||||
session.add(profile)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="campaign.recipient_import_mapping_profile_updated",
|
||||
object_type="recipient_import_mapping_profile",
|
||||
object_id=profile.id,
|
||||
details={"name": profile.name, "ordered_header_fingerprint": profile.ordered_header_fingerprint},
|
||||
commit=True,
|
||||
)
|
||||
session.refresh(profile)
|
||||
return RecipientImportMappingProfileResponse.model_validate(profile)
|
||||
|
||||
|
||||
@router.delete("/recipient-import/mapping-profiles/{profile_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_recipient_import_mapping_profile(
|
||||
profile_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:recipient:import")),
|
||||
):
|
||||
profile = _get_recipient_import_profile_for_principal(session, profile_id, principal)
|
||||
details = {"name": profile.name, "ordered_header_fingerprint": profile.ordered_header_fingerprint}
|
||||
session.delete(profile)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="campaign.recipient_import_mapping_profile_deleted",
|
||||
object_type="recipient_import_mapping_profile",
|
||||
object_id=profile_id,
|
||||
details=details,
|
||||
commit=True,
|
||||
)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.get("/{campaign_id}", response_model=CampaignResponse)
|
||||
def get_campaign(
|
||||
campaign_id: str,
|
||||
@@ -2008,4 +2135,3 @@ def preview_campaign_attachments(
|
||||
rules=rules,
|
||||
unused_shared_files=[_file_preview(session, asset) for asset in unused] if payload.include_unmatched else [],
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user