feat/task-type-hierarchy #5
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user