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 = [
("rides", "0007_ridephoto_ridephotoevent_and_more"),
]
operations = [
migrations.AlterField(
model_name="ridephoto",
name="image",
field=cloudflare_images.field.CloudflareImagesField(
help_text="Ride photo stored on Cloudflare Images",
upload_to="",
variant="public",
),
),
migrations.AlterField(
model_name="ridephotoevent",
name="image",
field=cloudflare_images.field.CloudflareImagesField(
help_text="Ride 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 = [
("rides", "0008_cloudflare_images_integration"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="ride",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="ride",
name="update_update",
),
migrations.AddField(
model_name="ride",
name="banner_image",
field=models.ForeignKey(
blank=True,
help_text="Photo to use as banner image for this ride",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="rides_using_as_banner",
to="rides.ridephoto",
),
),
migrations.AddField(
model_name="ride",
name="card_image",
field=models.ForeignKey(
blank=True,
help_text="Photo to use as card image for this ride",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="rides_using_as_card",
to="rides.ridephoto",
),
),
migrations.AddField(
model_name="rideevent",
name="banner_image",
field=models.ForeignKey(
blank=True,
db_constraint=False,
help_text="Photo to use as banner image for this ride",
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="rides.ridephoto",
),
),
migrations.AddField(
model_name="rideevent",
name="card_image",
field=models.ForeignKey(
blank=True,
db_constraint=False,
help_text="Photo to use as card image for this ride",
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="rides.ridephoto",
),
),
pgtrigger.migrations.AddTrigger(
model_name="ride",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "rides_rideevent" ("average_rating", "banner_image_id", "capacity_per_hour", "card_image_id", "category", "closing_date", "created_at", "description", "designer_id", "id", "manufacturer_id", "max_height_in", "min_height_in", "name", "opening_date", "park_area_id", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "post_closing_status", "ride_duration_seconds", "ride_model_id", "slug", "status", "status_since", "updated_at") VALUES (NEW."average_rating", NEW."banner_image_id", NEW."capacity_per_hour", NEW."card_image_id", NEW."category", NEW."closing_date", NEW."created_at", NEW."description", NEW."designer_id", NEW."id", NEW."manufacturer_id", NEW."max_height_in", NEW."min_height_in", NEW."name", NEW."opening_date", NEW."park_area_id", NEW."park_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."post_closing_status", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."slug", NEW."status", NEW."status_since", NEW."updated_at"); RETURN NULL;',
hash="462120d462bacf795e3e8d2d48e56a8adb85c63b",
operation="INSERT",
pgid="pgtrigger_insert_insert_52074",
table="rides_ride",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="ride",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "rides_rideevent" ("average_rating", "banner_image_id", "capacity_per_hour", "card_image_id", "category", "closing_date", "created_at", "description", "designer_id", "id", "manufacturer_id", "max_height_in", "min_height_in", "name", "opening_date", "park_area_id", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "post_closing_status", "ride_duration_seconds", "ride_model_id", "slug", "status", "status_since", "updated_at") VALUES (NEW."average_rating", NEW."banner_image_id", NEW."capacity_per_hour", NEW."card_image_id", NEW."category", NEW."closing_date", NEW."created_at", NEW."description", NEW."designer_id", NEW."id", NEW."manufacturer_id", NEW."max_height_in", NEW."min_height_in", NEW."name", NEW."opening_date", NEW."park_area_id", NEW."park_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."post_closing_status", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."slug", NEW."status", NEW."status_since", NEW."updated_at"); RETURN NULL;',
hash="dc36bcf1b24242b781d63799024095b0f8da79b6",
operation="UPDATE",
pgid="pgtrigger_update_update_4917a",
table="rides_ride",
when="AFTER",
),
),
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
# Generated by Django 5.2.5 on 2025-08-28 19:10
from django.db import migrations
from django.utils.text import slugify
def populate_ride_model_slugs(apps, schema_editor):
"""Populate unique slugs for existing RideModel records."""
RideModel = apps.get_model('rides', 'RideModel')
Company = apps.get_model('rides', 'Company')
for ride_model in RideModel.objects.all():
# Generate base slug from manufacturer name + model name
if ride_model.manufacturer:
base_slug = slugify(f"{ride_model.manufacturer.name} {ride_model.name}")
else:
base_slug = slugify(ride_model.name)
# Ensure uniqueness
slug = base_slug
counter = 1
while RideModel.objects.filter(slug=slug).exclude(pk=ride_model.pk).exists():
slug = f"{base_slug}-{counter}"
counter += 1
# Update the slug
ride_model.slug = slug
ride_model.save(update_fields=['slug'])
def reverse_populate_ride_model_slugs(apps, schema_editor):
"""Reverse operation - clear slugs (not really needed but for completeness)."""
RideModel = apps.get_model('rides', 'RideModel')
RideModel.objects.all().update(slug='')
class Migration(migrations.Migration):
dependencies = [
("rides", "0010_add_comprehensive_ride_model_system"),
]
operations = [
migrations.RunPython(
populate_ride_model_slugs,
reverse_populate_ride_model_slugs,
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.2.5 on 2025-08-28 19:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("rides", "0011_populate_ride_model_slugs"),
]
operations = [
migrations.AlterField(
model_name="ridemodel",
name="slug",
field=models.SlugField(
help_text="URL-friendly identifier", max_length=255, unique=True
),
),
]