Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX

This commit is contained in:
pacnpal
2025-12-22 16:56:27 -05:00
parent 2e35f8c5d9
commit ae31e889d7
144 changed files with 25792 additions and 4440 deletions

View File

@@ -0,0 +1,230 @@
"""
Integration tests for FSM (Finite State Machine) transition workflows.
These tests verify the complete state transition workflows for
Parks and Rides using the FSM implementation.
"""
import pytest
from datetime import date, timedelta
from django.test import TestCase
from django.core.exceptions import ValidationError
from apps.parks.models import Park
from apps.rides.models import Ride
from tests.factories import (
ParkFactory,
RideFactory,
UserFactory,
ParkAreaFactory,
)
@pytest.mark.django_db
class TestParkFSMTransitions(TestCase):
"""Integration tests for Park FSM transitions."""
def test__park_operating_to_closed_temp__transition_succeeds(self):
"""Test transitioning operating park to temporarily closed."""
park = ParkFactory(status="OPERATING")
user = UserFactory()
park.close_temporarily(user=user)
assert park.status == "CLOSED_TEMP"
def test__park_closed_temp_to_operating__transition_succeeds(self):
"""Test reopening temporarily closed park."""
park = ParkFactory(status="CLOSED_TEMP")
user = UserFactory()
park.open(user=user)
assert park.status == "OPERATING"
def test__park_operating_to_closed_perm__transition_succeeds(self):
"""Test closing operating park permanently."""
park = ParkFactory(status="OPERATING")
user = UserFactory()
park.close_permanently(user=user)
assert park.status == "CLOSED_PERM"
def test__park_closed_perm_to_operating__transition_not_allowed(self):
"""Test permanently closed park cannot reopen."""
park = ParkFactory(status="CLOSED_PERM")
user = UserFactory()
# This should fail - can't reopen permanently closed park
with pytest.raises(Exception):
park.open(user=user)
@pytest.mark.django_db
class TestRideFSMTransitions(TestCase):
"""Integration tests for Ride FSM transitions."""
def test__ride_operating_to_closed_temp__transition_succeeds(self):
"""Test transitioning operating ride to temporarily closed."""
ride = RideFactory(status="OPERATING")
user = UserFactory()
ride.close_temporarily(user=user)
assert ride.status == "CLOSED_TEMP"
def test__ride_closed_temp_to_operating__transition_succeeds(self):
"""Test reopening temporarily closed ride."""
ride = RideFactory(status="CLOSED_TEMP")
user = UserFactory()
ride.open(user=user)
assert ride.status == "OPERATING"
def test__ride_operating_to_sbno__transition_succeeds(self):
"""Test transitioning operating ride to SBNO (Standing But Not Operating)."""
ride = RideFactory(status="OPERATING")
user = UserFactory()
ride.mark_sbno(user=user)
assert ride.status == "SBNO"
def test__ride_sbno_to_operating__transition_succeeds(self):
"""Test reopening SBNO ride."""
ride = RideFactory(status="SBNO")
user = UserFactory()
ride.open(user=user)
assert ride.status == "OPERATING"
def test__ride_operating_to_closing__with_date__transition_succeeds(self):
"""Test scheduling ride for closing."""
ride = RideFactory(status="OPERATING")
user = UserFactory()
closing_date = date.today() + timedelta(days=30)
ride.mark_closing(
closing_date=closing_date,
post_closing_status="DEMOLISHED",
user=user,
)
assert ride.status == "CLOSING"
assert ride.closing_date == closing_date
assert ride.post_closing_status == "DEMOLISHED"
def test__ride_closing_to_demolished__transition_succeeds(self):
"""Test transitioning closing ride to demolished."""
ride = RideFactory(status="CLOSING")
ride.post_closing_status = "DEMOLISHED"
ride.save()
user = UserFactory()
ride.demolish(user=user)
assert ride.status == "DEMOLISHED"
def test__ride_operating_to_relocated__transition_succeeds(self):
"""Test marking ride as relocated."""
ride = RideFactory(status="OPERATING")
user = UserFactory()
ride.relocate(user=user)
assert ride.status == "RELOCATED"
@pytest.mark.django_db
class TestRideRelocationWorkflow(TestCase):
"""Integration tests for ride relocation workflow."""
def test__relocate_ride__to_new_park__updates_park(self):
"""Test relocating ride to new park updates the park relationship."""
old_park = ParkFactory(name="Old Park")
new_park = ParkFactory(name="New Park")
ride = RideFactory(park=old_park, status="OPERATING")
user = UserFactory()
# Mark as relocated first
ride.relocate(user=user)
assert ride.status == "RELOCATED"
# Move to new park
ride.move_to_park(new_park, clear_park_area=True)
assert ride.park == new_park
assert ride.park_area is None # Cleared during relocation
def test__relocate_ride__clears_park_area(self):
"""Test relocating ride clears park area."""
park = ParkFactory()
area = ParkAreaFactory(park=park)
new_park = ParkFactory()
ride = RideFactory(park=park, park_area=area, status="OPERATING")
user = UserFactory()
ride.relocate(user=user)
ride.move_to_park(new_park, clear_park_area=True)
assert ride.park_area is None
@pytest.mark.django_db
class TestRideStatusTransitionHistory(TestCase):
"""Integration tests for ride status transition history."""
def test__multiple_transitions__records_status_since(self):
"""Test multiple transitions update status_since correctly."""
ride = RideFactory(status="OPERATING")
user = UserFactory()
# First transition
ride.close_temporarily(user=user)
first_status_since = ride.status_since
assert ride.status == "CLOSED_TEMP"
# Second transition
ride.open(user=user)
second_status_since = ride.status_since
assert ride.status == "OPERATING"
# status_since should be updated for new transition
assert second_status_since >= first_status_since
@pytest.mark.django_db
class TestParkRideCascadeStatus(TestCase):
"""Integration tests for park status affecting rides."""
def test__close_park__does_not_auto_close_rides(self):
"""Test closing park doesn't automatically close rides."""
park = ParkFactory(status="OPERATING")
ride = RideFactory(park=park, status="OPERATING")
user = UserFactory()
# Close the park
park.close_temporarily(user=user)
# Ride should still be operating (business decision)
ride.refresh_from_db()
assert ride.status == "OPERATING" # Rides keep their independent status
def test__reopen_park__allows_ride_operation(self):
"""Test reopening park allows rides to continue operating."""
park = ParkFactory(status="CLOSED_TEMP")
ride = RideFactory(park=park, status="OPERATING")
user = UserFactory()
# Reopen park
park.open(user=user)
assert park.status == "OPERATING"
ride.refresh_from_db()
assert ride.status == "OPERATING"

