نسخه ۲ بک‌اند آرامش پلازا — بازنویسی کامل

- API عمومی محتوای سایت (پروژه‌ها، مقالات، خدمات و...)
- فرم تماس و خبرنامه با اعتبارسنجی سرور
- پنل مدیریت با JWT و CRUD کامل + مدیریت پیام‌ها و مشترکین
- اسکریپت seed با داده‌های واقعی سایت
- تست‌شده: ۳۶/۳۶ انتهاپوینت
This commit is contained in:
Behrad
2026-08-29 12:55:13 +03:30
parent 854309f976
commit ed14435485
30 changed files with 1237 additions and 1655 deletions
+206 -283
View File
@@ -1,23 +1,160 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, EmailStr, field_validator
from typing import Literal
from app.models import ProjectStatus
from pydantic import BaseModel, ConfigDict, EmailStr, Field
# ---------- پایه ----------
# ---------- Auth ----------
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
class ORMModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
class LoginRequest(BaseModel):
# ---------- خروجی‌ها ----------
class BrandOut(ORMModel):
id: int
name: str
logo: str
hero: str
contact_bg: str
phone: str
phone_href: str
email: str
address: str
class ServiceOut(ORMModel):
id: int
title: str
desc: str
icon: str
class ProjectOut(ORMModel):
id: int
slug: str
title: str
location: str
year: str
status: str
image: str
gallery: list[str]
area: str
floors: str
units: str
client: str
summary: str
body: list[str]
class ArticleOut(ORMModel):
id: int
slug: str
title: str
date: str
category: str
image: str
read: str
excerpt: str
body: list[str]
class TestimonialOut(ORMModel):
id: int
name: str
role: str
quote: str
class FaqOut(ORMModel):
id: int
question: str
answer: str
class TeamMemberOut(ORMModel):
id: int
name: str
role: str
bio: str
class TimelineItemOut(ORMModel):
id: int
year: str
title: str
desc: str
image: str
class StatOut(ORMModel):
id: int
value: int
suffix: str
label: str
class WhyUsItemOut(ORMModel):
id: int
title: str
desc: str
class BeforeAfterOut(ORMModel):
id: int
title: str
lead: str
before: str
after: str
before_label: str
after_label: 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)
class NewsletterIn(BaseModel):
email: EmailStr
# ---------- ورودی‌های پنل مدیریت ----------
class LoginIn(BaseModel):
username: str
password: str
# ---------- Brand ----------
class BrandBase(BaseModel):
class TokenOut(BaseModel):
access_token: str
token_type: str = "bearer"
class BrandIn(BaseModel):
name: str = ""
logo: str = ""
hero: str = ""
@@ -28,296 +165,82 @@ class BrandBase(BaseModel):
address: str = ""
class BrandOut(BrandBase):
model_config = ConfigDict(from_attributes=True)
id: int
class ServiceIn(BaseModel):
title: str = Field(min_length=1, max_length=256)
desc: str = ""
icon: str = "draft"
class BrandUpdate(BrandBase):
pass
# ---------- Service ----------
class ServiceBase(BaseModel):
title: str
desc: str
icon: str
order: int = 0
class ServiceCreate(ServiceBase):
pass
class ServiceUpdate(BaseModel):
title: Optional[str] = None
desc: Optional[str] = None
icon: Optional[str] = None
order: Optional[int] = None
class ServiceOut(ServiceBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- Project ----------
class ProjectBase(BaseModel):
slug: str
title: str
location: str
year: str
status: ProjectStatus
image: str
class ProjectIn(BaseModel):
slug: str = Field(min_length=1, max_length=128)
title: str = Field(min_length=1, max_length=256)
location: str = ""
year: str = ""
status: Literal["done", "progress"] = "done"
image: str = ""
gallery: list[str] = []
area: str
floors: str
units: str
client: str
summary: str
area: str = ""
floors: str = ""
units: str = ""
client: str = ""
summary: str = ""
body: list[str] = []
order: int = 0
class ProjectCreate(ProjectBase):
pass
class ArticleIn(BaseModel):
slug: str = Field(min_length=1, max_length=128)
title: str = Field(min_length=1, max_length=256)
date: str = ""
category: str = ""
image: str = ""
read: str = ""
excerpt: str = ""
body: list[str] = []
class ProjectUpdate(BaseModel):
slug: Optional[str] = None
title: Optional[str] = None
location: Optional[str] = None
year: Optional[str] = None
status: Optional[ProjectStatus] = None
image: Optional[str] = None
gallery: Optional[list[str]] = None
area: Optional[str] = None
floors: Optional[str] = None
units: Optional[str] = None
client: Optional[str] = None
summary: Optional[str] = None
body: Optional[list[str]] = None
order: Optional[int] = None
class TestimonialIn(BaseModel):
name: str = Field(min_length=1, max_length=128)
role: str = ""
quote: str = ""
class ProjectOut(ProjectBase):
model_config = ConfigDict(from_attributes=True)
id: int
class FaqIn(BaseModel):
question: str = Field(min_length=1)
answer: str = ""
# ---------- Stat ----------
class StatBase(BaseModel):
class TeamMemberIn(BaseModel):
name: str = Field(min_length=1, max_length=128)
role: str = ""
bio: str = ""
class TimelineItemIn(BaseModel):
year: str = Field(min_length=1, max_length=32)
title: str = Field(min_length=1, max_length=256)
desc: str = ""
image: str = ""
class StatIn(BaseModel):
value: int
suffix: str = ""
label: str
order: int = 0
label: str = Field(min_length=1, max_length=128)
class StatCreate(StatBase):
pass
class WhyUsItemIn(BaseModel):
title: str = Field(min_length=1, max_length=256)
desc: str = ""
class StatUpdate(BaseModel):
value: Optional[int] = None
suffix: Optional[str] = None
label: Optional[str] = None
order: Optional[int] = None
class BeforeAfterIn(BaseModel):
title: str = ""
lead: str = ""
before: str = ""
after: str = ""
before_label: str = ""
after_label: str = ""
class StatOut(StatBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- Timeline ----------
class TimelineBase(BaseModel):
year: str
title: str
desc: str
image: str
order: int = 0
class TimelineCreate(TimelineBase):
pass
class TimelineUpdate(BaseModel):
year: Optional[str] = None
title: Optional[str] = None
desc: Optional[str] = None
image: Optional[str] = None
order: Optional[int] = None
class TimelineOut(TimelineBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- WhyUs ----------
class WhyUsBase(BaseModel):
title: str
desc: str
order: int = 0
class WhyUsCreate(WhyUsBase):
pass
class WhyUsUpdate(BaseModel):
title: Optional[str] = None
desc: Optional[str] = None
order: Optional[int] = None
class WhyUsOut(WhyUsBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- Testimonial ----------
class TestimonialBase(BaseModel):
name: str
role: str
quote: str
order: int = 0
class TestimonialCreate(TestimonialBase):
pass
class TestimonialUpdate(BaseModel):
name: Optional[str] = None
role: Optional[str] = None
quote: Optional[str] = None
order: Optional[int] = None
class TestimonialOut(TestimonialBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- Team ----------
class TeamMemberBase(BaseModel):
name: str
role: str
bio: str
photo: str = ""
order: int = 0
class TeamMemberCreate(TeamMemberBase):
pass
class TeamMemberUpdate(BaseModel):
name: Optional[str] = None
role: Optional[str] = None
bio: Optional[str] = None
photo: Optional[str] = None
order: Optional[int] = None
class TeamMemberOut(TeamMemberBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- Article ----------
class ArticleBase(BaseModel):
slug: str
title: str
date: str
category: str
image: str
read: str
excerpt: str
body: list[str] = []
order: int = 0
class ArticleCreate(ArticleBase):
pass
class ArticleUpdate(BaseModel):
slug: Optional[str] = None
title: Optional[str] = None
date: Optional[str] = None
category: Optional[str] = None
image: Optional[str] = None
read: Optional[str] = None
excerpt: Optional[str] = None
body: Optional[list[str]] = None
order: Optional[int] = None
class ArticleOut(ArticleBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- FAQ ----------
class FaqBase(BaseModel):
q: str
a: str
order: int = 0
class FaqCreate(FaqBase):
pass
class FaqUpdate(BaseModel):
q: Optional[str] = None
a: Optional[str] = None
order: Optional[int] = None
class FaqOut(FaqBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- Contact ----------
class ContactCreate(BaseModel):
name: str = ""
phone: str
note: str = ""
@field_validator("phone")
@classmethod
def phone_min_length(cls, v: str) -> str:
if len(v.strip()) < 8:
raise ValueError("شماره تلفن معتبر نیست")
return v.strip()
class ContactOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
phone: str
note: str
is_read: bool
created_at: datetime
class ContactUpdate(BaseModel):
is_read: Optional[bool] = None
# ---------- Newsletter ----------
class NewsletterCreate(BaseModel):
email: EmailStr
class NewsletterOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
email: str
created_at: datetime
class MessageReadIn(BaseModel):
is_read: bool = True