Files
voidea/app/api/v1/collaboration.py

205 lines
6.3 KiB
Python

"""Collaboration API routes for VoIdea.
Voting, comments, activity log, and notifications.
"""
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.dependencies import get_current_user, get_db
from app.models.collaboration import Notification
from app.models.user import User
from app.schemas.collaboration import (
ActivityResponse,
CommentCreate,
CommentResponse,
NotificationResponse,
NotificationUnreadCount,
VoteCount,
VoteResponse,
VoteToggleRequest,
)
from app.services.collaboration_service import CollaborationService
from app.services.idea_service import IdeaService
router = APIRouter()
@router.post("/ideas/{idea_id}/vote", response_model=VoteCount)
async def toggle_vote(
idea_id: str,
body: VoteToggleRequest,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = CollaborationService(db)
idea_svc = IdeaService(db)
idea = await idea_svc.get_by_id(idea_id)
if not idea or str(idea.user_id) != str(user.id):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Idea not found")
await svc.toggle_vote(UUID(idea_id), user.id, body.vote)
counts = await svc.get_vote_counts(UUID(idea_id))
user_vote = await svc.get_user_vote(UUID(idea_id), user.id)
return VoteCount(**counts, user_vote=user_vote)
@router.get("/ideas/{idea_id}/votes", response_model=VoteCount)
async def get_votes(
idea_id: str,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = CollaborationService(db)
idea_svc = IdeaService(db)
idea = await idea_svc.get_by_id(idea_id)
if not idea or str(idea.user_id) != str(user.id):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Idea not found")
counts = await svc.get_vote_counts(UUID(idea_id))
user_vote = await svc.get_user_vote(UUID(idea_id), user.id)
return VoteCount(**counts, user_vote=user_vote)
# ── Comments ──
@router.get("/ideas/{idea_id}/comments", response_model=list[CommentResponse])
async def list_comments(
idea_id: str,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = CollaborationService(db)
return await svc.list_comments(UUID(idea_id))
@router.post("/ideas/{idea_id}/comments", response_model=CommentResponse, status_code=status.HTTP_201_CREATED)
async def create_comment(
idea_id: str,
body: CommentCreate,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = CollaborationService(db)
parent_id = UUID(body.parent_id) if body.parent_id else None
c = await svc.create_comment(UUID(idea_id), user.id, body.content, parent_id)
await svc.log_activity(
UUID(idea_id), user.id, "comment",
{"comment_id": str(c.id), "preview": body.content[:100]},
)
return CommentResponse(
id=str(c.id),
idea_id=str(c.idea_id),
user_id=str(c.user_id),
content=c.content,
parent_id=str(c.parent_id) if c.parent_id else None,
display_name=user.display_name,
avatar_url=user.avatar_url,
created_at=c.created_at,
updated_at=c.updated_at,
)
@router.delete("/ideas/{idea_id}/comments/{comment_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_comment(
idea_id: str,
comment_id: str,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = CollaborationService(db)
success = await svc.delete_comment(UUID(comment_id), user.id)
if not success:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Comment not found")
# ── Activity ──
@router.get("/ideas/{idea_id}/activity", response_model=list[ActivityResponse])
async def list_activity(
idea_id: str,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
limit: int = Query(50, ge=1, le=200),
):
svc = CollaborationService(db)
return await svc.list_activities(UUID(idea_id), limit=limit)
# ── Notifications ──
@router.get("/notifications", response_model=list[NotificationResponse])
async def list_notifications(
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
unread_only: bool = Query(False),
):
svc = CollaborationService(db)
notifications = await svc.list_notifications(user.id, unread_only=unread_only)
return [
NotificationResponse(
id=str(n.id),
type=n.type,
title=n.title,
body=n.body,
link=n.link,
is_read=n.is_read,
created_at=n.created_at,
)
for n in notifications
]
@router.get("/notifications/unread-count", response_model=NotificationUnreadCount)
async def unread_count(
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = CollaborationService(db)
count = await svc.get_unread_count(user.id)
return NotificationUnreadCount(count=count)
@router.post("/notifications/{notification_id}/read", response_model=NotificationResponse)
async def mark_read(
notification_id: str,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = CollaborationService(db)
success = await svc.mark_notification_read(UUID(notification_id), user.id)
if not success:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notification not found")
result = await db.execute(
select(Notification).where(Notification.id == UUID(notification_id))
)
n = result.scalar_one()
return NotificationResponse(
id=str(n.id), type=n.type, title=n.title, body=n.body,
link=n.link, is_read=n.is_read, created_at=n.created_at,
)
@router.post("/notifications/read-all", response_model=NotificationUnreadCount)
async def mark_all_read(
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = CollaborationService(db)
count = await svc.mark_all_read(user.id)
return NotificationUnreadCount(count=count)