mitlist/be/app/api/v1/endpoints/items.py
2025-05-07 23:30:23 +02:00

175 lines
7.3 KiB
Python

# app/api/v1/endpoints/items.py
import logging
from typing import List as PyList, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Response, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.api.dependencies import get_current_user
# --- Import Models Correctly ---
from app.models import User as UserModel
from app.models import Item as ItemModel # <-- IMPORT Item and alias it
# --- End Import Models ---
from app.schemas.item import ItemCreate, ItemUpdate, ItemPublic
from app.crud import item as crud_item
from app.crud import list as crud_list
from app.core.exceptions import ItemNotFoundError, ListPermissionError, ConflictError
logger = logging.getLogger(__name__)
router = APIRouter()
# --- Helper Dependency for Item Permissions ---
# Now ItemModel is defined before being used as a type hint
async def get_item_and_verify_access(
item_id: int,
db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user)
) -> ItemModel:
"""Dependency to get an item and verify the user has access to its list."""
item_db = await crud_item.get_item_by_id(db, item_id=item_id)
if not item_db:
raise ItemNotFoundError(item_id)
# Check permission on the parent list
try:
await crud_list.check_list_permission(db=db, list_id=item_db.list_id, user_id=current_user.id)
except ListPermissionError as e:
# Re-raise with a more specific message
raise ListPermissionError(item_db.list_id, "access this item's list")
return item_db
# --- Endpoints ---
@router.post(
"/lists/{list_id}/items", # Nested under lists
response_model=ItemPublic,
status_code=status.HTTP_201_CREATED,
summary="Add Item to List",
tags=["Items"]
)
async def create_list_item(
list_id: int,
item_in: ItemCreate,
db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user),
):
"""Adds a new item to a specific list. User must have access to the list."""
logger.info(f"User {current_user.email} adding item to list {list_id}: {item_in.name}")
# Verify user has access to the target list
try:
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
except ListPermissionError as e:
# Re-raise with a more specific message
raise ListPermissionError(list_id, "add items to this list")
created_item = await crud_item.create_item(
db=db, item_in=item_in, list_id=list_id, user_id=current_user.id
)
logger.info(f"Item '{created_item.name}' (ID: {created_item.id}) added to list {list_id} by user {current_user.email}.")
return created_item
@router.get(
"/lists/{list_id}/items", # Nested under lists
response_model=PyList[ItemPublic],
summary="List Items in List",
tags=["Items"]
)
async def read_list_items(
list_id: int,
db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user),
# Add sorting/filtering params later if needed: sort_by: str = 'created_at', order: str = 'asc'
):
"""Retrieves all items for a specific list if the user has access."""
logger.info(f"User {current_user.email} listing items for list {list_id}")
# Verify user has access to the list
try:
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
except ListPermissionError as e:
# Re-raise with a more specific message
raise ListPermissionError(list_id, "view items in this list")
items = await crud_item.get_items_by_list_id(db=db, list_id=list_id)
return items
@router.put(
"/items/{item_id}", # Operate directly on item ID
response_model=ItemPublic,
summary="Update Item",
tags=["Items"],
responses={
status.HTTP_409_CONFLICT: {"description": "Conflict: Item has been modified by someone else"}
}
)
async def update_item(
item_id: int, # Item ID from path
item_in: ItemUpdate,
item_db: ItemModel = Depends(get_item_and_verify_access), # Use dependency to get item and check list access
db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), # Need user ID for completed_by
):
"""
Updates an item's details (name, quantity, is_complete, price).
User must have access to the list the item belongs to.
The client MUST provide the current `version` of the item in the `item_in` payload.
If the version does not match, a 409 Conflict is returned.
Sets/unsets `completed_by_id` based on `is_complete` flag.
"""
logger.info(f"User {current_user.email} attempting to update item ID: {item_id} with version {item_in.version}")
# Permission check is handled by get_item_and_verify_access dependency
try:
updated_item = await crud_item.update_item(
db=db, item_db=item_db, item_in=item_in, user_id=current_user.id
)
logger.info(f"Item {item_id} updated successfully by user {current_user.email} to version {updated_item.version}.")
return updated_item
except ConflictError as e:
logger.warning(f"Conflict updating item {item_id} for user {current_user.email}: {str(e)}")
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
except Exception as e:
logger.error(f"Error updating item {item_id} for user {current_user.email}: {str(e)}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected error occurred while updating the item.")
@router.delete(
"/items/{item_id}", # Operate directly on item ID
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete Item",
tags=["Items"],
responses={
status.HTTP_409_CONFLICT: {"description": "Conflict: Item has been modified, cannot delete specified version"}
}
)
async def delete_item(
item_id: int, # Item ID from path
expected_version: Optional[int] = Query(None, description="The expected version of the item to delete for optimistic locking."),
item_db: ItemModel = Depends(get_item_and_verify_access), # Use dependency to get item and check list access
db: AsyncSession = Depends(get_db),
current_user: UserModel = Depends(get_current_user), # Log who deleted it
):
"""
Deletes an item. User must have access to the list the item belongs to.
If `expected_version` is provided and does not match the item's current version,
a 409 Conflict is returned.
"""
logger.info(f"User {current_user.email} attempting to delete item ID: {item_id}, expected version: {expected_version}")
# Permission check is handled by get_item_and_verify_access dependency
if expected_version is not None and item_db.version != expected_version:
logger.warning(
f"Conflict deleting item {item_id} for user {current_user.email}. "
f"Expected version {expected_version}, actual version {item_db.version}."
)
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Item has been modified. Expected version {expected_version}, but current version is {item_db.version}. Please refresh."
)
await crud_item.delete_item(db=db, item_db=item_db)
logger.info(f"Item {item_id} (version {item_db.version}) deleted successfully by user {current_user.email}.")
return Response(status_code=status.HTTP_204_NO_CONTENT)