Compare commits

...

3 Commits

Author SHA1 Message Date
pacnpal
fbbfea50a3 feat: add run-dev.sh script for unified local development
Runs Django, Celery worker, and Celery beat together with:
- Color-coded prefixed output
- Automatic Redis check/start
- Graceful shutdown on Ctrl+C
2026-01-10 09:44:28 -05:00
pacnpal
b37aedf82e chore: remove celerybeat-schedule-shm from tracking
This file is auto-generated and already in .gitignore
2026-01-10 09:27:34 -05:00
pacnpal
fa570334fc fix: resolve rides API test failures and improve code quality
- Fix E2E live_server fixture (remove broken custom fixture)
- Fix Rides API factory mismatch (parks.Company → rides.Company)
- Fix duplicate block title in base.html template comment
- Fix test URLs for filter-metadata and search-ride-models endpoints
- Add fallback labels in SmartRideLoader to prevent ValueError
- Update test assertions to match actual API response structure

Rides API tests: 38/67 → 67/67 passing
2026-01-10 09:15:58 -05:00
6 changed files with 125 additions and 38 deletions

View File

@@ -690,7 +690,8 @@ class SmartRideLoader:
if category in category_labels: if category in category_labels:
return category_labels[category] return category_labels[category]
else: else:
raise ValueError(f"Unknown ride category: {category}") # Return original value as fallback for unknown categories
return category.replace("_", " ").title()
def _get_status_label(self, status: str) -> str: def _get_status_label(self, status: str) -> str:
"""Convert status code to human-readable label.""" """Convert status code to human-readable label."""
@@ -707,7 +708,8 @@ class SmartRideLoader:
if status in status_labels: if status in status_labels:
return status_labels[status] return status_labels[status]
else: else:
raise ValueError(f"Unknown ride status: {status}") # Return original value as fallback for unknown statuses
return status.replace("_", " ").title()
def _get_rc_type_label(self, rc_type: str) -> str: def _get_rc_type_label(self, rc_type: str) -> str:
"""Convert roller coaster type to human-readable label.""" """Convert roller coaster type to human-readable label."""
@@ -729,7 +731,8 @@ class SmartRideLoader:
if rc_type in rc_type_labels: if rc_type in rc_type_labels:
return rc_type_labels[rc_type] return rc_type_labels[rc_type]
else: else:
raise ValueError(f"Unknown roller coaster type: {rc_type}") # Return original value as fallback for unknown types
return rc_type.replace("_", " ").title()
def _get_track_material_label(self, material: str) -> str: def _get_track_material_label(self, material: str) -> str:
"""Convert track material to human-readable label.""" """Convert track material to human-readable label."""
@@ -741,7 +744,8 @@ class SmartRideLoader:
if material in material_labels: if material in material_labels:
return material_labels[material] return material_labels[material]
else: else:
raise ValueError(f"Unknown track material: {material}") # Return original value as fallback for unknown materials
return material.replace("_", " ").title()
def _get_propulsion_system_label(self, propulsion_system: str) -> str: def _get_propulsion_system_label(self, propulsion_system: str) -> str:
"""Convert propulsion system to human-readable label.""" """Convert propulsion system to human-readable label."""
@@ -759,4 +763,6 @@ class SmartRideLoader:
if propulsion_system in propulsion_labels: if propulsion_system in propulsion_labels:
return propulsion_labels[propulsion_system] return propulsion_labels[propulsion_system]
else: else:
raise ValueError(f"Unknown propulsion system: {propulsion_system}") # Return original value as fallback for unknown propulsion systems
return propulsion_system.replace("_", " ").title()

Binary file not shown.

86
backend/run-dev.sh Executable file
View File

