initial commit after split
This commit is contained in:
555
src/govoplan_campaign/backend/schemas.py
Normal file
555
src/govoplan_campaign/backend/schemas.py
Normal file
@@ -0,0 +1,555 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from govoplan_mail.backend.config import ImapConfig, SmtpConfig
|
||||
|
||||
|
||||
class CampaignCreateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
config: dict[str, Any]
|
||||
source_filename: str | None = None
|
||||
source_base_path: str | None = None
|
||||
|
||||
|
||||
|
||||
|
||||
class CampaignUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
external_id: str | None = None
|
||||
name: str | None = None
|
||||
status: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class CampaignCreateMinimalRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
external_id: str
|
||||
name: str
|
||||
description: str | None = None
|
||||
current_flow: str = "create"
|
||||
current_step: str = "basics"
|
||||
|
||||
|
||||
class CampaignVersionUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
campaign_json: dict[str, Any] | None = None
|
||||
current_flow: str | None = None
|
||||
current_step: str | None = None
|
||||
workflow_state: str | None = None
|
||||
is_complete: bool | None = None
|
||||
editor_state: dict[str, Any] | None = None
|
||||
source_filename: str | None = None
|
||||
source_base_path: str | None = None
|
||||
|
||||
|
||||
class CampaignVersionSetStepRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
current_flow: str | None = None
|
||||
current_step: str
|
||||
|
||||
|
||||
class CampaignReviewStateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
inspection_complete: bool = False
|
||||
reviewed_message_keys: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class CampaignPartialValidationRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
campaign_json: dict[str, Any] | None = None
|
||||
section: str | None = None
|
||||
|
||||
|
||||
class CampaignVersionResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
campaign_id: str
|
||||
version_number: int
|
||||
schema_version: str
|
||||
source_filename: str | None = None
|
||||
source_base_path: str | None = None
|
||||
workflow_state: str = "editing"
|
||||
current_flow: str = "manual"
|
||||
current_step: str | None = None
|
||||
is_complete: bool = False
|
||||
editor_state: dict[str, Any] = Field(default_factory=dict)
|
||||
autosaved_at: datetime | None = None
|
||||
published_at: datetime | None = None
|
||||
locked_at: datetime | None = None
|
||||
locked_by_user_id: str | None = None
|
||||
user_lock_state: Literal["temporary", "permanent"] | None = None
|
||||
user_locked_at: datetime | None = None
|
||||
user_locked_by_user_id: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
validation_summary: dict[str, Any] | None = None
|
||||
build_summary: dict[str, Any] | None = None
|
||||
execution_snapshot_hash: str | None = None
|
||||
execution_snapshot_at: datetime | None = None
|
||||
|
||||
|
||||
class CampaignVersionDetailResponse(CampaignVersionResponse):
|
||||
raw_json: dict[str, Any]
|
||||
|
||||
|
||||
class CampaignPartialValidationResponse(BaseModel):
|
||||
ok: bool
|
||||
section: str | None = None
|
||||
error_count: int
|
||||
warning_count: int
|
||||
info_count: int
|
||||
issues: list[dict[str, Any]]
|
||||
|
||||
|
||||
class CampaignResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
external_id: str
|
||||
name: str
|
||||
description: str | None = None
|
||||
status: str
|
||||
current_version_id: str | None = None
|
||||
owner_user_id: str | None = None
|
||||
owner_group_id: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class CampaignCreateResponse(BaseModel):
|
||||
campaign: CampaignResponse
|
||||
version: CampaignVersionResponse
|
||||
|
||||
|
||||
class CampaignListResponse(BaseModel):
|
||||
campaigns: list[CampaignResponse]
|
||||
|
||||
|
||||
class CampaignShareItem(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
campaign_id: str
|
||||
target_type: Literal["user", "group"]
|
||||
target_id: str
|
||||
permission: Literal["read", "write"] = "read"
|
||||
revoked_at: datetime | None = None
|
||||
|
||||
|
||||
class CampaignShareListResponse(BaseModel):
|
||||
shares: list[CampaignShareItem]
|
||||
|
||||
|
||||
class CampaignShareTargetItem(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
secondary: str | None = None
|
||||
|
||||
|
||||
class CampaignShareTargetsResponse(BaseModel):
|
||||
users: list[CampaignShareTargetItem] = Field(default_factory=list)
|
||||
groups: list[CampaignShareTargetItem] = Field(default_factory=list)
|
||||
|
||||
|
||||
class CampaignShareUpsertRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
target_type: Literal["user", "group"]
|
||||
target_id: str
|
||||
permission: Literal["read", "write"] = "read"
|
||||
|
||||
|
||||
class CampaignOwnerUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
owner_user_id: str | None = None
|
||||
owner_group_id: str | None = None
|
||||
|
||||
|
||||
class CampaignJobsResponse(BaseModel):
|
||||
jobs: list[dict[str, Any]]
|
||||
page: int = 1
|
||||
page_size: int = 50
|
||||
total: int = 0
|
||||
total_unfiltered: int = 0
|
||||
pages: int = 0
|
||||
counts: dict[str, dict[str, int]] = Field(default_factory=dict)
|
||||
filtered_counts: dict[str, dict[str, int]] = Field(default_factory=dict)
|
||||
review: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class CampaignJobDetailResponse(BaseModel):
|
||||
job: dict[str, Any]
|
||||
attempts: dict[str, list[dict[str, Any]]] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class CampaignRetryJobsRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
version_id: str | None = None
|
||||
job_ids: list[str] = Field(default_factory=list)
|
||||
include_permanent: bool = False
|
||||
force_max_attempts: bool = False
|
||||
enqueue_celery: bool = True
|
||||
dry_run: bool = False
|
||||
|
||||
|
||||
class CampaignSendUnattemptedRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
version_id: str | None = None
|
||||
job_ids: list[str] = Field(default_factory=list)
|
||||
enqueue_celery: bool = True
|
||||
dry_run: bool = False
|
||||
|
||||
|
||||
class CampaignResolveOutcomeRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
decision: Literal["smtp_accepted", "not_sent"]
|
||||
note: str | None = None
|
||||
|
||||
|
||||
class ValidateCampaignRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
check_files: bool = False
|
||||
|
||||
|
||||
class BuildCampaignRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
write_eml: bool = True
|
||||
|
||||
|
||||
class MailSmtpTestRequest(SmtpConfig):
|
||||
"""SMTP settings supplied directly from the WebUI mail settings form."""
|
||||
|
||||
|
||||
class MailImapTestRequest(ImapConfig):
|
||||
"""IMAP settings supplied directly from the WebUI mail settings form."""
|
||||
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
|
||||
|
||||
MailProfileScope = Literal["system", "tenant", "user", "group", "campaign"]
|
||||
|
||||
|
||||
class MailCredentialPolicyPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
inherit: bool | None = None
|
||||
allow_override: bool | None = None
|
||||
|
||||
|
||||
class MailProfilePolicyPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
allowed_profile_ids: list[str] = Field(default_factory=list)
|
||||
allow_user_profiles: bool | None = None
|
||||
allow_group_profiles: bool | None = None
|
||||
allow_campaign_profiles: bool | None = None
|
||||
smtp_credentials: MailCredentialPolicyPayload = Field(default_factory=MailCredentialPolicyPayload)
|
||||
imap_credentials: MailCredentialPolicyPayload = Field(default_factory=MailCredentialPolicyPayload)
|
||||
whitelist: dict[str, list[str]] = Field(default_factory=dict)
|
||||
blacklist: dict[str, list[str]] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class MailProfilePolicyUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
policy: MailProfilePolicyPayload = Field(default_factory=MailProfilePolicyPayload)
|
||||
|
||||
|
||||
class MailProfilePolicyResponse(BaseModel):
|
||||
scope_type: MailProfileScope
|
||||
scope_id: str | None = None
|
||||
policy: dict[str, Any]
|
||||
effective_policy: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class MailServerProfileCreateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
slug: str | None = Field(default=None, max_length=100)
|
||||
description: str | None = None
|
||||
is_active: bool = True
|
||||
scope_type: MailProfileScope = "tenant"
|
||||
scope_id: str | None = None
|
||||
smtp: SmtpConfig
|
||||
imap: ImapConfig | None = None
|
||||
|
||||
|
||||
class MailServerProfileUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str | None = Field(default=None, max_length=255)
|
||||
slug: str | None = Field(default=None, max_length=100)
|
||||
description: str | None = None
|
||||
is_active: bool | None = None
|
||||
smtp: SmtpConfig | None = None
|
||||
imap: ImapConfig | None = None
|
||||
clear_imap: bool = False
|
||||
|
||||
|
||||
class MailServerProfileResponse(BaseModel):
|
||||
id: str
|
||||
tenant_id: str | None = None
|
||||
scope_type: MailProfileScope = "tenant"
|
||||
scope_id: str | None = None
|
||||
name: str
|
||||
slug: str
|
||||
description: str | None = None
|
||||
is_active: bool
|
||||
smtp: dict[str, Any]
|
||||
imap: dict[str, Any] | None = None
|
||||
smtp_password_configured: bool = False
|
||||
imap_password_configured: bool = False
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class MailServerProfileListResponse(BaseModel):
|
||||
profiles: list[MailServerProfileResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class MailConnectionTestResponse(BaseModel):
|
||||
ok: bool
|
||||
protocol: Literal["smtp", "imap"]
|
||||
host: str | None = None
|
||||
port: int | None = None
|
||||
security: str | None = None
|
||||
message: str
|
||||
details: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class MailImapFolderResponse(BaseModel):
|
||||
name: str
|
||||
flags: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class MailImapFolderListResponse(BaseModel):
|
||||
ok: bool
|
||||
protocol: Literal["imap"] = "imap"
|
||||
host: str | None = None
|
||||
port: int | None = None
|
||||
security: str | None = None
|
||||
message: str
|
||||
folders: list[MailImapFolderResponse] = Field(default_factory=list)
|
||||
detected_sent_folder: str | None = None
|
||||
details: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ApiKeyCreateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str
|
||||
scopes: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ApiKeyCreateResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
prefix: str
|
||||
scopes: list[str]
|
||||
secret: str
|
||||
|
||||
|
||||
class QueueCampaignRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
version_id: str | None = None
|
||||
include_warnings: bool = True
|
||||
enqueue_celery: bool = True
|
||||
dry_run: bool = False
|
||||
|
||||
|
||||
class QueueCampaignResponse(BaseModel):
|
||||
campaign_id: str
|
||||
version_id: str
|
||||
queued_count: int
|
||||
skipped_count: int
|
||||
blocked_count: int
|
||||
enqueued_count: int
|
||||
dry_run: bool = False
|
||||
|
||||
|
||||
class SendCampaignNowRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
version_id: str | None = None
|
||||
include_warnings: bool = True
|
||||
check_files: bool = False
|
||||
validate_before_send: bool = False
|
||||
build_before_send: bool = False
|
||||
dry_run: bool = False
|
||||
use_rate_limit: bool = True
|
||||
enqueue_imap_task: bool = False
|
||||
|
||||
|
||||
class SendCampaignNowResponse(BaseModel):
|
||||
result: dict[str, Any]
|
||||
|
||||
|
||||
class MockCampaignSendRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
version_id: str | None = None
|
||||
send: bool = False
|
||||
include_warnings: bool = True
|
||||
include_needs_review: bool = False
|
||||
append_sent: bool = True
|
||||
clear_mailbox: bool = False
|
||||
check_files: bool = False
|
||||
|
||||
|
||||
class MockCampaignSendResponse(BaseModel):
|
||||
result: dict[str, Any]
|
||||
|
||||
|
||||
class AppendSentRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
enqueue_celery: bool = True
|
||||
dry_run: bool = False
|
||||
|
||||
|
||||
class CampaignActionResponse(BaseModel):
|
||||
result: dict[str, Any]
|
||||
|
||||
class ReportEmailRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
to: list[str]
|
||||
version_id: str | None = None
|
||||
include_jobs: bool = False
|
||||
attach_jobs_csv: bool = True
|
||||
attach_report_json: bool = False
|
||||
dry_run: bool = False
|
||||
|
||||
|
||||
class ReportEmailResponse(BaseModel):
|
||||
result: dict[str, Any]
|
||||
|
||||
|
||||
class AuditLogItemResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
tenant_id: str | None = None
|
||||
user_id: str | None = None
|
||||
api_key_id: str | None = None
|
||||
action: str
|
||||
object_type: str | None = None
|
||||
object_id: str | None = None
|
||||
details: dict[str, Any] | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class AuditLogListResponse(BaseModel):
|
||||
items: list[AuditLogItemResponse]
|
||||
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
email: str
|
||||
password: str
|
||||
# Kept optional for backwards compatibility and future tenant-switch login flows.
|
||||
# The WebUI no longer sends it. If omitted, the backend resolves the user by email.
|
||||
tenant_slug: str | None = None
|
||||
|
||||
|
||||
|
||||
|
||||
class SwitchTenantRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
tenant_id: str
|
||||
|
||||
|
||||
class TenantInfo(BaseModel):
|
||||
id: str
|
||||
slug: str
|
||||
name: str
|
||||
is_active: bool = True
|
||||
default_locale: str = "en"
|
||||
|
||||
|
||||
class TenantMembershipInfo(TenantInfo):
|
||||
roles: list[str] = Field(default_factory=list)
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
id: str
|
||||
account_id: str
|
||||
email: str
|
||||
# Global account identity used by the title bar and account settings.
|
||||
display_name: str | None = None
|
||||
# Optional tenant-local alias used in tenant administration and ownership.
|
||||
tenant_display_name: str | None = None
|
||||
is_tenant_admin: bool = False
|
||||
password_reset_required: bool = False
|
||||
|
||||
|
||||
class ProfileUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
display_name: str | None = Field(default=None, max_length=255)
|
||||
tenant_display_name: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
class RoleInfo(BaseModel):
|
||||
id: str
|
||||
slug: str
|
||||
name: str
|
||||
permissions: list[str] = Field(default_factory=list)
|
||||
level: Literal["tenant", "system"] = "tenant"
|
||||
|
||||
|
||||
class GroupInfo(BaseModel):
|
||||
id: str
|
||||
slug: str
|
||||
name: str
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_at: datetime
|
||||
user: UserInfo
|
||||
# Backwards-compatible alias for the active tenant.
|
||||
tenant: TenantInfo
|
||||
active_tenant: TenantInfo
|
||||
tenants: list[TenantMembershipInfo] = Field(default_factory=list)
|
||||
scopes: list[str]
|
||||
roles: list[RoleInfo] = Field(default_factory=list)
|
||||
groups: list[GroupInfo] = Field(default_factory=list)
|
||||
|
||||
|
||||
class MeResponse(BaseModel):
|
||||
user: UserInfo
|
||||
# Backwards-compatible alias for the active tenant.
|
||||
tenant: TenantInfo
|
||||
active_tenant: TenantInfo
|
||||
tenants: list[TenantMembershipInfo] = Field(default_factory=list)
|
||||
scopes: list[str]
|
||||
roles: list[RoleInfo] = Field(default_factory=list)
|
||||
groups: list[GroupInfo] = Field(default_factory=list)
|
||||
Reference in New Issue
Block a user