Kairo-Jinkendo/backend/routers/projects.py
Lars 4ca0a2c460
All checks were successful
Deploy Development / deploy (push) Successful in 53s
Test Suite / pytest-backend (push) Successful in 1m42s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 21s
Test Suite / playwright-smoke (push) Successful in 14s
AP1.5c: Rekursive Projects fuer Mega-Programme ohne Stream-Typ.
parent_project_id und container_kind ermoeglichen verschachtelte Projektstruktur; Arbeitspakete bleiben an Blatt-Projekten gebunden.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 08:38:58 +02:00

136 lines
4.8 KiB
Python

"""Project API — AP1.5."""
from __future__ import annotations
from datetime import date
from typing import Literal, Optional
from capabilities import require_capability
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from services import projects as project_service
from tenant_context import TenantContext
initiative_router = APIRouter(prefix="/api/initiatives", tags=["projects"])
items_router = APIRouter(prefix="/api/projects", tags=["projects"])
class ProjectCreateRequest(BaseModel):
title: str = Field(min_length=1, max_length=255)
description: str = ""
status: Literal["active", "paused", "completed", "archived"] = "active"
parent_project_id: Optional[str] = None
container_kind: Optional[Literal["project", "stream", "phase", "release"]] = None
roadmap_item_id: Optional[str] = None
sort_order: int = 0
target_date: Optional[date] = None
class ProjectUpdateRequest(BaseModel):
title: Optional[str] = Field(default=None, min_length=1, max_length=255)
description: Optional[str] = None
status: Optional[Literal["active", "paused", "completed", "archived"]] = None
parent_project_id: Optional[str] = None
clear_parent_project: bool = False
container_kind: Optional[Literal["project", "stream", "phase", "release"]] = None
clear_container_kind: bool = False
roadmap_item_id: Optional[str] = None
clear_roadmap_item: bool = False
sort_order: Optional[int] = None
target_date: Optional[date] = None
clear_target_date: bool = False
@initiative_router.get("/{initiative_id}/projects")
def list_initiative_projects(
initiative_id: str,
ctx: TenantContext = Depends(require_capability("kairo.project.read")),
):
try:
return project_service.list_projects_for_initiative(
tenant_id=ctx.tenant_id, initiative_id=initiative_id
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@initiative_router.post("/{initiative_id}/projects", status_code=201)
def create_initiative_project(
initiative_id: str,
body: ProjectCreateRequest,
ctx: TenantContext = Depends(require_capability("kairo.project.manage")),
):
try:
return project_service.create_project(
tenant_id=ctx.tenant_id,
initiative_id=initiative_id,
user_id=ctx.user_id,
title=body.title,
description=body.description,
status=body.status,
parent_project_id=body.parent_project_id,
container_kind=body.container_kind,
roadmap_item_id=body.roadmap_item_id,
sort_order=body.sort_order,
target_date=body.target_date,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@items_router.get("/{project_id}")
def get_project(
project_id: str,
ctx: TenantContext = Depends(require_capability("kairo.project.read")),
):
item = project_service.get_project(tenant_id=ctx.tenant_id, project_id=project_id)
if not item:
raise HTTPException(status_code=404, detail="Projekt nicht gefunden")
return item
@items_router.patch("/{project_id}")
def update_project(
project_id: str,
body: ProjectUpdateRequest,
ctx: TenantContext = Depends(require_capability("kairo.project.manage")),
):
try:
item = project_service.update_project(
tenant_id=ctx.tenant_id,
project_id=project_id,
user_id=ctx.user_id,
title=body.title,
description=body.description,
status=body.status,
parent_project_id=body.parent_project_id,
clear_parent_project=body.clear_parent_project,
container_kind=body.container_kind,
clear_container_kind=body.clear_container_kind,
roadmap_item_id=body.roadmap_item_id,
clear_roadmap_item=body.clear_roadmap_item,
sort_order=body.sort_order,
target_date=body.target_date,
clear_target_date=body.clear_target_date,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if not item:
raise HTTPException(status_code=404, detail="Projekt nicht gefunden")
return item
@items_router.delete("/{project_id}", status_code=204)
def delete_project(
project_id: str,
ctx: TenantContext = Depends(require_capability("kairo.project.manage")),
):
try:
deleted = project_service.delete_project(
tenant_id=ctx.tenant_id, project_id=project_id, user_id=ctx.user_id
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if not deleted:
raise HTTPException(status_code=404, detail="Projekt nicht gefunden")