feat: implement Instagram clone SocialPhoto API
- FastAPI backend with SQLite database - JWT authentication (register, login) - User profiles with follow/unfollow - Posts with image upload and likes/dislikes - Comments with likes - Global and personalized feed - Comprehensive pytest test suite (37 tests) TASK-ID: 758f4029-702
This commit is contained in:
205
app/routes/users.py
Normal file
205
app/routes/users.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""User routes for SocialPhoto."""
|
||||
import sqlite3
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
from app.auth import get_current_user_id
|
||||
from app.database import get_db, row_to_dict
|
||||
from app.models import PostResponse, UserResponse, UserStats
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["Users"])
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
def _get_user_with_stats(conn: sqlite3.Connection, user_id: int) -> dict:
|
||||
"""Get user data with stats."""
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get user
|
||||
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found",
|
||||
)
|
||||
user = row_to_dict(row)
|
||||
|
||||
# Get posts count
|
||||
cursor.execute("SELECT COUNT(*) as count FROM posts WHERE user_id = ?", (user_id,))
|
||||
posts_count = cursor.fetchone()["count"]
|
||||
|
||||
# Get followers count
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) as count FROM follows WHERE following_id = ?",
|
||||
(user_id,),
|
||||
)
|
||||
followers_count = cursor.fetchone()["count"]
|
||||
|
||||
# Get following count
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) as count FROM follows WHERE follower_id = ?",
|
||||
(user_id,),
|
||||
)
|
||||
following_count = cursor.fetchone()["count"]
|
||||
|
||||
user["posts_count"] = posts_count
|
||||
user["followers_count"] = followers_count
|
||||
user["following_count"] = following_count
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
async def get_user(
|
||||
user_id: int,
|
||||
conn: sqlite3.Connection = Depends(get_db),
|
||||
) -> UserResponse:
|
||||
"""Get user profile."""
|
||||
user = _get_user_with_stats(conn, user_id)
|
||||
return UserResponse(**user)
|
||||
|
||||
|
||||
@router.get("/{user_id}/posts", response_model=List[PostResponse])
|
||||
async def get_user_posts(
|
||||
user_id: int,
|
||||
conn: sqlite3.Connection = Depends(get_db),
|
||||
) -> List[PostResponse]:
|
||||
"""Get all posts by a user."""
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check user exists
|
||||
cursor.execute("SELECT id FROM users WHERE id = ?", (user_id,))
|
||||
if not cursor.fetchone():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
# Get posts with counts
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT
|
||||
p.id, p.user_id, u.username, p.image_path, p.caption, p.created_at,
|
||||
(SELECT COUNT(*) FROM likes WHERE post_id = p.id) as likes_count,
|
||||
(SELECT COUNT(*) FROM dislikes WHERE post_id = p.id) as dislikes_count,
|
||||
(SELECT COUNT(*) FROM comments WHERE post_id = p.id) as comments_count
|
||||
FROM posts p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.user_id = ?
|
||||
ORDER BY p.created_at DESC
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
|
||||
posts = []
|
||||
for row in cursor.fetchall():
|
||||
post = row_to_dict(row)
|
||||
posts.append(
|
||||
PostResponse(
|
||||
id=post["id"],
|
||||
user_id=post["user_id"],
|
||||
username=post["username"],
|
||||
image_url=f"/uploads/{post['image_path'].split('/')[-1]}",
|
||||
caption=post["caption"],
|
||||
likes_count=post["likes_count"],
|
||||
dislikes_count=post["dislikes_count"],
|
||||
comments_count=post["comments_count"],
|
||||
created_at=post["created_at"],
|
||||
)
|
||||
)
|
||||
|
||||
return posts
|
||||
|
||||
|
||||
@router.get("/{user_id}/stats", response_model=UserStats)
|
||||
async def get_user_stats(
|
||||
user_id: int,
|
||||
conn: sqlite3.Connection = Depends(get_db),
|
||||
) -> UserStats:
|
||||
"""Get user statistics."""
|
||||
user = _get_user_with_stats(conn, user_id)
|
||||
return UserStats(
|
||||
posts_count=user["posts_count"],
|
||||
followers_count=user["followers_count"],
|
||||
following_count=user["following_count"],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{user_id}/follow", status_code=status.HTTP_201_CREATED)
|
||||
async def follow_user(
|
||||
user_id: int,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
conn: sqlite3.Connection = Depends(get_db),
|
||||
) -> dict:
|
||||
"""Follow a user."""
|
||||
current_user_id = await get_current_user_id(credentials)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check user exists
|
||||
cursor.execute("SELECT id FROM users WHERE id = ?", (user_id,))
|
||||
if not cursor.fetchone():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
# Cannot follow yourself
|
||||
if current_user_id == user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="You cannot follow yourself",
|
||||
)
|
||||
|
||||
# Check if already following
|
||||
cursor.execute(
|
||||
"SELECT id FROM follows WHERE follower_id = ? AND following_id = ?",
|
||||
(current_user_id, user_id),
|
||||
)
|
||||
if cursor.fetchone():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="You already follow this user",
|
||||
)
|
||||
|
||||
# Create follow
|
||||
cursor.execute(
|
||||
"INSERT INTO follows (follower_id, following_id) VALUES (?, ?)",
|
||||
(current_user_id, user_id),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
return {"message": "Successfully followed user"}
|
||||
|
||||
|
||||
@router.delete("/{user_id}/follow", status_code=status.HTTP_200_OK)
|
||||
async def unfollow_user(
|
||||
user_id: int,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
conn: sqlite3.Connection = Depends(get_db),
|
||||
) -> dict:
|
||||
"""Unfollow a user."""
|
||||
current_user_id = await get_current_user_id(credentials)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if following
|
||||
cursor.execute(
|
||||
"SELECT id FROM follows WHERE follower_id = ? AND following_id = ?",
|
||||
(current_user_id, user_id),
|
||||
)
|
||||
if not cursor.fetchone():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="You are not following this user",
|
||||
)
|
||||
|
||||
# Delete follow
|
||||
cursor.execute(
|
||||
"DELETE FROM follows WHERE follower_id = ? AND following_id = ?",
|
||||
(current_user_id, user_id),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
return {"message": "Successfully unfollowed user"}
|
||||
Reference in New Issue
Block a user