Add institutional provenance and approval gates
This commit is contained in:
@@ -3,7 +3,15 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, ValidationInfo, field_validator, model_validator
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
BeforeValidator,
|
||||
ConfigDict,
|
||||
Field,
|
||||
ValidationInfo,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
||||
from govoplan_campaign.backend.campaign.mail_profile_boundary import (
|
||||
@@ -25,8 +33,6 @@ class CampaignCreateRequest(BaseModel):
|
||||
source_base_path: str | None = None
|
||||
|
||||
|
||||
|
||||
|
||||
class CampaignUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
@@ -64,7 +70,9 @@ class CampaignVersionUpdateRequest(BaseModel):
|
||||
|
||||
@field_validator("editor_state")
|
||||
@classmethod
|
||||
def validate_editor_state(cls, value: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
def validate_editor_state(
|
||||
cls, value: dict[str, Any] | None
|
||||
) -> dict[str, Any] | None:
|
||||
return validate_campaign_editor_state(value) if value is not None else None
|
||||
|
||||
|
||||
@@ -129,12 +137,16 @@ class CampaignVersionResponse(BaseModel):
|
||||
build_summary: dict[str, Any] | None = None
|
||||
execution_snapshot_hash: str | None = None
|
||||
execution_snapshot_at: datetime | None = None
|
||||
delivery_mode: Literal["synchronous", "worker_queue", "database_queue"] | None = None
|
||||
delivery_mode: Literal["synchronous", "worker_queue", "database_queue"] | None = (
|
||||
None
|
||||
)
|
||||
delivery_mode_selected_at: datetime | None = None
|
||||
|
||||
@field_validator("editor_state", mode="before")
|
||||
@classmethod
|
||||
def remove_unsupported_editor_state(cls, value: Any, info: ValidationInfo) -> dict[str, Any]:
|
||||
def remove_unsupported_editor_state(
|
||||
cls, value: Any, info: ValidationInfo
|
||||
) -> dict[str, Any]:
|
||||
return public_campaign_editor_state(
|
||||
value,
|
||||
include_diagnostics=bool((info.context or {}).get("include_diagnostics")),
|
||||
@@ -291,7 +303,9 @@ class RecipientImportColumnMappingPayload(BaseModel):
|
||||
column_index: int = Field(ge=0, alias="columnIndex")
|
||||
kind: RecipientImportColumnKind
|
||||
field_name: str | None = Field(default=None, max_length=255, alias="fieldName")
|
||||
new_field_name: str | None = Field(default=None, max_length=255, alias="newFieldName")
|
||||
new_field_name: str | None = Field(
|
||||
default=None, max_length=255, alias="newFieldName"
|
||||
)
|
||||
|
||||
|
||||
class RecipientImportMappingProfilePayload(BaseModel):
|
||||
@@ -300,14 +314,22 @@ class RecipientImportMappingProfilePayload(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
column_count: int = Field(ge=0, le=500, alias="columnCount")
|
||||
headers: list[str] = Field(default_factory=list, max_length=500)
|
||||
normalized_headers: list[str] = Field(default_factory=list, max_length=500, alias="normalizedHeaders")
|
||||
ordered_header_fingerprint: str = Field(min_length=1, max_length=64, alias="orderedHeaderFingerprint")
|
||||
unordered_header_fingerprint: str = Field(min_length=1, max_length=64, alias="unorderedHeaderFingerprint")
|
||||
normalized_headers: list[str] = Field(
|
||||
default_factory=list, max_length=500, alias="normalizedHeaders"
|
||||
)
|
||||
ordered_header_fingerprint: str = Field(
|
||||
min_length=1, max_length=64, alias="orderedHeaderFingerprint"
|
||||
)
|
||||
unordered_header_fingerprint: str = Field(
|
||||
min_length=1, max_length=64, alias="unorderedHeaderFingerprint"
|
||||
)
|
||||
delimiter: Literal[",", ";", "\t"]
|
||||
header_rows: int = Field(ge=0, le=10, alias="headerRows")
|
||||
quoted: bool = True
|
||||
value_separators: str = Field(default=",;|", max_length=50, alias="valueSeparators")
|
||||
mappings: list[RecipientImportColumnMappingPayload] = Field(default_factory=list, max_length=500)
|
||||
mappings: list[RecipientImportColumnMappingPayload] = Field(
|
||||
default_factory=list, max_length=500
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_column_shape(self) -> "RecipientImportMappingProfilePayload":
|
||||
@@ -583,6 +605,7 @@ class CampaignDeliveryOptionsResponse(BaseModel):
|
||||
version_id: str
|
||||
worker_queue_available: bool
|
||||
postbox_available: bool = False
|
||||
approval_gate: dict[str, Any] = Field(default_factory=dict)
|
||||
synchronous_send: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -633,14 +656,17 @@ def _normalize_report_recipient(value: Any) -> str:
|
||||
if len(recipient) > 320:
|
||||
raise ValueError("report recipient addresses must be at most 320 characters")
|
||||
if any(ord(character) < 32 or ord(character) == 127 for character in recipient):
|
||||
raise ValueError("report recipient addresses must not contain control characters")
|
||||
raise ValueError(
|
||||
"report recipient addresses must not contain control characters"
|
||||
)
|
||||
if recipient.count("@") != 1:
|
||||
raise ValueError("report recipients must be email addresses")
|
||||
local, domain = recipient.split("@", 1)
|
||||
invalid_local = not local or local.startswith(".") or local.endswith(".") or ".." in local
|
||||
invalid_address = (
|
||||
any(character.isspace() for character in recipient)
|
||||
or any(character in ',;:<>[]()\\"' for character in recipient)
|
||||
invalid_local = (
|
||||
not local or local.startswith(".") or local.endswith(".") or ".." in local
|
||||
)
|
||||
invalid_address = any(character.isspace() for character in recipient) or any(
|
||||
character in ',;:<>[]()\\"' for character in recipient
|
||||
)
|
||||
if invalid_local or invalid_address or not _valid_report_email_domain(domain):
|
||||
raise ValueError("report recipients must be email addresses")
|
||||
|
||||
Reference in New Issue
Block a user