from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import require_role from app.db.database import get_db from app.db.models import User from app.schemas.vote import VoteCreate, VoteUpdate, VoteSubmit, VoteOptionOut, VoteOut from app.schemas.common import PageResponse from app.services.vote_service import ( create_vote, get_vote_by_id, list_votes, submit_vote, close_vote, delete_vote, ) router = APIRouter(prefix="/api/votes", tags=["votes"]) def _build_vote_out(vote: any, user_id: int) -> VoteOut: # Compute per-option stats options_out = [] for opt in (vote.options or []): responses = opt.responses or [] vote_count = len(responses) voter_names = None if not vote.is_anonymous: voter_names = [r.voter.name for r in responses if r.voter] options_out.append(VoteOptionOut( id=opt.id, content=opt.content, sort_order=opt.sort_order, vote_count=vote_count, voter_names=voter_names, )) # Total unique voters all_voter_ids = set() my_option_ids = [] for opt in (vote.options or []): for r in (opt.responses or []): all_voter_ids.add(r.voter_id) if r.voter_id == user_id: my_option_ids.append(opt.id) return VoteOut( id=vote.id, class_id=vote.class_id, creator_id=vote.creator_id, creator_name=vote.creator.name if vote.creator else "Unknown", title=vote.title, description=vote.description, vote_type=vote.vote_type, is_anonymous=vote.is_anonymous, max_choices=vote.max_choices, deadline=vote.deadline, status=vote.status, total_voters=len(all_voter_ids), has_voted=user_id in all_voter_ids, my_option_ids=my_option_ids if my_option_ids else None, options=options_out, created_at=vote.created_at, updated_at=vote.updated_at, ) @router.get("/", response_model=PageResponse[VoteOut]) async def get_votes( page: int = 1, page_size: int = 20, class_id: int | None = None, user: User = Depends(require_role("super_admin", "class_admin", "student")), db: AsyncSession = Depends(get_db), ): effective_class_id = class_id if user.role == "super_admin" and class_id else user.class_id if effective_class_id is None: return PageResponse(items=[], total=0, page=page, page_size=page_size, total_pages=0) votes, total = await list_votes(db, effective_class_id, page, page_size) total_pages = (total + page_size - 1) // page_size items = [_build_vote_out(v, user.id) for v in votes] return PageResponse(items=items, total=total, page=page, page_size=page_size, total_pages=total_pages) @router.post("/", response_model=VoteOut) async def create_new_vote( data: VoteCreate, class_id: int | None = None, user: User = Depends(require_role("super_admin", "class_admin", "student")), db: AsyncSession = Depends(get_db), ): if len(data.options) < 2: raise HTTPException(status_code=400, detail="至少需要 2 个选项") if data.vote_type == "multiple" and data.max_choices < 2: raise HTTPException(status_code=400, detail="多选投票最多可选数不能小于 2") effective_class_id = class_id if user.role == "super_admin" and class_id else user.class_id if effective_class_id is None: raise HTTPException(status_code=400, detail="You are not assigned to a class") vote = await create_vote(db, effective_class_id, user.id, data) # Reload with relationships vote = await get_vote_by_id(db, vote.id) return _build_vote_out(vote, user.id) @router.get("/{vote_id}", response_model=VoteOut) async def get_vote_detail( vote_id: int, user: User = Depends(require_role("super_admin", "class_admin", "student")), db: AsyncSession = Depends(get_db), ): vote = await get_vote_by_id(db, vote_id) if vote is None: raise HTTPException(status_code=404, detail="Vote not found") if user.role != "super_admin" and vote.class_id != user.class_id: raise HTTPException(status_code=403, detail="Access denied") return _build_vote_out(vote, user.id) @router.post("/{vote_id}/submit") async def submit_vote_response( vote_id: int, data: VoteSubmit, user: User = Depends(require_role("super_admin", "class_admin", "student")), db: AsyncSession = Depends(get_db), ): vote = await get_vote_by_id(db, vote_id) if vote is None: raise HTTPException(status_code=404, detail="Vote not found") if user.role != "super_admin" and vote.class_id != user.class_id: raise HTTPException(status_code=403, detail="Access denied") try: await submit_vote(db, vote_id, user.id, data.option_ids) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) return {"message": "投票成功"} @router.put("/{vote_id}/close") async def close_vote_endpoint( vote_id: int, user: User = Depends(require_role("super_admin", "class_admin", "student")), db: AsyncSession = Depends(get_db), ): vote = await get_vote_by_id(db, vote_id) if vote is None: raise HTTPException(status_code=404, detail="Vote not found") # Only creator or admin can close if user.role == "student" and vote.creator_id != user.id: raise HTTPException(status_code=403, detail="只有创建者或管理员可以关闭投票") if user.role != "super_admin" and vote.class_id != user.class_id: raise HTTPException(status_code=403, detail="Access denied") await close_vote(db, vote) return {"message": "投票已关闭"} @router.delete("/{vote_id}") async def delete_vote_endpoint( vote_id: int, user: User = Depends(require_role("super_admin", "class_admin", "student")), db: AsyncSession = Depends(get_db), ): vote = await get_vote_by_id(db, vote_id) if vote is None: raise HTTPException(status_code=404, detail="Vote not found") if user.role == "student" and vote.creator_id != user.id: raise HTTPException(status_code=403, detail="只有创建者或管理员可以删除投票") if user.role != "super_admin" and vote.class_id != user.class_id: raise HTTPException(status_code=403, detail="Access denied") await delete_vote(db, vote) return {"message": "投票已删除"}