230 lines
9.7 KiB
Python
230 lines
9.7 KiB
Python
# app/api/v1/endpoints/lists.py
|
|
import logging
|
|
from typing import List as PyList # Alias for Python List type hint
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Response
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.api.dependencies import get_current_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 expense as crud_expense
|
|
from app.crud import group as crud_group # Need for group membership check
|
|
from app.schemas.list import ListStatus
|
|
from app.schemas.expense import ExpenseRecordPublic
|
|
|
|
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(get_current_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 HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="You are not a member of the specified group",
|
|
)
|
|
|
|
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(get_current_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(get_current_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}")
|
|
# Use the helper to fetch and check permission simultaneously
|
|
list_db = await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
|
|
|
|
if not list_db:
|
|
# check_list_permission returns None if list not found OR permission denied
|
|
# We need to check if the list exists at all to return 404 vs 403
|
|
exists = await crud_list.get_list_by_id(db, list_id)
|
|
if not exists:
|
|
logger.warning(f"List ID {list_id} not found for request by user {current_user.email}.")
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List not found")
|
|
else:
|
|
logger.warning(f"Access denied: User {current_user.email} cannot access list {list_id}.")
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You do not have permission to access this list")
|
|
|
|
# list_db already has items loaded due to check_list_permission
|
|
return list_db
|
|
|
|
|
|
@router.put(
|
|
"/{list_id}",
|
|
response_model=ListPublic, # Return updated basic info
|
|
summary="Update List",
|
|
tags=["Lists"]
|
|
)
|
|
async def update_list(
|
|
list_id: int,
|
|
list_in: ListUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Updates a list's details (name, description, is_complete).
|
|
Requires user to be the creator or a member of the list's group.
|
|
(MVP: Allows any member to update these fields).
|
|
"""
|
|
logger.info(f"User {current_user.email} attempting to update list ID: {list_id}")
|
|
list_db = await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
|
|
|
|
if not list_db:
|
|
exists = await crud_list.get_list_by_id(db, list_id)
|
|
status_code = status.HTTP_404_NOT_FOUND if not exists else status.HTTP_403_FORBIDDEN
|
|
detail = "List not found" if not exists else "You do not have permission to update this list"
|
|
logger.warning(f"Update failed for list {list_id} by user {current_user.email}: {detail}")
|
|
raise HTTPException(status_code=status_code, detail=detail)
|
|
|
|
# Prevent changing group_id or creator via this endpoint for simplicity
|
|
# if list_in.group_id is not None or list_in.created_by_id is not None:
|
|
# raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot change group or creator via this endpoint")
|
|
|
|
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}.")
|
|
return updated_list
|
|
|
|
|
|
@router.delete(
|
|
"/{list_id}",
|
|
status_code=status.HTTP_204_NO_CONTENT, # Standard for successful DELETE with no body
|
|
summary="Delete List",
|
|
tags=["Lists"]
|
|
)
|
|
async def delete_list(
|
|
list_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Deletes a list. Requires user to be the creator of the list.
|
|
(Alternatively, could allow group owner).
|
|
"""
|
|
logger.info(f"User {current_user.email} attempting to delete list ID: {list_id}")
|
|
# 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 not list_db:
|
|
exists = await crud_list.get_list_by_id(db, list_id)
|
|
status_code = status.HTTP_404_NOT_FOUND if not exists else status.HTTP_403_FORBIDDEN
|
|
detail = "List not found" if not exists else "Only the list creator can delete this list"
|
|
logger.warning(f"Delete failed for list {list_id} by user {current_user.email}: {detail}")
|
|
raise HTTPException(status_code=status_code, detail=detail)
|
|
|
|
await crud_list.delete_list(db=db, list_db=list_db)
|
|
logger.info(f"List {list_id} deleted successfully by user {current_user.email}.")
|
|
# Return Response with 204 status explicitly if needed, otherwise FastAPI handles it
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
@router.get(
|
|
"/{list_id}/status",
|
|
response_model=ListStatus,
|
|
summary="Get List Status (for polling)",
|
|
tags=["Lists"]
|
|
)
|
|
async def read_list_status(
|
|
list_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_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)
|
|
status_code = status.HTTP_404_NOT_FOUND if not exists else status.HTTP_403_FORBIDDEN
|
|
detail = "List not found" if not exists else "You do not have permission to access this list's status"
|
|
logger.warning(f"Status check failed for list {list_id} by user {current_user.email}: {detail}")
|
|
raise HTTPException(status_code=status_code, detail=detail)
|
|
|
|
# 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 HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List status not found")
|
|
|
|
return list_status
|
|
|
|
@router.post("/{list_id}/calculate-split", response_model=ExpenseRecordPublic, summary="Calculate and Record Expense Split", status_code=status.HTTP_201_CREATED, tags=["Expenses", "Lists"])
|
|
async def calculate_list_split(
|
|
list_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
priced_items = await crud_expense.get_priced_items_for_list(db, list_id)
|
|
total_amount = sum(item.price for item in priced_items if item.price is not None)
|
|
participant_ids = await crud_expense.get_group_member_ids(db, list_id.group_id)
|
|
return await crud_expense.create_expense_record_and_shares(
|
|
db=db,
|
|
list_id=list_id,
|
|
calculated_by_id=current_user.id,
|
|
total_amount=total_amount,
|
|
participant_ids=participant_ids
|
|
) |