mitlist/be/app/api/v1/endpoints/chores.py
mohamad f49e15c05c
Some checks failed
Deploy to Production, build images and push to Gitea Registry / build_and_push (pull_request) Failing after 1m24s
feat: Introduce FastAPI and Vue.js guidelines, enhance API structure, and add caching support
This commit adds new guidelines for FastAPI and Vue.js development, emphasizing best practices for component structure, API performance, and data handling. It also introduces caching mechanisms using Redis for improved performance and updates the API structure to streamline authentication and user management. Additionally, new endpoints for categories and time entries are implemented, enhancing the overall functionality of the application.
2025-06-09 21:02:51 +02:00

631 lines
30 KiB
Python

# app/api/v1/endpoints/chores.py
import logging
from typing import List as PyList, Optional
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status, Response
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.database import get_transactional_session, get_session
from app.auth import current_active_user
from app.models import User as UserModel, Chore as ChoreModel, ChoreTypeEnum, TimeEntry
from app.schemas.chore import (
ChoreCreate, ChoreUpdate, ChorePublic,
ChoreAssignmentCreate, ChoreAssignmentUpdate, ChoreAssignmentPublic,
ChoreHistoryPublic, ChoreAssignmentHistoryPublic
)
from app.schemas.time_entry import TimeEntryPublic
from app.crud import chore as crud_chore
from app.crud import history as crud_history
from app.core.exceptions import ChoreNotFoundError, PermissionDeniedError, GroupNotFoundError, DatabaseIntegrityError
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get(
"/all",
response_model=PyList[ChorePublic],
summary="List All Chores",
tags=["Chores"]
)
async def list_all_chores(
db: AsyncSession = Depends(get_session),
current_user: UserModel = Depends(current_active_user),
):
"""Retrieves all chores (personal and group) for the current user in a single optimized request."""
logger.info(f"User {current_user.email} listing all their chores")
all_chores = await crud_chore.get_all_user_chores(db=db, user_id=current_user.id)
return all_chores
# --- 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_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:
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:
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 ---
@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_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}).")
chore_payload = chore_in.model_copy(update={"type": ChoreTypeEnum.group, "group_id": group_id} if chore_in.type is None else {"group_id": group_id})
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:
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:
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)
# === CHORE ASSIGNMENT ENDPOINTS ===
@router.post(
"/assignments",
response_model=ChoreAssignmentPublic,
status_code=status.HTTP_201_CREATED,
summary="Create Chore Assignment",
tags=["Chore Assignments"]
)
async def create_chore_assignment(
assignment_in: ChoreAssignmentCreate,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""Creates a new chore assignment. User must have permission to manage the chore."""
logger.info(f"User {current_user.email} creating assignment for chore {assignment_in.chore_id}")
try:
return await crud_chore.create_chore_assignment(db=db, assignment_in=assignment_in, user_id=current_user.id)
except ChoreNotFoundError as e:
logger.warning(f"Chore {e.chore_id} not found for assignment 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} creating assignment: {e.detail}")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail)
except ValueError as e:
logger.warning(f"ValueError creating assignment 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 assignment for {current_user.email}: {e.detail}", exc_info=True)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail)
@router.get(
"/assignments/my",
response_model=PyList[ChoreAssignmentPublic],
summary="List My Chore Assignments",
tags=["Chore Assignments"]
)
async def list_my_assignments(
include_completed: bool = False,
db: AsyncSession = Depends(get_session),
current_user: UserModel = Depends(current_active_user),
):
"""Retrieves all chore assignments for the current user."""
logger.info(f"User {current_user.email} listing their assignments (include_completed={include_completed})")
try:
return await crud_chore.get_user_assignments(db=db, user_id=current_user.id, include_completed=include_completed)
except Exception as e:
logger.error(f"Error listing assignments for user {current_user.email}: {e}", exc_info=True)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve assignments")
@router.get(
"/chores/{chore_id}/assignments",
response_model=PyList[ChoreAssignmentPublic],
summary="List Chore Assignments",
tags=["Chore Assignments"]
)
async def list_chore_assignments(
chore_id: int,
db: AsyncSession = Depends(get_session),
current_user: UserModel = Depends(current_active_user),
):
"""Retrieves all assignments for a specific chore."""
logger.info(f"User {current_user.email} listing assignments for chore {chore_id}")
try:
return await crud_chore.get_chore_assignments(db=db, chore_id=chore_id, user_id=current_user.id)
except ChoreNotFoundError as e:
logger.warning(f"Chore {e.chore_id} not found for assignment listing 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} listing assignments for chore {chore_id}: {e.detail}")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail)
@router.put(
"/assignments/{assignment_id}",
response_model=ChoreAssignmentPublic,
summary="Update Chore Assignment",
tags=["Chore Assignments"]
)
async def update_chore_assignment(
assignment_id: int,
assignment_in: ChoreAssignmentUpdate,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""Updates a chore assignment. Only assignee can mark complete, managers can reschedule."""
logger.info(f"User {current_user.email} updating assignment {assignment_id}")
try:
updated_assignment = await crud_chore.update_chore_assignment(
db=db, assignment_id=assignment_id, assignment_in=assignment_in, user_id=current_user.id
)
if not updated_assignment:
raise ChoreNotFoundError(assignment_id=assignment_id)
return updated_assignment
except ChoreNotFoundError as e:
logger.warning(f"Assignment {assignment_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 assignment {assignment_id}: {e.detail}")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail)
except ValueError as e:
logger.warning(f"ValueError updating assignment {assignment_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 assignment {assignment_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(
"/assignments/{assignment_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete Chore Assignment",
tags=["Chore Assignments"]
)
async def delete_chore_assignment(
assignment_id: int,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""Deletes a chore assignment. User must have permission to manage the chore."""
logger.info(f"User {current_user.email} deleting assignment {assignment_id}")
try:
success = await crud_chore.delete_chore_assignment(db=db, assignment_id=assignment_id, user_id=current_user.id)
if not success:
raise ChoreNotFoundError(assignment_id=assignment_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
except ChoreNotFoundError as e:
logger.warning(f"Assignment {assignment_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 assignment {assignment_id}: {e.detail}")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail)
except DatabaseIntegrityError as e:
logger.error(f"DatabaseIntegrityError deleting assignment {assignment_id} for {current_user.email}: {e.detail}", exc_info=True)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail)
@router.patch(
"/assignments/{assignment_id}/complete",
response_model=ChoreAssignmentPublic,
summary="Mark Assignment Complete",
tags=["Chore Assignments"]
)
async def complete_chore_assignment(
assignment_id: int,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""Convenience endpoint to mark an assignment as complete."""
logger.info(f"User {current_user.email} marking assignment {assignment_id} as complete")
assignment_update = ChoreAssignmentUpdate(is_complete=True)
try:
updated_assignment = await crud_chore.update_chore_assignment(
db=db, assignment_id=assignment_id, assignment_in=assignment_update, user_id=current_user.id
)
if not updated_assignment:
raise ChoreNotFoundError(assignment_id=assignment_id)
return updated_assignment
except ChoreNotFoundError as e:
logger.warning(f"Assignment {assignment_id} not found for user {current_user.email} during completion.")
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} completing assignment {assignment_id}: {e.detail}")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail)
except DatabaseIntegrityError as e:
logger.error(f"DatabaseIntegrityError completing assignment {assignment_id} for {current_user.email}: {e.detail}", exc_info=True)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail)
# === CHORE HISTORY ENDPOINTS ===
@router.get(
"/{chore_id}/history",
response_model=PyList[ChoreHistoryPublic],
summary="Get Chore History",
tags=["Chores", "History"]
)
async def get_chore_history(
chore_id: int,
db: AsyncSession = Depends(get_session),
current_user: UserModel = Depends(current_active_user),
):
"""Retrieves the history of a specific chore."""
chore = await crud_chore.get_chore_by_id(db, chore_id)
if not chore:
raise ChoreNotFoundError(chore_id=chore_id)
if chore.type == ChoreTypeEnum.personal and chore.created_by_id != current_user.id:
raise PermissionDeniedError("You can only view history for your own personal chores.")
if chore.type == ChoreTypeEnum.group:
is_member = await crud_chore.is_user_member(db, chore.group_id, current_user.id)
if not is_member:
raise PermissionDeniedError("You must be a member of the group to view this chore's history.")
logger.info(f"User {current_user.email} getting history for chore {chore_id}")
return await crud_history.get_chore_history(db=db, chore_id=chore_id)
@router.get(
"/assignments/{assignment_id}/history",
response_model=PyList[ChoreAssignmentHistoryPublic],
summary="Get Chore Assignment History",
tags=["Chore Assignments", "History"]
)
async def get_chore_assignment_history(
assignment_id: int,
db: AsyncSession = Depends(get_session),
current_user: UserModel = Depends(current_active_user),
):
"""Retrieves the history of a specific chore assignment."""
assignment = await crud_chore.get_chore_assignment_by_id(db, assignment_id)
if not assignment:
raise ChoreNotFoundError(assignment_id=assignment_id)
chore = await crud_chore.get_chore_by_id(db, assignment.chore_id)
if not chore:
raise ChoreNotFoundError(chore_id=assignment.chore_id)
if chore.type == ChoreTypeEnum.personal and chore.created_by_id != current_user.id:
raise PermissionDeniedError("You can only view history for assignments of your own personal chores.")
if chore.type == ChoreTypeEnum.group:
is_member = await crud_chore.is_user_member(db, chore.group_id, current_user.id)
if not is_member:
raise PermissionDeniedError("You must be a member of the group to view this assignment's history.")
logger.info(f"User {current_user.email} getting history for assignment {assignment_id}")
return await crud_history.get_assignment_history(db=db, assignment_id=assignment_id)
# === TIME ENTRY ENDPOINTS ===
@router.get(
"/assignments/{assignment_id}/time-entries",
response_model=PyList[TimeEntryPublic],
summary="Get Time Entries",
tags=["Time Tracking"]
)
async def get_time_entries_for_assignment(
assignment_id: int,
db: AsyncSession = Depends(get_session),
current_user: UserModel = Depends(current_active_user),
):
"""Retrieves all time entries for a specific chore assignment."""
assignment = await crud_chore.get_chore_assignment_by_id(db, assignment_id)
if not assignment:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Assignment not found")
chore = await crud_chore.get_chore_by_id(db, assignment.chore_id)
if not chore:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Chore not found")
# Permission check
if chore.type == ChoreTypeEnum.personal and chore.created_by_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied")
if chore.type == ChoreTypeEnum.group:
is_member = await crud_chore.is_user_member(db, chore.group_id, current_user.id)
if not is_member:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied")
# For now, return time entries for the current user only
time_entries = await db.execute(
select(TimeEntry)
.where(TimeEntry.chore_assignment_id == assignment_id)
.where(TimeEntry.user_id == current_user.id)
.order_by(TimeEntry.start_time.desc())
)
return time_entries.scalars().all()
@router.post(
"/assignments/{assignment_id}/time-entries",
response_model=TimeEntryPublic,
status_code=status.HTTP_201_CREATED,
summary="Start Time Entry",
tags=["Time Tracking"]
)
async def start_time_entry(
assignment_id: int,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""Starts a new time entry for a chore assignment."""
assignment = await crud_chore.get_chore_assignment_by_id(db, assignment_id)
if not assignment:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Assignment not found")
chore = await crud_chore.get_chore_by_id(db, assignment.chore_id)
if not chore:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Chore not found")
# Permission check - only assigned user can track time
if assignment.assigned_to_user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only assigned user can track time")
# Check if there's already an active time entry
existing_active = await db.execute(
select(TimeEntry)
.where(TimeEntry.chore_assignment_id == assignment_id)
.where(TimeEntry.user_id == current_user.id)
.where(TimeEntry.end_time.is_(None))
)
if existing_active.scalar_one_or_none():
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Time entry already active")
# Create new time entry
time_entry = TimeEntry(
chore_assignment_id=assignment_id,
user_id=current_user.id,
start_time=datetime.now(timezone.utc)
)
db.add(time_entry)
await db.commit()
await db.refresh(time_entry)
return time_entry
@router.put(
"/time-entries/{time_entry_id}",
response_model=TimeEntryPublic,
summary="Stop Time Entry",
tags=["Time Tracking"]
)
async def stop_time_entry(
time_entry_id: int,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""Stops an active time entry."""
time_entry = await db.get(TimeEntry, time_entry_id)
if not time_entry:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Time entry not found")
if time_entry.user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied")
if time_entry.end_time:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Time entry already stopped")
# Stop the time entry
end_time = datetime.now(timezone.utc)
time_entry.end_time = end_time
time_entry.duration_seconds = int((end_time - time_entry.start_time).total_seconds())
await db.commit()
await db.refresh(time_entry)
return time_entry