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

165 lines
6.4 KiB
Python

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_transactional_session
from app.models import User as UserModel
from app.models import Item as ItemModel
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
from app.auth import current_active_user
logger = logging.getLogger(__name__)
router = APIRouter()
async def get_item_and_verify_access(
item_id: int,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_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)
try:
await crud_list.check_list_permission(db=db, list_id=item_db.list_id, user_id=current_user.id)
except ListPermissionError as e:
raise ListPermissionError(item_db.list_id, "access this item's list")
return item_db
@router.post(
"/lists/{list_id}/items",
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_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""Adds a new item to a specific list. User must have access to the list."""
user_email = current_user.email
logger.info(f"User {user_email} adding item to list {list_id}: {item_in.name}")
try:
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
except ListPermissionError as e:
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 {user_email}.")
return created_item
@router.get(
"/lists/{list_id}/items",
response_model=PyList[ItemPublic],
summary="List Items in List",
tags=["Items"]
)
async def read_list_items(
list_id: int,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""Retrieves all items for a specific list if the user has access."""
user_email = current_user.email
logger.info(f"User {user_email} listing items for list {list_id}")
try:
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
except ListPermissionError as e:
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(
"/lists/{list_id}/items/{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(
list_id: int,
item_id: int,
item_in: ItemUpdate,
item_db: ItemModel = Depends(get_item_and_verify_access),
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""
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.
"""
user_email = current_user.email
logger.info(f"User {user_email} attempting to update item ID: {item_id} with version {item_in.version}")
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 {user_email} to version {updated_item.version}.")
return updated_item
except ConflictError as e:
logger.warning(f"Conflict updating item {item_id} for user {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 {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(
"/lists/{list_id}/items/{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(
list_id: int,
item_id: int,
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),
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""
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.
"""
user_email = current_user.email
if expected_version is not None and item_db.version != expected_version:
logger.warning(
f"Conflict deleting item {item_id} for user {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 {user_email}.")
return Response(status_code=status.HTTP_204_NO_CONTENT)