hku-class-hub/backend/app/api/timeline.py
2026-04-11 12:52:23 +08:00

159 lines
5.4 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status
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.timeline import TimelineCreate, TimelineUpdate, TimelineOut
from app.schemas.common import PageResponse
from app.services.timeline_service import (
create_timeline,
update_timeline,
delete_timeline,
get_timeline_by_id,
list_timelines,
add_images_to_timeline,
)
from app.services.cos_service import upload_image
router = APIRouter(prefix="/api/timeline", tags=["timeline"])
@router.get("/", response_model=PageResponse[TimelineOut])
async def get_timelines(
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)
posts, total = await list_timelines(db, effective_class_id, page, page_size)
total_pages = (total + page_size - 1) // page_size
items = []
for p in posts:
items.append(
TimelineOut(
id=p.id,
class_id=p.class_id,
author_id=p.author_id,
author_name=p.author.name if p.author else "Unknown",
title=p.title,
content=p.content,
image_urls=p.get_image_urls_list(),
created_at=p.created_at,
updated_at=p.updated_at,
)
)
return PageResponse(
items=items, total=total, page=page, page_size=page_size, total_pages=total_pages
)
@router.post("/", response_model=TimelineOut)
async def create_new_timeline(
data: TimelineCreate,
class_id: int | None = None,
user: User = Depends(require_role("super_admin", "class_admin")),
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:
raise HTTPException(status_code=400, detail="You are not assigned to a class")
post = await create_timeline(db, effective_class_id, user.id, data)
return TimelineOut(
id=post.id,
class_id=post.class_id,
author_id=post.author_id,
author_name=user.name,
title=post.title,
content=post.content,
image_urls=[],
created_at=post.created_at,
updated_at=post.updated_at,
)
@router.post("/{post_id}/images")
async def upload_timeline_images(
post_id: int,
files: list[UploadFile] = File(...),
user: User = Depends(require_role("super_admin", "class_admin")),
db: AsyncSession = Depends(get_db),
):
post = await get_timeline_by_id(db, post_id)
if post is None:
raise HTTPException(status_code=404, detail="Timeline post not found")
# Verify post belongs to user's class (super_admin can access any)
if user.role != "super_admin" and post.class_id != user.class_id:
raise HTTPException(status_code=403, detail="Access denied")
urls = []
for f in files:
contents = await f.read()
if len(contents) > 10 * 1024 * 1024: # 10MB limit
raise HTTPException(
status_code=400, detail=f"File {f.filename} too large (max 10MB)"
)
if f.content_type not in {"image/jpeg", "image/png", "image/gif", "image/webp"}:
raise HTTPException(
status_code=400, detail=f"File {f.filename} has invalid type"
)
url = upload_image(
f"timeline/{post_id}", f.filename or "image.jpg", contents, f.content_type
)
urls.append(url)
await add_images_to_timeline(db, post, urls)
return {"image_urls": urls}
@router.put("/{post_id}", response_model=TimelineOut)
async def update_existing_timeline(
post_id: int,
data: TimelineUpdate,
user: User = Depends(require_role("super_admin", "class_admin")),
db: AsyncSession = Depends(get_db),
):
post = await get_timeline_by_id(db, post_id)
if post is None:
raise HTTPException(status_code=404, detail="Timeline post not found")
if user.role != "super_admin" and post.class_id != user.class_id:
raise HTTPException(status_code=403, detail="Access denied")
updated = await update_timeline(db, post, data)
return TimelineOut(
id=updated.id,
class_id=updated.class_id,
author_id=updated.author_id,
author_name=user.name,
title=updated.title,
content=updated.content,
image_urls=updated.get_image_urls_list(),
created_at=updated.created_at,
updated_at=updated.updated_at,
)
@router.delete("/{post_id}")
async def delete_existing_timeline(
post_id: int,
user: User = Depends(require_role("super_admin", "class_admin")),
db: AsyncSession = Depends(get_db),
):
post = await get_timeline_by_id(db, post_id)
if post is None:
raise HTTPException(status_code=404, detail="Timeline post not found")
if user.role != "super_admin" and post.class_id != user.class_id:
raise HTTPException(status_code=403, detail="Access denied")
await delete_timeline(db, post)
return {"message": "Timeline post deleted"}