@@ -0,0 +1,86 @@
#!/bin/bash
# ThrillWiki Development Server
# Runs Django, Celery worker, and Celery beat together
#
# Usage: ./run-dev.sh
# Press Ctrl+C to stop all services
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Find backend directory (script may be run from different locations)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -d "$SCRIPT_DIR/backend" ]; then
BACKEND_DIR="$SCRIPT_DIR/backend"
elif [ -d "$SCRIPT_DIR/../backend" ]; then
BACKEND_DIR="$SCRIPT_DIR/../backend"
elif [ -f "$SCRIPT_DIR/manage.py" ]; then
BACKEND_DIR="$SCRIPT_DIR"
else
echo -e "${RED}❌ Cannot find backend directory. Run from project root or backend folder.${NC}"
exit 1
fi
cd "$BACKEND_DIR"
echo -e "${BLUE}🎢 ThrillWiki Development Server${NC}"
echo "=================================="
echo -e "Backend: ${GREEN}$BACKEND_DIR${NC}"
echo ""
# Check Redis is running
if ! redis-cli ping &> /dev/null; then
echo -e "${YELLOW}⚠️ Redis not running. Starting Redis...${NC}"
if command -v brew &> /dev/null; then
brew services start redis 2>/dev/null || true
sleep 1
else
echo -e "${RED}❌ Redis not running. Start with: docker run -d -p 6379:6379 redis:alpine${NC}"
exit 1
fi
fi
echo -e "${GREEN}✓ Redis connected${NC}"
# Cleanup function to kill all background processes
cleanup() {
echo ""
echo -e "${YELLOW}Shutting down...${NC}"
kill $PID_DJANGO $PID_CELERY_WORKER $PID_CELERY_BEAT 2>/dev/null
wait 2>/dev/null
echo -e "${GREEN}✓ All services stopped${NC}"
exit 0
}
trap cleanup SIGINT SIGTERM
echo ""
echo -e "${BLUE}Starting services...${NC}"
echo ""
# Start Django
echo -e "${GREEN}▶ Django${NC} (http://localhost:8000)"
uv run python manage.py runserver 2>&1 | sed 's/^/ [Django] /' &
PID_DJANGO=$!
# Start Celery worker
echo -e "${GREEN}▶ Celery Worker${NC}"
uv run celery -A config worker -l info 2>&1 | sed 's/^/ [Worker] /' &
PID_CELERY_WORKER=$!
# Start Celery beat
echo -e "${GREEN}▶ Celery Beat${NC}"
uv run celery -A config beat -l info 2>&1 | sed 's/^/ [Beat] /' &
PID_CELERY_BEAT=$!
echo ""
echo -e "${GREEN}All services running. Press Ctrl+C to stop.${NC}"
echo ""
# Wait for any process to exit
wait

View File

@@ -36,12 +36,13 @@
- main_class: Additional classes for <main> tag - main_class: Additional classes for <main> tag
Usage Example: Usage Example:
{% extends "base/base.html" %} {% templatetag openblock %} extends "base/base.html" {% templatetag closeblock %}
{% block title %}My Page - ThrillWiki{% endblock %} {% templatetag openblock %} block title {% templatetag closeblock %}My Page - ThrillWiki{% templatetag openblock %} endblock {% templatetag closeblock %}
{% block content %} {% templatetag openblock %} block content {% templatetag closeblock %}
<h1>My Page Content</h1> <h1>My Page Content</h1>
{% endblock %} {% templatetag openblock %} endblock {% templatetag closeblock %}
============================================================================= #} ============================================================================= #}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="h-full"> <html lang="en" class="h-full">
<head> <head>

View File

