feat: Implement initial schema and add various API, service, and management command enhancements across the application.

This commit is contained in:
pacnpal
2026-01-01 15:13:01 -05:00
parent c95f99ca10
commit b243b17af7
413 changed files with 11164 additions and 17433 deletions

View File

@@ -31,39 +31,28 @@ class ParkTransitionTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123',
role='USER'
username="testuser", email="test@example.com", password="testpass123", role="USER"
)
self.moderator = User.objects.create_user(
username='moderator',
email='moderator@example.com',
password='testpass123',
role='MODERATOR'
username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR"
)
self.admin = User.objects.create_user(
username='admin',
email='admin@example.com',
password='testpass123',
role='ADMIN'
username="admin", email="admin@example.com", password="testpass123", role="ADMIN"
)
# Create operator company
self.operator = Company.objects.create(
name='Test Operator',
description='Test operator company',
roles=['OPERATOR']
name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
def _create_park(self, status='OPERATING', **kwargs):
def _create_park(self, status="OPERATING", **kwargs):
"""Helper to create a Park with specified status."""
defaults = {
'name': 'Test Park',
'slug': 'test-park',
'description': 'A test park',
'operator': self.operator,
'timezone': 'America/New_York'
"name": "Test Park",
"slug": "test-park",
"description": "A test park",
"operator": self.operator,
"timezone": "America/New_York",
}
defaults.update(kwargs)
return Park.objects.create(status=status, **defaults)
@@ -74,25 +63,25 @@ class ParkTransitionTests(TestCase):
def test_operating_to_closed_temp_transition(self):
"""Test transition from OPERATING to CLOSED_TEMP."""
park = self._create_park(status='OPERATING')
self.assertEqual(park.status, 'OPERATING')
park = self._create_park(status="OPERATING")
self.assertEqual(park.status, "OPERATING")
park.transition_to_closed_temp(user=self.user)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'CLOSED_TEMP')
self.assertEqual(park.status, "CLOSED_TEMP")
def test_operating_to_closed_perm_transition(self):
"""Test transition from OPERATING to CLOSED_PERM."""
park = self._create_park(status='OPERATING')
park = self._create_park(status="OPERATING")
park.transition_to_closed_perm(user=self.moderator)
park.closing_date = date.today()
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'CLOSED_PERM')
self.assertEqual(park.status, "CLOSED_PERM")
self.assertIsNotNone(park.closing_date)
# -------------------------------------------------------------------------
@@ -101,14 +90,14 @@ class ParkTransitionTests(TestCase):
def test_under_construction_to_operating_transition(self):
"""Test transition from UNDER_CONSTRUCTION to OPERATING."""
park = self._create_park(status='UNDER_CONSTRUCTION')
self.assertEqual(park.status, 'UNDER_CONSTRUCTION')
park = self._create_park(status="UNDER_CONSTRUCTION")
self.assertEqual(park.status, "UNDER_CONSTRUCTION")
park.transition_to_operating(user=self.user)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'OPERATING')
self.assertEqual(park.status, "OPERATING")
# -------------------------------------------------------------------------
# Closed temp transitions
@@ -116,24 +105,24 @@ class ParkTransitionTests(TestCase):
def test_closed_temp_to_operating_transition(self):
"""Test transition from CLOSED_TEMP to OPERATING (reopen)."""
park = self._create_park(status='CLOSED_TEMP')
park = self._create_park(status="CLOSED_TEMP")
park.transition_to_operating(user=self.user)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'OPERATING')
self.assertEqual(park.status, "OPERATING")
def test_closed_temp_to_closed_perm_transition(self):
"""Test transition from CLOSED_TEMP to CLOSED_PERM."""
park = self._create_park(status='CLOSED_TEMP')
park = self._create_park(status="CLOSED_TEMP")
park.transition_to_closed_perm(user=self.moderator)
park.closing_date = date.today()
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'CLOSED_PERM')
self.assertEqual(park.status, "CLOSED_PERM")
# -------------------------------------------------------------------------
# Closed perm transitions (to final states)
@@ -141,23 +130,23 @@ class ParkTransitionTests(TestCase):
def test_closed_perm_to_demolished_transition(self):
"""Test transition from CLOSED_PERM to DEMOLISHED."""
park = self._create_park(status='CLOSED_PERM')
park = self._create_park(status="CLOSED_PERM")
park.transition_to_demolished(user=self.moderator)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'DEMOLISHED')
self.assertEqual(park.status, "DEMOLISHED")
def test_closed_perm_to_relocated_transition(self):
"""Test transition from CLOSED_PERM to RELOCATED."""
park = self._create_park(status='CLOSED_PERM')
park = self._create_park(status="CLOSED_PERM")
park.transition_to_relocated(user=self.moderator)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'RELOCATED')
self.assertEqual(park.status, "RELOCATED")
# -------------------------------------------------------------------------
# Invalid transitions (final states)
@@ -165,28 +154,28 @@ class ParkTransitionTests(TestCase):
def test_demolished_cannot_transition(self):
"""Test that DEMOLISHED state cannot transition further."""
park = self._create_park(status='DEMOLISHED')
park = self._create_park(status="DEMOLISHED")
with self.assertRaises(TransitionNotAllowed):
park.transition_to_operating(user=self.moderator)
def test_relocated_cannot_transition(self):
"""Test that RELOCATED state cannot transition further."""
park = self._create_park(status='RELOCATED')
park = self._create_park(status="RELOCATED")
with self.assertRaises(TransitionNotAllowed):
park.transition_to_operating(user=self.moderator)
def test_operating_cannot_directly_demolish(self):
"""Test that OPERATING cannot directly transition to DEMOLISHED."""
park = self._create_park(status='OPERATING')
park = self._create_park(status="OPERATING")
with self.assertRaises(TransitionNotAllowed):
park.transition_to_demolished(user=self.moderator)
def test_operating_cannot_directly_relocate(self):
"""Test that OPERATING cannot directly transition to RELOCATED."""
park = self._create_park(status='OPERATING')
park = self._create_park(status="OPERATING")
with self.assertRaises(TransitionNotAllowed):
park.transition_to_relocated(user=self.moderator)
@@ -197,69 +186,69 @@ class ParkTransitionTests(TestCase):
def test_reopen_wrapper_method(self):
"""Test the reopen() wrapper method."""
park = self._create_park(status='CLOSED_TEMP')
park = self._create_park(status="CLOSED_TEMP")
park.reopen(user=self.user)
park.refresh_from_db()
self.assertEqual(park.status, 'OPERATING')
self.assertEqual(park.status, "OPERATING")
def test_close_temporarily_wrapper_method(self):
"""Test the close_temporarily() wrapper method."""
park = self._create_park(status='OPERATING')
park = self._create_park(status="OPERATING")
park.close_temporarily(user=self.user)
park.refresh_from_db()
self.assertEqual(park.status, 'CLOSED_TEMP')
self.assertEqual(park.status, "CLOSED_TEMP")
def test_close_permanently_wrapper_method(self):
"""Test the close_permanently() wrapper method."""
park = self._create_park(status='OPERATING')
park = self._create_park(status="OPERATING")
closing = date(2025, 12, 31)
park.close_permanently(closing_date=closing, user=self.moderator)
park.refresh_from_db()
self.assertEqual(park.status, 'CLOSED_PERM')
self.assertEqual(park.status, "CLOSED_PERM")
self.assertEqual(park.closing_date, closing)
def test_close_permanently_without_date(self):
"""Test close_permanently() without closing_date."""
park = self._create_park(status='OPERATING')
park = self._create_park(status="OPERATING")
park.close_permanently(user=self.moderator)
park.refresh_from_db()
self.assertEqual(park.status, 'CLOSED_PERM')
self.assertEqual(park.status, "CLOSED_PERM")
self.assertIsNone(park.closing_date)
def test_demolish_wrapper_method(self):
"""Test the demolish() wrapper method."""
park = self._create_park(status='CLOSED_PERM')
park = self._create_park(status="CLOSED_PERM")
park.demolish(user=self.moderator)
park.refresh_from_db()
self.assertEqual(park.status, 'DEMOLISHED')
self.assertEqual(park.status, "DEMOLISHED")
def test_relocate_wrapper_method(self):
"""Test the relocate() wrapper method."""
park = self._create_park(status='CLOSED_PERM')
park = self._create_park(status="CLOSED_PERM")
park.relocate(user=self.moderator)
park.refresh_from_db()
self.assertEqual(park.status, 'RELOCATED')
self.assertEqual(park.status, "RELOCATED")
def test_start_construction_wrapper_method(self):
"""Test the start_construction() wrapper method if applicable."""
# This depends on allowed transitions - skip if not allowed
try:
park = self._create_park(status='OPERATING')
park = self._create_park(status="OPERATING")
park.start_construction(user=self.moderator)
park.refresh_from_db()
self.assertEqual(park.status, 'UNDER_CONSTRUCTION')
self.assertEqual(park.status, "UNDER_CONSTRUCTION")
except TransitionNotAllowed:
# If transition from OPERATING to UNDER_CONSTRUCTION is not allowed
pass
@@ -276,52 +265,44 @@ class ParkTransitionHistoryTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.moderator = User.objects.create_user(
username='moderator',
email='moderator@example.com',
password='testpass123',
role='MODERATOR'
username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR"
)
self.operator = Company.objects.create(
name='Test Operator',
description='Test operator company',
roles=['OPERATOR']
name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
def _create_park(self, status='OPERATING'):
def _create_park(self, status="OPERATING"):
"""Helper to create a Park."""
return Park.objects.create(
name='Test Park',
slug='test-park',
description='A test park',
name="Test Park",
slug="test-park",
description="A test park",
operator=self.operator,
status=status,
timezone='America/New_York'
timezone="America/New_York",
)
def test_transition_creates_state_log(self):
"""Test that transitions create StateLog entries."""
from django_fsm_log.models import StateLog
park = self._create_park(status='OPERATING')
park = self._create_park(status="OPERATING")
park.transition_to_closed_temp(user=self.moderator)
park.save()
park_ct = ContentType.objects.get_for_model(park)
log = StateLog.objects.filter(
content_type=park_ct,
object_id=park.id
).first()
log = StateLog.objects.filter(content_type=park_ct, object_id=park.id).first()
self.assertIsNotNone(log)
self.assertEqual(log.state, 'CLOSED_TEMP')
self.assertEqual(log.state, "CLOSED_TEMP")
self.assertEqual(log.by, self.moderator)
def test_multiple_transitions_create_multiple_logs(self):
"""Test that multiple transitions create multiple log entries."""
from django_fsm_log.models import StateLog
park = self._create_park(status='OPERATING')
park = self._create_park(status="OPERATING")
park_ct = ContentType.objects.get_for_model(park)
# First transition
@@ -332,29 +313,23 @@ class ParkTransitionHistoryTests(TestCase):
park.transition_to_operating(user=self.moderator)
park.save()
logs = StateLog.objects.filter(
content_type=park_ct,
object_id=park.id
).order_by('timestamp')
logs = StateLog.objects.filter(content_type=park_ct, object_id=park.id).order_by("timestamp")
self.assertEqual(logs.count(), 2)
self.assertEqual(logs[0].state, 'CLOSED_TEMP')
self.assertEqual(logs[1].state, 'OPERATING')
self.assertEqual(logs[0].state, "CLOSED_TEMP")
self.assertEqual(logs[1].state, "OPERATING")
def test_transition_log_includes_user(self):
"""Test that transition logs include the user who made the change."""
from django_fsm_log.models import StateLog
park = self._create_park(status='OPERATING')
park = self._create_park(status="OPERATING")
park.transition_to_closed_perm(user=self.moderator)
park.save()
park_ct = ContentType.objects.get_for_model(park)
log = StateLog.objects.filter(
content_type=park_ct,
object_id=park.id
).first()
log = StateLog.objects.filter(content_type=park_ct, object_id=park.id).first()
self.assertEqual(log.by, self.moderator)
@@ -370,24 +345,20 @@ class ParkBusinessLogicTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.operator = Company.objects.create(
name='Test Operator',
description='Test operator company',
roles=['OPERATOR']
name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
self.property_owner = Company.objects.create(
name='Property Owner',
description='Property owner company',
roles=['PROPERTY_OWNER']
name="Property Owner", description="Property owner company", roles=["PROPERTY_OWNER"]
)
def test_park_creates_with_valid_operator(self):
"""Test park can be created with valid operator."""
park = Park.objects.create(
name='Test Park',
slug='test-park',
description='A test park',
name="Test Park",
slug="test-park",
description="A test park",
operator=self.operator,
timezone='America/New_York'
timezone="America/New_York",
)
self.assertEqual(park.operator, self.operator)
@@ -395,35 +366,32 @@ class ParkBusinessLogicTests(TestCase):
def test_park_slug_auto_generated(self):
"""Test that park slug is auto-generated from name."""
park = Park.objects.create(
name='My Amazing Theme Park',
description='A test park',
operator=self.operator,
timezone='America/New_York'
name="My Amazing Theme Park", description="A test park", operator=self.operator, timezone="America/New_York"
)
self.assertEqual(park.slug, 'my-amazing-theme-park')
self.assertEqual(park.slug, "my-amazing-theme-park")
def test_park_url_generated(self):
"""Test that frontend URL is generated on save."""
park = Park.objects.create(
name='Test Park',
slug='test-park',
description='A test park',
name="Test Park",
slug="test-park",
description="A test park",
operator=self.operator,
timezone='America/New_York'
timezone="America/New_York",
)
self.assertIn('test-park', park.url)
self.assertIn("test-park", park.url)
def test_opening_year_computed_from_opening_date(self):
"""Test that opening_year is computed from opening_date."""
park = Park.objects.create(
name='Test Park',
slug='test-park',
description='A test park',
name="Test Park",
slug="test-park",
description="A test park",
operator=self.operator,
opening_date=date(2020, 6, 15),
timezone='America/New_York'
timezone="America/New_York",
)
self.assertEqual(park.opening_year, 2020)
@@ -431,26 +399,26 @@ class ParkBusinessLogicTests(TestCase):
def test_search_text_populated(self):
"""Test that search_text is populated on save."""
park = Park.objects.create(
name='Test Park',
slug='test-park',
description='A wonderful theme park',
name="Test Park",
slug="test-park",
description="A wonderful theme park",
operator=self.operator,
timezone='America/New_York'
timezone="America/New_York",
)
self.assertIn('test park', park.search_text)
self.assertIn('wonderful theme park', park.search_text)
self.assertIn('test operator', park.search_text)
self.assertIn("test park", park.search_text)
self.assertIn("wonderful theme park", park.search_text)
self.assertIn("test operator", park.search_text)
def test_park_with_property_owner(self):
"""Test park with separate property owner."""
park = Park.objects.create(
name='Test Park',
slug='test-park',
description='A test park',
name="Test Park",
slug="test-park",
description="A test park",
operator=self.operator,
property_owner=self.property_owner,
timezone='America/New_York'
timezone="America/New_York",
)
self.assertEqual(park.operator, self.operator)
@@ -468,9 +436,7 @@ class ParkSlugHistoryTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.operator = Company.objects.create(
name='Test Operator',
description='Test operator company',
roles=['OPERATOR']
name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
def test_historical_slug_created_on_name_change(self):
@@ -480,25 +446,18 @@ class ParkSlugHistoryTests(TestCase):
from apps.core.history import HistoricalSlug
park = Park.objects.create(
name='Original Name',
description='A test park',
operator=self.operator,
timezone='America/New_York'
name="Original Name", description="A test park", operator=self.operator, timezone="America/New_York"
)
original_slug = park.slug
# Change name
park.name = 'New Name'
park.name = "New Name"
park.save()
# Check historical slug was created
park_ct = ContentType.objects.get_for_model(park)
historical = HistoricalSlug.objects.filter(
content_type=park_ct,
object_id=park.id,
slug=original_slug
).first()
historical = HistoricalSlug.objects.filter(content_type=park_ct, object_id=park.id, slug=original_slug).first()
self.assertIsNotNone(historical)
self.assertEqual(historical.slug, original_slug)
@@ -506,14 +465,14 @@ class ParkSlugHistoryTests(TestCase):
def test_get_by_slug_finds_current_slug(self):
"""Test get_by_slug finds park by current slug."""
park = Park.objects.create(
name='Test Park',
slug='test-park',
description='A test park',
name="Test Park",
slug="test-park",
description="A test park",
operator=self.operator,
timezone='America/New_York'
timezone="America/New_York",
)
found_park, is_historical = Park.get_by_slug('test-park')
found_park, is_historical = Park.get_by_slug("test-park")
self.assertEqual(found_park, park)
self.assertFalse(is_historical)
@@ -522,16 +481,13 @@ class ParkSlugHistoryTests(TestCase):
"""Test get_by_slug finds park by historical slug."""
park = Park.objects.create(
name='Original Name',
description='A test park',
operator=self.operator,
timezone='America/New_York'
name="Original Name", description="A test park", operator=self.operator, timezone="America/New_York"
)
original_slug = park.slug
# Change name to create historical slug
park.name = 'New Name'
park.name = "New Name"
park.save()
# Find by historical slug