attachment backend use
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -38,7 +37,10 @@ from app.mailer.persistence.campaigns import (
|
||||
create_campaign_version_from_json,
|
||||
validate_campaign_version,
|
||||
)
|
||||
from app.storage.services import list_assets_for_user, resolve_patterns
|
||||
from app.storage.files import current_version_and_blob
|
||||
from app.storage.campaign_attachments import managed_match_payloads, prepared_campaign_snapshot
|
||||
from app.mailer.campaign.loader import load_campaign_config
|
||||
from app.mailer.attachments.resolver import resolve_campaign_attachments
|
||||
from app.mailer.persistence.versions import (
|
||||
LockedCampaignVersionError,
|
||||
create_minimal_campaign,
|
||||
@@ -969,6 +971,7 @@ def append_sent(
|
||||
|
||||
class CampaignAttachmentPreviewRequest(BaseModel):
|
||||
include_unmatched: bool = True
|
||||
campaign_json: dict[str, object] | None = None
|
||||
|
||||
|
||||
class CampaignAttachmentPreviewResponse(BaseModel):
|
||||
@@ -979,44 +982,19 @@ class CampaignAttachmentPreviewResponse(BaseModel):
|
||||
unused_shared_files: list[dict[str, object]] = Field(default_factory=list)
|
||||
|
||||
|
||||
_BRACE_PLACEHOLDER_RE = re.compile(r"(?<!\\)\{\{\s*(.*?)\s*\}\}")
|
||||
_DOLLAR_PLACEHOLDER_RE = re.compile(r"(?<!\\)\$\{(.*?)(?<!\\)\}")
|
||||
|
||||
|
||||
def _preview_render_template(template: str, *, global_values: dict[str, object], entry_fields: dict[str, object]) -> str:
|
||||
def value_for(raw_key: str) -> str:
|
||||
key = raw_key.strip()
|
||||
if key.startswith("local:"):
|
||||
value = entry_fields.get(key.removeprefix("local:"), "")
|
||||
elif key.startswith("local."):
|
||||
value = entry_fields.get(key.removeprefix("local."), "")
|
||||
elif key.startswith("global:"):
|
||||
value = global_values.get(key.removeprefix("global:"), "")
|
||||
elif key.startswith("global."):
|
||||
value = global_values.get(key.removeprefix("global."), "")
|
||||
else:
|
||||
value = entry_fields.get(key, global_values.get(key, ""))
|
||||
return "" if value is None else str(value)
|
||||
|
||||
rendered = _BRACE_PLACEHOLDER_RE.sub(lambda match: value_for(match.group(1)), template)
|
||||
return _DOLLAR_PLACEHOLDER_RE.sub(lambda match: value_for(match.group(1)), rendered)
|
||||
|
||||
|
||||
def _rule_pattern(rule: dict[str, object], base_path_names: set[str], *, global_values: dict[str, object], entry_fields: dict[str, object]) -> str:
|
||||
base_dir = _preview_render_template(str(rule.get("base_dir") or "."), global_values=global_values, entry_fields=entry_fields).strip().strip("/")
|
||||
file_filter = _preview_render_template(str(rule.get("file_filter") or "*"), global_values=global_values, entry_fields=entry_fields).strip() or "*"
|
||||
if not base_dir or base_dir == "." or base_dir in base_path_names:
|
||||
return file_filter
|
||||
return f"{base_dir}/{file_filter}"
|
||||
|
||||
|
||||
def _file_preview(asset) -> dict[str, object]:
|
||||
def _file_preview(session: Session, asset) -> dict[str, object]:
|
||||
version, blob = current_version_and_blob(session, asset)
|
||||
return {
|
||||
"id": asset.id,
|
||||
"version_id": version.id,
|
||||
"blob_id": blob.id,
|
||||
"display_path": asset.display_path,
|
||||
"filename": asset.filename,
|
||||
"owner_type": asset.owner_type,
|
||||
"owner_id": asset.owner_user_id if asset.owner_type == "user" else asset.owner_group_id,
|
||||
"checksum_sha256": blob.checksum_sha256,
|
||||
"size_bytes": blob.size_bytes,
|
||||
"content_type": blob.content_type,
|
||||
}
|
||||
|
||||
|
||||
@@ -1033,61 +1011,77 @@ def preview_campaign_attachments(
|
||||
if version.campaign_id != campaign.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Campaign version not found")
|
||||
|
||||
raw = version.raw_json if isinstance(version.raw_json, dict) else {}
|
||||
attachments = raw.get("attachments") if isinstance(raw.get("attachments"), dict) else {}
|
||||
base_paths = attachments.get("base_paths") if isinstance(attachments.get("base_paths"), list) else []
|
||||
base_path_names = {str(item.get("name")) for item in base_paths if isinstance(item, dict) and item.get("name")}
|
||||
global_values = raw.get("global_values") if isinstance(raw.get("global_values"), dict) else {}
|
||||
rules: list[dict[str, object]] = []
|
||||
payload = payload or CampaignAttachmentPreviewRequest()
|
||||
raw = payload.campaign_json if isinstance(payload.campaign_json, dict) else version.raw_json
|
||||
raw = raw if isinstance(raw, dict) else {}
|
||||
|
||||
global_rules = attachments.get("global") if isinstance(attachments.get("global"), list) else []
|
||||
for index, rule in enumerate(global_rules):
|
||||
if not isinstance(rule, dict):
|
||||
continue
|
||||
rules.append({
|
||||
"source": "global",
|
||||
"index": index,
|
||||
"label": rule.get("label"),
|
||||
"required": bool(rule.get("required", True)),
|
||||
"pattern": _rule_pattern(rule, base_path_names, global_values=global_values, entry_fields={}),
|
||||
})
|
||||
|
||||
entries = raw.get("entries") if isinstance(raw.get("entries"), dict) else {}
|
||||
inline_entries = entries.get("inline") if isinstance(entries.get("inline"), list) else []
|
||||
for entry_index, entry in enumerate(inline_entries, start=1):
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
entry_fields = entry.get("fields") if isinstance(entry.get("fields"), dict) else {}
|
||||
entry_rules = entry.get("attachments") if isinstance(entry.get("attachments"), list) else []
|
||||
for rule_index, rule in enumerate(entry_rules):
|
||||
if not isinstance(rule, dict):
|
||||
continue
|
||||
rules.append({
|
||||
"source": "entry",
|
||||
"entry_index": entry_index,
|
||||
"entry_id": entry.get("id"),
|
||||
"index": rule_index,
|
||||
"label": rule.get("label"),
|
||||
"required": bool(rule.get("required", True)),
|
||||
"pattern": _rule_pattern(rule, base_path_names, global_values=global_values, entry_fields=entry_fields),
|
||||
})
|
||||
|
||||
shared_assets = list_assets_for_user(
|
||||
with prepared_campaign_snapshot(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=principal.user.id,
|
||||
campaign_id=campaign.id,
|
||||
is_admin=principal.user.is_tenant_admin or "*" in set(principal.scopes or []),
|
||||
)
|
||||
resolved, unmatched = resolve_patterns(shared_assets, [str(rule["pattern"]) for rule in rules])
|
||||
for rule, result in zip(rules, resolved, strict=False):
|
||||
rule["matches"] = [_file_preview(asset) for asset in result.matches]
|
||||
rule["match_count"] = len(result.matches)
|
||||
raw_json=raw,
|
||||
include_bytes=False,
|
||||
prefix="multimailer-managed-preview-",
|
||||
) as prepared:
|
||||
config = load_campaign_config(prepared.path)
|
||||
report = resolve_campaign_attachments(config, campaign_file=prepared.path)
|
||||
rules: list[dict[str, object]] = []
|
||||
matched_asset_ids: set[str] = set()
|
||||
|
||||
for entry in report.entries:
|
||||
for attachment in entry.attachments:
|
||||
managed_matches = managed_match_payloads(attachment.matches, prepared.managed_files_by_local_path)
|
||||
matched_asset_ids.update(str(item["asset_id"]) for item in managed_matches)
|
||||
matches: list[dict[str, object]] = [
|
||||
{
|
||||
"id": item["asset_id"],
|
||||
"version_id": item["version_id"],
|
||||
"blob_id": item["blob_id"],
|
||||
"display_path": item["display_path"],
|
||||
"filename": item["filename"],
|
||||
"owner_type": item["owner_type"],
|
||||
"owner_id": item["owner_id"],
|
||||
"checksum_sha256": item["checksum_sha256"],
|
||||
"size_bytes": item["size_bytes"],
|
||||
"content_type": item["content_type"],
|
||||
}
|
||||
for item in managed_matches
|
||||
]
|
||||
if not matches:
|
||||
matches = [
|
||||
{
|
||||
"id": "",
|
||||
"display_path": match,
|
||||
"filename": match.rsplit("/", 1)[-1].rsplit("\\", 1)[-1],
|
||||
"owner_type": "legacy",
|
||||
"owner_id": "",
|
||||
}
|
||||
for match in attachment.matches
|
||||
]
|
||||
rules.append({
|
||||
"source": attachment.scope.value,
|
||||
"entry_index": entry.entry_index,
|
||||
"entry_id": entry.entry_id,
|
||||
"index": attachment.index,
|
||||
"attachment_id": attachment.attachment_id,
|
||||
"label": attachment.label,
|
||||
"required": attachment.required,
|
||||
"pattern": attachment.file_filter,
|
||||
"base_path_name": attachment.base_path_name,
|
||||
"base_path": attachment.base_path,
|
||||
"status": attachment.status.value,
|
||||
"behavior": attachment.behavior.value if attachment.behavior else None,
|
||||
"matches": matches,
|
||||
"match_count": len(matches),
|
||||
"issues": [issue.model_dump(mode="json") for issue in attachment.issues],
|
||||
})
|
||||
|
||||
unused = [asset for asset in prepared.shared_assets if asset.id not in matched_asset_ids]
|
||||
return CampaignAttachmentPreviewResponse(
|
||||
campaign_id=campaign.id,
|
||||
version_id=version.id,
|
||||
shared_file_count=len(prepared.shared_assets),
|
||||
rules=rules,
|
||||
unused_shared_files=[_file_preview(session, asset) for asset in unused] if payload.include_unmatched else [],
|
||||
)
|
||||
|
||||
return CampaignAttachmentPreviewResponse(
|
||||
campaign_id=campaign.id,
|
||||
version_id=version.id,
|
||||
shared_file_count=len(shared_assets),
|
||||
rules=rules,
|
||||
unused_shared_files=[_file_preview(asset) for asset in unmatched] if payload is None or payload.include_unmatched else [],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user