263 lines
9.4 KiB
Python
263 lines
9.4 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
|
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,
|
|
TimelineCommentCreate, TimelineCommentOut,
|
|
)
|
|
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,
|
|
toggle_like,
|
|
create_comment,
|
|
delete_comment,
|
|
get_comment_by_id,
|
|
list_comments,
|
|
)
|
|
from app.services.cos_service import upload_image
|
|
|
|
router = APIRouter(prefix="/api/timeline", tags=["timeline"])
|
|
|
|
|
|
def _build_timeline_out(p, user_id: int, include_comments: bool = False) -> TimelineOut:
|
|
like_count = len(p.likes) if p.likes else 0
|
|
has_liked = any(l.user_id == user_id for l in (p.likes or []))
|
|
comment_count = len(p.comments) if p.comments else 0
|
|
comments_out = None
|
|
if include_comments and p.comments:
|
|
comments_out = [
|
|
TimelineCommentOut(
|
|
id=c.id,
|
|
post_id=c.post_id,
|
|
author_id=c.author_id,
|
|
author_name=c.author.name if c.author else "Unknown",
|
|
content=c.content,
|
|
created_at=c.created_at,
|
|
updated_at=c.updated_at,
|
|
)
|
|
for c in p.comments
|
|
]
|
|
return 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(),
|
|
like_count=like_count,
|
|
has_liked=has_liked,
|
|
comment_count=comment_count,
|
|
comments=comments_out,
|
|
created_at=p.created_at,
|
|
updated_at=p.updated_at,
|
|
)
|
|
|
|
|
|
@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 = [_build_timeline_out(p, user.id) for p in posts]
|
|
|
|
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", "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:
|
|
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=[],
|
|
like_count=0,
|
|
has_liked=False,
|
|
comment_count=0,
|
|
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", "student")),
|
|
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")
|
|
|
|
# Student can only upload to own post; admin can upload to any in their class
|
|
if user.role == "student" and post.author_id != user.id:
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
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:
|
|
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", "student")),
|
|
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 == "student" and post.author_id != user.id:
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
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 _build_timeline_out(updated, user.id)
|
|
|
|
|
|
@router.delete("/{post_id}")
|
|
async def delete_existing_timeline(
|
|
post_id: int,
|
|
user: User = Depends(require_role("super_admin", "class_admin", "student")),
|
|
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 == "student" and post.author_id != user.id:
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
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"}
|
|
|
|
|
|
# --- Like & Comment endpoints ---
|
|
|
|
@router.post("/{post_id}/like")
|
|
async def like_timeline_post(
|
|
post_id: int,
|
|
user: User = Depends(require_role("super_admin", "class_admin", "student")),
|
|
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")
|
|
return await toggle_like(db, post_id, user.id)
|
|
|
|
|
|
@router.get("/{post_id}/comments", response_model=PageResponse[TimelineCommentOut])
|
|
async def get_post_comments(
|
|
post_id: int,
|
|
page: int = 1,
|
|
page_size: int = 50,
|
|
user: User = Depends(require_role("super_admin", "class_admin", "student")),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
comments, total = await list_comments(db, post_id, page, page_size)
|
|
total_pages = (total + page_size - 1) // page_size
|
|
items = [
|
|
TimelineCommentOut(
|
|
id=c.id,
|
|
post_id=c.post_id,
|
|
author_id=c.author_id,
|
|
author_name=c.author.name if c.author else "Unknown",
|
|
content=c.content,
|
|
created_at=c.created_at,
|
|
updated_at=c.updated_at,
|
|
)
|
|
for c in comments
|
|
]
|
|
return PageResponse(items=items, total=total, page=page, page_size=page_size, total_pages=total_pages)
|
|
|
|
|
|
@router.post("/{post_id}/comments", response_model=TimelineCommentOut)
|
|
async def add_post_comment(
|
|
post_id: int,
|
|
data: TimelineCommentCreate,
|
|
user: User = Depends(require_role("super_admin", "class_admin", "student")),
|
|
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")
|
|
|
|
comment = await create_comment(db, post_id, user.id, data)
|
|
return TimelineCommentOut(
|
|
id=comment.id,
|
|
post_id=comment.post_id,
|
|
author_id=comment.author_id,
|
|
author_name=comment.author.name if comment.author else "Unknown",
|
|
content=comment.content,
|
|
created_at=comment.created_at,
|
|
updated_at=comment.updated_at,
|
|
)
|
|
|
|
|
|
@router.delete("/comments/{comment_id}")
|
|
async def delete_timeline_comment(
|
|
comment_id: int,
|
|
user: User = Depends(require_role("super_admin", "class_admin", "student")),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
comment = await get_comment_by_id(db, comment_id)
|
|
if comment is None:
|
|
raise HTTPException(status_code=404, detail="Comment not found")
|
|
if user.role == "student" and comment.author_id != user.id:
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
|
|
await delete_comment(db, comment)
|
|
return {"message": "Comment deleted"}
|