from pydantic import BaseModel, ConfigDict, Field, model_validator from datetime import datetime, date from typing import Optional, List, Literal from app.schemas.project_task import ProjectTaskResponse ProjectStatus = Literal["not_started", "in_progress", "completed", "blocked", "review", "on_hold"] class ProjectCreate(BaseModel): model_config = ConfigDict(extra="forbid") name: str = Field(min_length=1, max_length=255) description: Optional[str] = Field(None, max_length=5000) status: ProjectStatus = "not_started" color: Optional[str] = Field(None, max_length=20) due_date: Optional[date] = None is_tracked: bool = False class ProjectUpdate(BaseModel): model_config = ConfigDict(extra="forbid") name: Optional[str] = Field(None, min_length=1, max_length=255) description: Optional[str] = Field(None, max_length=5000) status: Optional[ProjectStatus] = None color: Optional[str] = Field(None, max_length=20) due_date: Optional[date] = None is_tracked: Optional[bool] = None class ProjectResponse(BaseModel): id: int user_id: int = 0 name: str description: Optional[str] status: str color: Optional[str] due_date: Optional[date] is_tracked: bool member_count: int = 0 created_at: datetime updated_at: datetime tasks: List[ProjectTaskResponse] = [] model_config = ConfigDict(from_attributes=True) @model_validator(mode="before") @classmethod def compute_member_count(cls, data): # type: ignore[override] """Compute member_count from eagerly loaded members relationship.""" if hasattr(data, "members"): try: data = dict( id=data.id, user_id=data.user_id, name=data.name, description=data.description, status=data.status, color=data.color, due_date=data.due_date, is_tracked=data.is_tracked, member_count=len([m for m in data.members if m.status == "accepted"]), created_at=data.created_at, updated_at=data.updated_at, tasks=data.tasks, ) except Exception: pass # If members aren't loaded, default to 0 return data class TrackedTaskResponse(BaseModel): id: int title: str status: str priority: str due_date: date project_name: str project_id: int parent_task_title: Optional[str] = None