Add operators and property owners functionality

- Implemented OperatorListView and OperatorDetailView for managing operators.
- Created corresponding templates for operator listing and detail views.
- Added PropertyOwnerListView and PropertyOwnerDetailView for managing property owners.
- Developed templates for property owner listing and detail views.
- Established relationships between parks and operators, and parks and property owners in the models.
- Created migrations to reflect the new relationships and fields in the database.
- Added admin interfaces for PropertyOwner management.
- Implemented tests for operators and property owners.
This commit is contained in:
pacnpal
2025-07-04 14:49:36 -04:00
parent 8360f3fd43
commit 751cd86a31
80 changed files with 2943 additions and 2358 deletions

View File

@@ -3,7 +3,7 @@ from django.utils.html import format_html
from .models import Park, ParkArea
class ParkAdmin(admin.ModelAdmin):
list_display = ('name', 'formatted_location', 'status', 'owner', 'created_at', 'updated_at')
list_display = ('name', 'formatted_location', 'status', 'operator', 'property_owner', 'created_at', 'updated_at')
list_filter = ('status',)
search_fields = ('name', 'description', 'location__name', 'location__city', 'location__country')
readonly_fields = ('created_at', 'updated_at')

View File

@@ -13,7 +13,7 @@ from django_filters import (
)
from .models import Park
from .querysets import get_base_park_queryset
from companies.models import Company
from operators.models import Operator
def validate_positive_integer(value):
"""Validate that a value is a positive integer"""
@@ -47,17 +47,17 @@ class ParkFilter(LocationFilterMixin, RatingFilterMixin, DateRangeFilterMixin, F
help_text=_("Filter parks by their current operating status")
)
# Owner filters with helpful descriptions
owner = ModelChoiceFilter(
field_name='owner',
queryset=Company.objects.all(),
empty_label=_('Any company'),
# Operator filters with helpful descriptions
operator = ModelChoiceFilter(
field_name='operator',
queryset=Operator.objects.all(),
empty_label=_('Any operator'),
label=_("Operating Company"),
help_text=_("Filter parks by their operating company")
)
has_owner = BooleanFilter(
method='filter_has_owner',
label=_("Company Status"),
has_operator = BooleanFilter(
method='filter_has_operator',
label=_("Operator Status"),
help_text=_("Show parks with or without an operating company")
)
@@ -113,9 +113,9 @@ class ParkFilter(LocationFilterMixin, RatingFilterMixin, DateRangeFilterMixin, F
return queryset.filter(query).distinct()
def filter_has_owner(self, queryset, name, value):
"""Filter parks based on whether they have an owner"""
return queryset.filter(owner__isnull=not value)
def filter_has_operator(self, queryset, name, value):
"""Filter parks based on whether they have an operator"""
return queryset.filter(operator__isnull=not value)
@property
def qs(self):

View File

@@ -24,7 +24,7 @@ class ParkAutocomplete(BaseAutocomplete):
"""Return search results with related data."""
return (get_base_park_queryset()
.filter(name__icontains=search)
.select_related('owner')
.select_related('operator', 'property_owner')
.order_by('name'))
def format_result(self, park):
@@ -117,7 +117,8 @@ class ParkForm(forms.ModelForm):
fields = [
"name",
"description",
"owner",
"operator",
"property_owner",
"status",
"opening_date",
"closing_date",
@@ -145,7 +146,12 @@ class ParkForm(forms.ModelForm):
"rows": 2,
}
),
"owner": forms.Select(
"operator": forms.Select(
attrs={
"class": "w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
),
"property_owner": forms.Select(
attrs={
"class": "w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}

View File

@@ -9,7 +9,8 @@ from django.core.files import File
import requests
from parks.models import Park
from rides.models import Ride, RollerCoasterStats
from companies.models import Company, Manufacturer
from operators.models import Operator
from manufacturers.models import Manufacturer
from reviews.models import Review
from media.models import Photo
from django.contrib.auth.models import Permission
@@ -85,7 +86,7 @@ class Command(BaseCommand):
User.objects.exclude(username='admin').delete() # Delete all users except admin
Park.objects.all().delete()
Ride.objects.all().delete()
Company.objects.all().delete()
Operator.objects.all().delete()
Manufacturer.objects.all().delete()
Review.objects.all().delete()
Photo.objects.all().delete()
@@ -167,7 +168,7 @@ class Command(BaseCommand):
]
for name in companies:
Company.objects.create(name=name)
Operator.objects.create(name=name)
self.stdout.write(f"Created company: {name}")
def create_manufacturers(self):
@@ -213,7 +214,7 @@ class Command(BaseCommand):
status=park_data["status"],
description=park_data["description"],
website=park_data["website"],
owner=Company.objects.get(name=park_data["owner"]),
operator=Operator.objects.get(name=park_data["owner"]),
size_acres=park_data["size_acres"],
# Add location fields
latitude=park_coords["latitude"],

View File

@@ -1,6 +1,6 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from companies.models import Company
from operators.models import Operator
from parks.models import Park, ParkArea
from location.models import Location
from django.contrib.contenttypes.models import ContentType
@@ -51,12 +51,12 @@ class Command(BaseCommand):
companies = {}
for company_data in companies_data:
company, created = Company.objects.get_or_create(
operator, created = Operator.objects.get_or_create(
name=company_data['name'],
defaults=company_data
)
companies[company.name] = company
self.stdout.write(f'{"Created" if created else "Found"} company: {company.name}')
companies[operator.name] = operator
self.stdout.write(f'{"Created" if created else "Found"} company: {operator.name}')
# Create parks with their locations
parks_data = [

View File

@@ -0,0 +1,111 @@
# Generated by Django 5.1.4 on 2025-07-04 15:26
import django.db.models.deletion
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("operators", "0001_initial"),
("parks", "0003_alter_park_id_alter_parkarea_id_and_more"),
("property_owners", "0001_initial"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="park",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="park",
name="update_update",
),
migrations.RemoveField(
model_name="park",
name="owner",
),
migrations.RemoveField(
model_name="parkevent",
name="owner",
),
migrations.AddField(
model_name="park",
name="operator",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="parks",
to="operators.operator",
),
),
migrations.AddField(
model_name="park",
name="property_owner",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="owned_parks",
to="property_owners.propertyowner",
),
),
migrations.AddField(
model_name="parkevent",
name="operator",
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="operators.operator",
),
),
migrations.AddField(
model_name="parkevent",
name="property_owner",
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="property_owners.propertyowner",
),
),
pgtrigger.migrations.AddTrigger(
model_name="park",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "parks_parkevent" ("average_rating", "closing_date", "coaster_count", "created_at", "description", "id", "name", "opening_date", "operating_season", "operator_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "property_owner_id", "ride_count", "size_acres", "slug", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."closing_date", NEW."coaster_count", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."operating_season", NEW."operator_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."property_owner_id", NEW."ride_count", NEW."size_acres", NEW."slug", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="INSERT",
pgid="pgtrigger_insert_insert_66883",
table="parks_park",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="park",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "parks_parkevent" ("average_rating", "closing_date", "coaster_count", "created_at", "description", "id", "name", "opening_date", "operating_season", "operator_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "property_owner_id", "ride_count", "size_acres", "slug", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."closing_date", NEW."coaster_count", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."operating_season", NEW."operator_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."property_owner_id", NEW."ride_count", NEW."size_acres", NEW."slug", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="UPDATE",
pgid="pgtrigger_update_update_19f56",
table="parks_park",
when="AFTER",
),
),
),
]

View File

@@ -7,7 +7,8 @@ from decimal import Decimal, ROUND_DOWN, InvalidOperation
from typing import Tuple, Optional, Any, TYPE_CHECKING
import pghistory
from companies.models import Company
from operators.models import Operator
from property_owners.models import PropertyOwner
from media.models import Photo
from history_tracking.models import TrackedModel
from location.models import Location
@@ -54,8 +55,11 @@ class Park(TrackedModel):
coaster_count = models.IntegerField(null=True, blank=True)
# Relationships
owner = models.ForeignKey(
Company, on_delete=models.SET_NULL, null=True, blank=True, related_name="parks"
operator = models.ForeignKey(
Operator, on_delete=models.SET_NULL, null=True, blank=True, related_name="parks"
)
property_owner = models.ForeignKey(
PropertyOwner, on_delete=models.SET_NULL, null=True, blank=True, related_name="owned_parks"
)
photos = GenericRelation(Photo, related_query_name="park")
areas: models.Manager['ParkArea'] # Type hint for reverse relation

View File

@@ -7,7 +7,7 @@ from django.contrib.gis.geos import Point
from django.http import HttpResponse
from typing import cast, Optional, Tuple
from .models import Park, ParkArea
from companies.models import Company
from operators.models import Operator
from location.models import Location
User = get_user_model()
@@ -38,7 +38,7 @@ class ParkModelTests(TestCase):
)
# Create test company
cls.company = Company.objects.create(
cls.operator = Operator.objects.create(
name='Test Company',
website='http://example.com'
)
@@ -46,7 +46,7 @@ class ParkModelTests(TestCase):
# Create test park
cls.park = Park.objects.create(
name='Test Park',
owner=cls.company,
owner=cls.operator,
status='OPERATING',
website='http://testpark.com'
)
@@ -57,7 +57,7 @@ class ParkModelTests(TestCase):
def test_park_creation(self) -> None:
"""Test park instance creation and field values"""
self.assertEqual(self.park.name, 'Test Park')
self.assertEqual(self.park.owner, self.company)
self.assertEqual(self.park.operator, self.operator)
self.assertEqual(self.park.status, 'OPERATING')
self.assertEqual(self.park.website, 'http://testpark.com')
self.assertTrue(self.park.slug)
@@ -92,7 +92,7 @@ class ParkModelTests(TestCase):
class ParkAreaTests(TestCase):
def setUp(self) -> None:
# Create test company
self.company = Company.objects.create(
self.operator = Operator.objects.create(
name='Test Company',
website='http://example.com'
)
@@ -100,7 +100,7 @@ class ParkAreaTests(TestCase):
# Create test park
self.park = Park.objects.create(
name='Test Park',
owner=self.company,
owner=self.operator,
status='OPERATING'
)
@@ -139,13 +139,13 @@ class ParkViewTests(TestCase):
email='test@example.com',
password='testpass123'
)
self.company = Company.objects.create(
self.operator = Operator.objects.create(
name='Test Company',
website='http://example.com'
)
self.park = Park.objects.create(
name='Test Park',
owner=self.company,
owner=self.operator,
status='OPERATING'
)
self.location = create_test_location(self.park)

View File

@@ -9,19 +9,19 @@ from datetime import date, timedelta
from parks.models import Park
from parks.filters import ParkFilter
from companies.models import Company
from operators.models import Operator
from location.models import Location
class ParkFilterTests(TestCase):
@classmethod
def setUpTestData(cls):
"""Set up test data for all filter tests"""
# Create companies
cls.company1 = Company.objects.create(
# Create operators
cls.operator1 = Operator.objects.create(
name="Thrilling Adventures Inc",
slug="thrilling-adventures"
)
cls.company2 = Company.objects.create(
cls.operator2 = Operator.objects.create(
name="Family Fun Corp",
slug="family-fun"
)
@@ -31,7 +31,7 @@ class ParkFilterTests(TestCase):
name="Thrilling Adventures Park",
description="A thrilling park with lots of roller coasters",
status="OPERATING",
owner=cls.company1,
operator=cls.operator1,
opening_date=date(2020, 1, 1),
size_acres=100,
ride_count=20,
@@ -55,7 +55,7 @@ class ParkFilterTests(TestCase):
name="Family Fun Park",
description="Family-friendly entertainment and attractions",
status="CLOSED_TEMP",
owner=cls.company2,
owner=cls.operator2,
opening_date=date(2015, 6, 15),
size_acres=50,
ride_count=15,
@@ -193,12 +193,12 @@ class ParkFilterTests(TestCase):
def test_company_filtering(self):
"""Test company/owner filtering"""
# Test specific company
queryset = ParkFilter(data={"owner": str(self.company1.id)}).qs
queryset = ParkFilter(data={"operator": str(self.operator1.id)}).qs
self.assertEqual(queryset.count(), 1)
self.assertIn(self.park1, queryset)
# Test other company
queryset = ParkFilter(data={"owner": str(self.company2.id)}).qs
queryset = ParkFilter(data={"operator": str(self.operator2.id)}).qs
self.assertEqual(queryset.count(), 1)
self.assertIn(self.park2, queryset)
@@ -218,7 +218,7 @@ class ParkFilterTests(TestCase):
self.assertEqual(queryset.count(), 3)
# Test invalid company ID
queryset = ParkFilter(data={"owner": "99999"}).qs
queryset = ParkFilter(data={"operator": "99999"}).qs
self.assertEqual(queryset.count(), 0)
def test_numeric_filtering(self):

View File

@@ -9,13 +9,13 @@ from django.utils import timezone
from datetime import date
from parks.models import Park, ParkArea
from companies.models import Company
from operators.models import Operator
from location.models import Location
class ParkModelTests(TestCase):
def setUp(self):
"""Set up test data"""
self.company = Company.objects.create(
self.operator = Operator.objects.create(
name="Test Company",
slug="test-company"
)
@@ -25,7 +25,7 @@ class ParkModelTests(TestCase):
name="Test Park",
description="A test park",
status="OPERATING",
owner=self.company
owner=self.operator
)
# Create location for the park
@@ -47,7 +47,7 @@ class ParkModelTests(TestCase):
self.assertEqual(self.park.name, "Test Park")
self.assertEqual(self.park.slug, "test-park")
self.assertEqual(self.park.status, "OPERATING")
self.assertEqual(self.park.owner, self.company)
self.assertEqual(self.park.operator, self.operator)
def test_slug_generation(self):
"""Test automatic slug generation"""