"""Moderated comments on articles and questions.""" from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, field_validator from sqlalchemy.orm import Session from app.database import get_db from app.models.article import Article from app.models.comment import Comment from app.models.question import Question from app.models.user import User from app.services.quiz_builder import bank_question_predicate from app.utils.auth import check_rate_limit, get_current_user, require_moderator router = APIRouter() class CommentCreate(BaseModel): article_id: int | None = None question_id: int | None = None content: str @field_validator("content") @classmethod def content_shape(cls, value): value = value.strip() if not value: raise ValueError("Comment cannot be empty") if len(value) > 2000: raise ValueError("Comment is too long (max 2000 characters)") return value class CommentModerate(BaseModel): status: str @field_validator("status") @classmethod def status_shape(cls, value): if value not in ("pending", "approved", "rejected"): raise ValueError("Invalid moderation status") return value def _target(db: Session, article_id: int | None, question_id: int | None, user: User): if (article_id is None) == (question_id is None): raise HTTPException(400, "Provide exactly one of article_id or question_id") if article_id is not None: article = db.get(Article, article_id) if not article: raise HTTPException(404, "Article not found") if article.status != "published" and not user.is_moderator: raise HTTPException(404, "Article not found") return "article_id", article question = db.get(Question, question_id) if not question or not db.query(Question.id).filter( Question.id == question_id, bank_question_predicate(user)).first(): raise HTTPException(404, "Question not found") return "question_id", question @router.post("/") def create_comment( data: CommentCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): target_field, _ = _target(db, data.article_id, data.question_id, current_user) check_rate_limit( key=f"comments:daily:{current_user.id}", max_calls=20, window_seconds=86400, detail="You've reached today's comment limit. Try again tomorrow.", user=current_user, ) comment = Comment(article_id=data.article_id, question_id=data.question_id, user_id=current_user.id, content=data.content, status="pending") db.add(comment) db.commit() db.refresh(comment) return _json(db, comment, current_user) def _json(db: Session, comment: Comment, user: User): author = db.get(User, comment.user_id) if comment.user_id else None return { "id": comment.id, "article_id": comment.article_id, "question_id": comment.question_id, "user_id": comment.user_id, "author_name": author.name if author else "Unknown", "content": comment.content, "status": comment.status, "created_at": comment.created_at, "own": comment.user_id == user.id, "can_moderate": user.is_moderator, } @router.get("/") def list_comments( article_id: int | None = Query(None), question_id: int | None = Query(None), limit: int = Query(20, le=100), offset: int = Query(0), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): target_field, target = _target(db, article_id, question_id, current_user) target_id = getattr(target, "id") query = db.query(Comment).filter(getattr(Comment, target_field) == target_id).filter( (Comment.status == "approved") | (Comment.user_id == current_user.id), ) total = query.count() comments = query.order_by(Comment.created_at.desc()).offset(offset).limit(limit).all() return {"total": total, "comments": [_json(db, c, current_user) for c in comments]} @router.get("/moderation") def list_pending_comments( limit: int = Query(50, le=200), offset: int = Query(0), db: Session = Depends(get_db), current_user: User = Depends(require_moderator), ): query = db.query(Comment).filter(Comment.status == "pending") total = query.count() comments = query.order_by(Comment.created_at.asc()).offset(offset).limit(limit).all() return {"total": total, "comments": [_json(db, c, current_user) for c in comments]} @router.patch("/{comment_id}") def moderate_comment( comment_id: int, data: CommentModerate, db: Session = Depends(get_db), current_user: User = Depends(require_moderator), ): comment = db.get(Comment, comment_id) if not comment: raise HTTPException(404, "Comment not found") comment.status = data.status db.commit() return _json(db, comment, current_user) @router.delete("/{comment_id}", status_code=204) def delete_comment( comment_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): comment = db.get(Comment, comment_id) if not comment: raise HTTPException(404, "Comment not found") if comment.user_id != current_user.id and not current_user.is_moderator: raise HTTPException(403, "Not your comment") db.delete(comment) db.commit()