feat(phase8-task201): EditTemplate + EditPlan 数据模型、仓储、迁移与测试 #141

Closed
xiaoxia wants to merge 1 commits from feature/phase8-task201-data-models into develop
9 changed files with 826 additions and 6 deletions
@@ -0,0 +1,115 @@
"""phase8 edit template plan
Revision ID: 016
Revises: 015
Create Date: 2026-07-01
"""
import sqlalchemy as sa
from alembic import op
revision = "016"
down_revision = "015"
branch_labels = None
depends_on = None
def upgrade() -> None:
# --- edit_templates: 替换为 Phase 8 新 schema ---
# 删除旧列
op.drop_column("edit_templates", "project_id")
op.drop_column("edit_templates", "target_duration")
op.drop_column("edit_templates", "clip_count")
op.drop_column("edit_templates", "is_active")
op.drop_column("edit_templates", "created_by_user_id")
op.drop_column("edit_templates", "metadata")
# 添加新列
op.add_column(
"edit_templates",
sa.Column("template_type", sa.String(50), nullable=False, server_default="default"),
)
op.add_column(
"edit_templates",
sa.Column("config", sa.JSON(), nullable=False, server_default="{}"),
)
op.add_column(
"edit_templates",
sa.Column("preview_url", sa.String(1000), nullable=False, server_default=""),
)
op.add_column(
"edit_templates",
sa.Column("sort_weight", sa.Integer(), nullable=False, server_default="0"),
)
op.add_column(
"edit_templates",
sa.Column("status", sa.String(20), nullable=False, server_default="active"),
)
# 添加索引
op.create_index("ix_edit_templates_template_type", "edit_templates", ["template_type"])
op.create_index("ix_edit_templates_sort_weight", "edit_templates", ["sort_weight"])
op.create_index("ix_edit_templates_status", "edit_templates", ["status"])
# --- edit_plans: 重建表(在 011 中被删除) ---
op.create_table(
"edit_plans",
sa.Column("id", sa.String(32), primary_key=True),
sa.Column("template_id", sa.String(32), nullable=False, index=True),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("status", sa.String(20), nullable=False, server_default="draft", index=True),
sa.Column("total_duration", sa.Float(), nullable=False, server_default="0"),
sa.Column("config", sa.JSON(), nullable=False, server_default="{}"),
sa.Column(
"created_at",
sa.DateTime(),
nullable=False,
server_default=sa.func.now(),
),
sa.Column(
"updated_at",
sa.DateTime(),
nullable=False,
server_default=sa.func.now(),
),
)
def downgrade() -> None:
op.drop_table("edit_plans")
op.drop_index("ix_edit_templates_status", "edit_templates")
op.drop_index("ix_edit_templates_sort_weight", "edit_templates")
op.drop_index("ix_edit_templates_template_type", "edit_templates")
op.drop_column("edit_templates", "status")
op.drop_column("edit_templates", "sort_weight")
op.drop_column("edit_templates", "preview_url")
op.drop_column("edit_templates", "config")
op.drop_column("edit_templates", "template_type")
# 恢复旧列
op.add_column(
"edit_templates",
sa.Column("project_id", sa.String(32), nullable=False, server_default=""),
)
op.add_column(
"edit_templates",
sa.Column("target_duration", sa.Float(), nullable=False, server_default="30"),
)
op.add_column(
"edit_templates",
sa.Column("clip_count", sa.Integer(), nullable=False, server_default="3"),
)
op.add_column(
"edit_templates",
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
)
op.add_column(
"edit_templates",
sa.Column("created_by_user_id", sa.String(32), nullable=False, server_default=""),
)
op.add_column(
"edit_templates",
sa.Column("metadata", sa.JSON(), nullable=False, server_default="{}"),
)
@@ -3,6 +3,8 @@
from .asset_library_repository import SQLAlchemyAssetLibraryRepository
from .asset_repository import SQLAlchemyAssetRepository
from .classification_job_repository import SQLAlchemyClassificationJobRepository
from .edit_plan_repository import SQLAlchemyEditPlanRepository
from .edit_template_repository import SQLAlchemyEditTemplateRepository
from .generated_video_repository import SQLAlchemyGeneratedVideoRepository
from .generation_task_repository import SQLAlchemyGenerationTaskRepository
from .ingest_job_repository import SQLAlchemyIngestJobRepository
@@ -20,6 +22,8 @@ __all__ = [
"SQLAlchemyAssetLibraryRepository",
"SQLAlchemyAssetRepository",
"SQLAlchemyClassificationJobRepository",
"SQLAlchemyEditPlanRepository",
"SQLAlchemyEditTemplateRepository",
"SQLAlchemyGeneratedVideoRepository",
"SQLAlchemyGenerationTaskRepository",
"SQLAlchemyIngestJobRepository",
@@ -0,0 +1,128 @@
"""SQLAlchemy implementation of EditPlanRepository."""
from __future__ import annotations
from typing import List, Optional
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
from packages.domain.edit_plan import EditPlan, EditPlanStatus
class SQLAlchemyEditPlanRepository:
"""SQLAlchemy 剪辑计划仓储"""
def __init__(self, session: Session) -> None:
self.session = session
def list_by_template(
self,
template_id: str,
*,
status: Optional[EditPlanStatus] = None,
skip: int = 0,
limit: int = 50,
) -> List[EditPlan]:
"""按模板列出剪辑计划"""
query = self.session.query(EditPlanModel).filter(
EditPlanModel.template_id == template_id,
)
if status:
query = query.filter(EditPlanModel.status == status)
query = query.order_by(EditPlanModel.created_at.desc())
models = query.offset(skip).limit(limit).all()
return [self._model_to_entity(m) for m in models]
def list_all(
self,
*,
status: Optional[EditPlanStatus] = None,
skip: int = 0,
limit: int = 50,
) -> List[EditPlan]:
"""列出所有剪辑计划"""
query = self.session.query(EditPlanModel)
if status:
query = query.filter(EditPlanModel.status == status)
query = query.order_by(EditPlanModel.created_at.desc())
models = query.offset(skip).limit(limit).all()
return [self._model_to_entity(m) for m in models]
def get(self, plan_id: str) -> Optional[EditPlan]:
"""根据 ID 获取计划"""
model = (
self.session.query(EditPlanModel)
.filter(EditPlanModel.id == plan_id)
.first()
)
if model is None:
return None
return self._model_to_entity(model)
def create(self, plan: EditPlan) -> EditPlan:
"""创建计划"""
model = EditPlanModel(
id=plan.id,
template_id=plan.template_id,
name=plan.name,
status=plan.status,
total_duration=plan.total_duration,
config=plan.config,
)
self.session.add(model)
self.session.commit()
self.session.refresh(model)
return self._model_to_entity(model)
def update(self, plan: EditPlan) -> EditPlan:
"""更新计划"""
model = (
self.session.query(EditPlanModel)
.filter(EditPlanModel.id == plan.id)
.first()
)
if model is None:
raise ValueError(f"EditPlan {plan.id} not found")
model.template_id = plan.template_id
model.name = plan.name
model.status = plan.status
model.total_duration = plan.total_duration
model.config = plan.config
model.updated_at = plan.updated_at
self.session.commit()
self.session.refresh(model)
return self._model_to_entity(model)
def delete(self, plan_id: str) -> bool:
"""删除计划"""
model = (
self.session.query(EditPlanModel)
.filter(EditPlanModel.id == plan_id)
.first()
)
if model is None:
return False
self.session.delete(model)
self.session.commit()
return True
def count(self, *, status: Optional[EditPlanStatus] = None) -> int:
"""统计计划数量"""
query = self.session.query(EditPlanModel)
if status:
query = query.filter(EditPlanModel.status == status)
return query.count()
@staticmethod
def _model_to_entity(model: EditPlanModel) -> EditPlan:
return EditPlan(
id=model.id,
template_id=model.template_id,
name=model.name,
status=EditPlanStatus(model.status) if model.status else EditPlanStatus.DRAFT,
total_duration=model.total_duration or 0.0,
config=model.config or {},
created_at=model.created_at,
updated_at=model.updated_at,
)
@@ -0,0 +1,142 @@
"""SQLAlchemy implementation of EditTemplateRepository."""
from __future__ import annotations
from typing import List, Optional
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.models import EditTemplateModel
from packages.domain.edit_template import EditTemplate, EditTemplateStatus
class SQLAlchemyEditTemplateRepository:
"""SQLAlchemy 剪辑模板仓储"""
def __init__(self, session: Session) -> None:
self.session = session
def list_active(
self,
*,
template_type: Optional[str] = None,
skip: int = 0,
limit: int = 50,
) -> List[EditTemplate]:
"""列出所有激活的模板"""
query = self.session.query(EditTemplateModel).filter(
EditTemplateModel.status == EditTemplateStatus.ACTIVE,
)
if template_type:
query = query.filter(EditTemplateModel.template_type == template_type)
query = query.order_by(
EditTemplateModel.sort_weight.desc(),
EditTemplateModel.created_at.desc(),
)
models = query.offset(skip).limit(limit).all()
return [self._model_to_entity(m) for m in models]
def list_all(
self,
*,
template_type: Optional[str] = None,
status: Optional[EditTemplateStatus] = None,
skip: int = 0,
limit: int = 50,
) -> List[EditTemplate]:
"""列出所有模板(含非激活)"""
query = self.session.query(EditTemplateModel)
if template_type:
query = query.filter(EditTemplateModel.template_type == template_type)
if status:
query = query.filter(EditTemplateModel.status == status)
query = query.order_by(
EditTemplateModel.sort_weight.desc(),
EditTemplateModel.created_at.desc(),
)
models = query.offset(skip).limit(limit).all()
return [self._model_to_entity(m) for m in models]
def get(self, template_id: str) -> Optional[EditTemplate]:
"""根据 ID 获取模板"""
model = (
self.session.query(EditTemplateModel)
.filter(EditTemplateModel.id == template_id)
.first()
)
if model is None:
return None
return self._model_to_entity(model)
def create(self, template: EditTemplate) -> EditTemplate:
"""创建模板"""
model = EditTemplateModel(
id=template.id,
name=template.name,
description=template.description,
template_type=template.template_type,
config=template.config,
preview_url=template.preview_url,
sort_weight=template.sort_weight,
status=template.status,
)
self.session.add(model)
self.session.commit()
self.session.refresh(model)
return self._model_to_entity(model)
def update(self, template: EditTemplate) -> EditTemplate:
"""更新模板"""
model = (
self.session.query(EditTemplateModel)
.filter(EditTemplateModel.id == template.id)
.first()
)
if model is None:
raise ValueError(f"EditTemplate {template.id} not found")
model.name = template.name
model.description = template.description
model.template_type = template.template_type
model.config = template.config
model.preview_url = template.preview_url
model.sort_weight = template.sort_weight
model.status = template.status
model.updated_at = template.updated_at
self.session.commit()
self.session.refresh(model)
return self._model_to_entity(model)
def delete(self, template_id: str) -> bool:
"""删除模板"""
model = (
self.session.query(EditTemplateModel)
.filter(EditTemplateModel.id == template_id)
.first()
)
if model is None:
return False
self.session.delete(model)
self.session.commit()
return True
def count(self, *, status: Optional[EditTemplateStatus] = None) -> int:
"""统计模板数量"""
query = self.session.query(EditTemplateModel)
if status:
query = query.filter(EditTemplateModel.status == status)
return query.count()
@staticmethod
def _model_to_entity(model: EditTemplateModel) -> EditTemplate:
return EditTemplate(
id=model.id,
name=model.name,
description=model.description or "",
template_type=model.template_type or "default",
config=model.config or {},
preview_url=model.preview_url or "",
sort_weight=model.sort_weight or 0,
status=EditTemplateStatus(model.status) if model.status else EditTemplateStatus.ACTIVE,
created_at=model.created_at,
updated_at=model.updated_at,
)
+28 -6
View File
@@ -86,17 +86,39 @@ class AssetModel(Base):
class EditTemplateModel(Base):
"""Phase 8 剪辑模板 ORM 模型
全局模板库中的模板,定义剪辑风格、配置参数和预览信息。
"""
__tablename__ = "edit_templates"
id = Column(String(32), primary_key=True)
project_id = Column(String(32), nullable=False, index=True)
name = Column(String(120), nullable=False)
description = Column(Text, nullable=False, default="")
target_duration = Column(Float, nullable=False, default=30)
clip_count = Column(Integer, nullable=False, default=3)
is_active = Column(Boolean, nullable=False, default=True)
created_by_user_id = Column(String(32), nullable=False, default="")
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
template_type = Column(String(50), nullable=False, default="default", index=True)
config = Column(JSON, nullable=False, default=dict)
preview_url = Column(String(1000), nullable=False, default="")
sort_weight = Column(Integer, nullable=False, default=0, index=True)
status = Column(String(20), nullable=False, default="active", index=True)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class EditPlanModel(Base):
"""Phase 8 剪辑计划 ORM 模型
基于某个 EditTemplate 创建的剪辑计划,包含具体的配置和状态追踪。
"""
__tablename__ = "edit_plans"
id = Column(String(32), primary_key=True)
template_id = Column(String(32), nullable=False, index=True)
name = Column(String(200), nullable=False)
status = Column(String(20), nullable=False, default="draft", index=True)
total_duration = Column(Float, nullable=False, default=0.0)
config = Column(JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
+6
View File
@@ -7,6 +7,8 @@ from .classification import (
)
from .duplication import DuplicateSegment, DuplicationRecord
from .editing_mode import EditingMode
from .edit_plan import EditPlan, EditPlanStatus
from .edit_template import EditTemplate, EditTemplateStatus
from .entities import (
Asset,
AssetLibrary,
@@ -35,6 +37,10 @@ __all__ = [
"DuplicateSegment",
"DuplicationRecord",
"EditingMode",
"EditPlan",
"EditPlanStatus",
"EditTemplate",
"EditTemplateStatus",
"GeneratedVideo",
"GenerationTask",
"GenerationTaskStatus",
+106
View File
@@ -0,0 +1,106 @@
"""EditPlan domain entity for Phase 8 模板编排引擎."""
from __future__ import annotations
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from enum import Enum
class StrEnum(str, Enum):
pass
from typing import Any
from uuid import uuid4
class EditPlanStatus(StrEnum):
"""剪辑计划状态"""
DRAFT = "draft"
EDITING = "editing"
RENDERING = "rendering"
COMPLETED = "completed"
FAILED = "failed"
@dataclass(slots=True)
class EditPlan:
"""Phase 8 剪辑计划实体
基于某个 EditTemplate 创建的剪辑计划,包含具体的配置和状态追踪。
状态流转:draft → editing → rendering → completed / failed
"""
id: str
template_id: str
name: str
status: EditPlanStatus = EditPlanStatus.DRAFT
total_duration: float = 0.0
config: dict[str, Any] = field(default_factory=dict)
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@classmethod
def create(
cls,
template_id: str,
name: str,
*,
config: dict[str, Any] | None = None,
total_duration: float = 0.0,
) -> EditPlan:
"""创建新剪辑计划实例"""
clean_name = name.strip()
if not clean_name:
raise ValueError("计划名称不能为空")
if not template_id.strip():
raise ValueError("template_id 不能为空")
return cls(
id=uuid4().hex,
template_id=template_id.strip(),
name=clean_name,
status=EditPlanStatus.DRAFT,
total_duration=total_duration,
config=config or {},
)
def start_editing(self) -> None:
"""开始编辑"""
if self.status != EditPlanStatus.DRAFT:
raise ValueError(f"只有 draft 状态的计划可以开始编辑,当前状态: {self.status}")
self.status = EditPlanStatus.EDITING
self.updated_at = datetime.now(timezone.utc)
def start_rendering(self) -> None:
"""开始渲染"""
if self.status != EditPlanStatus.EDITING:
raise ValueError(f"只有 editing 状态的计划可以开始渲染,当前状态: {self.status}")
self.status = EditPlanStatus.RENDERING
self.updated_at = datetime.now(timezone.utc)
def mark_completed(self) -> None:
"""标记为完成"""
if self.status != EditPlanStatus.RENDERING:
raise ValueError(f"只有 rendering 状态的计划可以标记完成,当前状态: {self.status}")
self.status = EditPlanStatus.COMPLETED
self.updated_at = datetime.now(timezone.utc)
def mark_failed(self) -> None:
"""标记为失败"""
if self.status != EditPlanStatus.RENDERING:
raise ValueError(f"只有 rendering 状态的计划可以标记失败,当前状态: {self.status}")
self.status = EditPlanStatus.FAILED
self.updated_at = datetime.now(timezone.utc)
def reset_to_draft(self) -> None:
"""重置为草稿状态(仅从 failed 状态可重置)"""
if self.status != EditPlanStatus.FAILED:
raise ValueError(f"只有 failed 状态的计划可以重置,当前状态: {self.status}")
self.status = EditPlanStatus.DRAFT
self.updated_at = datetime.now(timezone.utc)
+88
View File
@@ -0,0 +1,88 @@
"""EditTemplate domain entity for Phase 8 模板编排引擎."""
from __future__ import annotations
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from enum import Enum
class StrEnum(str, Enum):
pass
from typing import Any
from uuid import uuid4
class EditTemplateStatus(StrEnum):
"""模板状态"""
ACTIVE = "active"
INACTIVE = "inactive"
@dataclass(slots=True)
class EditTemplate:
"""Phase 8 剪辑模板实体
全局模板库中的模板,定义剪辑风格、配置参数和预览信息。
不绑定到具体项目,可被多个 EditPlan 引用。
"""
id: str
name: str
description: str = ""
template_type: str = "default"
config: dict[str, Any] = field(default_factory=dict)
preview_url: str = ""
sort_weight: int = 0
status: EditTemplateStatus = EditTemplateStatus.ACTIVE
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@classmethod
def create(
cls,
name: str,
*,
description: str = "",
template_type: str = "default",
config: dict[str, Any] | None = None,
preview_url: str = "",
sort_weight: int = 0,
status: EditTemplateStatus = EditTemplateStatus.ACTIVE,
) -> EditTemplate:
"""创建新模板实例"""
clean_name = name.strip()
if not clean_name:
raise ValueError("模板名称不能为空")
return cls(
id=uuid4().hex,
name=clean_name,
description=description.strip(),
template_type=template_type.strip() or "default",
config=config or {},
preview_url=preview_url.strip(),
sort_weight=sort_weight,
status=status,
)
def activate(self) -> None:
"""激活模板"""
self.status = EditTemplateStatus.ACTIVE
self.updated_at = datetime.now(timezone.utc)
def deactivate(self) -> None:
"""停用模板"""
self.status = EditTemplateStatus.INACTIVE
self.updated_at = datetime.now(timezone.utc)
@property
def is_active(self) -> bool:
"""模板是否处于激活状态"""
return self.status == EditTemplateStatus.ACTIVE
+209
View File
@@ -0,0 +1,209 @@
"""Unit tests for Phase 8 EditTemplate and EditPlan domain entities + repositories."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from packages.adapters.sqlalchemy_impl.edit_plan_repository import (
SQLAlchemyEditPlanRepository,
)
from packages.adapters.sqlalchemy_impl.edit_template_repository import (
SQLAlchemyEditTemplateRepository,
)
from packages.adapters.sqlalchemy_impl.models import Base
from packages.domain.edit_plan import EditPlan, EditPlanStatus
from packages.domain.edit_template import EditTemplate, EditTemplateStatus
# ── EditTemplate 领域实体测试 ──────────────────────────────────────────
class TestEditTemplate:
def test_create_success(self):
t = EditTemplate.create("vlog模板", description="日常vlog", template_type="vlog")
assert t.name == "vlog模板"
assert t.description == "日常vlog"
assert t.template_type == "vlog"
assert t.status == EditTemplateStatus.ACTIVE
assert t.config == {}
assert t.sort_weight == 0
assert t.id # 自动生成
def test_create_strips_whitespace(self):
t = EditTemplate.create(" 模板 ")
assert t.name == "模板"
def test_create_empty_name_raises(self):
try:
EditTemplate.create(" ")
assert False, "应该抛出 ValueError"
except ValueError as e:
assert "模板名称不能为空" in str(e)
def test_activate_deactivate(self):
t = EditTemplate.create("test")
assert t.is_active is True
t.deactivate()
assert t.is_active is False
assert t.status == EditTemplateStatus.INACTIVE
t.activate()
assert t.is_active is True
assert t.status == EditTemplateStatus.ACTIVE
# ── EditPlan 领域实体测试 ──────────────────────────────────────────────
class TestEditPlan:
def test_create_success(self):
p = EditPlan.create("tpl-1", "我的计划", config={"bgm": "happy"})
assert p.template_id == "tpl-1"
assert p.name == "我的计划"
assert p.status == EditPlanStatus.DRAFT
assert p.config == {"bgm": "happy"}
assert p.total_duration == 0.0
def test_create_empty_name_raises(self):
try:
EditPlan.create("tpl-1", " ")
assert False, "应该抛出 ValueError"
except ValueError as e:
assert "计划名称不能为空" in str(e)
def test_create_empty_template_id_raises(self):
try:
EditPlan.create(" ", "test")
assert False, "应该抛出 ValueError"
except ValueError as e:
assert "template_id 不能为空" in str(e)
def test_status_transitions_happy_path(self):
p = EditPlan.create("tpl-1", "test")
assert p.status == EditPlanStatus.DRAFT
p.start_editing()
assert p.status == EditPlanStatus.EDITING
p.start_rendering()
assert p.status == EditPlanStatus.RENDERING
p.mark_completed()
assert p.status == EditPlanStatus.COMPLETED
def test_status_transitions_failure_path(self):
p = EditPlan.create("tpl-1", "test")
p.start_editing()
p.start_rendering()
p.mark_failed()
assert p.status == EditPlanStatus.FAILED
p.reset_to_draft()
assert p.status == EditPlanStatus.DRAFT
def test_invalid_transition_raises(self):
p = EditPlan.create("tpl-1", "test")
try:
p.start_rendering() # draft → rendering 不合法
assert False, "应该抛出 ValueError"
except ValueError:
pass
def test_mark_completed_from_non_rendering_raises(self):
p = EditPlan.create("tpl-1", "test")
try:
p.mark_completed() # draft → completed 不合法
assert False, "应该抛出 ValueError"
except ValueError:
pass
def test_reset_from_non_failed_raises(self):
p = EditPlan.create("tpl-1", "test")
try:
p.reset_to_draft() # draft → draft 不合法
assert False, "应该抛出 ValueError"
except ValueError:
pass
# ── Repository 集成测试(内存 SQLite) ─────────────────────────────────
class TestRepositories:
def _make_session(self):
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine)
return SessionLocal()
def test_edit_template_repository_crud(self):
session = self._make_session()
try:
repo = SQLAlchemyEditTemplateRepository(session)
t = EditTemplate.create("模板A", description="测试模板")
repo.create(t)
fetched = repo.get(t.id)
assert fetched is not None
assert fetched.name == "模板A"
assert fetched.description == "测试模板"
# list
active = repo.list_active()
assert len(active) == 1
assert active[0].id == t.id
# update
fetched.deactivate()
repo.update(fetched)
assert repo.get(t.id).status == EditTemplateStatus.INACTIVE
assert len(repo.list_active()) == 0
# count
assert repo.count() == 1
# delete
assert repo.delete(t.id) is True
assert repo.get(t.id) is None
assert repo.count() == 0
finally:
session.close()
def test_edit_plan_repository_crud(self):
session = self._make_session()
try:
repo = SQLAlchemyEditPlanRepository(session)
p = EditPlan.create("tpl-1", "计划A", config={"key": "val"})
repo.create(p)
fetched = repo.get(p.id)
assert fetched is not None
assert fetched.name == "计划A"
assert fetched.config == {"key": "val"}
assert fetched.status == EditPlanStatus.DRAFT
# list_by_template
plans = repo.list_by_template("tpl-1")
assert len(plans) == 1
# update status
fetched.start_editing()
repo.update(fetched)
editing = repo.list_by_template("tpl-1", status=EditPlanStatus.EDITING)
assert len(editing) == 1
# count
assert repo.count() == 1
assert repo.count(status=EditPlanStatus.EDITING) == 1
assert repo.count(status=EditPlanStatus.DRAFT) == 0
# delete
assert repo.delete(p.id) is True
assert repo.get(p.id) is None
finally:
session.close()