mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 09:31:09 -05:00
feat: Implement avatar upload system with Cloudflare integration
- Added migration to transition avatar data from CloudflareImageField to ForeignKey structure in UserProfile. - Fixed UserProfileEvent avatar field to align with new avatar structure. - Created serializers for social authentication, including connected and available providers. - Developed request logging middleware for comprehensive request/response logging. - Updated moderation and parks migrations to remove outdated triggers and adjust foreign key relationships. - Enhanced rides migrations to ensure proper handling of image uploads and triggers. - Introduced a test script for the 3-step avatar upload process, ensuring functionality with Cloudflare. - Documented the fix for avatar upload issues, detailing root cause, implementation, and verification steps. - Implemented automatic deletion of Cloudflare images upon avatar, park, and ride photo changes or removals.
This commit is contained in:
@@ -1,32 +0,0 @@
|
||||
# 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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -9,7 +9,7 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("rides", "0008_cloudflare_images_integration"),
|
||||
("rides", "0007_ridephoto_ridephotoevent_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
@@ -7,7 +7,7 @@ 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")
|
||||
apps.get_model("rides", "Company")
|
||||
|
||||
for ride_model in RideModel.objects.all():
|
||||
# Generate base slug from manufacturer name + model name
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# Generated by Django 5.2.5 on 2025-08-30 21:41
|
||||
|
||||
import django.db.models.deletion
|
||||
import pgtrigger.compiler
|
||||
import pgtrigger.migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_cloudflareimages_toolkit", "0001_initial"),
|
||||
("rides", "0016_remove_ride_insert_insert_remove_ride_update_update_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="ridemodelphoto",
|
||||
name="insert_insert",
|
||||
),
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="ridemodelphoto",
|
||||
name="update_update",
|
||||
),
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="ridephoto",
|
||||
name="insert_insert",
|
||||
),
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="ridephoto",
|
||||
name="update_update",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphoto",
|
||||
name="image",
|
||||
field=models.ForeignKey(
|
||||
help_text="Photo of the ride model stored on Cloudflare Images",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="django_cloudflareimages_toolkit.cloudflareimage",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
name="image",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Photo of the ride model stored on Cloudflare Images",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="django_cloudflareimages_toolkit.cloudflareimage",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridephoto",
|
||||
name="image",
|
||||
field=models.ForeignKey(
|
||||
help_text="Ride photo stored on Cloudflare Images",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="django_cloudflareimages_toolkit.cloudflareimage",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridephotoevent",
|
||||
name="image",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Ride photo stored on Cloudflare Images",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="django_cloudflareimages_toolkit.cloudflareimage",
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="ridemodelphoto",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="insert_insert",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func='INSERT INTO "rides_ridemodelphotoevent" ("alt_text", "caption", "copyright_info", "created_at", "id", "image_id", "is_primary", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo_type", "photographer", "ride_model_id", "source", "updated_at") VALUES (NEW."alt_text", NEW."caption", NEW."copyright_info", NEW."created_at", NEW."id", NEW."image_id", NEW."is_primary", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."photo_type", NEW."photographer", NEW."ride_model_id", NEW."source", NEW."updated_at"); RETURN NULL;',
|
||||
hash="fa289c31e25da0c08740d9e9c4072f3e4df81c42",
|
||||
operation="INSERT",
|
||||
pgid="pgtrigger_insert_insert_c5e58",
|
||||
table="rides_ridemodelphoto",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="ridemodelphoto",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="update_update",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
|
||||
func='INSERT INTO "rides_ridemodelphotoevent" ("alt_text", "caption", "copyright_info", "created_at", "id", "image_id", "is_primary", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo_type", "photographer", "ride_model_id", "source", "updated_at") VALUES (NEW."alt_text", NEW."caption", NEW."copyright_info", NEW."created_at", NEW."id", NEW."image_id", NEW."is_primary", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."photo_type", NEW."photographer", NEW."ride_model_id", NEW."source", NEW."updated_at"); RETURN NULL;',
|
||||
hash="1ead1d3fd3dd553f585ae76aa6f3215314322ff4",
|
||||
operation="UPDATE",
|
||||
pgid="pgtrigger_update_update_3afcd",
|
||||
table="rides_ridemodelphoto",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
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_id", "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_id", 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="51487ac871d9d90c75f695f106e5f1f43fdb00c6",
|
||||
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_id", "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_id", 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="6147489f087c144f887386548cba269ffc193094",
|
||||
operation="UPDATE",
|
||||
pgid="pgtrigger_update_update_93a7e",
|
||||
table="rides_ridephoto",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -9,7 +9,6 @@ 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
|
||||
|
||||
|
||||
@@ -37,8 +36,10 @@ class RidePhoto(TrackedModel):
|
||||
"rides.Ride", on_delete=models.CASCADE, related_name="photos"
|
||||
)
|
||||
|
||||
image = CloudflareImagesField(
|
||||
variant="public", help_text="Ride photo stored on Cloudflare Images"
|
||||
image = models.ForeignKey(
|
||||
'django_cloudflareimages_toolkit.CloudflareImage',
|
||||
on_delete=models.CASCADE,
|
||||
help_text="Ride photo stored on Cloudflare Images"
|
||||
)
|
||||
|
||||
caption = models.CharField(max_length=255, blank=True)
|
||||
|
||||
@@ -362,8 +362,10 @@ class RideModelPhoto(TrackedModel):
|
||||
ride_model = models.ForeignKey(
|
||||
RideModel, on_delete=models.CASCADE, related_name="photos"
|
||||
)
|
||||
image = models.ImageField(
|
||||
upload_to="ride_models/photos/", help_text="Photo of the ride model"
|
||||
image = models.ForeignKey(
|
||||
'django_cloudflareimages_toolkit.CloudflareImage',
|
||||
on_delete=models.CASCADE,
|
||||
help_text="Photo of the ride model stored on Cloudflare Images"
|
||||
)
|
||||
caption = models.CharField(max_length=500, blank=True)
|
||||
alt_text = models.CharField(max_length=255, blank=True)
|
||||
@@ -624,9 +626,30 @@ class Ride(TrackedModel):
|
||||
return f"{self.name} at {self.park.name}"
|
||||
|
||||
def save(self, *args, **kwargs) -> None:
|
||||
# Handle slug generation and conflicts
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
|
||||
# Check for slug conflicts when park changes or slug is new
|
||||
original_ride = None
|
||||
if self.pk:
|
||||
try:
|
||||
original_ride = Ride.objects.get(pk=self.pk)
|
||||
except Ride.DoesNotExist:
|
||||
pass
|
||||
|
||||
# If park changed or this is a new ride, ensure slug uniqueness within the park
|
||||
park_changed = original_ride and original_ride.park_id != self.park_id
|
||||
if not self.pk or park_changed:
|
||||
self._ensure_unique_slug_in_park()
|
||||
|
||||
# Handle park area validation when park changes
|
||||
if park_changed and self.park_area:
|
||||
# Check if park_area belongs to the new park
|
||||
if self.park_area.park_id != self.park_id:
|
||||
# Clear park_area if it doesn't belong to the new park
|
||||
self.park_area = None
|
||||
|
||||
# Generate frontend URLs
|
||||
if self.park:
|
||||
frontend_domain = getattr(
|
||||
@@ -637,6 +660,73 @@ class Ride(TrackedModel):
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _ensure_unique_slug_in_park(self) -> None:
|
||||
"""Ensure the ride's slug is unique within its park."""
|
||||
base_slug = slugify(self.name)
|
||||
self.slug = base_slug
|
||||
|
||||
counter = 1
|
||||
while (
|
||||
Ride.objects.filter(park=self.park, slug=self.slug)
|
||||
.exclude(pk=self.pk)
|
||||
.exists()
|
||||
):
|
||||
self.slug = f"{base_slug}-{counter}"
|
||||
counter += 1
|
||||
|
||||
def move_to_park(self, new_park, clear_park_area=True):
|
||||
"""
|
||||
Move this ride to a different park with proper handling of related data.
|
||||
|
||||
Args:
|
||||
new_park: The new Park instance to move the ride to
|
||||
clear_park_area: Whether to clear park_area (default True, since areas are park-specific)
|
||||
|
||||
Returns:
|
||||
dict: Summary of changes made
|
||||
"""
|
||||
from django.apps import apps
|
||||
|
||||
old_park = self.park
|
||||
old_url = self.url
|
||||
old_park_area = self.park_area
|
||||
|
||||
# Update park
|
||||
self.park = new_park
|
||||
|
||||
# Handle park area
|
||||
if clear_park_area:
|
||||
self.park_area = None
|
||||
|
||||
# Save will handle slug conflicts and URL updates
|
||||
self.save()
|
||||
|
||||
# Return summary of changes
|
||||
changes = {
|
||||
'old_park': {
|
||||
'id': old_park.id,
|
||||
'name': old_park.name,
|
||||
'slug': old_park.slug
|
||||
},
|
||||
'new_park': {
|
||||
'id': new_park.id,
|
||||
'name': new_park.name,
|
||||
'slug': new_park.slug
|
||||
},
|
||||
'url_changed': old_url != self.url,
|
||||
'old_url': old_url,
|
||||
'new_url': self.url,
|
||||
'park_area_cleared': clear_park_area and old_park_area is not None,
|
||||
'old_park_area': {
|
||||
'id': old_park_area.id,
|
||||
'name': old_park_area.name
|
||||
} if old_park_area else None,
|
||||
'slug_changed': self.slug != slugify(self.name),
|
||||
'final_slug': self.slug
|
||||
}
|
||||
|
||||
return changes
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
class RollerCoasterStats(models.Model):
|
||||
|
||||
@@ -3,7 +3,6 @@ from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import HttpRequest, HttpResponse, Http404
|
||||
from django.db.models import Count
|
||||
from .models.rides import Ride, RideModel, Categories
|
||||
@@ -13,7 +12,6 @@ from .forms.search import MasterFilterForm
|
||||
from .services.search import RideSearchService
|
||||
from apps.parks.models import Park
|
||||
from apps.moderation.mixins import EditSubmissionMixin, HistoryMixin
|
||||
from apps.moderation.models import EditSubmission
|
||||
from apps.moderation.services import ModerationService
|
||||
from .models.rankings import RideRanking, RankingSnapshot
|
||||
from .services.ranking_service import RideRankingService
|
||||
|
||||
Reference in New Issue
Block a user