Add migrations for ParkPhoto and RidePhoto models with associated events

- Created ParkPhoto and ParkPhotoEvent models in the parks app, including fields for image, caption, alt text, and relationships to the Park model.
- Implemented triggers for insert and update operations on ParkPhoto to log changes in ParkPhotoEvent.
- Created RidePhoto and RidePhotoEvent models in the rides app, with similar structure and functionality as ParkPhoto.
- Added fields for photo type in RidePhoto and implemented corresponding triggers for logging changes.
- Established necessary indexes and unique constraints for both models to ensure data integrity and optimize queries.
This commit is contained in:
pacnpal
2025-08-26 14:40:46 -04:00
parent 831be6a2ee
commit e4e36c7899
133 changed files with 1321 additions and 1001 deletions

View File

@@ -346,9 +346,9 @@ class RideForm(forms.ModelForm):
# editing
if self.instance and self.instance.pk:
if self.instance.manufacturer:
self.fields["manufacturer_search"].initial = (
self.instance.manufacturer.name
)
self.fields[
"manufacturer_search"
].initial = self.instance.manufacturer.name
self.fields["manufacturer"].initial = self.instance.manufacturer
if self.instance.designer:
self.fields["designer_search"].initial = self.instance.designer.name

View File

@@ -346,9 +346,9 @@ class RideForm(forms.ModelForm):
# editing
if self.instance and self.instance.pk:
if self.instance.manufacturer:
self.fields["manufacturer_search"].initial = (
self.instance.manufacturer.name
)
self.fields[
"manufacturer_search"
].initial = self.instance.manufacturer.name
self.fields["manufacturer"].initial = self.instance.manufacturer
if self.instance.designer:
self.fields["designer_search"].initial = self.instance.designer.name

View File

@@ -11,7 +11,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [

View File

@@ -6,7 +6,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("parks", "0003_add_business_constraints"),
("rides", "0001_initial"),

View File

@@ -6,7 +6,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("rides", "0002_add_business_constraints"),
]

View File

@@ -7,7 +7,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("parks", "0006_remove_company_insert_insert_and_more"),
("pghistory", "0007_auto_20250421_0444"),

View File

@@ -8,7 +8,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pghistory", "0007_auto_20250421_0444"),
("rides", "0004_rideevent_ridemodelevent_rollercoasterstatsevent_and_more"),

View File

@@ -9,7 +9,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pghistory", "0007_auto_20250421_0444"),
("rides", "0005_ridelocationevent_ridelocation_insert_insert_and_more"),

View File

@@ -0,0 +1,224 @@
# Generated by Django 5.2.5 on 2025-08-26 17:39
import apps.rides.models.media
import django.db.models.deletion
import pgtrigger.compiler
import pgtrigger.migrations
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pghistory", "0007_auto_20250421_0444"),
("rides", "0006_add_ride_rankings"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="RidePhoto",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"image",
models.ImageField(
max_length=255,
upload_to=apps.rides.models.media.ride_photo_upload_path,
),
),
("caption", models.CharField(blank=True, max_length=255)),
("alt_text", models.CharField(blank=True, max_length=255)),
("is_primary", models.BooleanField(default=False)),
("is_approved", models.BooleanField(default=False)),
(
"photo_type",
models.CharField(
choices=[
("exterior", "Exterior View"),
("queue", "Queue Area"),
("station", "Station"),
("onride", "On-Ride"),
("construction", "Construction"),
("other", "Other"),
],
default="exterior",
max_length=50,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("date_taken", models.DateTimeField(blank=True, null=True)),
(
"ride",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="photos",
to="rides.ride",
),
),
(
"uploaded_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="uploaded_ride_photos",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-is_primary", "-created_at"],
},
),
migrations.CreateModel(
name="RidePhotoEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),
(
"image",
models.ImageField(
max_length=255,
upload_to=apps.rides.models.media.ride_photo_upload_path,
),
),
("caption", models.CharField(blank=True, max_length=255)),
("alt_text", models.CharField(blank=True, max_length=255)),
("is_primary", models.BooleanField(default=False)),
("is_approved", models.BooleanField(default=False)),
(
"photo_type",
models.CharField(
choices=[
("exterior", "Exterior View"),
("queue", "Queue Area"),
("station", "Station"),
("onride", "On-Ride"),
("construction", "Construction"),
("other", "Other"),
],
default="exterior",
max_length=50,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("date_taken", models.DateTimeField(blank=True, null=True)),
(
"pgh_context",
models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="pghistory.context",
),
),
(
"pgh_obj",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="events",
to="rides.ridephoto",
),
),
(
"ride",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="rides.ride",
),
),
(
"uploaded_by",
models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
migrations.AddIndex(
model_name="ridephoto",
index=models.Index(
fields=["ride", "is_primary"], name="rides_ridep_ride_id_aa49f1_idx"
),
),
migrations.AddIndex(
model_name="ridephoto",
index=models.Index(
fields=["ride", "is_approved"], name="rides_ridep_ride_id_f1eddc_idx"
),
),
migrations.AddIndex(
model_name="ridephoto",
index=models.Index(
fields=["ride", "photo_type"], name="rides_ridep_ride_id_49e7ec_idx"
),
),
migrations.AddIndex(
model_name="ridephoto",
index=models.Index(
fields=["created_at"], name="rides_ridep_created_106e02_idx"
),
),
migrations.AddConstraint(
model_name="ridephoto",
constraint=models.UniqueConstraint(
condition=models.Q(("is_primary", True)),
fields=("ride",),
name="unique_primary_ride_photo",
),
),
pgtrigger.migrations.AddTrigger(
model_name="ridephoto",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "rides_ridephotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image", "is_approved", "is_primary", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo_type", "ride_id", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."created_at", NEW."date_taken", NEW."id", NEW."image", NEW."is_approved", NEW."is_primary", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."photo_type", NEW."ride_id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;',
hash="8027f17cac76b8301927e468ab4873ae9f38f27a",
operation="INSERT",
pgid="pgtrigger_insert_insert_0043a",
table="rides_ridephoto",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="ridephoto",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "rides_ridephotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image", "is_approved", "is_primary", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo_type", "ride_id", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."created_at", NEW."date_taken", NEW."id", NEW."image", NEW."is_approved", NEW."is_primary", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."photo_type", NEW."ride_id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;',
hash="54562f9a78754cac359f1efd5c0e8d6d144d1806",
operation="UPDATE",
pgid="pgtrigger_update_update_93a7e",
table="rides_ridephoto",
when="AFTER",
),
),
),
]

