# retoor import re from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, field_validator from molodetz.constants import JOIN_STATUSES, POST_STATUSES, ROLES, TOPICS URL_HOST_PATTERN = re.compile(r"^https?://[^\s/]+\.[^\s/]+") PROSE_CAP = 125000 def normalize_website(value): value = (value or "").strip() if not value: return None if not value.lower().startswith(("http://", "https://")): value = "https://" + value if not URL_HOST_PATTERN.match(value): raise ValueError("Enter a valid link, for example https://git.example.com/name") return value[:500] def normalize_european_date(value): value = (value or "").strip() if not value: return None for fmt in ("%d/%m/%Y", "%Y-%m-%d"): try: return datetime.strptime(value, fmt).date().isoformat() except ValueError: continue raise ValueError("Use DD/MM/YYYY") class FormModel(BaseModel): model_config = ConfigDict(str_strip_whitespace=True, extra="ignore") class LoginForm(FormModel): username: str = Field(min_length=3, max_length=255) password: str = Field(min_length=6, max_length=128) remember: bool = False next: str | None = None class PostForm(FormModel): title: str = Field(min_length=3, max_length=200) body: str = Field(min_length=10, max_length=PROSE_CAP) topic: str = "roll" status: str = "draft" is_placeholder: bool = False @field_validator("topic") @classmethod def check_topic(cls, value): if value not in TOPICS: raise ValueError("Unknown topic") return value @field_validator("status") @classmethod def check_status(cls, value): if value not in POST_STATUSES: raise ValueError("Unknown status") return value class JoinForm(FormModel): name: str = Field(min_length=2, max_length=80) contact: str = Field(min_length=3, max_length=255) repo_url: str | None = Field(default=None, max_length=500) message: str | None = Field(default=None, max_length=2000) website: str | None = Field(default=None, max_length=200) @field_validator("repo_url") @classmethod def check_repo(cls, value): return normalize_website(value) class JoinStatusForm(FormModel): status: str @field_validator("status") @classmethod def check_status(cls, value): if value not in JOIN_STATUSES: raise ValueError("Unknown status") return value class InviteClaimForm(FormModel): username: str = Field(min_length=3, max_length=32) email: str = Field(min_length=3, max_length=255) password: str = Field(min_length=6, max_length=128) password_confirm: str = Field(min_length=6, max_length=128) terms: bool = False @field_validator("username") @classmethod def check_username(cls, value): if not re.fullmatch(r"[A-Za-z0-9_-]{3,32}", value or ""): raise ValueError("Use 3 to 32 letters, digits, _ or -") return value class RoleForm(FormModel): role: str @field_validator("role") @classmethod def check_role(cls, value): if value not in ROLES: raise ValueError("Unknown role") return value class SettingsForm(FormModel): site_title: str | None = Field(default=None, max_length=80) site_tagline: str | None = Field(default=None, max_length=200) site_intro: str | None = Field(default=None, max_length=20000) maintenance_mode: str | None = None maintenance_message: str | None = Field(default=None, max_length=500) rate_limit_per_minute: str | None = None rate_limit_window_seconds: str | None = None session_max_age_days: str | None = None session_remember_days: str | None = None terms_version: str | None = None friendly_404: str | None = None audit_retention_days: str | None = None outbound_proxy_url: str | None = Field(default=None, max_length=500) @field_validator("site_intro") @classmethod def normalize_newlines(cls, value): return value.replace("\r\n", "\n").replace("\r", "\n") if value else value class ServiceConfigForm(FormModel): model_config = ConfigDict(extra="allow", str_strip_whitespace=True) class BackupForm(FormModel): target: str = "database" @field_validator("target") @classmethod def check_target(cls, value): if value not in ("database", "uploads", "keys", "full"): raise ValueError("Unknown target") return value