added RAG, Multiuser, TG bot
This commit is contained in:
+155
-153
@@ -1,153 +1,155 @@
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import get_settings
|
||||
from app.integrations.taiga import TaigaClient
|
||||
from app.projects.service import ProjectService
|
||||
|
||||
MAX_PROJECTS_IN_CONTEXT = 20
|
||||
MAX_OPEN_PER_PROJECT = 8
|
||||
PROJECTS_CACHE_SEC = 120
|
||||
|
||||
_cache: dict[str, Any] = {"data": None, "expires_at": 0.0}
|
||||
|
||||
|
||||
def invalidate_projects_snapshot_cache() -> None:
|
||||
_cache["data"] = None
|
||||
_cache["expires_at"] = 0.0
|
||||
|
||||
|
||||
def get_projects_snapshot(db: Session, *, force: bool = False) -> dict[str, Any]:
|
||||
now = time.time()
|
||||
if not force and _cache["data"] is not None and now < _cache["expires_at"]:
|
||||
return _cache["data"]
|
||||
|
||||
snapshot = _fetch_projects_snapshot(db)
|
||||
_cache["data"] = snapshot
|
||||
_cache["expires_at"] = now + PROJECTS_CACHE_SEC
|
||||
return snapshot
|
||||
|
||||
|
||||
def _fetch_projects_snapshot(db: Session) -> dict[str, Any]:
|
||||
settings = get_settings()
|
||||
service = ProjectService(db)
|
||||
|
||||
if not settings.taiga_configured:
|
||||
return {"configured": False, "projects": [], "open_items": [], "taiga_open": []}
|
||||
|
||||
projects = service.list_projects()
|
||||
if not projects:
|
||||
try:
|
||||
projects = service.sync_taiga_projects()
|
||||
except Exception as exc:
|
||||
return {
|
||||
"configured": True,
|
||||
"projects": [],
|
||||
"open_items": [],
|
||||
"taiga_open": [],
|
||||
"error": str(exc),
|
||||
}
|
||||
|
||||
open_items = service.list_work_items(limit=15, status="open")
|
||||
taiga_open: list[dict[str, Any]] = []
|
||||
fetch_error: str | None = None
|
||||
|
||||
try:
|
||||
client = TaigaClient()
|
||||
for proj in projects[:MAX_PROJECTS_IN_CONTEXT]:
|
||||
stories = client.list_open_userstories(
|
||||
proj["taiga_id"], limit=MAX_OPEN_PER_PROJECT
|
||||
)
|
||||
tasks = client.list_open_tasks(proj["taiga_id"], limit=MAX_OPEN_PER_PROJECT)
|
||||
taiga_open.append(
|
||||
{
|
||||
"slug": proj["slug"],
|
||||
"name": proj["name"],
|
||||
"stories": [
|
||||
{
|
||||
"ref": s.get("ref"),
|
||||
"subject": s.get("subject", "")[:120],
|
||||
}
|
||||
for s in stories
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"ref": t.get("ref"),
|
||||
"subject": t.get("subject", "")[:120],
|
||||
}
|
||||
for t in tasks
|
||||
],
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
fetch_error = str(exc)
|
||||
|
||||
return {
|
||||
"configured": True,
|
||||
"projects": projects,
|
||||
"open_items": open_items,
|
||||
"taiga_open": taiga_open,
|
||||
"error": fetch_error,
|
||||
}
|
||||
|
||||
|
||||
def format_projects_context(snapshot: dict[str, Any]) -> str:
|
||||
if not snapshot.get("configured"):
|
||||
return "[Taiga/Gitea]\nНе настроено (нет TAIGA_USERNAME/PASSWORD в .env)."
|
||||
|
||||
lines = ["[Проекты и задачи — снимок на начало ответа]"]
|
||||
|
||||
if snapshot.get("error"):
|
||||
lines.append(f"⚠ Ошибка загрузки задач из Taiga: {snapshot['error']}")
|
||||
|
||||
projects = snapshot.get("projects") or []
|
||||
if not projects:
|
||||
lines.append("Проекты Taiga: кэш пуст. Вызови sync_taiga_projects.")
|
||||
else:
|
||||
lines.append(f"Проекты Taiga ({len(projects)}):")
|
||||
for p in projects[:MAX_PROJECTS_IN_CONTEXT]:
|
||||
gitea = (
|
||||
f"{p.get('gitea_owner')}/{p.get('gitea_repo')}"
|
||||
if p.get("gitea_configured")
|
||||
else "Gitea не привязан"
|
||||
)
|
||||
lines.append(f"- `{p.get('slug')}`: {p.get('name')} · {gitea}")
|
||||
|
||||
taiga_open = snapshot.get("taiga_open") or []
|
||||
if taiga_open:
|
||||
lines.append("")
|
||||
lines.append("Открытые задачи в Taiga (live):")
|
||||
for block in taiga_open:
|
||||
stories = block.get("stories") or []
|
||||
tasks = block.get("tasks") or []
|
||||
if not stories and not tasks:
|
||||
lines.append(f" `{block.get('slug')}`: нет открытых")
|
||||
continue
|
||||
lines.append(f" `{block.get('slug')}`:")
|
||||
for story in stories:
|
||||
lines.append(f" story #{story.get('ref')} {story.get('subject')}")
|
||||
for task in tasks:
|
||||
lines.append(f" task #{task.get('ref')} {task.get('subject')}")
|
||||
|
||||
open_items = snapshot.get("open_items") or []
|
||||
if open_items:
|
||||
lines.append("")
|
||||
lines.append("Work items созданные ассистентом (локальная БД):")
|
||||
for item in open_items[:10]:
|
||||
gitea_part = f", gitea #{item.get('gitea_issue')}" if item.get("gitea_issue") else ""
|
||||
lines.append(
|
||||
f"- #{item.get('taiga_ref')} {item.get('title')} "
|
||||
f"({item.get('taiga_slug')}{gitea_part})"
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"Правила: "
|
||||
"«какие задачи» → list_taiga_tasks (Taiga API), НЕ list_work_items. "
|
||||
"list_work_items — только созданные через ассистента. "
|
||||
"Не пиши «ожидаю систему» — сразу вызывай tool или отвечай из снимка выше. "
|
||||
"create_work_item — для новых фич/багов из вольного текста."
|
||||
)
|
||||
return "\n".join(lines)
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import get_settings
|
||||
from app.integrations.taiga import TaigaClient
|
||||
from app.projects.service import ProjectService
|
||||
|
||||
MAX_PROJECTS_IN_CONTEXT = 20
|
||||
MAX_OPEN_PER_PROJECT = 8
|
||||
PROJECTS_CACHE_SEC = 120
|
||||
|
||||
_cache: dict[int, dict[str, Any]] = {}
|
||||
|
||||
|
||||
def invalidate_projects_snapshot_cache(user_id: int | None = None) -> None:
|
||||
if user_id is None:
|
||||
_cache.clear()
|
||||
else:
|
||||
_cache.pop(user_id, None)
|
||||
|
||||
|
||||
def get_projects_snapshot(db: Session, user_id: int, *, force: bool = False) -> dict[str, Any]:
|
||||
now = time.time()
|
||||
entry = _cache.get(user_id)
|
||||
if not force and entry and now < entry.get("expires_at", 0):
|
||||
return entry["data"]
|
||||
|
||||
snapshot = _fetch_projects_snapshot(db, user_id)
|
||||
_cache[user_id] = {"data": snapshot, "expires_at": now + PROJECTS_CACHE_SEC}
|
||||
return snapshot
|
||||
|
||||
|
||||
def _fetch_projects_snapshot(db: Session, user_id: int) -> dict[str, Any]:
|
||||
settings = get_settings()
|
||||
service = ProjectService(db, user_id)
|
||||
|
||||
if not settings.taiga_configured:
|
||||
return {"configured": False, "projects": [], "open_items": [], "taiga_open": []}
|
||||
|
||||
projects = service.list_projects()
|
||||
if not projects:
|
||||
try:
|
||||
projects = service.sync_taiga_projects()
|
||||
except Exception as exc:
|
||||
return {
|
||||
"configured": True,
|
||||
"projects": [],
|
||||
"open_items": [],
|
||||
"taiga_open": [],
|
||||
"error": str(exc),
|
||||
}
|
||||
|
||||
open_items = service.list_work_items(limit=15, status="open")
|
||||
taiga_open: list[dict[str, Any]] = []
|
||||
fetch_error: str | None = None
|
||||
|
||||
try:
|
||||
client = TaigaClient()
|
||||
for proj in projects[:MAX_PROJECTS_IN_CONTEXT]:
|
||||
stories = client.list_open_userstories(
|
||||
proj["taiga_id"], limit=MAX_OPEN_PER_PROJECT
|
||||
)
|
||||
tasks = client.list_open_tasks(proj["taiga_id"], limit=MAX_OPEN_PER_PROJECT)
|
||||
taiga_open.append(
|
||||
{
|
||||
"slug": proj["slug"],
|
||||
"name": proj["name"],
|
||||
"stories": [
|
||||
{
|
||||
"ref": s.get("ref"),
|
||||
"subject": s.get("subject", "")[:120],
|
||||
}
|
||||
for s in stories
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"ref": t.get("ref"),
|
||||
"subject": t.get("subject", "")[:120],
|
||||
}
|
||||
for t in tasks
|
||||
],
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
fetch_error = str(exc)
|
||||
|
||||
return {
|
||||
"configured": True,
|
||||
"projects": projects,
|
||||
"open_items": open_items,
|
||||
"taiga_open": taiga_open,
|
||||
"error": fetch_error,
|
||||
}
|
||||
|
||||
|
||||
def format_projects_context(snapshot: dict[str, Any]) -> str:
|
||||
if not snapshot.get("configured"):
|
||||
return "[Taiga/Gitea]\nНе настроено (нет TAIGA_USERNAME/PASSWORD в .env)."
|
||||
|
||||
lines = ["[Проекты и задачи — снимок на начало ответа]"]
|
||||
|
||||
if snapshot.get("error"):
|
||||
lines.append(f"⚠ Ошибка загрузки задач из Taiga: {snapshot['error']}")
|
||||
|
||||
projects = snapshot.get("projects") or []
|
||||
if not projects:
|
||||
lines.append("Проекты Taiga: кэш пуст. Вызови sync_taiga_projects.")
|
||||
else:
|
||||
lines.append(f"Проекты Taiga ({len(projects)}):")
|
||||
for p in projects[:MAX_PROJECTS_IN_CONTEXT]:
|
||||
gitea = (
|
||||
f"{p.get('gitea_owner')}/{p.get('gitea_repo')}"
|
||||
if p.get("gitea_configured")
|
||||
else "Gitea не привязан"
|
||||
)
|
||||
lines.append(f"- `{p.get('slug')}`: {p.get('name')} · {gitea}")
|
||||
|
||||
taiga_open = snapshot.get("taiga_open") or []
|
||||
if taiga_open:
|
||||
lines.append("")
|
||||
lines.append("Открытые задачи в Taiga (live):")
|
||||
for block in taiga_open:
|
||||
stories = block.get("stories") or []
|
||||
tasks = block.get("tasks") or []
|
||||
if not stories and not tasks:
|
||||
lines.append(f" `{block.get('slug')}`: нет открытых")
|
||||
continue
|
||||
lines.append(f" `{block.get('slug')}`:")
|
||||
for story in stories:
|
||||
lines.append(f" story #{story.get('ref')} {story.get('subject')}")
|
||||
for task in tasks:
|
||||
lines.append(f" task #{task.get('ref')} {task.get('subject')}")
|
||||
|
||||
open_items = snapshot.get("open_items") or []
|
||||
if open_items:
|
||||
lines.append("")
|
||||
lines.append("Work items созданные ассистентом (локальная БД):")
|
||||
for item in open_items[:10]:
|
||||
gitea_part = f", gitea #{item.get('gitea_issue')}" if item.get("gitea_issue") else ""
|
||||
lines.append(
|
||||
f"- #{item.get('taiga_ref')} {item.get('title')} "
|
||||
f"({item.get('taiga_slug')}{gitea_part})"
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"Правила: "
|
||||
"«какие задачи» → list_taiga_tasks (Taiga API), НЕ list_work_items. "
|
||||
"list_work_items — только созданные через ассистента. "
|
||||
"Не пиши «ожидаю систему» — сразу вызывай tool или отвечай из снимка выше. "
|
||||
"create_work_item — для новых фич/багов из вольного текста."
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
+476
-466
@@ -1,466 +1,476 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import get_settings
|
||||
from app.db.models import ProjectBinding, TaigaProject, WorkItem
|
||||
from app.integrations.gitea import GiteaClient
|
||||
from app.integrations.taiga import TaigaClient
|
||||
from app.projects.commit_parser import parse_commit_message
|
||||
from app.projects.structuring import (
|
||||
format_gitea_body,
|
||||
format_story_description,
|
||||
slugify_branch,
|
||||
structure_work_item,
|
||||
)
|
||||
|
||||
|
||||
class ProjectService:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.settings = get_settings()
|
||||
|
||||
def sync_taiga_projects(self) -> list[dict[str, Any]]:
|
||||
if not self.settings.taiga_configured:
|
||||
raise ValueError("Taiga не настроена: задайте TAIGA_USERNAME и TAIGA_PASSWORD")
|
||||
|
||||
client = TaigaClient()
|
||||
remote = client.list_projects()
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
for item in remote:
|
||||
slug = item.get("slug") or ""
|
||||
if not slug:
|
||||
continue
|
||||
existing = self.db.scalar(
|
||||
select(TaigaProject).where(TaigaProject.slug == slug)
|
||||
)
|
||||
if existing:
|
||||
existing.name = item.get("name", slug)
|
||||
existing.taiga_id = item["id"]
|
||||
existing.synced_at = now
|
||||
else:
|
||||
self.db.add(
|
||||
TaigaProject(
|
||||
taiga_id=item["id"],
|
||||
name=item.get("name", slug),
|
||||
slug=slug,
|
||||
synced_at=now,
|
||||
)
|
||||
)
|
||||
self.db.commit()
|
||||
return self.list_projects()
|
||||
|
||||
def list_projects(self) -> list[dict[str, Any]]:
|
||||
stmt = (
|
||||
select(TaigaProject, ProjectBinding)
|
||||
.outerjoin(ProjectBinding, ProjectBinding.taiga_slug == TaigaProject.slug)
|
||||
.order_by(TaigaProject.name)
|
||||
)
|
||||
rows = self.db.execute(stmt).all()
|
||||
result = []
|
||||
for taiga_proj, binding in rows:
|
||||
result.append(
|
||||
{
|
||||
"taiga_id": taiga_proj.taiga_id,
|
||||
"name": taiga_proj.name,
|
||||
"slug": taiga_proj.slug,
|
||||
"gitea_owner": binding.gitea_owner if binding else "",
|
||||
"gitea_repo": binding.gitea_repo if binding else "",
|
||||
"default_branch": binding.default_branch if binding else "main",
|
||||
"gitea_configured": bool(binding and binding.gitea_owner and binding.gitea_repo),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
def bind_gitea(
|
||||
self,
|
||||
taiga_slug: str,
|
||||
gitea_owner: str,
|
||||
gitea_repo: str,
|
||||
default_branch: str = "main",
|
||||
) -> dict[str, Any]:
|
||||
if not self.db.scalar(select(TaigaProject).where(TaigaProject.slug == taiga_slug)):
|
||||
raise ValueError(f"Проект Taiga '{taiga_slug}' не найден. Сначала sync-taiga.")
|
||||
|
||||
binding = self.db.scalar(
|
||||
select(ProjectBinding).where(ProjectBinding.taiga_slug == taiga_slug)
|
||||
)
|
||||
if binding:
|
||||
binding.gitea_owner = gitea_owner
|
||||
binding.gitea_repo = gitea_repo
|
||||
binding.default_branch = default_branch
|
||||
else:
|
||||
binding = ProjectBinding(
|
||||
taiga_slug=taiga_slug,
|
||||
gitea_owner=gitea_owner,
|
||||
gitea_repo=gitea_repo,
|
||||
default_branch=default_branch,
|
||||
)
|
||||
self.db.add(binding)
|
||||
self.db.commit()
|
||||
|
||||
for proj in self.list_projects():
|
||||
if proj["slug"] == taiga_slug:
|
||||
return proj
|
||||
raise ValueError("Binding failed")
|
||||
|
||||
def _resolve_project(self, slug: str | None) -> tuple[TaigaProject, ProjectBinding | None]:
|
||||
projects = self.db.scalars(select(TaigaProject).order_by(TaigaProject.name)).all()
|
||||
if not projects:
|
||||
raise ValueError("Нет проектов Taiga. Вызовите sync_taiga_projects.")
|
||||
|
||||
taiga_proj: TaigaProject | None = None
|
||||
if slug:
|
||||
taiga_proj = self.db.scalar(
|
||||
select(TaigaProject).where(TaigaProject.slug == slug)
|
||||
)
|
||||
if not taiga_proj:
|
||||
raise ValueError(f"Проект '{slug}' не найден")
|
||||
else:
|
||||
taiga_proj = projects[0]
|
||||
|
||||
binding = self.db.scalar(
|
||||
select(ProjectBinding).where(ProjectBinding.taiga_slug == taiga_proj.slug)
|
||||
)
|
||||
return taiga_proj, binding
|
||||
|
||||
async def create_work_item(
|
||||
self, raw_text: str, project_slug: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
if not self.settings.taiga_configured:
|
||||
raise ValueError("Taiga не настроена")
|
||||
|
||||
project_list = self.list_projects()
|
||||
if not project_list:
|
||||
self.sync_taiga_projects()
|
||||
project_list = self.list_projects()
|
||||
|
||||
structured = await structure_work_item(raw_text, project_list)
|
||||
slug = project_slug or structured.get("project_slug")
|
||||
taiga_proj, binding = self._resolve_project(slug)
|
||||
|
||||
if binding and not (binding.gitea_owner and binding.gitea_repo):
|
||||
binding = None
|
||||
|
||||
taiga = TaigaClient()
|
||||
title = (structured.get("title") or raw_text).strip()[:500]
|
||||
description = format_story_description(structured, raw_text)
|
||||
tags = structured.get("tags") or []
|
||||
issue_type = structured.get("issue_type", "feature")
|
||||
if issue_type == "bug" and "bug" not in [t.lower() for t in tags]:
|
||||
tags.append("bug")
|
||||
|
||||
story = taiga.create_userstory(
|
||||
taiga_proj.taiga_id,
|
||||
title,
|
||||
description,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
subtasks = []
|
||||
for child in structured.get("children") or []:
|
||||
if isinstance(child, dict):
|
||||
subtasks.append(
|
||||
taiga.create_task(
|
||||
taiga_proj.taiga_id,
|
||||
story["id"],
|
||||
child.get("title", "Подзадача"),
|
||||
child.get("description", ""),
|
||||
)
|
||||
)
|
||||
|
||||
branch = f"feature/{story['ref']}-{slugify_branch(title)}"
|
||||
gitea_issue_number = None
|
||||
gitea_url = ""
|
||||
|
||||
if binding and self.settings.gitea_configured:
|
||||
gitea = GiteaClient()
|
||||
gitea_body = format_gitea_body(
|
||||
structured,
|
||||
raw_text,
|
||||
story["ref"],
|
||||
taiga.story_url(taiga_proj.taiga_id, story["ref"]),
|
||||
branch,
|
||||
)
|
||||
if issue_type:
|
||||
gitea_body = f"**Тип:** {issue_type}\n\n{gitea_body}"
|
||||
issue = gitea.create_issue(
|
||||
binding.gitea_owner,
|
||||
binding.gitea_repo,
|
||||
title,
|
||||
gitea_body,
|
||||
)
|
||||
gitea_issue_number = issue["number"]
|
||||
gitea_url = gitea.issue_url(
|
||||
binding.gitea_owner, binding.gitea_repo, gitea_issue_number
|
||||
)
|
||||
|
||||
work_item = WorkItem(
|
||||
taiga_slug=taiga_proj.slug,
|
||||
taiga_project_id=taiga_proj.taiga_id,
|
||||
taiga_story_id=story["id"],
|
||||
taiga_story_ref=story["ref"],
|
||||
gitea_owner=binding.gitea_owner if binding else "",
|
||||
gitea_repo=binding.gitea_repo if binding else "",
|
||||
gitea_issue_number=gitea_issue_number,
|
||||
suggested_branch=branch,
|
||||
raw_text=raw_text,
|
||||
title=title,
|
||||
status="open",
|
||||
)
|
||||
self.db.add(work_item)
|
||||
self.db.commit()
|
||||
self.db.refresh(work_item)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"work_item_id": work_item.id,
|
||||
"taiga": {
|
||||
"ref": story["ref"],
|
||||
"id": story["id"],
|
||||
"subject": story["subject"],
|
||||
"url": taiga.story_url(taiga_proj.taiga_id, story["ref"]),
|
||||
},
|
||||
"gitea": {
|
||||
"number": gitea_issue_number,
|
||||
"url": gitea_url,
|
||||
},
|
||||
"branch": branch,
|
||||
"issue_type": issue_type,
|
||||
"subtasks": [{"ref": t.get("ref"), "subject": t.get("subject")} for t in subtasks],
|
||||
"questions": structured.get("questions") or [],
|
||||
"project_slug": taiga_proj.slug,
|
||||
}
|
||||
|
||||
def process_push(
|
||||
self, owner: str, repo: str, commits: list[dict[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
if not self.settings.taiga_configured:
|
||||
return []
|
||||
|
||||
taiga = TaigaClient()
|
||||
gitea = GiteaClient() if self.settings.gitea_configured else None
|
||||
results: list[dict[str, Any]] = []
|
||||
|
||||
for commit in commits:
|
||||
message = commit.get("message", "")
|
||||
parsed = parse_commit_message(message)
|
||||
sha = commit.get("id", "")[:8]
|
||||
|
||||
gitea_refs = set(parsed["gitea"])
|
||||
taiga_story_refs = set(parsed["taiga_story"])
|
||||
taiga_task_refs = set(parsed["taiga_task"])
|
||||
|
||||
linked_items = self.db.scalars(
|
||||
select(WorkItem).where(
|
||||
WorkItem.gitea_owner == owner,
|
||||
WorkItem.gitea_repo == repo,
|
||||
WorkItem.status == "open",
|
||||
)
|
||||
).all()
|
||||
|
||||
for item in linked_items:
|
||||
if item.gitea_issue_number and item.gitea_issue_number in gitea_refs:
|
||||
taiga_story_refs.add(item.taiga_story_ref)
|
||||
if item.taiga_story_ref in taiga_story_refs and item.gitea_issue_number:
|
||||
gitea_refs.add(item.gitea_issue_number)
|
||||
|
||||
for gitea_num in gitea_refs:
|
||||
if gitea:
|
||||
try:
|
||||
gitea.close_issue(owner, repo, gitea_num)
|
||||
except Exception as exc:
|
||||
results.append({"error": f"gitea #{gitea_num}: {exc}"})
|
||||
continue
|
||||
|
||||
for item in linked_items:
|
||||
if item.gitea_issue_number == gitea_num:
|
||||
try:
|
||||
self._close_work_item(item, taiga)
|
||||
results.append(
|
||||
{
|
||||
"commit": sha,
|
||||
"closed": f"gitea #{gitea_num}, taiga #{item.taiga_story_ref}",
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
results.append(
|
||||
{"error": f"work item {item.id} (gitea #{gitea_num}): {exc}"}
|
||||
)
|
||||
|
||||
for ref in taiga_story_refs:
|
||||
project_id = self._project_id_for_ref(owner, repo, ref, linked_items)
|
||||
if not project_id:
|
||||
continue
|
||||
story = taiga.get_by_ref(project_id, ref, kind="userstory")
|
||||
if story and not story.get("is_closed"):
|
||||
try:
|
||||
taiga.close_userstory(story["id"], project_id)
|
||||
results.append({"commit": sha, "closed": f"taiga #{ref}"})
|
||||
except Exception as exc:
|
||||
results.append({"error": f"taiga #{ref}: {exc}"})
|
||||
for item in linked_items:
|
||||
if item.taiga_story_ref == ref and item.status != "closed":
|
||||
try:
|
||||
self._close_work_item(item, taiga, close_gitea=bool(gitea))
|
||||
except Exception as exc:
|
||||
results.append(
|
||||
{"error": f"work item {item.id} (taiga #{ref}): {exc}"}
|
||||
)
|
||||
|
||||
for ref in taiga_task_refs:
|
||||
binding = self.db.scalar(
|
||||
select(ProjectBinding).where(
|
||||
ProjectBinding.gitea_owner == owner,
|
||||
ProjectBinding.gitea_repo == repo,
|
||||
)
|
||||
)
|
||||
if not binding:
|
||||
continue
|
||||
taiga_proj = self.db.scalar(
|
||||
select(TaigaProject).where(TaigaProject.slug == binding.taiga_slug)
|
||||
)
|
||||
if not taiga_proj:
|
||||
continue
|
||||
task = taiga.get_by_ref(taiga_proj.taiga_id, ref, kind="task")
|
||||
if task and not task.get("is_closed"):
|
||||
try:
|
||||
taiga.close_task(task["id"], taiga_proj.taiga_id)
|
||||
results.append({"commit": sha, "closed": f"taiga task #{ref}"})
|
||||
except Exception as exc:
|
||||
results.append({"error": f"taiga task #{ref}: {exc}"})
|
||||
|
||||
self.db.commit()
|
||||
return results
|
||||
|
||||
def _project_id_for_ref(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
ref: int,
|
||||
items: list[WorkItem],
|
||||
) -> int | None:
|
||||
for item in items:
|
||||
if item.taiga_story_ref == ref:
|
||||
return item.taiga_project_id
|
||||
binding = self.db.scalar(
|
||||
select(ProjectBinding).where(
|
||||
ProjectBinding.gitea_owner == owner,
|
||||
ProjectBinding.gitea_repo == repo,
|
||||
)
|
||||
)
|
||||
if binding:
|
||||
taiga_proj = self.db.scalar(
|
||||
select(TaigaProject).where(TaigaProject.slug == binding.taiga_slug)
|
||||
)
|
||||
return taiga_proj.taiga_id if taiga_proj else None
|
||||
return None
|
||||
|
||||
def _close_work_item(
|
||||
self,
|
||||
item: WorkItem,
|
||||
taiga: TaigaClient,
|
||||
*,
|
||||
close_gitea: bool = True,
|
||||
) -> None:
|
||||
if item.status == "closed":
|
||||
return
|
||||
story = taiga.get_by_ref(item.taiga_project_id, item.taiga_story_ref, kind="userstory")
|
||||
if story:
|
||||
taiga.close_userstory(story["id"], item.taiga_project_id)
|
||||
if (
|
||||
close_gitea
|
||||
and item.gitea_issue_number
|
||||
and self.settings.gitea_configured
|
||||
):
|
||||
GiteaClient().close_issue(
|
||||
item.gitea_owner, item.gitea_repo, item.gitea_issue_number
|
||||
)
|
||||
item.status = "closed"
|
||||
item.closed_at = datetime.now(timezone.utc)
|
||||
|
||||
def list_taiga_open_tasks(
|
||||
self,
|
||||
project_slug: str | None = None,
|
||||
limit: int = 20,
|
||||
) -> dict[str, Any]:
|
||||
if not self.settings.taiga_configured:
|
||||
raise ValueError("Taiga не настроена")
|
||||
|
||||
projects = self.list_projects()
|
||||
if not projects:
|
||||
projects = self.sync_taiga_projects()
|
||||
|
||||
if project_slug:
|
||||
projects = [p for p in projects if p["slug"] == project_slug]
|
||||
if not projects:
|
||||
raise ValueError(
|
||||
f"Проект '{project_slug}' не найден. Вызови sync_taiga_projects."
|
||||
)
|
||||
|
||||
client = TaigaClient()
|
||||
blocks: list[dict[str, Any]] = []
|
||||
|
||||
for proj in projects:
|
||||
stories = client.list_open_userstories(proj["taiga_id"], limit=limit)
|
||||
tasks = client.list_open_tasks(proj["taiga_id"], limit=limit)
|
||||
blocks.append(
|
||||
{
|
||||
"slug": proj["slug"],
|
||||
"name": proj["name"],
|
||||
"taiga_id": proj["taiga_id"],
|
||||
"stories": [
|
||||
{
|
||||
"ref": s.get("ref"),
|
||||
"subject": s.get("subject", ""),
|
||||
"url": client.story_url(proj["taiga_id"], s.get("ref", 0)),
|
||||
}
|
||||
for s in stories
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"ref": t.get("ref"),
|
||||
"subject": t.get("subject", ""),
|
||||
"user_story": t.get("user_story"),
|
||||
}
|
||||
for t in tasks
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
total_stories = sum(len(b["stories"]) for b in blocks)
|
||||
total_tasks = sum(len(b["tasks"]) for b in blocks)
|
||||
return {
|
||||
"projects": blocks,
|
||||
"total_stories": total_stories,
|
||||
"total_tasks": total_tasks,
|
||||
}
|
||||
|
||||
def list_work_items(self, limit: int = 30, status: str | None = None) -> list[dict[str, Any]]:
|
||||
stmt = select(WorkItem).order_by(WorkItem.created_at.desc()).limit(limit)
|
||||
if status:
|
||||
stmt = stmt.where(WorkItem.status == status)
|
||||
items = self.db.scalars(stmt).all()
|
||||
settings = get_settings()
|
||||
return [
|
||||
{
|
||||
"id": i.id,
|
||||
"title": i.title,
|
||||
"status": i.status,
|
||||
"taiga_slug": i.taiga_slug,
|
||||
"taiga_ref": i.taiga_story_ref,
|
||||
"gitea_issue": i.gitea_issue_number,
|
||||
"branch": i.suggested_branch,
|
||||
"taiga_url": f"{settings.taiga_public_url}/project/0/{i.taiga_project_id}/us/{i.taiga_story_ref}",
|
||||
"gitea_url": (
|
||||
f"{settings.gitea_public_url}/{i.gitea_owner}/{i.gitea_repo}/issues/{i.gitea_issue_number}"
|
||||
if i.gitea_issue_number
|
||||
else ""
|
||||
),
|
||||
"created_at": i.created_at.isoformat() if i.created_at else None,
|
||||
}
|
||||
for i in items
|
||||
]
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import get_settings
|
||||
from app.db.models import ProjectBinding, TaigaProject, WorkItem
|
||||
from app.integrations.gitea import GiteaClient
|
||||
from app.integrations.taiga import TaigaClient
|
||||
from app.projects.commit_parser import parse_commit_message
|
||||
from app.projects.structuring import (
|
||||
format_gitea_body,
|
||||
format_story_description,
|
||||
slugify_branch,
|
||||
structure_work_item,
|
||||
)
|
||||
|
||||
|
||||
class ProjectService:
|
||||
def __init__(self, db: Session, user_id: int):
|
||||
self.db = db
|
||||
self.user_id = user_id
|
||||
self.settings = get_settings()
|
||||
|
||||
def sync_taiga_projects(self) -> list[dict[str, Any]]:
|
||||
if not self.settings.taiga_configured:
|
||||
raise ValueError("Taiga не настроена: задайте TAIGA_USERNAME и TAIGA_PASSWORD")
|
||||
|
||||
client = TaigaClient()
|
||||
remote = client.list_projects()
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
for item in remote:
|
||||
slug = item.get("slug") or ""
|
||||
if not slug:
|
||||
continue
|
||||
existing = self.db.scalar(
|
||||
select(TaigaProject).where(TaigaProject.slug == slug)
|
||||
)
|
||||
if existing:
|
||||
existing.name = item.get("name", slug)
|
||||
existing.taiga_id = item["id"]
|
||||
existing.synced_at = now
|
||||
else:
|
||||
self.db.add(
|
||||
TaigaProject(
|
||||
taiga_id=item["id"],
|
||||
name=item.get("name", slug),
|
||||
slug=slug,
|
||||
synced_at=now,
|
||||
)
|
||||
)
|
||||
self.db.commit()
|
||||
return self.list_projects()
|
||||
|
||||
def list_projects(self) -> list[dict[str, Any]]:
|
||||
stmt = (
|
||||
select(TaigaProject, ProjectBinding)
|
||||
.outerjoin(
|
||||
ProjectBinding,
|
||||
(ProjectBinding.taiga_slug == TaigaProject.slug)
|
||||
& (ProjectBinding.user_id == self.user_id),
|
||||
)
|
||||
.order_by(TaigaProject.name)
|
||||
)
|
||||
rows = self.db.execute(stmt).all()
|
||||
result = []
|
||||
for taiga_proj, binding in rows:
|
||||
result.append(
|
||||
{
|
||||
"taiga_id": taiga_proj.taiga_id,
|
||||
"name": taiga_proj.name,
|
||||
"slug": taiga_proj.slug,
|
||||
"gitea_owner": binding.gitea_owner if binding else "",
|
||||
"gitea_repo": binding.gitea_repo if binding else "",
|
||||
"default_branch": binding.default_branch if binding else "main",
|
||||
"gitea_configured": bool(binding and binding.gitea_owner and binding.gitea_repo),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
def bind_gitea(
|
||||
self,
|
||||
taiga_slug: str,
|
||||
gitea_owner: str,
|
||||
gitea_repo: str,
|
||||
default_branch: str = "main",
|
||||
) -> dict[str, Any]:
|
||||
if not self.db.scalar(select(TaigaProject).where(TaigaProject.slug == taiga_slug)):
|
||||
raise ValueError(f"Проект Taiga '{taiga_slug}' не найден. Сначала sync-taiga.")
|
||||
|
||||
binding = self.db.scalar(
|
||||
select(ProjectBinding).where(ProjectBinding.user_id == self.user_id, ProjectBinding.taiga_slug == taiga_slug)
|
||||
)
|
||||
if binding:
|
||||
binding.gitea_owner = gitea_owner
|
||||
binding.gitea_repo = gitea_repo
|
||||
binding.default_branch = default_branch
|
||||
else:
|
||||
binding = ProjectBinding(
|
||||
user_id=self.user_id,
|
||||
taiga_slug=taiga_slug,
|
||||
gitea_owner=gitea_owner,
|
||||
gitea_repo=gitea_repo,
|
||||
default_branch=default_branch,
|
||||
)
|
||||
self.db.add(binding)
|
||||
self.db.commit()
|
||||
|
||||
for proj in self.list_projects():
|
||||
if proj["slug"] == taiga_slug:
|
||||
return proj
|
||||
raise ValueError("Binding failed")
|
||||
|
||||
def _resolve_project(self, slug: str | None) -> tuple[TaigaProject, ProjectBinding | None]:
|
||||
projects = self.db.scalars(select(TaigaProject).order_by(TaigaProject.name)).all()
|
||||
if not projects:
|
||||
raise ValueError("Нет проектов Taiga. Вызовите sync_taiga_projects.")
|
||||
|
||||
taiga_proj: TaigaProject | None = None
|
||||
if slug:
|
||||
taiga_proj = self.db.scalar(
|
||||
select(TaigaProject).where(TaigaProject.slug == slug)
|
||||
)
|
||||
if not taiga_proj:
|
||||
raise ValueError(f"Проект '{slug}' не найден")
|
||||
else:
|
||||
taiga_proj = projects[0]
|
||||
|
||||
binding = self.db.scalar(
|
||||
select(ProjectBinding).where(ProjectBinding.user_id == self.user_id, ProjectBinding.taiga_slug == taiga_proj.slug)
|
||||
)
|
||||
return taiga_proj, binding
|
||||
|
||||
async def create_work_item(
|
||||
self, raw_text: str, project_slug: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
if not self.settings.taiga_configured:
|
||||
raise ValueError("Taiga не настроена")
|
||||
|
||||
project_list = self.list_projects()
|
||||
if not project_list:
|
||||
self.sync_taiga_projects()
|
||||
project_list = self.list_projects()
|
||||
|
||||
structured = await structure_work_item(raw_text, project_list)
|
||||
slug = project_slug or structured.get("project_slug")
|
||||
taiga_proj, binding = self._resolve_project(slug)
|
||||
|
||||
if binding and not (binding.gitea_owner and binding.gitea_repo):
|
||||
binding = None
|
||||
|
||||
taiga = TaigaClient()
|
||||
title = (structured.get("title") or raw_text).strip()[:500]
|
||||
description = format_story_description(structured, raw_text)
|
||||
tags = structured.get("tags") or []
|
||||
issue_type = structured.get("issue_type", "feature")
|
||||
if issue_type == "bug" and "bug" not in [t.lower() for t in tags]:
|
||||
tags.append("bug")
|
||||
|
||||
story = taiga.create_userstory(
|
||||
taiga_proj.taiga_id,
|
||||
title,
|
||||
description,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
subtasks = []
|
||||
for child in structured.get("children") or []:
|
||||
if isinstance(child, dict):
|
||||
subtasks.append(
|
||||
taiga.create_task(
|
||||
taiga_proj.taiga_id,
|
||||
story["id"],
|
||||
child.get("title", "Подзадача"),
|
||||
child.get("description", ""),
|
||||
)
|
||||
)
|
||||
|
||||
branch = f"feature/{story['ref']}-{slugify_branch(title)}"
|
||||
gitea_issue_number = None
|
||||
gitea_url = ""
|
||||
|
||||
if binding and self.settings.gitea_configured:
|
||||
gitea = GiteaClient()
|
||||
gitea_body = format_gitea_body(
|
||||
structured,
|
||||
raw_text,
|
||||
story["ref"],
|
||||
taiga.story_url(taiga_proj.taiga_id, story["ref"]),
|
||||
branch,
|
||||
)
|
||||
if issue_type:
|
||||
gitea_body = f"**Тип:** {issue_type}\n\n{gitea_body}"
|
||||
issue = gitea.create_issue(
|
||||
binding.gitea_owner,
|
||||
binding.gitea_repo,
|
||||
title,
|
||||
gitea_body,
|
||||
)
|
||||
gitea_issue_number = issue["number"]
|
||||
gitea_url = gitea.issue_url(
|
||||
binding.gitea_owner, binding.gitea_repo, gitea_issue_number
|
||||
)
|
||||
|
||||
work_item = WorkItem(
|
||||
user_id=self.user_id,
|
||||
taiga_slug=taiga_proj.slug,
|
||||
taiga_project_id=taiga_proj.taiga_id,
|
||||
taiga_story_id=story["id"],
|
||||
taiga_story_ref=story["ref"],
|
||||
gitea_owner=binding.gitea_owner if binding else "",
|
||||
gitea_repo=binding.gitea_repo if binding else "",
|
||||
gitea_issue_number=gitea_issue_number,
|
||||
suggested_branch=branch,
|
||||
raw_text=raw_text,
|
||||
title=title,
|
||||
status="open",
|
||||
)
|
||||
self.db.add(work_item)
|
||||
self.db.commit()
|
||||
self.db.refresh(work_item)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"work_item_id": work_item.id,
|
||||
"taiga": {
|
||||
"ref": story["ref"],
|
||||
"id": story["id"],
|
||||
"subject": story["subject"],
|
||||
"url": taiga.story_url(taiga_proj.taiga_id, story["ref"]),
|
||||
},
|
||||
"gitea": {
|
||||
"number": gitea_issue_number,
|
||||
"url": gitea_url,
|
||||
},
|
||||
"branch": branch,
|
||||
"issue_type": issue_type,
|
||||
"subtasks": [{"ref": t.get("ref"), "subject": t.get("subject")} for t in subtasks],
|
||||
"questions": structured.get("questions") or [],
|
||||
"project_slug": taiga_proj.slug,
|
||||
}
|
||||
|
||||
def process_push(
|
||||
self, owner: str, repo: str, commits: list[dict[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
if not self.settings.taiga_configured:
|
||||
return []
|
||||
|
||||
taiga = TaigaClient()
|
||||
gitea = GiteaClient() if self.settings.gitea_configured else None
|
||||
results: list[dict[str, Any]] = []
|
||||
|
||||
for commit in commits:
|
||||
message = commit.get("message", "")
|
||||
parsed = parse_commit_message(message)
|
||||
sha = commit.get("id", "")[:8]
|
||||
|
||||
gitea_refs = set(parsed["gitea"])
|
||||
taiga_story_refs = set(parsed["taiga_story"])
|
||||
taiga_task_refs = set(parsed["taiga_task"])
|
||||
|
||||
linked_items = self.db.scalars(
|
||||
select(WorkItem).where(
|
||||
WorkItem.user_id == self.user_id,
|
||||
WorkItem.gitea_owner == owner,
|
||||
WorkItem.gitea_repo == repo,
|
||||
WorkItem.status == "open",
|
||||
)
|
||||
).all()
|
||||
|
||||
for item in linked_items:
|
||||
if item.gitea_issue_number and item.gitea_issue_number in gitea_refs:
|
||||
taiga_story_refs.add(item.taiga_story_ref)
|
||||
if item.taiga_story_ref in taiga_story_refs and item.gitea_issue_number:
|
||||
gitea_refs.add(item.gitea_issue_number)
|
||||
|
||||
for gitea_num in gitea_refs:
|
||||
if gitea:
|
||||
try:
|
||||
gitea.close_issue(owner, repo, gitea_num)
|
||||
except Exception as exc:
|
||||
results.append({"error": f"gitea #{gitea_num}: {exc}"})
|
||||
continue
|
||||
|
||||
for item in linked_items:
|
||||
if item.gitea_issue_number == gitea_num:
|
||||
try:
|
||||
self._close_work_item(item, taiga)
|
||||
results.append(
|
||||
{
|
||||
"commit": sha,
|
||||
"closed": f"gitea #{gitea_num}, taiga #{item.taiga_story_ref}",
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
results.append(
|
||||
{"error": f"work item {item.id} (gitea #{gitea_num}): {exc}"}
|
||||
)
|
||||
|
||||
for ref in taiga_story_refs:
|
||||
project_id = self._project_id_for_ref(owner, repo, ref, linked_items)
|
||||
if not project_id:
|
||||
continue
|
||||
story = taiga.get_by_ref(project_id, ref, kind="userstory")
|
||||
if story and not story.get("is_closed"):
|
||||
try:
|
||||
taiga.close_userstory(story["id"], project_id)
|
||||
results.append({"commit": sha, "closed": f"taiga #{ref}"})
|
||||
except Exception as exc:
|
||||
results.append({"error": f"taiga #{ref}: {exc}"})
|
||||
for item in linked_items:
|
||||
if item.taiga_story_ref == ref and item.status != "closed":
|
||||
try:
|
||||
self._close_work_item(item, taiga, close_gitea=bool(gitea))
|
||||
except Exception as exc:
|
||||
results.append(
|
||||
{"error": f"work item {item.id} (taiga #{ref}): {exc}"}
|
||||
)
|
||||
|
||||
for ref in taiga_task_refs:
|
||||
binding = self.db.scalar(
|
||||
select(ProjectBinding).where(
|
||||
ProjectBinding.user_id == self.user_id,
|
||||
ProjectBinding.gitea_owner == owner,
|
||||
ProjectBinding.gitea_repo == repo,
|
||||
)
|
||||
)
|
||||
if not binding:
|
||||
continue
|
||||
taiga_proj = self.db.scalar(
|
||||
select(TaigaProject).where(TaigaProject.slug == binding.taiga_slug)
|
||||
)
|
||||
if not taiga_proj:
|
||||
continue
|
||||
task = taiga.get_by_ref(taiga_proj.taiga_id, ref, kind="task")
|
||||
if task and not task.get("is_closed"):
|
||||
try:
|
||||
taiga.close_task(task["id"], taiga_proj.taiga_id)
|
||||
results.append({"commit": sha, "closed": f"taiga task #{ref}"})
|
||||
except Exception as exc:
|
||||
results.append({"error": f"taiga task #{ref}: {exc}"})
|
||||
|
||||
self.db.commit()
|
||||
return results
|
||||
|
||||
def _project_id_for_ref(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
ref: int,
|
||||
items: list[WorkItem],
|
||||
) -> int | None:
|
||||
for item in items:
|
||||
if item.taiga_story_ref == ref:
|
||||
return item.taiga_project_id
|
||||
binding = self.db.scalar(
|
||||
select(ProjectBinding).where(
|
||||
ProjectBinding.user_id == self.user_id,
|
||||
ProjectBinding.gitea_owner == owner,
|
||||
ProjectBinding.gitea_repo == repo,
|
||||
)
|
||||
)
|
||||
if binding:
|
||||
taiga_proj = self.db.scalar(
|
||||
select(TaigaProject).where(TaigaProject.slug == binding.taiga_slug)
|
||||
)
|
||||
return taiga_proj.taiga_id if taiga_proj else None
|
||||
return None
|
||||
|
||||
def _close_work_item(
|
||||
self,
|
||||
item: WorkItem,
|
||||
taiga: TaigaClient,
|
||||
*,
|
||||
close_gitea: bool = True,
|
||||
) -> None:
|
||||
if item.status == "closed":
|
||||
return
|
||||
story = taiga.get_by_ref(item.taiga_project_id, item.taiga_story_ref, kind="userstory")
|
||||
if story:
|
||||
taiga.close_userstory(story["id"], item.taiga_project_id)
|
||||
if (
|
||||
close_gitea
|
||||
and item.gitea_issue_number
|
||||
and self.settings.gitea_configured
|
||||
):
|
||||
GiteaClient().close_issue(
|
||||
item.gitea_owner, item.gitea_repo, item.gitea_issue_number
|
||||
)
|
||||
item.status = "closed"
|
||||
item.closed_at = datetime.now(timezone.utc)
|
||||
|
||||
def list_taiga_open_tasks(
|
||||
self,
|
||||
project_slug: str | None = None,
|
||||
limit: int = 20,
|
||||
) -> dict[str, Any]:
|
||||
if not self.settings.taiga_configured:
|
||||
raise ValueError("Taiga не настроена")
|
||||
|
||||
projects = self.list_projects()
|
||||
if not projects:
|
||||
projects = self.sync_taiga_projects()
|
||||
|
||||
if project_slug:
|
||||
projects = [p for p in projects if p["slug"] == project_slug]
|
||||
if not projects:
|
||||
raise ValueError(
|
||||
f"Проект '{project_slug}' не найден. Вызови sync_taiga_projects."
|
||||
)
|
||||
|
||||
client = TaigaClient()
|
||||
blocks: list[dict[str, Any]] = []
|
||||
|
||||
for proj in projects:
|
||||
stories = client.list_open_userstories(proj["taiga_id"], limit=limit)
|
||||
tasks = client.list_open_tasks(proj["taiga_id"], limit=limit)
|
||||
blocks.append(
|
||||
{
|
||||
"slug": proj["slug"],
|
||||
"name": proj["name"],
|
||||
"taiga_id": proj["taiga_id"],
|
||||
"stories": [
|
||||
{
|
||||
"ref": s.get("ref"),
|
||||
"subject": s.get("subject", ""),
|
||||
"url": client.story_url(proj["taiga_id"], s.get("ref", 0)),
|
||||
}
|
||||
for s in stories
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"ref": t.get("ref"),
|
||||
"subject": t.get("subject", ""),
|
||||
"user_story": t.get("user_story"),
|
||||
}
|
||||
for t in tasks
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
total_stories = sum(len(b["stories"]) for b in blocks)
|
||||
total_tasks = sum(len(b["tasks"]) for b in blocks)
|
||||
return {
|
||||
"projects": blocks,
|
||||
"total_stories": total_stories,
|
||||
"total_tasks": total_tasks,
|
||||
}
|
||||
|
||||
def list_work_items(self, limit: int = 30, status: str | None = None) -> list[dict[str, Any]]:
|
||||
stmt = select(WorkItem).where(WorkItem.user_id == self.user_id).order_by(WorkItem.created_at.desc()).limit(limit)
|
||||
if status:
|
||||
stmt = stmt.where(WorkItem.status == status)
|
||||
items = self.db.scalars(stmt).all()
|
||||
settings = get_settings()
|
||||
return [
|
||||
{
|
||||
"id": i.id,
|
||||
"title": i.title,
|
||||
"status": i.status,
|
||||
"taiga_slug": i.taiga_slug,
|
||||
"taiga_ref": i.taiga_story_ref,
|
||||
"gitea_issue": i.gitea_issue_number,
|
||||
"branch": i.suggested_branch,
|
||||
"taiga_url": f"{settings.taiga_public_url}/project/0/{i.taiga_project_id}/us/{i.taiga_story_ref}",
|
||||
"gitea_url": (
|
||||
f"{settings.gitea_public_url}/{i.gitea_owner}/{i.gitea_repo}/issues/{i.gitea_issue_number}"
|
||||
if i.gitea_issue_number
|
||||
else ""
|
||||
),
|
||||
"created_at": i.created_at.isoformat() if i.created_at else None,
|
||||
}
|
||||
for i in items
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user