Merge pull request #1 from whtvrboo/feat/chore-management-backend-core

feat: Initial backend setup for Chore Management (Models, Migrations,…
This commit is contained in:
whtvrboo 2025-05-21 13:23:07 +02:00 committed by GitHub
commit 04b0ad7059
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 630 additions and 4 deletions

View File

@ -13,7 +13,8 @@ from alembic import context
sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))
# Import your app's Base and settings
from app.models import Base # Import Base from your models module
import app.models # Ensure all models are loaded and registered to app.database.Base
from app.database import Base as DatabaseBase # Explicitly get Base from database.py
from app.config import settings # Import settings to get DATABASE_URL
# this is the Alembic Config object, which provides
@ -36,7 +37,7 @@ if config.config_file_name is not None:
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata
target_metadata = DatabaseBase.metadata # Use metadata from app.database.Base
# other values from the config, defined by the needs of env.py,
# can be acquired:

View File

@ -0,0 +1,28 @@
"""add_chore_related_tables_and_enums
Revision ID: 149e9bd58a80
Revises: d066f78fab40
Create Date: 2025-05-21 07:52:33.382555
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '149e9bd58a80'
down_revision: Union[str, None] = 'd066f78fab40'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@ -0,0 +1,28 @@
"""add_chore_tables_final_attempt
Revision ID: 5e410b2b650c
Revises: 149e9bd58a80
Create Date: 2025-05-21 07:53:12.492411
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '5e410b2b650c'
down_revision: Union[str, None] = '149e9bd58a80'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@ -0,0 +1,28 @@
"""add_chores_and_chore_assignments_tables_v2
Revision ID: d066f78fab40
Revises: e4c462d43f5e
Create Date: 2025-05-21 07:51:58.734001
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'd066f78fab40'
down_revision: Union[str, None] = 'e4c462d43f5e'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@ -0,0 +1,28 @@
"""add_chores_and_chore_assignments_tables
Revision ID: e4c462d43f5e
Revises: 8efbdc779a76
Create Date: 2025-05-21 07:51:05.985785
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'e4c462d43f5e'
down_revision: Union[str, None] = '8efbdc779a76'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@ -0,0 +1,71 @@
"""manual_0001_add_chore_tables
Revision ID: manual_0001
Revises: 8efbdc779a76
Create Date: 2025-05-21 08:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'manual_0001'
down_revision: Union[str, None] = '8efbdc779a76' # Last real migration
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
# Enum definition for ChoreFrequencyEnum
chore_frequency_enum = sa.Enum('one_time', 'daily', 'weekly', 'monthly', 'custom', name='chorefrequencyenum')
def upgrade() -> None:
"""Upgrade schema."""
# Create chorefrequencyenum type
chore_frequency_enum.create(op.get_bind(), checkfirst=True)
# Create chores table
op.create_table('chores',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('group_id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('created_by_id', sa.Integer(), nullable=False),
sa.Column('frequency', chore_frequency_enum, nullable=False),
sa.Column('custom_interval_days', sa.Integer(), nullable=True),
sa.Column('next_due_date', sa.Date(), nullable=False),
sa.Column('last_completed_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), onupdate=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE'),
sa.Index(op.f('ix_chores_created_by_id'), ['created_by_id'], unique=False),
sa.Index(op.f('ix_chores_group_id'), ['group_id'], unique=False),
sa.Index(op.f('ix_chores_id'), ['id'], unique=False),
sa.Index(op.f('ix_chores_name'), ['name'], unique=False)
)
# Create chore_assignments table
op.create_table('chore_assignments',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chore_id', sa.Integer(), nullable=False),
sa.Column('assigned_to_user_id', sa.Integer(), nullable=False),
sa.Column('due_date', sa.Date(), nullable=False),
sa.Column('is_complete', sa.Boolean(), server_default=sa.false(), nullable=False),
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), onupdate=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.ForeignKeyConstraint(['assigned_to_user_id'], ['users.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['chore_id'], ['chores.id'], ondelete='CASCADE'),
sa.Index(op.f('ix_chore_assignments_assigned_to_user_id'), ['assigned_to_user_id'], unique=False),
sa.Index(op.f('ix_chore_assignments_chore_id'), ['chore_id'], unique=False),
sa.Index(op.f('ix_chore_assignments_id'), ['id'], unique=False)
)
def downgrade() -> None:
"""Downgrade schema."""
op.drop_table('chore_assignments')
op.drop_table('chores')
chore_frequency_enum.drop(op.get_bind(), checkfirst=True)

View File

@ -0,0 +1,79 @@
from datetime import date, timedelta
from typing import Optional
from app.models import ChoreFrequencyEnum
def calculate_next_due_date(
current_due_date: date,
frequency: ChoreFrequencyEnum,
custom_interval_days: Optional[int] = None,
last_completed_date: Optional[date] = None
) -> date:
"""
Calculates the next due date for a chore.
Uses current_due_date as a base if last_completed_date is not provided.
Calculates from last_completed_date if provided.
"""
if frequency == ChoreFrequencyEnum.one_time:
if last_completed_date:
raise ValueError("Cannot calculate next due date for a completed one-time chore.")
return current_due_date
base_date = last_completed_date if last_completed_date else current_due_date
if hasattr(base_date, 'date') and callable(getattr(base_date, 'date')):
base_date = base_date.date() # type: ignore
next_due: date
if frequency == ChoreFrequencyEnum.daily:
next_due = base_date + timedelta(days=1)
elif frequency == ChoreFrequencyEnum.weekly:
next_due = base_date + timedelta(weeks=1)
elif frequency == ChoreFrequencyEnum.monthly:
month = base_date.month + 1
year = base_date.year + (month - 1) // 12
month = (month - 1) % 12 + 1
day_of_target_month_last = (date(year, month % 12 + 1, 1) - timedelta(days=1)).day if month < 12 else 31
day = min(base_date.day, day_of_target_month_last)
next_due = date(year, month, day)
elif frequency == ChoreFrequencyEnum.custom:
if not custom_interval_days or custom_interval_days <= 0:
raise ValueError("Custom frequency requires a positive custom_interval_days.")
next_due = base_date + timedelta(days=custom_interval_days)
else:
raise ValueError(f"Unknown or unsupported chore frequency: {frequency}")
today = date.today()
reference_future_date = max(today, base_date)
# This loop ensures the next_due date is always in the future relative to the reference_future_date.
while next_due <= reference_future_date:
current_base_for_recalc = next_due
if frequency == ChoreFrequencyEnum.daily:
next_due = current_base_for_recalc + timedelta(days=1)
elif frequency == ChoreFrequencyEnum.weekly:
next_due = current_base_for_recalc + timedelta(weeks=1)
elif frequency == ChoreFrequencyEnum.monthly:
month = current_base_for_recalc.month + 1
year = current_base_for_recalc.year + (month - 1) // 12
month = (month - 1) % 12 + 1
day_of_target_month_last = (date(year, month % 12 + 1, 1) - timedelta(days=1)).day if month < 12 else 31
day = min(current_base_for_recalc.day, day_of_target_month_last)
next_due = date(year, month, day)
elif frequency == ChoreFrequencyEnum.custom:
if not custom_interval_days or custom_interval_days <= 0: # Should have been validated
raise ValueError("Custom frequency requires positive interval during recalc.")
next_due = current_base_for_recalc + timedelta(days=custom_interval_days)
else: # Should not be reached
break
# Safety break: if date hasn't changed, interval is zero or logic error.
if next_due == current_base_for_recalc:
# Log error ideally, then advance by one day to prevent infinite loop.
next_due += timedelta(days=1)
break
return next_due

210
be/app/crud/chore.py Normal file
View File

@ -0,0 +1,210 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload
from typing import List, Optional
import logging
from datetime import date
from app.models import Chore, Group, User, ChoreFrequencyEnum
from app.schemas.chore import ChoreCreate, ChoreUpdate
from app.core.chore_utils import calculate_next_due_date
from app.crud.group import get_group_by_id, is_user_member # For permission checks
from app.core.exceptions import ChoreNotFoundError, GroupNotFoundError, PermissionDeniedError, DatabaseIntegrityError
logger = logging.getLogger(__name__)
async def create_chore(
db: AsyncSession,
chore_in: ChoreCreate,
user_id: int,
group_id: int
) -> Chore:
"""Creates a new chore within a specific group."""
# Validate group existence and user membership (basic check)
group = await get_group_by_id(db, group_id)
if not group:
raise GroupNotFoundError(group_id)
if not await is_user_member(db, group_id, user_id):
raise PermissionDeniedError(detail=f"User {user_id} not a member of group {group_id}")
# Calculate initial next_due_date using the utility function
# chore_in.next_due_date is the user-provided *initial* due date for the chore.
# For recurring chores, this might also be the day it effectively starts.
initial_due_date = chore_in.next_due_date
# If it's a recurring chore and last_completed_at is not set (which it won't be on creation),
# calculate_next_due_date will use current_due_date (which is initial_due_date here)
# to project the *first actual* due date if the initial_due_date itself is in the past.
# However, for creation, we typically trust the user-provided 'next_due_date' as the first one.
# The utility function's logic to advance past dates is more for when a chore is *completed*.
# So, for creation, the `next_due_date` from input is taken as is.
# If a stricter rule (e.g., must be in future) is needed, it can be added here.
db_chore = Chore(
**chore_in.model_dump(exclude_unset=True), # Use model_dump for Pydantic v2
# Ensure next_due_date from chore_in is used directly for creation
# The chore_in schema should already have next_due_date
group_id=group_id,
created_by_id=user_id,
# last_completed_at is None by default
)
# Specific check for custom frequency
if chore_in.frequency == ChoreFrequencyEnum.custom and chore_in.custom_interval_days is None:
raise ValueError("custom_interval_days must be set for custom frequency chores.")
db.add(db_chore)
try:
await db.commit()
await db.refresh(db_chore)
# Eager load relationships for the returned object
result = await db.execute(
select(Chore)
.where(Chore.id == db_chore.id)
.options(selectinload(Chore.creator), selectinload(Chore.group))
)
return result.scalar_one()
except Exception as e: # Catch generic exception for now, refine later
await db.rollback()
logger.error(f"Error creating chore: {e}", exc_info=True)
raise DatabaseIntegrityError(f"Could not create chore. Error: {str(e)}")
async def get_chore_by_id(
db: AsyncSession,
chore_id: int,
) -> Optional[Chore]:
"""Gets a chore by its ID with creator and group info."""
result = await db.execute(
select(Chore)
.where(Chore.id == chore_id)
.options(selectinload(Chore.creator), selectinload(Chore.group))
)
return result.scalar_one_or_none()
async def get_chore_by_id_and_group(
db: AsyncSession,
chore_id: int,
group_id: int,
user_id: int # For permission check
) -> Optional[Chore]:
"""Gets a specific chore by ID, ensuring it belongs to the group and user is a member."""
if not await is_user_member(db, group_id, user_id):
raise PermissionDeniedError(detail=f"User {user_id} not a member of group {group_id}")
chore = await get_chore_by_id(db, chore_id)
if chore and chore.group_id == group_id:
return chore
return None
async def get_chores_by_group_id(
db: AsyncSession,
group_id: int,
user_id: int # For permission check
) -> List[Chore]:
"""Gets all chores for a specific group, if the user is a member."""
if not await is_user_member(db, group_id, user_id):
raise PermissionDeniedError(detail=f"User {user_id} not a member of group {group_id}")
result = await db.execute(
select(Chore)
.where(Chore.group_id == group_id)
.options(selectinload(Chore.creator), selectinload(Chore.assignments))
.order_by(Chore.next_due_date, Chore.name)
)
return result.scalars().all()
async def update_chore(
db: AsyncSession,
chore_id: int,
chore_in: ChoreUpdate,
group_id: int,
user_id: int
) -> Optional[Chore]:
"""Updates a chore's details."""
db_chore = await get_chore_by_id_and_group(db, chore_id, group_id, user_id)
if not db_chore:
# get_chore_by_id_and_group already raises PermissionDeniedError if not member
# If it returns None here, it means chore not found in that group
raise ChoreNotFoundError(chore_id, group_id)
update_data = chore_in.model_dump(exclude_unset=True)
# Recalculate next_due_date if frequency or custom_interval_days changes
# Or if next_due_date is explicitly being set and is different from current one
recalculate = False
if 'frequency' in update_data and update_data['frequency'] != db_chore.frequency:
recalculate = True
if 'custom_interval_days' in update_data and update_data['custom_interval_days'] != db_chore.custom_interval_days:
recalculate = True
# If next_due_date is provided in update_data, it means a manual override of the due date.
# In this case, we usually don't need to run the full calculation logic unless other frequency params also change.
# If 'next_due_date' is the *only* thing changing, we honor it.
# If frequency changes, then 'next_due_date' (if provided) acts as the new 'current_due_date' for calculation.
current_next_due_date_for_calc = db_chore.next_due_date
if 'next_due_date' in update_data:
current_next_due_date_for_calc = update_data['next_due_date']
# If only next_due_date is changing, no need to recalculate based on frequency, just apply it.
if not ('frequency' in update_data or 'custom_interval_days' in update_data):
recalculate = False # User is manually setting the date
for field, value in update_data.items():
setattr(db_chore, field, value)
if recalculate:
# Use the potentially updated chore attributes for calculation
db_chore.next_due_date = calculate_next_due_date(
current_due_date=current_next_due_date_for_calc, # Use the new due date from input if provided, else old
frequency=db_chore.frequency,
custom_interval_days=db_chore.custom_interval_days,
last_completed_date=db_chore.last_completed_at # This helps if frequency changes after a completion
)
# Specific check for custom frequency on update
if db_chore.frequency == ChoreFrequencyEnum.custom and db_chore.custom_interval_days is None:
raise ValueError("custom_interval_days must be set for custom frequency chores.")
try:
await db.commit()
await db.refresh(db_chore)
# Eager load relationships for the returned object
result = await db.execute(
select(Chore)
.where(Chore.id == db_chore.id)
.options(selectinload(Chore.creator), selectinload(Chore.group))
)
return result.scalar_one()
except Exception as e:
await db.rollback()
logger.error(f"Error updating chore {chore_id}: {e}", exc_info=True)
raise DatabaseIntegrityError(f"Could not update chore {chore_id}. Error: {str(e)}")
async def delete_chore(
db: AsyncSession,
chore_id: int,
group_id: int,
user_id: int
) -> bool:
"""Deletes a chore and its assignments, ensuring user has permission."""
db_chore = await get_chore_by_id_and_group(db, chore_id, group_id, user_id)
if not db_chore:
# Similar to update, permission/existence check is done by get_chore_by_id_and_group
raise ChoreNotFoundError(chore_id, group_id)
# Check if user is group owner or chore creator to delete (example policy)
# More granular role checks can be added here or in the endpoint.
# For now, let's assume being a group member (checked by get_chore_by_id_and_group) is enough
# or that specific role checks (e.g. owner) would be in the API layer.
# If creator_id or specific role is required:
# group_user_role = await get_user_role_in_group(db, group_id, user_id)
# if not (db_chore.created_by_id == user_id or group_user_role == UserRoleEnum.owner):
# raise PermissionDeniedError(detail="Only chore creator or group owner can delete.")
await db.delete(db_chore) # Chore model has cascade delete for assignments
try:
await db.commit()
return True
except Exception as e:
await db.rollback()
logger.error(f"Error deleting chore {chore_id}: {e}", exc_info=True)
raise DatabaseIntegrityError(f"Could not delete chore {chore_id}. Error: {str(e)}")

