Init Repo
This commit is contained in:
@@ -0,0 +1,563 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify custom status support across multiple projects with different configurations.
|
||||
|
||||
This test ensures that:
|
||||
1. Projects with no custom statuses work correctly
|
||||
2. Projects with different custom status sets work independently
|
||||
3. Mixed project queries handle custom statuses correctly
|
||||
4. Default status fallback works when custom statuses are missing
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import json
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from database import Base
|
||||
from models.user import User, UserRole
|
||||
from models.project import Project, ProjectType, ProjectStatus, ProjectMember
|
||||
from models.task import Task
|
||||
from models.asset import Asset, AssetCategory, AssetStatus
|
||||
from models.episode import Episode, EpisodeStatus
|
||||
from models.shot import Shot, ShotStatus
|
||||
|
||||
# Create test database
|
||||
TEST_DB = "test_multi_project_custom.db"
|
||||
if os.path.exists(TEST_DB):
|
||||
try:
|
||||
os.remove(TEST_DB)
|
||||
except PermissionError:
|
||||
pass # File in use, continue anyway
|
||||
|
||||
engine = create_engine(f"sqlite:///{TEST_DB}")
|
||||
Base.metadata.create_all(engine)
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
|
||||
def setup_multi_project_data():
|
||||
"""Set up test data with multiple projects having different custom status configurations."""
|
||||
print("Setting up multi-project test data...")
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Create test user
|
||||
user = User(
|
||||
email="coordinator@test.com",
|
||||
password_hash="test",
|
||||
first_name="Test",
|
||||
last_name="Coordinator",
|
||||
role=UserRole.COORDINATOR,
|
||||
is_approved=True
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
|
||||
# Project 1: No custom statuses (uses only default statuses)
|
||||
project1 = Project(
|
||||
name="Default Status Project",
|
||||
code_name="DSP",
|
||||
client_name="Client 1",
|
||||
project_type=ProjectType.CINEMA,
|
||||
status=ProjectStatus.IN_PROGRESS,
|
||||
custom_task_statuses=json.dumps([]) # Empty custom statuses
|
||||
)
|
||||
db.add(project1)
|
||||
|
||||
# Project 2: Custom statuses with different names
|
||||
project2_custom = [
|
||||
{
|
||||
"id": "review_needed",
|
||||
"name": "Review Needed",
|
||||
"color": "#FFA500",
|
||||
"order": 0,
|
||||
"is_default": False
|
||||
},
|
||||
{
|
||||
"id": "client_approved",
|
||||
"name": "Client Approved",
|
||||
"color": "#32CD32",
|
||||
"order": 1,
|
||||
"is_default": True
|
||||
}
|
||||
]
|
||||
|
||||
project2 = Project(
|
||||
name="Custom Status Project",
|
||||
code_name="CSP",
|
||||
client_name="Client 2",
|
||||
project_type=ProjectType.TV,
|
||||
status=ProjectStatus.IN_PROGRESS,
|
||||
custom_task_statuses=json.dumps(project2_custom)
|
||||
)
|
||||
db.add(project2)
|
||||
|
||||
# Project 3: Different custom statuses
|
||||
project3_custom = [
|
||||
{
|
||||
"id": "waiting_assets",
|
||||
"name": "Waiting for Assets",
|
||||
"color": "#FFD700",
|
||||
"order": 0,
|
||||
"is_default": False
|
||||
},
|
||||
{
|
||||
"id": "final_review",
|
||||
"name": "Final Review",
|
||||
"color": "#9370DB",
|
||||
"order": 1,
|
||||
"is_default": False
|
||||
}
|
||||
]
|
||||
|
||||
project3 = Project(
|
||||
name="Different Custom Project",
|
||||
code_name="DCP",
|
||||
client_name="Client 3",
|
||||
project_type=ProjectType.CINEMA,
|
||||
status=ProjectStatus.IN_PROGRESS,
|
||||
custom_task_statuses=json.dumps(project3_custom)
|
||||
)
|
||||
db.add(project3)
|
||||
|
||||
db.commit()
|
||||
|
||||
# Add user as member of all projects
|
||||
for project in [project1, project2, project3]:
|
||||
member = ProjectMember(
|
||||
user_id=user.id,
|
||||
project_id=project.id
|
||||
)
|
||||
db.add(member)
|
||||
|
||||
db.commit()
|
||||
|
||||
# Create episodes for each project
|
||||
episodes = []
|
||||
for i, project in enumerate([project1, project2, project3], 1):
|
||||
episode = Episode(
|
||||
project_id=project.id,
|
||||
name=f"Episode {i}",
|
||||
episode_number=i,
|
||||
status=EpisodeStatus.IN_PROGRESS
|
||||
)
|
||||
db.add(episode)
|
||||
episodes.append(episode)
|
||||
|
||||
db.commit()
|
||||
|
||||
# Create shots with different status patterns for each project
|
||||
|
||||
# Project 1: Only default statuses
|
||||
shot1 = Shot(
|
||||
project_id=project1.id,
|
||||
episode_id=episodes[0].id,
|
||||
name="default_shot_001",
|
||||
frame_start=1001,
|
||||
frame_end=1100,
|
||||
status=ShotStatus.IN_PROGRESS
|
||||
)
|
||||
db.add(shot1)
|
||||
db.flush()
|
||||
|
||||
# Tasks with only default statuses
|
||||
for task_type, status in [("layout", "not_started"), ("animation", "in_progress"), ("lighting", "approved")]:
|
||||
task = Task(
|
||||
project_id=project1.id,
|
||||
episode_id=episodes[0].id,
|
||||
shot_id=shot1.id,
|
||||
name=f"{shot1.name}_{task_type}",
|
||||
task_type=task_type,
|
||||
status=status
|
||||
)
|
||||
db.add(task)
|
||||
|
||||
# Project 2: Mix of default and custom statuses
|
||||
shot2 = Shot(
|
||||
project_id=project2.id,
|
||||
episode_id=episodes[1].id,
|
||||
name="custom_shot_001",
|
||||
frame_start=1001,
|
||||
frame_end=1100,
|
||||
status=ShotStatus.IN_PROGRESS
|
||||
)
|
||||
db.add(shot2)
|
||||
db.flush()
|
||||
|
||||
# Tasks with mix of default and custom statuses
|
||||
for task_type, status in [("layout", "review_needed"), ("animation", "submitted"), ("lighting", "client_approved")]:
|
||||
task = Task(
|
||||
project_id=project2.id,
|
||||
episode_id=episodes[1].id,
|
||||
shot_id=shot2.id,
|
||||
name=f"{shot2.name}_{task_type}",
|
||||
task_type=task_type,
|
||||
status=status
|
||||
)
|
||||
db.add(task)
|
||||
|
||||
# Project 3: Different custom statuses
|
||||
shot3 = Shot(
|
||||
project_id=project3.id,
|
||||
episode_id=episodes[2].id,
|
||||
name="different_shot_001",
|
||||
frame_start=1001,
|
||||
frame_end=1100,
|
||||
status=ShotStatus.IN_PROGRESS
|
||||
)
|
||||
db.add(shot3)
|
||||
db.flush()
|
||||
|
||||
# Tasks with different custom statuses
|
||||
for task_type, status in [("layout", "waiting_assets"), ("animation", "final_review"), ("lighting", "retake")]:
|
||||
task = Task(
|
||||
project_id=project3.id,
|
||||
episode_id=episodes[2].id,
|
||||
shot_id=shot3.id,
|
||||
name=f"{shot3.name}_{task_type}",
|
||||
task_type=task_type,
|
||||
status=status
|
||||
)
|
||||
db.add(task)
|
||||
|
||||
# Create assets for each project with different status patterns
|
||||
|
||||
# Project 1 asset: Only default statuses
|
||||
asset1 = Asset(
|
||||
project_id=project1.id,
|
||||
name="default_character",
|
||||
category=AssetCategory.CHARACTERS,
|
||||
status=AssetStatus.IN_PROGRESS
|
||||
)
|
||||
db.add(asset1)
|
||||
db.flush()
|
||||
|
||||
for task_type, status in [("modeling", "not_started"), ("surfacing", "in_progress")]:
|
||||
task = Task(
|
||||
project_id=project1.id,
|
||||
asset_id=asset1.id,
|
||||
name=f"{asset1.name}_{task_type}",
|
||||
task_type=task_type,
|
||||
status=status
|
||||
)
|
||||
db.add(task)
|
||||
|
||||
# Project 2 asset: Custom statuses
|
||||
asset2 = Asset(
|
||||
project_id=project2.id,
|
||||
name="custom_prop",
|
||||
category=AssetCategory.PROPS,
|
||||
status=AssetStatus.IN_PROGRESS
|
||||
)
|
||||
db.add(asset2)
|
||||
db.flush()
|
||||
|
||||
for task_type, status in [("modeling", "review_needed"), ("surfacing", "client_approved")]:
|
||||
task = Task(
|
||||
project_id=project2.id,
|
||||
asset_id=asset2.id,
|
||||
name=f"{asset2.name}_{task_type}",
|
||||
task_type=task_type,
|
||||
status=status
|
||||
)
|
||||
db.add(task)
|
||||
|
||||
# Project 3 asset: Different custom statuses
|
||||
asset3 = Asset(
|
||||
project_id=project3.id,
|
||||
name="different_set",
|
||||
category=AssetCategory.SETS,
|
||||
status=AssetStatus.IN_PROGRESS
|
||||
)
|
||||
db.add(asset3)
|
||||
db.flush()
|
||||
|
||||
for task_type, status in [("modeling", "waiting_assets"), ("surfacing", "final_review")]:
|
||||
task = Task(
|
||||
project_id=project3.id,
|
||||
asset_id=asset3.id,
|
||||
name=f"{asset3.name}_{task_type}",
|
||||
task_type=task_type,
|
||||
status=status
|
||||
)
|
||||
db.add(task)
|
||||
|
||||
db.commit()
|
||||
print("✅ Multi-project test data setup complete")
|
||||
return user, [project1, project2, project3], episodes
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"❌ Failed to setup multi-project test data: {e}")
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_project_isolation():
|
||||
"""Test that custom statuses are properly isolated between projects."""
|
||||
print("\n=== Test 1: Project Custom Status Isolation ===")
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).first()
|
||||
projects = db.query(Project).all()
|
||||
|
||||
from routers.shots import list_shots
|
||||
import asyncio
|
||||
|
||||
# Test each project individually
|
||||
for project in projects:
|
||||
episode = db.query(Episode).filter(Episode.project_id == project.id).first()
|
||||
|
||||
async def get_project_shots():
|
||||
return await list_shots(
|
||||
episode_id=episode.id,
|
||||
project_id=project.id,
|
||||
task_status_filter=None,
|
||||
sort_by=None,
|
||||
sort_direction="asc",
|
||||
skip=0,
|
||||
limit=100,
|
||||
db=db,
|
||||
current_user=user
|
||||
)
|
||||
|
||||
shots = asyncio.run(get_project_shots())
|
||||
|
||||
print(f"\nProject: {project.name} (ID: {project.id})")
|
||||
print(f" Custom statuses: {json.loads(project.custom_task_statuses or '[]')}")
|
||||
|
||||
for shot in shots:
|
||||
print(f" Shot: {shot.name}")
|
||||
print(f" Task statuses: {shot.task_status}")
|
||||
|
||||
# Verify that only appropriate statuses are present
|
||||
for task_type, status in shot.task_status.items():
|
||||
if status.startswith('custom_') or status in ['review_needed', 'client_approved', 'waiting_assets', 'final_review']:
|
||||
# This is a custom status, verify it belongs to this project
|
||||
project_custom_statuses = json.loads(project.custom_task_statuses or '[]')
|
||||
custom_status_ids = [cs['id'] for cs in project_custom_statuses]
|
||||
|
||||
if status not in custom_status_ids and status not in ['not_started', 'in_progress', 'submitted', 'approved', 'retake']:
|
||||
# This might be a status from another project - this would be an error
|
||||
print(f" ⚠️ Status '{status}' not found in project custom statuses: {custom_status_ids}")
|
||||
else:
|
||||
print(f" ✅ Status '{status}' correctly belongs to this project")
|
||||
|
||||
print("✅ Test 1 PASSED: Project custom status isolation works correctly")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Test 1 FAILED: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_mixed_project_queries():
|
||||
"""Test queries that span multiple projects with different custom statuses."""
|
||||
print("\n=== Test 2: Mixed Project Queries ===")
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).first()
|
||||
|
||||
from routers.shots import list_shots
|
||||
from routers.assets import list_assets
|
||||
import asyncio
|
||||
|
||||
# Test shots across all projects (no project filter)
|
||||
async def get_all_shots():
|
||||
return await list_shots(
|
||||
episode_id=None,
|
||||
project_id=None, # No project filter - should get all accessible shots
|
||||
task_status_filter=None,
|
||||
sort_by=None,
|
||||
sort_direction="asc",
|
||||
skip=0,
|
||||
limit=100,
|
||||
db=db,
|
||||
current_user=user
|
||||
)
|
||||
|
||||
all_shots = asyncio.run(get_all_shots())
|
||||
|
||||
print(f"✅ Retrieved {len(all_shots)} shots across all projects")
|
||||
|
||||
# Verify that each shot has appropriate statuses for its project
|
||||
for shot in all_shots:
|
||||
print(f"\nShot: {shot.name}")
|
||||
print(f" Task statuses: {shot.task_status}")
|
||||
|
||||
# Get the project for this shot to verify statuses
|
||||
shot_project = db.query(Project).filter(Project.id == shot.project_id).first()
|
||||
project_custom_statuses = json.loads(shot_project.custom_task_statuses or '[]')
|
||||
custom_status_ids = [cs['id'] for cs in project_custom_statuses]
|
||||
|
||||
for task_type, status in shot.task_status.items():
|
||||
if status not in ['not_started', 'in_progress', 'submitted', 'approved', 'retake']:
|
||||
# This is a custom status, verify it belongs to the shot's project
|
||||
assert status in custom_status_ids, f"Custom status '{status}' not found in project {shot_project.name} custom statuses"
|
||||
print(f" ✅ Custom status '{status}' correctly belongs to project {shot_project.name}")
|
||||
|
||||
# Test assets across all projects
|
||||
async def get_all_assets():
|
||||
return await list_assets(
|
||||
project_id=None, # No project filter
|
||||
category=None,
|
||||
task_status_filter=None,
|
||||
sort_by=None,
|
||||
sort_direction="asc",
|
||||
skip=0,
|
||||
limit=100,
|
||||
db=db,
|
||||
current_user=user
|
||||
)
|
||||
|
||||
all_assets = asyncio.run(get_all_assets())
|
||||
|
||||
print(f"\n✅ Retrieved {len(all_assets)} assets across all projects")
|
||||
|
||||
# Verify that each asset has appropriate statuses for its project
|
||||
for asset in all_assets:
|
||||
print(f"\nAsset: {asset.name}")
|
||||
print(f" Task statuses: {asset.task_status}")
|
||||
|
||||
# Get the project for this asset to verify statuses
|
||||
asset_project = db.query(Project).filter(Project.id == asset.project_id).first()
|
||||
project_custom_statuses = json.loads(asset_project.custom_task_statuses or '[]')
|
||||
custom_status_ids = [cs['id'] for cs in project_custom_statuses]
|
||||
|
||||
for task_type, status in asset.task_status.items():
|
||||
if status not in ['not_started', 'in_progress', 'submitted', 'approved', 'retake']:
|
||||
# This is a custom status, verify it belongs to the asset's project
|
||||
assert status in custom_status_ids, f"Custom status '{status}' not found in project {asset_project.name} custom statuses"
|
||||
print(f" ✅ Custom status '{status}' correctly belongs to project {asset_project.name}")
|
||||
|
||||
print("✅ Test 2 PASSED: Mixed project queries handle custom statuses correctly")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Test 2 FAILED: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_custom_status_sorting_across_projects():
|
||||
"""Test that custom status sorting works correctly across different projects."""
|
||||
print("\n=== Test 3: Custom Status Sorting Across Projects ===")
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).first()
|
||||
|
||||
from routers.shots import list_shots
|
||||
import asyncio
|
||||
|
||||
# Test sorting by task status across all projects
|
||||
async def get_sorted_shots():
|
||||
return await list_shots(
|
||||
episode_id=None,
|
||||
project_id=None,
|
||||
task_status_filter=None,
|
||||
sort_by="layout_status", # Sort by layout task status
|
||||
sort_direction="asc",
|
||||
skip=0,
|
||||
limit=100,
|
||||
db=db,
|
||||
current_user=user
|
||||
)
|
||||
|
||||
sorted_shots = asyncio.run(get_sorted_shots())
|
||||
|
||||
print(f"✅ Retrieved {len(sorted_shots)} shots sorted by layout status")
|
||||
|
||||
# Verify sorting works with mixed custom statuses from different projects
|
||||
for shot in sorted_shots:
|
||||
layout_status = shot.task_status.get('layout', 'not_started')
|
||||
print(f" Shot {shot.name}: layout = {layout_status}")
|
||||
|
||||
# Get the project to access custom statuses for sorting verification
|
||||
shot_project = db.query(Project).filter(Project.id == shot.project_id).first()
|
||||
project_custom_statuses = json.loads(shot_project.custom_task_statuses or '[]')
|
||||
|
||||
# Import the sorting function to verify order
|
||||
from routers.shots import get_status_sort_order
|
||||
sort_order = get_status_sort_order(layout_status, project_custom_statuses)
|
||||
print(f" Project: {shot_project.name}, Sort order: {sort_order}")
|
||||
|
||||
print("✅ Test 3 PASSED: Custom status sorting across projects works correctly")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Test 3 FAILED: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all multi-project custom status tests."""
|
||||
print("🚀 Starting Multi-Project Custom Status Tests")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# Setup test data
|
||||
setup_multi_project_data()
|
||||
|
||||
# Run tests
|
||||
tests = [
|
||||
test_project_isolation,
|
||||
test_mixed_project_queries,
|
||||
test_custom_status_sorting_across_projects
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for test in tests:
|
||||
try:
|
||||
if test():
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"❌ Test failed with exception: {e}")
|
||||
failed += 1
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"📊 Test Results: {passed} passed, {failed} failed")
|
||||
|
||||
if failed == 0:
|
||||
print("🎉 All multi-project tests passed! Custom status support works across projects.")
|
||||
return True
|
||||
else:
|
||||
print("❌ Some tests failed. Multi-project custom status support needs fixes.")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Test setup failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
finally:
|
||||
# Cleanup
|
||||
try:
|
||||
if os.path.exists(TEST_DB):
|
||||
os.remove(TEST_DB)
|
||||
except PermissionError:
|
||||
pass # File in use, ignore
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
Reference in New Issue
Block a user