hku-class-hub/backend/app/api/votes.py
2026-04-12 11:56:39 +08:00

178 lines
6.4 KiB
Python

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": "投票已删除"}