Files
aramesh-backend/backend/app/schemas.py
T

879 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import datetime
from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, create_model, field_validator
from .sanitize import sanitize_doc
# ---------- پایه ----------
class ORMModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
# فیلدهای *_en معادل انگلیسی‌اند؛ خالی بودنشان یعنی «بدون ترجمه».
# ---------- خروجی‌ها ----------
class BrandOut(ORMModel):
id: int
name: str
name_en: str = ""
logo: str
hero: str
contact_bg: str
phone: str
phone_href: str
email: str
address: str
address_en: str = ""
class ServiceOut(ORMModel):
id: int
title: str
title_en: str = ""
desc: str
desc_en: str = ""
icon: str
class ProjectOut(ORMModel):
id: int
slug: str
title: str
title_en: str = ""
location: str
location_en: str = ""
year: str
year_en: str = ""
status: str
image: str
gallery: list[str]
area: str
area_en: str = ""
floors: str
floors_en: str = ""
land_area: str = ""
pilot_floors: str = ""
units_residential: str = ""
units_commercial: str = ""
units_office: str = ""
client: str
client_en: str = ""
project_type: str = ""
project_type_en: str = ""
usage: str = ""
usage_en: str = ""
summary: str
summary_en: str = ""
body: list[str]
body_en: Optional[list[str]] = None
class ArticleOut(ORMModel):
id: int
slug: str
author_id: int | None = None
title: str
title_en: str = ""
date: str
date_en: str = ""
category: str
category_en: str = ""
image: str
image_alt: str = ""
image_title: str = ""
read: str
read_en: str = ""
excerpt: str
excerpt_en: str = ""
body: dict
body_en: Optional[dict] = None
# پرسش‌وپاسخ‌های پایان مقاله — [{question, answer}]؛ سطرهای قدیمی ممکن است NULL باشند
faqs: Optional[list[dict]] = None
faqs_en: Optional[list[dict]] = None
status: str
seo_keyphrase: str
# نام نویسنده و توضیحات سئوی اختصاصی — سربرگ مقاله، تگ description و اسکیمای Article
author: str = ""
meta_description: str = ""
meta_description_en: str = ""
slug_en: str = ""
seo_keyphrase_en: str = ""
author_en: str = ""
tags: list[str] = []
tags_en: list[str] = []
class TestimonialOut(ORMModel):
id: int
name: str
name_en: str = ""
role: str
role_en: str = ""
quote: str
quote_en: str = ""
class FaqOut(ORMModel):
id: int
question: str
question_en: str = ""
answer: str
answer_en: str = ""
class TeamMemberOut(ORMModel):
id: int
name: str
name_en: str = ""
role: str
role_en: str = ""
bio: str
bio_en: str = ""
class TimelineItemOut(ORMModel):
id: int
year: str
year_en: str = ""
title: str
title_en: str = ""
desc: str
desc_en: str = ""
image: str
class StatOut(ORMModel):
id: int
value: int
suffix: str
suffix_en: str = ""
label: str
label_en: str = ""
class WhyUsItemOut(ORMModel):
id: int
title: str
title_en: str = ""
desc: str
desc_en: str = ""
class BeforeAfterOut(ORMModel):
id: int
title: str
title_en: str = ""
lead: str
lead_en: str = ""
before: str
after: str
before_label: str
before_label_en: str = ""
after_label: str
after_label_en: str = ""
class NavItemOut(ORMModel):
id: int
label: str
label_en: str = ""
href: str
class HeroOut(ORMModel):
id: int
kicker: str
kicker_en: str = ""
title: str
title_en: str = ""
title_highlight: str
title_highlight_en: str = ""
subtitle: str
subtitle_en: str = ""
badge_value: str
badge_value_en: str = ""
badge_label: str
badge_label_en: str = ""
cta_primary_label: str
cta_primary_label_en: str = ""
cta_primary_href: str
cta_secondary_label: str
cta_secondary_label_en: str = ""
cta_secondary_href: str
scroll_label: str
scroll_label_en: str = ""
class StoryOut(ORMModel):
id: int
title: str
title_en: str = ""
paragraphs: list[str]
paragraphs_en: Optional[list[str]] = None
quote: str
quote_en: str = ""
quote_author: str
quote_author_en: str = ""
stat1_value: str
stat1_value_en: str = ""
stat1_label: str
stat1_label_en: str = ""
stat2_value: str
stat2_value_en: str = ""
stat2_label: str
stat2_label_en: str = ""
link_label: str
link_label_en: str = ""
link_href: str
bento: list[dict]
bento_en: Optional[list[dict]] = None
class SectionHeadingOut(ORMModel):
id: int
key: str
label: str
label_en: str = ""
title: str
title_en: str = ""
lead: str
lead_en: str = ""
image: str
cta_label: str
cta_label_en: str = ""
cta_href: str
class PageHeroOut(ORMModel):
id: int
key: str
title: str
title_en: str = ""
lead: str
lead_en: str = ""
image: str
class InvestmentModelOut(ORMModel):
id: int
title: str
title_en: str = ""
desc: str
desc_en: str = ""
points: list[str]
points_en: Optional[list[str]] = None
class ProcessStepOut(ORMModel):
id: int
step: str
step_en: str = ""
title: str
title_en: str = ""
desc: str
desc_en: str = ""
class OfficeOut(ORMModel):
id: int
city: str
city_en: str = ""
address: str
address_en: str = ""
hours: str
hours_en: str = ""
class GalleryImageOut(ORMModel):
id: int
src: str
alt: str
alt_en: str = ""
class StatusLabelOut(ORMModel):
id: int
key: str
label: str
label_en: str = ""
class ContactSectionOut(ORMModel):
id: int
label: str
label_en: str = ""
title: str
title_en: str = ""
lead: str
lead_en: str = ""
address_label: str
address_label_en: str = ""
phone_label: str
phone_label_en: str = ""
email_label: str
email_label_en: str = ""
name_label: str
name_label_en: str = ""
name_placeholder: str
name_placeholder_en: str = ""
phone_field_label: str
phone_field_label_en: str = ""
phone_placeholder: str
phone_placeholder_en: str = ""
note_label: str
note_label_en: str = ""
note_optional_label: str
note_optional_label_en: str = ""
note_placeholder: str
note_placeholder_en: str = ""
submit_label: str
submit_label_en: str = ""
sending_label: str
sending_label_en: str = ""
success_title: str
success_title_en: str = ""
success_text: str
success_text_en: str = ""
success_button_label: str
success_button_label_en: str = ""
class NewsletterOut(ORMModel):
id: int
title: str
title_en: str = ""
lead: str
lead_en: str = ""
placeholder: str
placeholder_en: str = ""
button_label: str
button_label_en: str = ""
sending_label: str
sending_label_en: str = ""
success_text: str
success_text_en: str = ""
class FooterOut(ORMModel):
id: int
about_text: str
about_text_en: str = ""
pages_heading: str
pages_heading_en: str = ""
services_heading: str
services_heading_en: str = ""
contact_heading: str
contact_heading_en: str = ""
copyright: str
copyright_en: str = ""
tagline: str
tagline_en: str = ""
socials: list[dict]
socials_en: Optional[list[dict]] = None
class SeoMetaOut(ORMModel):
id: int
page_key: str
title: str
title_en: str = ""
description: str
description_en: str = ""
class ContactMessageOut(ORMModel):
id: int
name: str
phone: str
note: str
is_read: bool
created_at: datetime
class SubscriberOut(ORMModel):
id: int
email: str
is_active: bool
created_at: datetime
# ---------- ورودی فرم‌ها ----------
class ContactIn(BaseModel):
name: str = Field(min_length=3, max_length=128)
phone: str = Field(pattern=r"^[\d۰-۹+\-\s]{8,15}$")
note: str = Field(default="", max_length=2000)
# honeypot — فیلد پنهان فرم؛ کاربر واقعی هرگز آن را پر نمی‌کند
website: str = Field(default="", max_length=128)
class NewsletterIn(BaseModel):
email: EmailStr
# honeypot — فیلد پنهان فرم؛ کاربر واقعی هرگز آن را پر نمی‌کند
website: str = Field(default="", max_length=128)
# ---------- ورودی‌های پنل مدیریت ----------
class LoginIn(BaseModel):
username: str
password: str
class TokenOut(BaseModel):
access_token: str
token_type: str = "bearer"
role: str = "admin"
class ChangePasswordIn(BaseModel):
current_password: str
new_password: str = Field(min_length=8, max_length=128)
class BrandIn(BaseModel):
name: str = ""
name_en: str = ""
logo: str = ""
hero: str = ""
contact_bg: str = ""
phone: str = ""
phone_href: str = ""
email: str = ""
address: str = ""
address_en: str = ""
class ServiceIn(BaseModel):
title: str = Field(min_length=1, max_length=256)
title_en: str = ""
desc: str = ""
desc_en: str = ""
icon: str = "draft"
class ProjectIn(BaseModel):
slug: str = Field(min_length=1, max_length=128)
title: str = Field(min_length=1, max_length=256)
title_en: str = ""
location: str = ""
location_en: str = ""
year: str = ""
year_en: str = ""
status: Literal["done", "progress"] = "done"
land_area: str = ""
pilot_floors: str = ""
units_residential: str = ""
units_commercial: str = ""
units_office: str = ""
project_type: str = ""
project_type_en: str = ""
usage: str = ""
usage_en: str = ""
image: str = ""
gallery: list[str] = []
area: str = ""
area_en: str = ""
floors: str = ""
floors_en: str = ""
client: str = ""
client_en: str = ""
summary: str = ""
summary_en: str = ""
body: list[str] = []
body_en: list[str] = []
EMPTY_DOC = {"type": "doc", "content": [{"type": "paragraph"}]}
def validate_doc_body(value: object) -> dict:
"""بدنه مقاله باید یک سند TipTap (ProseMirror) معتبر از نظر ساختار باشد و
آدرس‌های href/src آن به اسکیم‌های امن محدود شوند."""
if not value:
return dict(EMPTY_DOC)
if not isinstance(value, dict) or value.get("type") != "doc" or not isinstance(value.get("content"), list):
raise ValueError("body must be a TipTap doc: {type: 'doc', content: [...]}")
return sanitize_doc(value)
class ArticleIn(BaseModel):
slug: str = Field(min_length=1, max_length=128)
title: str = Field(min_length=1, max_length=256)
title_en: str = ""
date: str = ""
date_en: str = ""
category: str = ""
category_en: str = ""
image: str = ""
image_alt: str = ""
image_title: str = ""
read: str = ""
read_en: str = ""
excerpt: str = ""
excerpt_en: str = ""
body: dict = Field(default_factory=lambda: dict(EMPTY_DOC))
body_en: dict = Field(default_factory=lambda: dict(EMPTY_DOC))
faqs: list[dict] = []
faqs_en: list[dict] = []
status: Literal["draft", "published"] = "draft"
seo_keyphrase: str = Field(default="", max_length=128)
seo_keyphrase_en: str = Field(default="", max_length=128)
slug_en: str = Field(default="", max_length=128)
author: str = Field(default="", max_length=128)
author_en: str = Field(default="", max_length=128)
tags: list[str] = []
tags_en: list[str] = []
meta_description: str = ""
meta_description_en: str = ""
_validate_body = field_validator("body")(validate_doc_body)
_validate_body_en = field_validator("body_en")(validate_doc_body)
class TestimonialIn(BaseModel):
name: str = Field(min_length=1, max_length=128)
name_en: str = ""
role: str = ""
role_en: str = ""
quote: str = ""
quote_en: str = ""
class FaqIn(BaseModel):
question: str = Field(min_length=1)
question_en: str = ""
answer: str = ""
answer_en: str = ""
class TeamMemberIn(BaseModel):
name: str = Field(min_length=1, max_length=128)
name_en: str = ""
role: str = ""
role_en: str = ""
bio: str = ""
bio_en: str = ""
class TimelineItemIn(BaseModel):
year: str = Field(min_length=1, max_length=32)
year_en: str = ""
title: str = Field(min_length=1, max_length=256)
title_en: str = ""
desc: str = ""
desc_en: str = ""
image: str = ""
class StatIn(BaseModel):
value: int
suffix: str = ""
suffix_en: str = ""
label: str = Field(min_length=1, max_length=128)
label_en: str = ""
class WhyUsItemIn(BaseModel):
title: str = Field(min_length=1, max_length=256)
title_en: str = ""
desc: str = ""
desc_en: str = ""
class BeforeAfterIn(BaseModel):
title: str = ""
title_en: str = ""
lead: str = ""
lead_en: str = ""
before: str = ""
after: str = ""
before_label: str = ""
before_label_en: str = ""
after_label: str = ""
after_label_en: str = ""
class NavItemIn(BaseModel):
label: str = Field(min_length=1, max_length=64)
label_en: str = ""
href: str = Field(min_length=1, max_length=128)
class HeroIn(BaseModel):
kicker: str = ""
kicker_en: str = ""
title: str = ""
title_en: str = ""
title_highlight: str = ""
title_highlight_en: str = ""
subtitle: str = ""
subtitle_en: str = ""
badge_value: str = ""
badge_value_en: str = ""
badge_label: str = ""
badge_label_en: str = ""
cta_primary_label: str = ""
cta_primary_label_en: str = ""
cta_primary_href: str = ""
cta_secondary_label: str = ""
cta_secondary_label_en: str = ""
cta_secondary_href: str = ""
scroll_label: str = ""
scroll_label_en: str = ""
class StoryIn(BaseModel):
title: str = ""
title_en: str = ""
paragraphs: list[str] = []
paragraphs_en: list[str] = []
quote: str = ""
quote_en: str = ""
quote_author: str = ""
quote_author_en: str = ""
stat1_value: str = ""
stat1_value_en: str = ""
stat1_label: str = ""
stat1_label_en: str = ""
stat2_value: str = ""
stat2_value_en: str = ""
stat2_label: str = ""
stat2_label_en: str = ""
link_label: str = ""
link_label_en: str = ""
link_href: str = ""
bento: list[dict] = []
bento_en: list[dict] = []
class SectionHeadingIn(BaseModel):
key: str = Field(min_length=1, max_length=64)
label: str = ""
label_en: str = ""
title: str = ""
title_en: str = ""
lead: str = ""
lead_en: str = ""
image: str = ""
cta_label: str = ""
cta_label_en: str = ""
cta_href: str = ""
class PageHeroIn(BaseModel):
key: str = Field(min_length=1, max_length=32)
title: str = ""
title_en: str = ""
lead: str = ""
lead_en: str = ""
image: str = ""
class InvestmentModelIn(BaseModel):
title: str = Field(min_length=1, max_length=256)
title_en: str = ""
desc: str = ""
desc_en: str = ""
points: list[str] = []
points_en: list[str] = []
class ProcessStepIn(BaseModel):
step: str = ""
step_en: str = ""
title: str = Field(min_length=1, max_length=256)
title_en: str = ""
desc: str = ""
desc_en: str = ""
class OfficeIn(BaseModel):
city: str = Field(min_length=1, max_length=64)
city_en: str = ""
address: str = ""
address_en: str = ""
hours: str = ""
hours_en: str = ""
class GalleryImageIn(BaseModel):
src: str = ""
alt: str = ""
alt_en: str = ""
class StatusLabelIn(BaseModel):
key: str = Field(min_length=1, max_length=16)
label: str = Field(min_length=1, max_length=64)
label_en: str = ""
class ContactSectionIn(BaseModel):
label: str = ""
label_en: str = ""
title: str = ""
title_en: str = ""
lead: str = ""
lead_en: str = ""
address_label: str = ""
address_label_en: str = ""
phone_label: str = ""
phone_label_en: str = ""
email_label: str = ""
email_label_en: str = ""
name_label: str = ""
name_label_en: str = ""
name_placeholder: str = ""
name_placeholder_en: str = ""
phone_field_label: str = ""
phone_field_label_en: str = ""
phone_placeholder: str = ""
phone_placeholder_en: str = ""
note_label: str = ""
note_label_en: str = ""
note_optional_label: str = ""
note_optional_label_en: str = ""
note_placeholder: str = ""
note_placeholder_en: str = ""
submit_label: str = ""
submit_label_en: str = ""
sending_label: str = ""
sending_label_en: str = ""
success_title: str = ""
success_title_en: str = ""
success_text: str = ""
success_text_en: str = ""
success_button_label: str = ""
success_button_label_en: str = ""
class NewsletterSectionIn(BaseModel):
title: str = ""
title_en: str = ""
lead: str = ""
lead_en: str = ""
placeholder: str = ""
placeholder_en: str = ""
button_label: str = ""
button_label_en: str = ""
sending_label: str = ""
sending_label_en: str = ""
success_text: str = ""
success_text_en: str = ""
class FooterIn(BaseModel):
about_text: str = ""
about_text_en: str = ""
pages_heading: str = ""
pages_heading_en: str = ""
services_heading: str = ""
services_heading_en: str = ""
contact_heading: str = ""
contact_heading_en: str = ""
copyright: str = ""
copyright_en: str = ""
tagline: str = ""
tagline_en: str = ""
socials: list[dict] = []
socials_en: list[dict] = []
class SeoMetaIn(BaseModel):
page_key: str = Field(min_length=1, max_length=32)
title: str = ""
title_en: str = ""
description: str = ""
description_en: str = ""
class SeoMetaPatch(BaseModel):
page_key: Optional[str] = None
title: Optional[str] = None
title_en: Optional[str] = None
description: Optional[str] = None
description_en: Optional[str] = None
class MessageReadIn(BaseModel):
is_read: bool = True
# ---------- اسکیماهای PATCH (ویرایش جزئی — همه فیلدها اختیاری) ----------
def _make_patch(name: str, model_cls: type[BaseModel]) -> type[BaseModel]:
"""از اسکیمای ورودی، اسکیمای PATCH می‌سازد؛ فیلدهای ارسال‌نشده دست‌نخورده می‌مانند."""
fields = {fname: (Optional[f.annotation], None) for fname, f in model_cls.model_fields.items()}
return create_model(name, **fields)
BrandPatch = _make_patch("BrandPatch", BrandIn)
ServicePatch = _make_patch("ServicePatch", ServiceIn)
ProjectPatch = _make_patch("ProjectPatch", ProjectIn)
class ArticlePatch(BaseModel):
slug: Optional[str] = None
title: Optional[str] = None
title_en: Optional[str] = None
date: Optional[str] = None
date_en: Optional[str] = None
category: Optional[str] = None
category_en: Optional[str] = None
image: Optional[str] = None
image_alt: Optional[str] = None
image_title: Optional[str] = None
read: Optional[str] = None
read_en: Optional[str] = None
excerpt: Optional[str] = None
excerpt_en: Optional[str] = None
body: Optional[dict] = None
body_en: Optional[dict] = None
faqs: Optional[list[dict]] = None
faqs_en: Optional[list[dict]] = None
status: Optional[Literal["draft", "published"]] = None
seo_keyphrase: Optional[str] = None
seo_keyphrase_en: Optional[str] = None
slug_en: Optional[str] = None
author: Optional[str] = None
author_en: Optional[str] = None
tags: Optional[list[str]] = None
tags_en: Optional[list[str]] = None
meta_description: Optional[str] = None
meta_description_en: Optional[str] = None
_validate_body = field_validator("body")(validate_doc_body)
_validate_body_en = field_validator("body_en")(validate_doc_body)
TestimonialPatch = _make_patch("TestimonialPatch", TestimonialIn)
FaqPatch = _make_patch("FaqPatch", FaqIn)
TeamMemberPatch = _make_patch("TeamMemberPatch", TeamMemberIn)
TimelineItemPatch = _make_patch("TimelineItemPatch", TimelineItemIn)
StatPatch = _make_patch("StatPatch", StatIn)
WhyUsItemPatch = _make_patch("WhyUsItemPatch", WhyUsItemIn)
BeforeAfterPatch = _make_patch("BeforeAfterPatch", BeforeAfterIn)
NavItemPatch = _make_patch("NavItemPatch", NavItemIn)
HeroPatch = _make_patch("HeroPatch", HeroIn)
StoryPatch = _make_patch("StoryPatch", StoryIn)
SectionHeadingPatch = _make_patch("SectionHeadingPatch", SectionHeadingIn)
PageHeroPatch = _make_patch("PageHeroPatch", PageHeroIn)
InvestmentModelPatch = _make_patch("InvestmentModelPatch", InvestmentModelIn)
ProcessStepPatch = _make_patch("ProcessStepPatch", ProcessStepIn)
OfficePatch = _make_patch("OfficePatch", OfficeIn)
GalleryImagePatch = _make_patch("GalleryImagePatch", GalleryImageIn)
StatusLabelPatch = _make_patch("StatusLabelPatch", StatusLabelIn)
ContactSectionPatch = _make_patch("ContactSectionPatch", ContactSectionIn)
NewsletterSectionPatch = _make_patch("NewsletterSectionPatch", NewsletterSectionIn)
FooterPatch = _make_patch("FooterPatch", FooterIn)
class ReorderIn(BaseModel):
"""ترتیب جدید آیتم‌های یک مجموعه بر اساس id."""
ids: list[int]