diff --git a/backend/apps/api/v1/parks/park_views.py b/backend/apps/api/v1/parks/park_views.py index 31c4903e..e2a01af9 100644 --- a/backend/apps/api/v1/parks/park_views.py +++ b/backend/apps/api/v1/parks/park_views.py @@ -240,10 +240,15 @@ class ParkListCreateAPIView(APIView): if city: qs = qs.filter(location__city__iexact=city) - # NOTE: continent and park_type filters are not implemented because - # these fields don't exist in the current Django models: - # - ParkLocation model has no 'continent' field - # - Park model has no 'park_type' field + # Continent filter (now available field) + continent = params.get("continent") + if continent: + qs = qs.filter(location__continent__iexact=continent) + + # Park type filter (now available field) + park_type = params.get("park_type") + if park_type: + qs = qs.filter(park_type=park_type) # Status filter (available field) status_filter = params.get("status") @@ -559,16 +564,24 @@ class FilterOptionsAPIView(APIView): # Try to get dynamic options from database try: - # NOTE: continent field doesn't exist in ParkLocation model, so we use static list - continents = [ - "North America", - "South America", - "Europe", - "Asia", - "Africa", - "Australia", - "Antarctica" - ] + # Get continents from database (now available field) + continents = list(Park.objects.exclude( + location__continent__isnull=True + ).exclude( + location__continent__exact='' + ).values_list('location__continent', flat=True).distinct().order_by('location__continent')) + + # Fallback to static list if no continents in database + if not continents: + continents = [ + "North America", + "South America", + "Europe", + "Asia", + "Africa", + "Australia", + "Antarctica" + ] countries = list(Park.objects.exclude( location__country__isnull=True @@ -582,22 +595,11 @@ class FilterOptionsAPIView(APIView): location__state__exact='' ).values_list('location__state', flat=True).distinct().order_by('location__state')) - # Try to use ModelChoices if available - if HAVE_MODELCHOICES and ModelChoices is not None: - try: - park_types = ModelChoices.get_park_type_choices() - except Exception: - park_types = [ - {"value": "THEME_PARK", "label": "Theme Park"}, - {"value": "AMUSEMENT_PARK", "label": "Amusement Park"}, - {"value": "WATER_PARK", "label": "Water Park"}, - ] - else: - park_types = [ - {"value": "THEME_PARK", "label": "Theme Park"}, - {"value": "AMUSEMENT_PARK", "label": "Amusement Park"}, - {"value": "WATER_PARK", "label": "Water Park"}, - ] + # Get park types from model choices (now available field) + park_types = [ + {"value": choice[0], "label": choice[1]} + for choice in Park.PARK_TYPE_CHOICES + ] return Response({ "park_types": park_types, diff --git a/backend/apps/parks/migrations/0013_remove_park_insert_insert_remove_park_update_update_and_more.py b/backend/apps/parks/migrations/0013_remove_park_insert_insert_remove_park_update_update_and_more.py new file mode 100644 index 00000000..22a7157f --- /dev/null +++ b/backend/apps/parks/migrations/0013_remove_park_insert_insert_remove_park_update_update_and_more.py @@ -0,0 +1,153 @@ +# Generated by Django 5.2.5 on 2025-08-31 01:46 + +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("parks", "0012_remove_parkphoto_insert_insert_and_more"), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name="park", + name="insert_insert", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="park", + name="update_update", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="parklocation", + name="insert_insert", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="parklocation", + name="update_update", + ), + migrations.AddField( + model_name="park", + name="park_type", + field=models.CharField( + choices=[ + ("THEME_PARK", "Theme Park"), + ("AMUSEMENT_PARK", "Amusement Park"), + ("WATER_PARK", "Water Park"), + ("FAMILY_ENTERTAINMENT_CENTER", "Family Entertainment Center"), + ("CARNIVAL", "Carnival"), + ("FAIR", "Fair"), + ("PIER", "Pier"), + ("BOARDWALK", "Boardwalk"), + ("SAFARI_PARK", "Safari Park"), + ("ZOO", "Zoo"), + ("OTHER", "Other"), + ], + db_index=True, + default="THEME_PARK", + help_text="Type/category of the park", + max_length=30, + ), + ), + migrations.AddField( + model_name="parkevent", + name="park_type", + field=models.CharField( + choices=[ + ("THEME_PARK", "Theme Park"), + ("AMUSEMENT_PARK", "Amusement Park"), + ("WATER_PARK", "Water Park"), + ("FAMILY_ENTERTAINMENT_CENTER", "Family Entertainment Center"), + ("CARNIVAL", "Carnival"), + ("FAIR", "Fair"), + ("PIER", "Pier"), + ("BOARDWALK", "Boardwalk"), + ("SAFARI_PARK", "Safari Park"), + ("ZOO", "Zoo"), + ("OTHER", "Other"), + ], + default="THEME_PARK", + help_text="Type/category of the park", + max_length=30, + ), + ), + migrations.AddField( + model_name="parklocation", + name="continent", + field=models.CharField( + blank=True, + db_index=True, + help_text="Continent where the park is located", + max_length=50, + ), + ), + migrations.AddField( + model_name="parklocationevent", + name="continent", + field=models.CharField( + blank=True, + help_text="Continent where the park is located", + max_length=50, + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="park", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "parks_parkevent" ("average_rating", "banner_image_id", "card_image_id", "closing_date", "coaster_count", "created_at", "description", "id", "name", "opening_date", "operating_season", "operator_id", "park_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "property_owner_id", "ride_count", "size_acres", "slug", "status", "updated_at", "url", "website") VALUES (NEW."average_rating", NEW."banner_image_id", NEW."card_image_id", NEW."closing_date", NEW."coaster_count", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."operating_season", NEW."operator_id", NEW."park_type", _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."url", NEW."website"); RETURN NULL;', + hash="406615e99a0bd58eadc2ad3023c988364c61f65a", + 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", "banner_image_id", "card_image_id", "closing_date", "coaster_count", "created_at", "description", "id", "name", "opening_date", "operating_season", "operator_id", "park_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "property_owner_id", "ride_count", "size_acres", "slug", "status", "updated_at", "url", "website") VALUES (NEW."average_rating", NEW."banner_image_id", NEW."card_image_id", NEW."closing_date", NEW."coaster_count", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."operating_season", NEW."operator_id", NEW."park_type", _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."url", NEW."website"); RETURN NULL;', + hash="ca8b337b9b1f1a937a4dd88cde8b31231d0fdff5", + operation="UPDATE", + pgid="pgtrigger_update_update_19f56", + table="parks_park", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="parklocation", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "parks_parklocationevent" ("best_arrival_time", "city", "continent", "country", "highway_exit", "id", "osm_id", "osm_type", "park_id", "parking_notes", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "postal_code", "seasonal_notes", "state", "street_address") VALUES (NEW."best_arrival_time", NEW."city", NEW."continent", NEW."country", NEW."highway_exit", NEW."id", NEW."osm_id", NEW."osm_type", NEW."park_id", NEW."parking_notes", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."point", NEW."postal_code", NEW."seasonal_notes", NEW."state", NEW."street_address"); RETURN NULL;', + hash="aecd083c917cea3170e944c73c4906a78eccd676", + operation="INSERT", + pgid="pgtrigger_insert_insert_f8c53", + table="parks_parklocation", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="parklocation", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "parks_parklocationevent" ("best_arrival_time", "city", "continent", "country", "highway_exit", "id", "osm_id", "osm_type", "park_id", "parking_notes", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "postal_code", "seasonal_notes", "state", "street_address") VALUES (NEW."best_arrival_time", NEW."city", NEW."continent", NEW."country", NEW."highway_exit", NEW."id", NEW."osm_id", NEW."osm_type", NEW."park_id", NEW."parking_notes", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."point", NEW."postal_code", NEW."seasonal_notes", NEW."state", NEW."street_address"); RETURN NULL;', + hash="a70bc26b34235fe4342009d491d80b990ee3ed7e", + operation="UPDATE", + pgid="pgtrigger_update_update_6dd0d", + table="parks_parklocation", + when="AFTER", + ), + ), + ), + ] diff --git a/backend/apps/parks/models/location.py b/backend/apps/parks/models/location.py index d4a3d30d..31509b6f 100644 --- a/backend/apps/parks/models/location.py +++ b/backend/apps/parks/models/location.py @@ -26,6 +26,12 @@ class ParkLocation(models.Model): city = models.CharField(max_length=100, db_index=True) state = models.CharField(max_length=100, db_index=True) country = models.CharField(max_length=100, default="USA") + continent = models.CharField( + max_length=50, + blank=True, + db_index=True, + help_text="Continent where the park is located" + ) postal_code = models.CharField(max_length=20, blank=True) # Road Trip Metadata diff --git a/backend/apps/parks/models/parks.py b/backend/apps/parks/models/parks.py index c1cd1b34..8e7e7927 100644 --- a/backend/apps/parks/models/parks.py +++ b/backend/apps/parks/models/parks.py @@ -36,6 +36,28 @@ class Park(TrackedModel): max_length=20, choices=STATUS_CHOICES, default="OPERATING" ) + PARK_TYPE_CHOICES = [ + ("THEME_PARK", "Theme Park"), + ("AMUSEMENT_PARK", "Amusement Park"), + ("WATER_PARK", "Water Park"), + ("FAMILY_ENTERTAINMENT_CENTER", "Family Entertainment Center"), + ("CARNIVAL", "Carnival"), + ("FAIR", "Fair"), + ("PIER", "Pier"), + ("BOARDWALK", "Boardwalk"), + ("SAFARI_PARK", "Safari Park"), + ("ZOO", "Zoo"), + ("OTHER", "Other"), + ] + + park_type = models.CharField( + max_length=30, + choices=PARK_TYPE_CHOICES, + default="THEME_PARK", + db_index=True, + help_text="Type/category of the park" + ) + # Location relationship - reverse relation from ParkLocation # location will be available via the 'location' related_name on # ParkLocation diff --git a/docs/frontend.md b/docs/frontend.md index 4781665f..eb39c717 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -212,13 +212,15 @@ The moderation system provides comprehensive content moderation, user management ### Parks Listing - **GET** `/api/v1/parks/` -- **Query Parameters** (22 filtering parameters supported by Django backend): +- **Query Parameters** (24 filtering parameters fully supported by Django backend): - `page` (int): Page number for pagination - `page_size` (int): Number of results per page - `search` (string): Search in park names and descriptions + - `continent` (string): Filter by continent - `country` (string): Filter by country - `state` (string): Filter by state/province - `city` (string): Filter by city + - `park_type` (string): Filter by park type (THEME_PARK, AMUSEMENT_PARK, WATER_PARK, etc.) - `status` (string): Filter by operational status - `operator_id` (int): Filter by operator company ID - `operator_slug` (string): Filter by operator company slug @@ -236,12 +238,6 @@ The moderation system provides comprehensive content moderation, user management - `max_roller_coaster_count` (int): Maximum roller coaster count - `ordering` (string): Order by fields (name, opening_date, ride_count, average_rating, coaster_count, etc.) -**⚠️ Note**: The following parameters are documented in the API schema but not currently implemented in the Django backend due to missing model fields: -- `continent` (string): ParkLocation model has no continent field -- `park_type` (string): Park model has no park_type field - -These parameters are accepted by the API but will be ignored until the corresponding model fields are added. - ### Filter Options - **GET** `/api/v1/parks/filter-options/` - **Returns**: Comprehensive filter options including continents, countries, states, park types, and ordering options diff --git a/docs/lib-api.ts b/docs/lib-api.ts index fff43278..368cc958 100644 --- a/docs/lib-api.ts +++ b/docs/lib-api.ts @@ -745,7 +745,6 @@ export const parksApi = { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { // Note: continent and park_type parameters are accepted but ignored by backend - // due to missing model fields (ParkLocation has no continent, Park has no park_type) searchParams.append(key, value.toString()); } }); diff --git a/docs/types-api.ts b/docs/types-api.ts index 28736194..82f1bd94 100644 --- a/docs/types-api.ts +++ b/docs/types-api.ts @@ -978,11 +978,8 @@ export interface ParkSearchFilters { min_roller_coaster_count?: number; max_roller_coaster_count?: number; ordering?: string; - - // Note: The following parameters are not currently supported by the backend - // due to missing model fields, but are kept for future compatibility: - continent?: string; // ParkLocation model has no continent field - park_type?: string; // Park model has no park_type field + continent?: string; + park_type?: string; } export interface ParkCompanySearchResult {