feat/task-type-hierarchy #5

Merged
hzhang merged 17 commits from feat/task-type-hierarchy into main 2026-03-12 13:05:00 +00:00
5 changed files with 58 additions and 10 deletions
Showing only changes of commit 6b3e42195d - Show all commits

View File

@@ -17,6 +17,35 @@ from app.services.activity import log_activity
router = APIRouter(tags=["Issues"])
# ---- Type / Subtype validation ----
ISSUE_SUBTYPE_MAP = {
'meeting': {'conference', 'handover', 'recap'},
'support': {'access', 'information'},
'issue': {'infrastructure', 'performance', 'regression', 'security', 'user_experience'},
'maintenance': {'deploy', 'release'},
'review': {'code_review', 'decision_review', 'function_review'},
'story': {'feature', 'improvement', 'refactor'},
'test': {'regression', 'security', 'smoke', 'stress'},
'research': set(),
'task': set(),
'resolution': set(),
}
ALLOWED_ISSUE_TYPES = set(ISSUE_SUBTYPE_MAP.keys())
def _validate_issue_type_subtype(issue_type: str | None, issue_subtype: str | None, require_subtype: bool = False):
if issue_type is None:
raise HTTPException(status_code=400, detail='issue_type is required')
if issue_type not in ALLOWED_ISSUE_TYPES:
raise HTTPException(status_code=400, detail=f'Invalid issue_type: {issue_type}')
allowed = ISSUE_SUBTYPE_MAP.get(issue_type, set())
if issue_subtype:
if issue_subtype not in allowed:
raise HTTPException(status_code=400, detail=f'Invalid issue_subtype for {issue_type}: {issue_subtype}')
else:
if require_subtype and allowed:
raise HTTPException(status_code=400, detail=f'issue_subtype required for type: {issue_type}')
def _notify_user(db, user_id, ntype, title, message=None, entity_type=None, entity_id=None):
n = NotificationModel(user_id=user_id, type=ntype, title=title, message=message,
@@ -45,7 +74,7 @@ def create_issue(issue: schemas.IssueCreate, bg: BackgroundTasks, db: Session =
@router.get("/issues")
def list_issues(
project_id: int = None, issue_status: str = None, issue_type: str = None,
project_id: int = None, issue_status: str = None, issue_type: str = None, issue_subtype: str = None,
assignee_id: int = None, tag: str = None,
sort_by: str = "created_at", sort_order: str = "desc",
page: int = 1, page_size: int = 50,
@@ -59,6 +88,8 @@ def list_issues(
query = query.filter(models.Issue.status == issue_status)
if issue_type:
query = query.filter(models.Issue.issue_type == issue_type)
if issue_subtype:
query = query.filter(models.Issue.issue_subtype == issue_subtype)
if assignee_id:
query = query.filter(models.Issue.assignee_id == assignee_id)
if tag:
@@ -108,7 +139,7 @@ def update_issue(issue_id: int, issue_update: schemas.IssueUpdate, db: Session =
check_project_role(db, current_user.id, issue.project_id, min_role="dev")
if not issue:
raise HTTPException(status_code=404, detail="Issue not found")
for field, value in issue_update.model_dump(exclude_unset=True).items():
for field, value in update_data.items():
setattr(issue, field, value)
db.commit()
db.refresh(issue)

View File

@@ -298,7 +298,7 @@ def export_issues_csv(project_id: int = None, db: Session = Depends(get_db)):
"reporter_id", "assignee_id", "milestone_id", "due_date",
"tags", "created_at", "updated_at"])
for i in issues:
writer.writerow([i.id, i.title, i.issue_type, i.status, i.priority, i.project_id,
writer.writerow([i.id, i.title, i.issue_type, i.issue_subtype or , i.status, i.priority, i.project_id,
i.reporter_id, i.assignee_id, i.milestone_id, i.due_date,
i.tags, i.created_at, i.updated_at])
output.seek(0)

View File

@@ -51,6 +51,7 @@ def startup():
from app.core.config import Base, engine, SessionLocal
from app.models import webhook, apikey, activity, milestone, notification, worklog, monitor
Base.metadata.create_all(bind=engine)
_migrate_schema()
# Initialize from AbstractWizard (admin user, default project, etc.)
from app.init_wizard import run_init

View File

@@ -6,10 +6,16 @@ import enum
class IssueType(str, enum.Enum):
TASK = "task"
STORY = "story"
TEST = "test"
RESOLUTION = "resolution" # 决议案 - 用于 Agent 僵局提交
MEETING = meeting
SUPPORT = support
ISSUE = issue
MAINTENANCE = maintenance
RESEARCH = research
REVIEW = review
STORY = story
TEST = test
RESOLUTION = resolution # 决议案 - 用于 Agent 僵局提交
TASK = task # legacy generic type
class IssueStatus(str, enum.Enum):
@@ -33,7 +39,8 @@ class Issue(Base):
id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
issue_type = Column(Enum(IssueType), default=IssueType.TASK)
issue_type = Column(String(32), default=IssueType.ISSUE.value)
issue_subtype = Column(String(64), nullable=True)
status = Column(Enum(IssueStatus), default=IssueStatus.OPEN)
priority = Column(Enum(IssuePriority), default=IssuePriority.MEDIUM)

View File

@@ -5,10 +5,16 @@ from enum import Enum
class IssueTypeEnum(str, Enum):
TASK = "task"
MEETING = "meeting"
SUPPORT = "support"
ISSUE = "issue"
MAINTENANCE = "maintenance"
RESEARCH = "research"
REVIEW = "review"
STORY = "story"
TEST = "test"
RESOLUTION = "resolution"
TASK = "task" # legacy
class IssueStatusEnum(str, Enum):
@@ -30,7 +36,8 @@ class IssuePriorityEnum(str, Enum):
class IssueBase(BaseModel):
title: str
description: Optional[str] = None
issue_type: IssueTypeEnum = IssueTypeEnum.TASK
issue_type: IssueTypeEnum = IssueTypeEnum.ISSUE
issue_subtype: Optional[str] = None
priority: IssuePriorityEnum = IssuePriorityEnum.MEDIUM
tags: Optional[str] = None
depends_on_id: Optional[int] = None
@@ -51,6 +58,8 @@ class IssueCreate(IssueBase):
class IssueUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
issue_type: Optional[IssueTypeEnum] = None
issue_subtype: Optional[str] = None
status: Optional[IssueStatusEnum] = None
priority: Optional[IssuePriorityEnum] = None
assignee_id: Optional[int] = None