mitlist/be/app/api/v1/endpoints/lists.py

210 lines
8.6 KiB
Python

# app/api/v1/endpoints/lists.py
import logging
from typing import List as PyList, Optional # Alias for Python List type hint
from fastapi import APIRouter, Depends, HTTPException, status, Response, Query # Added Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.auth import current_active_user
from app.models import User as UserModel
from app.schemas.list import ListCreate, ListUpdate, ListPublic, ListDetail
from app.schemas.message import Message # For simple responses
from app.crud import list as crud_list
from app.crud import group as crud_group # Need for group membership check
from app.schemas.list import ListStatus
from app.core.exceptions import (
GroupMembershipError,
ListNotFoundError,
ListPermissionError,
ListStatusNotFoundError,
ConflictError # Added ConflictError
)
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post(
"", # Route relative to prefix "/lists"
response_model=ListPublic, # Return basic list info on creation
status_code=status.HTTP_201_CREATED,
summary="Create New List",
tags=["Lists"]
)
async def create_list(
list_in: ListCreate,
db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(current_active_user),
):
"""
Creates a new shopping list.
- If `group_id` is provided, the user must be a member of that group.
- If `group_id` is null, it's a personal list.
"""
logger.info(f"User {current_user.email} creating list: {list_in.name}")
group_id = list_in.group_id
# Permission Check: If sharing with a group, verify membership
if group_id:
is_member = await crud_group.is_user_member(db, group_id=group_id, user_id=current_user.id)
if not is_member:
logger.warning(f"User {current_user.email} attempted to create list in group {group_id} but is not a member.")
raise GroupMembershipError(group_id, "create lists")
created_list = await crud_list.create_list(db=db, list_in=list_in, creator_id=current_user.id)
logger.info(f"List '{created_list.name}' (ID: {created_list.id}) created successfully for user {current_user.email}.")
return created_list
@router.get(
"", # Route relative to prefix "/lists"
response_model=PyList[ListPublic], # Return a list of basic list info
summary="List Accessible Lists",
tags=["Lists"]
)
async def read_lists(
db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(current_active_user),
# Add pagination parameters later if needed: skip: int = 0, limit: int = 100
):
"""
Retrieves lists accessible to the current user:
- Personal lists created by the user.
- Lists belonging to groups the user is a member of.
"""
logger.info(f"Fetching lists accessible to user: {current_user.email}")
lists = await crud_list.get_lists_for_user(db=db, user_id=current_user.id)
return lists
@router.get(
"/{list_id}",
response_model=ListDetail, # Return detailed list info including items
summary="Get List Details",
tags=["Lists"]
)
async def read_list(
list_id: int,
db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(current_active_user),
):
"""
Retrieves details for a specific list, including its items,
if the user has permission (creator or group member).
"""
logger.info(f"User {current_user.email} requesting details for list ID: {list_id}")
# The check_list_permission function will raise appropriate exceptions
list_db = await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
return list_db
@router.put(
"/{list_id}",
response_model=ListPublic, # Return updated basic info
summary="Update List",
tags=["Lists"],
responses={ # Add 409 to responses
status.HTTP_409_CONFLICT: {"description": "Conflict: List has been modified by someone else"}
}
)
async def update_list(
list_id: int,
list_in: ListUpdate,
db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(current_active_user),
):
"""
Updates a list's details (name, description, is_complete).
Requires user to be the creator or a member of the list's group.
The client MUST provide the current `version` of the list in the `list_in` payload.
If the version does not match, a 409 Conflict is returned.
"""
logger.info(f"User {current_user.email} attempting to update list ID: {list_id} with version {list_in.version}")
list_db = await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
try:
updated_list = await crud_list.update_list(db=db, list_db=list_db, list_in=list_in)
logger.info(f"List {list_id} updated successfully by user {current_user.email} to version {updated_list.version}.")
return updated_list
except ConflictError as e: # Catch and re-raise as HTTPException for proper FastAPI response
logger.warning(f"Conflict updating list {list_id} for user {current_user.email}: {str(e)}")
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
except Exception as e: # Catch other potential errors from crud operation
logger.error(f"Error updating list {list_id} for user {current_user.email}: {str(e)}")
# Consider a more generic error, but for now, let's keep it specific if possible
# Re-raising might be better if crud layer already raises appropriate HTTPExceptions
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected error occurred while updating the list.")
@router.delete(
"/{list_id}",
status_code=status.HTTP_204_NO_CONTENT, # Standard for successful DELETE with no body
summary="Delete List",
tags=["Lists"],
responses={ # Add 409 to responses
status.HTTP_409_CONFLICT: {"description": "Conflict: List has been modified, cannot delete specified version"}
}
)
async def delete_list(
list_id: int,
expected_version: Optional[int] = Query(None, description="The expected version of the list to delete for optimistic locking."),
db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(current_active_user),
):
"""
Deletes a list. Requires user to be the creator of the list.
If `expected_version` is provided and does not match the list's current version,
a 409 Conflict is returned.
"""
logger.info(f"User {current_user.email} attempting to delete list ID: {list_id}, expected version: {expected_version}")
# Use the helper, requiring creator permission
list_db = await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id, require_creator=True)
if expected_version is not None and list_db.version != expected_version:
logger.warning(
f"Conflict deleting list {list_id} for user {current_user.email}. "
f"Expected version {expected_version}, actual version {list_db.version}."
)
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"List has been modified. Expected version {expected_version}, but current version is {list_db.version}. Please refresh."
)
await crud_list.delete_list(db=db, list_db=list_db)
logger.info(f"List {list_id} (version: {list_db.version}) deleted successfully by user {current_user.email}.")
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get(
"/{list_id}/status",
response_model=ListStatus,
summary="Get List Status",
tags=["Lists"]
)
async def read_list_status(
list_id: int,
db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(current_active_user),
):
"""
Retrieves the last update time for the list and its items, plus item count.
Used for polling to check if a full refresh is needed.
Requires user to have permission to view the list.
"""
# Verify user has access to the list first
list_db = await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
if not list_db:
# Check if list exists at all for correct error code
exists = await crud_list.get_list_by_id(db, list_id)
if not exists:
raise ListNotFoundError(list_id)
raise ListPermissionError(list_id, "access this list's status")
# Fetch the status details
list_status = await crud_list.get_list_status(db=db, list_id=list_id)
if not list_status:
# Should not happen if check_list_permission passed, but handle defensively
logger.error(f"Could not retrieve status for list {list_id} even though permission check passed.")
raise ListStatusNotFoundError(list_id)
return list_status