from datetime import datetime, timezone from typing import Any from sqlalchemy import CheckConstraint, ForeignKey, Index, JSON, String, Text from sqlalchemy.ext.mutable import MutableList from sqlalchemy.orm import Mapped, mapped_column, relationship from app.extensions import db TASK_STATUSES = ("not_started", "in_progress", "done") TASK_PRIORITIES = ("urgent", "important", "normal") class Task(db.Model): __table_args__ = ( CheckConstraint( "status IN ('not_started', 'in_progress', 'done')", name="ck_task_status", ), CheckConstraint( "priority IN ('urgent', 'important', 'normal')", name="ck_task_priority", ), Index( "ix_task_owner_visibility_status_due", "user_id", "cleared_at", "status", "due_date", ), ) id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] = mapped_column(String(200)) description: Mapped[str | None] = mapped_column(Text) status: Mapped[str] = mapped_column(String(20), default="not_started") priority: Mapped[str] = mapped_column(String(20), default="normal") created_at: Mapped[datetime] = mapped_column( default=lambda: datetime.now(timezone.utc) ) due_date: Mapped[datetime | None] completed_at: Mapped[datetime | None] cleared_at: Mapped[datetime | None] clear_batch_id: Mapped[str | None] = mapped_column(String(36), index=True) user_id: Mapped[int] = mapped_column( ForeignKey("user.id", ondelete="CASCADE"), index=True ) # Stored now so Phase 3 can add timers and chunking without reshaping CRUD. timer_seconds: Mapped[int] = mapped_column(default=0) context: Mapped[str | None] = mapped_column(String(100)) chunk_sessions: Mapped[list[Any]] = mapped_column( MutableList.as_mutable(JSON), default=list ) subtasks: Mapped[list["Subtask"]] = relationship( back_populates="task", cascade="all, delete-orphan", order_by="Subtask.id", ) @property def progress_percentage(self) -> int: if not self.subtasks: return 100 if self.status == "done" else 0 completed = sum(subtask.completed for subtask in self.subtasks) return round(completed / len(self.subtasks) * 100) class Subtask(db.Model): id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] = mapped_column(String(100)) completed: Mapped[bool] = mapped_column(default=False) task_id: Mapped[int] = mapped_column( ForeignKey("task.id", ondelete="CASCADE"), index=True ) task: Mapped[Task] = relationship(back_populates="subtasks")