
This commit introduces a comprehensive chore management system, allowing users to create, manage, and track both personal and group chores. Key changes include: - Addition of new API endpoints for personal and group chores in `be/app/api/v1/endpoints/chores.py`. - Implementation of chore models and schemas to support the new functionality in `be/app/models.py` and `be/app/schemas/chore.py`. - Integration of chore services in the frontend to handle API interactions for chore management. - Creation of new Vue components for displaying and managing chores, including `ChoresPage.vue` and `PersonalChoresPage.vue`. - Updates to the router to include chore-related routes and navigation. This feature enhances user collaboration and organization within shared living environments, aligning with the project's goal of streamlining household management.
269 lines
15 KiB
Python
269 lines
15 KiB
Python
# app/api/v1/endpoints/chores.py
|
|
import logging
|
|
from typing import List as PyList, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_transactional_session
|
|
from app.auth import current_active_user
|
|
from app.models import User as UserModel, Chore as ChoreModel, ChoreTypeEnum
|
|
from app.schemas.chore import ChoreCreate, ChoreUpdate, ChorePublic
|
|
from app.crud import chore as crud_chore
|
|
from app.core.exceptions import ChoreNotFoundError, PermissionDeniedError, GroupNotFoundError, DatabaseIntegrityError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
# --- Personal Chores Endpoints ---
|
|
|
|
@router.post(
|
|
"/personal",
|
|
response_model=ChorePublic,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Create Personal Chore",
|
|
tags=["Chores", "Personal Chores"]
|
|
)
|
|
async def create_personal_chore(
|
|
chore_in: ChoreCreate,
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""Creates a new personal chore for the current user."""
|
|
logger.info(f"User {current_user.email} creating personal chore: {chore_in.name}")
|
|
if chore_in.type != ChoreTypeEnum.personal:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Chore type must be personal.")
|
|
if chore_in.group_id is not None:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="group_id must be null for personal chores.")
|
|
try:
|
|
return await crud_chore.create_chore(db=db, chore_in=chore_in, user_id=current_user.id)
|
|
except ValueError as e:
|
|
logger.warning(f"ValueError creating personal chore for user {current_user.email}: {str(e)}")
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
except DatabaseIntegrityError as e:
|
|
logger.error(f"DatabaseIntegrityError creating personal chore for {current_user.email}: {e.detail}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail)
|
|
|
|
@router.get(
|
|
"/personal",
|
|
response_model=PyList[ChorePublic],
|
|
summary="List Personal Chores",
|
|
tags=["Chores", "Personal Chores"]
|
|
)
|
|
async def list_personal_chores(
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""Retrieves all personal chores for the current user."""
|
|
logger.info(f"User {current_user.email} listing their personal chores")
|
|
return await crud_chore.get_personal_chores(db=db, user_id=current_user.id)
|
|
|
|
@router.put(
|
|
"/personal/{chore_id}",
|
|
response_model=ChorePublic,
|
|
summary="Update Personal Chore",
|
|
tags=["Chores", "Personal Chores"]
|
|
)
|
|
async def update_personal_chore(
|
|
chore_id: int,
|
|
chore_in: ChoreUpdate,
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""Updates a personal chore for the current user."""
|
|
logger.info(f"User {current_user.email} updating personal chore ID: {chore_id}")
|
|
if chore_in.type is not None and chore_in.type != ChoreTypeEnum.personal:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot change chore type to group via this endpoint.")
|
|
if chore_in.group_id is not None:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="group_id must be null for personal chores.")
|
|
try:
|
|
updated_chore = await crud_chore.update_chore(db=db, chore_id=chore_id, chore_in=chore_in, user_id=current_user.id, group_id=None)
|
|
if not updated_chore:
|
|
raise ChoreNotFoundError(chore_id=chore_id)
|
|
if updated_chore.type != ChoreTypeEnum.personal or updated_chore.created_by_id != current_user.id:
|
|
# This should ideally be caught by the CRUD layer permission checks
|
|
raise PermissionDeniedError(detail="Chore is not a personal chore of the current user or does not exist.")
|
|
return updated_chore
|
|
except ChoreNotFoundError as e:
|
|
logger.warning(f"Personal chore {e.chore_id} not found for user {current_user.email} during update.")
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=e.detail)
|
|
except PermissionDeniedError as e:
|
|
logger.warning(f"Permission denied for user {current_user.email} updating personal chore {chore_id}: {e.detail}")
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail)
|
|
except ValueError as e:
|
|
logger.warning(f"ValueError updating personal chore {chore_id} for user {current_user.email}: {str(e)}")
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
except DatabaseIntegrityError as e:
|
|
logger.error(f"DatabaseIntegrityError updating personal chore {chore_id} for {current_user.email}: {e.detail}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail)
|
|
|
|
@router.delete(
|
|
"/personal/{chore_id}",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="Delete Personal Chore",
|
|
tags=["Chores", "Personal Chores"]
|
|
)
|
|
async def delete_personal_chore(
|
|
chore_id: int,
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""Deletes a personal chore for the current user."""
|
|
logger.info(f"User {current_user.email} deleting personal chore ID: {chore_id}")
|
|
try:
|
|
# First, verify it's a personal chore belonging to the user
|
|
chore_to_delete = await crud_chore.get_chore_by_id(db, chore_id)
|
|
if not chore_to_delete or chore_to_delete.type != ChoreTypeEnum.personal or chore_to_delete.created_by_id != current_user.id:
|
|
raise ChoreNotFoundError(chore_id=chore_id, detail="Personal chore not found or not owned by user.")
|
|
|
|
success = await crud_chore.delete_chore(db=db, chore_id=chore_id, user_id=current_user.id, group_id=None)
|
|
if not success:
|
|
# This case should be rare if the above check passes and DB is consistent
|
|
raise ChoreNotFoundError(chore_id=chore_id)
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
except ChoreNotFoundError as e:
|
|
logger.warning(f"Personal chore {e.chore_id} not found for user {current_user.email} during delete.")
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=e.detail)
|
|
except PermissionDeniedError as e: # Should be caught by the check above
|
|
logger.warning(f"Permission denied for user {current_user.email} deleting personal chore {chore_id}: {e.detail}")
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail)
|
|
except DatabaseIntegrityError as e:
|
|
logger.error(f"DatabaseIntegrityError deleting personal chore {chore_id} for {current_user.email}: {e.detail}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail)
|
|
|
|
# --- Group Chores Endpoints ---
|
|
# (These would be similar to what you might have had before, but now explicitly part of this router)
|
|
|
|
@router.post(
|
|
"/groups/{group_id}/chores",
|
|
response_model=ChorePublic,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Create Group Chore",
|
|
tags=["Chores", "Group Chores"]
|
|
)
|
|
async def create_group_chore(
|
|
group_id: int,
|
|
chore_in: ChoreCreate,
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""Creates a new chore within a specific group."""
|
|
logger.info(f"User {current_user.email} creating chore in group {group_id}: {chore_in.name}")
|
|
if chore_in.type != ChoreTypeEnum.group:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Chore type must be group.")
|
|
if chore_in.group_id != group_id and chore_in.group_id is not None: # Make sure chore_in.group_id matches path if provided
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Chore's group_id ({chore_in.group_id}) must match path group_id ({group_id}) or be omitted.")
|
|
|
|
# Ensure chore_in has the correct group_id and type for the CRUD operation
|
|
chore_payload = chore_in.model_copy(update={"group_id": group_id, "type": ChoreTypeEnum.group})
|
|
|
|
try:
|
|
return await crud_chore.create_chore(db=db, chore_in=chore_payload, user_id=current_user.id, group_id=group_id)
|
|
except GroupNotFoundError as e:
|
|
logger.warning(f"Group {e.group_id} not found for chore creation by user {current_user.email}.")
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=e.detail)
|
|
except PermissionDeniedError as e:
|
|
logger.warning(f"Permission denied for user {current_user.email} in group {group_id} for chore creation: {e.detail}")
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail)
|
|
except ValueError as e:
|
|
logger.warning(f"ValueError creating group chore for user {current_user.email} in group {group_id}: {str(e)}")
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
except DatabaseIntegrityError as e:
|
|
logger.error(f"DatabaseIntegrityError creating group chore for {current_user.email} in group {group_id}: {e.detail}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail)
|
|
|
|
@router.get(
|
|
"/groups/{group_id}/chores",
|
|
response_model=PyList[ChorePublic],
|
|
summary="List Group Chores",
|
|
tags=["Chores", "Group Chores"]
|
|
)
|
|
async def list_group_chores(
|
|
group_id: int,
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""Retrieves all chores for a specific group, if the user is a member."""
|
|
logger.info(f"User {current_user.email} listing chores for group {group_id}")
|
|
try:
|
|
return await crud_chore.get_chores_by_group_id(db=db, group_id=group_id, user_id=current_user.id)
|
|
except PermissionDeniedError as e:
|
|
logger.warning(f"Permission denied for user {current_user.email} accessing chores for group {group_id}: {e.detail}")
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail)
|
|
|
|
@router.put(
|
|
"/groups/{group_id}/chores/{chore_id}",
|
|
response_model=ChorePublic,
|
|
summary="Update Group Chore",
|
|
tags=["Chores", "Group Chores"]
|
|
)
|
|
async def update_group_chore(
|
|
group_id: int,
|
|
chore_id: int,
|
|
chore_in: ChoreUpdate,
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""Updates a chore's details within a specific group."""
|
|
logger.info(f"User {current_user.email} updating chore ID {chore_id} in group {group_id}")
|
|
if chore_in.type is not None and chore_in.type != ChoreTypeEnum.group:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot change chore type to personal via this endpoint.")
|
|
if chore_in.group_id is not None and chore_in.group_id != group_id:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Chore's group_id if provided must match path group_id ({group_id}).")
|
|
|
|
# Ensure chore_in has the correct type for the CRUD operation
|
|
chore_payload = chore_in.model_copy(update={"type": ChoreTypeEnum.group, "group_id": group_id} if chore_in.type is None else chore_in)
|
|
|
|
try:
|
|
updated_chore = await crud_chore.update_chore(db=db, chore_id=chore_id, chore_in=chore_payload, user_id=current_user.id, group_id=group_id)
|
|
if not updated_chore:
|
|
raise ChoreNotFoundError(chore_id=chore_id, group_id=group_id)
|
|
return updated_chore
|
|
except ChoreNotFoundError as e:
|
|
logger.warning(f"Chore {e.chore_id} in group {e.group_id} not found for user {current_user.email} during update.")
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=e.detail)
|
|
except PermissionDeniedError as e:
|
|
logger.warning(f"Permission denied for user {current_user.email} updating chore {chore_id} in group {group_id}: {e.detail}")
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail)
|
|
except ValueError as e:
|
|
logger.warning(f"ValueError updating group chore {chore_id} for user {current_user.email} in group {group_id}: {str(e)}")
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
except DatabaseIntegrityError as e:
|
|
logger.error(f"DatabaseIntegrityError updating group chore {chore_id} for {current_user.email}: {e.detail}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail)
|
|
|
|
@router.delete(
|
|
"/groups/{group_id}/chores/{chore_id}",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="Delete Group Chore",
|
|
tags=["Chores", "Group Chores"]
|
|
)
|
|
async def delete_group_chore(
|
|
group_id: int,
|
|
chore_id: int,
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""Deletes a chore from a group, ensuring user has permission."""
|
|
logger.info(f"User {current_user.email} deleting chore ID {chore_id} from group {group_id}")
|
|
try:
|
|
# Verify chore exists and belongs to the group before attempting deletion via CRUD
|
|
# This gives a more precise error if the chore exists but isn't in this group.
|
|
chore_to_delete = await crud_chore.get_chore_by_id_and_group(db, chore_id, group_id, current_user.id) # checks permission too
|
|
if not chore_to_delete : # get_chore_by_id_and_group will raise PermissionDeniedError if user not member
|
|
raise ChoreNotFoundError(chore_id=chore_id, group_id=group_id)
|
|
|
|
success = await crud_chore.delete_chore(db=db, chore_id=chore_id, user_id=current_user.id, group_id=group_id)
|
|
if not success:
|
|
# This case should be rare if the above check passes and DB is consistent
|
|
raise ChoreNotFoundError(chore_id=chore_id, group_id=group_id)
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
except ChoreNotFoundError as e:
|
|
logger.warning(f"Chore {e.chore_id} in group {e.group_id} not found for user {current_user.email} during delete.")
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=e.detail)
|
|
except PermissionDeniedError as e:
|
|
logger.warning(f"Permission denied for user {current_user.email} deleting chore {chore_id} in group {group_id}: {e.detail}")
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail)
|
|
except DatabaseIntegrityError as e:
|
|
logger.error(f"DatabaseIntegrityError deleting group chore {chore_id} for {current_user.email}: {e.detail}", exc_info=True)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail) |