mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:31:07 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0003_add_business_constraints"),
|
||||
("rides", "0001_initial"),
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("rides", "0002_add_business_constraints"),
|
||||
]
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.urls import path, include
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = "rides"
|
||||
|
||||
Reference in New Issue
Block a user