Step 11:百问百答(FAQ / Knowledge Base)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import faq_category as category_crud
|
||||
from app.crud import member as member_crud
|
||||
from app.schemas.faq import CategoryCreate, CategoryRead, CategoryUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _check_permission_for_scope(study_id: uuid.UUID | None, current_user, member_role: str | None):
|
||||
if study_id is None:
|
||||
if current_user.role != "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only admin can manage global FAQ")
|
||||
else:
|
||||
if current_user.role != "ADMIN" and member_role != "PM":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=CategoryRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_category(
|
||||
payload: CategoryCreate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> CategoryRead:
|
||||
member_role = None
|
||||
if payload.study_id:
|
||||
member = await member_crud.get_member(db, payload.study_id, current_user.id)
|
||||
if not member:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
member_role = member.role_in_study
|
||||
_check_permission_for_scope(payload.study_id, current_user, member_role)
|
||||
category = await category_crud.create_category(db, payload)
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=payload.study_id,
|
||||
entity_type="faq_category",
|
||||
entity_id=category.id,
|
||||
action="CREATE_FAQ_CATEGORY",
|
||||
detail=f"FAQ category {category.name} created",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
return CategoryRead.model_validate(category)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=list[CategoryRead],
|
||||
)
|
||||
async def list_categories(
|
||||
study_id: uuid.UUID | None = None,
|
||||
include_global: bool = True,
|
||||
is_active: bool | None = True,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[CategoryRead]:
|
||||
if study_id:
|
||||
member = await member_crud.get_member(db, study_id, current_user.id)
|
||||
if not member and current_user.role != "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
categories = await category_crud.list_categories(db, study_id, include_global=include_global, is_active=is_active)
|
||||
return [CategoryRead.model_validate(c) for c in categories]
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{category_id}",
|
||||
response_model=CategoryRead,
|
||||
)
|
||||
async def update_category(
|
||||
category_id: uuid.UUID,
|
||||
payload: CategoryUpdate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> CategoryRead:
|
||||
category = await category_crud.get_category(db, category_id)
|
||||
if not category:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Category not found")
|
||||
member_role = None
|
||||
if category.study_id:
|
||||
from app.crud import member as member_crud
|
||||
member = await member_crud.get_member(db, category.study_id, current_user.id)
|
||||
member_role = member.role_in_study if member else None
|
||||
_check_permission_for_scope(category.study_id, current_user, member_role)
|
||||
updated = await category_crud.update_category(db, category, payload)
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=category.study_id,
|
||||
entity_type="faq_category",
|
||||
entity_id=category_id,
|
||||
action="UPDATE_FAQ_CATEGORY",
|
||||
detail=f"FAQ category {category_id} updated",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
return CategoryRead.model_validate(updated)
|
||||
@@ -0,0 +1,182 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import faq_category as category_crud
|
||||
from app.crud import faq_item as faq_crud
|
||||
from app.crud import member as member_crud
|
||||
from app.schemas.faq import FaqCreate, FaqRead, FaqUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _check_write_permission(study_id: uuid.UUID | None, current_user, member_role: str | None):
|
||||
if study_id is None:
|
||||
if current_user.role != "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only admin can manage global FAQ")
|
||||
else:
|
||||
if current_user.role != "ADMIN" and member_role != "PM":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=FaqRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_faq(
|
||||
payload: FaqCreate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> FaqRead:
|
||||
cat = await category_crud.get_category(db, payload.category_id)
|
||||
if not cat:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Category not found")
|
||||
if cat.study_id != payload.study_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Category scope mismatch")
|
||||
member_role = None
|
||||
if payload.study_id:
|
||||
member = await member_crud.get_member(db, payload.study_id, current_user.id)
|
||||
member_role = member.role_in_study if member else None
|
||||
_check_write_permission(payload.study_id, current_user, member_role)
|
||||
try:
|
||||
item = await faq_crud.create_item(db, payload, created_by=current_user.id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=payload.study_id,
|
||||
entity_type="faq_item",
|
||||
entity_id=item.id,
|
||||
action="CREATE_FAQ_ITEM",
|
||||
detail="FAQ created",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
return FaqRead.model_validate(item)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=list[FaqRead],
|
||||
)
|
||||
async def list_faqs(
|
||||
study_id: uuid.UUID | None = None,
|
||||
category_id: uuid.UUID | None = None,
|
||||
keyword: str | None = None,
|
||||
is_active: bool | None = True,
|
||||
study_scope: str | None = "project",
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[FaqRead]:
|
||||
membership_cache: dict[uuid.UUID, str | None] = {}
|
||||
|
||||
async def _get_member_role(sid: uuid.UUID) -> str | None:
|
||||
if sid in membership_cache:
|
||||
return membership_cache[sid]
|
||||
member = await member_crud.get_member(db, sid, current_user.id)
|
||||
role = member.role_in_study if member else None
|
||||
membership_cache[sid] = role
|
||||
return role
|
||||
|
||||
if study_id:
|
||||
if current_user.role != "ADMIN":
|
||||
role = await _get_member_role(study_id)
|
||||
if not role:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
|
||||
if is_active is False and current_user.role != "ADMIN":
|
||||
role = None
|
||||
if study_id:
|
||||
role = await _get_member_role(study_id)
|
||||
if role != "PM":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
|
||||
items = await faq_crud.list_items(
|
||||
db,
|
||||
study_id=study_id,
|
||||
category_id=category_id,
|
||||
keyword=keyword,
|
||||
is_active=is_active,
|
||||
study_scope=study_scope,
|
||||
)
|
||||
|
||||
visible: list[FaqRead] = []
|
||||
for it in items:
|
||||
if it.study_id and current_user.role != "ADMIN":
|
||||
role = await _get_member_role(it.study_id)
|
||||
if not role:
|
||||
continue
|
||||
if not it.is_active and current_user.role != "ADMIN":
|
||||
role = await _get_member_role(it.study_id) if it.study_id else None
|
||||
if role != "PM":
|
||||
continue
|
||||
visible.append(FaqRead.model_validate(it))
|
||||
return visible
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{item_id}",
|
||||
response_model=FaqRead,
|
||||
)
|
||||
async def get_faq(
|
||||
item_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> FaqRead:
|
||||
item = await faq_crud.get_item(db, item_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found")
|
||||
if item.study_id and current_user.role != "ADMIN":
|
||||
member = await member_crud.get_member(db, item.study_id, current_user.id)
|
||||
if not member:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
if not item.is_active and current_user.role not in {"ADMIN"}:
|
||||
member_role = None
|
||||
if item.study_id:
|
||||
member = await member_crud.get_member(db, item.study_id, current_user.id)
|
||||
member_role = member.role_in_study if member else None
|
||||
if member_role != "PM":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Inactive FAQ")
|
||||
return FaqRead.model_validate(item)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{item_id}",
|
||||
response_model=FaqRead,
|
||||
)
|
||||
async def update_faq(
|
||||
item_id: uuid.UUID,
|
||||
payload: FaqUpdate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> FaqRead:
|
||||
item = await faq_crud.get_item(db, item_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found")
|
||||
member_role = None
|
||||
if item.study_id:
|
||||
member = await member_crud.get_member(db, item.study_id, current_user.id)
|
||||
member_role = member.role_in_study if member else None
|
||||
_check_write_permission(item.study_id, current_user, member_role)
|
||||
old_active = item.is_active
|
||||
updated = await faq_crud.update_item(db, item, payload)
|
||||
action = "UPDATE_FAQ_ITEM"
|
||||
detail = "FAQ updated"
|
||||
if payload.is_active is not None and payload.is_active != old_active:
|
||||
action = "FAQ_STATUS_CHANGE"
|
||||
detail = "FAQ disabled" if not payload.is_active else "FAQ enabled"
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=item.study_id,
|
||||
entity_type="faq_item",
|
||||
entity_id=item_id,
|
||||
action=action,
|
||||
detail=detail,
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
return FaqRead.model_validate(updated)
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard, subjects, visits, aes, issues, data_queries, verifications, imp_products, imp_batches, imp_inventory, imp_transactions, finance, finance_dashboard
|
||||
from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard, subjects, visits, aes, issues, data_queries, verifications, imp_products, imp_batches, imp_inventory, imp_transactions, finance, finance_dashboard, faq_categories, faqs
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
@@ -26,3 +26,5 @@ api_router.include_router(imp_inventory.router, prefix="/studies/{study_id}/imp/
|
||||
api_router.include_router(imp_transactions.router, prefix="/studies/{study_id}/imp/transactions", tags=["imp-transactions"])
|
||||
api_router.include_router(finance.router, prefix="/studies/{study_id}/finance", tags=["finance"])
|
||||
api_router.include_router(finance_dashboard.router, prefix="/studies/{study_id}/finance", tags=["finance"])
|
||||
api_router.include_router(faq_categories.router, prefix="/faqs/categories", tags=["faq-categories"])
|
||||
api_router.include_router(faqs.router, prefix="/faqs/items", tags=["faqs"])
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select, update as sa_update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.faq_category import FaqCategory
|
||||
from app.schemas.faq import CategoryCreate, CategoryUpdate
|
||||
|
||||
|
||||
async def create_category(db: AsyncSession, category_in: CategoryCreate) -> FaqCategory:
|
||||
category = FaqCategory(
|
||||
study_id=category_in.study_id,
|
||||
name=category_in.name,
|
||||
description=category_in.description,
|
||||
sort_order=category_in.sort_order,
|
||||
is_active=category_in.is_active,
|
||||
)
|
||||
db.add(category)
|
||||
await db.commit()
|
||||
await db.refresh(category)
|
||||
return category
|
||||
|
||||
|
||||
async def list_categories(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID | None,
|
||||
include_global: bool = True,
|
||||
is_active: bool | None = True,
|
||||
) -> Sequence[FaqCategory]:
|
||||
stmt = select(FaqCategory)
|
||||
if include_global:
|
||||
stmt = stmt.where((FaqCategory.study_id == study_id) | (FaqCategory.study_id.is_(None)))
|
||||
else:
|
||||
stmt = stmt.where(FaqCategory.study_id == study_id)
|
||||
if is_active is not None:
|
||||
stmt = stmt.where(FaqCategory.is_active == is_active)
|
||||
stmt = stmt.order_by(FaqCategory.sort_order, FaqCategory.name)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def get_category(db: AsyncSession, category_id: uuid.UUID) -> FaqCategory | None:
|
||||
result = await db.execute(select(FaqCategory).where(FaqCategory.id == category_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def update_category(db: AsyncSession, category: FaqCategory, category_in: CategoryUpdate) -> FaqCategory:
|
||||
update_data = category_in.model_dump(exclude_unset=True)
|
||||
if update_data:
|
||||
await db.execute(
|
||||
sa_update(FaqCategory)
|
||||
.where(FaqCategory.id == category.id)
|
||||
.values(**update_data)
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(category)
|
||||
return category
|
||||
@@ -0,0 +1,90 @@
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import or_, select, update as sa_update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.faq_category import FaqCategory
|
||||
from app.models.faq_item import FaqItem
|
||||
from app.schemas.faq import FaqCreate, FaqUpdate
|
||||
|
||||
|
||||
async def create_item(db: AsyncSession, item_in: FaqCreate, *, created_by: uuid.UUID) -> FaqItem:
|
||||
# ensure category exists and matches study scope
|
||||
category = await db.get(FaqCategory, item_in.category_id)
|
||||
if not category:
|
||||
raise ValueError("Category not found")
|
||||
if category.study_id != item_in.study_id:
|
||||
# allow global category if item is global (None) or project if matches
|
||||
if not (category.study_id is None and item_in.study_id is None):
|
||||
raise ValueError("Category scope mismatch")
|
||||
|
||||
item = FaqItem(
|
||||
study_id=item_in.study_id,
|
||||
category_id=item_in.category_id,
|
||||
question=item_in.question,
|
||||
answer=item_in.answer,
|
||||
keywords=item_in.keywords,
|
||||
version=1,
|
||||
is_active=True,
|
||||
created_by=created_by,
|
||||
)
|
||||
db.add(item)
|
||||
await db.commit()
|
||||
await db.refresh(item)
|
||||
return item
|
||||
|
||||
|
||||
async def get_item(db: AsyncSession, item_id: uuid.UUID) -> FaqItem | None:
|
||||
result = await db.execute(select(FaqItem).where(FaqItem.id == item_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_items(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
study_id: uuid.UUID | None,
|
||||
category_id: uuid.UUID | None = None,
|
||||
keyword: str | None = None,
|
||||
is_active: bool | None = True,
|
||||
study_scope: str | None = "project",
|
||||
) -> Sequence[FaqItem]:
|
||||
stmt = select(FaqItem).join(FaqCategory, FaqCategory.id == FaqItem.category_id)
|
||||
|
||||
if study_scope == "all":
|
||||
pass
|
||||
elif study_scope == "global":
|
||||
stmt = stmt.where(FaqItem.study_id.is_(None))
|
||||
else: # project (default)
|
||||
stmt = stmt.where((FaqItem.study_id == study_id) | (FaqItem.study_id.is_(None)))
|
||||
|
||||
if category_id:
|
||||
stmt = stmt.where(FaqItem.category_id == category_id)
|
||||
if keyword:
|
||||
like_pattern = f"%{keyword}%"
|
||||
stmt = stmt.where(
|
||||
or_(
|
||||
FaqItem.question.ilike(like_pattern),
|
||||
FaqItem.answer.ilike(like_pattern),
|
||||
FaqItem.keywords.ilike(like_pattern),
|
||||
)
|
||||
)
|
||||
if is_active is not None:
|
||||
stmt = stmt.where(FaqItem.is_active == is_active)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def update_item(db: AsyncSession, item: FaqItem, item_in: FaqUpdate) -> FaqItem:
|
||||
update_data = item_in.model_dump(exclude_unset=True)
|
||||
if "question" in update_data or "answer" in update_data or "keywords" in update_data:
|
||||
update_data["version"] = item.version + 1
|
||||
if update_data:
|
||||
await db.execute(
|
||||
sa_update(FaqItem)
|
||||
.where(FaqItem.id == item.id)
|
||||
.values(**update_data)
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(item)
|
||||
return item
|
||||
@@ -21,3 +21,5 @@ from app.models.imp_batch import ImpBatch # noqa: F401
|
||||
from app.models.imp_inventory import ImpInventory # noqa: F401
|
||||
from app.models.imp_transaction import ImpTransaction # noqa: F401
|
||||
from app.models.finance import FinanceItem # noqa: F401
|
||||
from app.models.faq_category import FaqCategory # noqa: F401
|
||||
from app.models.faq_item import FaqItem # noqa: F401
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class FaqCategory(Base):
|
||||
__tablename__ = "faq_categories"
|
||||
__table_args__ = (UniqueConstraint("study_id", "name", name="uq_faq_category_name"),)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
study_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), nullable=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
sort_order: Mapped[int] = mapped_column(nullable=False, default=0)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class FaqItem(Base):
|
||||
__tablename__ = "faq_items"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
study_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), nullable=True, index=True)
|
||||
category_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("faq_categories.id"), index=True, nullable=False)
|
||||
question: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
answer: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
keywords: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class CategoryCreate(BaseModel):
|
||||
study_id: Optional[uuid.UUID] = None
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
sort_order: int = 0
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class CategoryUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
sort_order: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class CategoryRead(BaseModel):
|
||||
id: uuid.UUID
|
||||
study_id: Optional[uuid.UUID]
|
||||
name: str
|
||||
description: Optional[str]
|
||||
sort_order: int
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class FaqCreate(BaseModel):
|
||||
study_id: Optional[uuid.UUID] = None
|
||||
category_id: uuid.UUID
|
||||
question: str
|
||||
answer: str
|
||||
keywords: Optional[str] = None
|
||||
|
||||
|
||||
class FaqUpdate(BaseModel):
|
||||
question: Optional[str] = None
|
||||
answer: Optional[str] = None
|
||||
keywords: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class FaqRead(BaseModel):
|
||||
id: uuid.UUID
|
||||
study_id: Optional[uuid.UUID]
|
||||
category_id: uuid.UUID
|
||||
question: str
|
||||
answer: str
|
||||
keywords: Optional[str]
|
||||
version: int
|
||||
is_active: bool
|
||||
created_by: uuid.UUID
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
Reference in New Issue
Block a user