View File

@@ -7,6 +7,7 @@ enabling imports like: from rides.models import Ride, Manufacturer
The Company model is aliased as Manufacturer to clarify its role as ride manufacturers,
while maintaining backward compatibility through the Company alias.
"""
from .rides import Ride, RideModel, RollerCoasterStats, Categories, CATEGORY_CHOICES
from .location import RideLocation
from .reviews import RideReview

View File

@@ -7,7 +7,6 @@ This module contains media models specific to rides domain.
from typing import Any, Optional, cast
from django.db import models
from django.conf import settings
from django.utils import timezone
from apps.core.history import TrackedModel
from apps.core.services.media_service import MediaService
import pghistory
@@ -15,7 +14,7 @@ import pghistory
def ride_photo_upload_path(instance: models.Model, filename: str) -> str:
"""Generate upload path for ride photos."""
photo = cast('RidePhoto', instance)
photo = cast("RidePhoto", instance)
ride = photo.ride
if ride is None:
@@ -25,7 +24,7 @@ def ride_photo_upload_path(instance: models.Model, filename: str) -> str:
domain="park",
identifier=ride.slug,
filename=filename,
subdirectory=ride.park.slug
subdirectory=ride.park.slug,
)
@@ -34,9 +33,7 @@ class RidePhoto(TrackedModel):
"""Photo model specific to rides."""
ride = models.ForeignKey(
'rides.Ride',
on_delete=models.CASCADE,
related_name='photos'
"rides.Ride", on_delete=models.CASCADE, related_name="photos"
)
image = models.ImageField(
@@ -53,14 +50,14 @@ class RidePhoto(TrackedModel):
photo_type = models.CharField(
max_length=50,
choices=[
('exterior', 'Exterior View'),
('queue', 'Queue Area'),
('station', 'Station'),
('onride', 'On-Ride'),
('construction', 'Construction'),
('other', 'Other'),
("exterior", "Exterior View"),
("queue", "Queue Area"),
("station", "Station"),
("onride", "On-Ride"),
("construction", "Construction"),
("other", "Other"),
],
default='exterior'
default="exterior",
)
# Metadata
@@ -88,9 +85,9 @@ class RidePhoto(TrackedModel):
constraints = [
# Only one primary photo per ride
models.UniqueConstraint(
fields=['ride'],
fields=["ride"],
condition=models.Q(is_primary=True),
name='unique_primary_ride_photo'
name="unique_primary_ride_photo",
)
]

View File

@@ -1,7 +1,5 @@
from django.db import models
from django.utils.text import slugify
from django.contrib.contenttypes.fields import GenericRelation
from django.db.models import Avg
from apps.core.models import TrackedModel
from .company import Company
import pghistory
@@ -140,7 +138,6 @@ class Ride(TrackedModel):
average_rating = models.DecimalField(
max_digits=3, decimal_places=2, null=True, blank=True
)
photos = GenericRelation("media.Photo")
class Meta(TrackedModel.Meta):
ordering = ["name"]

View File

@@ -273,7 +273,6 @@ def ride_statistics_by_category() -> Dict[str, Any]:
Returns:
Dictionary containing ride statistics by category
"""
from .models import CATEGORY_CHOICES
stats = {}
for category_code, category_name in CATEGORY_CHOICES:

