Release v0.1.2

This commit is contained in:
2026-06-25 19:58:20 +02:00
parent 15794e920e
commit 02564047e9
73 changed files with 4073 additions and 478 deletions

View File

@@ -11,8 +11,8 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
from sqlalchemy.orm import Session
from govoplan_core.admin.governance import get_system_settings
from govoplan_core.core.optional import reraise_unless_missing_package
from govoplan_core.db.models import AuditLog, Group, SystemSettings, Tenant, User
from govoplan_campaign.backend.db.models import Campaign, CampaignJob, CampaignVersion, JobImapStatus, JobQueueStatus, JobSendStatus
from govoplan_core.settings import settings
PRIVACY_POLICY_SETTINGS_KEY = "privacy_retention_policy"
@@ -57,11 +57,11 @@ FINAL_VERSION_STATES = {
"archived",
}
FINAL_EML_SEND_STATUSES = {
JobSendStatus.SMTP_ACCEPTED.value,
JobSendStatus.SENT.value,
JobSendStatus.FAILED_PERMANENT.value,
JobSendStatus.CANCELLED.value,
JobSendStatus.OUTCOME_UNKNOWN.value,
"smtp_accepted",
"sent",
"failed_permanent",
"cancelled",
"outcome_unknown",
}
AUDIT_DETAIL_REDACTION_KEYS = {
"attachments",
@@ -148,6 +148,22 @@ class PrivacyPolicyError(RuntimeError):
pass
def _campaign_models():
try:
from govoplan_campaign.backend.db.models import Campaign, CampaignJob, CampaignVersion, JobImapStatus, JobQueueStatus
except ModuleNotFoundError as exc:
reraise_unless_missing_package(exc, "govoplan_campaign")
raise PrivacyPolicyError("Campaign module is not installed") from exc
return Campaign, CampaignJob, CampaignVersion, JobImapStatus, JobQueueStatus
def _campaign_models_or_none():
try:
return _campaign_models()
except PrivacyPolicyError:
return None
def _utcnow() -> datetime:
return datetime.now(timezone.utc)
@@ -251,8 +267,9 @@ def effective_privacy_policy(
campaign_id: str | None = None,
) -> PrivacyRetentionPolicy:
policy = privacy_policy_from_session(session)
campaign: Campaign | None = None
campaign: Any | None = None
if campaign_id:
Campaign, _, _, _, _ = _campaign_models()
campaign = session.get(Campaign, campaign_id)
if campaign is None:
raise PrivacyPolicyError("Campaign not found for privacy policy")
@@ -290,6 +307,7 @@ def parent_privacy_policy(session: Session, *, tenant_id: str, scope_type: str,
return policy
if clean_scope != "campaign" or not scope_id:
return policy
Campaign, _, _, _, _ = _campaign_models()
campaign = session.get(Campaign, scope_id)
if campaign is None or campaign.tenant_id != tenant_id:
raise PrivacyPolicyError("Campaign not found for privacy policy")
@@ -318,11 +336,13 @@ def _retention_policy_source_fields(patch: dict[str, Any], *, baseline: bool = F
def _retention_policy_source_step(scope_type: str, label: str, scope_id: str | None, patch: dict[str, Any], *, baseline: bool = False) -> dict[str, Any]:
source_policy = PrivacyRetentionPolicy.model_validate(patch).model_dump(mode="json") if baseline else dict(patch)
return {
"scope_type": scope_type,
"scope_id": scope_id,
"label": label,
"applied_fields": _retention_policy_source_fields(patch, baseline=baseline),
"policy": source_policy,
}
@@ -336,8 +356,9 @@ def effective_privacy_policy_sources(
) -> list[dict[str, Any]]:
system_settings = get_system_settings(session)
sources = [_retention_policy_source_step("system", "System", None, _privacy_policy_patch_from_settings(system_settings.settings or {}), baseline=True)]
campaign: Campaign | None = None
campaign: Any | None = None
if campaign_id:
Campaign, _, _, _, _ = _campaign_models()
campaign = session.get(Campaign, campaign_id)
if campaign is None:
raise PrivacyPolicyError("Campaign not found for privacy policy")
@@ -378,6 +399,7 @@ def parent_privacy_policy_sources(session: Session, *, tenant_id: str, scope_typ
return sources
if clean_scope != "campaign" or not scope_id:
return sources
Campaign, _, _, _, _ = _campaign_models()
campaign = session.get(Campaign, scope_id)
if campaign is None or campaign.tenant_id != tenant_id:
raise PrivacyPolicyError("Campaign not found for privacy policy")
@@ -413,6 +435,7 @@ def get_privacy_policy_for_scope(session: Session, *, tenant_id: str, scope_type
raise PrivacyPolicyError("Group privacy policy not found")
return _privacy_policy_patch_from_settings(group.settings or {})
if clean_scope == "campaign":
Campaign, _, _, _, _ = _campaign_models()
campaign = session.get(Campaign, scope_id)
if campaign is None or campaign.tenant_id != tenant_id:
raise PrivacyPolicyError("Campaign privacy policy not found")
@@ -460,6 +483,7 @@ def set_privacy_policy_for_scope(
session.add(group)
return patch
if clean_scope == "campaign":
Campaign, _, _, _, _ = _campaign_models()
campaign = session.get(Campaign, scope_id)
if campaign is None or campaign.tenant_id != tenant_id:
raise PrivacyPolicyError("Campaign privacy policy not found")
@@ -542,6 +566,10 @@ def _campaign_policy_for_id(session: Session, campaign_id: str | None, cache: di
def _apply_raw_json_retention(session: Session, *, dry_run: bool, now: datetime) -> dict[str, int]:
result = {"eligible": 0, "redacted": 0, "skipped_not_final": 0, "already_redacted": 0}
models = _campaign_models_or_none()
if models is None:
return result
_, _, CampaignVersion, _, _ = models
policy_cache: dict[str, PrivacyRetentionPolicy] = {}
versions = (
session.query(CampaignVersion)
@@ -579,6 +607,10 @@ def _apply_raw_json_retention(session: Session, *, dry_run: bool, now: datetime)
def _apply_eml_retention(session: Session, *, dry_run: bool, now: datetime) -> dict[str, int]:
result = {"eligible": 0, "metadata_cleared": 0, "files_deleted": 0, "files_missing": 0, "skipped_not_final": 0}
models = _campaign_models_or_none()
if models is None:
return result
_, CampaignJob, _, JobImapStatus, JobQueueStatus = models
policy_cache: dict[str, PrivacyRetentionPolicy] = {}
jobs = (
session.query(CampaignJob)
@@ -616,6 +648,10 @@ def _apply_eml_retention(session: Session, *, dry_run: bool, now: datetime) -> d
def _apply_report_detail_retention(session: Session, *, dry_run: bool, now: datetime) -> dict[str, int]:
result = {"eligible_versions": 0, "summaries_redacted": 0, "already_redacted": 0}
models = _campaign_models_or_none()
if models is None:
return result
_, _, CampaignVersion, _, _ = models
policy_cache: dict[str, PrivacyRetentionPolicy] = {}
versions = (
session.query(CampaignVersion)
@@ -708,9 +744,12 @@ def _parse_datetime(value: Any) -> datetime | None:
def _privacy_policy_for_audit_item(session: Session, item: AuditLog, campaign_cache: dict[str, PrivacyRetentionPolicy], tenant_cache: dict[str, PrivacyRetentionPolicy]) -> PrivacyRetentionPolicy:
if item.object_type == "campaign" and item.object_id:
campaign = session.get(Campaign, str(item.object_id))
if campaign is not None:
return _campaign_policy_for_id(session, campaign.id, campaign_cache)
models = _campaign_models_or_none()
if models is not None:
Campaign, _, _, _, _ = models
campaign = session.get(Campaign, str(item.object_id))
if campaign is not None:
return _campaign_policy_for_id(session, campaign.id, campaign_cache)
if item.tenant_id:
if item.tenant_id not in tenant_cache:
tenant_cache[item.tenant_id] = effective_privacy_policy(session, tenant_id=item.tenant_id)