View File

@@ -0,0 +1,233 @@
"""
Integration tests for park creation workflow.
These tests verify the complete workflow of park creation including
validation, location creation, and related operations.
"""
import pytest
from django.test import TestCase, TransactionTestCase
from django.db import transaction
from apps.parks.models import Park, ParkArea, ParkReview
from apps.parks.forms import ParkForm
from tests.factories import (
ParkFactory,
ParkAreaFactory,
OperatorCompanyFactory,
UserFactory,
RideFactory,
)
@pytest.mark.django_db
class TestParkCreationWorkflow(TestCase):
"""Integration tests for complete park creation workflow."""
def test__create_park_with_form__valid_data__creates_park_and_location(self):
"""Test creating a park with form creates both park and location."""
operator = OperatorCompanyFactory()
data = {
"name": "New Test Park",
"operator": operator.pk,
"status": "OPERATING",
"latitude": "37.123456",
"longitude": "-122.654321",
"city": "San Francisco",
"state": "CA",
"country": "USA",
}
form = ParkForm(data=data)
if form.is_valid():
park = form.save()
# Verify park was created
assert park.pk is not None
assert park.name == "New Test Park"
assert park.operator == operator
# Verify location was created
if park.location.exists():
location = park.location.first()
assert location.city == "San Francisco"
assert location.country == "USA"
def test__create_park__with_areas__creates_complete_structure(self):
"""Test creating a park with areas creates complete structure."""
park = ParkFactory()
# Add areas
area1 = ParkAreaFactory(park=park, name="Main Entrance")
area2 = ParkAreaFactory(park=park, name="Thrill Zone")
area3 = ParkAreaFactory(park=park, name="Kids Area")
# Verify structure
assert park.areas.count() == 3
assert park.areas.filter(name="Main Entrance").exists()
assert park.areas.filter(name="Thrill Zone").exists()
assert park.areas.filter(name="Kids Area").exists()
def test__create_park__with_rides__updates_counts(self):
"""Test creating a park with rides updates ride counts."""
park = ParkFactory()
# Add rides
RideFactory(park=park, category="RC") # Roller coaster
RideFactory(park=park, category="RC") # Roller coaster
RideFactory(park=park, category="TR") # Thrill ride
RideFactory(park=park, category="DR") # Dark ride
# Verify ride counts
assert park.rides.count() == 4
assert park.rides.filter(category="RC").count() == 2
@pytest.mark.django_db
class TestParkUpdateWorkflow(TestCase):
"""Integration tests for park update workflow."""
def test__update_park__changes_status__updates_correctly(self):
"""Test updating park status updates correctly."""
park = ParkFactory(status="OPERATING")
# Update via FSM transition
park.close_temporarily()
park.refresh_from_db()
assert park.status == "CLOSED_TEMP"
def test__update_park_location__updates_location_record(self):
"""Test updating park location updates the location record."""
park = ParkFactory()
form_data = {
"name": park.name,
"operator": park.operator.pk,
"status": park.status,
"city": "New City",
"state": "NY",
"country": "USA",
}
form = ParkForm(instance=park, data=form_data)
if form.is_valid():
updated_park = form.save()
# Verify location was updated
if updated_park.location.exists():
location = updated_park.location.first()
assert location.city == "New City"
@pytest.mark.django_db
class TestParkReviewWorkflow(TestCase):
"""Integration tests for park review workflow."""
def test__add_review__updates_park_rating(self):
"""Test adding a review affects park's average rating."""
park = ParkFactory()
user1 = UserFactory()
user2 = UserFactory()
# Add reviews
from tests.factories import ParkReviewFactory
ParkReviewFactory(park=park, user=user1, rating=8, is_published=True)
ParkReviewFactory(park=park, user=user2, rating=10, is_published=True)
# Calculate average
avg = park.reviews.filter(is_published=True).values_list(
"rating", flat=True
)
calculated_avg = sum(avg) / len(avg)
assert calculated_avg == 9.0
def test__unpublish_review__excludes_from_rating(self):
"""Test unpublishing a review excludes it from rating calculation."""
park = ParkFactory()
user1 = UserFactory()
user2 = UserFactory()
from tests.factories import ParkReviewFactory
review1 = ParkReviewFactory(park=park, user=user1, rating=10, is_published=True)
review2 = ParkReviewFactory(park=park, user=user2, rating=2, is_published=True)
# Unpublish the low rating
review2.is_published = False
review2.save()
# Calculate average - should only include published reviews
published_reviews = park.reviews.filter(is_published=True)
assert published_reviews.count() == 1
assert published_reviews.first().rating == 10
@pytest.mark.django_db
class TestParkAreaRideWorkflow(TestCase):
"""Integration tests for park area and ride workflow."""
def test__add_ride_to_area__associates_correctly(self):
"""Test adding a ride to an area associates them correctly."""
park = ParkFactory()
area = ParkAreaFactory(park=park, name="Thrill Zone")
ride = RideFactory(park=park, park_area=area, name="Super Coaster")
assert ride.park_area == area
assert ride in area.rides.all()
def test__delete_area__handles_rides_correctly(self):
"""Test deleting an area handles associated rides."""
park = ParkFactory()
area = ParkAreaFactory(park=park)
ride = RideFactory(park=park, park_area=area)
ride_pk = ride.pk
# Delete area - ride should have park_area set to NULL
area.delete()
ride.refresh_from_db()
assert ride.park_area is None
assert ride.pk == ride_pk # Ride still exists
@pytest.mark.django_db
class TestParkOperatorWorkflow(TestCase):
"""Integration tests for park operator workflow."""
def test__change_operator__updates_park(self):
"""Test changing park operator updates the relationship."""
old_operator = OperatorCompanyFactory(name="Old Operator")
new_operator = OperatorCompanyFactory(name="New Operator")
park = ParkFactory(operator=old_operator)
# Change operator
park.operator = new_operator
park.save()
park.refresh_from_db()
assert park.operator == new_operator
assert park.operator.name == "New Operator"
def test__operator_with_multiple_parks__lists_all_parks(self):
"""Test operator with multiple parks lists all parks."""
operator = OperatorCompanyFactory()
park1 = ParkFactory(operator=operator, name="Park One")
park2 = ParkFactory(operator=operator, name="Park Two")
park3 = ParkFactory(operator=operator, name="Park Three")
# Verify operator's parks
operator_parks = operator.operated_parks.all()
assert operator_parks.count() == 3
assert park1 in operator_parks
assert park2 in operator_parks
assert park3 in operator_parks

