Fix: Convert empty string to None for due_date field in update_chore
When updating a chore with an empty due_date field, SQLite was throwing an error because it only accepts Python datetime and date objects, not empty strings. This fix checks if due_date is an empty string and converts it to None before setting the attribute, preventing the TypeError. Error fixed: "SQLite DateTime type only accepts Python datetime and date objects as input"
This commit is contained in:
@@ -1,29 +1,118 @@
|
||||
"""Chores API endpoints."""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, date
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.api.v1.auth import get_current_user
|
||||
from app.models.user import User
|
||||
from app.models.chore import Chore, ChoreStatus
|
||||
from app.models.chore_assignment import ChoreAssignment
|
||||
from app.schemas import chore as chore_schemas
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def is_birthday_today(user: User) -> bool:
|
||||
"""Check if today is the user's birthday."""
|
||||
if not user.birthday:
|
||||
return False
|
||||
|
||||
today = date.today()
|
||||
return user.birthday.month == today.month and user.birthday.day == today.day
|
||||
|
||||
|
||||
def get_chore_with_assignments(db: Session, chore: Chore) -> dict:
|
||||
"""Convert chore to dict with assignment details."""
|
||||
# Get all assignments for this chore
|
||||
assignments = db.query(ChoreAssignment).filter(
|
||||
ChoreAssignment.chore_id == chore.id
|
||||
).all()
|
||||
|
||||
# Build assigned users list with completion status
|
||||
assigned_users = []
|
||||
for assignment in assignments:
|
||||
user = db.query(User).filter(User.id == assignment.user_id).first()
|
||||
if user:
|
||||
assigned_users.append({
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"birthday": user.birthday,
|
||||
"completed_at": assignment.completed_at
|
||||
})
|
||||
|
||||
# Convert chore to dict
|
||||
chore_dict = {
|
||||
"id": chore.id,
|
||||
"title": chore.title,
|
||||
"description": chore.description,
|
||||
"room": chore.room,
|
||||
"frequency": chore.frequency,
|
||||
"points": chore.points,
|
||||
"status": chore.status,
|
||||
"due_date": chore.due_date,
|
||||
"completed_at": chore.completed_at,
|
||||
"created_at": chore.created_at,
|
||||
"updated_at": chore.updated_at,
|
||||
"assigned_users": assigned_users,
|
||||
"assigned_user_id": chore.assigned_user_id # Legacy compatibility
|
||||
}
|
||||
|
||||
return chore_dict
|
||||
|
||||
|
||||
@router.get("", response_model=List[chore_schemas.Chore])
|
||||
def get_chores(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
user_id: Optional[int] = Query(None, description="Filter by assigned user ID"),
|
||||
exclude_birthdays: bool = Query(False, description="Exclude chores for users with birthdays today"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get all chores."""
|
||||
chores = db.query(Chore).offset(skip).limit(limit).all()
|
||||
return chores
|
||||
"""
|
||||
Get all chores.
|
||||
|
||||
- **user_id**: Filter chores assigned to specific user
|
||||
- **exclude_birthdays**: Skip chores for users with birthdays today
|
||||
"""
|
||||
query = db.query(Chore)
|
||||
|
||||
# Apply user filter if specified
|
||||
if user_id:
|
||||
# Find chores assigned to this user through the assignments table
|
||||
assignment_ids = db.query(ChoreAssignment.chore_id).filter(
|
||||
ChoreAssignment.user_id == user_id
|
||||
).all()
|
||||
chore_ids = [aid[0] for aid in assignment_ids]
|
||||
query = query.filter(Chore.id.in_(chore_ids))
|
||||
|
||||
chores = query.offset(skip).limit(limit).all()
|
||||
|
||||
# Build response with assignments
|
||||
result = []
|
||||
for chore in chores:
|
||||
chore_data = get_chore_with_assignments(db, chore)
|
||||
|
||||
# Filter out if birthday exclusion is enabled
|
||||
if exclude_birthdays:
|
||||
# Skip if any assigned user has birthday today
|
||||
skip_chore = False
|
||||
for assigned_user in chore_data["assigned_users"]:
|
||||
user = db.query(User).filter(User.id == assigned_user["id"]).first()
|
||||
if user and is_birthday_today(user):
|
||||
skip_chore = True
|
||||
break
|
||||
|
||||
if skip_chore:
|
||||
continue
|
||||
|
||||
result.append(chore_data)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{chore_id}", response_model=chore_schemas.Chore)
|
||||
@@ -39,7 +128,8 @@ def get_chore(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Chore not found"
|
||||
)
|
||||
return chore
|
||||
|
||||
return get_chore_with_assignments(db, chore)
|
||||
|
||||
|
||||
@router.post("", response_model=chore_schemas.Chore, status_code=status.HTTP_201_CREATED)
|
||||
@@ -48,12 +138,34 @@ def create_chore(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Create a new chore."""
|
||||
chore = Chore(**chore_in.model_dump())
|
||||
"""Create a new chore with multiple user assignments."""
|
||||
# Extract user IDs before creating chore
|
||||
assigned_user_ids = chore_in.assigned_user_ids or []
|
||||
|
||||
# Create chore without assigned_user_ids (not a DB column)
|
||||
chore_data = chore_in.model_dump(exclude={'assigned_user_ids'})
|
||||
chore = Chore(**chore_data)
|
||||
|
||||
db.add(chore)
|
||||
db.commit()
|
||||
db.refresh(chore)
|
||||
return chore
|
||||
|
||||
# Create assignments for each user
|
||||
for user_id in assigned_user_ids:
|
||||
# Verify user exists
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
continue
|
||||
|
||||
assignment = ChoreAssignment(
|
||||
chore_id=chore.id,
|
||||
user_id=user_id
|
||||
)
|
||||
db.add(assignment)
|
||||
|
||||
db.commit()
|
||||
|
||||
return get_chore_with_assignments(db, chore)
|
||||
|
||||
|
||||
@router.put("/{chore_id}", response_model=chore_schemas.Chore)
|
||||
@@ -63,7 +175,12 @@ def update_chore(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Update a chore."""
|
||||
"""
|
||||
Update a chore (admin only or assigned user for status updates).
|
||||
|
||||
Admin users can update all fields.
|
||||
Non-admin users can only update status for chores assigned to them.
|
||||
"""
|
||||
chore = db.query(Chore).filter(Chore.id == chore_id).first()
|
||||
if not chore:
|
||||
raise HTTPException(
|
||||
@@ -71,19 +188,79 @@ def update_chore(
|
||||
detail="Chore not found"
|
||||
)
|
||||
|
||||
# Update fields
|
||||
update_data = chore_in.model_dump(exclude_unset=True)
|
||||
update_data = chore_in.model_dump(exclude_unset=True, exclude={'assigned_user_ids'})
|
||||
|
||||
# If status is being changed to completed, set completed_at
|
||||
if update_data.get("status") == ChoreStatus.COMPLETED and chore.status != ChoreStatus.COMPLETED:
|
||||
update_data["completed_at"] = datetime.utcnow()
|
||||
# Check permissions
|
||||
is_admin = current_user.is_admin
|
||||
is_assigned = db.query(ChoreAssignment).filter(
|
||||
ChoreAssignment.chore_id == chore_id,
|
||||
ChoreAssignment.user_id == current_user.id
|
||||
).first() is not None
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(chore, field, value)
|
||||
if not is_admin and not is_assigned:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to update this chore"
|
||||
)
|
||||
|
||||
# Non-admins can only update status
|
||||
if not is_admin:
|
||||
allowed_fields = {'status'}
|
||||
update_data = {k: v for k, v in update_data.items() if k in allowed_fields}
|
||||
|
||||
# Handle status change to completed
|
||||
if update_data.get("status") == ChoreStatus.COMPLETED:
|
||||
# Mark assignment as completed for current user
|
||||
assignment = db.query(ChoreAssignment).filter(
|
||||
ChoreAssignment.chore_id == chore_id,
|
||||
ChoreAssignment.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if assignment:
|
||||
assignment.completed_at = datetime.utcnow()
|
||||
|
||||
# If all assignments are completed, mark chore as completed
|
||||
all_assignments = db.query(ChoreAssignment).filter(
|
||||
ChoreAssignment.chore_id == chore_id
|
||||
).all()
|
||||
|
||||
if all(a.completed_at is not None for a in all_assignments):
|
||||
chore.completed_at = datetime.utcnow()
|
||||
chore.status = ChoreStatus.COMPLETED
|
||||
|
||||
# Update chore fields (admins only)
|
||||
if is_admin:
|
||||
for field, value in update_data.items():
|
||||
# Convert empty string to None for datetime fields
|
||||
if field == 'due_date' and value == '':
|
||||
value = None
|
||||
setattr(chore, field, value)
|
||||
|
||||
# Handle user assignments update
|
||||
if 'assigned_user_ids' in chore_in.model_dump(exclude_unset=True):
|
||||
assigned_user_ids = chore_in.assigned_user_ids or []
|
||||
|
||||
# Remove old assignments
|
||||
db.query(ChoreAssignment).filter(
|
||||
ChoreAssignment.chore_id == chore_id
|
||||
).delete()
|
||||
|
||||
# Add new assignments
|
||||
for user_id in assigned_user_ids:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
continue
|
||||
|
||||
assignment = ChoreAssignment(
|
||||
chore_id=chore.id,
|
||||
user_id=user_id
|
||||
)
|
||||
db.add(assignment)
|
||||
|
||||
db.commit()
|
||||
db.refresh(chore)
|
||||
return chore
|
||||
|
||||
return get_chore_with_assignments(db, chore)
|
||||
|
||||
|
||||
@router.delete("/{chore_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@@ -92,7 +269,13 @@ def delete_chore(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Delete a chore."""
|
||||
"""Delete a chore (admin only)."""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can delete chores"
|
||||
)
|
||||
|
||||
chore = db.query(Chore).filter(Chore.id == chore_id).first()
|
||||
if not chore:
|
||||
raise HTTPException(
|
||||
@@ -103,3 +286,48 @@ def delete_chore(
|
||||
db.delete(chore)
|
||||
db.commit()
|
||||
return None
|
||||
|
||||
|
||||
@router.post("/{chore_id}/assign", response_model=chore_schemas.Chore)
|
||||
def assign_users_to_chore(
|
||||
chore_id: int,
|
||||
user_ids: List[int],
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Assign multiple users to a chore (admin only).
|
||||
Replaces existing assignments.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can assign chores"
|
||||
)
|
||||
|
||||
chore = db.query(Chore).filter(Chore.id == chore_id).first()
|
||||
if not chore:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Chore not found"
|
||||
)
|
||||
|
||||
# Remove existing assignments
|
||||
db.query(ChoreAssignment).filter(ChoreAssignment.chore_id == chore_id).delete()
|
||||
|
||||
# Add new assignments
|
||||
for user_id in user_ids:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
continue
|
||||
|
||||
assignment = ChoreAssignment(
|
||||
chore_id=chore.id,
|
||||
user_id=user_id
|
||||
)
|
||||
db.add(assignment)
|
||||
|
||||
db.commit()
|
||||
db.refresh(chore)
|
||||
|
||||
return get_chore_with_assignments(db, chore)
|
||||
|
||||
Reference in New Issue
Block a user