mitlist/be/app/api/v1/endpoints/chores.py
Mohamad.Elsena 29ccab2f7e feat: Implement chore management feature with personal and group chores
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.
2025-05-21 18:18:22 +02:00

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)