feat: harden campaign delivery and editing

This commit is contained in:
2026-07-30 14:27:04 +02:00
parent 4a120e8009
commit cd223cbb95
34 changed files with 3370 additions and 165 deletions
@@ -7,7 +7,12 @@ from typing import Any
from uuid import uuid4
from sqlalchemy.orm import Session
from sqlalchemy.orm.exc import StaleDataError
from govoplan_core.core.concurrency import (
RevisionConflictError,
strong_resource_etag,
)
from govoplan_campaign.backend.db.models import (
Campaign,
CampaignIssue,
@@ -720,12 +725,31 @@ def update_campaign_version(
source_base_path: str | None = None,
autosave: bool = False,
migrate_legacy_mail_settings: bool = False,
expected_revision: int | None = None,
commit: bool = True,
) -> CampaignVersion:
_assert_update_paths_safe(raw_json, source_filename=source_filename, source_base_path=source_base_path)
version = get_campaign_version_for_tenant(session, tenant_id=tenant_id, campaign_id=campaign_id, version_id=version_id)
campaign = _require_campaign(session, campaign_id)
ensure_current_working_version(campaign, version, action="edit")
if (
expected_revision is not None
and version.edit_revision != int(expected_revision)
):
raise RevisionConflictError(
resource_type="campaign_version",
resource_id=version.id,
current_revision=version.edit_revision,
submitted_base_revision=int(expected_revision),
refresh_path=(
f"/api/v1/campaigns/{campaign.id}/versions/{version.id}"
),
current_etag=strong_resource_etag(
"campaign_version",
version.id,
version.edit_revision,
),
)
if is_version_locked(version):
raise LockedCampaignVersionError(
@@ -761,7 +785,42 @@ def update_campaign_version(
# Changes invalidate previous build and validation summaries.
if raw_json is not None:
_invalidate_version_content(session, campaign=campaign, version=version)
_persist_updated_version(session, campaign=campaign, version=version, commit=commit)
try:
_persist_updated_version(
session,
campaign=campaign,
version=version,
commit=commit,
)
except StaleDataError as exc:
session.rollback()
current_revision = (
session.query(CampaignVersion.edit_revision)
.filter(CampaignVersion.id == version_id)
.scalar()
)
if current_revision is None:
raise CampaignPersistenceError(
f"Campaign version not found: {version_id}"
) from exc
raise RevisionConflictError(
resource_type="campaign_version",
resource_id=version_id,
current_revision=int(current_revision),
submitted_base_revision=int(
expected_revision
if expected_revision is not None
else version.edit_revision
),
refresh_path=(
f"/api/v1/campaigns/{campaign_id}/versions/{version_id}"
),
current_etag=strong_resource_etag(
"campaign_version",
version_id,
int(current_revision),
),
) from exc
return version