feat: task numbers (TEA-1, TEA-2...) + project key_prefix
All checks were successful
Deploy Tracker / deploy (push) Successful in 7s
All checks were successful
Deploy Tracker / deploy (push) Successful in 7s
- Project: key_prefix (auto from name), task_counter - Task: number (auto-increment per project) - API returns 'key' field (e.g. TEA-1) - Migration numbers existing tasks
This commit is contained in:
parent
37c2ac3717
commit
5f9ea80621
@ -0,0 +1,55 @@
|
||||
"""add task number and project key_prefix
|
||||
|
||||
Revision ID: 864b0ce5d657
|
||||
Revises: 59dd2d9071fd
|
||||
Create Date: 2026-02-15 19:27:43.338390
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = '864b0ce5d657'
|
||||
down_revision: Union[str, None] = '59dd2d9071fd'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add as nullable first
|
||||
op.add_column('projects', sa.Column('key_prefix', sa.String(length=10), nullable=True))
|
||||
op.add_column('projects', sa.Column('task_counter', sa.Integer(), nullable=True))
|
||||
op.add_column('tasks', sa.Column('number', sa.Integer(), nullable=True))
|
||||
|
||||
# Fill defaults
|
||||
op.execute("UPDATE projects SET key_prefix = UPPER(LEFT(REGEXP_REPLACE(name, '[^a-zA-Z]', '', 'g'), 3)) WHERE key_prefix IS NULL")
|
||||
op.execute("UPDATE projects SET key_prefix = 'PRJ' WHERE key_prefix IS NULL OR key_prefix = ''")
|
||||
op.execute("UPDATE projects SET task_counter = 0 WHERE task_counter IS NULL")
|
||||
op.execute("UPDATE tasks SET number = 0 WHERE number IS NULL")
|
||||
|
||||
# Number existing tasks sequentially per project
|
||||
op.execute("""
|
||||
WITH numbered AS (
|
||||
SELECT id, ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY created_at) as rn
|
||||
FROM tasks
|
||||
)
|
||||
UPDATE tasks SET number = numbered.rn FROM numbered WHERE tasks.id = numbered.id
|
||||
""")
|
||||
# Update project counters
|
||||
op.execute("""
|
||||
UPDATE projects SET task_counter = COALESCE(
|
||||
(SELECT MAX(number) FROM tasks WHERE tasks.project_id = projects.id), 0
|
||||
)
|
||||
""")
|
||||
|
||||
# Make NOT NULL
|
||||
op.alter_column('projects', 'key_prefix', nullable=False)
|
||||
op.alter_column('projects', 'task_counter', nullable=False, server_default='0')
|
||||
op.alter_column('tasks', 'number', nullable=False, server_default='0')
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('tasks', 'number')
|
||||
op.drop_column('projects', 'task_counter')
|
||||
op.drop_column('projects', 'key_prefix')
|
||||
@ -17,6 +17,7 @@ class ProjectCreate(BaseModel):
|
||||
name: str
|
||||
slug: str
|
||||
description: str | None = None
|
||||
key_prefix: str | None = None # auto-generated from name if not provided
|
||||
git_repo: str | None = None
|
||||
|
||||
|
||||
@ -31,6 +32,8 @@ class ProjectOut(BaseModel):
|
||||
name: str
|
||||
slug: str
|
||||
description: str | None
|
||||
key_prefix: str
|
||||
task_counter: int
|
||||
git_repo: str | None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
@ -44,7 +47,12 @@ async def list_projects(db: AsyncSession = Depends(get_db)):
|
||||
|
||||
@router.post("/", response_model=ProjectOut, status_code=201)
|
||||
async def create_project(data: ProjectCreate, db: AsyncSession = Depends(get_db)):
|
||||
project = Project(**data.model_dump())
|
||||
dump = data.model_dump()
|
||||
# Auto-generate key_prefix from name (first 3 chars, uppercase)
|
||||
if not dump.get("key_prefix"):
|
||||
prefix = "".join(c for c in data.name.upper() if c.isalpha())[:3]
|
||||
dump["key_prefix"] = prefix or "PRJ"
|
||||
project = Project(**dump)
|
||||
db.add(project)
|
||||
await db.commit()
|
||||
await db.refresh(project)
|
||||
|
||||
@ -9,7 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from tracker.database import get_db
|
||||
from tracker.models import Task
|
||||
from tracker.models import Task, Project
|
||||
|
||||
router = APIRouter(prefix="/tasks", tags=["tasks"])
|
||||
|
||||
@ -46,11 +46,33 @@ class TaskOut(BaseModel):
|
||||
requires_pr: bool
|
||||
pr_url: str | None
|
||||
assigned_agent_id: uuid.UUID | None
|
||||
number: int
|
||||
key: str | None = None # e.g. "TEA-1"
|
||||
position: int
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
async def _enrich_tasks(tasks: list[Task], db: AsyncSession) -> list[TaskOut]:
|
||||
"""Add key (e.g. TEA-1) to task outputs."""
|
||||
if not tasks:
|
||||
return []
|
||||
# Collect project ids
|
||||
project_ids = {t.project_id for t in tasks}
|
||||
projects = {}
|
||||
for pid in project_ids:
|
||||
p = await db.get(Project, pid)
|
||||
if p:
|
||||
projects[p.id] = p.key_prefix
|
||||
result = []
|
||||
for t in tasks:
|
||||
out = TaskOut.model_validate(t)
|
||||
prefix = projects.get(t.project_id, "???")
|
||||
out.key = f"{prefix}-{t.number}" if t.number else None
|
||||
result.append(out)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/", response_model=list[TaskOut])
|
||||
async def list_tasks(
|
||||
project_id: uuid.UUID | None = None,
|
||||
@ -63,16 +85,27 @@ async def list_tasks(
|
||||
if status:
|
||||
q = q.where(Task.status == status)
|
||||
result = await db.execute(q)
|
||||
return result.scalars().all()
|
||||
tasks = list(result.scalars().all())
|
||||
return await _enrich_tasks(tasks, db)
|
||||
|
||||
|
||||
@router.post("/", response_model=TaskOut, status_code=201)
|
||||
async def create_task(data: TaskCreate, db: AsyncSession = Depends(get_db)):
|
||||
task = Task(**data.model_dump())
|
||||
# Get project and increment counter
|
||||
project = await db.get(Project, data.project_id)
|
||||
if not project:
|
||||
raise HTTPException(404, "Project not found")
|
||||
project.task_counter += 1
|
||||
number = project.task_counter
|
||||
|
||||
task = Task(**data.model_dump(), number=number)
|
||||
db.add(task)
|
||||
await db.commit()
|
||||
await db.refresh(task)
|
||||
return task
|
||||
# Attach key
|
||||
task_out = TaskOut.model_validate(task)
|
||||
task_out.key = f"{project.key_prefix}-{number}"
|
||||
return task_out
|
||||
|
||||
|
||||
@router.get("/{task_id}", response_model=TaskOut)
|
||||
@ -80,7 +113,8 @@ async def get_task(task_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
||||
task = await db.get(Task, task_id)
|
||||
if not task:
|
||||
raise HTTPException(404, "Task not found")
|
||||
return task
|
||||
enriched = await _enrich_tasks([task], db)
|
||||
return enriched[0]
|
||||
|
||||
|
||||
@router.patch("/{task_id}", response_model=TaskOut)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import String, Text
|
||||
from sqlalchemy import Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from tracker.models.base import Base
|
||||
@ -19,6 +19,8 @@ class Project(Base):
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
slug: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
key_prefix: Mapped[str] = mapped_column(String(10), nullable=False, default="PRJ") # e.g. "TEA", "DIC"
|
||||
task_counter: Mapped[int] = mapped_column(Integer, default=0) # auto-increment for task numbers
|
||||
git_repo: Mapped[str | None] = mapped_column(String(500))
|
||||
|
||||
tasks: Mapped[list["Task"]] = relationship(back_populates="project", cascade="all, delete-orphan")
|
||||
|
||||
@ -26,6 +26,7 @@ class Task(Base):
|
||||
requires_pr: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
pr_url: Mapped[str | None] = mapped_column(String(500))
|
||||
assigned_agent_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("agents.id"))
|
||||
number: Mapped[int] = mapped_column(Integer, nullable=False, default=0) # project-scoped sequential number (TEA-1, TEA-2...)
|
||||
position: Mapped[int] = mapped_column(Integer, default=0) # ordering within column
|
||||
|
||||
project: Mapped["Project"] = relationship(back_populates="tasks")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user