Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/adapters/api/v1/admin/notification_templates.py
2026-04-14 19:34:55 +03:30

281 lines
9.2 KiB
Python
Executable file

from typing import Any, Dict, Optional, Literal
from fastapi import APIRouter, Depends, Request, Body, HTTPException, Query
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from adapters.db.session import get_db
from app.core.auth_dependency import get_current_user, AuthContext
from app.core.permissions import require_app_permission
from app.core.responses import success_response
from adapters.db.repositories.notification_repo import NotificationTemplateRepository
router = APIRouter(prefix="/admin/notification-templates", tags=["admin.notification_templates"])
class TemplatePayload(BaseModel):
event_key: str = Field(..., max_length=100)
channel: str = Field(..., max_length=32)
locale: Optional[str] = Field(default=None, max_length=10)
subject: Optional[str] = Field(default=None, max_length=200)
body: str
is_active: bool = True
@staticmethod
def validate_channel(ch: str) -> str:
allowed = {"telegram", "bale", "email", "sms", "inapp"}
if ch not in allowed:
raise HTTPException(status_code=400, detail=f"کانال نامعتبر است. مقادیر مجاز: {', '.join(sorted(allowed))}")
return ch
@staticmethod
def validate_locale(loc: str | None) -> str | None:
if loc is None:
return None
allowed = {"fa", "en", "ar", "tr"}
loc_lower = loc.lower().strip()
if loc_lower not in allowed:
raise HTTPException(status_code=400, detail=f"زبان نامعتبر است. مقادیر مجاز: {', '.join(sorted(allowed))}")
return loc_lower
def model_post_init(self, __context: Any) -> None: # type: ignore[override]
self.channel = self.validate_channel(self.channel)
self.locale = self.validate_locale(self.locale)
@router.get("", summary="لیست قالب‌ها")
@require_app_permission("system_settings")
def list_templates(
request: Request,
event_key: Optional[str] = Query(None, description="فیلتر بر اساس event_key"),
channel: Optional[str] = Query(None, description="فیلتر بر اساس channel"),
is_active: Optional[bool] = Query(None, description="فیلتر بر اساس وضعیت فعال/غیرفعال"),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> Dict[str, Any]:
repo = NotificationTemplateRepository(db)
items = repo.list(event_key=event_key, channel=channel, is_active=is_active)
data = [
{
"id": t.id,
"event_key": t.event_key,
"channel": t.channel,
"locale": t.locale,
"subject": t.subject,
"body": t.body,
"is_active": t.is_active,
"created_at": t.created_at,
"updated_at": t.updated_at,
}
for t in items
]
return success_response({"items": data}, request)
@router.post("/list", summary="لیست قالب‌ها با QueryInfo")
@require_app_permission("system_settings")
def list_templates_with_query(
request: Request,
query_info: Dict[str, Any] = Body(...),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> Dict[str, Any]:
from adapters.api.v1.schemas import QueryInfo as QueryInfoSchema
from sqlalchemy import or_, and_
repo = NotificationTemplateRepository(db)
# تبدیل QueryInfo به فیلترها
query = QueryInfoSchema(**query_info)
# استخراج فیلترها
event_key = None
channel = None
is_active = None
if query.filters:
for f in query.filters:
if f.property == "event_key" and f.operator == "=":
event_key = f.value
elif f.property == "channel" and f.operator == "=":
channel = f.value
elif f.property == "is_active" and f.operator == "=":
is_active = f.value
# جستجو در event_key, channel, subject, body
items = repo.list(event_key=event_key, channel=channel, is_active=is_active)
# اعمال جستجو
if query.search:
search_lower = query.search.lower()
items = [
t for t in items
if (
(search_lower in (t.event_key or "").lower()) or
(search_lower in (t.channel or "").lower()) or
(search_lower in (t.subject or "").lower() if t.subject else False) or
(search_lower in (t.body or "").lower())
)
]
# مرتب‌سازی
if query.sort_by:
reverse = query.sort_desc
if query.sort_by == "event_key":
items.sort(key=lambda x: x.event_key or "", reverse=reverse)
elif query.sort_by == "channel":
items.sort(key=lambda x: x.channel or "", reverse=reverse)
elif query.sort_by == "created_at":
items.sort(key=lambda x: x.created_at, reverse=reverse)
elif query.sort_by == "updated_at":
items.sort(key=lambda x: x.updated_at, reverse=reverse)
# صفحه‌بندی
total = len(items)
skip = query.skip or 0
take = query.take or 20
items = items[skip:skip + take]
data = [
{
"id": t.id,
"event_key": t.event_key,
"channel": t.channel,
"locale": t.locale,
"subject": t.subject,
"body": t.body,
"is_active": t.is_active,
"created_at": t.created_at,
"updated_at": t.updated_at,
}
for t in items
]
# محاسبه pagination
page = (skip // take) + 1
total_pages = (total + take - 1) // take
return success_response({
"items": data,
"pagination": {
"total": total,
"page": page,
"per_page": take,
"total_pages": total_pages,
"has_next": page < total_pages,
"has_prev": page > 1,
}
}, request)
@router.post("", summary="ایجاد قالب")
@require_app_permission("system_settings")
def create_template(
request: Request,
payload: TemplatePayload = Body(...),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> Dict[str, Any]:
repo = NotificationTemplateRepository(db)
# بررسی duplicate
if repo.exists(event_key=payload.event_key, channel=payload.channel, locale=payload.locale):
raise HTTPException(
status_code=400,
detail=f"قالب با event_key='{payload.event_key}', channel='{payload.channel}', locale='{payload.locale or 'None'}' قبلاً وجود دارد"
)
obj = repo.create(
event_key=payload.event_key,
channel=payload.channel,
locale=payload.locale,
subject=payload.subject,
body=payload.body,
is_active=payload.is_active,
)
return success_response({"id": obj.id}, request)
@router.put("/{template_id}", summary="ویرایش قالب")
@require_app_permission("system_settings")
def update_template(
request: Request,
template_id: int,
payload: TemplatePayload = Body(...),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> Dict[str, Any]:
repo = NotificationTemplateRepository(db)
obj = repo.get_by_id(template_id)
if not obj:
raise HTTPException(status_code=404, detail="قالب یافت نشد")
# بررسی duplicate (به جز خود قالب فعلی)
if repo.exists(event_key=payload.event_key, channel=payload.channel, locale=payload.locale, exclude_id=template_id):
raise HTTPException(
status_code=400,
detail=f"قالب با event_key='{payload.event_key}', channel='{payload.channel}', locale='{payload.locale or 'None'}' قبلاً وجود دارد"
)
obj = repo.update(
obj,
event_key=payload.event_key,
channel=payload.channel,
locale=payload.locale,
subject=payload.subject,
body=payload.body,
is_active=payload.is_active,
)
return success_response({"ok": True}, request)
@router.delete("/{template_id}", summary="حذف قالب")
@require_app_permission("system_settings")
def delete_template(
template_id: int,
request: Request,
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> Dict[str, Any]:
repo = NotificationTemplateRepository(db)
obj = repo.get_by_id(template_id)
if not obj:
raise HTTPException(status_code=404, detail="قالب یافت نشد")
repo.delete(obj)
return success_response({"ok": True}, request)
@router.post("/preview", summary="پیش‌نمایش رندر قالب نوتیفیکیشن")
@require_app_permission("system_settings")
def preview_template(
request: Request,
body: Dict[str, Any] = Body(...),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> Dict[str, Any]:
from jinja2.sandbox import SandboxedEnvironment
from jinja2 import StrictUndefined, BaseLoader, TemplateSyntaxError, UndefinedError
channel_raw = (body or {}).get("channel") or ""
subject = (body or {}).get("subject") or ""
text = (body or {}).get("body") or ""
context = (body or {}).get("context") or {}
# اگر channel خالی است، از یک کانال پیش‌فرض استفاده می‌کنیم
channel = str(channel_raw).strip() if channel_raw else "email"
# اعتبارسنجی کانال (اگر channel وجود دارد)
if channel:
try:
TemplatePayload.validate_channel(channel)
except HTTPException:
# اگر کانال نامعتبر است، از email به عنوان پیش‌فرض استفاده می‌کنیم
channel = "email"
try:
env = SandboxedEnvironment(loader=BaseLoader(), autoescape=True, undefined=StrictUndefined, enable_async=False)
subj = ""
if subject:
subj = env.from_string(str(subject)).render(**context)
out = env.from_string(str(text)).render(**context)
return success_response({"channel": channel, "subject": subj, "body": out}, request)
except (TemplateSyntaxError, UndefinedError) as e:
raise HTTPException(status_code=400, detail=f"خطا در قالب: {e}")
except Exception as e:
raise HTTPException(status_code=400, detail=f"خطا در رندر: {e}")