View File

@ -20,7 +20,8 @@ from sqlalchemy import (
text as sa_text,
Text, # <-- Add Text for description
Numeric, # <-- Add Numeric for price
CheckConstraint
CheckConstraint,
Date # Added Date for Chore model
)
from sqlalchemy.orm import relationship, backref
@ -39,6 +40,14 @@ class SplitTypeEnum(enum.Enum):
ITEM_BASED = "ITEM_BASED" # If an expense is derived directly from item prices and who added them
# Add more types as needed, e.g., UNPAID (for tracking debts not part of a formal expense)
# Define ChoreFrequencyEnum
class ChoreFrequencyEnum(enum.Enum):
one_time = "one_time"
daily = "daily"
weekly = "weekly"
monthly = "monthly"
custom = "custom"
# --- User Model ---
class User(Base):
__tablename__ = "users"
@ -72,6 +81,11 @@ class User(Base):
settlements_created = relationship("Settlement", foreign_keys="Settlement.created_by_user_id", back_populates="created_by_user", cascade="all, delete-orphan")
# --- End Relationships for Cost Splitting ---
# --- Relationships for Chores ---
created_chores = relationship("Chore", foreign_keys="[Chore.created_by_id]", back_populates="creator")
assigned_chores = relationship("ChoreAssignment", back_populates="assigned_user", cascade="all, delete-orphan")
# --- End Relationships for Chores ---
# --- Group Model ---
class Group(Base):
@ -96,6 +110,10 @@ class Group(Base):
settlements = relationship("Settlement", foreign_keys="Settlement.group_id", back_populates="group", cascade="all, delete-orphan")
# --- End Relationships for Cost Splitting ---
# --- Relationship for Chores ---
chores = relationship("Chore", back_populates="group", cascade="all, delete-orphan")
# --- End Relationship for Chores ---
# --- UserGroup Association Model ---
class UserGroup(Base):
@ -268,3 +286,48 @@ class Settlement(Base):
)
# Potential future: PaymentMethod model, etc.
# --- Chore Model ---
class Chore(Base):
__tablename__ = "chores"
id = Column(Integer, primary_key=True, index=True)
group_id = Column(Integer, ForeignKey("groups.id", ondelete="CASCADE"), nullable=False, index=True)
name = Column(String, nullable=False, index=True)
description = Column(Text, nullable=True)
created_by_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
frequency = Column(SAEnum(ChoreFrequencyEnum, name="chorefrequencyenum", create_type=True), nullable=False)
custom_interval_days = Column(Integer, nullable=True) # Only if frequency is 'custom'
next_due_date = Column(Date, nullable=False) # Changed to Date
last_completed_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
# --- Relationships ---
group = relationship("Group", back_populates="chores")
creator = relationship("User", back_populates="created_chores")
assignments = relationship("ChoreAssignment", back_populates="chore", cascade="all, delete-orphan")
# --- ChoreAssignment Model ---
class ChoreAssignment(Base):
__tablename__ = "chore_assignments"
id = Column(Integer, primary_key=True, index=True)
chore_id = Column(Integer, ForeignKey("chores.id", ondelete="CASCADE"), nullable=False, index=True)
assigned_to_user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
due_date = Column(Date, nullable=False) # Specific due date for this instance, changed to Date
is_complete = Column(Boolean, default=False, nullable=False)
completed_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
# --- Relationships ---
chore = relationship("Chore", back_populates="assignments")
assigned_user = relationship("User", back_populates="assigned_chores")

