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

@@ -5,12 +5,10 @@ This module provides ViewSets for accessing historical data and change tracking
across all models in the ThrillWiki system using django-pghistory.
"""
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
from drf_spectacular.types import OpenApiTypes
from rest_framework.filters import OrderingFilter
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet
from django.shortcuts import get_object_or_404

View File

@@ -3,18 +3,9 @@ Centralized map API views.
Migrated from apps.core.views.map_views
"""
import json
import logging
import time
from typing import Dict, Any, Optional
from django.http import JsonResponse, HttpRequest
from django.views.decorators.cache import cache_page
from django.views.decorators.gzip import gzip_page
from django.utils.decorators import method_decorator
from django.views import View
from django.core.exceptions import ValidationError
from django.conf import settings
from django.http import HttpRequest
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
@@ -30,15 +21,54 @@ logger = logging.getLogger(__name__)
summary="Get map locations",
description="Get map locations with optional clustering and filtering.",
parameters=[
{"name": "north", "in": "query", "required": False, "schema": {"type": "number"}},
{"name": "south", "in": "query", "required": False, "schema": {"type": "number"}},
{"name": "east", "in": "query", "required": False, "schema": {"type": "number"}},
{"name": "west", "in": "query", "required": False, "schema": {"type": "number"}},
{"name": "zoom", "in": "query", "required": False, "schema": {"type": "integer"}},
{"name": "types", "in": "query", "required": False, "schema": {"type": "string"}},
{"name": "cluster", "in": "query", "required": False,
"schema": {"type": "boolean"}},
{"name": "q", "in": "query", "required": False, "schema": {"type": "string"}},
{
"name": "north",
"in": "query",
"required": False,
"schema": {"type": "number"},
},
{
"name": "south",
"in": "query",
"required": False,
"schema": {"type": "number"},
},
{
"name": "east",
"in": "query",
"required": False,
"schema": {"type": "number"},
},
{
"name": "west",
"in": "query",
"required": False,
"schema": {"type": "number"},
},
{
"name": "zoom",
"in": "query",
"required": False,
"schema": {"type": "integer"},
},
{
"name": "types",
"in": "query",
"required": False,
"schema": {"type": "string"},
},
{
"name": "cluster",
"in": "query",
"required": False,
"schema": {"type": "boolean"},
},
{
"name": "q",
"in": "query",
"required": False,
"schema": {"type": "string"},
},
],
responses={200: OpenApiTypes.OBJECT},
tags=["Maps"],
@@ -54,18 +84,20 @@ class MapLocationsAPIView(APIView):
try:
# Simple implementation to fix import error
# TODO: Implement full functionality
return Response({
"status": "success",
"message": "Map locations endpoint - implementation needed",
"data": []
})
return Response(
{
"status": "success",
"message": "Map locations endpoint - implementation needed",
"data": [],
}
)
except Exception as e:
logger.error(f"Error in MapLocationsAPIView: {str(e)}", exc_info=True)
return Response({
"status": "error",
"message": "Failed to retrieve map locations"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(
{"status": "error", "message": "Failed to retrieve map locations"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@extend_schema_view(
@@ -73,10 +105,18 @@ class MapLocationsAPIView(APIView):
summary="Get location details",
description="Get detailed information about a specific location.",
parameters=[
{"name": "location_type", "in": "path",
"required": True, "schema": {"type": "string"}},
{"name": "location_id", "in": "path",
"required": True, "schema": {"type": "integer"}},
{
"name": "location_type",
"in": "path",
"required": True,
"schema": {"type": "string"},
},
{
"name": "location_id",
"in": "path",
"required": True,
"schema": {"type": "integer"},
},
],
responses={200: OpenApiTypes.OBJECT, 404: OpenApiTypes.OBJECT},
tags=["Maps"],
@@ -87,25 +127,29 @@ class MapLocationDetailAPIView(APIView):
permission_classes = [AllowAny]
def get(self, request: HttpRequest, location_type: str, location_id: int) -> Response:
def get(
self, request: HttpRequest, location_type: str, location_id: int
) -> Response:
"""Get detailed information for a specific location."""
try:
# Simple implementation to fix import error
return Response({
"status": "success",
"message": f"Location detail for {location_type}/{location_id} - implementation needed",
"data": {
"location_type": location_type,
"location_id": location_id
return Response(
{
"status": "success",
"message": f"Location detail for {location_type}/{location_id} - implementation needed",
"data": {
"location_type": location_type,
"location_id": location_id,
},
}
})
)
except Exception as e:
logger.error(f"Error in MapLocationDetailAPIView: {str(e)}", exc_info=True)
return Response({
"status": "error",
"message": "Failed to retrieve location details"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(
{"status": "error", "message": "Failed to retrieve location details"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@extend_schema_view(
@@ -113,7 +157,12 @@ class MapLocationDetailAPIView(APIView):
summary="Search map locations",
description="Search locations by text query with optional bounds filtering.",
parameters=[
{"name": "q", "in": "query", "required": True, "schema": {"type": "string"}},
{
"name": "q",
"in": "query",
"required": True,
"schema": {"type": "string"},
},
],
responses={200: OpenApiTypes.OBJECT, 400: OpenApiTypes.OBJECT},
tags=["Maps"],
@@ -129,24 +178,29 @@ class MapSearchAPIView(APIView):
try:
query = request.GET.get("q", "").strip()
if not query:
return Response({
"status": "error",
"message": "Search query 'q' parameter is required"
}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{
"status": "error",
"message": "Search query 'q' parameter is required",
},
status=status.HTTP_400_BAD_REQUEST,
)
# Simple implementation to fix import error
return Response({
"status": "success",
"message": f"Search for '{query}' - implementation needed",
"data": []
})
return Response(
{
"status": "success",
"message": f"Search for '{query}' - implementation needed",
"data": [],
}
)
except Exception as e:
logger.error(f"Error in MapSearchAPIView: {str(e)}", exc_info=True)
return Response({
"status": "error",
"message": "Search failed due to internal error"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(
{"status": "error", "message": "Search failed due to internal error"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@extend_schema_view(
@@ -154,10 +208,30 @@ class MapSearchAPIView(APIView):
summary="Get locations within bounds",
description="Get locations within specific geographic bounds.",
parameters=[
{"name": "north", "in": "query", "required": True, "schema": {"type": "number"}},
{"name": "south", "in": "query", "required": True, "schema": {"type": "number"}},
{"name": "east", "in": "query", "required": True, "schema": {"type": "number"}},
{"name": "west", "in": "query", "required": True, "schema": {"type": "number"}},
{
"name": "north",
"in": "query",
"required": True,
"schema": {"type": "number"},
},
{
"name": "south",
"in": "query",
"required": True,
"schema": {"type": "number"},
},
{
"name": "east",
"in": "query",
"required": True,
"schema": {"type": "number"},
},
{
"name": "west",
"in": "query",
"required": True,
"schema": {"type": "number"},
},
],
responses={200: OpenApiTypes.OBJECT, 400: OpenApiTypes.OBJECT},
tags=["Maps"],
@@ -172,18 +246,23 @@ class MapBoundsAPIView(APIView):
"""Get locations within specific geographic bounds."""
try:
# Simple implementation to fix import error
return Response({
"status": "success",
"message": "Bounds query - implementation needed",
"data": []
})
return Response(
{
"status": "success",
"message": "Bounds query - implementation needed",
"data": [],
}
)
except Exception as e:
logger.error(f"Error in MapBoundsAPIView: {str(e)}", exc_info=True)
return Response({
"status": "error",
"message": "Failed to retrieve locations within bounds"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(
{
"status": "error",
"message": "Failed to retrieve locations within bounds",
},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@extend_schema_view(
@@ -203,14 +282,12 @@ class MapStatsAPIView(APIView):
"""Get map service statistics and performance metrics."""
try:
# Simple implementation to fix import error
return Response({
"status": "success",
"data": {
"total_locations": 0,
"cache_hits": 0,
"cache_misses": 0
return Response(
{
"status": "success",
"data": {"total_locations": 0, "cache_hits": 0, "cache_misses": 0},
}
})
)
except Exception as e:
return Response(
@@ -242,10 +319,9 @@ class MapCacheAPIView(APIView):
"""Clear all map cache (admin only)."""
try:
# Simple implementation to fix import error
return Response({
"status": "success",
"message": "Map cache cleared successfully"
})
return Response(
{"status": "success", "message": "Map cache cleared successfully"}
)
except Exception as e:
return Response(
@@ -257,10 +333,9 @@ class MapCacheAPIView(APIView):
"""Invalidate specific cache entries."""
try:
# Simple implementation to fix import error
return Response({
"status": "success",
"message": "Cache invalidated successfully"
})
return Response(
{"status": "success", "message": "Cache invalidated successfully"}
)
except Exception as e:
return Response(

View File

@@ -1,6 +1,7 @@
"""
Park API URLs for ThrillWiki API v1.
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter

View File

@@ -1,6 +1,7 @@
"""
Park API views for ThrillWiki API v1.
"""
import logging
from django.core.exceptions import PermissionDenied

View File

@@ -1,6 +1,7 @@
"""
Ride API URLs for ThrillWiki API v1.
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter

View File

@@ -1,6 +1,7 @@
"""
Ride API views for ThrillWiki API v1.
"""
import logging
from django.core.exceptions import PermissionDenied

View File

@@ -6,8 +6,6 @@ for the ThrillWiki API, including better documentation and examples.
"""
from drf_spectacular.openapi import AutoSchema
from drf_spectacular.utils import OpenApiExample
from drf_spectacular.types import OpenApiTypes
# Custom examples for common serializers

View File

@@ -12,12 +12,7 @@ from drf_spectacular.utils import (
OpenApiExample,
)
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.crypto import get_random_string
from django.utils import timezone
from datetime import timedelta
from django.contrib.sites.shortcuts import get_current_site
from django.template.loader import render_to_string
from .shared import UserModel, ModelChoices

View File

@@ -6,8 +6,7 @@ using django-pghistory.
"""
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer, extend_schema_field
import pghistory.models
from drf_spectacular.utils import extend_schema_field
class ParkHistoryEventSerializer(serializers.Serializer):

View File

@@ -7,9 +7,7 @@ miscellaneous functionality.
from rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_serializer,
extend_schema_field,
OpenApiExample,
)

View File

@@ -239,7 +239,8 @@ class ParkFilterInputSerializer(serializers.Serializer):
# Status filter
status = serializers.MultipleChoiceField(
choices=[], required=False # Choices set dynamically
choices=[],
required=False, # Choices set dynamically
)
# Location filters

View File

@@ -12,39 +12,40 @@ class ParkPhotoOutputSerializer(serializers.ModelSerializer):
"""Output serializer for park photos."""
uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True)
source="uploaded_by.username", read_only=True
)
file_size = serializers.ReadOnlyField()
dimensions = serializers.ReadOnlyField()
park_slug = serializers.CharField(source='park.slug', read_only=True)
park_name = serializers.CharField(source='park.name', read_only=True)
park_slug = serializers.CharField(source="park.slug", read_only=True)
park_name = serializers.CharField(source="park.name", read_only=True)
class Meta:
model = ParkPhoto
fields = [
'id',
'image',
'caption',
'alt_text',
'is_primary',
'is_approved',
'created_at',
'updated_at',
'date_taken',
'uploaded_by_username',
'file_size',
'dimensions',
'park_slug',
'park_name',
"id",
"image",
"caption",
"alt_text",
"is_primary",
"is_approved",
"created_at",
"updated_at",
"date_taken",
"uploaded_by_username",
"file_size",
"dimensions",
"park_slug",
"park_name",
]
read_only_fields = [
'id',
'created_at',
'updated_at',
'uploaded_by_username',
'file_size',
'dimensions',
'park_slug',
'park_name',
"id",
"created_at",
"updated_at",
"uploaded_by_username",
"file_size",
"dimensions",
"park_slug",
"park_name",
]
@@ -54,10 +55,10 @@ class ParkPhotoCreateInputSerializer(serializers.ModelSerializer):
class Meta:
model = ParkPhoto
fields = [
'image',
'caption',
'alt_text',
'is_primary',
"image",
"caption",
"alt_text",
"is_primary",
]
@@ -67,9 +68,9 @@ class ParkPhotoUpdateInputSerializer(serializers.ModelSerializer):
class Meta:
model = ParkPhoto
fields = [
'caption',
'alt_text',
'is_primary',
"caption",
"alt_text",
"is_primary",
]
@@ -77,18 +78,19 @@ class ParkPhotoListOutputSerializer(serializers.ModelSerializer):
"""Simplified output serializer for park photo lists."""
uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True)
source="uploaded_by.username", read_only=True
)
class Meta:
model = ParkPhoto
fields = [
'id',
'image',
'caption',
'is_primary',
'is_approved',
'created_at',
'uploaded_by_username',
"id",
"image",
"caption",
"is_primary",
"is_approved",
"created_at",
"uploaded_by_username",
]
read_only_fields = fields
@@ -97,12 +99,10 @@ class ParkPhotoApprovalInputSerializer(serializers.Serializer):
"""Input serializer for photo approval operations."""
photo_ids = serializers.ListField(
child=serializers.IntegerField(),
help_text="List of photo IDs to approve"
child=serializers.IntegerField(), help_text="List of photo IDs to approve"
)
approve = serializers.BooleanField(
default=True,
help_text="Whether to approve (True) or reject (False) the photos"
default=True, help_text="Whether to approve (True) or reject (False) the photos"
)

View File

@@ -344,7 +344,8 @@ class RideFilterInputSerializer(serializers.Serializer):
# Status filter
status = serializers.MultipleChoiceField(
choices=[], required=False # Choices set dynamically
choices=[],
required=False, # Choices set dynamically
)
# Park filter

View File

@@ -12,46 +12,47 @@ class RidePhotoOutputSerializer(serializers.ModelSerializer):
"""Output serializer for ride photos."""
uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True)
source="uploaded_by.username", read_only=True
)
file_size = serializers.ReadOnlyField()
dimensions = serializers.ReadOnlyField()
ride_slug = serializers.CharField(source='ride.slug', read_only=True)
ride_name = serializers.CharField(source='ride.name', read_only=True)
park_slug = serializers.CharField(source='ride.park.slug', read_only=True)
park_name = serializers.CharField(source='ride.park.name', read_only=True)
ride_slug = serializers.CharField(source="ride.slug", read_only=True)
ride_name = serializers.CharField(source="ride.name", read_only=True)
park_slug = serializers.CharField(source="ride.park.slug", read_only=True)
park_name = serializers.CharField(source="ride.park.name", read_only=True)
class Meta:
model = RidePhoto
fields = [
'id',
'image',
'caption',
'alt_text',
'is_primary',
'is_approved',
'photo_type',
'created_at',
'updated_at',
'date_taken',
'uploaded_by_username',
'file_size',
'dimensions',
'ride_slug',
'ride_name',
'park_slug',
'park_name',
"id",
"image",
"caption",
"alt_text",
"is_primary",
"is_approved",
"photo_type",
"created_at",
"updated_at",
"date_taken",
"uploaded_by_username",
"file_size",
"dimensions",
"ride_slug",
"ride_name",
"park_slug",
"park_name",
]
read_only_fields = [
'id',
'created_at',
'updated_at',
'uploaded_by_username',
'file_size',
'dimensions',
'ride_slug',
'ride_name',
'park_slug',
'park_name',
"id",
"created_at",
"updated_at",
"uploaded_by_username",
"file_size",
"dimensions",
"ride_slug",
"ride_name",
"park_slug",
"park_name",
]
@@ -61,11 +62,11 @@ class RidePhotoCreateInputSerializer(serializers.ModelSerializer):
class Meta:
model = RidePhoto
fields = [
'image',
'caption',
'alt_text',
'photo_type',
'is_primary',
"image",
"caption",
"alt_text",
"photo_type",
"is_primary",
]
@@ -75,10 +76,10 @@ class RidePhotoUpdateInputSerializer(serializers.ModelSerializer):
class Meta:
model = RidePhoto
fields = [
'caption',
'alt_text',
'photo_type',
'is_primary',
"caption",
"alt_text",
"photo_type",
"is_primary",
]
@@ -86,19 +87,20 @@ class RidePhotoListOutputSerializer(serializers.ModelSerializer):
"""Simplified output serializer for ride photo lists."""
uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True)
source="uploaded_by.username", read_only=True
)
class Meta:
model = RidePhoto
fields = [
'id',
'image',
'caption',
'photo_type',
'is_primary',
'is_approved',
'created_at',
'uploaded_by_username',
"id",
"image",
"caption",
"photo_type",
"is_primary",
"is_approved",
"created_at",
"uploaded_by_username",
]
read_only_fields = fields
@@ -107,12 +109,10 @@ class RidePhotoApprovalInputSerializer(serializers.Serializer):
"""Input serializer for photo approval operations."""
photo_ids = serializers.ListField(
child=serializers.IntegerField(),
help_text="List of photo IDs to approve"
child=serializers.IntegerField(), help_text="List of photo IDs to approve"
)
approve = serializers.BooleanField(
default=True,
help_text="Whether to approve (True) or reject (False) the photos"
default=True, help_text="Whether to approve (True) or reject (False) the photos"
)
@@ -125,8 +125,7 @@ class RidePhotoStatsOutputSerializer(serializers.Serializer):
has_primary = serializers.BooleanField()
recent_uploads = serializers.IntegerField()
by_type = serializers.DictField(
child=serializers.IntegerField(),
help_text="Photo counts by type"
child=serializers.IntegerField(), help_text="Photo counts by type"
)
@@ -135,13 +134,13 @@ class RidePhotoTypeFilterSerializer(serializers.Serializer):
photo_type = serializers.ChoiceField(
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"),
],
required=False,
help_text="Filter photos by type"
help_text="Filter photos by type",
)

View File

@@ -6,11 +6,6 @@ and other search functionality.
"""
from rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_serializer,
extend_schema_field,
OpenApiExample,
)
# === CORE ENTITY SEARCH SERIALIZERS ===

View File

@@ -7,9 +7,7 @@ history tracking, moderation, and roadtrip planning.
from rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_serializer,
extend_schema_field,
OpenApiExample,
)

View File

@@ -13,7 +13,6 @@ from drf_spectacular.utils import (
)
from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.crypto import get_random_string
from django.utils import timezone
from datetime import timedelta
@@ -400,7 +399,8 @@ class ParkFilterInputSerializer(serializers.Serializer):
# Status filter
status = serializers.MultipleChoiceField(
choices=[], required=False # Choices set dynamically
choices=[],
required=False, # Choices set dynamically
)
# Location filters
@@ -777,7 +777,8 @@ class RideFilterInputSerializer(serializers.Serializer):
# Status filter
status = serializers.MultipleChoiceField(
choices=[], required=False # Choices set dynamically
choices=[],
required=False, # Choices set dynamically
)
# Park filter

View File

@@ -5,18 +5,14 @@ This module contains all authentication-related API endpoints including
login, signup, logout, password management, and social authentication.
"""
import time
from django.contrib.auth import authenticate, login, logout, get_user_model
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.conf import settings
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
from allauth.socialaccount import providers
from drf_spectacular.utils import extend_schema, extend_schema_view
# Import serializers inside methods to avoid Django initialization issues
@@ -274,7 +270,7 @@ class LogoutAPIView(APIView):
{"message": "Logout successful"}
)
return Response(response_serializer.data)
except Exception as e:
except Exception:
return Response(
{"error": "Logout failed"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@@ -385,7 +381,6 @@ class SocialProvidersAPIView(APIView):
def get(self, request: Request) -> Response:
from django.core.cache import cache
from django.contrib.sites.shortcuts import get_current_site
site = get_current_site(request._request) # type: ignore[attr-defined]

View File

@@ -8,7 +8,6 @@ performance metrics, and database analysis.
import time
from django.utils import timezone
from django.conf import settings
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response

View File

@@ -6,8 +6,6 @@ including trending parks, rides, and recently added content.
"""
from datetime import datetime, date
from django.utils import timezone
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response

View File

@@ -2,7 +2,7 @@
API viewsets for the ride ranking system.
"""
from django.db.models import Q, Count, Max
from django.db.models import Q
from django.utils import timezone
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
@@ -147,9 +147,7 @@ class RideRankingViewSet(ReadOnlyModelViewSet):
ranking = self.get_object()
history = RankingSnapshot.objects.filter(ride=ranking.ride).order_by(
"-snapshot_date"
)[
:90
] # Last 3 months
)[:90] # Last 3 months
serializer = self.get_serializer(history, many=True)
return Response(serializer.data)