""" =============================================================================== FILE: services/tasks/exporter.py PROJECT: SCUD Orion AI (Unified Architecture) MODULE: services / tasks ROLE: Экспорт бэклога задач в форматированный Markdown файл (ROADMAP). AI-CONTEXT-ANCHORS: - ANCHOR[TASK_EXPORT_MARKDOWN]: Построение структуры Markdown с чекбоксами. =============================================================================== """ import os import uuid import logging from datetime import datetime from typing import Dict, Any, Optional, List from config import OUTPUT_DIR from .repository import repo_get_tasks logger = logging.getLogger("TASK_EXPORTER") # ANCHOR[TASK_EXPORT_MARKDOWN] def export_tasks_to_markdown(user_id: int, filename: Optional[str] = None, status_filter: Optional[str] = None) -> Dict[str, Any]: """Генерирует Markdown-отчет задач с сохранением в output/web/tasks_export/{uuid}/.""" tasks = repo_get_tasks(user_id) if not tasks: return {"status": "error", "message": "Список задач пуст, экспорт отменен"} # 1. Фильтрация задач по статусу if status_filter and status_filter.upper() != "ALL": tgt = status_filter.upper() if tgt in ["COMPLETED", "DONE", "ВЫПОЛНЕННЫЕ"]: tasks = [t for t in tasks if str(t.get("status", "")).upper() in ["COMPLETED", "DONE"]] elif tgt in ["IN_PROGRESS", "PROGRESS", "В РАБОТЕ"]: tasks = [t for t in tasks if str(t.get("status", "")).upper() in ["IN_PROGRESS", "PROGRESS"]] elif tgt in ["BACKLOG", "PLANNED", "В ПЛАНАХ"]: tasks = [t for t in tasks if str(t.get("status", "")).upper() in ["BACKLOG", "PLANNED"]] if not tasks: return {"status": "error", "message": f"Нет задач с фильтром '{status_filter}' для экспорта"} target_filename = filename.strip() if (filename and filename.strip()) else "ROADMAP.md" if not target_filename.endswith(".md"): target_filename = f"{target_filename}.md" now_str = datetime.now().strftime("%Y-%m-%d %H:%M") # 2. Группировка по модулям modules: Dict[str, List[Dict[str, Any]]] = {} for t in tasks: mod = t.get("module") or "general" modules.setdefault(mod, []).append(t) md_lines = [ "# 🗺️ Дорожная карта задач проекта (ROADMAP)\n", f"> **Сформировано:** {now_str} | **Всего задач:** {len(tasks)}\n", "---\n" ] for mod_name, mod_tasks in sorted(modules.items()): md_lines.append(f"## Модуль `{mod_name}`\n") for t in sorted(mod_tasks, key=lambda x: x.get("id", 0)): status = str(t.get("status", "BACKLOG")).upper() is_done = status in ["COMPLETED", "DONE"] is_progress = status in ["IN_PROGRESS", "PROGRESS"] check_box = "[x]" if is_done else "[ ]" t_id = t.get("id") title = t.get("title", "Без названия") prio = t.get("priority", "MEDIUM") due = f" *(срок: {t['due_date']})*" if t.get("due_date") else "" status_tag = " `[В РАБОТЕ]`" if is_progress else (" `[ЗАВЕРШЕНО]`" if is_done else "") md_lines.append(f"- {check_box} **#{t_id}** [{prio}]{status_tag} {title}{due}") md_lines.append("\n---\n") content = "\n".join(md_lines) # 3. Сохранение файла в изолированную сессионную папку tool_dir = os.path.join(OUTPUT_DIR, "web", "tasks_export") os.makedirs(tool_dir, exist_ok=True) session_token = uuid.uuid4().hex[:8] session_dir = os.path.join(tool_dir, session_token) os.makedirs(session_dir, exist_ok=True) filepath = os.path.join(session_dir, target_filename) with open(filepath, "w", encoding="utf-8") as f: f.write(content) return { "status": "success", "filename": target_filename, "filepath": filepath, "download_url": f"/api/v1/files/download/tasks_export/{session_token}/{target_filename}", "tasks_count": len(tasks), "message": f"Отчет успешно сформирован в файл `{target_filename}` (всего задач: {len(tasks)})." }