- CreatorFollow model + pure SQL migration 022
- Follow router: POST/DELETE /follows/{creator_id}, GET /follows/me, GET /follows/{id}/status
- follower_count on creator detail endpoint
- Follow button on CreatorDetail (authenticated users only)
- CreatorTiers page with Free/Pro/Premium cards, Coming Soon modals
- Tiers link in creator sidebar nav
- Route /creator/tiers (protected)
116 lines
4.1 KiB
Python
116 lines
4.1 KiB
Python
"""Follow system — users follow creators.
|
|
|
|
Endpoints:
|
|
POST /follows/{creator_id} — follow a creator
|
|
DELETE /follows/{creator_id} — unfollow a creator
|
|
GET /follows/{creator_id}/status — check if following
|
|
GET /follows/me — list followed creators
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy import delete, func, select
|
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from auth import get_current_user
|
|
from database import get_session
|
|
from models import Creator, CreatorFollow, User
|
|
from schemas import FollowResponse, FollowStatusResponse, FollowedCreatorItem
|
|
|
|
router = APIRouter(prefix="/follows", tags=["follows"])
|
|
|
|
|
|
async def _follower_count(session: AsyncSession, creator_id: uuid.UUID) -> int:
|
|
result = await session.execute(
|
|
select(func.count()).select_from(CreatorFollow).where(
|
|
CreatorFollow.creator_id == creator_id
|
|
)
|
|
)
|
|
return result.scalar_one()
|
|
|
|
|
|
@router.post("/{creator_id}", response_model=FollowResponse)
|
|
async def follow_creator(
|
|
creator_id: uuid.UUID,
|
|
current_user: Annotated[User, Depends(get_current_user)],
|
|
session: Annotated[AsyncSession, Depends(get_session)],
|
|
):
|
|
"""Follow a creator. Idempotent — following twice is a no-op."""
|
|
# Verify creator exists
|
|
result = await session.execute(select(Creator).where(Creator.id == creator_id))
|
|
if result.scalar_one_or_none() is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Creator not found")
|
|
|
|
# Upsert — idempotent
|
|
stmt = pg_insert(CreatorFollow).values(
|
|
user_id=current_user.id,
|
|
creator_id=creator_id,
|
|
).on_conflict_do_nothing(constraint="uq_creator_follow_user_creator")
|
|
await session.execute(stmt)
|
|
await session.commit()
|
|
|
|
count = await _follower_count(session, creator_id)
|
|
return FollowResponse(followed=True, creator_id=creator_id, follower_count=count)
|
|
|
|
|
|
@router.delete("/{creator_id}", response_model=FollowResponse)
|
|
async def unfollow_creator(
|
|
creator_id: uuid.UUID,
|
|
current_user: Annotated[User, Depends(get_current_user)],
|
|
session: Annotated[AsyncSession, Depends(get_session)],
|
|
):
|
|
"""Unfollow a creator. Idempotent — unfollowing when not following is a no-op."""
|
|
await session.execute(
|
|
delete(CreatorFollow).where(
|
|
CreatorFollow.user_id == current_user.id,
|
|
CreatorFollow.creator_id == creator_id,
|
|
)
|
|
)
|
|
await session.commit()
|
|
|
|
count = await _follower_count(session, creator_id)
|
|
return FollowResponse(followed=False, creator_id=creator_id, follower_count=count)
|
|
|
|
|
|
@router.get("/me", response_model=list[FollowedCreatorItem])
|
|
async def my_follows(
|
|
current_user: Annotated[User, Depends(get_current_user)],
|
|
session: Annotated[AsyncSession, Depends(get_session)],
|
|
):
|
|
"""List all creators the current user follows."""
|
|
result = await session.execute(
|
|
select(
|
|
CreatorFollow.creator_id,
|
|
Creator.name.label("creator_name"),
|
|
Creator.slug.label("creator_slug"),
|
|
CreatorFollow.created_at.label("followed_at"),
|
|
)
|
|
.join(Creator, CreatorFollow.creator_id == Creator.id)
|
|
.where(CreatorFollow.user_id == current_user.id)
|
|
.order_by(CreatorFollow.created_at.desc())
|
|
)
|
|
return [FollowedCreatorItem(**row._mapping) for row in result.all()]
|
|
|
|
|
|
@router.get("/{creator_id}/status", response_model=FollowStatusResponse)
|
|
async def follow_status(
|
|
creator_id: uuid.UUID,
|
|
current_user: Annotated[User, Depends(get_current_user)],
|
|
session: Annotated[AsyncSession, Depends(get_session)],
|
|
):
|
|
"""Check if the current user follows a specific creator."""
|
|
result = await session.execute(
|
|
select(CreatorFollow.id).where(
|
|
CreatorFollow.user_id == current_user.id,
|
|
CreatorFollow.creator_id == creator_id,
|
|
)
|
|
)
|
|
return FollowStatusResponse(
|
|
following=result.scalar_one_or_none() is not None,
|
|
creator_id=creator_id,
|
|
)
|