Automated Action 66410bdab5 Implement comprehensive due date functionality for todos
Added due_date field to TodoBase, TodoCreate, TodoUpdate schemas with proper validation
and timezone handling. Included computed fields is_overdue and days_until_due
for enhanced todo management capabilities.
2025-06-19 13:20:33 +00:00

132 lines
4.9 KiB
Python

from datetime import datetime, timezone
from typing import Optional, TYPE_CHECKING, List
from pydantic import BaseModel, Field, field_validator, computed_field
from app.models.todo import Priority
if TYPE_CHECKING:
from app.schemas.category import Category
from app.schemas.project import Project
class TodoBase(BaseModel):
title: str = Field(..., min_length=1, max_length=200, description="Todo title")
description: Optional[str] = Field(None, max_length=500, description="Todo description")
completed: bool = Field(False, description="Whether the todo is completed")
priority: Priority = Field(Priority.MEDIUM, description="Priority level")
category_id: Optional[int] = Field(None, description="Category ID")
project_id: Optional[int] = Field(None, description="Project ID")
due_date: Optional[datetime] = Field(None, description="Due date with timezone")
@field_validator('due_date')
@classmethod
def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
if v is not None:
# Ensure timezone awareness
if v.tzinfo is None:
v = v.replace(tzinfo=timezone.utc)
# For creation/updates, ensure due date is in the future
if v < datetime.now(timezone.utc):
raise ValueError('Due date must be in the future')
return v
class TodoCreate(TodoBase):
tag_ids: Optional[List[int]] = Field(default=None, description="List of tag IDs to associate with the todo")
class TodoUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=200, description="Todo title")
description: Optional[str] = Field(None, max_length=500, description="Todo description")
completed: Optional[bool] = Field(None, description="Whether the todo is completed")
priority: Optional[Priority] = Field(None, description="Priority level")
category_id: Optional[int] = Field(None, description="Category ID")
project_id: Optional[int] = Field(None, description="Project ID")
due_date: Optional[datetime] = Field(None, description="Due date with timezone")
tag_ids: Optional[List[int]] = Field(None, description="List of tag IDs to associate with this todo")
@field_validator('due_date')
@classmethod
def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
if v is not None:
# Ensure timezone awareness
if v.tzinfo is None:
v = v.replace(tzinfo=timezone.utc)
# For updates, only validate future date if it's being set to a new value
# Allow None to clear the due date
return v
# Forward declaration to handle circular reference
class Todo(TodoBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
category: Optional["Category"] = None
project: Optional["Project"] = None
@computed_field
@property
def is_overdue(self) -> bool:
"""Check if this todo is overdue."""
if self.due_date is None or self.completed:
return False
return datetime.now(timezone.utc) > self.due_date
@computed_field
@property
def days_until_due(self) -> Optional[int]:
"""Calculate the number of days until the due date.
Returns:
int: Number of days until due (negative if overdue)
None: If no due date is set or todo is completed
"""
if self.due_date is None or self.completed:
return None
now = datetime.now(timezone.utc)
delta = (self.due_date - now).days
return delta
@field_validator('due_date', mode='before')
@classmethod
def ensure_timezone_aware(cls, v: Optional[datetime]) -> Optional[datetime]:
if v is not None and v.tzinfo is None:
v = v.replace(tzinfo=timezone.utc)
return v
model_config = {"from_attributes": True}
class TodoListResponse(BaseModel):
items: list[Todo]
total: int
page: int
per_page: int
has_next: bool
has_prev: bool
class SubtaskCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=200, description="Subtask title")
description: Optional[str] = Field(None, max_length=500, description="Subtask description")
priority: Priority = Field(Priority.MEDIUM, description="Priority level")
due_date: Optional[datetime] = Field(None, description="Due date with timezone")
@field_validator('due_date')
@classmethod
def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
if v is not None:
# Ensure timezone awareness
if v.tzinfo is None:
v = v.replace(tzinfo=timezone.utc)
# For creation, ensure due date is in the future
if v < datetime.now(timezone.utc):
raise ValueError('Due date must be in the future')
return v
# Update forward references
Todo.model_rebuild()