From 6b3e42195db735dde6423954751c8f546ac6be25 Mon Sep 17 00:00:00 2001 From: zhi Date: Wed, 11 Mar 2026 23:55:52 +0000 Subject: [PATCH] feat: add task type hierarchy with subtypes (issue/meeting/support/maintenance/research/review/story/test) --- app/api/routers/issues.py | 35 +++++++++++++++++++++++++++++++++-- app/api/routers/misc.py | 2 +- app/main.py | 1 + app/models/models.py | 17 ++++++++++++----- app/schemas/schemas.py | 13 +++++++++++-- 5 files changed, 58 insertions(+), 10 deletions(-) diff --git a/app/api/routers/issues.py b/app/api/routers/issues.py index deaca01..d6fd16b 100644 --- a/app/api/routers/issues.py +++ b/app/api/routers/issues.py @@ -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) diff --git a/app/api/routers/misc.py b/app/api/routers/misc.py index 20fe36a..d5c4f9f 100644 --- a/app/api/routers/misc.py +++ b/app/api/routers/misc.py @@ -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) diff --git a/app/main.py b/app/main.py index 2ea5790..7c1968a 100644 --- a/app/main.py +++ b/app/main.py @@ -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 diff --git a/app/models/models.py b/app/models/models.py index 207d316..ca1af3a 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -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) diff --git a/app/schemas/schemas.py b/app/schemas/schemas.py index 77e2619..b8fdf48 100644 --- a/app/schemas/schemas.py +++ b/app/schemas/schemas.py @@ -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