attachment backend use
This commit is contained in:
@@ -211,16 +211,29 @@ def _base_path_by_path(config: CampaignConfig, rendered_base_dir: str) -> Attach
|
||||
return None
|
||||
|
||||
|
||||
def _base_path_by_id(config: CampaignConfig, base_path_id: str | None) -> AttachmentBasePathConfig | None:
|
||||
if not base_path_id:
|
||||
return None
|
||||
return next((base_path for base_path in config.attachments.base_paths if base_path.id == base_path_id), None)
|
||||
|
||||
|
||||
def _default_base_path(config: CampaignConfig) -> AttachmentBasePathConfig:
|
||||
return config.attachments.base_paths[0]
|
||||
|
||||
|
||||
def _selected_base_path(config: CampaignConfig, rendered_base_dir: str) -> AttachmentBasePathConfig | None:
|
||||
if config.attachments.base_paths:
|
||||
if rendered_base_dir in {"", "."}:
|
||||
return _default_base_path(config)
|
||||
return _base_path_by_path(config, rendered_base_dir)
|
||||
return None
|
||||
def _selected_base_path(
|
||||
config: CampaignConfig,
|
||||
attachment_config: AttachmentConfig,
|
||||
rendered_base_dir: str,
|
||||
) -> AttachmentBasePathConfig | None:
|
||||
if not config.attachments.base_paths:
|
||||
return None
|
||||
selected_by_id = _base_path_by_id(config, attachment_config.base_path_id)
|
||||
if selected_by_id is not None:
|
||||
return selected_by_id
|
||||
if rendered_base_dir in {"", "."}:
|
||||
return _default_base_path(config)
|
||||
return _base_path_by_path(config, rendered_base_dir)
|
||||
|
||||
|
||||
def _rule_allows_multiple(config: AttachmentConfig, rendered_file_filter: str) -> bool:
|
||||
@@ -248,9 +261,9 @@ def _ambiguous_behavior(campaign_config: CampaignConfig, config: AttachmentConfi
|
||||
|
||||
def _entry_attachment_allowed(config: CampaignConfig, attachment_config: AttachmentConfig, values: dict[str, Any]) -> bool:
|
||||
rendered_base_dir = _rendered_base_dir(attachment_config, values)
|
||||
individual_paths = config.attachments.individual_base_path_values
|
||||
if individual_paths:
|
||||
return rendered_base_dir in individual_paths
|
||||
selected = _selected_base_path(config, attachment_config, rendered_base_dir)
|
||||
if config.attachments.base_paths:
|
||||
return bool(selected and selected.allow_individual)
|
||||
return config.attachments.allow_individual
|
||||
|
||||
|
||||
@@ -271,6 +284,7 @@ def _resolve_attachment_directory(
|
||||
*,
|
||||
campaign_file: str | Path,
|
||||
campaign_config: CampaignConfig,
|
||||
attachment_config: AttachmentConfig,
|
||||
rendered_base_dir: str,
|
||||
) -> tuple[Path, AttachmentBasePathConfig | None]:
|
||||
"""Resolve the directory for an attachment rule.
|
||||
@@ -281,7 +295,7 @@ def _resolve_attachment_directory(
|
||||
e.g. attachments/base_path + base_dir twice.
|
||||
"""
|
||||
|
||||
selected_base_path = _selected_base_path(campaign_config, rendered_base_dir)
|
||||
selected_base_path = _selected_base_path(campaign_config, attachment_config, rendered_base_dir)
|
||||
if selected_base_path is not None:
|
||||
return _resolve_path(campaign_file, selected_base_path.path), selected_base_path
|
||||
|
||||
@@ -337,6 +351,7 @@ def _resolve_one_config(
|
||||
directory, selected_base_path = _resolve_attachment_directory(
|
||||
campaign_file=campaign_file,
|
||||
campaign_config=campaign_config,
|
||||
attachment_config=config,
|
||||
rendered_base_dir=rendered_base_dir,
|
||||
)
|
||||
matches = _match_files(directory, rendered_file_filter, config.include_subdirs)
|
||||
|
||||
@@ -223,6 +223,7 @@ class AttachmentBasePathConfig(StrictModel):
|
||||
class AttachmentConfig(StrictModel):
|
||||
id: str | None = None
|
||||
label: str | None = None
|
||||
base_path_id: str | None = None
|
||||
# Legacy UI helper. Current attachment resolution ignores this value and
|
||||
# treats direct files as plain file_filter patterns without wildcards.
|
||||
# Keep accepting it so existing drafts with {"type": ""}, "direct"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from email import policy
|
||||
from email.message import EmailMessage
|
||||
from pathlib import Path
|
||||
@@ -14,6 +12,10 @@ from app.mailer.campaign.loader import load_campaign_config
|
||||
from app.mailer.campaign.validation import validate_campaign_config
|
||||
from app.mailer.messages.builder import build_campaign_messages
|
||||
from app.mailer.messages.models import MessageAddress, MessageDraft, MessageValidationStatus
|
||||
from app.storage.campaign_attachments import (
|
||||
annotate_built_messages_with_managed_files,
|
||||
prepared_campaign_snapshot,
|
||||
)
|
||||
from app.mailer.dev.mock_mailbox import (
|
||||
clear_records,
|
||||
consume_fail_next_imap,
|
||||
@@ -147,13 +149,18 @@ def run_mock_campaign_send(
|
||||
if clear_mailbox:
|
||||
clear_records()
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="multimailer-mock-send-") as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
campaign_path = temp_path / f"campaign-{version.id}.json"
|
||||
campaign_path.write_text(json.dumps(version.raw_json, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
config = load_campaign_config(campaign_path)
|
||||
validation_report = validate_campaign_config(config, campaign_file=campaign_path, check_files=check_files)
|
||||
build_result = build_campaign_messages(config, campaign_file=campaign_path, write_eml=False)
|
||||
with prepared_campaign_snapshot(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign.id,
|
||||
raw_json=version.raw_json if isinstance(version.raw_json, dict) else {},
|
||||
include_bytes=True,
|
||||
prefix="multimailer-mock-send-",
|
||||
) as prepared:
|
||||
config = load_campaign_config(prepared.path)
|
||||
validation_report = validate_campaign_config(config, campaign_file=prepared.path, check_files=check_files)
|
||||
build_result = build_campaign_messages(config, campaign_file=prepared.path, write_eml=False)
|
||||
annotate_built_messages_with_managed_files(build_result.built_messages, prepared.managed_files_by_local_path)
|
||||
|
||||
send_results: list[dict[str, Any]] = []
|
||||
sent_count = 0
|
||||
|
||||
@@ -57,6 +57,7 @@ class MessageAttachmentSummary(BaseModel):
|
||||
file_filter: str
|
||||
directory: str
|
||||
matches: list[str] = Field(default_factory=list)
|
||||
managed_matches: list[dict[str, object]] = Field(default_factory=list)
|
||||
|
||||
|
||||
class MessageDraft(BaseModel):
|
||||
|
||||
@@ -26,6 +26,10 @@ from app.mailer.campaign.validation import Severity, validate_campaign_config
|
||||
from app.mailer.messages.builder import build_campaign_messages
|
||||
from app.mailer.messages.models import MessageDraft
|
||||
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,
|
||||
)
|
||||
|
||||
RUNTIME_DIR = Path(__file__).resolve().parents[3] / "runtime"
|
||||
CAMPAIGN_SNAPSHOT_DIR = RUNTIME_DIR / "campaign_snapshots"
|
||||
@@ -195,7 +199,19 @@ def validate_campaign_version(
|
||||
}:
|
||||
raise CampaignPersistenceError("Audit-safe/final campaign versions cannot be validated. Create an editable copy instead.")
|
||||
|
||||
report = validate_campaign_config(config, campaign_file=snapshot_path, check_files=check_files)
|
||||
if check_files:
|
||||
with prepared_campaign_snapshot(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign.id,
|
||||
raw_json=version.raw_json if isinstance(version.raw_json, dict) else {},
|
||||
include_bytes=False,
|
||||
prefix="multimailer-managed-validate-",
|
||||
) as prepared:
|
||||
managed_config = load_campaign_config(prepared.path)
|
||||
report = validate_campaign_config(managed_config, campaign_file=prepared.path, check_files=True)
|
||||
else:
|
||||
report = validate_campaign_config(config, campaign_file=snapshot_path, check_files=False)
|
||||
report_json = report.model_dump(mode="json")
|
||||
report_json.update({"ok": report.ok, "error_count": report.error_count, "warning_count": report.warning_count})
|
||||
version.validation_summary = report_json
|
||||
@@ -298,7 +314,17 @@ def build_campaign_version(
|
||||
_ensure_version_validated_and_locked(version)
|
||||
|
||||
output_dir = BUILD_OUTPUT_DIR / campaign.id / version.id
|
||||
result = build_campaign_messages(config, campaign_file=snapshot_path, output_dir=output_dir, write_eml=write_eml)
|
||||
with prepared_campaign_snapshot(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign.id,
|
||||
raw_json=version.raw_json if isinstance(version.raw_json, dict) else {},
|
||||
include_bytes=True,
|
||||
prefix="multimailer-managed-build-",
|
||||
) as prepared:
|
||||
managed_config = load_campaign_config(prepared.path)
|
||||
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)
|
||||
report_json.update({
|
||||
"built_count": result.report.built_count,
|
||||
|
||||
@@ -648,6 +648,13 @@
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"base_path_id": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "Stable reference to attachments.base_paths[].id. Preferred over path matching when multiple managed spaces use the same logical path."
|
||||
},
|
||||
"base_dir": {
|
||||
"type": "string",
|
||||
"description": "Selected attachment base path for current WebUI campaigns, or a directory relative to attachments.base_path for legacy campaigns."
|
||||
|
||||
Reference in New Issue
Block a user