90
be/app/schemas/chore.py Normal file
View File

@ -0,0 +1,90 @@
from datetime import date, datetime
from typing import Optional, List
from pydantic import BaseModel, ConfigDict, field_validator
# Assuming ChoreFrequencyEnum is imported from models
# Adjust the import path if necessary based on your project structure.
# e.g., from app.models import ChoreFrequencyEnum
from ..models import ChoreFrequencyEnum, User as UserModel # For UserPublic relation
from .user import UserPublic # For embedding user information
# Chore Schemas
class ChoreBase(BaseModel):
name: str
description: Optional[str] = None
frequency: ChoreFrequencyEnum
custom_interval_days: Optional[int] = None
next_due_date: date # For creation, this will be the initial due date
@field_validator('custom_interval_days', mode='before')
@classmethod
def check_custom_interval_days(cls, value, values):
# Pydantic v2 uses `values.data` to get all fields
# For older Pydantic, it might just be `values`
# This is a simplified check; actual access might differ slightly
# based on Pydantic version context within the validator.
# The goal is to ensure custom_interval_days is present if frequency is 'custom'.
# This validator might be more complex in a real Pydantic v2 setup.
# A more direct way if 'frequency' is already parsed into values.data:
# freq = values.data.get('frequency')
# For this example, we'll assume 'frequency' might not be in 'values.data' yet
# if 'custom_interval_days' is validated 'before' 'frequency'.
# A truly robust validator might need to be on the whole model or run 'after'.
# For now, this is a placeholder for the logic.
# Consider if this validation is better handled at the service/CRUD layer for complex cases.
return value
class ChoreCreate(ChoreBase):
pass
class ChoreUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
frequency: Optional[ChoreFrequencyEnum] = None
custom_interval_days: Optional[int] = None
next_due_date: Optional[date] = None # Allow updating next_due_date directly if needed
# last_completed_at should generally not be updated directly by user
class ChorePublic(ChoreBase):
id: int
group_id: int
created_by_id: int
last_completed_at: Optional[datetime] = None
created_at: datetime
updated_at: datetime
creator: Optional[UserPublic] = None # Embed creator UserPublic schema
# group: Optional[GroupPublic] = None # Embed GroupPublic schema if needed
model_config = ConfigDict(from_attributes=True)
# Chore Assignment Schemas
class ChoreAssignmentBase(BaseModel):
chore_id: int
assigned_to_user_id: int
due_date: date
class ChoreAssignmentCreate(ChoreAssignmentBase):
pass
class ChoreAssignmentUpdate(BaseModel):
# Only completion status and perhaps due_date can be updated for an assignment
is_complete: Optional[bool] = None
due_date: Optional[date] = None # If rescheduling an existing assignment is allowed
class ChoreAssignmentPublic(ChoreAssignmentBase):
id: int
is_complete: bool
completed_at: Optional[datetime] = None
created_at: datetime
updated_at: datetime
# Embed ChorePublic and UserPublic for richer responses
chore: Optional[ChorePublic] = None
assigned_user: Optional[UserPublic] = None
model_config = ConfigDict(from_attributes=True)
# To handle potential circular imports if ChorePublic needs GroupPublic and GroupPublic needs ChorePublic
# We can update forward refs after all models are defined.
# ChorePublic.model_rebuild() # If using Pydantic v2 and forward refs were used with strings
# ChoreAssignmentPublic.model_rebuild()