View File

@@ -5,8 +5,6 @@ Handles location management for individual rides within parks.
import requests
from typing import List, Dict, Any, Optional, Tuple
from django.conf import settings
from django.core.cache import cache
from django.db import transaction
import logging
@@ -317,7 +315,6 @@ class RideLocationService:
ride_location.ride.name.lower() in display_name
and park.name.lower() in display_name
):
# Update the ride location
ride_location.set_coordinates(
float(result["lat"]), float(result["lon"])

View File

@@ -28,7 +28,7 @@ class RideMediaService:
alt_text: str = "",
photo_type: str = "exterior",
is_primary: bool = False,
auto_approve: bool = False
auto_approve: bool = False,
) -> RidePhoto:
"""
Upload a photo for a ride.
@@ -67,7 +67,7 @@ class RideMediaService:
photo_type=photo_type,
is_primary=is_primary,
is_approved=auto_approve,
uploaded_by=user
uploaded_by=user,
)
# Extract EXIF date
@@ -83,7 +83,7 @@ class RideMediaService:
ride: Ride,
approved_only: bool = True,
primary_first: bool = True,
photo_type: Optional[str] = None
photo_type: Optional[str] = None,
) -> List[RidePhoto]:
"""
Get photos for a ride.
@@ -106,9 +106,9 @@ class RideMediaService:
queryset = queryset.filter(photo_type=photo_type)
if primary_first:
queryset = queryset.order_by('-is_primary', '-created_at')
queryset = queryset.order_by("-is_primary", "-created_at")
else:
queryset = queryset.order_by('-created_at')
queryset = queryset.order_by("-created_at")
return list(queryset)
@@ -141,10 +141,9 @@ class RideMediaService:
List of RidePhoto instances
"""
return list(
ride.photos.filter(
photo_type=photo_type,
is_approved=True
).order_by('-created_at')
ride.photos.filter(photo_type=photo_type, is_approved=True).order_by(
"-created_at"
)
)
@staticmethod
@@ -217,7 +216,8 @@ class RideMediaService:
photo.delete()
logger.info(
f"Photo {photo_id} deleted from ride {ride_slug} by user {deleted_by.username}")
f"Photo {photo_id} deleted from ride {ride_slug} by user {deleted_by.username}"
)
return True
except Exception as e:
logger.error(f"Failed to delete photo {photo.pk}: {str(e)}")
@@ -238,7 +238,7 @@ class RideMediaService:
# Get counts by photo type
type_counts = {}
for photo_type, _ in RidePhoto._meta.get_field('photo_type').choices:
for photo_type, _ in RidePhoto._meta.get_field("photo_type").choices:
type_counts[photo_type] = photos.filter(photo_type=photo_type).count()
return {
@@ -246,8 +246,8 @@ class RideMediaService:
"approved_photos": photos.filter(is_approved=True).count(),
"pending_photos": photos.filter(is_approved=False).count(),
"has_primary": photos.filter(is_primary=True).exists(),
"recent_uploads": photos.order_by('-created_at')[:5].count(),
"by_type": type_counts
"recent_uploads": photos.order_by("-created_at")[:5].count(),
"by_type": type_counts,
}
@staticmethod
@@ -270,7 +270,8 @@ class RideMediaService:
approved_count += 1
logger.info(
f"Bulk approved {approved_count} photos by user {approved_by.username}")
f"Bulk approved {approved_count} photos by user {approved_by.username}"
)
return approved_count
@staticmethod
@@ -285,10 +286,9 @@ class RideMediaService:
List of construction RidePhoto instances ordered by date taken
"""
return list(
ride.photos.filter(
photo_type='construction',
is_approved=True
).order_by('date_taken', 'created_at')
ride.photos.filter(photo_type="construction", is_approved=True).order_by(
"date_taken", "created_at"
)
)
@staticmethod
@@ -302,4 +302,4 @@ class RideMediaService:
Returns:
List of on-ride RidePhoto instances
"""
return RideMediaService.get_photos_by_type(ride, 'onride')
return RideMediaService.get_photos_by_type(ride, "onride")

View File

@@ -12,7 +12,7 @@ from decimal import Decimal
from datetime import date
from django.db import transaction
from django.db.models import Avg, Count, Q, F
from django.db.models import Avg, Count, Q
from django.utils import timezone
from apps.rides.models import (

View File

@@ -1,4 +1,4 @@
from django.urls import path, include
from django.urls import path
from . import views
app_name = "rides"