from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from sqlalchemy.orm import selectinload from app.dependencies import get_db, get_current_user from app.models.subject import Subject, SubjectOption from app.models.user import User from app.schemas.subject import ( SubjectCreate, SubjectOut, SubjectUpdate, SubjectOptionCreate, SubjectOptionUpdate, SubjectOptionOut, ) router = APIRouter(prefix="/api/subjects", tags=["subjects"]) def _opts(): return selectinload(Subject.options) @router.get("", response_model=list[SubjectOut]) async def list_subjects( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(Subject) .where(Subject.user_id == current_user.id) .options(_opts()) .order_by(Subject.name) ) return result.scalars().all() @router.post("", response_model=SubjectOut, status_code=status.HTTP_201_CREATED) async def create_subject( body: SubjectCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): subject = Subject(**body.model_dump(), user_id=current_user.id) db.add(subject) await db.commit() result = await db.execute( select(Subject).where(Subject.id == subject.id).options(_opts()) ) return result.scalar_one() @router.get("/{subject_id}", response_model=SubjectOut) async def get_subject( subject_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(Subject) .where(Subject.id == subject_id, Subject.user_id == current_user.id) .options(_opts()) ) subject = result.scalar_one_or_none() if not subject: raise HTTPException(status_code=404, detail="Subject not found") return subject @router.patch("/{subject_id}", response_model=SubjectOut) async def update_subject( subject_id: int, body: SubjectUpdate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(Subject) .where(Subject.id == subject_id, Subject.user_id == current_user.id) .options(_opts()) ) subject = result.scalar_one_or_none() if not subject: raise HTTPException(status_code=404, detail="Subject not found") for field, value in body.model_dump(exclude_none=True).items(): setattr(subject, field, value) await db.commit() result = await db.execute( select(Subject).where(Subject.id == subject.id).options(_opts()) ) return result.scalar_one() @router.delete("/{subject_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_subject( subject_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(Subject).where(Subject.id == subject_id, Subject.user_id == current_user.id) ) subject = result.scalar_one_or_none() if not subject: raise HTTPException(status_code=404, detail="Subject not found") if subject.is_system: raise HTTPException(status_code=403, detail="System subjects cannot be deleted") await db.delete(subject) await db.commit() # --- Subject Option sub-routes --- @router.post("/{subject_id}/options", response_model=SubjectOptionOut, status_code=status.HTTP_201_CREATED) async def add_option( subject_id: int, body: SubjectOptionCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(Subject).where(Subject.id == subject_id, Subject.user_id == current_user.id) ) if not result.scalar_one_or_none(): raise HTTPException(status_code=404, detail="Subject not found") option = SubjectOption(subject_id=subject_id, **body.model_dump()) db.add(option) await db.commit() await db.refresh(option) return option @router.patch("/{subject_id}/options/{option_id}", response_model=SubjectOptionOut) async def update_option( subject_id: int, option_id: int, body: SubjectOptionUpdate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(SubjectOption) .join(Subject) .where( SubjectOption.id == option_id, SubjectOption.subject_id == subject_id, Subject.user_id == current_user.id, ) ) option = result.scalar_one_or_none() if not option: raise HTTPException(status_code=404, detail="Option not found") for field, value in body.model_dump(exclude_unset=True).items(): setattr(option, field, value) await db.commit() await db.refresh(option) return option @router.delete("/{subject_id}/options/{option_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_option( subject_id: int, option_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(SubjectOption) .join(Subject) .where( SubjectOption.id == option_id, SubjectOption.subject_id == subject_id, Subject.user_id == current_user.id, ) ) option = result.scalar_one_or_none() if not option: raise HTTPException(status_code=404, detail="Option not found") await db.delete(option) await db.commit()