- مقالهها ستون status (draft/published) گرفتند — GET /api/site و همه انتهای عمومی فقط منتشرشده را برمیگردانند؛ autosave دیگر پیشنویس نیمهکاره را زنده نمیکند - کلمه کلیدی هدف چکلیست سئو فیلد seo_keyphrase شد (قبلاً localStorage بود) - robots.txt و sitemap.xml از بکاند + سرو /blog و /projects با تزریق title/description/OG/JSON-LD از دیتابیس برای خزندههای بدون JS (app/seo_pages.py) - فرانت: applySeo در seo.ts (OG/توییتر/canonical/JSON-LD)، ادغام پیشنویس برای ادمین، برچسب پیشنویس در وبلاگ، حالت خالی وبلاگ، دکمه انتشار در صفحه نوشتن - تغییر رمز ادمین POST /api/auth/change-password + دیالوگ «رمز» در نوار مدیریت - محدودسازی ورود: ۵ تلاش ناموفق در ۱۵ دقیقه از هر IP → 429 - ساخت دکمه + برای ردیف سئوی صفحههای بدون ردیف (SingletonPill) - تستهای pytest (۱۵ مورد) + پینکردن requirements + حذف package.json سرگردان فرانت
72 lines
2.7 KiB
Python
72 lines
2.7 KiB
Python
"""مهاجرتهای سبک دیتابیس — DDL و تبدیل دادههای قدیمی؛ همه idempotent.
|
|
|
|
هم main.py (startup) و هم seed.py اینها را صدا میزنند تا هر مسیر اجرایی
|
|
دیتابیس موجود را بدون از دست رفتن داده ارتقا دهد.
|
|
"""
|
|
|
|
from sqlalchemy import text
|
|
|
|
from .database import SessionLocal, engine
|
|
|
|
# جدولهای مجموعهای که ستون position دارند
|
|
POSITION_TABLES = [
|
|
"services", "projects", "articles", "testimonials", "faqs", "team",
|
|
"timeline", "stats", "why_us", "investment_models", "process_steps",
|
|
"offices", "gallery_images", "nav_items",
|
|
]
|
|
|
|
|
|
def run_ddl_migrations() -> None:
|
|
"""ستونهای جدید — برای PostgreSQL با ADD COLUMN IF NOT EXISTS."""
|
|
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")
|
|
)
|
|
# مقالات موجود منتشر شدهاند؛ فقط مقالههای تازه میتوانند پیشنویس باشند
|
|
conn.execute(
|
|
text("ALTER TABLE articles ADD COLUMN IF NOT EXISTS status VARCHAR(16) NOT NULL DEFAULT 'published'")
|
|
)
|
|
conn.execute(
|
|
text("ALTER TABLE articles ADD COLUMN IF NOT EXISTS seo_keyphrase VARCHAR(128) NOT NULL DEFAULT ''")
|
|
)
|
|
|
|
|
|
def paragraphs_to_doc(paragraphs: list) -> dict:
|
|
"""تبدیل لیست پاراگرافهای متنی (فرمت قدیمی بدنه مقاله) به سند TipTap."""
|
|
content = []
|
|
for para in paragraphs:
|
|
if isinstance(para, str) and para.strip():
|
|
content.append({
|
|
"type": "paragraph",
|
|
"content": [{"type": "text", "text": para}],
|
|
})
|
|
if not content:
|
|
content = [{"type": "paragraph"}]
|
|
return {"type": "doc", "content": content}
|
|
|
|
|
|
def migrate_article_bodies() -> None:
|
|
"""تبدیل بدنه مقالات از لیست پاراگراف به سند JSON — idempotent."""
|
|
from . import models # lazy — جلوگیری از import حلقهای
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
changed = 0
|
|
for article in db.query(models.Article).all():
|
|
if isinstance(article.body, list):
|
|
article.body = paragraphs_to_doc(article.body)
|
|
changed += 1
|
|
db.commit()
|
|
if changed:
|
|
print(f"مقالات تبدیلشده به فرمت سند: {changed}")
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def run_all() -> None:
|
|
run_ddl_migrations()
|
|
migrate_article_bodies()
|