بکاند: ویرایش جزئی (PATCH) و ترتیببندی مجموعهها برای ویرایش زنده
- PATCH /api/admin/{resource}/{id} — ویرایش جزئی؛ فقط فیلدهای ارسالشده
بهروزرسانی میشوند (PUT قبلاً کل رکورد را جایگزین میکرد و برای
ویرایش زنده قابل استفاده نبود). اسکیماهای Patch بهصورت خودکار از
اسکیماهای In با create_model ساخته میشوند (هر ۲۴ منبع).
- ستون position برای ۱۴ جدول مجموعهای (خدمات، پروژهها، مقالات، نظرات،
سوالات، تیم، تایملاین، آمار، چرا آرامش، مدلهای سرمایهگذاری، مراحل کار،
دفاتر، گالری، منو) — مهاجرت سبک idempotent در startup و seed
(ALTER TABLE ADD COLUMN IF NOT EXISTS) تا دیتابیسهای موجود هم ارتقا یابند
- POST /api/admin/{resource}/reorder — ذخیره ترتیب جدید بر اساس لیست id
- خواندن عمومی بر اساس (position, id) مرتب میشود؛ رکورد جدید در انتهای لیست
- seed فقط جدولهای خالی را با position صحیح پر میکند
This commit is contained in:
+94
-27
@@ -5,6 +5,7 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||||
|
from sqlalchemy import func
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@@ -15,57 +16,82 @@ from ..database import get_db
|
|||||||
from ..schemas import (
|
from ..schemas import (
|
||||||
ArticleIn,
|
ArticleIn,
|
||||||
ArticleOut,
|
ArticleOut,
|
||||||
|
ArticlePatch,
|
||||||
BeforeAfterIn,
|
BeforeAfterIn,
|
||||||
BeforeAfterOut,
|
BeforeAfterOut,
|
||||||
|
BeforeAfterPatch,
|
||||||
BrandIn,
|
BrandIn,
|
||||||
BrandOut,
|
BrandOut,
|
||||||
|
BrandPatch,
|
||||||
ContactSectionIn,
|
ContactSectionIn,
|
||||||
ContactSectionOut,
|
ContactSectionOut,
|
||||||
|
ContactSectionPatch,
|
||||||
ContactMessageOut,
|
ContactMessageOut,
|
||||||
FaqIn,
|
FaqIn,
|
||||||
FaqOut,
|
FaqOut,
|
||||||
|
FaqPatch,
|
||||||
FooterIn,
|
FooterIn,
|
||||||
FooterOut,
|
FooterOut,
|
||||||
|
FooterPatch,
|
||||||
GalleryImageIn,
|
GalleryImageIn,
|
||||||
GalleryImageOut,
|
GalleryImageOut,
|
||||||
|
GalleryImagePatch,
|
||||||
HeroIn,
|
HeroIn,
|
||||||
HeroOut,
|
HeroOut,
|
||||||
|
HeroPatch,
|
||||||
InvestmentModelIn,
|
InvestmentModelIn,
|
||||||
InvestmentModelOut,
|
InvestmentModelOut,
|
||||||
|
InvestmentModelPatch,
|
||||||
LoginIn,
|
LoginIn,
|
||||||
MessageReadIn,
|
MessageReadIn,
|
||||||
NavItemIn,
|
NavItemIn,
|
||||||
NavItemOut,
|
NavItemOut,
|
||||||
|
NavItemPatch,
|
||||||
NewsletterSectionIn,
|
NewsletterSectionIn,
|
||||||
NewsletterOut,
|
NewsletterOut,
|
||||||
|
NewsletterSectionPatch,
|
||||||
OfficeIn,
|
OfficeIn,
|
||||||
OfficeOut,
|
OfficeOut,
|
||||||
|
OfficePatch,
|
||||||
PageHeroIn,
|
PageHeroIn,
|
||||||
PageHeroOut,
|
PageHeroOut,
|
||||||
|
PageHeroPatch,
|
||||||
ProcessStepIn,
|
ProcessStepIn,
|
||||||
ProcessStepOut,
|
ProcessStepOut,
|
||||||
|
ProcessStepPatch,
|
||||||
ProjectIn,
|
ProjectIn,
|
||||||
ProjectOut,
|
ProjectOut,
|
||||||
|
ProjectPatch,
|
||||||
|
ReorderIn,
|
||||||
SectionHeadingIn,
|
SectionHeadingIn,
|
||||||
SectionHeadingOut,
|
SectionHeadingOut,
|
||||||
|
SectionHeadingPatch,
|
||||||
ServiceIn,
|
ServiceIn,
|
||||||
ServiceOut,
|
ServiceOut,
|
||||||
|
ServicePatch,
|
||||||
StatIn,
|
StatIn,
|
||||||
StatOut,
|
StatOut,
|
||||||
|
StatPatch,
|
||||||
StatusLabelIn,
|
StatusLabelIn,
|
||||||
StatusLabelOut,
|
StatusLabelOut,
|
||||||
|
StatusLabelPatch,
|
||||||
StoryIn,
|
StoryIn,
|
||||||
StoryOut,
|
StoryOut,
|
||||||
|
StoryPatch,
|
||||||
SubscriberOut,
|
SubscriberOut,
|
||||||
TeamMemberIn,
|
TeamMemberIn,
|
||||||
TeamMemberOut,
|
TeamMemberOut,
|
||||||
|
TeamMemberPatch,
|
||||||
TestimonialIn,
|
TestimonialIn,
|
||||||
TestimonialOut,
|
TestimonialOut,
|
||||||
|
TestimonialPatch,
|
||||||
TimelineItemIn,
|
TimelineItemIn,
|
||||||
TimelineItemOut,
|
TimelineItemOut,
|
||||||
|
TimelineItemPatch,
|
||||||
TokenOut,
|
TokenOut,
|
||||||
WhyUsItemIn,
|
WhyUsItemIn,
|
||||||
WhyUsItemOut,
|
WhyUsItemOut,
|
||||||
|
WhyUsItemPatch,
|
||||||
)
|
)
|
||||||
|
|
||||||
# روتر عمومی برای ورود — بدون نیاز به توکن
|
# روتر عمومی برای ورود — بدون نیاز به توکن
|
||||||
@@ -86,16 +112,25 @@ def login(payload: LoginIn, db: Session = Depends(get_db)):
|
|||||||
return TokenOut(access_token=create_access_token(user.username))
|
return TokenOut(access_token=create_access_token(user.username))
|
||||||
|
|
||||||
|
|
||||||
def register_crud(path: str, model, out_schema, create_schema) -> None:
|
def register_crud(path: str, model, out_schema, create_schema, patch_schema=None) -> None:
|
||||||
"""ثبت پنج مسیر CRUD استاندارد (لیست، ساخت، دریافت، ویرایش، حذف) روی روتر مدیریت."""
|
"""ثبت مسیرهای استاندارد CRUD (لیست، ساخت، دریافت، ویرایش کامل، ویرایش جزئی،
|
||||||
|
حذف و ترتیببندی) روی روتر مدیریت."""
|
||||||
|
|
||||||
@router.get(f"/{path}", response_model=list[out_schema])
|
@router.get(f"/{path}", response_model=list[out_schema])
|
||||||
def list_items(db: Session = Depends(get_db)):
|
def list_items(db: Session = Depends(get_db)):
|
||||||
return db.query(model).order_by(model.id).all()
|
query = db.query(model)
|
||||||
|
if hasattr(model, "position"):
|
||||||
|
query = query.order_by(model.position, model.id)
|
||||||
|
else:
|
||||||
|
query = query.order_by(model.id)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
@router.post(f"/{path}", response_model=out_schema, status_code=201)
|
@router.post(f"/{path}", response_model=out_schema, status_code=201)
|
||||||
def create_item(payload: create_schema, db: Session = Depends(get_db)):
|
def create_item(payload: create_schema, db: Session = Depends(get_db)):
|
||||||
item = model(**payload.model_dump())
|
item = model(**payload.model_dump())
|
||||||
|
if hasattr(model, "position"):
|
||||||
|
# آیتم جدید در انتهای لیست قرار میگیرد
|
||||||
|
item.position = (db.query(func.max(model.position)).scalar() or 0) + 1
|
||||||
db.add(item)
|
db.add(item)
|
||||||
try:
|
try:
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -127,6 +162,38 @@ def register_crud(path: str, model, out_schema, create_schema) -> None:
|
|||||||
db.refresh(item)
|
db.refresh(item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
if patch_schema is not None:
|
||||||
|
|
||||||
|
@router.patch(f"/{path}/{{item_id}}", response_model=out_schema)
|
||||||
|
def patch_item(item_id: int, payload: patch_schema, db: Session = Depends(get_db)):
|
||||||
|
"""ویرایش جزئی — فقط فیلدهای ارسالشده بهروزرسانی میشوند (برای ویرایش زنده)."""
|
||||||
|
item = db.get(model, item_id)
|
||||||
|
if item is None:
|
||||||
|
raise HTTPException(status_code=404, detail="رکورد یافت نشد")
|
||||||
|
for key, value in payload.model_dump(exclude_unset=True).items():
|
||||||
|
setattr(item, key, value)
|
||||||
|
try:
|
||||||
|
db.commit()
|
||||||
|
except IntegrityError:
|
||||||
|
db.rollback()
|
||||||
|
raise HTTPException(status_code=409, detail="رکوردی با این مقدار تکراری وجود دارد (مثلاً slug یا ایمیل)")
|
||||||
|
db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
@router.post(f"/{path}/reorder", response_model=list[out_schema])
|
||||||
|
def reorder_items(payload: ReorderIn, db: Session = Depends(get_db)):
|
||||||
|
"""ذخیره ترتیب جدید آیتمها بر اساس لیست idهای ارسالی."""
|
||||||
|
if not hasattr(model, "position"):
|
||||||
|
raise HTTPException(status_code=400, detail="این منبع از ترتیببندی پشتیبانی نمیکند")
|
||||||
|
items = db.query(model).filter(model.id.in_(payload.ids)).all()
|
||||||
|
if len(items) != len(payload.ids):
|
||||||
|
raise HTTPException(status_code=404, detail="برخی رکوردها یافت نشدند")
|
||||||
|
by_id = {item.id: item for item in items}
|
||||||
|
for pos, item_id in enumerate(payload.ids):
|
||||||
|
by_id[item_id].position = pos
|
||||||
|
db.commit()
|
||||||
|
return db.query(model).order_by(model.position, model.id).all()
|
||||||
|
|
||||||
@router.delete(f"/{path}/{{item_id}}", status_code=204)
|
@router.delete(f"/{path}/{{item_id}}", status_code=204)
|
||||||
def delete_item(item_id: int, db: Session = Depends(get_db)):
|
def delete_item(item_id: int, db: Session = Depends(get_db)):
|
||||||
item = db.get(model, item_id)
|
item = db.get(model, item_id)
|
||||||
@@ -137,30 +204,30 @@ def register_crud(path: str, model, out_schema, create_schema) -> None:
|
|||||||
|
|
||||||
|
|
||||||
# CRUD محتوا
|
# CRUD محتوا
|
||||||
register_crud("brand", models.Brand, BrandOut, BrandIn)
|
register_crud("brand", models.Brand, BrandOut, BrandIn, BrandPatch)
|
||||||
register_crud("nav", models.NavItem, NavItemOut, NavItemIn)
|
register_crud("nav", models.NavItem, NavItemOut, NavItemIn, NavItemPatch)
|
||||||
register_crud("hero", models.Hero, HeroOut, HeroIn)
|
register_crud("hero", models.Hero, HeroOut, HeroIn, HeroPatch)
|
||||||
register_crud("story", models.StorySection, StoryOut, StoryIn)
|
register_crud("story", models.StorySection, StoryOut, StoryIn, StoryPatch)
|
||||||
register_crud("section-headings", models.SectionHeadingContent, SectionHeadingOut, SectionHeadingIn)
|
register_crud("section-headings", models.SectionHeadingContent, SectionHeadingOut, SectionHeadingIn, SectionHeadingPatch)
|
||||||
register_crud("page-heroes", models.PageHeroContent, PageHeroOut, PageHeroIn)
|
register_crud("page-heroes", models.PageHeroContent, PageHeroOut, PageHeroIn, PageHeroPatch)
|
||||||
register_crud("services", models.Service, ServiceOut, ServiceIn)
|
register_crud("services", models.Service, ServiceOut, ServiceIn, ServicePatch)
|
||||||
register_crud("projects", models.Project, ProjectOut, ProjectIn)
|
register_crud("projects", models.Project, ProjectOut, ProjectIn, ProjectPatch)
|
||||||
register_crud("articles", models.Article, ArticleOut, ArticleIn)
|
register_crud("articles", models.Article, ArticleOut, ArticleIn, ArticlePatch)
|
||||||
register_crud("testimonials", models.Testimonial, TestimonialOut, TestimonialIn)
|
register_crud("testimonials", models.Testimonial, TestimonialOut, TestimonialIn, TestimonialPatch)
|
||||||
register_crud("faqs", models.Faq, FaqOut, FaqIn)
|
register_crud("faqs", models.Faq, FaqOut, FaqIn, FaqPatch)
|
||||||
register_crud("team", models.TeamMember, TeamMemberOut, TeamMemberIn)
|
register_crud("team", models.TeamMember, TeamMemberOut, TeamMemberIn, TeamMemberPatch)
|
||||||
register_crud("timeline", models.TimelineItem, TimelineItemOut, TimelineItemIn)
|
register_crud("timeline", models.TimelineItem, TimelineItemOut, TimelineItemIn, TimelineItemPatch)
|
||||||
register_crud("stats", models.Stat, StatOut, StatIn)
|
register_crud("stats", models.Stat, StatOut, StatIn, StatPatch)
|
||||||
register_crud("why-us", models.WhyUsItem, WhyUsItemOut, WhyUsItemIn)
|
register_crud("why-us", models.WhyUsItem, WhyUsItemOut, WhyUsItemIn, WhyUsItemPatch)
|
||||||
register_crud("before-after", models.BeforeAfter, BeforeAfterOut, BeforeAfterIn)
|
register_crud("before-after", models.BeforeAfter, BeforeAfterOut, BeforeAfterIn, BeforeAfterPatch)
|
||||||
register_crud("investment-models", models.InvestmentModel, InvestmentModelOut, InvestmentModelIn)
|
register_crud("investment-models", models.InvestmentModel, InvestmentModelOut, InvestmentModelIn, InvestmentModelPatch)
|
||||||
register_crud("process-steps", models.ProcessStep, ProcessStepOut, ProcessStepIn)
|
register_crud("process-steps", models.ProcessStep, ProcessStepOut, ProcessStepIn, ProcessStepPatch)
|
||||||
register_crud("offices", models.Office, OfficeOut, OfficeIn)
|
register_crud("offices", models.Office, OfficeOut, OfficeIn, OfficePatch)
|
||||||
register_crud("gallery", models.GalleryImage, GalleryImageOut, GalleryImageIn)
|
register_crud("gallery", models.GalleryImage, GalleryImageOut, GalleryImageIn, GalleryImagePatch)
|
||||||
register_crud("status-labels", models.StatusLabel, StatusLabelOut, StatusLabelIn)
|
register_crud("status-labels", models.StatusLabel, StatusLabelOut, StatusLabelIn, StatusLabelPatch)
|
||||||
register_crud("contact-section", models.ContactSectionContent, ContactSectionOut, ContactSectionIn)
|
register_crud("contact-section", models.ContactSectionContent, ContactSectionOut, ContactSectionIn, ContactSectionPatch)
|
||||||
register_crud("newsletter", models.NewsletterSectionContent, NewsletterOut, NewsletterSectionIn)
|
register_crud("newsletter", models.NewsletterSectionContent, NewsletterOut, NewsletterSectionIn, NewsletterSectionPatch)
|
||||||
register_crud("footer", models.FooterContent, FooterOut, FooterIn)
|
register_crud("footer", models.FooterContent, FooterOut, FooterIn, FooterPatch)
|
||||||
|
|
||||||
|
|
||||||
# آپلود تصویر به Object Storage (MinIO) — URL نسبی برمیگرداند تا بکاند ذخیرهسازی قابل تعویض باشد
|
# آپلود تصویر به Object Storage (MinIO) — URL نسبی برمیگرداند تا بکاند ذخیرهسازی قابل تعویض باشد
|
||||||
|
|||||||
+23
-23
@@ -37,12 +37,12 @@ def get_brand(db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
@router.get("/services", response_model=list[ServiceOut])
|
@router.get("/services", response_model=list[ServiceOut])
|
||||||
def get_services(db: Session = Depends(get_db)):
|
def get_services(db: Session = Depends(get_db)):
|
||||||
return db.query(models.Service).all()
|
return db.query(models.Service).order_by(models.Service.position, models.Service.id).all()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/projects", response_model=list[ProjectOut])
|
@router.get("/projects", response_model=list[ProjectOut])
|
||||||
def get_projects(db: Session = Depends(get_db)):
|
def get_projects(db: Session = Depends(get_db)):
|
||||||
return db.query(models.Project).order_by(models.Project.id).all()
|
return db.query(models.Project).order_by(models.Project.position, models.Project.id).all()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/projects/{slug}", response_model=ProjectOut)
|
@router.get("/projects/{slug}", response_model=ProjectOut)
|
||||||
@@ -55,7 +55,7 @@ def get_project(slug: str, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
@router.get("/articles", response_model=list[ArticleOut])
|
@router.get("/articles", response_model=list[ArticleOut])
|
||||||
def get_articles(db: Session = Depends(get_db)):
|
def get_articles(db: Session = Depends(get_db)):
|
||||||
return db.query(models.Article).order_by(models.Article.id).all()
|
return db.query(models.Article).order_by(models.Article.position, models.Article.id).all()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/articles/{slug}", response_model=ArticleOut)
|
@router.get("/articles/{slug}", response_model=ArticleOut)
|
||||||
@@ -68,32 +68,32 @@ def get_article(slug: str, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
@router.get("/testimonials", response_model=list[TestimonialOut])
|
@router.get("/testimonials", response_model=list[TestimonialOut])
|
||||||
def get_testimonials(db: Session = Depends(get_db)):
|
def get_testimonials(db: Session = Depends(get_db)):
|
||||||
return db.query(models.Testimonial).all()
|
return db.query(models.Testimonial).order_by(models.Testimonial.position, models.Testimonial.id).all()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/faqs", response_model=list[FaqOut])
|
@router.get("/faqs", response_model=list[FaqOut])
|
||||||
def get_faqs(db: Session = Depends(get_db)):
|
def get_faqs(db: Session = Depends(get_db)):
|
||||||
return db.query(models.Faq).all()
|
return db.query(models.Faq).order_by(models.Faq.position, models.Faq.id).all()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/team", response_model=list[TeamMemberOut])
|
@router.get("/team", response_model=list[TeamMemberOut])
|
||||||
def get_team(db: Session = Depends(get_db)):
|
def get_team(db: Session = Depends(get_db)):
|
||||||
return db.query(models.TeamMember).all()
|
return db.query(models.TeamMember).order_by(models.TeamMember.position, models.TeamMember.id).all()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/timeline", response_model=list[TimelineItemOut])
|
@router.get("/timeline", response_model=list[TimelineItemOut])
|
||||||
def get_timeline(db: Session = Depends(get_db)):
|
def get_timeline(db: Session = Depends(get_db)):
|
||||||
return db.query(models.TimelineItem).order_by(models.TimelineItem.id).all()
|
return db.query(models.TimelineItem).order_by(models.TimelineItem.position, models.TimelineItem.id).all()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stats", response_model=list[StatOut])
|
@router.get("/stats", response_model=list[StatOut])
|
||||||
def get_stats(db: Session = Depends(get_db)):
|
def get_stats(db: Session = Depends(get_db)):
|
||||||
return db.query(models.Stat).all()
|
return db.query(models.Stat).order_by(models.Stat.position, models.Stat.id).all()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/why-us", response_model=list[WhyUsItemOut])
|
@router.get("/why-us", response_model=list[WhyUsItemOut])
|
||||||
def get_why_us(db: Session = Depends(get_db)):
|
def get_why_us(db: Session = Depends(get_db)):
|
||||||
return db.query(models.WhyUsItem).all()
|
return db.query(models.WhyUsItem).order_by(models.WhyUsItem.position, models.WhyUsItem.id).all()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/before-after", response_model=BeforeAfterOut)
|
@router.get("/before-after", response_model=BeforeAfterOut)
|
||||||
@@ -109,25 +109,25 @@ def get_site(db: Session = Depends(get_db)):
|
|||||||
"""همه محتوای سایت در یک درخواست — مناسب برای بارگذاری اولیه فرانتاند."""
|
"""همه محتوای سایت در یک درخواست — مناسب برای بارگذاری اولیه فرانتاند."""
|
||||||
return {
|
return {
|
||||||
"brand": db.query(models.Brand).first(),
|
"brand": db.query(models.Brand).first(),
|
||||||
"nav": db.query(models.NavItem).order_by(models.NavItem.id).all(),
|
"nav": db.query(models.NavItem).order_by(models.NavItem.position, models.NavItem.id).all(),
|
||||||
"hero": db.query(models.Hero).first(),
|
"hero": db.query(models.Hero).first(),
|
||||||
"story": db.query(models.StorySection).first(),
|
"story": db.query(models.StorySection).first(),
|
||||||
"section_headings": db.query(models.SectionHeadingContent).order_by(models.SectionHeadingContent.id).all(),
|
"section_headings": db.query(models.SectionHeadingContent).order_by(models.SectionHeadingContent.id).all(),
|
||||||
"page_heroes": db.query(models.PageHeroContent).order_by(models.PageHeroContent.id).all(),
|
"page_heroes": db.query(models.PageHeroContent).order_by(models.PageHeroContent.id).all(),
|
||||||
"services": db.query(models.Service).all(),
|
"services": db.query(models.Service).order_by(models.Service.position, models.Service.id).all(),
|
||||||
"projects": db.query(models.Project).order_by(models.Project.id).all(),
|
"projects": db.query(models.Project).order_by(models.Project.position, models.Project.id).all(),
|
||||||
"articles": db.query(models.Article).order_by(models.Article.id).all(),
|
"articles": db.query(models.Article).order_by(models.Article.position, models.Article.id).all(),
|
||||||
"testimonials": db.query(models.Testimonial).all(),
|
"testimonials": db.query(models.Testimonial).order_by(models.Testimonial.position, models.Testimonial.id).all(),
|
||||||
"faqs": db.query(models.Faq).all(),
|
"faqs": db.query(models.Faq).order_by(models.Faq.position, models.Faq.id).all(),
|
||||||
"team": db.query(models.TeamMember).all(),
|
"team": db.query(models.TeamMember).order_by(models.TeamMember.position, models.TeamMember.id).all(),
|
||||||
"timeline": db.query(models.TimelineItem).order_by(models.TimelineItem.id).all(),
|
"timeline": db.query(models.TimelineItem).order_by(models.TimelineItem.position, models.TimelineItem.id).all(),
|
||||||
"stats": db.query(models.Stat).all(),
|
"stats": db.query(models.Stat).order_by(models.Stat.position, models.Stat.id).all(),
|
||||||
"why_us": db.query(models.WhyUsItem).all(),
|
"why_us": db.query(models.WhyUsItem).order_by(models.WhyUsItem.position, models.WhyUsItem.id).all(),
|
||||||
"before_after": db.query(models.BeforeAfter).first(),
|
"before_after": db.query(models.BeforeAfter).first(),
|
||||||
"investment_models": db.query(models.InvestmentModel).order_by(models.InvestmentModel.id).all(),
|
"investment_models": db.query(models.InvestmentModel).order_by(models.InvestmentModel.position, models.InvestmentModel.id).all(),
|
||||||
"process_steps": db.query(models.ProcessStep).order_by(models.ProcessStep.id).all(),
|
"process_steps": db.query(models.ProcessStep).order_by(models.ProcessStep.position, models.ProcessStep.id).all(),
|
||||||
"offices": db.query(models.Office).order_by(models.Office.id).all(),
|
"offices": db.query(models.Office).order_by(models.Office.position, models.Office.id).all(),
|
||||||
"gallery": db.query(models.GalleryImage).order_by(models.GalleryImage.id).all(),
|
"gallery": db.query(models.GalleryImage).order_by(models.GalleryImage.position, models.GalleryImage.id).all(),
|
||||||
"status_labels": db.query(models.StatusLabel).order_by(models.StatusLabel.id).all(),
|
"status_labels": db.query(models.StatusLabel).order_by(models.StatusLabel.id).all(),
|
||||||
"contact_section": db.query(models.ContactSectionContent).first(),
|
"contact_section": db.query(models.ContactSectionContent).first(),
|
||||||
"newsletter": db.query(models.NewsletterSectionContent).first(),
|
"newsletter": db.query(models.NewsletterSectionContent).first(),
|
||||||
|
|||||||
+21
-1
@@ -1,4 +1,4 @@
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine, text
|
||||||
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
||||||
|
|
||||||
from .config import settings
|
from .config import settings
|
||||||
@@ -18,3 +18,23 @@ def get_db():
|
|||||||
yield db
|
yield db
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
# جدولهای مجموعهای که ستون position دارند
|
||||||
|
POSITION_TABLES = [
|
||||||
|
"services", "projects", "articles", "testimonials", "faqs", "team",
|
||||||
|
"timeline", "stats", "why_us", "investment_models", "process_steps",
|
||||||
|
"offices", "gallery_images", "nav_items",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def run_light_migrations() -> None:
|
||||||
|
"""مهاجرتهای سبک idempotent — برای دیتابیسهای موجود که create_all آنها را
|
||||||
|
بهروز نمیکند اجرا میشود (seed و startup هر دو آن را صدا میزنند)."""
|
||||||
|
if engine.dialect.name != "postgresql":
|
||||||
|
return
|
||||||
|
with engine.begin() as conn:
|
||||||
|
for table in POSITION_TABLES:
|
||||||
|
conn.execute(
|
||||||
|
text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS position INTEGER NOT NULL DEFAULT 0")
|
||||||
|
)
|
||||||
|
|||||||
+2
-1
@@ -4,9 +4,10 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from . import models # noqa: F401 — ثبت مدلها پیش از ساخت جداول
|
from . import models # noqa: F401 — ثبت مدلها پیش از ساخت جداول
|
||||||
from .api import admin, forms, public
|
from .api import admin, forms, public
|
||||||
from .config import settings
|
from .config import settings
|
||||||
from .database import Base, engine
|
from .database import Base, engine, run_light_migrations
|
||||||
|
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
run_light_migrations()
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Aramesh Building Group API",
|
title="Aramesh Building Group API",
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class Brand(Base):
|
|||||||
class Service(Base):
|
class Service(Base):
|
||||||
__tablename__ = "services"
|
__tablename__ = "services"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
title: Mapped[str] = mapped_column(String(256))
|
title: Mapped[str] = mapped_column(String(256))
|
||||||
desc: Mapped[str] = mapped_column(Text, default="")
|
desc: Mapped[str] = mapped_column(Text, default="")
|
||||||
@@ -44,6 +46,8 @@ class Service(Base):
|
|||||||
class Project(Base):
|
class Project(Base):
|
||||||
__tablename__ = "projects"
|
__tablename__ = "projects"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
slug: Mapped[str] = mapped_column(String(128), unique=True, index=True)
|
slug: Mapped[str] = mapped_column(String(128), unique=True, index=True)
|
||||||
title: Mapped[str] = mapped_column(String(256))
|
title: Mapped[str] = mapped_column(String(256))
|
||||||
@@ -63,6 +67,8 @@ class Project(Base):
|
|||||||
class Article(Base):
|
class Article(Base):
|
||||||
__tablename__ = "articles"
|
__tablename__ = "articles"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
slug: Mapped[str] = mapped_column(String(128), unique=True, index=True)
|
slug: Mapped[str] = mapped_column(String(128), unique=True, index=True)
|
||||||
title: Mapped[str] = mapped_column(String(256))
|
title: Mapped[str] = mapped_column(String(256))
|
||||||
@@ -77,6 +83,8 @@ class Article(Base):
|
|||||||
class Testimonial(Base):
|
class Testimonial(Base):
|
||||||
__tablename__ = "testimonials"
|
__tablename__ = "testimonials"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
name: Mapped[str] = mapped_column(String(128))
|
name: Mapped[str] = mapped_column(String(128))
|
||||||
role: Mapped[str] = mapped_column(String(128), default="")
|
role: Mapped[str] = mapped_column(String(128), default="")
|
||||||
@@ -86,6 +94,8 @@ class Testimonial(Base):
|
|||||||
class Faq(Base):
|
class Faq(Base):
|
||||||
__tablename__ = "faqs"
|
__tablename__ = "faqs"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
question: Mapped[str] = mapped_column(Text)
|
question: Mapped[str] = mapped_column(Text)
|
||||||
answer: Mapped[str] = mapped_column(Text, default="")
|
answer: Mapped[str] = mapped_column(Text, default="")
|
||||||
@@ -94,6 +104,8 @@ class Faq(Base):
|
|||||||
class TeamMember(Base):
|
class TeamMember(Base):
|
||||||
__tablename__ = "team"
|
__tablename__ = "team"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
name: Mapped[str] = mapped_column(String(128))
|
name: Mapped[str] = mapped_column(String(128))
|
||||||
role: Mapped[str] = mapped_column(String(128), default="")
|
role: Mapped[str] = mapped_column(String(128), default="")
|
||||||
@@ -103,6 +115,8 @@ class TeamMember(Base):
|
|||||||
class TimelineItem(Base):
|
class TimelineItem(Base):
|
||||||
__tablename__ = "timeline"
|
__tablename__ = "timeline"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
year: Mapped[str] = mapped_column(String(32))
|
year: Mapped[str] = mapped_column(String(32))
|
||||||
title: Mapped[str] = mapped_column(String(256))
|
title: Mapped[str] = mapped_column(String(256))
|
||||||
@@ -113,6 +127,8 @@ class TimelineItem(Base):
|
|||||||
class Stat(Base):
|
class Stat(Base):
|
||||||
__tablename__ = "stats"
|
__tablename__ = "stats"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
value: Mapped[int] = mapped_column(Integer)
|
value: Mapped[int] = mapped_column(Integer)
|
||||||
suffix: Mapped[str] = mapped_column(String(16), default="")
|
suffix: Mapped[str] = mapped_column(String(16), default="")
|
||||||
@@ -122,6 +138,8 @@ class Stat(Base):
|
|||||||
class WhyUsItem(Base):
|
class WhyUsItem(Base):
|
||||||
__tablename__ = "why_us"
|
__tablename__ = "why_us"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
title: Mapped[str] = mapped_column(String(256))
|
title: Mapped[str] = mapped_column(String(256))
|
||||||
desc: Mapped[str] = mapped_column(Text, default="")
|
desc: Mapped[str] = mapped_column(Text, default="")
|
||||||
@@ -142,6 +160,8 @@ class BeforeAfter(Base):
|
|||||||
class NavItem(Base):
|
class NavItem(Base):
|
||||||
__tablename__ = "nav_items"
|
__tablename__ = "nav_items"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
label: Mapped[str] = mapped_column(String(64))
|
label: Mapped[str] = mapped_column(String(64))
|
||||||
href: Mapped[str] = mapped_column(String(128))
|
href: Mapped[str] = mapped_column(String(128))
|
||||||
@@ -215,6 +235,8 @@ class PageHeroContent(Base):
|
|||||||
class InvestmentModel(Base):
|
class InvestmentModel(Base):
|
||||||
__tablename__ = "investment_models"
|
__tablename__ = "investment_models"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
title: Mapped[str] = mapped_column(String(256))
|
title: Mapped[str] = mapped_column(String(256))
|
||||||
desc: Mapped[str] = mapped_column(Text, default="")
|
desc: Mapped[str] = mapped_column(Text, default="")
|
||||||
@@ -224,6 +246,8 @@ class InvestmentModel(Base):
|
|||||||
class ProcessStep(Base):
|
class ProcessStep(Base):
|
||||||
__tablename__ = "process_steps"
|
__tablename__ = "process_steps"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
step: Mapped[str] = mapped_column(String(16), default="")
|
step: Mapped[str] = mapped_column(String(16), default="")
|
||||||
title: Mapped[str] = mapped_column(String(256))
|
title: Mapped[str] = mapped_column(String(256))
|
||||||
@@ -233,6 +257,8 @@ class ProcessStep(Base):
|
|||||||
class Office(Base):
|
class Office(Base):
|
||||||
__tablename__ = "offices"
|
__tablename__ = "offices"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
city: Mapped[str] = mapped_column(String(64))
|
city: Mapped[str] = mapped_column(String(64))
|
||||||
address: Mapped[str] = mapped_column(Text, default="")
|
address: Mapped[str] = mapped_column(Text, default="")
|
||||||
@@ -242,6 +268,8 @@ class Office(Base):
|
|||||||
class GalleryImage(Base):
|
class GalleryImage(Base):
|
||||||
__tablename__ = "gallery_images"
|
__tablename__ = "gallery_images"
|
||||||
|
|
||||||
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
src: Mapped[str] = mapped_column(Text, default="")
|
src: Mapped[str] = mapped_column(Text, default="")
|
||||||
alt: Mapped[str] = mapped_column(Text, default="")
|
alt: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
|||||||
+43
-2
@@ -1,7 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
from pydantic import BaseModel, ConfigDict, EmailStr, Field, create_model
|
||||||
|
|
||||||
# ---------- پایه ----------
|
# ---------- پایه ----------
|
||||||
|
|
||||||
@@ -493,3 +493,44 @@ class FooterIn(BaseModel):
|
|||||||
|
|
||||||
class MessageReadIn(BaseModel):
|
class MessageReadIn(BaseModel):
|
||||||
is_read: bool = True
|
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)
|
||||||
|
ArticlePatch = _make_patch("ArticlePatch", ArticleIn)
|
||||||
|
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]
|
||||||
|
|||||||
+7
-3
@@ -14,7 +14,7 @@ from typing import Any
|
|||||||
from . import models, seed_data
|
from . import models, seed_data
|
||||||
from .auth import hash_password
|
from .auth import hash_password
|
||||||
from .config import settings
|
from .config import settings
|
||||||
from .database import Base, SessionLocal, engine
|
from .database import Base, SessionLocal, engine, run_light_migrations
|
||||||
|
|
||||||
# (مدل، دادههای اولیه، برچسب گزارش)
|
# (مدل، دادههای اولیه، برچسب گزارش)
|
||||||
CONTENT: list[tuple[Any, list[dict], str]] = [
|
CONTENT: list[tuple[Any, list[dict], str]] = [
|
||||||
@@ -53,12 +53,16 @@ def seed(force: bool = False) -> None:
|
|||||||
print("جداول قبلی حذف شدند.")
|
print("جداول قبلی حذف شدند.")
|
||||||
|
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
run_light_migrations()
|
||||||
|
|
||||||
for model, rows, label in CONTENT:
|
for model, rows, label in CONTENT:
|
||||||
if db.query(model).count() > 0:
|
if db.query(model).count() > 0:
|
||||||
continue
|
continue
|
||||||
for row in rows:
|
for idx, row in enumerate(rows):
|
||||||
db.add(model(**row))
|
payload = dict(row)
|
||||||
|
if hasattr(model, "position") and "position" not in payload:
|
||||||
|
payload["position"] = idx
|
||||||
|
db.add(model(**payload))
|
||||||
|
|
||||||
if db.query(models.AdminUser).count() == 0:
|
if db.query(models.AdminUser).count() == 0:
|
||||||
db.add(
|
db.add(
|
||||||
|
|||||||
Reference in New Issue
Block a user