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

2
.gitignore vendored
View File

@@ -115,3 +115,5 @@ temp/
/uploads/
/backups/
.django_tailwind_cli/
backend/.env
frontend/.env

View File

@@ -39,15 +39,15 @@ backend/
### Required URL Pattern
- **Frontend requests:** `/api/{endpoint}`
- **Vite proxy rewrites to:** `/api/v1/{endpoint}`
- **Django serves from:** `backend/api/v1/{endpoint}`
- **Django serves from:** `backend/apps/api/v1/{endpoint}`
### Migration Requirements
When consolidating rogue API endpoints:
1. **BEFORE REMOVAL:** Ensure ALL functionality exists in `backend/api/v1/`
2. **Move views:** Transfer all API views to appropriate `backend/api/v1/{domain}/views.py`
3. **Move serializers:** Transfer to `backend/api/v1/{domain}/serializers.py`
4. **Update URLs:** Consolidate routes in `backend/api/v1/{domain}/urls.py`
1. **BEFORE REMOVAL:** Ensure ALL functionality exists in `backend/apps/api/v1/`
2. **Move views:** Transfer all API views to appropriate `backend/apps/api/v1/{domain}/views.py`
3. **Move serializers:** Transfer to `backend/apps/api/v1/{domain}/serializers.py`
4. **Update URLs:** Consolidate routes in `backend/apps/api/v1/{domain}/urls.py`
5. **Test thoroughly:** Verify all endpoints work via central API
6. **Only then remove:** Delete the rogue `api_urls.py` and `api_views.py` files
@@ -61,8 +61,8 @@ If rogue API files are discovered:
5. **Remove rogue files only after verification**
### URL Routing Rules
- **Main API router:** `backend/api/urls.py` includes `api/v1/urls.py`
- **Version router:** `backend/api/v1/urls.py` includes domain-specific routes
- **Main API router:** `backend/apps/api/urls.py` includes `apps/api/v1/urls.py`
- **Version router:** `backend/apps/api/v1/urls.py` includes domain-specific routes
- **Domain routers:** `backend/api/v1/{domain}/urls.py` defines actual endpoints
- **No direct app routing:** Apps CANNOT define their own API URLs
@@ -72,21 +72,21 @@ If rogue API files are discovered:
- **URL consistency:** All frontend API calls follow this pattern
### Quality Assurance
- **No API endpoints** may exist outside `backend/api/v1/`
- **No API endpoints** may exist outside `backend/apps/api/v1/`
- **All API responses** must use proper DRF serializers
- **Consistent error handling** across all endpoints
- **Proper authentication** and permissions on all routes
### Examples of Proper Structure
```python
# backend/api/urls.py
# backend/apps/api/urls.py
from django.urls import path, include
urlpatterns = [
path('v1/', include('api.v1.urls')),
]
# backend/api/v1/urls.py
# backend/apps/api/v1/urls.py
from django.urls import path, include
urlpatterns = [
@@ -95,7 +95,7 @@ urlpatterns = [
path('auth/', include('api.v1.auth.urls')),
]
# backend/api/v1/rides/urls.py
# backend/apps/api/v1/rides/urls.py
from django.urls import path
from . import views

View File

@@ -8,5 +8,5 @@ Currently supports v1 API endpoints.
from django.urls import path, include
urlpatterns = [
path('v1/', include('api.v1.urls')),
path("v1/", include("api.v1.urls")),
]

View File

@@ -12,15 +12,9 @@ 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 django.contrib.auth import get_user_model
from apps.accounts.models import UserProfile, TopList, TopListItem
UserModel = get_user_model()
@@ -44,6 +38,7 @@ class ModelChoices:
# === AUTHENTICATION SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
@@ -298,6 +293,7 @@ class AuthStatusOutputSerializer(serializers.Serializer):
# === USER PROFILE SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
@@ -388,6 +384,7 @@ class UserProfileUpdateInputSerializer(serializers.Serializer):
# === TOP LIST SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
@@ -447,6 +444,7 @@ class TopListUpdateInputSerializer(serializers.Serializer):
# === TOP LIST ITEM SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(

View File

@@ -21,13 +21,22 @@ urlpatterns = [
path("signup/", views.SignupAPIView.as_view(), name="auth-signup"),
path("logout/", views.LogoutAPIView.as_view(), name="auth-logout"),
path("user/", views.CurrentUserAPIView.as_view(), name="auth-current-user"),
path("password/reset/", views.PasswordResetAPIView.as_view(), name="auth-password-reset"),
path("password/change/", views.PasswordChangeAPIView.as_view(),
name="auth-password-change"),
path("social/providers/", views.SocialProvidersAPIView.as_view(),
name="auth-social-providers"),
path(
"password/reset/",
views.PasswordResetAPIView.as_view(),
name="auth-password-reset",
),
path(
"password/change/",
views.PasswordChangeAPIView.as_view(),
name="auth-password-change",
),
path(
"social/providers/",
views.SocialProvidersAPIView.as_view(),
name="auth-social-providers",
),
path("status/", views.AuthStatusAPIView.as_view(), name="auth-status"),
# Include router URLs for ViewSets (profiles, top lists)
path("", include(router.urls)),
]

View File

@@ -6,12 +6,9 @@ login, signup, logout, password management, social authentication,
user profiles, and top lists.
"""
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 django.db.models import Q
from rest_framework import status
from rest_framework.views import APIView
@@ -20,7 +17,6 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.decorators import action
from allauth.socialaccount import providers
from drf_spectacular.utils import extend_schema, extend_schema_view
from apps.accounts.models import UserProfile, TopList, TopListItem
@@ -72,6 +68,7 @@ UserModel = get_user_model()
# === AUTHENTICATION API VIEWS ===
@extend_schema_view(
post=extend_schema(
summary="User login",
@@ -250,7 +247,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
)
@@ -361,7 +358,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]
@@ -446,6 +442,7 @@ class AuthStatusAPIView(APIView):
# === USER PROFILE API VIEWS ===
class UserProfileViewSet(ModelViewSet):
"""ViewSet for managing user profiles."""
@@ -481,6 +478,7 @@ class UserProfileViewSet(ModelViewSet):
# === TOP LIST API VIEWS ===
class TopListViewSet(ModelViewSet):
"""ViewSet for managing user top lists."""

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

@@ -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

@@ -3,6 +3,7 @@ Park API views for ThrillWiki API v1.
This module contains consolidated park photo viewset for the centralized API structure.
"""
import logging
from django.core.exceptions import PermissionDenied
@@ -108,28 +109,26 @@ class ParkPhotoViewSet(ModelViewSet):
def get_queryset(self):
"""Get photos for the current park with optimized queries."""
return ParkPhoto.objects.select_related(
'park',
'park__operator',
'uploaded_by'
).filter(
park_id=self.kwargs.get('park_pk')
).order_by('-created_at')
return (
ParkPhoto.objects.select_related("park", "park__operator", "uploaded_by")
.filter(park_id=self.kwargs.get("park_pk"))
.order_by("-created_at")
)
def get_serializer_class(self):
"""Return appropriate serializer based on action."""
if self.action == 'list':
if self.action == "list":
return ParkPhotoListOutputSerializer
elif self.action == 'create':
elif self.action == "create":
return ParkPhotoCreateInputSerializer
elif self.action in ['update', 'partial_update']:
elif self.action in ["update", "partial_update"]:
return ParkPhotoUpdateInputSerializer
else:
return ParkPhotoOutputSerializer
def perform_create(self, serializer):
"""Create a new park photo using ParkMediaService."""
park_id = self.kwargs.get('park_pk')
park_id = self.kwargs.get("park_pk")
if not park_id:
raise ValidationError("Park ID is required")
@@ -138,7 +137,7 @@ class ParkPhotoViewSet(ModelViewSet):
photo = ParkMediaService.create_photo(
park_id=park_id,
uploaded_by=self.request.user,
**serializer.validated_data
**serializer.validated_data,
)
# Set the instance for the serializer response
@@ -153,19 +152,20 @@ class ParkPhotoViewSet(ModelViewSet):
instance = self.get_object()
# Check permissions
if not (self.request.user == instance.uploaded_by or self.request.user.is_staff):
if not (
self.request.user == instance.uploaded_by or self.request.user.is_staff
):
raise PermissionDenied("You can only edit your own photos or be an admin.")
# Handle primary photo logic using service
if serializer.validated_data.get('is_primary', False):
if serializer.validated_data.get("is_primary", False):
try:
ParkMediaService.set_primary_photo(
park_id=instance.park_id,
photo_id=instance.id
park_id=instance.park_id, photo_id=instance.id
)
# Remove is_primary from validated_data since service handles it
if 'is_primary' in serializer.validated_data:
del serializer.validated_data['is_primary']
if "is_primary" in serializer.validated_data:
del serializer.validated_data["is_primary"]
except Exception as e:
logger.error(f"Error setting primary photo: {e}")
raise ValidationError(f"Failed to set primary photo: {str(e)}")
@@ -175,9 +175,12 @@ class ParkPhotoViewSet(ModelViewSet):
def perform_destroy(self, instance):
"""Delete park photo with permission checking."""
# Check permissions
if not (self.request.user == instance.uploaded_by or self.request.user.is_staff):
if not (
self.request.user == instance.uploaded_by or self.request.user.is_staff
):
raise PermissionDenied(
"You can only delete your own photos or be an admin.")
"You can only delete your own photos or be an admin."
)
try:
ParkMediaService.delete_photo(instance.id)
@@ -185,7 +188,7 @@ class ParkPhotoViewSet(ModelViewSet):
logger.error(f"Error deleting park photo: {e}")
raise ValidationError(f"Failed to delete photo: {str(e)}")
@action(detail=True, methods=['post'])
@action(detail=True, methods=["post"])
def set_primary(self, request, **kwargs):
"""Set this photo as the primary photo for the park."""
photo = self.get_object()
@@ -193,13 +196,11 @@ class ParkPhotoViewSet(ModelViewSet):
# Check permissions
if not (request.user == photo.uploaded_by or request.user.is_staff):
raise PermissionDenied(
"You can only modify your own photos or be an admin.")
"You can only modify your own photos or be an admin."
)
try:
ParkMediaService.set_primary_photo(
park_id=photo.park_id,
photo_id=photo.id
)
ParkMediaService.set_primary_photo(park_id=photo.park_id, photo_id=photo.id)
# Refresh the photo instance
photo.refresh_from_db()
@@ -207,20 +208,20 @@ class ParkPhotoViewSet(ModelViewSet):
return Response(
{
'message': 'Photo set as primary successfully',
'photo': serializer.data
"message": "Photo set as primary successfully",
"photo": serializer.data,
},
status=status.HTTP_200_OK
status=status.HTTP_200_OK,
)
except Exception as e:
logger.error(f"Error setting primary photo: {e}")
return Response(
{'error': f'Failed to set primary photo: {str(e)}'},
status=status.HTTP_400_BAD_REQUEST
{"error": f"Failed to set primary photo: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST,
)
@action(detail=False, methods=['post'], permission_classes=[IsAuthenticated])
@action(detail=False, methods=["post"], permission_classes=[IsAuthenticated])
def bulk_approve(self, request, **kwargs):
"""Bulk approve or reject multiple photos (admin only)."""
if not request.user.is_staff:
@@ -229,38 +230,35 @@ class ParkPhotoViewSet(ModelViewSet):
serializer = ParkPhotoApprovalInputSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
photo_ids = serializer.validated_data['photo_ids']
approve = serializer.validated_data['approve']
park_id = self.kwargs.get('park_pk')
photo_ids = serializer.validated_data["photo_ids"]
approve = serializer.validated_data["approve"]
park_id = self.kwargs.get("park_pk")
try:
# Filter photos to only those belonging to this park
photos = ParkPhoto.objects.filter(
id__in=photo_ids,
park_id=park_id
)
photos = ParkPhoto.objects.filter(id__in=photo_ids, park_id=park_id)
updated_count = photos.update(is_approved=approve)
return Response(
{
'message': f'Successfully {"approved" if approve else "rejected"} {updated_count} photos',
'updated_count': updated_count
"message": f"Successfully {'approved' if approve else 'rejected'} {updated_count} photos",
"updated_count": updated_count,
},
status=status.HTTP_200_OK
status=status.HTTP_200_OK,
)
except Exception as e:
logger.error(f"Error in bulk photo approval: {e}")
return Response(
{'error': f'Failed to update photos: {str(e)}'},
status=status.HTTP_400_BAD_REQUEST
{"error": f"Failed to update photos: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST,
)
@action(detail=False, methods=['get'])
@action(detail=False, methods=["get"])
def stats(self, request, **kwargs):
"""Get photo statistics for the park."""
park_id = self.kwargs.get('park_pk')
park_id = self.kwargs.get("park_pk")
try:
stats = ParkMediaService.get_photo_stats(park_id=park_id)
@@ -271,6 +269,6 @@ class ParkPhotoViewSet(ModelViewSet):
except Exception as e:
logger.error(f"Error getting park photo stats: {e}")
return Response(
{'error': f'Failed to get photo statistics: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
{"error": f"Failed to get photo statistics: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

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

@@ -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

@@ -3,6 +3,7 @@ Ride API views for ThrillWiki API v1.
This module contains consolidated ride photo viewset for the centralized API structure.
"""
import logging
from django.core.exceptions import PermissionDenied
@@ -108,28 +109,26 @@ class RidePhotoViewSet(ModelViewSet):
def get_queryset(self):
"""Get photos for the current ride with optimized queries."""
return RidePhoto.objects.select_related(
'ride',
'ride__park',
'uploaded_by'
).filter(
ride_id=self.kwargs.get('ride_pk')
).order_by('-created_at')
return (
RidePhoto.objects.select_related("ride", "ride__park", "uploaded_by")
.filter(ride_id=self.kwargs.get("ride_pk"))
.order_by("-created_at")
)
def get_serializer_class(self):
"""Return appropriate serializer based on action."""
if self.action == 'list':
if self.action == "list":
return RidePhotoListOutputSerializer
elif self.action == 'create':
elif self.action == "create":
return RidePhotoCreateInputSerializer
elif self.action in ['update', 'partial_update']:
elif self.action in ["update", "partial_update"]:
return RidePhotoUpdateInputSerializer
else:
return RidePhotoOutputSerializer
def perform_create(self, serializer):
"""Create a new ride photo using RideMediaService."""
ride_id = self.kwargs.get('ride_pk')
ride_id = self.kwargs.get("ride_pk")
if not ride_id:
raise ValidationError("Ride ID is required")
@@ -138,7 +137,7 @@ class RidePhotoViewSet(ModelViewSet):
photo = RideMediaService.create_photo(
ride_id=ride_id,
uploaded_by=self.request.user,
**serializer.validated_data
**serializer.validated_data,
)
# Set the instance for the serializer response
@@ -153,19 +152,20 @@ class RidePhotoViewSet(ModelViewSet):
instance = self.get_object()
# Check permissions
if not (self.request.user == instance.uploaded_by or self.request.user.is_staff):
if not (
self.request.user == instance.uploaded_by or self.request.user.is_staff
):
raise PermissionDenied("You can only edit your own photos or be an admin.")
# Handle primary photo logic using service
if serializer.validated_data.get('is_primary', False):
if serializer.validated_data.get("is_primary", False):
try:
RideMediaService.set_primary_photo(
ride_id=instance.ride_id,
photo_id=instance.id
ride_id=instance.ride_id, photo_id=instance.id
)
# Remove is_primary from validated_data since service handles it
if 'is_primary' in serializer.validated_data:
del serializer.validated_data['is_primary']
if "is_primary" in serializer.validated_data:
del serializer.validated_data["is_primary"]
except Exception as e:
logger.error(f"Error setting primary photo: {e}")
raise ValidationError(f"Failed to set primary photo: {str(e)}")
@@ -175,9 +175,12 @@ class RidePhotoViewSet(ModelViewSet):
def perform_destroy(self, instance):
"""Delete ride photo with permission checking."""
# Check permissions
if not (self.request.user == instance.uploaded_by or self.request.user.is_staff):
if not (
self.request.user == instance.uploaded_by or self.request.user.is_staff
):
raise PermissionDenied(
"You can only delete your own photos or be an admin.")
"You can only delete your own photos or be an admin."
)
try:
RideMediaService.delete_photo(instance.id)
@@ -185,7 +188,7 @@ class RidePhotoViewSet(ModelViewSet):
logger.error(f"Error deleting ride photo: {e}")
raise ValidationError(f"Failed to delete photo: {str(e)}")
@action(detail=True, methods=['post'])
@action(detail=True, methods=["post"])
def set_primary(self, request, **kwargs):
"""Set this photo as the primary photo for the ride."""
photo = self.get_object()
@@ -193,13 +196,11 @@ class RidePhotoViewSet(ModelViewSet):
# Check permissions
if not (request.user == photo.uploaded_by or request.user.is_staff):
raise PermissionDenied(
"You can only modify your own photos or be an admin.")
"You can only modify your own photos or be an admin."
)
try:
RideMediaService.set_primary_photo(
ride_id=photo.ride_id,
photo_id=photo.id
)
RideMediaService.set_primary_photo(ride_id=photo.ride_id, photo_id=photo.id)
# Refresh the photo instance
photo.refresh_from_db()
@@ -207,20 +208,20 @@ class RidePhotoViewSet(ModelViewSet):
return Response(
{
'message': 'Photo set as primary successfully',
'photo': serializer.data
"message": "Photo set as primary successfully",
"photo": serializer.data,
},
status=status.HTTP_200_OK
status=status.HTTP_200_OK,
)
except Exception as e:
logger.error(f"Error setting primary photo: {e}")
return Response(
{'error': f'Failed to set primary photo: {str(e)}'},
status=status.HTTP_400_BAD_REQUEST
{"error": f"Failed to set primary photo: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST,
)
@action(detail=False, methods=['post'], permission_classes=[IsAuthenticated])
@action(detail=False, methods=["post"], permission_classes=[IsAuthenticated])
def bulk_approve(self, request, **kwargs):
"""Bulk approve or reject multiple photos (admin only)."""
if not request.user.is_staff:
@@ -229,38 +230,35 @@ class RidePhotoViewSet(ModelViewSet):
serializer = RidePhotoApprovalInputSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
photo_ids = serializer.validated_data['photo_ids']
approve = serializer.validated_data['approve']
ride_id = self.kwargs.get('ride_pk')
photo_ids = serializer.validated_data["photo_ids"]
approve = serializer.validated_data["approve"]
ride_id = self.kwargs.get("ride_pk")
try:
# Filter photos to only those belonging to this ride
photos = RidePhoto.objects.filter(
id__in=photo_ids,
ride_id=ride_id
)
photos = RidePhoto.objects.filter(id__in=photo_ids, ride_id=ride_id)
updated_count = photos.update(is_approved=approve)
return Response(
{
'message': f'Successfully {"approved" if approve else "rejected"} {updated_count} photos',
'updated_count': updated_count
"message": f"Successfully {'approved' if approve else 'rejected'} {updated_count} photos",
"updated_count": updated_count,
},
status=status.HTTP_200_OK
status=status.HTTP_200_OK,
)
except Exception as e:
logger.error(f"Error in bulk photo approval: {e}")
return Response(
{'error': f'Failed to update photos: {str(e)}'},
status=status.HTTP_400_BAD_REQUEST
{"error": f"Failed to update photos: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST,
)
@action(detail=False, methods=['get'])
@action(detail=False, methods=["get"])
def stats(self, request, **kwargs):
"""Get photo statistics for the ride."""
ride_id = self.kwargs.get('ride_pk')
ride_id = self.kwargs.get("ride_pk")
try:
stats = RideMediaService.get_photo_stats(ride_id=ride_id)
@@ -271,6 +269,6 @@ class RidePhotoViewSet(ModelViewSet):
except Exception as e:
logger.error(f"Error getting ride photo stats: {e}")
return Response(
{'error': f'Failed to get photo statistics: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
{"error": f"Failed to get photo statistics: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

View File

@@ -9,11 +9,10 @@ from django.urls import path, include
urlpatterns = [
# Domain-specific API endpoints
path('rides/', include('api.v1.rides.urls')),
path('parks/', include('api.v1.parks.urls')),
path('auth/', include('api.v1.auth.urls')),
path("rides/", include("api.v1.rides.urls")),
path("parks/", include("api.v1.parks.urls")),
path("auth/", include("api.v1.auth.urls")),
# Media endpoints (for photo management)
# Will be consolidated from the various media implementations
path('media/', include('api.v1.media.urls')),
path("media/", include("api.v1.media.urls")),
]

View File

@@ -11,11 +11,9 @@ class Command(BaseCommand):
self.stdout.write("\nChecking SocialApp table:")
for app in SocialApp.objects.all():
self.stdout.write(
f"ID: {
app.pk}, Provider: {
app.provider}, Name: {
app.name}, Client ID: {
app.client_id}"
f"ID: {app.pk}, Provider: {app.provider}, Name: {app.name}, Client ID: {
app.client_id
}"
)
self.stdout.write("Sites:")
for site in app.sites.all():
@@ -25,10 +23,7 @@ class Command(BaseCommand):
self.stdout.write("\nChecking SocialAccount table:")
for account in SocialAccount.objects.all():
self.stdout.write(
f"ID: {
account.pk}, Provider: {
account.provider}, UID: {
account.uid}"
f"ID: {account.pk}, Provider: {account.provider}, UID: {account.uid}"
)
# Check SocialToken

View File

@@ -13,15 +13,10 @@ class Command(BaseCommand):
return
for app in social_apps:
self.stdout.write(
self.style.SUCCESS(
f"\nProvider: {
app.provider}"
)
)
self.stdout.write(self.style.SUCCESS(f"\nProvider: {app.provider}"))
self.stdout.write(f"Name: {app.name}")
self.stdout.write(f"Client ID: {app.client_id}")
self.stdout.write(f"Secret: {app.secret}")
self.stdout.write(
f'Sites: {", ".join(str(site.domain) for site in app.sites.all())}'
f"Sites: {', '.join(str(site.domain) for site in app.sites.all())}"
)

View File

@@ -26,13 +26,15 @@ class Command(BaseCommand):
# Delete test photos - both park and ride photos
park_photos = ParkPhoto.objects.filter(
uploader__username__in=["testuser", "moderator"])
uploader__username__in=["testuser", "moderator"]
)
park_count = park_photos.count()
park_photos.delete()
self.stdout.write(self.style.SUCCESS(f"Deleted {park_count} test park photos"))
ride_photos = RidePhoto.objects.filter(
uploader__username__in=["testuser", "moderator"])
uploader__username__in=["testuser", "moderator"]
)
ride_count = ride_photos.count()
ride_photos.delete()
self.stdout.write(self.style.SUCCESS(f"Deleted {ride_count} test ride photos"))

View File

@@ -30,7 +30,7 @@ class Command(BaseCommand):
discord_app.secret = "ece7Pe_M4mD4mYzAgcINjTEKL_3ftL11"
discord_app.save()
discord_app.sites.add(site)
self.stdout.write(f'{"Created" if created else "Updated"} Discord app')
self.stdout.write(f"{'Created' if created else 'Updated'} Discord app")
# Create Google app
google_app, created = SocialApp.objects.get_or_create(
@@ -52,4 +52,4 @@ class Command(BaseCommand):
google_app.secret = "GOCSPX-Wd_0Ue0Ue0Ue0Ue0Ue0Ue0Ue0Ue"
google_app.save()
google_app.sites.add(site)
self.stdout.write(f'{"Created" if created else "Updated"} Google app')
self.stdout.write(f"{'Created' if created else 'Updated'} Google app")

View File

@@ -23,10 +23,7 @@ class Command(BaseCommand):
secret=os.getenv("GOOGLE_CLIENT_SECRET"),
)
google_app.sites.add(site)
self.stdout.write(
f"Created Google app with client_id: {
google_app.client_id}"
)
self.stdout.write(f"Created Google app with client_id: {google_app.client_id}")
# Create Discord provider
discord_app = SocialApp.objects.create(

View File

@@ -11,8 +11,5 @@ class Command(BaseCommand):
# This will trigger the avatar generation logic in the save method
profile.save()
self.stdout.write(
self.style.SUCCESS(
f"Regenerated avatar for {
profile.user.username}"
)
self.style.SUCCESS(f"Regenerated avatar for {profile.user.username}")
)

View File

@@ -102,12 +102,7 @@ class Command(BaseCommand):
self.stdout.write("Superuser created.")
except Exception as e:
self.stdout.write(
self.style.ERROR(
f"Error creating superuser: {
str(e)}"
)
)
self.stdout.write(self.style.ERROR(f"Error creating superuser: {str(e)}"))
raise
self.stdout.write(self.style.SUCCESS("Database reset complete."))

View File

@@ -41,9 +41,4 @@ class Command(BaseCommand):
self.stdout.write(f" - {perm.codename}")
except Exception as e:
self.stdout.write(
self.style.ERROR(
f"Error setting up groups: {
str(e)}"
)
)
self.stdout.write(self.style.ERROR(f"Error setting up groups: {str(e)}"))

View File

@@ -20,20 +20,24 @@ class Command(BaseCommand):
# DEBUG: Log environment variable values
self.stdout.write(
f"DEBUG: google_client_id type: {
type(google_client_id)}, value: {google_client_id}"
f"DEBUG: google_client_id type: {type(google_client_id)}, value: {
google_client_id
}"
)
self.stdout.write(
f"DEBUG: google_client_secret type: {
type(google_client_secret)}, value: {google_client_secret}"
f"DEBUG: google_client_secret type: {type(google_client_secret)}, value: {
google_client_secret
}"
)
self.stdout.write(
f"DEBUG: discord_client_id type: {
type(discord_client_id)}, value: {discord_client_id}"
f"DEBUG: discord_client_id type: {type(discord_client_id)}, value: {
discord_client_id
}"
)
self.stdout.write(
f"DEBUG: discord_client_secret type: {
type(discord_client_secret)}, value: {discord_client_secret}"
f"DEBUG: discord_client_secret type: {type(discord_client_secret)}, value: {
discord_client_secret
}"
)
if not all(
@@ -51,16 +55,13 @@ class Command(BaseCommand):
f"DEBUG: google_client_id is None: {google_client_id is None}"
)
self.stdout.write(
f"DEBUG: google_client_secret is None: {
google_client_secret is None}"
f"DEBUG: google_client_secret is None: {google_client_secret is None}"
)
self.stdout.write(
f"DEBUG: discord_client_id is None: {
discord_client_id is None}"
f"DEBUG: discord_client_id is None: {discord_client_id is None}"
)
self.stdout.write(
f"DEBUG: discord_client_secret is None: {
discord_client_secret is None}"
f"DEBUG: discord_client_secret is None: {discord_client_secret is None}"
)
return
@@ -81,7 +82,8 @@ class Command(BaseCommand):
if not created:
self.stdout.write(
f"DEBUG: About to assign google_client_id: {google_client_id} (type: {
type(google_client_id)})"
type(google_client_id)
})"
)
if google_client_id is not None and google_client_secret is not None:
google_app.client_id = google_client_id
@@ -108,7 +110,8 @@ class Command(BaseCommand):
if not created:
self.stdout.write(
f"DEBUG: About to assign discord_client_id: {discord_client_id} (type: {
type(discord_client_id)})"
type(discord_client_id)
})"
)
if discord_client_id is not None and discord_client_secret is not None:
discord_app.client_id = discord_client_id

View File

@@ -21,7 +21,7 @@ class Command(BaseCommand):
site.domain = "localhost:8000"
site.name = "ThrillWiki Development"
site.save()
self.stdout.write(f'{"Created" if _ else "Updated"} site: {site.domain}')
self.stdout.write(f"{'Created' if _ else 'Updated'} site: {site.domain}")
# Create superuser if it doesn't exist
if not User.objects.filter(username="admin").exists():

View File

@@ -19,5 +19,5 @@ class Command(BaseCommand):
for site in sites:
app.sites.add(site)
self.stdout.write(
f'Added sites: {", ".join(site.domain for site in sites)}'
f"Added sites: {', '.join(site.domain for site in sites)}"
)

View File

@@ -31,12 +31,9 @@ class Command(BaseCommand):
self.stdout.write("\nOAuth2 settings in settings.py:")
discord_settings = settings.SOCIALACCOUNT_PROVIDERS.get("discord", {})
self.stdout.write(
f'PKCE Enabled: {
discord_settings.get(
"OAUTH_PKCE_ENABLED",
False)}'
f"PKCE Enabled: {discord_settings.get('OAUTH_PKCE_ENABLED', False)}"
)
self.stdout.write(f'Scopes: {discord_settings.get("SCOPE", [])}')
self.stdout.write(f"Scopes: {discord_settings.get('SCOPE', [])}")
except SocialApp.DoesNotExist:
self.stdout.write(self.style.ERROR("Discord app not found"))

View File

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

View File

@@ -5,7 +5,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("accounts", "0001_initial"),
]

View File

@@ -10,7 +10,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0002_remove_toplistevent_pgh_context_and_more"),
("pghistory", "0007_auto_20250421_0444"),

View File

@@ -1,7 +1,6 @@
from rest_framework import serializers
from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.utils.crypto import get_random_string
from django.utils import timezone
from datetime import timedelta

View File

@@ -42,9 +42,9 @@ def create_user_profile(sender, instance, created, **kwargs):
profile.avatar.save(file_name, File(img_temp), save=True)
except Exception as e:
print(
f"Error downloading avatar for user {
instance.username}: {
str(e)}"
f"Error downloading avatar for user {instance.username}: {
str(e)
}"
)
except Exception as e:
print(f"Error creating profile for user {instance.username}: {str(e)}")
@@ -117,9 +117,7 @@ def sync_user_role_with_groups(sender, instance, **kwargs):
pass
except Exception as e:
print(
f"Error syncing role with groups for user {
instance.username}: {
str(e)}"
f"Error syncing role with groups for user {instance.username}: {str(e)}"
)

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)

View File

@@ -8,7 +8,6 @@ Create Date: 2025-06-17 15:00:00.000000
from alembic import op
import sqlalchemy as sa
import json
# revision identifiers, used by Alembic.
revision = "20250617"

View File

@@ -100,12 +100,9 @@ def cache_api_response(
)
else:
logger.debug(
f"Not caching response for view {
view_func.__name__} (status: {
getattr(
response,
'status_code',
'unknown')})"
f"Not caching response for view {view_func.__name__} (status: {
getattr(response, 'status_code', 'unknown')
})"
)
return response
@@ -135,10 +132,7 @@ def cache_queryset_result(
cache_key = cache_key_template.format(*args, **kwargs)
except (KeyError, IndexError):
# Fallback to simpler key generation
cache_key = f"{cache_key_template}:{
hash(
str(args) +
str(kwargs))}"
cache_key = f"{cache_key_template}:{hash(str(args) + str(kwargs))}"
cache_service = EnhancedCacheService()
cached_result = getattr(cache_service, cache_backend + "_cache").get(
@@ -146,10 +140,7 @@ def cache_queryset_result(
)
if cached_result is not None:
logger.debug(
f"Cache hit for queryset operation: {
func.__name__}"
)
logger.debug(f"Cache hit for queryset operation: {func.__name__}")
return cached_result
# Execute function and cache result
@@ -314,9 +305,9 @@ def smart_cache(
"kwargs": json.dumps(kwargs, sort_keys=True, default=str),
}
key_string = json.dumps(key_data, sort_keys=True)
cache_key = f"smart_cache:{
hashlib.md5(
key_string.encode()).hexdigest()}"
cache_key = (
f"smart_cache:{hashlib.md5(key_string.encode()).hexdigest()}"
)
# Try to get from cache
cache_service = EnhancedCacheService()

View File

@@ -57,13 +57,11 @@ class CacheHealthCheck(BaseHealthCheckBackend):
memory_usage_percent = (used_memory / max_memory) * 100
if memory_usage_percent > 90:
self.add_error(
f"Redis memory usage critical: {
memory_usage_percent:.1f}%"
f"Redis memory usage critical: {memory_usage_percent:.1f}%"
)
elif memory_usage_percent > 80:
logger.warning(
f"Redis memory usage high: {
memory_usage_percent:.1f}%"
f"Redis memory usage high: {memory_usage_percent:.1f}%"
)
except ImportError:
@@ -190,10 +188,7 @@ class ApplicationHealthCheck(BaseHealthCheckBackend):
import os
if not os.path.exists(settings.MEDIA_ROOT):
self.add_error(
f"Media directory does not exist: {
settings.MEDIA_ROOT}"
)
self.add_error(f"Media directory does not exist: {settings.MEDIA_ROOT}")
if not os.path.exists(settings.STATIC_ROOT) and not settings.DEBUG:
self.add_error(
@@ -305,8 +300,7 @@ class DiskSpaceHealthCheck(BaseHealthCheckBackend):
)
elif media_free_percent < 20:
logger.warning(
f"Low disk space: {
media_free_percent:.1f}% free in media directory"
f"Low disk space: {media_free_percent:.1f}% free in media directory"
)
if logs_free_percent < 10:
@@ -316,8 +310,7 @@ class DiskSpaceHealthCheck(BaseHealthCheckBackend):
)
elif logs_free_percent < 20:
logger.warning(
f"Low disk space: {
logs_free_percent:.1f}% free in logs directory"
f"Low disk space: {logs_free_percent:.1f}% free in logs directory"
)
except Exception as e:

View File

@@ -5,8 +5,6 @@ This command automatically sets up the development environment and starts
the server, replacing the need for the dev_server.sh script.
"""
import subprocess
import sys
from django.core.management.base import BaseCommand
from django.core.management import execute_from_command_line
@@ -62,7 +60,7 @@ class Command(BaseCommand):
self.stdout.write("")
self.stdout.write(
self.style.SUCCESS(
f'🌟 Starting Django development server on http://{options["host"]}:{options["port"]}'
f"🌟 Starting Django development server on http://{options['host']}:{options['port']}"
)
)
self.stdout.write("Press Ctrl+C to stop the server")
@@ -74,12 +72,12 @@ class Command(BaseCommand):
[
"manage.py",
"runserver_plus",
f'{options["host"]}:{options["port"]}',
f"{options['host']}:{options['port']}",
]
)
else:
execute_from_command_line(
["manage.py", "runserver", f'{options["host"]}:{options["port"]}']
["manage.py", "runserver", f"{options['host']}:{options['port']}"]
)
except KeyboardInterrupt:
self.stdout.write("")

View File

@@ -5,13 +5,11 @@ This command performs all the setup tasks that the dev_server.sh script does,
allowing the project to run without requiring the shell script.
"""
import os
import subprocess
import sys
from pathlib import Path
from django.core.management.base import BaseCommand
from django.core.management import execute_from_command_line
from django.conf import settings

View File

@@ -5,10 +5,6 @@ Analytics and tracking middleware for Django application.
import pghistory
from django.contrib.auth.models import AnonymousUser
from django.core.handlers.wsgi import WSGIRequest
from django.utils.deprecation import MiddlewareMixin
from django.contrib.contenttypes.models import ContentType
from django.views.generic.detail import DetailView
from apps.core.analytics import PageView
class RequestContextProvider(pghistory.context):

View File

@@ -151,12 +151,10 @@ class PerformanceMiddleware(MiddlewareMixin):
}
performance_logger.error(
f"Request exception: {
request.method} {
request.path} - "
f"{
duration:.3f}s, {total_queries} queries, {
type(exception).__name__}: {exception}",
f"Request exception: {request.method} {request.path} - "
f"{duration:.3f}s, {total_queries} queries, {type(exception).__name__}: {
exception
}",
extra=performance_data,
)
@@ -216,10 +214,10 @@ class QueryCountMiddleware(MiddlewareMixin):
if request_query_count > self.query_limit:
logger.warning(
f"Excessive query count: {
request.path} executed {request_query_count} queries "
f"(limit: {
self.query_limit})",
f"Excessive query count: {request.path} executed {
request_query_count
} queries "
f"(limit: {self.query_limit})",
extra={
"path": request.path,
"method": request.method,
@@ -308,9 +306,7 @@ class CachePerformanceMiddleware(MiddlewareMixin):
)
else:
logger.debug(
f"Cache performance for {
request.path}: {
hit_rate:.1f}% hit rate",
f"Cache performance for {request.path}: {hit_rate:.1f}% hit rate",
extra=cache_data,
)

View File

@@ -8,14 +8,13 @@ analytics for the trending algorithm.
import logging
import re
from datetime import datetime, timedelta
from datetime import timedelta
from typing import Optional, Union
from django.http import HttpRequest, HttpResponse
from django.utils import timezone
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.conf import settings
from django.db import models
from apps.core.analytics import PageView
from apps.parks.models import Park
@@ -68,7 +67,6 @@ class ViewTrackingMiddleware:
and 200 <= response.status_code < 300
and not self._should_skip_tracking(request)
):
try:
self._track_view_if_applicable(request)
except Exception as e:

View File

@@ -5,7 +5,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 = [
("contenttypes", "0002_remove_content_type_name"),
("core", "0001_initial"),

View File

@@ -7,7 +7,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("core", "0002_historicalslug_pageview"),

View File

@@ -50,8 +50,7 @@ class EnhancedCacheService:
# Log cache miss and function execution time
logger.info(
f"Cache miss for key '{cache_key}', executed in {
duration:.3f}s",
f"Cache miss for key '{cache_key}', executed in {duration:.3f}s",
extra={"cache_key": cache_key, "execution_time": duration},
)
@@ -96,11 +95,9 @@ class EnhancedCacheService:
):
"""Cache geographic data with spatial keys"""
# Generate spatial cache key based on bounds and zoom level
cache_key = f"geo:{
bounds.min_lat}:{
bounds.min_lng}:{
bounds.max_lat}:{
bounds.max_lng}:z{zoom_level}"
cache_key = f"geo:{bounds.min_lat}:{bounds.min_lng}:{bounds.max_lat}:{
bounds.max_lng
}:z{zoom_level}"
self.default_cache.set(cache_key, data, timeout)
logger.debug(f"Cached geographic data for bounds {bounds}")
@@ -108,11 +105,9 @@ class EnhancedCacheService:
self, bounds: "GeoBounds", zoom_level: int
) -> Optional[Any]:
"""Retrieve cached geographic data"""
cache_key = f"geo:{
bounds.min_lat}:{
bounds.min_lng}:{
bounds.max_lat}:{
bounds.max_lng}:z{zoom_level}"
cache_key = f"geo:{bounds.min_lat}:{bounds.min_lng}:{bounds.max_lat}:{
bounds.max_lng
}:z{zoom_level}"
return self.default_cache.get(cache_key)
# Cache invalidation utilities
@@ -206,10 +201,7 @@ def cache_api_response(timeout=1800, vary_on=None, key_prefix=""):
response = view_func(self, request, *args, **kwargs)
if hasattr(response, "status_code") and response.status_code == 200:
cache_service.api_cache.set(cache_key, response, timeout)
logger.debug(
f"Cached API response for view {
view_func.__name__}"
)
logger.debug(f"Cached API response for view {view_func.__name__}")
return response
@@ -273,10 +265,7 @@ class CacheWarmer:
try:
self.cache_service.warm_cache(**operation)
except Exception as e:
logger.error(
f"Error warming cache for {
operation['cache_key']}: {e}"
)
logger.error(f"Error warming cache for {operation['cache_key']}: {e}")
# Cache statistics and monitoring

View File

@@ -2,7 +2,6 @@
Location adapters for converting between domain-specific models and UnifiedLocation.
"""
from django.db import models
from typing import List, Optional
from django.db.models import QuerySet
from django.urls import reverse

View File

@@ -447,13 +447,10 @@ class LocationSearchService:
suggestions.append(
{
"type": "city",
"name": f"{
city_data['city']}, {
city_data['state']}",
"address": f"{
city_data['city']}, {
city_data['state']}, {
city_data['country']}",
"name": f"{city_data['city']}, {city_data['state']}",
"address": f"{city_data['city']}, {city_data['state']}, {
city_data['country']
}",
"coordinates": None,
}
)

View File

@@ -289,11 +289,7 @@ class MapCacheService:
"""Record query performance metrics for analysis."""
try:
# 5-minute buckets
stats_key = f"{
self.STATS_PREFIX}:performance:{
int(
time.time() //
300)}"
stats_key = f"{self.STATS_PREFIX}:performance:{int(time.time() // 300)}"
current_stats = cache.get(
stats_key,

View File

@@ -21,10 +21,7 @@ class MediaService:
@staticmethod
def generate_upload_path(
domain: str,
identifier: str,
filename: str,
subdirectory: Optional[str] = None
domain: str, identifier: str, filename: str, subdirectory: Optional[str] = None
) -> str:
"""
Generate standardized upload path for media files.
@@ -86,16 +83,26 @@ class MediaService:
"""
try:
# Check file size
max_size = getattr(settings, 'MAX_PHOTO_SIZE',
10 * 1024 * 1024) # 10MB default
max_size = getattr(
settings, "MAX_PHOTO_SIZE", 10 * 1024 * 1024
) # 10MB default
if image_file.size > max_size:
return False, f"File size too large. Maximum size is {max_size // (1024 * 1024)}MB"
return (
False,
f"File size too large. Maximum size is {max_size // (1024 * 1024)}MB",
)
# Check file type
allowed_types = getattr(settings, 'ALLOWED_PHOTO_TYPES', [
'image/jpeg', 'image/png', 'image/webp'])
allowed_types = getattr(
settings,
"ALLOWED_PHOTO_TYPES",
["image/jpeg", "image/png", "image/webp"],
)
if image_file.content_type not in allowed_types:
return False, f"File type not allowed. Allowed types: {', '.join(allowed_types)}"
return (
False,
f"File type not allowed. Allowed types: {', '.join(allowed_types)}",
)
# Try to open with PIL to validate it's a real image
with Image.open(image_file) as img:
@@ -111,7 +118,7 @@ class MediaService:
image_file: UploadedFile,
max_width: int = 1920,
max_height: int = 1080,
quality: int = 85
quality: int = 85,
) -> UploadedFile:
"""
Process and optimize image file.
@@ -128,8 +135,8 @@ class MediaService:
try:
with Image.open(image_file) as img:
# Convert to RGB if necessary
if img.mode in ('RGBA', 'LA', 'P'):
img = img.convert('RGB')
if img.mode in ("RGBA", "LA", "P"):
img = img.convert("RGB")
# Resize if necessary
if img.width > max_width or img.height > max_height:
@@ -140,16 +147,16 @@ class MediaService:
from django.core.files.uploadedfile import InMemoryUploadedFile
output = BytesIO()
img.save(output, format='JPEG', quality=quality, optimize=True)
img.save(output, format="JPEG", quality=quality, optimize=True)
output.seek(0)
return InMemoryUploadedFile(
output,
'ImageField',
"ImageField",
f"{os.path.splitext(image_file.name)[0]}.jpg",
'image/jpeg',
"image/jpeg",
output.getbuffer().nbytes,
None
None,
)
except Exception as e:
@@ -168,6 +175,7 @@ class MediaService:
Default caption string
"""
from django.utils import timezone
current_time = timezone.now()
return f"Uploaded by {username} on {current_time.strftime('%B %d, %Y at %I:%M %p')}"
@@ -185,7 +193,7 @@ class MediaService:
"total_files": 0,
"total_size_bytes": 0,
"storage_backend": "default",
"available_space": "unknown"
"available_space": "unknown",
}
except Exception as e:
logger.error(f"Failed to get storage stats: {str(e)}")

View File

@@ -57,16 +57,16 @@ def monitor_performance(operation_name: str, **tags):
)
logger.log(
log_level,
f"Performance: {operation_name} completed in {
duration:.3f}s with {total_queries} queries",
f"Performance: {operation_name} completed in {duration:.3f}s with {
total_queries
} queries",
extra=performance_context,
)
# Log slow operations with additional detail
if duration > 2.0:
logger.warning(
f"Slow operation detected: {operation_name} took {
duration:.3f}s",
f"Slow operation detected: {operation_name} took {duration:.3f}s",
extra={
"slow_operation": True,
"threshold_exceeded": "duration",
@@ -246,9 +246,9 @@ class PerformanceProfiler:
log_level = logging.WARNING if total_duration > 1.0 else logging.INFO
logger.log(
log_level,
f"Profiling complete: {
self.name} took {
total_duration:.3f}s with {total_queries} queries",
f"Profiling complete: {self.name} took {total_duration:.3f}s with {
total_queries
} queries",
extra=report,
)

View File

@@ -395,7 +395,9 @@ class TrendingService:
"""Calculate popularity score based on total view count."""
try:
total_views = PageView.get_total_views_count(
content_type, object_id, hours=168 # Last 7 days
content_type,
object_id,
hours=168, # Last 7 days
)
# Normalize views to 0-1 scale

View File

@@ -323,10 +323,7 @@ class IndexAnalyzer:
common_filter_fields = ["slug", "name", "created_at", "updated_at"]
for field in opts.fields:
if field.name in common_filter_fields and not field.db_index:
suggestions.append(
f"Consider adding db_index=True to {
field.name}"
)
suggestions.append(f"Consider adding db_index=True to {field.name}")
return suggestions
@@ -419,9 +416,9 @@ def monitor_db_performance(operation_name: str):
if duration > 1.0 or total_queries > 15 or slow_queries:
logger.warning(
f"Performance issue in {operation_name}: "
f"{
duration:.3f}s, {total_queries} queries, {
len(slow_queries)} slow",
f"{duration:.3f}s, {total_queries} queries, {
len(slow_queries)
} slow",
extra=performance_data,
)
else:

View File

@@ -41,11 +41,7 @@ class MapAPIView(View):
response["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
# Add performance headers
response["X-Response-Time"] = (
f"{(time.time() -
start_time) *
1000:.2f}ms"
)
response["X-Response-Time"] = f"{(time.time() - start_time) * 1000:.2f}ms"
# Add compression hint for large responses
if hasattr(response, "content") and len(response.content) > 1024:
@@ -55,9 +51,7 @@ class MapAPIView(View):
except Exception as e:
logger.error(
f"API error in {
request.path}: {
str(e)}",
f"API error in {request.path}: {str(e)}",
exc_info=True,
)
return self._error_response("An internal server error occurred", status=500)
@@ -412,7 +406,8 @@ class MapLocationDetailView(MapAPIView):
if location_type not in valid_types:
return self._error_response(
f"Invalid location type: {location_type}. Valid types: {
', '.join(valid_types)}",
', '.join(valid_types)
}",
400,
error_code="INVALID_LOCATION_TYPE",
)
@@ -450,8 +445,7 @@ class MapLocationDetailView(MapAPIView):
return self._error_response(str(e), 400, error_code="INVALID_PARAMETER")
except Exception as e:
logger.error(
f"Error in MapLocationDetailView: {
str(e)}",
f"Error in MapLocationDetailView: {str(e)}",
exc_info=True,
)
return self._error_response(
@@ -606,8 +600,7 @@ class MapBoundsView(MapAPIView):
return self._error_response(str(e), 400)
except Exception as e:
return self._error_response(
f"Internal server error: {
str(e)}",
f"Internal server error: {str(e)}",
500,
)
@@ -628,8 +621,7 @@ class MapStatsView(MapAPIView):
except Exception as e:
return self._error_response(
f"Internal server error: {
str(e)}",
f"Internal server error: {str(e)}",
500,
)
@@ -657,8 +649,7 @@ class MapCacheView(MapAPIView):
except Exception as e:
return self._error_response(
f"Internal server error: {
str(e)}",
f"Internal server error: {str(e)}",
500,
)
@@ -693,7 +684,6 @@ class MapCacheView(MapAPIView):
return self._error_response(f"Invalid request data: {str(e)}", 400)
except Exception as e:
return self._error_response(
f"Internal server error: {
str(e)}",
f"Internal server error: {str(e)}",
500,
)

View File

@@ -143,8 +143,7 @@ class NearbyLocationsView(MapViewMixin, TemplateView):
context.update(
{
"page_title": f"Locations Near {
center_lat:.4f}, {
"page_title": f"Locations Near {center_lat:.4f}, {
center_lng:.4f}",
"map_type": "nearby",
"center_coordinates": {

View File

@@ -52,10 +52,7 @@ class ForwardEmailBackend(BaseEmailBackend):
try:
config = EmailConfiguration.objects.get(site=site)
except EmailConfiguration.DoesNotExist:
raise ValueError(
f"Email configuration not found for site: {
site.domain}"
)
raise ValueError(f"Email configuration not found for site: {site.domain}")
# Get the from email, falling back to site's default if not provided
if email_message.from_email:

View File

@@ -86,17 +86,14 @@ class Command(BaseCommand):
else:
self.stdout.write(
self.style.WARNING(
f"Registration returned status {
response.status_code}: {
response.content.decode()}\n"
f"Registration returned status {response.status_code}: {
response.content.decode()
}\n"
)
)
except Exception as e:
self.stdout.write(
self.style.ERROR(
f"Registration email test failed: {
str(e)}\n"
)
self.style.ERROR(f"Registration email test failed: {str(e)}\n")
)
def test_password_change(self, user):
@@ -120,17 +117,14 @@ class Command(BaseCommand):
else:
self.stdout.write(
self.style.WARNING(
f"Password change returned status {
response.status_code}: {
response.content.decode()}\n"
f"Password change returned status {response.status_code}: {
response.content.decode()
}\n"
)
)
except Exception as e:
self.stdout.write(
self.style.ERROR(
f"Password change email test failed: {
str(e)}\n"
)
self.style.ERROR(f"Password change email test failed: {str(e)}\n")
)
def test_email_change(self, user):
@@ -151,17 +145,14 @@ class Command(BaseCommand):
else:
self.stdout.write(
self.style.WARNING(
f"Email change returned status {
response.status_code}: {
response.content.decode()}\n"
f"Email change returned status {response.status_code}: {
response.content.decode()
}\n"
)
)
except Exception as e:
self.stdout.write(
self.style.ERROR(
f"Email change verification test failed: {
str(e)}\n"
)
self.style.ERROR(f"Email change verification test failed: {str(e)}\n")
)
def test_password_reset(self, user):
@@ -182,15 +173,12 @@ class Command(BaseCommand):
else:
self.stdout.write(
self.style.WARNING(
f"Password reset returned status {
response.status_code}: {
response.content.decode()}\n"
f"Password reset returned status {response.status_code}: {
response.content.decode()
}\n"
)
)
except Exception as e:
self.stdout.write(
self.style.ERROR(
f"Password reset email test failed: {
str(e)}\n"
)
self.style.ERROR(f"Password reset email test failed: {str(e)}\n")
)

View File

@@ -84,7 +84,7 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS("Using configuration:"))
self.stdout.write(f" From: {from_email}")
self.stdout.write(f" To: {to_email}")
self.stdout.write(f' API Key: {"*" * len(api_key)}')
self.stdout.write(f" API Key: {'*' * len(api_key)}")
self.stdout.write(f" Site: {site.domain}")
try:
@@ -132,10 +132,7 @@ class Command(BaseCommand):
return config
except Exception as e:
self.stdout.write(
self.style.ERROR(
f"✗ Site configuration failed: {
str(e)}"
)
self.style.ERROR(f"✗ Site configuration failed: {str(e)}")
)
raise
@@ -164,8 +161,8 @@ class Command(BaseCommand):
self.stdout.write(
self.style.ERROR(
f"✗ API endpoint test failed with status {
response.status_code}: {
response.text}"
response.status_code
}: {response.text}"
)
)
raise Exception(f"API test failed: {response.text}")
@@ -178,12 +175,7 @@ class Command(BaseCommand):
)
raise Exception("Could not connect to Django server")
except Exception as e:
self.stdout.write(
self.style.ERROR(
f"✗ API endpoint test failed: {
str(e)}"
)
)
self.stdout.write(self.style.ERROR(f"✗ API endpoint test failed: {str(e)}"))
raise
def test_email_backend(self, to_email, site):
@@ -196,8 +188,7 @@ class Command(BaseCommand):
# Debug output
self.stdout.write(
f" Debug: Using from_email: {
site.email_config.default_from_email}"
f" Debug: Using from_email: {site.email_config.default_from_email}"
)
self.stdout.write(f" Debug: Using to_email: {to_email}")
@@ -212,10 +203,7 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS("✓ Email backend test successful"))
except Exception as e:
self.stdout.write(
self.style.ERROR(
f"✗ Email backend test failed: {
str(e)}"
)
self.style.ERROR(f"✗ Email backend test failed: {str(e)}")
)
raise
@@ -236,9 +224,6 @@ class Command(BaseCommand):
return response
except Exception as e:
self.stdout.write(
self.style.ERROR(
f"✗ Direct EmailService test failed: {
str(e)}"
)
self.style.ERROR(f"✗ Direct EmailService test failed: {str(e)}")
)
raise

View File

@@ -7,7 +7,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
class Migration(migrations.Migration):
dependencies = [
("email_service", "0001_initial"),
]

View File

@@ -34,9 +34,7 @@ class EmailService:
# Use provided from_email or construct from config
if not from_email:
from_email = f"{
email_config.from_name} <{
email_config.from_email}>"
from_email = f"{email_config.from_name} <{email_config.from_email}>"
elif "<" not in from_email:
# If from_email is provided but doesn't include a name, add the
# configured name
@@ -101,8 +99,9 @@ class EmailService:
if response.status_code != 200:
error_message = response.text if response.text else "Unknown error"
raise Exception(
f"Failed to send email (Status {
response.status_code}): {error_message}"
f"Failed to send email (Status {response.status_code}): {
error_message
}"
)
return response.json()

View File

@@ -8,7 +8,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
class Migration(migrations.Migration):
dependencies = [
("moderation", "0001_initial"),
]

View File

@@ -171,10 +171,9 @@ class EditSubmission(TrackedModel):
self.status = "REJECTED"
self.handled_by = user # type: ignore
self.handled_at = timezone.now()
self.notes = f"A {
model_class.__name__} with the name '{
prepared_data['name']}' already exists (ID: {
existing_obj.pk})"
self.notes = f"A {model_class.__name__} with the name '{
prepared_data['name']
}' already exists (ID: {existing_obj.pk})"
self.save()
raise ValueError(self.notes)
@@ -279,9 +278,7 @@ class PhotoSubmission(TrackedModel):
]
def __str__(self) -> str:
return f"Photo submission by {
self.user.username} for {
self.content_object}"
return f"Photo submission by {self.user.username} for {self.content_object}"
def approve(self, moderator: UserType, notes: str = "") -> None:
"""Approve the photo submission"""

View File

@@ -106,9 +106,7 @@ class Command(BaseCommand):
)
self.created_companies[company.slug] = company
self.stdout.write(
f' {
"Created" if created else "Found"} park company: {
company.name}'
f" {'Created' if created else 'Found'} park company: {company.name}"
)
# Ride manufacturers and designers (using rides.models.Company)
@@ -201,9 +199,7 @@ class Command(BaseCommand):
)
self.created_companies[company.slug] = company
self.stdout.write(
f' {
"Created" if created else "Found"} ride company: {
company.name}'
f" {'Created' if created else 'Found'} ride company: {company.name}"
)
def create_parks(self):

View File

@@ -53,7 +53,7 @@ class Command(BaseCommand):
)
companies[operator.name] = operator
self.stdout.write(
f'{"Created" if created else "Found"} company: {operator.name}'
f"{'Created' if created else 'Found'} company: {operator.name}"
)
# Create parks with their locations
@@ -301,7 +301,7 @@ class Command(BaseCommand):
"owner": company,
},
)
self.stdout.write(f'{"Created" if created else "Found"} park: {park.name}')
self.stdout.write(f"{'Created' if created else 'Found'} park: {park.name}")
# Create location for park
if created:
@@ -328,7 +328,7 @@ class Command(BaseCommand):
defaults={"description": area_data["description"]},
)
self.stdout.write(
f'{"Created" if created else "Found"} area: {area.name} in {park.name}'
f"{'Created' if created else 'Found'} area: {area.name} in {park.name}"
)
self.stdout.write(self.style.SUCCESS("Successfully seeded initial park data"))

View File

@@ -121,8 +121,7 @@ class Command(BaseCommand):
except Exception as e:
self.logger.error(
f"Error during data cleanup: {
str(e)}",
f"Error during data cleanup: {str(e)}",
exc_info=True,
)
self.stdout.write(
@@ -205,7 +204,7 @@ class Command(BaseCommand):
if missing_tables:
self.stdout.write(
self.style.WARNING(
f'Missing tables for models: {", ".join(missing_tables)}'
f"Missing tables for models: {', '.join(missing_tables)}"
)
)
return False
@@ -353,13 +352,13 @@ class Command(BaseCommand):
)
self.park_companies[data["name"]] = company
self.stdout.write(
f' {
"Created" if created else "Found"} park company: {
company.name}'
f" {'Created' if created else 'Found'} park company: {
company.name
}"
)
except Exception as e:
self.logger.error(
f'Error creating park company {data["name"]}: {str(e)}'
f"Error creating park company {data['name']}: {str(e)}"
)
raise
@@ -378,13 +377,13 @@ class Command(BaseCommand):
)
self.ride_companies[data["name"]] = company
self.stdout.write(
f' {
"Created" if created else "Found"} ride company: {
company.name}'
f" {'Created' if created else 'Found'} ride company: {
company.name
}"
)
except Exception as e:
self.logger.error(
f'Error creating ride company {data["name"]}: {str(e)}'
f"Error creating ride company {data['name']}: {str(e)}"
)
raise
@@ -532,9 +531,7 @@ class Command(BaseCommand):
)
self.parks[park_data["name"]] = park
self.stdout.write(
f' {
"Created" if created else "Found"} park: {
park.name}'
f" {'Created' if created else 'Found'} park: {park.name}"
)
# Create location for park
@@ -556,15 +553,15 @@ class Command(BaseCommand):
park_location.save()
except Exception as e:
self.logger.error(
f'Error creating location for park {
park_data["name"]}: {
str(e)}'
f"Error creating location for park {
park_data['name']
}: {str(e)}"
)
raise
except Exception as e:
self.logger.error(
f'Error creating park {park_data["name"]}: {str(e)}'
f"Error creating park {park_data['name']}: {str(e)}"
)
raise
@@ -631,15 +628,13 @@ class Command(BaseCommand):
)
self.ride_models[model_data["name"]] = model
self.stdout.write(
f' {
"Created" if created else "Found"} ride model: {
model.name}'
f" {'Created' if created else 'Found'} ride model: {
model.name
}"
)
except Exception as e:
self.logger.error(
f'Error creating ride model {
model_data["name"]}: {
str(e)}'
f"Error creating ride model {model_data['name']}: {str(e)}"
)
raise
@@ -860,9 +855,7 @@ class Command(BaseCommand):
)
self.rides[ride_data["name"]] = ride
self.stdout.write(
f' {
"Created" if created else "Found"} ride: {
ride.name}'
f" {'Created' if created else 'Found'} ride: {ride.name}"
)
# Create roller coaster stats if provided
@@ -872,15 +865,15 @@ class Command(BaseCommand):
RollerCoasterStats.objects.create(ride=ride, **stats_data)
except Exception as e:
self.logger.error(
f'Error creating stats for ride {
ride_data["name"]}: {
str(e)}'
f"Error creating stats for ride {ride_data['name']}: {
str(e)
}"
)
raise
except Exception as e:
self.logger.error(
f'Error creating ride {ride_data["name"]}: {str(e)}'
f"Error creating ride {ride_data['name']}: {str(e)}"
)
raise
@@ -1013,16 +1006,13 @@ class Command(BaseCommand):
},
)
self.stdout.write(
f' {
"Created" if created else "Found"} area: {
area.name} in {
park.name}'
f" {'Created' if created else 'Found'} area: {
area.name
} in {park.name}"
)
except Exception as e:
self.logger.error(
f'Error creating areas for park {
area_group["park"]}: {
str(e)}'
f"Error creating areas for park {area_group['park']}: {str(e)}"
)
raise
@@ -1095,15 +1085,15 @@ class Command(BaseCommand):
},
)
self.stdout.write(
f' {
"Created" if created else "Found"} park review: {
review.title}'
f" {'Created' if created else 'Found'} park review: {
review.title
}"
)
except Exception as e:
self.logger.error(
f'Error creating park review for {
review_data["park"]}: {
str(e)}'
f"Error creating park review for {review_data['park']}: {
str(e)
}"
)
raise
@@ -1154,15 +1144,15 @@ class Command(BaseCommand):
},
)
self.stdout.write(
f' {
"Created" if created else "Found"} ride review: {
review.title}'
f" {'Created' if created else 'Found'} ride review: {
review.title
}"
)
except Exception as e:
self.logger.error(
f'Error creating ride review for {
review_data["ride"]}: {
str(e)}'
f"Error creating ride review for {review_data['ride']}: {
str(e)
}"
)
raise

View File

@@ -55,10 +55,7 @@ class Command(BaseCommand):
# Test Park model integration
self.stdout.write("\n🔍 Testing Park model integration:")
self.stdout.write(
f" Park formatted location: {
park.formatted_location}"
)
self.stdout.write(f" Park formatted location: {park.formatted_location}")
self.stdout.write(f" Park coordinates: {park.coordinates}")
# Create another location for distance testing
@@ -112,10 +109,7 @@ class Command(BaseCommand):
nearby_locations = ParkLocation.objects.filter(
point__distance_lte=(search_point, D(km=100))
)
self.stdout.write(
f" Found {
nearby_locations.count()} parks within 100km"
)
self.stdout.write(f" Found {nearby_locations.count()} parks within 100km")
for loc in nearby_locations:
self.stdout.write(f" - {loc.park.name} in {loc.city}, {loc.state}")
except Exception as e:

View File

@@ -1,6 +1,6 @@
# Generated manually for enhanced filtering performance
from django.db import migrations, models
from django.db import migrations
class Migration(migrations.Migration):

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("parks", "0002_alter_parkarea_unique_together"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),

View File

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

View File

@@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("parks", "0001_add_filter_indexes"),
("parks", "0004_fix_pghistory_triggers"),

View File

@@ -6,7 +6,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("parks", "0005_merge_20250820_2020"),
]

View File

@@ -8,7 +8,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

@@ -0,0 +1,188 @@
# Generated by Django 5.2.5 on 2025-08-26 17:39
import apps.parks.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 = [
("parks", "0007_companyheadquartersevent_parklocationevent_and_more"),
("pghistory", "0007_auto_20250421_0444"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="ParkPhoto",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"image",
models.ImageField(
max_length=255,
upload_to=apps.parks.models.media.park_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)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("date_taken", models.DateTimeField(blank=True, null=True)),
(
"park",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="photos",
to="parks.park",
),
),
(
"uploaded_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="uploaded_park_photos",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-is_primary", "-created_at"],
},
),
migrations.CreateModel(
name="ParkPhotoEvent",
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.parks.models.media.park_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)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("date_taken", models.DateTimeField(blank=True, null=True)),
(
"park",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="parks.park",
),
),
(
"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="parks.parkphoto",
),
),
(
"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="parkphoto",
index=models.Index(
fields=["park", "is_primary"], name="parks_parkp_park_id_eda26e_idx"
),
),
migrations.AddIndex(
model_name="parkphoto",
index=models.Index(
fields=["park", "is_approved"], name="parks_parkp_park_id_5fe576_idx"
),
),
migrations.AddIndex(
model_name="parkphoto",
index=models.Index(
fields=["created_at"], name="parks_parkp_created_033dc3_idx"
),
),
migrations.AddConstraint(
model_name="parkphoto",
constraint=models.UniqueConstraint(
condition=models.Q(("is_primary", True)),
fields=("park",),
name="unique_primary_park_photo",
),
),
pgtrigger.migrations.AddTrigger(
model_name="parkphoto",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "parks_parkphotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image", "is_approved", "is_primary", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_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", NEW."park_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;',
hash="eeeb8afb335eb66cb4550a0f5abfaf7280472827",
operation="INSERT",
pgid="pgtrigger_insert_insert_e2033",
table="parks_parkphoto",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="parkphoto",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "parks_parkphotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image", "is_approved", "is_primary", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_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", NEW."park_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;',
hash="bd95069068ba9e1a78708a0a9cc73d6507fab691",
operation="UPDATE",
pgid="pgtrigger_update_update_42711",
table="parks_parkphoto",
when="AFTER",
),
),
),
]

View File

@@ -14,6 +14,7 @@ from .location import ParkLocation
from .reviews import ParkReview
from .companies import Company, CompanyHeadquarters
from .media import ParkPhoto
# Alias Company as Operator for clarity
Operator = Company

View File

@@ -8,7 +8,6 @@ from .parks import Park
@pghistory.track()
class ParkArea(TrackedModel):
# Import managers
from ..managers import ParkAreaManager

View File

@@ -7,7 +7,6 @@ import pghistory
@pghistory.track()
class Company(TrackedModel):
# Import managers
from ..managers import CompanyManager
@@ -107,13 +106,7 @@ class CompanyHeadquarters(models.Model):
components.append(self.postal_code)
if self.country and self.country != "USA":
components.append(self.country)
return (
", ".join(components)
if components
else f"{
self.city}, {
self.country}"
)
return ", ".join(components) if components else f"{self.city}, {self.country}"
@property
def location_display(self):

View File

@@ -7,7 +7,6 @@ This module contains media models specific to parks 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,16 +14,14 @@ import pghistory
def park_photo_upload_path(instance: models.Model, filename: str) -> str:
"""Generate upload path for park photos."""
photo = cast('ParkPhoto', instance)
photo = cast("ParkPhoto", instance)
park = photo.park
if park is None:
raise ValueError("Park cannot be None")
return MediaService.generate_upload_path(
domain="park",
identifier=park.slug,
filename=filename
domain="park", identifier=park.slug, filename=filename
)
@@ -33,9 +30,7 @@ class ParkPhoto(TrackedModel):
"""Photo model specific to parks."""
park = models.ForeignKey(
'parks.Park',
on_delete=models.CASCADE,
related_name='photos'
"parks.Park", on_delete=models.CASCADE, related_name="photos"
)
image = models.ImageField(
@@ -72,9 +67,9 @@ class ParkPhoto(TrackedModel):
constraints = [
# Only one primary photo per park
models.UniqueConstraint(
fields=['park'],
fields=["park"],
condition=models.Q(is_primary=True),
name='unique_primary_park_photo'
name="unique_primary_park_photo",
)
]

View File

@@ -13,7 +13,6 @@ if TYPE_CHECKING:
@pghistory.track()
class Park(TrackedModel):
# Import managers
from ..managers import ParkManager
@@ -226,7 +225,8 @@ class Park(TrackedModel):
if historical:
print(
f"Found historical slug record for object_id: {
historical.object_id}"
historical.object_id
}"
)
try:
park = cls.objects.get(pk=historical.object_id)
@@ -250,7 +250,8 @@ class Park(TrackedModel):
if historical_event:
print(
f"Found pghistory event for pgh_obj_id: {
historical_event.pgh_obj_id}"
historical_event.pgh_obj_id
}"
)
try:
park = cls.objects.get(pk=historical_event.pgh_obj_id)

View File

@@ -7,7 +7,6 @@ import pghistory
@pghistory.track()
class ParkReview(TrackedModel):
# Import managers
from ..managers import ParkReviewManager

Some files were not shown because too many files have changed in this diff Show More