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()