feat(rides): populate slugs for existing RideModel records and ensure uniqueness

- Added migration 0011 to populate unique slugs for existing RideModel records based on manufacturer and model names.
- Implemented logic to ensure slug uniqueness during population.
- Added reverse migration to clear slugs if needed.

feat(rides): enforce unique slugs for RideModel

- Created migration 0012 to alter the slug field in RideModel to be unique.
- Updated the slug field to include help text and a maximum length of 255 characters.

docs: integrate Cloudflare Images into rides and parks models

- Updated RidePhoto and ParkPhoto models to use CloudflareImagesField for image storage.
- Enhanced API serializers for rides and parks to support Cloudflare Images, including new fields for image URLs and variants.
- Provided comprehensive OpenAPI schema metadata for new fields.
- Documented database migrations for the integration.
- Detailed configuration settings for Cloudflare Images.
- Updated API response formats to include Cloudflare Images URLs and variants.
- Added examples for uploading photos via API and outlined testing procedures.
This commit is contained in:
pacnpal
2025-08-28 15:12:39 -04:00
parent 715e284b3e
commit 67db0aa46e
34 changed files with 6002 additions and 894 deletions

View File

@@ -0,0 +1,32 @@
# Generated by Django 5.2.5 on 2025-08-28 18:17
import cloudflare_images.field
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("parks", "0008_parkphoto_parkphotoevent_and_more"),
]
operations = [
migrations.AlterField(
model_name="parkphoto",
name="image",
field=cloudflare_images.field.CloudflareImagesField(
help_text="Park photo stored on Cloudflare Images",
upload_to="",
variant="public",
),
),
migrations.AlterField(
model_name="parkphotoevent",
name="image",
field=cloudflare_images.field.CloudflareImagesField(
help_text="Park photo stored on Cloudflare Images",
upload_to="",
variant="public",
),
),
]

View File

@@ -0,0 +1,105 @@
# Generated by Django 5.2.5 on 2025-08-28 18:35
import django.db.models.deletion
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("parks", "0009_cloudflare_images_integration"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="park",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="park",
name="update_update",
),
migrations.AddField(
model_name="park",
name="banner_image",
field=models.ForeignKey(
blank=True,
help_text="Photo to use as banner image for this park",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="parks_using_as_banner",
to="parks.parkphoto",
),
),
migrations.AddField(
model_name="park",
name="card_image",
field=models.ForeignKey(
blank=True,
help_text="Photo to use as card image for this park",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="parks_using_as_card",
to="parks.parkphoto",
),
),
migrations.AddField(
model_name="parkevent",
name="banner_image",
field=models.ForeignKey(
blank=True,
db_constraint=False,
help_text="Photo to use as banner image for this park",
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="parks.parkphoto",
),
),
migrations.AddField(
model_name="parkevent",
name="card_image",
field=models.ForeignKey(
blank=True,
db_constraint=False,
help_text="Photo to use as card image for this park",
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="parks.parkphoto",
),
),
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", "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."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", _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="291a6e8efb89a33ee43bff05f44598a7814a05f0",
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", "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."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", _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="a689acf5a74ebd3aa7ad333881edb99778185da2",
operation="UPDATE",
pgid="pgtrigger_update_update_19f56",
table="parks_park",
when="AFTER",
),
),
),
]

View File

@@ -9,6 +9,7 @@ from django.db import models
from django.conf import settings
from apps.core.history import TrackedModel
from apps.core.services.media_service import MediaService
from cloudflare_images.field import CloudflareImagesField
import pghistory
@@ -33,9 +34,9 @@ class ParkPhoto(TrackedModel):
"parks.Park", on_delete=models.CASCADE, related_name="photos"
)
image = models.ImageField(
upload_to=park_photo_upload_path,
max_length=255,
image = CloudflareImagesField(
variant="public",
help_text="Park photo stored on Cloudflare Images"
)
caption = models.CharField(max_length=255, blank=True)
@@ -56,7 +57,7 @@ class ParkPhoto(TrackedModel):
related_name="uploaded_park_photos",
)
class Meta:
class Meta(TrackedModel.Meta):
app_label = "parks"
ordering = ["-is_primary", "-created_at"]
indexes = [

View File

@@ -54,6 +54,24 @@ class Park(TrackedModel):
ride_count = models.IntegerField(null=True, blank=True)
coaster_count = models.IntegerField(null=True, blank=True)
# Image settings - references to existing photos
banner_image = models.ForeignKey(
"ParkPhoto",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="parks_using_as_banner",
help_text="Photo to use as banner image for this park"
)
card_image = models.ForeignKey(
"ParkPhoto",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="parks_using_as_card",
help_text="Photo to use as card image for this park"
)
# Relationships
operator = models.ForeignKey(
"Company",