Release v0.1.2
This commit is contained in:
@@ -31,13 +31,7 @@ from govoplan_campaign.backend.messages.builder import build_campaign_messages
|
||||
from govoplan_campaign.backend.messages.models import MessageDraft
|
||||
from govoplan_campaign.backend.sending.execution import create_execution_snapshot
|
||||
from govoplan_campaign.backend.campaign.models import CampaignConfig
|
||||
from govoplan_mail.backend.mail_profiles import materialize_campaign_mail_profile_config
|
||||
from govoplan_files.backend.storage.campaign_usage import record_campaign_attachment_uses_for_jobs
|
||||
from govoplan_files.backend.storage.campaign_attachments import (
|
||||
annotate_built_messages_with_managed_files,
|
||||
prepared_campaign_snapshot,
|
||||
public_attachment_summary_payload,
|
||||
)
|
||||
from govoplan_campaign.backend.integrations import files_integration, mail_integration
|
||||
|
||||
RUNTIME_DIR = Path(__file__).resolve().parents[3] / "runtime"
|
||||
CAMPAIGN_SNAPSHOT_DIR = RUNTIME_DIR / "campaign_snapshots"
|
||||
@@ -64,7 +58,7 @@ def load_campaign_config_from_json(
|
||||
owner_user_id: str | None = None,
|
||||
owner_group_id: str | None = None,
|
||||
) -> CampaignConfig:
|
||||
materialized = materialize_campaign_mail_profile_config(
|
||||
materialized = mail_integration().materialize_campaign_mail_profile_config(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
raw_json=raw_json,
|
||||
@@ -274,7 +268,8 @@ def validate_campaign_version(
|
||||
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(
|
||||
files = files_integration()
|
||||
with files.prepared_campaign_snapshot(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign.id,
|
||||
@@ -379,7 +374,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=[public_attachment_summary_payload(item) for item in message.attachments],
|
||||
resolved_attachments=[files_integration().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,
|
||||
)
|
||||
@@ -405,7 +400,8 @@ def build_campaign_version(
|
||||
_ensure_version_validated_and_locked(version)
|
||||
|
||||
output_dir = BUILD_OUTPUT_DIR / campaign.id / version.id
|
||||
with prepared_campaign_snapshot(
|
||||
files = files_integration()
|
||||
with files.prepared_campaign_snapshot(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign.id,
|
||||
@@ -416,11 +412,11 @@ def build_campaign_version(
|
||||
managed_raw = load_campaign_json(prepared.path)
|
||||
managed_config = load_campaign_config_from_json(session, tenant_id=tenant_id, raw_json=managed_raw, campaign_id=campaign.id)
|
||||
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)
|
||||
files.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]
|
||||
message_payload["attachments"] = [files.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({
|
||||
@@ -459,17 +455,18 @@ def build_campaign_version(
|
||||
# records in bulk. This avoids one flush plus several metadata queries per
|
||||
# recipient for large campaigns.
|
||||
session.flush()
|
||||
record_campaign_attachment_uses_for_jobs(
|
||||
files.record_campaign_attachment_uses_for_jobs(
|
||||
session,
|
||||
[job for job, _message in job_build_pairs],
|
||||
stage="built",
|
||||
)
|
||||
if not managed_config.server.smtp:
|
||||
runtime_smtp = managed_config.server.runtime_smtp_config()
|
||||
if not runtime_smtp:
|
||||
raise CampaignPersistenceError("Campaign has no SMTP configuration; an execution snapshot cannot be created")
|
||||
execution_snapshot, execution_snapshot_hash = create_execution_snapshot(
|
||||
version,
|
||||
smtp=managed_config.server.smtp,
|
||||
imap=managed_config.server.imap,
|
||||
smtp=runtime_smtp,
|
||||
imap=managed_config.server.runtime_imap_config(),
|
||||
delivery=managed_config.delivery,
|
||||
jobs=[job for job, _message in job_build_pairs],
|
||||
build_summary=report_json,
|
||||
|
||||
@@ -18,7 +18,7 @@ from govoplan_campaign.backend.db.models import (
|
||||
JobSendStatus,
|
||||
)
|
||||
from govoplan_campaign.backend.sending.execution import clear_execution_snapshot
|
||||
from govoplan_mail.backend.mail_profiles import assert_campaign_mail_policy_allows_json
|
||||
from govoplan_campaign.backend.integrations import mail_integration
|
||||
from govoplan_campaign.backend.persistence.campaigns import (
|
||||
CampaignPersistenceError,
|
||||
_next_version_number,
|
||||
@@ -77,19 +77,18 @@ def minimal_campaign_json(*, external_id: str, name: str, description: str | Non
|
||||
"smtp": {
|
||||
"host": "",
|
||||
"port": 587,
|
||||
"username": "",
|
||||
"password": "",
|
||||
"security": "starttls",
|
||||
},
|
||||
"imap": {
|
||||
"enabled": False,
|
||||
"host": "",
|
||||
"port": 993,
|
||||
"username": "",
|
||||
"password": "",
|
||||
"security": "tls",
|
||||
"sent_folder": "auto",
|
||||
},
|
||||
"credentials": {
|
||||
"smtp": {"username": "", "password": ""},
|
||||
"imap": {"username": "", "password": ""},
|
||||
},
|
||||
},
|
||||
"recipients": {
|
||||
"from": [],
|
||||
@@ -339,7 +338,7 @@ def fork_campaign_version_for_edit(
|
||||
|
||||
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)
|
||||
assert_campaign_mail_policy_allows_json(session, tenant_id=tenant_id, raw_json=runtime_json, campaign_id=campaign.id)
|
||||
mail_integration().assert_campaign_mail_policy_allows_json(session, tenant_id=tenant_id, raw_json=runtime_json, campaign_id=campaign.id)
|
||||
|
||||
new_version = CampaignVersion(
|
||||
campaign_id=campaign.id,
|
||||
@@ -502,7 +501,7 @@ def update_campaign_version(
|
||||
|
||||
if raw_json is not None:
|
||||
runtime_json = normalize_campaign_paths(raw_json, source_base_path) if source_base_path else copy.deepcopy(raw_json)
|
||||
assert_campaign_mail_policy_allows_json(session, tenant_id=tenant_id, raw_json=runtime_json, campaign_id=campaign.id)
|
||||
mail_integration().assert_campaign_mail_policy_allows_json(session, tenant_id=tenant_id, raw_json=runtime_json, campaign_id=campaign.id)
|
||||
version.raw_json = runtime_json
|
||||
version.schema_version = str(runtime_json.get("version", version.schema_version or "1.0"))
|
||||
_apply_campaign_metadata(campaign, runtime_json)
|
||||
@@ -796,9 +795,22 @@ def validate_campaign_partial(raw_json: dict[str, Any], *, section: str | None =
|
||||
issue("warning", "recipients", "entries.mapping", "missing_email_mapping", "No email field mapping is configured.")
|
||||
|
||||
template = raw_json.get("template") if isinstance(raw_json.get("template"), dict) else {}
|
||||
if not template.get("subject") and not (isinstance(template.get("source"), dict) and template["source"].get("subject_path")):
|
||||
source_template = template.get("source") if isinstance(template.get("source"), dict) else {}
|
||||
if not template.get("subject") and not source_template.get("subject_path"):
|
||||
issue("warning", "template", "template.subject", "missing_subject", "Template subject is empty.")
|
||||
if not template.get("text") and not template.get("html") and not isinstance(template.get("source"), dict):
|
||||
explicit_body_mode = template.get("body_mode") if template.get("body_mode") in {"text", "html", "both"} else None
|
||||
has_text_body = bool(template.get("text")) or bool(source_template.get("text_path"))
|
||||
has_html_body = bool(template.get("html")) or bool(source_template.get("html_path"))
|
||||
if explicit_body_mode == "text" and not has_text_body:
|
||||
issue("warning", "template", "template.text", "missing_template_text_body", "Template body mode is text only, but no text body is configured.")
|
||||
elif explicit_body_mode == "html" and not has_html_body:
|
||||
issue("warning", "template", "template.html", "missing_template_html_body", "Template body mode is HTML only, but no HTML body is configured.")
|
||||
elif explicit_body_mode == "both":
|
||||
if not has_text_body:
|
||||
issue("warning", "template", "template.text", "missing_template_text_body", "Template body mode is both, but no text body is configured.")
|
||||
if not has_html_body:
|
||||
issue("warning", "template", "template.html", "missing_template_html_body", "Template body mode is both, but no HTML body is configured.")
|
||||
elif not has_text_body and not has_html_body and not source_template:
|
||||
issue("warning", "template", "template", "missing_template_body", "No text, HTML or file-based template body configured yet.")
|
||||
|
||||
attachments = raw_json.get("attachments") if isinstance(raw_json.get("attachments"), dict) else {}
|
||||
|
||||
Reference in New Issue
Block a user