Only one writeable campaign version at a time
This commit is contained in:
@@ -257,6 +257,36 @@ def is_version_locked(version: CampaignVersion) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def ensure_current_working_version(campaign: Campaign, version: CampaignVersion, *, action: str = "modify") -> None:
|
||||
"""Require the campaign's single active working version.
|
||||
|
||||
Historical versions remain reviewable, but they never become writable in
|
||||
place. Continuing from immutable history must create a new working copy,
|
||||
and that copy becomes the campaign's sole current version.
|
||||
"""
|
||||
|
||||
if campaign.current_version_id != version.id:
|
||||
raise LockedCampaignVersionError(
|
||||
f"Historical campaign versions are read-only and cannot be used to {action}. "
|
||||
"Open the current working version instead."
|
||||
)
|
||||
|
||||
|
||||
def campaign_has_active_working_version(session: Session, campaign: Campaign) -> bool:
|
||||
"""Return True while the campaign already has a non-final working version.
|
||||
|
||||
Validation locks and temporary user locks are still the same working
|
||||
version; they must be unlocked rather than forked into parallel drafts.
|
||||
"""
|
||||
|
||||
if not campaign.current_version_id:
|
||||
return False
|
||||
current = session.get(CampaignVersion, campaign.current_version_id)
|
||||
if not current or current.campaign_id != campaign.id:
|
||||
return False
|
||||
return not is_audit_safe_version(current)
|
||||
|
||||
|
||||
def _apply_campaign_metadata(campaign: Campaign, raw_json: dict[str, Any]) -> None:
|
||||
campaign_meta = raw_json.get("campaign") if isinstance(raw_json.get("campaign"), dict) else {}
|
||||
if campaign_meta:
|
||||
@@ -279,17 +309,30 @@ def fork_campaign_version_for_edit(
|
||||
source_base_path: str | None = None,
|
||||
autosave: bool = True,
|
||||
) -> CampaignVersion:
|
||||
"""Create a new editable working version from a locked/validated version.
|
||||
"""Create the next sole working version from immutable campaign history.
|
||||
|
||||
This preserves the audit value of the validated/sent version while allowing
|
||||
users to continue editing a campaign. New content starts with the supplied
|
||||
raw_json when provided, otherwise with a clone of the source version.
|
||||
Validation and temporary user locks are still the active working version
|
||||
and must be unlocked in place. A copy is allowed only once the current
|
||||
version is permanently user-locked or delivery-final.
|
||||
"""
|
||||
|
||||
source = get_campaign_version_for_tenant(session, tenant_id=tenant_id, campaign_id=campaign_id, version_id=version_id)
|
||||
campaign = session.get(Campaign, campaign_id)
|
||||
assert campaign is not None
|
||||
|
||||
if campaign_has_active_working_version(session, campaign):
|
||||
current = session.get(CampaignVersion, campaign.current_version_id)
|
||||
current_number = current.version_number if current else "current"
|
||||
raise LockedCampaignVersionError(
|
||||
f"Campaign already has active working version #{current_number}. "
|
||||
"Unlock or continue editing that version instead of creating a parallel draft."
|
||||
)
|
||||
if campaign.current_version_id and source.id != campaign.current_version_id:
|
||||
raise LockedCampaignVersionError(
|
||||
"Historical versions remain review-only and cannot become a new branch. "
|
||||
"Create the next working copy from the campaign's current immutable version."
|
||||
)
|
||||
|
||||
base_json = raw_json if raw_json is not None else copy.deepcopy(source.raw_json)
|
||||
runtime_json = normalize_campaign_paths(base_json, source_base_path) if source_base_path else copy.deepcopy(base_json)
|
||||
|
||||
@@ -381,6 +424,7 @@ def unlock_validated_campaign_version(
|
||||
version = get_campaign_version_for_tenant(session, tenant_id=tenant_id, campaign_id=campaign_id, version_id=version_id)
|
||||
campaign = session.get(Campaign, campaign_id)
|
||||
assert campaign is not None
|
||||
ensure_current_working_version(campaign, version, action="unlock")
|
||||
|
||||
if is_temporary_user_locked_version(version):
|
||||
raise LockedCampaignVersionError("This version has a temporary user lock. Remove that lock before unlocking validation.")
|
||||
@@ -440,6 +484,7 @@ def update_campaign_version(
|
||||
version = get_campaign_version_for_tenant(session, tenant_id=tenant_id, campaign_id=campaign_id, version_id=version_id)
|
||||
campaign = session.get(Campaign, campaign_id)
|
||||
assert campaign is not None
|
||||
ensure_current_working_version(campaign, version, action="edit")
|
||||
|
||||
if is_version_locked(version):
|
||||
raise LockedCampaignVersionError(
|
||||
@@ -511,6 +556,9 @@ def update_campaign_review_state(
|
||||
campaign_id=campaign_id,
|
||||
version_id=version_id,
|
||||
)
|
||||
campaign = session.get(Campaign, campaign_id)
|
||||
assert campaign is not None
|
||||
ensure_current_working_version(campaign, version, action="record review state for")
|
||||
if is_version_final_locked(version):
|
||||
raise LockedCampaignVersionError("Delivery has started; message review state can no longer be changed.")
|
||||
build_summary = version.build_summary if isinstance(version.build_summary, dict) else {}
|
||||
@@ -555,6 +603,9 @@ def lock_campaign_version_temporarily(
|
||||
campaign_id=campaign_id,
|
||||
version_id=version_id,
|
||||
)
|
||||
campaign = session.get(Campaign, campaign_id)
|
||||
assert campaign is not None
|
||||
ensure_current_working_version(campaign, version, action="lock")
|
||||
if is_version_final_locked(version):
|
||||
raise LockedCampaignVersionError("Delivery/final versions are permanently locked and cannot receive a temporary user lock.")
|
||||
if is_permanent_user_locked_version(version):
|
||||
@@ -587,6 +638,9 @@ def unlock_user_locked_campaign_version(
|
||||
campaign_id=campaign_id,
|
||||
version_id=version_id,
|
||||
)
|
||||
campaign = session.get(Campaign, campaign_id)
|
||||
assert campaign is not None
|
||||
ensure_current_working_version(campaign, version, action="unlock")
|
||||
state = campaign_version_user_lock_state(version)
|
||||
if state == USER_LOCK_PERMANENT:
|
||||
raise LockedCampaignVersionError("Permanently locked versions cannot be unlocked. Create an editable copy instead.")
|
||||
@@ -623,6 +677,9 @@ def permanently_lock_campaign_version(
|
||||
campaign_id=campaign_id,
|
||||
version_id=version_id,
|
||||
)
|
||||
campaign = session.get(Campaign, campaign_id)
|
||||
assert campaign is not None
|
||||
ensure_current_working_version(campaign, version, action="lock permanently")
|
||||
if is_version_final_locked(version):
|
||||
raise LockedCampaignVersionError("This version is already permanently locked by its delivery/final state.")
|
||||
if is_permanent_user_locked_version(version):
|
||||
|
||||
Reference in New Issue
Block a user