Release v0.1.5

This commit is contained in:
2026-07-07 15:49:06 +02:00
parent fb6fb67d45
commit 533c4c8fe3
19 changed files with 705 additions and 58 deletions

View File

@@ -41,9 +41,11 @@ from govoplan_campaign.backend.schemas import (
ReportEmailRequest,
ReportEmailResponse,
)
from govoplan_core.auth.dependencies import ApiPrincipal, has_scope, require_scope
from govoplan_access.backend.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, RecipientImportMappingProfile, SendAttempt, User, UserGroupMembership
from govoplan_core.core.access import CAPABILITY_ACCESS_DIRECTORY, AccessDirectory
from govoplan_core.core.runtime import get_registry
from govoplan_campaign.backend.db.models import Campaign, CampaignJob, CampaignShare, CampaignVersion, ImapAppendAttempt, RecipientImportMappingProfile, SendAttempt
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
@@ -79,6 +81,16 @@ from govoplan_campaign.backend.persistence.versions import (
router = APIRouter(prefix="/campaigns", tags=["campaigns"])
def _access_directory() -> AccessDirectory:
registry = get_registry()
if registry is None or not hasattr(registry, "has_capability") or not registry.has_capability(CAPABILITY_ACCESS_DIRECTORY):
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Access directory capability is not configured")
capability = registry.require_capability(CAPABILITY_ACCESS_DIRECTORY)
if not isinstance(capability, AccessDirectory):
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Access directory capability is invalid")
return capability
def _get_campaign_for_tenant(session: Session, campaign_id: str, tenant_id: str) -> Campaign:
campaign = session.get(Campaign, campaign_id)
if not campaign or campaign.tenant_id != tenant_id:
@@ -99,12 +111,8 @@ def _get_version_for_tenant(session: Session, version_id: str, tenant_id: str) -
def _principal_group_ids(session: Session, principal: ApiPrincipal) -> set[str]:
return {
row[0]
for row in session.query(UserGroupMembership.group_id)
.filter(UserGroupMembership.tenant_id == principal.tenant_id, UserGroupMembership.user_id == principal.user.id)
.all()
}
del session
return {group.id for group in _access_directory().groups_for_user(principal.user.id, tenant_id=principal.tenant_id)}
@@ -1530,21 +1538,12 @@ def list_campaign_share_targets(
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:share")),
):
_get_campaign_for_principal(session, campaign_id, principal, write=True)
users = (
session.query(User)
.filter(User.tenant_id == principal.tenant_id, User.is_active.is_(True))
.order_by(User.display_name.asc(), User.email.asc())
.all()
)
groups = (
session.query(Group)
.filter(Group.tenant_id == principal.tenant_id, Group.is_active.is_(True))
.order_by(Group.name.asc())
.all()
)
directory = _access_directory()
users = [user for user in directory.users_for_tenant(principal.tenant_id) if user.status == "active"]
groups = [group for group in directory.groups_for_tenant(principal.tenant_id) if group.status == "active"]
return CampaignShareTargetsResponse(
users=[CampaignShareTargetItem(id=item.id, name=item.display_name or item.email, secondary=item.email) for item in users],
groups=[CampaignShareTargetItem(id=item.id, name=item.name, secondary=item.slug) for item in groups],
groups=[CampaignShareTargetItem(id=item.id, name=item.name, secondary=None) for item in groups],
)
@@ -1574,12 +1573,17 @@ def update_campaign_owner(
campaign = _get_campaign_for_principal(session, campaign_id, principal, write=True)
if payload.owner_user_id and payload.owner_group_id:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Choose either a user owner or a group owner, not both")
directory = _access_directory()
if payload.owner_user_id:
owner = session.query(User).filter(User.id == payload.owner_user_id, User.tenant_id == principal.tenant_id, User.is_active.is_(True)).one_or_none()
owner = directory.get_user(payload.owner_user_id)
if owner is not None and (owner.tenant_id != principal.tenant_id or owner.status != "active"):
owner = None
if owner is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Owner user not found")
if payload.owner_group_id:
group = session.query(Group).filter(Group.id == payload.owner_group_id, Group.tenant_id == principal.tenant_id, Group.is_active.is_(True)).one_or_none()
group = directory.get_group(payload.owner_group_id)
if group is not None and (group.tenant_id != principal.tenant_id or group.status != "active"):
group = None
if group is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Owner group not found")
campaign.owner_user_id = payload.owner_user_id
@@ -1597,10 +1601,15 @@ def upsert_campaign_share(
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:share")),
):
campaign = _get_campaign_for_principal(session, campaign_id, principal, write=True)
directory = _access_directory()
if payload.target_type == "user":
target = session.query(User).filter(User.id == payload.target_id, User.tenant_id == principal.tenant_id, User.is_active.is_(True)).one_or_none()
target = directory.get_user(payload.target_id)
if target is not None and (target.tenant_id != principal.tenant_id or target.status != "active"):
target = None
else:
target = session.query(Group).filter(Group.id == payload.target_id, Group.tenant_id == principal.tenant_id, Group.is_active.is_(True)).one_or_none()
target = directory.get_group(payload.target_id)
if target is not None and (target.tenant_id != principal.tenant_id or target.status != "active"):
target = None
if target is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Share target not found")
share = (