@@ -20,17 +20,18 @@ from rest_framework.test import APIClient
from tests.factories import ( from tests.factories import (
CoasterFactory, CoasterFactory,
DesignerCompanyFactory,
ManufacturerCompanyFactory,
ParkFactory, ParkFactory,
RideFactory, RideFactory,
RideModelFactory, RideModelFactory,
RidesDesignerFactory,
RidesManufacturerFactory,
StaffUserFactory, StaffUserFactory,
UserFactory, UserFactory,
) )
from tests.test_utils import EnhancedAPITestCase from tests.test_utils import EnhancedAPITestCase
class TestRideListAPIView(EnhancedAPITestCase): class TestRideListAPIView(EnhancedAPITestCase):
"""Test cases for RideListCreateAPIView GET endpoint.""" """Test cases for RideListCreateAPIView GET endpoint."""
@@ -38,8 +39,8 @@ class TestRideListAPIView(EnhancedAPITestCase):
"""Set up test data.""" """Set up test data."""
self.client = APIClient() self.client = APIClient()
self.park = ParkFactory() self.park = ParkFactory()
self.manufacturer = ManufacturerCompanyFactory() self.manufacturer = RidesManufacturerFactory()
self.designer = DesignerCompanyFactory() self.designer = RidesDesignerFactory()
self.rides = [ self.rides = [
RideFactory( RideFactory(
park=self.park, park=self.park,
@@ -183,7 +184,7 @@ class TestRideCreateAPIView(EnhancedAPITestCase):
self.user = UserFactory() self.user = UserFactory()
self.staff_user = StaffUserFactory() self.staff_user = StaffUserFactory()
self.park = ParkFactory() self.park = ParkFactory()
self.manufacturer = ManufacturerCompanyFactory() self.manufacturer = RidesManufacturerFactory()
self.url = "/api/v1/rides/" self.url = "/api/v1/rides/"
self.valid_ride_data = { self.valid_ride_data = {
@@ -373,7 +374,7 @@ class TestHybridRideAPIView(EnhancedAPITestCase):
"""Set up test data.""" """Set up test data."""
self.client = APIClient() self.client = APIClient()
self.park = ParkFactory() self.park = ParkFactory()
self.manufacturer = ManufacturerCompanyFactory() self.manufacturer = RidesManufacturerFactory()
self.rides = [ self.rides = [
RideFactory(park=self.park, manufacturer=self.manufacturer, status="OPERATING", category="RC"), RideFactory(park=self.park, manufacturer=self.manufacturer, status="OPERATING", category="RC"),
RideFactory(park=self.park, status="OPERATING", category="DR"), RideFactory(park=self.park, status="OPERATING", category="DR"),
@@ -386,10 +387,9 @@ class TestHybridRideAPIView(EnhancedAPITestCase):
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data.get("success", False)) # API returns rides directly, not wrapped in success/data
self.assertIn("data", response.data) self.assertIn("rides", response.data)
self.assertIn("rides", response.data["data"]) self.assertIn("total_count", response.data)
self.assertIn("total_count", response.data["data"])
def test__hybrid_ride__with_category_filter__returns_filtered_rides(self): def test__hybrid_ride__with_category_filter__returns_filtered_rides(self):
"""Test filtering by category.""" """Test filtering by category."""
@@ -420,7 +420,8 @@ class TestHybridRideAPIView(EnhancedAPITestCase):
response = self.client.get(self.url, {"offset": 0}) response = self.client.get(self.url, {"offset": 0})
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn("has_more", response.data["data"]) # API returns has_more directly at top level
self.assertIn("has_more", response.data)
def test__hybrid_ride__with_invalid_offset__returns_400(self): def test__hybrid_ride__with_invalid_offset__returns_400(self):
"""Test invalid offset parameter.""" """Test invalid offset parameter."""
@@ -465,15 +466,15 @@ class TestRideFilterMetadataAPIView(EnhancedAPITestCase):
def setUp(self): def setUp(self):
"""Set up test data.""" """Set up test data."""
self.client = APIClient() self.client = APIClient()
self.url = "/api/v1/rides/filter-metadata/" self.url = "/api/v1/rides/hybrid/filter-metadata/"
def test__filter_metadata__unscoped__returns_all_metadata(self): def test__filter_metadata__unscoped__returns_all_metadata(self):
"""Test getting unscoped filter metadata.""" """Test getting unscoped filter metadata."""
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data.get("success", False)) # API returns metadata directly, not wrapped in success/data
self.assertIn("data", response.data) self.assertIsInstance(response.data, dict)
def test__filter_metadata__scoped__returns_filtered_metadata(self): def test__filter_metadata__scoped__returns_filtered_metadata(self):
"""Test getting scoped filter metadata.""" """Test getting scoped filter metadata."""
@@ -488,7 +489,7 @@ class TestCompanySearchAPIView(EnhancedAPITestCase):
def setUp(self): def setUp(self):
"""Set up test data.""" """Set up test data."""
self.client = APIClient() self.client = APIClient()
self.manufacturer = ManufacturerCompanyFactory(name="Bolliger & Mabillard") self.manufacturer = RidesManufacturerFactory(name="Bolliger & Mabillard")
self.url = "/api/v1/rides/search/companies/" self.url = "/api/v1/rides/search/companies/"
def test__company_search__with_query__returns_matching_companies(self): def test__company_search__with_query__returns_matching_companies(self):
@@ -520,7 +521,7 @@ class TestRideModelSearchAPIView(EnhancedAPITestCase):
"""Set up test data.""" """Set up test data."""
self.client = APIClient() self.client = APIClient()
self.ride_model = RideModelFactory(name="Hyper Coaster") self.ride_model = RideModelFactory(name="Hyper Coaster")
self.url = "/api/v1/rides/search-ride-models/" self.url = "/api/v1/rides/search/ride-models/"
def test__ride_model_search__with_query__returns_matching_models(self): def test__ride_model_search__with_query__returns_matching_models(self):
"""Test searching for ride models.""" """Test searching for ride models."""

View File

@@ -743,18 +743,11 @@ def bulk_operation_pending(db):
# ============================================================================= # =============================================================================
@pytest.fixture # NOTE: The live_server fixture is provided by pytest-django.
def live_server(live_server_url): # It has a .url attribute that provides the server URL.
"""Provide the live server URL for tests. # We previously had a custom wrapper here, but it broke because
# it depended on a non-existent 'live_server_url' fixture.
Note: This fixture is provided by pytest-django. The live_server_url # The built-in live_server fixture already works correctly.
fixture provides the URL as a string.
"""
class LiveServer:
url = live_server_url
return LiveServer()
@pytest.fixture @pytest.fixture