191 lines
6.8 KiB
Python
191 lines
6.8 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.deps import (
|
|
ensure_class_access,
|
|
ensure_class_module_enabled,
|
|
ensure_class_permission,
|
|
get_effective_class_permissions,
|
|
require_role,
|
|
resolve_class_id_for_user,
|
|
)
|
|
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", "teacher", "student")),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
effective_class_id = resolve_class_id_for_user(user, class_id)
|
|
if effective_class_id is None:
|
|
return PageResponse(items=[], total=0, page=page, page_size=page_size, total_pages=0)
|
|
ensure_class_access(user, effective_class_id)
|
|
await ensure_class_module_enabled(db, effective_class_id, "votes")
|
|
|
|
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", "teacher", "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 = resolve_class_id_for_user(user, class_id)
|
|
if effective_class_id is None:
|
|
raise HTTPException(status_code=400, detail="You are not assigned to a class")
|
|
ensure_class_access(user, effective_class_id)
|
|
await ensure_class_module_enabled(db, effective_class_id, "votes")
|
|
|
|
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", "teacher", "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")
|
|
ensure_class_access(user, vote.class_id)
|
|
await ensure_class_module_enabled(db, vote.class_id, "votes")
|
|
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", "teacher", "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")
|
|
ensure_class_access(user, vote.class_id)
|
|
await ensure_class_module_enabled(db, vote.class_id, "votes")
|
|
|
|
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", "teacher", "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")
|
|
await ensure_class_module_enabled(db, vote.class_id, "votes")
|
|
# Only creator or admin can close
|
|
can_manage = "vote_manage" in get_effective_class_permissions(user, vote.class_id)
|
|
if not can_manage and user.role == "student" and vote.creator_id != user.id:
|
|
raise HTTPException(status_code=403, detail="只有创建者或管理员可以关闭投票")
|
|
ensure_class_access(user, vote.class_id)
|
|
|
|
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", "teacher", "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")
|
|
await ensure_class_module_enabled(db, vote.class_id, "votes")
|
|
can_manage = "vote_manage" in get_effective_class_permissions(user, vote.class_id)
|
|
if not can_manage and user.role == "student" and vote.creator_id != user.id:
|
|
raise HTTPException(status_code=403, detail="只有创建者或管理员可以删除投票")
|
|
ensure_class_access(user, vote.class_id)
|
|
|
|
await delete_vote(db, vote)
|
|
return {"message": "投票已删除"}
|