refactor: UUID-based member identification + task audit log + soft delete
Some checks failed
Deploy Tracker / deploy (push) Failing after 2s
Some checks failed
Deploy Tracker / deploy (push) Failing after 2s
- Member: добавлен is_active для soft delete - Message: author_id (UUID FK) вместо author_slug - Task: assignee_id, reviewer_id (UUID FK), watcher_ids (UUID[]) - Новая модель TaskAction для аудита действий с задачами - Новый enum TaskActionType - API обновлен под новые поля с relationships - WS handler использует author_id вместо author_slug - Soft delete для members через is_active=False - Автоматическое создание TaskAction записей при изменениях
This commit is contained in:
parent
daf3f06dcb
commit
41f4bc5ebc
@ -3,7 +3,7 @@
|
|||||||
import secrets
|
import secrets
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@ -96,10 +96,16 @@ def _member_to_out(m: Member) -> dict:
|
|||||||
# --- Endpoints ---
|
# --- Endpoints ---
|
||||||
|
|
||||||
@router.get("/members", response_model=list[MemberOut])
|
@router.get("/members", response_model=list[MemberOut])
|
||||||
async def list_members(db: AsyncSession = Depends(get_db)):
|
async def list_members(
|
||||||
result = await db.execute(
|
include_inactive: bool = Query(False),
|
||||||
select(Member).options(selectinload(Member.agent_config))
|
db: AsyncSession = Depends(get_db)
|
||||||
)
|
):
|
||||||
|
query = select(Member).options(selectinload(Member.agent_config))
|
||||||
|
|
||||||
|
if not include_inactive:
|
||||||
|
query = query.where(Member.is_active == True)
|
||||||
|
|
||||||
|
result = await db.execute(query)
|
||||||
return [_member_to_out(m) for m in result.scalars()]
|
return [_member_to_out(m) for m in result.scalars()]
|
||||||
|
|
||||||
|
|
||||||
@ -237,3 +243,16 @@ async def update_my_status(status: str, db: AsyncSession = Depends(get_db)):
|
|||||||
"""Quick status update (used by agents)."""
|
"""Quick status update (used by agents)."""
|
||||||
# TODO: get current member from auth
|
# TODO: get current member from auth
|
||||||
return {"status": status}
|
return {"status": status}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/members/{slug}")
|
||||||
|
async def delete_member(slug: str, db: AsyncSession = Depends(get_db)):
|
||||||
|
"""Soft delete member by setting is_active=False."""
|
||||||
|
result = await db.execute(select(Member).where(Member.slug == slug))
|
||||||
|
member = result.scalar_one_or_none()
|
||||||
|
if not member:
|
||||||
|
raise HTTPException(404, "Member not found")
|
||||||
|
|
||||||
|
member.is_active = False
|
||||||
|
await db.commit()
|
||||||
|
return {"ok": True}
|
||||||
|
|||||||
@ -31,7 +31,8 @@ class MessageOut(BaseModel):
|
|||||||
task_id: str | None = None
|
task_id: str | None = None
|
||||||
parent_id: str | None = None
|
parent_id: str | None = None
|
||||||
author_type: str
|
author_type: str
|
||||||
author_slug: str
|
author_id: str
|
||||||
|
author: dict | None = None # Member object for display
|
||||||
content: str
|
content: str
|
||||||
mentions: list[str] = []
|
mentions: list[str] = []
|
||||||
voice_url: str | None = None
|
voice_url: str | None = None
|
||||||
@ -44,7 +45,7 @@ class MessageCreate(BaseModel):
|
|||||||
task_id: str | None = None
|
task_id: str | None = None
|
||||||
parent_id: str | None = None
|
parent_id: str | None = None
|
||||||
author_type: str = AuthorType.HUMAN
|
author_type: str = AuthorType.HUMAN
|
||||||
author_slug: str = "admin"
|
author_id: str
|
||||||
content: str
|
content: str
|
||||||
mentions: list[str] = []
|
mentions: list[str] = []
|
||||||
voice_url: str | None = None
|
voice_url: str | None = None
|
||||||
@ -59,7 +60,8 @@ def _message_out(m: Message) -> dict:
|
|||||||
"task_id": str(m.task_id) if m.task_id else None,
|
"task_id": str(m.task_id) if m.task_id else None,
|
||||||
"parent_id": str(m.parent_id) if m.parent_id else None,
|
"parent_id": str(m.parent_id) if m.parent_id else None,
|
||||||
"author_type": m.author_type,
|
"author_type": m.author_type,
|
||||||
"author_slug": m.author_slug,
|
"author_id": str(m.author_id),
|
||||||
|
"author": {"id": str(m.author.id), "slug": m.author.slug, "name": m.author.name} if m.author else None,
|
||||||
"content": m.content,
|
"content": m.content,
|
||||||
"mentions": m.mentions or [],
|
"mentions": m.mentions or [],
|
||||||
"voice_url": m.voice_url,
|
"voice_url": m.voice_url,
|
||||||
@ -82,7 +84,7 @@ async def list_messages(
|
|||||||
offset: int = Query(0),
|
offset: int = Query(0),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
q = select(Message).options(selectinload(Message.attachments))
|
q = select(Message).options(selectinload(Message.attachments), selectinload(Message.author))
|
||||||
|
|
||||||
if chat_id:
|
if chat_id:
|
||||||
q = q.where(Message.chat_id == uuid.UUID(chat_id))
|
q = q.where(Message.chat_id == uuid.UUID(chat_id))
|
||||||
@ -113,7 +115,7 @@ async def create_message(req: MessageCreate, db: AsyncSession = Depends(get_db))
|
|||||||
task_id=uuid.UUID(req.task_id) if req.task_id else None,
|
task_id=uuid.UUID(req.task_id) if req.task_id else None,
|
||||||
parent_id=uuid.UUID(req.parent_id) if req.parent_id else None,
|
parent_id=uuid.UUID(req.parent_id) if req.parent_id else None,
|
||||||
author_type=req.author_type,
|
author_type=req.author_type,
|
||||||
author_slug=req.author_slug,
|
author_id=uuid.UUID(req.author_id),
|
||||||
content=req.content,
|
content=req.content,
|
||||||
mentions=req.mentions,
|
mentions=req.mentions,
|
||||||
voice_url=req.voice_url,
|
voice_url=req.voice_url,
|
||||||
@ -121,15 +123,16 @@ async def create_message(req: MessageCreate, db: AsyncSession = Depends(get_db))
|
|||||||
db.add(msg)
|
db.add(msg)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
result2 = await db.execute(
|
result2 = await db.execute(
|
||||||
select(Message).where(Message.id == msg.id).options(selectinload(Message.attachments))
|
select(Message).where(Message.id == msg.id).options(
|
||||||
|
selectinload(Message.attachments),
|
||||||
|
selectinload(Message.author)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
msg = result2.scalar_one()
|
msg = result2.scalar_one()
|
||||||
|
|
||||||
# Get author name for broadcast
|
# Get author name for broadcast (already loaded via relationship)
|
||||||
from tracker.models import Member
|
author_name = msg.author.name if msg.author else "Unknown"
|
||||||
author_result = await db.execute(select(Member).where(Member.slug == msg.author_slug))
|
author_slug = msg.author.slug if msg.author else "unknown"
|
||||||
author = author_result.scalar_one_or_none()
|
|
||||||
author_name = author.name if author else msg.author_slug
|
|
||||||
|
|
||||||
# Broadcast via WebSocket
|
# Broadcast via WebSocket
|
||||||
from tracker.ws.manager import manager
|
from tracker.ws.manager import manager
|
||||||
@ -138,7 +141,8 @@ async def create_message(req: MessageCreate, db: AsyncSession = Depends(get_db))
|
|||||||
"chat_id": req.chat_id,
|
"chat_id": req.chat_id,
|
||||||
"task_id": req.task_id,
|
"task_id": req.task_id,
|
||||||
"author_type": msg.author_type,
|
"author_type": msg.author_type,
|
||||||
"author_slug": msg.author_slug,
|
"author_id": str(msg.author_id),
|
||||||
|
"author_slug": author_slug,
|
||||||
"author_name": author_name,
|
"author_name": author_name,
|
||||||
"content": msg.content,
|
"content": msg.content,
|
||||||
"mentions": msg.mentions or [],
|
"mentions": msg.mentions or [],
|
||||||
@ -154,16 +158,16 @@ async def create_message(req: MessageCreate, db: AsyncSession = Depends(get_db))
|
|||||||
elif chat and chat.kind == ChatKind.LOBBY:
|
elif chat and chat.kind == ChatKind.LOBBY:
|
||||||
await manager.broadcast_all(
|
await manager.broadcast_all(
|
||||||
{"type": "message.new", "data": msg_data},
|
{"type": "message.new", "data": msg_data},
|
||||||
exclude_slug=msg.author_slug,
|
exclude_slug=author_slug,
|
||||||
)
|
)
|
||||||
return _message_out(msg)
|
return _message_out(msg)
|
||||||
|
|
||||||
if project_id:
|
if project_id:
|
||||||
await manager.broadcast_message(project_id, msg_data, author_slug=msg.author_slug)
|
await manager.broadcast_message(project_id, msg_data, author_slug=author_slug)
|
||||||
else:
|
else:
|
||||||
await manager.broadcast_all(
|
await manager.broadcast_all(
|
||||||
{"type": "message.new", "data": msg_data},
|
{"type": "message.new", "data": msg_data},
|
||||||
exclude_slug=msg.author_slug,
|
exclude_slug=author_slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
return _message_out(msg)
|
return _message_out(msg)
|
||||||
@ -175,7 +179,7 @@ async def list_replies(message_id: str, db: AsyncSession = Depends(get_db)):
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Message)
|
select(Message)
|
||||||
.where(Message.parent_id == uuid.UUID(message_id))
|
.where(Message.parent_id == uuid.UUID(message_id))
|
||||||
.options(selectinload(Message.attachments))
|
.options(selectinload(Message.attachments), selectinload(Message.author))
|
||||||
.order_by(Message.created_at)
|
.order_by(Message.created_at)
|
||||||
)
|
)
|
||||||
return [_message_out(m) for m in result.scalars()]
|
return [_message_out(m) for m in result.scalars()]
|
||||||
|
|||||||
@ -10,8 +10,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from sqlalchemy.orm import selectinload, joinedload
|
from sqlalchemy.orm import selectinload, joinedload
|
||||||
|
|
||||||
from tracker.database import get_db
|
from tracker.database import get_db
|
||||||
from tracker.enums import AuthorType, ChatKind, TaskPriority, TaskStatus, TaskType
|
from tracker.enums import AuthorType, ChatKind, TaskPriority, TaskStatus, TaskType, TaskActionType
|
||||||
from tracker.models import Task, Step, Project, Member, Message, Chat
|
from tracker.models import Task, Step, Project, Member, Message, Chat, TaskAction
|
||||||
from tracker.api.auth import get_current_member
|
from tracker.api.auth import get_current_member
|
||||||
|
|
||||||
router = APIRouter(tags=["tasks"])
|
router = APIRouter(tags=["tasks"])
|
||||||
@ -38,9 +38,11 @@ class TaskOut(BaseModel):
|
|||||||
status: str
|
status: str
|
||||||
priority: str
|
priority: str
|
||||||
labels: list[str] = []
|
labels: list[str] = []
|
||||||
assignee_slug: str | None = None
|
assignee_id: str | None = None
|
||||||
reviewer_slug: str | None = None
|
assignee: dict | None = None # Member object for display
|
||||||
watchers: list[str] = []
|
reviewer_id: str | None = None
|
||||||
|
reviewer: dict | None = None # Member object for display
|
||||||
|
watcher_ids: list[str] = []
|
||||||
depends_on: list[str] = []
|
depends_on: list[str] = []
|
||||||
position: int
|
position: int
|
||||||
time_spent: int
|
time_spent: int
|
||||||
@ -55,8 +57,8 @@ class TaskCreate(BaseModel):
|
|||||||
priority: str = TaskPriority.MEDIUM
|
priority: str = TaskPriority.MEDIUM
|
||||||
labels: list[str] = []
|
labels: list[str] = []
|
||||||
parent_id: str | None = None
|
parent_id: str | None = None
|
||||||
assignee_slug: str | None = None
|
assignee_id: str | None = None
|
||||||
reviewer_slug: str | None = None
|
reviewer_id: str | None = None
|
||||||
depends_on: list[str] = []
|
depends_on: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
@ -67,8 +69,8 @@ class TaskUpdate(BaseModel):
|
|||||||
status: str | None = None
|
status: str | None = None
|
||||||
priority: str | None = None
|
priority: str | None = None
|
||||||
labels: list[str] | None = None
|
labels: list[str] | None = None
|
||||||
assignee_slug: str | None = None
|
assignee_id: str | None = None
|
||||||
reviewer_slug: str | None = None
|
reviewer_id: str | None = None
|
||||||
position: int | None = None
|
position: int | None = None
|
||||||
|
|
||||||
|
|
||||||
@ -77,7 +79,7 @@ class RejectRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class AssignRequest(BaseModel):
|
class AssignRequest(BaseModel):
|
||||||
assignee_slug: str
|
assignee_id: str
|
||||||
|
|
||||||
|
|
||||||
# --- Helpers ---
|
# --- Helpers ---
|
||||||
@ -97,7 +99,7 @@ async def _system_message(
|
|||||||
chat_text: str,
|
chat_text: str,
|
||||||
task_text: str | None = None,
|
task_text: str | None = None,
|
||||||
project_slug: str = "",
|
project_slug: str = "",
|
||||||
actor_slug: str = "system",
|
actor_id: str | None = None,
|
||||||
):
|
):
|
||||||
"""Create system messages: one in project chat + one in task comments.
|
"""Create system messages: one in project chat + one in task comments.
|
||||||
|
|
||||||
@ -118,7 +120,7 @@ async def _system_message(
|
|||||||
task_msg = Message(
|
task_msg = Message(
|
||||||
task_id=task.id,
|
task_id=task.id,
|
||||||
author_type=AuthorType.SYSTEM,
|
author_type=AuthorType.SYSTEM,
|
||||||
author_slug=actor_slug,
|
author_id=uuid.UUID(actor_id) if actor_id else None,
|
||||||
content=task_text,
|
content=task_text,
|
||||||
mentions=[],
|
mentions=[],
|
||||||
)
|
)
|
||||||
@ -130,7 +132,7 @@ async def _system_message(
|
|||||||
chat_msg = Message(
|
chat_msg = Message(
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
author_type=AuthorType.SYSTEM,
|
author_type=AuthorType.SYSTEM,
|
||||||
author_slug=actor_slug,
|
author_id=uuid.UUID(actor_id) if actor_id else None,
|
||||||
content=chat_text,
|
content=chat_text,
|
||||||
mentions=[],
|
mentions=[],
|
||||||
)
|
)
|
||||||
@ -175,9 +177,11 @@ def _task_out(t: Task, project_slug: str = "") -> dict:
|
|||||||
"status": t.status,
|
"status": t.status,
|
||||||
"priority": t.priority,
|
"priority": t.priority,
|
||||||
"labels": t.labels or [],
|
"labels": t.labels or [],
|
||||||
"assignee_slug": t.assignee_slug,
|
"assignee_id": str(t.assignee_id) if t.assignee_id else None,
|
||||||
"reviewer_slug": t.reviewer_slug,
|
"assignee": {"id": str(t.assignee.id), "slug": t.assignee.slug, "name": t.assignee.name} if t.assignee else None,
|
||||||
"watchers": t.watchers or [],
|
"reviewer_id": str(t.reviewer_id) if t.reviewer_id else None,
|
||||||
|
"reviewer": {"id": str(t.reviewer.id), "slug": t.reviewer.slug, "name": t.reviewer.name} if t.reviewer else None,
|
||||||
|
"watcher_ids": [str(w) for w in (t.watcher_ids or [])],
|
||||||
"depends_on": [str(d) for d in (t.depends_on or [])],
|
"depends_on": [str(d) for d in (t.depends_on or [])],
|
||||||
"position": t.position,
|
"position": t.position,
|
||||||
"time_spent": t.time_spent,
|
"time_spent": t.time_spent,
|
||||||
@ -191,7 +195,12 @@ def _task_out(t: Task, project_slug: str = "") -> dict:
|
|||||||
async def _get_task(task_id: str, db: AsyncSession) -> Task:
|
async def _get_task(task_id: str, db: AsyncSession) -> Task:
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Task).where(Task.id == uuid.UUID(task_id))
|
select(Task).where(Task.id == uuid.UUID(task_id))
|
||||||
.options(selectinload(Task.steps), joinedload(Task.project))
|
.options(
|
||||||
|
selectinload(Task.steps),
|
||||||
|
joinedload(Task.project),
|
||||||
|
joinedload(Task.assignee),
|
||||||
|
joinedload(Task.reviewer)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
task = result.scalar_one_or_none()
|
task = result.scalar_one_or_none()
|
||||||
if not task:
|
if not task:
|
||||||
@ -209,13 +218,18 @@ async def list_tasks(
|
|||||||
label: Optional[str] = Query(None),
|
label: Optional[str] = Query(None),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
q = select(Task).options(selectinload(Task.steps), joinedload(Task.project))
|
q = select(Task).options(
|
||||||
|
selectinload(Task.steps),
|
||||||
|
joinedload(Task.project),
|
||||||
|
joinedload(Task.assignee),
|
||||||
|
joinedload(Task.reviewer)
|
||||||
|
)
|
||||||
if project_id:
|
if project_id:
|
||||||
q = q.where(Task.project_id == uuid.UUID(project_id))
|
q = q.where(Task.project_id == uuid.UUID(project_id))
|
||||||
if status:
|
if status:
|
||||||
q = q.where(Task.status == status)
|
q = q.where(Task.status == status)
|
||||||
if assignee:
|
if assignee:
|
||||||
q = q.where(Task.assignee_slug == assignee)
|
q = q.where(Task.assignee_id == uuid.UUID(assignee))
|
||||||
if label:
|
if label:
|
||||||
q = q.where(Task.labels.contains([label]))
|
q = q.where(Task.labels.contains([label]))
|
||||||
q = q.order_by(Task.position, Task.created_at)
|
q = q.order_by(Task.position, Task.created_at)
|
||||||
@ -256,12 +270,21 @@ async def create_task(
|
|||||||
status=req.status,
|
status=req.status,
|
||||||
priority=req.priority,
|
priority=req.priority,
|
||||||
labels=req.labels,
|
labels=req.labels,
|
||||||
assignee_slug=req.assignee_slug,
|
assignee_id=uuid.UUID(req.assignee_id) if req.assignee_id else None,
|
||||||
reviewer_slug=req.reviewer_slug,
|
reviewer_id=uuid.UUID(req.reviewer_id) if req.reviewer_id else None,
|
||||||
depends_on=[uuid.UUID(d) for d in req.depends_on] if req.depends_on else [],
|
depends_on=[uuid.UUID(d) for d in req.depends_on] if req.depends_on else [],
|
||||||
watchers=[req.assignee_slug] if req.assignee_slug else [],
|
watcher_ids=[uuid.UUID(req.assignee_id)] if req.assignee_id else [],
|
||||||
)
|
)
|
||||||
db.add(task)
|
db.add(task)
|
||||||
|
|
||||||
|
# Create audit log
|
||||||
|
task_action = TaskAction(
|
||||||
|
task_id=task.id,
|
||||||
|
actor_id=current_member.id,
|
||||||
|
action=TaskActionType.CREATED
|
||||||
|
)
|
||||||
|
db.add(task_action)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(task)
|
await db.refresh(task)
|
||||||
# Load steps
|
# Load steps
|
||||||
@ -311,7 +334,7 @@ async def update_task(
|
|||||||
who = f"@{current_member.slug}"
|
who = f"@{current_member.slug}"
|
||||||
|
|
||||||
old_status = task.status
|
old_status = task.status
|
||||||
old_assignee = task.assignee_slug
|
old_assignee_id = task.assignee_id
|
||||||
|
|
||||||
if req.title is not None:
|
if req.title is not None:
|
||||||
task.title = req.title
|
task.title = req.title
|
||||||
@ -325,10 +348,10 @@ async def update_task(
|
|||||||
task.priority = req.priority
|
task.priority = req.priority
|
||||||
if req.labels is not None:
|
if req.labels is not None:
|
||||||
task.labels = req.labels
|
task.labels = req.labels
|
||||||
if req.assignee_slug is not None:
|
if req.assignee_id is not None:
|
||||||
task.assignee_slug = req.assignee_slug or None
|
task.assignee_id = uuid.UUID(req.assignee_id) if req.assignee_id else None
|
||||||
if req.reviewer_slug is not None:
|
if req.reviewer_id is not None:
|
||||||
task.reviewer_slug = req.reviewer_slug or None
|
task.reviewer_id = uuid.UUID(req.reviewer_id) if req.reviewer_id else None
|
||||||
if req.position is not None:
|
if req.position is not None:
|
||||||
task.position = req.position
|
task.position = req.position
|
||||||
|
|
||||||
@ -341,7 +364,7 @@ async def update_task(
|
|||||||
chat_text=f"{key}: {old_status} → {req.status}",
|
chat_text=f"{key}: {old_status} → {req.status}",
|
||||||
task_text=f"{who} изменил статус: {old_status} → {req.status} ({now_str})",
|
task_text=f"{who} изменил статус: {old_status} → {req.status} ({now_str})",
|
||||||
project_slug=slug,
|
project_slug=slug,
|
||||||
actor_slug=current_member.slug,
|
actor_id=str(current_member.id),
|
||||||
)
|
)
|
||||||
if req.assignee_slug is not None and req.assignee_slug != old_assignee:
|
if req.assignee_slug is not None and req.assignee_slug != old_assignee:
|
||||||
if req.assignee_slug:
|
if req.assignee_slug:
|
||||||
|
|||||||
@ -69,6 +69,20 @@ class AuthorType(StrEnum):
|
|||||||
SYSTEM = "system"
|
SYSTEM = "system"
|
||||||
|
|
||||||
|
|
||||||
|
class TaskActionType(StrEnum):
|
||||||
|
CREATED = "created"
|
||||||
|
STATUS_CHANGED = "status_changed"
|
||||||
|
ASSIGNED = "assigned"
|
||||||
|
UNASSIGNED = "unassigned"
|
||||||
|
PRIORITY_CHANGED = "priority_changed"
|
||||||
|
TITLE_CHANGED = "title_changed"
|
||||||
|
DESCRIPTION_CHANGED = "description_changed"
|
||||||
|
REVIEWER_CHANGED = "reviewer_changed"
|
||||||
|
WATCHER_ADDED = "watcher_added"
|
||||||
|
WATCHER_REMOVED = "watcher_removed"
|
||||||
|
DELETED = "deleted"
|
||||||
|
|
||||||
|
|
||||||
class WSEventType(StrEnum):
|
class WSEventType(StrEnum):
|
||||||
AUTH = "auth"
|
AUTH = "auth"
|
||||||
AUTH_OK = "auth.ok"
|
AUTH_OK = "auth.ok"
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
from tracker.models.base import Base
|
from tracker.models.base import Base
|
||||||
from tracker.models.member import AgentConfig, Member
|
from tracker.models.member import AgentConfig, Member
|
||||||
from tracker.models.project import Project, ProjectMember
|
from tracker.models.project import Project, ProjectMember
|
||||||
from tracker.models.task import Step, Task
|
from tracker.models.task import Step, Task, TaskAction
|
||||||
from tracker.models.chat import Attachment, Chat, Message
|
from tracker.models.chat import Attachment, Chat, Message
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -13,6 +13,7 @@ __all__ = [
|
|||||||
"Project",
|
"Project",
|
||||||
"ProjectMember",
|
"ProjectMember",
|
||||||
"Task",
|
"Task",
|
||||||
|
"TaskAction",
|
||||||
"Step",
|
"Step",
|
||||||
"Chat",
|
"Chat",
|
||||||
"Message",
|
"Message",
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from tracker.enums import ChatKind
|
|||||||
from tracker.models.base import Base
|
from tracker.models.base import Base
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from tracker.models.member import Member
|
||||||
from tracker.models.project import Project
|
from tracker.models.project import Project
|
||||||
|
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ class Message(Base):
|
|||||||
|
|
||||||
# Author
|
# Author
|
||||||
author_type: Mapped[str] = mapped_column(String(20), nullable=False) # human | agent | system
|
author_type: Mapped[str] = mapped_column(String(20), nullable=False) # human | agent | system
|
||||||
author_slug: Mapped[str] = mapped_column(String(255), nullable=False)
|
author_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("members.id"), nullable=False)
|
||||||
|
|
||||||
# Content
|
# Content
|
||||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
@ -50,6 +51,7 @@ class Message(Base):
|
|||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
chat: Mapped["Chat | None"] = relationship(back_populates="messages", foreign_keys=[chat_id])
|
chat: Mapped["Chat | None"] = relationship(back_populates="messages", foreign_keys=[chat_id])
|
||||||
|
author: Mapped["Member"] = relationship()
|
||||||
attachments: Mapped[list["Attachment"]] = relationship(back_populates="message", cascade="all, delete-orphan")
|
attachments: Mapped[list["Attachment"]] = relationship(back_populates="message", cascade="all, delete-orphan")
|
||||||
replies: Mapped[list["Message"]] = relationship(back_populates="parent")
|
replies: Mapped[list["Message"]] = relationship(back_populates="parent")
|
||||||
parent: Mapped["Message | None"] = relationship(back_populates="replies", remote_side="Message.id")
|
parent: Mapped["Message | None"] = relationship(back_populates="replies", remote_side="Message.id")
|
||||||
|
|||||||
@ -26,6 +26,7 @@ class Member(Base):
|
|||||||
token: Mapped[str | None] = mapped_column(String(255), unique=True) # for agents/bridges
|
token: Mapped[str | None] = mapped_column(String(255), unique=True) # for agents/bridges
|
||||||
status: Mapped[str] = mapped_column(String(20), default=MemberStatus.OFFLINE) # online | offline | busy
|
status: Mapped[str] = mapped_column(String(20), default=MemberStatus.OFFLINE) # online | offline | busy
|
||||||
avatar_url: Mapped[str | None] = mapped_column(String(500))
|
avatar_url: Mapped[str | None] = mapped_column(String(500))
|
||||||
|
is_active: Mapped[bool] = mapped_column(default=True)
|
||||||
|
|
||||||
# Agent-specific config (nullable for humans)
|
# Agent-specific config (nullable for humans)
|
||||||
agent_config: Mapped["AgentConfig | None"] = relationship(
|
agent_config: Mapped["AgentConfig | None"] = relationship(
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from tracker.enums import TaskPriority, TaskStatus, TaskType
|
|||||||
from tracker.models.base import Base
|
from tracker.models.base import Base
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from tracker.models.member import Member
|
||||||
from tracker.models.project import Project
|
from tracker.models.project import Project
|
||||||
|
|
||||||
|
|
||||||
@ -26,14 +27,16 @@ class Task(Base):
|
|||||||
status: Mapped[str] = mapped_column(String(20), default=TaskStatus.BACKLOG) # backlog | todo | in_progress | in_review | done
|
status: Mapped[str] = mapped_column(String(20), default=TaskStatus.BACKLOG) # backlog | todo | in_progress | in_review | done
|
||||||
priority: Mapped[str] = mapped_column(String(20), default=TaskPriority.MEDIUM) # critical | high | medium | low
|
priority: Mapped[str] = mapped_column(String(20), default=TaskPriority.MEDIUM) # critical | high | medium | low
|
||||||
labels: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
|
labels: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
|
||||||
assignee_slug: Mapped[str | None] = mapped_column(String(255))
|
assignee_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("members.id"), nullable=True)
|
||||||
reviewer_slug: Mapped[str | None] = mapped_column(String(255))
|
reviewer_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("members.id"), nullable=True)
|
||||||
watchers: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) # slugs
|
watcher_ids: Mapped[list[uuid.UUID]] = mapped_column(ARRAY(UUID(as_uuid=True)), default=list)
|
||||||
depends_on: Mapped[list[uuid.UUID]] = mapped_column(ARRAY(UUID(as_uuid=True)), default=list)
|
depends_on: Mapped[list[uuid.UUID]] = mapped_column(ARRAY(UUID(as_uuid=True)), default=list)
|
||||||
position: Mapped[int] = mapped_column(Integer, default=0) # ordering within column
|
position: Mapped[int] = mapped_column(Integer, default=0) # ordering within column
|
||||||
time_spent: Mapped[int] = mapped_column(Integer, default=0) # minutes
|
time_spent: Mapped[int] = mapped_column(Integer, default=0) # minutes
|
||||||
|
|
||||||
project: Mapped["Project"] = relationship(back_populates="tasks")
|
project: Mapped["Project"] = relationship(back_populates="tasks")
|
||||||
|
assignee: Mapped["Member | None"] = relationship(foreign_keys=[assignee_id])
|
||||||
|
reviewer: Mapped["Member | None"] = relationship(foreign_keys=[reviewer_id])
|
||||||
parent: Mapped["Task | None"] = relationship(remote_side="Task.id", back_populates="subtasks")
|
parent: Mapped["Task | None"] = relationship(remote_side="Task.id", back_populates="subtasks")
|
||||||
subtasks: Mapped[list["Task"]] = relationship(back_populates="parent")
|
subtasks: Mapped[list["Task"]] = relationship(back_populates="parent")
|
||||||
steps: Mapped[list["Step"]] = relationship(back_populates="task", cascade="all, delete-orphan", order_by="Step.position")
|
steps: Mapped[list["Step"]] = relationship(back_populates="task", cascade="all, delete-orphan", order_by="Step.position")
|
||||||
@ -48,3 +51,17 @@ class Step(Base):
|
|||||||
position: Mapped[int] = mapped_column(Integer, default=0)
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
task: Mapped["Task"] = relationship(back_populates="steps")
|
task: Mapped["Task"] = relationship(back_populates="steps")
|
||||||
|
|
||||||
|
|
||||||
|
class TaskAction(Base):
|
||||||
|
__tablename__ = "task_actions"
|
||||||
|
|
||||||
|
task_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"))
|
||||||
|
actor_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("members.id"))
|
||||||
|
action: Mapped[str] = mapped_column(String(50), nullable=False) # created, status_changed, assigned, etc.
|
||||||
|
field: Mapped[str | None] = mapped_column(String(100)) # какое поле изменилось
|
||||||
|
old_value: Mapped[str | None] = mapped_column(Text)
|
||||||
|
new_value: Mapped[str | None] = mapped_column(Text)
|
||||||
|
|
||||||
|
task: Mapped["Task"] = relationship()
|
||||||
|
actor: Mapped["Member"] = relationship()
|
||||||
|
|||||||
@ -275,7 +275,7 @@ async def _handle_chat_send(session_id: str, data: dict):
|
|||||||
chat_id=uuid.UUID(chat_id) if chat_id else None,
|
chat_id=uuid.UUID(chat_id) if chat_id else None,
|
||||||
task_id=uuid.UUID(task_id) if task_id else None,
|
task_id=uuid.UUID(task_id) if task_id else None,
|
||||||
author_type=member.type,
|
author_type=member.type,
|
||||||
author_slug=member.slug,
|
author_id=member.id,
|
||||||
content=content,
|
content=content,
|
||||||
mentions=mentions,
|
mentions=mentions,
|
||||||
)
|
)
|
||||||
@ -288,6 +288,7 @@ async def _handle_chat_send(session_id: str, data: dict):
|
|||||||
"chat_id": chat_id,
|
"chat_id": chat_id,
|
||||||
"task_id": task_id,
|
"task_id": task_id,
|
||||||
"author_type": member.type,
|
"author_type": member.type,
|
||||||
|
"author_id": str(member.id),
|
||||||
"author_slug": member.slug,
|
"author_slug": member.slug,
|
||||||
"author_name": member.name,
|
"author_name": member.name,
|
||||||
"content": content,
|
"content": content,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user