View File

@@ -0,0 +1,224 @@
"""
Integration tests for photo upload workflow.
These tests verify the complete workflow of photo uploads including
validation, processing, and moderation.
"""
import pytest
from unittest.mock import Mock, patch
from django.test import TestCase
from django.core.files.uploadedfile import SimpleUploadedFile
from apps.parks.models import ParkPhoto
from apps.rides.models import RidePhoto
from apps.parks.services.media_service import ParkMediaService
from tests.factories import (
ParkFactory,
RideFactory,
ParkPhotoFactory,
RidePhotoFactory,
UserFactory,
StaffUserFactory,
)
@pytest.mark.django_db
class TestParkPhotoUploadWorkflow(TestCase):
"""Integration tests for park photo upload workflow."""
@patch("apps.parks.services.media_service.MediaService.validate_image_file")
@patch("apps.parks.services.media_service.MediaService.process_image")
@patch("apps.parks.services.media_service.MediaService.generate_default_caption")
@patch("apps.parks.services.media_service.MediaService.extract_exif_date")
def test__upload_photo__creates_pending_photo(
self, mock_exif, mock_caption, mock_process, mock_validate
):
"""Test uploading photo creates a pending photo."""
mock_validate.return_value = (True, None)
mock_process.return_value = Mock()
mock_caption.return_value = "Photo by testuser"
mock_exif.return_value = None
park = ParkFactory()
user = UserFactory()
image = SimpleUploadedFile("test.jpg", b"image data", content_type="image/jpeg")
photo = ParkMediaService.upload_photo(
park=park,
image_file=image,
user=user,
caption="Test photo",
auto_approve=False,
)
assert photo.is_approved is False
assert photo.uploaded_by == user
assert photo.park == park
@patch("apps.parks.services.media_service.MediaService.validate_image_file")
@patch("apps.parks.services.media_service.MediaService.process_image")
@patch("apps.parks.services.media_service.MediaService.generate_default_caption")
@patch("apps.parks.services.media_service.MediaService.extract_exif_date")
def test__upload_photo__auto_approve__creates_approved_photo(
self, mock_exif, mock_caption, mock_process, mock_validate
):
"""Test uploading photo with auto_approve creates approved photo."""
mock_validate.return_value = (True, None)
mock_process.return_value = Mock()
mock_caption.return_value = "Photo by testuser"
mock_exif.return_value = None
park = ParkFactory()
user = UserFactory()
image = SimpleUploadedFile("test.jpg", b"image data", content_type="image/jpeg")
photo = ParkMediaService.upload_photo(
park=park,
image_file=image,
user=user,
auto_approve=True,
)
assert photo.is_approved is True
@pytest.mark.django_db
class TestPhotoModerationWorkflow(TestCase):
"""Integration tests for photo moderation workflow."""
def test__approve_photo__marks_as_approved(self):
"""Test approving a photo marks it as approved."""
photo = ParkPhotoFactory(is_approved=False)
moderator = StaffUserFactory()
result = ParkMediaService.approve_photo(photo, moderator)
photo.refresh_from_db()
assert result is True
assert photo.is_approved is True
def test__bulk_approve_photos__approves_all(self):
"""Test bulk approving photos approves all photos."""
park = ParkFactory()
photos = [
ParkPhotoFactory(park=park, is_approved=False),
ParkPhotoFactory(park=park, is_approved=False),
ParkPhotoFactory(park=park, is_approved=False),
]
moderator = StaffUserFactory()
count = ParkMediaService.bulk_approve_photos(photos, moderator)
assert count == 3
for photo in photos:
photo.refresh_from_db()
assert photo.is_approved is True
@pytest.mark.django_db
class TestPrimaryPhotoWorkflow(TestCase):
"""Integration tests for primary photo workflow."""
def test__set_primary_photo__unsets_previous_primary(self):
"""Test setting primary photo unsets previous primary."""
park = ParkFactory()
old_primary = ParkPhotoFactory(park=park, is_primary=True)
new_primary = ParkPhotoFactory(park=park, is_primary=False)
result = ParkMediaService.set_primary_photo(park, new_primary)
old_primary.refresh_from_db()
new_primary.refresh_from_db()
assert result is True
assert old_primary.is_primary is False
assert new_primary.is_primary is True
def test__get_primary_photo__returns_correct_photo(self):
"""Test get_primary_photo returns the primary photo."""
park = ParkFactory()
ParkPhotoFactory(park=park, is_primary=False, is_approved=True)
primary = ParkPhotoFactory(park=park, is_primary=True, is_approved=True)
ParkPhotoFactory(park=park, is_primary=False, is_approved=True)
result = ParkMediaService.get_primary_photo(park)
assert result == primary
@pytest.mark.django_db
class TestPhotoStatsWorkflow(TestCase):
"""Integration tests for photo statistics workflow."""
def test__get_photo_stats__returns_accurate_counts(self):
"""Test get_photo_stats returns accurate statistics."""
park = ParkFactory()
# Create various photos
ParkPhotoFactory(park=park, is_approved=True)
ParkPhotoFactory(park=park, is_approved=True)
ParkPhotoFactory(park=park, is_approved=False)
ParkPhotoFactory(park=park, is_approved=True, is_primary=True)
stats = ParkMediaService.get_photo_stats(park)
assert stats["total_photos"] == 4
assert stats["approved_photos"] == 3
assert stats["pending_photos"] == 1
assert stats["has_primary"] is True
@pytest.mark.django_db
class TestPhotoDeleteWorkflow(TestCase):
"""Integration tests for photo deletion workflow."""
def test__delete_photo__removes_photo(self):
"""Test deleting a photo removes it from database."""
photo = ParkPhotoFactory()
photo_id = photo.pk
moderator = StaffUserFactory()
result = ParkMediaService.delete_photo(photo, moderator)
assert result is True
assert not ParkPhoto.objects.filter(pk=photo_id).exists()
def test__delete_primary_photo__removes_primary(self):
"""Test deleting primary photo removes primary status."""
park = ParkFactory()
primary = ParkPhotoFactory(park=park, is_primary=True)
moderator = StaffUserFactory()
ParkMediaService.delete_photo(primary, moderator)
# Park should no longer have a primary photo
result = ParkMediaService.get_primary_photo(park)
assert result is None
@pytest.mark.django_db
class TestRidePhotoWorkflow(TestCase):
"""Integration tests for ride photo workflow."""
def test__ride_photo__includes_park_info(self):
"""Test ride photo includes park information."""
ride = RideFactory()
photo = RidePhotoFactory(ride=ride)
# Photo should have access to park through ride
assert photo.ride.park is not None
assert photo.ride.park.name is not None
def test__ride_photo__different_types(self):
"""Test ride photos can have different types."""
ride = RideFactory()
exterior = RidePhotoFactory(ride=ride, photo_type="exterior")
queue = RidePhotoFactory(ride=ride, photo_type="queue")
onride = RidePhotoFactory(ride=ride, photo_type="onride")
assert ride.photos.filter(photo_type="exterior").count() == 1
assert ride.photos.filter(photo_type="queue").count() == 1
assert ride.photos.filter(photo_type="onride").count() == 1