campaign version refinment, user locks, db repair
This commit is contained in:
@@ -4,6 +4,8 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
import copy
|
||||
from datetime import UTC, datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -29,6 +31,7 @@ from app.storage.services import record_campaign_attachment_uses_for_job
|
||||
from app.storage.campaign_attachments import (
|
||||
annotate_built_messages_with_managed_files,
|
||||
prepared_campaign_snapshot,
|
||||
public_attachment_summary_payload,
|
||||
)
|
||||
|
||||
RUNTIME_DIR = Path(__file__).resolve().parents[3] / "runtime"
|
||||
@@ -154,8 +157,15 @@ def create_campaign_version_from_json(
|
||||
|
||||
|
||||
|
||||
def _version_user_lock_state(version: CampaignVersion) -> str | None:
|
||||
state = getattr(version, "user_lock_state", None)
|
||||
if state in {"temporary", "permanent"}:
|
||||
return state
|
||||
return "permanent" if version.published_at else None
|
||||
|
||||
|
||||
def _version_is_user_locked(version: CampaignVersion) -> bool:
|
||||
return bool(version.published_at)
|
||||
return _version_user_lock_state(version) is not None
|
||||
|
||||
|
||||
def _version_is_validated_and_locked(version: CampaignVersion) -> bool:
|
||||
@@ -164,8 +174,11 @@ def _version_is_validated_and_locked(version: CampaignVersion) -> bool:
|
||||
|
||||
|
||||
def _ensure_version_validated_and_locked(version: CampaignVersion) -> None:
|
||||
if _version_is_user_locked(version):
|
||||
raise CampaignPersistenceError("User-locked audit-safe versions cannot be built, queued, dry-run or sent. Create an editable copy instead.")
|
||||
state = _version_user_lock_state(version)
|
||||
if state == "temporary":
|
||||
raise CampaignPersistenceError("This version has a temporary user lock. Unlock it before building, queueing, dry-run or sending.")
|
||||
if state == "permanent":
|
||||
raise CampaignPersistenceError("This version is permanently user-locked. Create an editable copy instead.")
|
||||
if not _version_is_validated_and_locked(version):
|
||||
raise CampaignPersistenceError("Campaign version must be validated and locked before building, queueing, dry-run or sending.")
|
||||
|
||||
@@ -190,14 +203,15 @@ def validate_campaign_version(
|
||||
campaign = session.get(Campaign, version.campaign_id)
|
||||
if not campaign or campaign.tenant_id != tenant_id:
|
||||
raise CampaignPersistenceError("Campaign version is not accessible for this tenant")
|
||||
if version.published_at or version.workflow_state in {
|
||||
if _version_is_user_locked(version) or version.workflow_state in {
|
||||
CampaignVersionWorkflowState.QUEUED.value,
|
||||
CampaignVersionWorkflowState.SENDING.value,
|
||||
CampaignVersionWorkflowState.COMPLETED.value,
|
||||
CampaignVersionWorkflowState.CANCELLED.value,
|
||||
CampaignVersionWorkflowState.ARCHIVED.value,
|
||||
}:
|
||||
raise CampaignPersistenceError("Audit-safe/final campaign versions cannot be validated. Create an editable copy instead.")
|
||||
lock_label = "temporarily user-locked" if _version_user_lock_state(version) == "temporary" else "permanently locked/final"
|
||||
raise CampaignPersistenceError(f"{lock_label.capitalize()} campaign versions cannot be validated. Unlock or create an editable copy instead.")
|
||||
|
||||
if check_files:
|
||||
with prepared_campaign_snapshot(
|
||||
@@ -289,7 +303,7 @@ def _job_from_message(
|
||||
"bounce_to": [item.model_dump(mode="json") for item in message.bounce_to],
|
||||
"disposition_notification_to": [item.model_dump(mode="json") for item in message.disposition_notification_to],
|
||||
},
|
||||
resolved_attachments=[item.model_dump(mode="json") for item in message.attachments],
|
||||
resolved_attachments=[public_attachment_summary_payload(item) for item in message.attachments],
|
||||
issues_snapshot=[item.model_dump(mode="json") for item in message.issues],
|
||||
last_error="; ".join(issue.message for issue in message.issues if issue.severity == "error") or None,
|
||||
)
|
||||
@@ -326,6 +340,11 @@ def build_campaign_version(
|
||||
result = build_campaign_messages(managed_config, campaign_file=prepared.path, output_dir=output_dir, write_eml=write_eml)
|
||||
annotate_built_messages_with_managed_files(result.built_messages, prepared.managed_files_by_local_path)
|
||||
report_json = result.report.model_dump(mode="json", by_alias=True)
|
||||
for message_payload, message in zip(report_json.get("messages", []), result.report.messages, strict=False):
|
||||
if isinstance(message_payload, dict):
|
||||
message_payload["attachments"] = [public_attachment_summary_payload(item) for item in message.attachments]
|
||||
report_json["built_at"] = datetime.now(UTC).isoformat()
|
||||
report_json["build_token"] = uuid4().hex
|
||||
report_json.update({
|
||||
"built_count": result.report.built_count,
|
||||
"build_failed_count": result.report.build_failed_count,
|
||||
@@ -338,6 +357,9 @@ def build_campaign_version(
|
||||
"queueable_count": result.report.queueable_count,
|
||||
})
|
||||
version.build_summary = report_json
|
||||
editor_state = copy.deepcopy(version.editor_state or {})
|
||||
editor_state.pop("review_send", None)
|
||||
version.editor_state = editor_state
|
||||
|
||||
# Rebuild jobs for the current version. Later, protect sent jobs from destructive rebuilds.
|
||||
session.query(CampaignIssue).filter(CampaignIssue.campaign_version_id == version.id, CampaignIssue.job_id.is_not(None)).delete(synchronize_session=False)
|
||||
|
||||
Reference in New Issue
Block a user