| """ |
| Database service layer for converting between database models and API models |
| """ |
|
|
| import time |
| import uuid |
| import logging |
| from typing import List, Optional, Dict, Any, Tuple |
| from sqlalchemy.ext.asyncio import AsyncSession |
|
|
| logger = logging.getLogger(__name__) |
|
|
| from .repositories import ( |
| ProjectRepository, TodoBoardRepository, TodoStageRepository, |
| ProjectVersionRepository, SlideDataRepository, PPTTemplateRepository, GlobalMasterTemplateRepository |
| ) |
| from .models import Project as DBProject, TodoBoard as DBTodoBoard, TodoStage as DBTodoStage, PPTTemplate as DBPPTTemplate, GlobalMasterTemplate as DBGlobalMasterTemplate |
| from ..api.models import ( |
| PPTProject, TodoBoard, TodoStage, ProjectListResponse, |
| PPTGenerationRequest |
| ) |
| from ..auth.request_context import current_user_id, USER_SCOPE_ALL |
|
|
|
|
| class DatabaseService: |
| """Service for database operations with model conversion""" |
| |
| def __init__(self, session: AsyncSession): |
| self.session = session |
| self.project_repo = ProjectRepository(session) |
| self.todo_board_repo = TodoBoardRepository(session) |
| self.todo_stage_repo = TodoStageRepository(session) |
| self.version_repo = ProjectVersionRepository(session) |
| self.slide_repo = SlideDataRepository(session) |
|
|
| @staticmethod |
| def _normalize_progress(progress: Any) -> float: |
| try: |
| value = float(progress) |
| except (TypeError, ValueError): |
| return 0.0 |
| return max(0.0, min(100.0, value)) |
|
|
| @classmethod |
| def _calculate_overall_progress(cls, stages: List[Any]) -> float: |
| if not stages: |
| return 0.0 |
| return sum(cls._normalize_progress(getattr(stage, "progress", 0.0)) for stage in stages) / len(stages) |
|
|
| @staticmethod |
| def _extract_expected_slide_count(outline: Any) -> int: |
| if not isinstance(outline, dict): |
| return 0 |
| slides = outline.get("slides") |
| return len(slides) if isinstance(slides, list) else 0 |
| |
| def _convert_db_project_to_api(self, db_project: DBProject) -> PPTProject: |
| """Convert database project to API model""" |
| |
| todo_board = None |
| if db_project.todo_board: |
| stages = [ |
| TodoStage( |
| id=stage.stage_id, |
| name=stage.title, |
| description=stage.description, |
| status=stage.status, |
| progress=stage.progress, |
| subtasks=[], |
| result=stage.result or {}, |
| created_at=stage.created_at, |
| updated_at=stage.updated_at |
| ) |
| for stage in db_project.todo_board.stages |
| ] |
|
|
| todo_board = TodoBoard( |
| task_id=db_project.project_id, |
| title=db_project.title, |
| stages=stages, |
| current_stage_index=db_project.todo_board.current_stage_index, |
| overall_progress=db_project.todo_board.overall_progress, |
| created_at=db_project.todo_board.created_at, |
| updated_at=db_project.todo_board.updated_at |
| ) |
| |
| |
| versions = [] |
|
|
| slides_data = [] |
| if db_project.slides: |
| |
| slides_data = [] |
| for slide in sorted(db_project.slides, key=lambda x: x.slide_index): |
| slide_dict = { |
| "slide_id": slide.slide_id, |
| "title": slide.title, |
| "content_type": slide.content_type, |
| "html_content": slide.html_content, |
| "metadata": slide.slide_metadata or {}, |
| "is_user_edited": slide.is_user_edited, |
| "created_at": slide.created_at, |
| "updated_at": slide.updated_at, |
| "page_number": slide.slide_index + 1 |
| } |
| slides_data.append(slide_dict) |
| logger.debug(f"Loaded {len(slides_data)} slides from slide_data table for project {db_project.project_id}") |
| elif db_project.slides_data: |
| |
| slides_data = db_project.slides_data |
| logger.debug(f"Using slides_data from projects table for project {db_project.project_id}: {len(slides_data)} slides") |
|
|
| expected_slide_count = self._extract_expected_slide_count(db_project.outline) |
| actual_slide_count = len(slides_data) |
| has_confirmed_requirements = bool(db_project.confirmed_requirements) |
| has_outline = expected_slide_count > 0 |
| has_any_ppt_output = actual_slide_count > 0 or bool(str(db_project.slides_html or "").strip()) |
| has_complete_ppt = actual_slide_count > 0 and ( |
| expected_slide_count == 0 or actual_slide_count >= expected_slide_count |
| ) |
|
|
| if db_project.todo_board: |
| reconciled_stages = [] |
| for stage in db_project.todo_board.stages: |
| status = stage.status |
| progress = self._normalize_progress(stage.progress) |
|
|
| if stage.stage_id == "requirements_confirmation": |
| if has_confirmed_requirements: |
| status = "completed" |
| progress = 100.0 |
| elif status == "completed": |
| status = "pending" |
| progress = 0.0 |
| elif stage.stage_id == "outline_generation": |
| if has_outline: |
| status = "completed" |
| progress = 100.0 |
| elif status == "completed": |
| status = "pending" |
| progress = 0.0 |
| elif stage.stage_id == "ppt_creation": |
| if has_complete_ppt: |
| status = "completed" |
| progress = 100.0 |
| elif status == "completed": |
| if expected_slide_count > 0 and actual_slide_count > 0: |
| status = "running" |
| progress = min(99.0, (actual_slide_count / expected_slide_count) * 100) |
| elif has_any_ppt_output: |
| status = "running" |
| progress = max(progress, 1.0) |
| else: |
| status = "pending" |
| progress = 0.0 |
| elif ( |
| expected_slide_count > 0 |
| and actual_slide_count > 0 |
| and status in {"pending", "running"} |
| ): |
| status = "running" |
| progress = max(progress, min(99.0, (actual_slide_count / expected_slide_count) * 100)) |
|
|
| if status == "completed": |
| progress = 100.0 |
|
|
| reconciled_stages.append( |
| TodoStage( |
| id=stage.stage_id, |
| name=stage.title, |
| description=stage.description, |
| status=status, |
| progress=progress, |
| subtasks=[], |
| result=stage.result or {}, |
| created_at=stage.created_at, |
| updated_at=stage.updated_at |
| ) |
| ) |
|
|
| current_stage_index = len(reconciled_stages) - 1 if reconciled_stages else 0 |
| for i, stage in enumerate(reconciled_stages): |
| if stage.status != "completed": |
| current_stage_index = i |
| break |
|
|
| todo_board = TodoBoard( |
| task_id=db_project.project_id, |
| title=db_project.title, |
| stages=reconciled_stages, |
| current_stage_index=current_stage_index, |
| overall_progress=self._calculate_overall_progress(reconciled_stages), |
| created_at=db_project.todo_board.created_at, |
| updated_at=db_project.todo_board.updated_at |
| ) |
|
|
| project_status = db_project.status |
| if project_status != "archived": |
| if has_complete_ppt: |
| project_status = "completed" |
| elif has_confirmed_requirements or has_outline or has_any_ppt_output: |
| project_status = "in_progress" |
| else: |
| project_status = "draft" |
|
|
| return PPTProject( |
| project_id=db_project.project_id, |
| title=db_project.title, |
| scenario=db_project.scenario, |
| topic=db_project.topic, |
| requirements=db_project.requirements, |
| status=project_status, |
| outline=db_project.outline, |
| slides_html=db_project.slides_html, |
| slides_data=slides_data, |
| confirmed_requirements=db_project.confirmed_requirements, |
| project_metadata=db_project.project_metadata, |
| todo_board=todo_board, |
| version=db_project.version, |
| versions=versions, |
| created_at=db_project.created_at, |
| updated_at=db_project.updated_at |
| ) |
| |
| async def create_project(self, request: PPTGenerationRequest, user_id: Optional[int] = None) -> PPTProject: |
| """Create a new project with todo board. |
| |
| Note: user_id must come from authenticated context; do not trust request.user_id from clients. |
| """ |
| project_id = str(uuid.uuid4()) |
| |
| owner_id = user_id if user_id is not None else (current_user_id.get() or request.user_id) |
| if owner_id is None: |
| raise ValueError("user_id is required to create a project") |
| |
| |
| project_data = { |
| "project_id": project_id, |
| "user_id": owner_id, |
| "title": f"{request.topic} - {request.scenario}", |
| "scenario": request.scenario, |
| "topic": request.topic, |
| "requirements": request.requirements, |
| "status": "draft", |
| "project_metadata": { |
| "network_mode": request.network_mode, |
| "language": request.language, |
| "created_with_network_mode": request.network_mode |
| } |
| } |
| |
| db_project = await self.project_repo.create(project_data) |
|
|
| |
| |
| board_data = { |
| "project_id": project_id, |
| "current_stage_index": 0, |
| "overall_progress": 0.0 |
| } |
| |
| db_board = await self.todo_board_repo.create(board_data) |
| |
| |
| stages_data = [ |
| { |
| "todo_board_id": db_board.id, |
| "project_id": project_id, |
| "stage_id": "requirements_confirmation", |
| "stage_index": 0, |
| "title": "需求确认", |
| "description": "确认PPT主题、内容重点、技术亮点和目标受众", |
| "status": "pending" |
| }, |
| { |
| "todo_board_id": db_board.id, |
| "project_id": project_id, |
| "stage_id": "outline_generation", |
| "stage_index": 1, |
| "title": "大纲生成", |
| "description": "基于确认的需求生成PPT大纲结构", |
| "status": "pending" |
| }, |
| { |
| "todo_board_id": db_board.id, |
| "project_id": project_id, |
| "stage_id": "ppt_creation", |
| "stage_index": 2, |
| "title": "PPT生成", |
| "description": "根据大纲生成完整的PPT页面", |
| "status": "pending" |
| } |
| ] |
| |
| await self.todo_stage_repo.create_stages(stages_data) |
| |
| |
| complete_project = await self.project_repo.get_by_id(project_id, user_id=owner_id) |
| return self._convert_db_project_to_api(complete_project) |
| |
| async def get_project(self, project_id: str, user_id: Optional[int] = None) -> Optional[PPTProject]: |
| """Get project by ID. If user_id is provided, enforces ownership.""" |
| db_project = await self.project_repo.get_by_id(project_id, user_id=user_id) |
| if not db_project: |
| return None |
| return self._convert_db_project_to_api(db_project) |
| |
| async def list_projects(self, page: int = 1, page_size: int = 10, |
| status: Optional[str] = None, |
| user_id: Optional[int] = None) -> ProjectListResponse: |
| """List projects with pagination. If user_id is provided, filters by owner.""" |
| if status: |
| total_candidates = await self.project_repo.count_projects(user_id=user_id) |
| if total_candidates == 0: |
| return ProjectListResponse( |
| projects=[], |
| total=0, |
| page=page, |
| page_size=page_size |
| ) |
|
|
| db_projects = await self.project_repo.list_projects( |
| user_id=user_id, |
| page=1, |
| page_size=total_candidates, |
| status=None, |
| ) |
| all_projects = [self._convert_db_project_to_api(db_project) for db_project in db_projects] |
| filtered_projects = [project for project in all_projects if project.status == status] |
|
|
| total = len(filtered_projects) |
| offset = max(page - 1, 0) * page_size |
| projects = filtered_projects[offset:offset + page_size] |
| else: |
| db_projects = await self.project_repo.list_projects( |
| user_id=user_id, |
| page=page, |
| page_size=page_size, |
| status=None, |
| ) |
| total = await self.project_repo.count_projects(user_id=user_id, status=None) |
| projects = [self._convert_db_project_to_api(db_project) for db_project in db_projects] |
| |
| return ProjectListResponse( |
| projects=projects, |
| total=total, |
| page=page, |
| page_size=page_size |
| ) |
| |
| async def update_project_status(self, project_id: str, status: str, user_id: Optional[int] = None) -> bool: |
| """Update project status. If user_id is provided, enforces ownership.""" |
| result = await self.project_repo.update(project_id, {"status": status}, user_id=user_id) |
| return result is not None |
| |
| async def update_stage_status(self, project_id: str, stage_id: str, |
| status: str, progress: float = None, |
| result: Dict[str, Any] = None, |
| user_id: Optional[int] = None) -> bool: |
| """Update stage status. If user_id is provided, enforces project ownership.""" |
| effective_user_id = user_id |
| if effective_user_id == USER_SCOPE_ALL: |
| effective_user_id = None |
| if effective_user_id is None: |
| effective_user_id = current_user_id.get() |
| if effective_user_id is not None: |
| project = await self.project_repo.get_by_id(project_id, user_id=effective_user_id) |
| if not project: |
| return False |
| update_data = {"status": status} |
| if progress is not None: |
| update_data["progress"] = self._normalize_progress(progress) |
| elif status == "completed": |
| update_data["progress"] = 100.0 |
| if result is not None: |
| update_data["result"] = result |
|
|
| |
| success = await self.todo_stage_repo.update_stage_by_project_and_stage(project_id, stage_id, update_data) |
| |
| if success: |
| |
| todo_board = await self.todo_board_repo.get_by_project_id(project_id) |
| if todo_board: |
| |
| await self.session.refresh(todo_board) |
|
|
| total_stages = len(todo_board.stages) |
| overall_progress = self._calculate_overall_progress(todo_board.stages) |
|
|
| |
| current_stage_index = total_stages - 1 |
| for i, stage in enumerate(todo_board.stages): |
| if stage.status != "completed": |
| current_stage_index = i |
| break |
|
|
| |
| update_result = await self.todo_board_repo.update(project_id, { |
| "overall_progress": overall_progress, |
| "current_stage_index": current_stage_index |
| }) |
|
|
| if update_result: |
| logger.info(f"Updated TODO board progress: {overall_progress}%, current stage: {current_stage_index}") |
| else: |
| logger.error(f"Failed to update TODO board progress for project {project_id}") |
| |
| return success |
| |
| async def save_project_outline(self, project_id: str, outline: Dict[str, Any]) -> bool: |
| """Save project outline""" |
| try: |
| effective_user_id = current_user_id.get() |
| if effective_user_id is not None: |
| owned = await self.project_repo.get_by_id(project_id, user_id=effective_user_id) |
| if not owned: |
| return False |
|
|
| logger.info(f"Saving outline for project {project_id}") |
| logger.debug(f"Outline data: {outline}") |
|
|
| |
| if not outline: |
| logger.error("Outline data is empty or None") |
| return False |
|
|
| |
| update_data = { |
| "outline": outline, |
| "updated_at": time.time() |
| } |
|
|
| result = await self.project_repo.update(project_id, update_data) |
|
|
| if result: |
| logger.info(f"Successfully saved outline for project {project_id}") |
|
|
| |
| saved_project = await self.project_repo.get_by_id(project_id) |
| if saved_project and saved_project.outline: |
| logger.info(f"Verified outline saved: {len(saved_project.outline.get('slides', []))} slides") |
| return True |
| else: |
| logger.error(f"Outline verification failed for project {project_id}") |
| return False |
| else: |
| logger.error(f"Failed to update project {project_id} with outline") |
| return False |
|
|
| except Exception as e: |
| logger.error(f"Error saving project outline: {e}") |
| import traceback |
| traceback.print_exc() |
| return False |
| |
| async def save_project_slides(self, project_id: str, slides_html: str, |
| slides_data: List[Dict[str, Any]] = None) -> bool: |
| """Save project slides - 优化的批量更新方式""" |
| effective_user_id = current_user_id.get() |
| if effective_user_id is not None: |
| owned = await self.project_repo.get_by_id(project_id, user_id=effective_user_id) |
| if not owned: |
| return False |
|
|
| update_data = {"slides_html": slides_html} |
| if slides_data: |
| update_data["slides_data"] = slides_data |
|
|
| |
| existing_slides = await self.slide_repo.get_slides_by_project_id(project_id) |
| existing_count = len(existing_slides) |
| new_count = len(slides_data) |
|
|
| logger.info(f"🔄 开始批量更新幻灯片: 现有{existing_count}页, 新数据{new_count}页") |
|
|
| |
| slides_records = [] |
| for i, slide_data in enumerate(slides_data): |
| slide_record = { |
| "project_id": project_id, |
| "slide_index": i, |
| "slide_id": slide_data.get("slide_id", f"slide_{i}"), |
| "title": slide_data.get("title", f"Slide {i+1}"), |
| "content_type": slide_data.get("content_type", "content"), |
| "html_content": slide_data.get("html_content", ""), |
| "slide_metadata": slide_data.get("metadata", {}), |
| "is_user_edited": slide_data.get("is_user_edited", False) |
| } |
| slides_records.append(slide_record) |
|
|
| |
| try: |
| batch_success = await self.slide_repo.batch_upsert_slides(project_id, slides_records) |
| if batch_success: |
| logger.info(f"✅ 批量更新幻灯片成功: {new_count}页") |
| else: |
| logger.error(f"❌ 批量更新幻灯片失败") |
| return False |
| except Exception as e: |
| logger.error(f"❌ 批量更新幻灯片异常: {e}") |
| return False |
|
|
| result = await self.project_repo.update(project_id, update_data) |
| return result is not None |
|
|
| async def cleanup_excess_slides( |
| self, |
| project_id: str, |
| current_slide_count: int, |
| user_id: Optional[int] = None, |
| ) -> int: |
| """清理多余的幻灯片 - 删除索引 >= current_slide_count 的幻灯片""" |
| logger.info(f"🧹 开始清理项目 {project_id} 的多余幻灯片,保留前 {current_slide_count} 张") |
| effective_user_id = user_id |
| if effective_user_id == USER_SCOPE_ALL: |
| effective_user_id = None |
| if effective_user_id is None: |
| effective_user_id = current_user_id.get() |
| if effective_user_id is not None: |
| owned = await self.project_repo.get_by_id(project_id, user_id=effective_user_id) |
| if not owned: |
| return 0 |
|
|
| deleted_count = await self.slide_repo.delete_slides_after_index(project_id, current_slide_count) |
| logger.info(f"✅ 清理完成,删除了 {deleted_count} 张多余的幻灯片") |
| return deleted_count |
|
|
| async def replace_all_project_slides(self, project_id: str, slides_html: str, |
| slides_data: List[Dict[str, Any]] = None) -> bool: |
| """完全替换项目的所有幻灯片 - 用于重新生成PPT等场景""" |
| update_data = {"slides_html": slides_html} |
| if slides_data: |
| update_data["slides_data"] = slides_data |
|
|
| |
| logger.info(f"🔄 完全替换项目 {project_id} 的所有幻灯片") |
| effective_user_id = current_user_id.get() |
| if effective_user_id is not None: |
| owned = await self.project_repo.get_by_id(project_id, user_id=effective_user_id) |
| if not owned: |
| return False |
|
|
| await self.slide_repo.delete_slides_by_project_id(project_id) |
|
|
| slide_records = [] |
| for i, slide_data in enumerate(slides_data): |
| slide_records.append({ |
| "project_id": project_id, |
| "slide_index": i, |
| "slide_id": slide_data.get("slide_id", f"slide_{i}"), |
| "title": slide_data.get("title", f"Slide {i+1}"), |
| "content_type": slide_data.get("content_type", "content"), |
| "html_content": slide_data.get("html_content", ""), |
| "slide_metadata": slide_data.get("metadata", {}), |
| "is_user_edited": slide_data.get("is_user_edited", False) |
| }) |
|
|
| if slide_records: |
| await self.slide_repo.create_slides(slide_records) |
|
|
| result = await self.project_repo.update(project_id, update_data) |
| return result is not None |
|
|
| async def save_single_slide(self, project_id: str, slide_index: int, slide_data: Dict[str, Any], skip_if_user_edited: bool = False) -> bool: |
| """Save a single slide to database immediately with retry logic for SQLite locks |
| |
| Args: |
| skip_if_user_edited: If True, skip updating slides that have is_user_edited=True. |
| Generator should pass True, editor should pass False. |
| """ |
| import asyncio |
|
|
| effective_user_id = current_user_id.get() |
| if effective_user_id is not None: |
| owned = await self.project_repo.get_by_id(project_id, user_id=effective_user_id) |
| if not owned: |
| return False |
| |
| max_retries = 5 |
| base_delay = 0.1 |
| |
| for attempt in range(max_retries): |
| try: |
| logger.debug(f"🔄 数据库服务开始保存幻灯片: 项目ID={project_id}, 索引={slide_index}, 尝试={attempt + 1}") |
|
|
| |
| if not project_id: |
| raise ValueError("项目ID不能为空") |
| if slide_index < 0: |
| raise ValueError(f"幻灯片索引不能为负数: {slide_index}") |
| if not slide_data: |
| raise ValueError("幻灯片数据不能为空") |
|
|
| |
| slide_record = { |
| "project_id": project_id, |
| "slide_index": slide_index, |
| "slide_id": slide_data.get("slide_id", f"slide_{slide_index}"), |
| "title": slide_data.get("title", f"Slide {slide_index + 1}"), |
| "content_type": slide_data.get("content_type", "content"), |
| "html_content": slide_data.get("html_content", ""), |
| "slide_metadata": slide_data.get("metadata", {}), |
| "is_user_edited": slide_data.get("is_user_edited", False) |
| } |
|
|
| logger.debug(f"📊 准备保存的幻灯片记录: 标题='{slide_record['title']}', 跳过用户编辑={skip_if_user_edited}") |
|
|
| |
| result_slide = await self.slide_repo.upsert_slide(project_id, slide_index, slide_record, skip_if_user_edited=skip_if_user_edited) |
|
|
| if result_slide: |
| logger.debug(f"✅ 幻灯片保存成功: 项目ID={project_id}, 索引={slide_index}, 数据库ID={result_slide.id}") |
| return True |
| else: |
| logger.error(f"❌ 幻灯片保存失败: upsert_slide返回None") |
| return False |
| |
| except Exception as e: |
| error_str = str(e).lower() |
| |
| if "database is locked" in error_str or "locked" in error_str: |
| if attempt < max_retries - 1: |
| delay = base_delay * (2 ** attempt) |
| logger.warning(f"⏳ 数据库锁定,{delay:.2f}秒后重试... (尝试 {attempt + 1}/{max_retries})") |
| await asyncio.sleep(delay) |
| continue |
| |
| logger.error(f"❌ 保存单个幻灯片失败: 项目ID={project_id}, 索引={slide_index}, 错误={str(e)}") |
| import traceback |
| logger.error(f"❌ 错误堆栈: {traceback.format_exc()}") |
| return False |
| |
| logger.error(f"❌ 保存单个幻灯片失败: 重试次数用尽, 项目ID={project_id}, 索引={slide_index}") |
| return False |
|
|
| async def update_project(self, project_id: str, update_data: Dict[str, Any], user_id: Optional[int] = None) -> bool: |
| """Update project data. If user_id is provided, enforces ownership.""" |
| try: |
| result = await self.project_repo.update(project_id, update_data, user_id=user_id) |
| return result is not None |
| except Exception as e: |
| logger.error(f"Failed to update project {project_id}: {e}") |
| return False |
|
|
| async def update_slide_user_edited_status(self, project_id: str, slide_index: int, is_user_edited: bool = True) -> bool: |
| """Update the user edited status for a specific slide""" |
| try: |
| effective_user_id = current_user_id.get() |
| if effective_user_id is not None: |
| owned = await self.project_repo.get_by_id(project_id, user_id=effective_user_id) |
| if not owned: |
| return False |
|
|
| |
| await self.slide_repo.update_slide_user_edited_status(project_id, slide_index, is_user_edited) |
|
|
| |
| project = await self.project_repo.get_by_id(project_id) |
| if project and project.slides_data and slide_index < len(project.slides_data): |
| project.slides_data[slide_index]["is_user_edited"] = is_user_edited |
| await self.project_repo.update(project_id, {"slides_data": project.slides_data}) |
|
|
| return True |
| except Exception as e: |
| logger.error(f"Failed to update slide user edited status: {e}") |
| return False |
|
|
| async def save_project_version(self, project_id: str, version_data: Dict[str, Any], user_id: Optional[int] = None) -> bool: |
| """Save a project version. If user_id is provided, enforces ownership.""" |
| project = await self.project_repo.get_by_id(project_id, user_id=user_id) |
| if not project: |
| return False |
| |
| version_info = { |
| "project_id": project_id, |
| "version": project.version, |
| "timestamp": time.time(), |
| "data": version_data, |
| "description": f"Version {project.version} - {time.strftime('%Y-%m-%d %H:%M:%S')}" |
| } |
| |
| await self.version_repo.create(version_info) |
| await self.project_repo.update(project_id, {"version": project.version + 1}, user_id=user_id) |
| |
| return True |
|
|
| |
| async def create_template(self, template_data: Dict[str, Any]) -> DBPPTTemplate: |
| """Create a new PPT template""" |
| template_repo = PPTTemplateRepository(self.session) |
| return await template_repo.create_template(template_data) |
|
|
| async def get_template_by_id(self, template_id: int) -> Optional[DBPPTTemplate]: |
| """Get template by ID""" |
| template_repo = PPTTemplateRepository(self.session) |
| return await template_repo.get_template_by_id(template_id) |
|
|
| async def get_templates_by_project_id(self, project_id: str) -> List[DBPPTTemplate]: |
| """Get all templates for a project""" |
| effective_user_id = current_user_id.get() |
| if effective_user_id is not None: |
| owned = await self.project_repo.get_by_id(project_id, user_id=effective_user_id) |
| if not owned: |
| return [] |
| template_repo = PPTTemplateRepository(self.session) |
| return await template_repo.get_templates_by_project_id(project_id) |
|
|
| async def get_templates_by_type(self, project_id: str, template_type: str) -> List[DBPPTTemplate]: |
| """Get templates by type for a project""" |
| effective_user_id = current_user_id.get() |
| if effective_user_id is not None: |
| owned = await self.project_repo.get_by_id(project_id, user_id=effective_user_id) |
| if not owned: |
| return [] |
| template_repo = PPTTemplateRepository(self.session) |
| return await template_repo.get_templates_by_type(project_id, template_type) |
|
|
| async def update_template(self, template_id: int, update_data: Dict[str, Any]) -> bool: |
| """Update a template""" |
| template_repo = PPTTemplateRepository(self.session) |
| return await template_repo.update_template(template_id, update_data) |
|
|
| async def increment_template_usage(self, template_id: int) -> bool: |
| """Increment template usage count""" |
| template_repo = PPTTemplateRepository(self.session) |
| return await template_repo.increment_usage_count(template_id) |
|
|
| async def delete_template(self, template_id: int) -> bool: |
| """Delete a template""" |
| template_repo = PPTTemplateRepository(self.session) |
| return await template_repo.delete_template(template_id) |
|
|
| async def delete_templates_by_project_id(self, project_id: str) -> bool: |
| """Delete all templates for a project""" |
| effective_user_id = current_user_id.get() |
| if effective_user_id is not None: |
| owned = await self.project_repo.get_by_id(project_id, user_id=effective_user_id) |
| if not owned: |
| return False |
| template_repo = PPTTemplateRepository(self.session) |
| return await template_repo.delete_templates_by_project_id(project_id) |
|
|
| |
| async def create_global_master_template( |
| self, |
| template_data: Dict[str, Any], |
| user_id: Optional[int] = None, |
| ) -> DBGlobalMasterTemplate: |
| """Create a new global master template""" |
| template_repo = GlobalMasterTemplateRepository(self.session) |
| return await template_repo.create_template(template_data, user_id=user_id) |
|
|
| async def get_global_master_template_by_id( |
| self, |
| template_id: int, |
| user_id: Optional[int] = None, |
| ) -> Optional[DBGlobalMasterTemplate]: |
| """Get global master template by ID""" |
| template_repo = GlobalMasterTemplateRepository(self.session) |
| return await template_repo.get_template_by_id(template_id, user_id=user_id) |
|
|
| async def get_global_master_template_by_name( |
| self, |
| template_name: str, |
| user_id: Optional[int] = None, |
| ) -> Optional[DBGlobalMasterTemplate]: |
| """Get global master template by name""" |
| template_repo = GlobalMasterTemplateRepository(self.session) |
| return await template_repo.get_template_by_name(template_name, user_id=user_id) |
|
|
| async def get_all_global_master_templates( |
| self, |
| active_only: bool = True, |
| user_id: Optional[int] = None, |
| ) -> List[DBGlobalMasterTemplate]: |
| """Get all global master templates""" |
| template_repo = GlobalMasterTemplateRepository(self.session) |
| return await template_repo.get_all_templates(active_only, user_id=user_id) |
|
|
| async def get_global_master_templates_by_tags( |
| self, |
| tags: List[str], |
| active_only: bool = True, |
| user_id: Optional[int] = None, |
| ) -> List[DBGlobalMasterTemplate]: |
| """Get global master templates by tags""" |
| template_repo = GlobalMasterTemplateRepository(self.session) |
| return await template_repo.get_templates_by_tags(tags, active_only, user_id=user_id) |
|
|
| async def get_global_master_templates_paginated( |
| self, |
| active_only: bool = True, |
| offset: int = 0, |
| limit: int = 6, |
| search: Optional[str] = None, |
| user_id: Optional[int] = None, |
| ) -> Tuple[List[DBGlobalMasterTemplate], int]: |
| """Get global master templates with pagination""" |
| template_repo = GlobalMasterTemplateRepository(self.session) |
| return await template_repo.get_templates_paginated( |
| active_only, |
| offset, |
| limit, |
| search, |
| user_id=user_id, |
| ) |
|
|
| async def get_global_master_templates_by_tags_paginated( |
| self, |
| tags: List[str], |
| active_only: bool = True, |
| offset: int = 0, |
| limit: int = 6, |
| search: Optional[str] = None, |
| user_id: Optional[int] = None, |
| ) -> Tuple[List[DBGlobalMasterTemplate], int]: |
| """Get global master templates by tags with pagination""" |
| template_repo = GlobalMasterTemplateRepository(self.session) |
| return await template_repo.get_templates_by_tags_paginated( |
| tags, |
| active_only, |
| offset, |
| limit, |
| search, |
| user_id=user_id, |
| ) |
|
|
| async def update_global_master_template( |
| self, |
| template_id: int, |
| update_data: Dict[str, Any], |
| user_id: Optional[int] = None, |
| allow_system_write: bool = False, |
| ) -> bool: |
| """Update a global master template""" |
| template_repo = GlobalMasterTemplateRepository(self.session) |
| return await template_repo.update_template( |
| template_id, |
| update_data, |
| user_id=user_id, |
| allow_system_write=allow_system_write, |
| ) |
|
|
| async def delete_global_master_template( |
| self, |
| template_id: int, |
| user_id: Optional[int] = None, |
| allow_system_write: bool = False, |
| ) -> bool: |
| """Delete a global master template""" |
| template_repo = GlobalMasterTemplateRepository(self.session) |
| return await template_repo.delete_template( |
| template_id, |
| user_id=user_id, |
| allow_system_write=allow_system_write, |
| ) |
|
|
| async def increment_global_master_template_usage(self, template_id: int, user_id: Optional[int] = None) -> bool: |
| """Increment global master template usage count""" |
| template_repo = GlobalMasterTemplateRepository(self.session) |
| return await template_repo.increment_usage_count(template_id, user_id=user_id) |
|
|
| async def set_default_global_master_template( |
| self, |
| template_id: int, |
| user_id: Optional[int] = None, |
| allow_system_write: bool = False, |
| ) -> bool: |
| """Set a global master template as default""" |
| template_repo = GlobalMasterTemplateRepository(self.session) |
| return await template_repo.set_default_template( |
| template_id, |
| user_id=user_id, |
| allow_system_write=allow_system_write, |
| ) |
|
|
| async def get_default_global_master_template(self, user_id: Optional[int] = None) -> Optional[DBGlobalMasterTemplate]: |
| """Get the default global master template""" |
| template_repo = GlobalMasterTemplateRepository(self.session) |
| return await template_repo.get_default_template(user_id=user_id) |
|
|