Files
HarborForge.Backend/app/schemas/schemas.py
zhi 90d1f22267 BE-PR-010: deprecate feat_task_id — retain column, read-only compat
- Updated model docstring with full deprecation strategy
- Updated column comment to mark as deprecated (BE-PR-010)
- Updated schema/router comments for deprecation clarity
- Added deprecation doc: docs/BE-PR-010-feat-task-id-deprecation.md
- feat_task_id superseded by Task.source_proposal_id (BE-PR-008)
2026-03-30 12:49:52 +00:00

410 lines
10 KiB
Python

from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime, time
from enum import Enum
class TaskTypeEnum(str, Enum):
ISSUE = "issue"
MAINTENANCE = "maintenance"
RESEARCH = "research"
REVIEW = "review"
STORY = "story"
TEST = "test"
RESOLUTION = "resolution"
# P7.1: 'task' type removed — defect subtype migrated to issue/defect
class TaskStatusEnum(str, Enum):
OPEN = "open"
PENDING = "pending"
UNDERGOING = "undergoing"
COMPLETED = "completed"
CLOSED = "closed"
class TaskPriorityEnum(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
# Task schemas
class TaskBase(BaseModel):
title: str
description: Optional[str] = None
task_type: TaskTypeEnum = TaskTypeEnum.ISSUE
task_subtype: Optional[str] = None
priority: TaskPriorityEnum = TaskPriorityEnum.MEDIUM
tags: Optional[str] = None
estimated_effort: Optional[int] = None
estimated_working_time: Optional[str] = None
class TaskCreate(TaskBase):
project_id: Optional[int] = None
project_code: Optional[str] = None
milestone_id: Optional[int] = None
milestone_code: Optional[str] = None
reporter_id: Optional[int] = None
assignee_id: Optional[int] = None
type: Optional[TaskTypeEnum] = None
# Resolution specific
resolution_summary: Optional[str] = None
positions: Optional[str] = None
pending_matters: Optional[str] = None
class TaskUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
task_type: Optional[TaskTypeEnum] = None
type: Optional[TaskTypeEnum] = None
task_subtype: Optional[str] = None
status: Optional[TaskStatusEnum] = None
priority: Optional[TaskPriorityEnum] = None
assignee_id: Optional[int] = None
taken_by: Optional[str] = None
tags: Optional[str] = None
estimated_effort: Optional[int] = None
# Resolution specific
resolution_summary: Optional[str] = None
positions: Optional[str] = None
pending_matters: Optional[str] = None
class TaskResponse(TaskBase):
id: int
status: TaskStatusEnum
task_code: Optional[str] = None
code: Optional[str] = None
type: Optional[str] = None
due_date: Optional[datetime] = None
project_id: int
project_code: Optional[str] = None
milestone_id: int
milestone_code: Optional[str] = None
reporter_id: int
assignee_id: Optional[int] = None
taken_by: Optional[str] = None
created_by_id: Optional[int] = None
estimated_working_time: Optional[time] = None
resolution_summary: Optional[str] = None
positions: Optional[str] = None
pending_matters: Optional[str] = None
# BE-PR-008: Proposal Accept tracking
source_proposal_id: Optional[int] = None
source_essential_id: Optional[int] = None
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Comment schemas
class CommentBase(BaseModel):
content: str
class CommentCreate(CommentBase):
task_id: int
author_id: int
class CommentUpdate(BaseModel):
content: Optional[str] = None
class CommentResponse(CommentBase):
id: int
task_id: int
author_id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Project schemas
class ProjectBase(BaseModel):
name: str
owner_name: Optional[str] = None
description: Optional[str] = None
repo: Optional[str] = None
sub_projects: Optional[list[str]] = None
related_projects: Optional[list[str]] = None
class ProjectCreate(ProjectBase):
owner_id: int
class ProjectUpdate(BaseModel):
description: Optional[str] = None
owner_name: Optional[str] = None
repo: Optional[str] = None
sub_projects: Optional[list[str]] = None
related_projects: Optional[list[str]] = None
class ProjectResponse(BaseModel):
id: int
name: str
owner_name: Optional[str] = None
project_code: Optional[str] = None
description: Optional[str] = None
repo: Optional[str] = None
sub_projects: Optional[list[str]] = None
related_projects: Optional[list[str]] = None
owner_id: int
created_at: datetime
class Config:
from_attributes = True
# User schemas
class UserBase(BaseModel):
username: str
email: str
full_name: Optional[str] = None
class UserCreate(UserBase):
password: Optional[str] = None
role_id: Optional[int] = None
class UserUpdate(BaseModel):
full_name: Optional[str] = None
email: Optional[str] = None
password: Optional[str] = None
role_id: Optional[int] = None
is_active: Optional[bool] = None
class UserResponse(UserBase):
id: int
is_active: bool
is_admin: bool
role_id: Optional[int] = None
role_name: Optional[str] = None
created_at: datetime
class Config:
from_attributes = True
# Project Member schemas
class ProjectMemberBase(BaseModel):
user_id: int
role: str = "dev"
class ProjectMemberCreate(ProjectMemberBase):
pass
class ProjectMemberResponse(BaseModel):
id: int
user_id: int
username: Optional[str] = None
full_name: Optional[str] = None
project_id: int
role: str = "dev"
class Config:
from_attributes = True
class MilestoneStatusEnum(str, Enum):
OPEN = "open"
FREEZE = "freeze"
UNDERGOING = "undergoing"
COMPLETED = "completed"
CLOSED = "closed"
# Milestone schemas
class MilestoneBase(BaseModel):
title: str
description: Optional[str] = None
status: Optional[MilestoneStatusEnum] = MilestoneStatusEnum.OPEN
due_date: Optional[datetime] = None
planned_release_date: Optional[datetime] = None
depend_on_milestones: Optional[List[str]] = None
depend_on_tasks: Optional[List[int]] = None
class MilestoneCreate(MilestoneBase):
project_id: Optional[int] = None
pass
class MilestoneUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
status: Optional[MilestoneStatusEnum] = None
due_date: Optional[datetime] = None
planned_release_date: Optional[datetime] = None
depend_on_milestones: Optional[List[str]] = None
depend_on_tasks: Optional[List[int]] = None
class MilestoneResponse(MilestoneBase):
id: int
milestone_code: Optional[str] = None
project_id: int
created_by_id: Optional[int] = None
started_at: Optional[datetime] = None
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Proposal schemas (renamed from Propose)
class ProposalStatusEnum(str, Enum):
OPEN = "open"
ACCEPTED = "accepted"
REJECTED = "rejected"
class ProposalBase(BaseModel):
title: str
description: Optional[str] = None
class ProposalCreate(ProposalBase):
project_id: Optional[int] = None
class ProposalUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
class ProposalResponse(ProposalBase):
id: int
proposal_code: Optional[str] = None # preferred name
propose_code: Optional[str] = None # backward compat alias (same value)
status: ProposalStatusEnum
project_id: int
created_by_id: Optional[int] = None
created_by_username: Optional[str] = None
feat_task_id: Optional[str] = None # DEPRECATED (BE-PR-010): legacy field, read-only. Use generated_tasks instead.
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# ---------------------------------------------------------------------------
# Essential schemas (under Proposal)
# ---------------------------------------------------------------------------
class EssentialTypeEnum(str, Enum):
FEATURE = "feature"
IMPROVEMENT = "improvement"
REFACTOR = "refactor"
class EssentialBase(BaseModel):
title: str
type: EssentialTypeEnum
description: Optional[str] = None
class EssentialCreate(EssentialBase):
"""Create a new Essential under a Proposal.
``proposal_id`` is inferred from the URL path, not the body.
"""
pass
class EssentialUpdate(BaseModel):
title: Optional[str] = None
type: Optional[EssentialTypeEnum] = None
description: Optional[str] = None
class EssentialResponse(EssentialBase):
id: int
essential_code: str
proposal_id: int
created_by_id: Optional[int] = None
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class GeneratedTaskBrief(BaseModel):
"""Brief info about a story task generated from Proposal Accept."""
task_id: int
task_code: Optional[str] = None
task_type: str
task_subtype: Optional[str] = None
title: str
status: Optional[str] = None
source_essential_id: Optional[int] = None
class ProposalDetailResponse(ProposalResponse):
"""Extended Proposal response that embeds its Essential list and generated tasks."""
essentials: List[EssentialResponse] = []
generated_tasks: List[GeneratedTaskBrief] = []
class Config:
from_attributes = True
class GeneratedTaskSummary(BaseModel):
"""Brief summary of a task generated from a Proposal Essential."""
task_id: int
task_code: str
task_type: str
task_subtype: str
title: str
essential_id: int
essential_code: str
class ProposalAcceptResponse(ProposalResponse):
"""Response for Proposal Accept — includes the generated story tasks."""
essentials: List[EssentialResponse] = []
generated_tasks: List[GeneratedTaskSummary] = []
class Config:
from_attributes = True
# Backward-compatible aliases
ProposeStatusEnum = ProposalStatusEnum
ProposeBase = ProposalBase
ProposeCreate = ProposalCreate
ProposeUpdate = ProposalUpdate
ProposeResponse = ProposalResponse
# Paginated response
from typing import Generic, TypeVar
T = TypeVar("T")
class PaginatedResponse(BaseModel, Generic[T]):
items: List[T]
total: int
page: int
page_size: int
total_pages: int