
This commit adds new functionality for tracking user activities within the application, including: - Implementation of a new activity service to fetch and manage group activities. - Creation of a dedicated activity store to handle state management for activities. - Introduction of new API endpoints for retrieving paginated activity data. - Enhancements to the UI with new components for displaying activity feeds and items. - Refactoring of existing components to utilize the new activity features, improving user engagement and interaction. These changes aim to enhance the application's activity tracking capabilities and provide users with a comprehensive view of their interactions.
246 lines
9.1 KiB
Python
246 lines
9.1 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
|
|
from app.core.redis import broadcast_event
|
|
|
|
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)
|
|
|
|
|
|
@router.post(
|
|
"/items/{item_id}/claim",
|
|
response_model=ItemPublic,
|
|
summary="Claim an Item",
|
|
tags=["Items"],
|
|
responses={
|
|
status.HTTP_409_CONFLICT: {"description": "Item is already claimed or completed"},
|
|
status.HTTP_403_FORBIDDEN: {"description": "User does not have permission to claim this item"}
|
|
}
|
|
)
|
|
async def claim_item(
|
|
item_id: int,
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""Marks an item as claimed by the current user."""
|
|
item_db = await get_item_and_verify_access(item_id, db, current_user)
|
|
|
|
if item_db.list.group_id is None:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Items on personal lists cannot be claimed.")
|
|
|
|
try:
|
|
updated_item = await crud_item.claim_item(db, item=item_db, user_id=current_user.id)
|
|
|
|
# Broadcast the event
|
|
event = {
|
|
"type": "item_claimed",
|
|
"payload": {
|
|
"list_id": updated_item.list_id,
|
|
"item_id": updated_item.id,
|
|
"claimed_by": {
|
|
"id": current_user.id,
|
|
"name": current_user.name
|
|
},
|
|
"claimed_at": updated_item.claimed_at.isoformat(),
|
|
"version": updated_item.version
|
|
}
|
|
}
|
|
await broadcast_event(f"list_{updated_item.list_id}", event)
|
|
|
|
return updated_item
|
|
except ConflictError as e:
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
|
|
|
|
|
|
@router.delete(
|
|
"/items/{item_id}/claim",
|
|
response_model=ItemPublic,
|
|
summary="Unclaim an Item",
|
|
tags=["Items"],
|
|
responses={
|
|
status.HTTP_403_FORBIDDEN: {"description": "User cannot unclaim an item they did not claim"}
|
|
}
|
|
)
|
|
async def unclaim_item(
|
|
item_id: int,
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""Removes the current user's claim from an item."""
|
|
item_db = await get_item_and_verify_access(item_id, db, current_user)
|
|
|
|
if item_db.claimed_by_user_id != current_user.id:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You can only unclaim items that you have claimed.")
|
|
|
|
updated_item = await crud_item.unclaim_item(db, item=item_db)
|
|
|
|
# Broadcast the event
|
|
event = {
|
|
"type": "item_unclaimed",
|
|
"payload": {
|
|
"list_id": updated_item.list_id,
|
|
"item_id": updated_item.id,
|
|
"version": updated_item.version
|
|
}
|
|
}
|
|
await broadcast_event(f"list_{updated_item.list_id}", event)
|
|
|
|
return updated_item |