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/ /uploads/
/backups/ /backups/
.django_tailwind_cli/ .django_tailwind_cli/
backend/.env
frontend/.env

View File

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

View File

@@ -3,4 +3,4 @@ Centralized API package for ThrillWiki.
This package contains all API endpoints organized by version. This package contains all API endpoints organized by version.
All API routes must be routed through this centralized structure. All API routes must be routed through this centralized structure.
""" """

View File

@@ -8,5 +8,5 @@ Currently supports v1 API endpoints.
from django.urls import path, include from django.urls import path, include
urlpatterns = [ 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, OpenApiExample,
) )
from django.contrib.auth.password_validation import validate_password 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.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 django.contrib.auth import get_user_model
from apps.accounts.models import UserProfile, TopList, TopListItem
UserModel = get_user_model() UserModel = get_user_model()
@@ -44,6 +38,7 @@ class ModelChoices:
# === AUTHENTICATION SERIALIZERS === # === AUTHENTICATION SERIALIZERS ===
@extend_schema_serializer( @extend_schema_serializer(
examples=[ examples=[
OpenApiExample( OpenApiExample(
@@ -298,6 +293,7 @@ class AuthStatusOutputSerializer(serializers.Serializer):
# === USER PROFILE SERIALIZERS === # === USER PROFILE SERIALIZERS ===
@extend_schema_serializer( @extend_schema_serializer(
examples=[ examples=[
OpenApiExample( OpenApiExample(
@@ -388,6 +384,7 @@ class UserProfileUpdateInputSerializer(serializers.Serializer):
# === TOP LIST SERIALIZERS === # === TOP LIST SERIALIZERS ===
@extend_schema_serializer( @extend_schema_serializer(
examples=[ examples=[
OpenApiExample( OpenApiExample(
@@ -447,6 +444,7 @@ class TopListUpdateInputSerializer(serializers.Serializer):
# === TOP LIST ITEM SERIALIZERS === # === TOP LIST ITEM SERIALIZERS ===
@extend_schema_serializer( @extend_schema_serializer(
examples=[ examples=[
OpenApiExample( OpenApiExample(

View File

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

View File

@@ -6,12 +6,9 @@ login, signup, logout, password management, social authentication,
user profiles, and top lists. user profiles, and top lists.
""" """
import time
from django.contrib.auth import authenticate, login, logout, get_user_model from django.contrib.auth import authenticate, login, logout, get_user_model
from django.contrib.sites.shortcuts import get_current_site from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils import timezone
from django.conf import settings
from django.db.models import Q from django.db.models import Q
from rest_framework import status from rest_framework import status
from rest_framework.views import APIView 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.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.decorators import action from rest_framework.decorators import action
from allauth.socialaccount import providers
from drf_spectacular.utils import extend_schema, extend_schema_view from drf_spectacular.utils import extend_schema, extend_schema_view
from apps.accounts.models import UserProfile, TopList, TopListItem from apps.accounts.models import UserProfile, TopList, TopListItem
@@ -72,6 +68,7 @@ UserModel = get_user_model()
# === AUTHENTICATION API VIEWS === # === AUTHENTICATION API VIEWS ===
@extend_schema_view( @extend_schema_view(
post=extend_schema( post=extend_schema(
summary="User login", summary="User login",
@@ -250,7 +247,7 @@ class LogoutAPIView(APIView):
{"message": "Logout successful"} {"message": "Logout successful"}
) )
return Response(response_serializer.data) return Response(response_serializer.data)
except Exception as e: except Exception:
return Response( return Response(
{"error": "Logout failed"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR {"error": "Logout failed"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
) )
@@ -361,7 +358,6 @@ class SocialProvidersAPIView(APIView):
def get(self, request: Request) -> Response: def get(self, request: Request) -> Response:
from django.core.cache import cache 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] site = get_current_site(request._request) # type: ignore[attr-defined]
@@ -446,6 +442,7 @@ class AuthStatusAPIView(APIView):
# === USER PROFILE API VIEWS === # === USER PROFILE API VIEWS ===
class UserProfileViewSet(ModelViewSet): class UserProfileViewSet(ModelViewSet):
"""ViewSet for managing user profiles.""" """ViewSet for managing user profiles."""
@@ -481,6 +478,7 @@ class UserProfileViewSet(ModelViewSet):
# === TOP LIST API VIEWS === # === TOP LIST API VIEWS ===
class TopListViewSet(ModelViewSet): class TopListViewSet(ModelViewSet):
"""ViewSet for managing user top lists.""" """ViewSet for managing user top lists."""

View File

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

View File

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

View File

@@ -12,46 +12,47 @@ class RidePhotoOutputSerializer(serializers.ModelSerializer):
"""Output serializer for ride photos.""" """Output serializer for ride photos."""
uploaded_by_username = serializers.CharField( uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True) source="uploaded_by.username", read_only=True
)
file_size = serializers.ReadOnlyField() file_size = serializers.ReadOnlyField()
dimensions = serializers.ReadOnlyField() dimensions = serializers.ReadOnlyField()
ride_slug = serializers.CharField(source='ride.slug', read_only=True) ride_slug = serializers.CharField(source="ride.slug", read_only=True)
ride_name = serializers.CharField(source='ride.name', 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_slug = serializers.CharField(source="ride.park.slug", read_only=True)
park_name = serializers.CharField(source='ride.park.name', read_only=True) park_name = serializers.CharField(source="ride.park.name", read_only=True)
class Meta: class Meta:
model = RidePhoto model = RidePhoto
fields = [ fields = [
'id', "id",
'image', "image",
'caption', "caption",
'alt_text', "alt_text",
'is_primary', "is_primary",
'is_approved', "is_approved",
'photo_type', "photo_type",
'created_at', "created_at",
'updated_at', "updated_at",
'date_taken', "date_taken",
'uploaded_by_username', "uploaded_by_username",
'file_size', "file_size",
'dimensions', "dimensions",
'ride_slug', "ride_slug",
'ride_name', "ride_name",
'park_slug', "park_slug",
'park_name', "park_name",
] ]
read_only_fields = [ read_only_fields = [
'id', "id",
'created_at', "created_at",
'updated_at', "updated_at",
'uploaded_by_username', "uploaded_by_username",
'file_size', "file_size",
'dimensions', "dimensions",
'ride_slug', "ride_slug",
'ride_name', "ride_name",
'park_slug', "park_slug",
'park_name', "park_name",
] ]
@@ -61,11 +62,11 @@ class RidePhotoCreateInputSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = RidePhoto model = RidePhoto
fields = [ fields = [
'image', "image",
'caption', "caption",
'alt_text', "alt_text",
'photo_type', "photo_type",
'is_primary', "is_primary",
] ]
@@ -75,10 +76,10 @@ class RidePhotoUpdateInputSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = RidePhoto model = RidePhoto
fields = [ fields = [
'caption', "caption",
'alt_text', "alt_text",
'photo_type', "photo_type",
'is_primary', "is_primary",
] ]
@@ -86,19 +87,20 @@ class RidePhotoListOutputSerializer(serializers.ModelSerializer):
"""Simplified output serializer for ride photo lists.""" """Simplified output serializer for ride photo lists."""
uploaded_by_username = serializers.CharField( uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True) source="uploaded_by.username", read_only=True
)
class Meta: class Meta:
model = RidePhoto model = RidePhoto
fields = [ fields = [
'id', "id",
'image', "image",
'caption', "caption",
'photo_type', "photo_type",
'is_primary', "is_primary",
'is_approved', "is_approved",
'created_at', "created_at",
'uploaded_by_username', "uploaded_by_username",
] ]
read_only_fields = fields read_only_fields = fields
@@ -107,12 +109,10 @@ class RidePhotoApprovalInputSerializer(serializers.Serializer):
"""Input serializer for photo approval operations.""" """Input serializer for photo approval operations."""
photo_ids = serializers.ListField( photo_ids = serializers.ListField(
child=serializers.IntegerField(), child=serializers.IntegerField(), help_text="List of photo IDs to approve"
help_text="List of photo IDs to approve"
) )
approve = serializers.BooleanField( approve = serializers.BooleanField(
default=True, default=True, help_text="Whether to approve (True) or reject (False) the photos"
help_text="Whether to approve (True) or reject (False) the photos"
) )
@@ -125,8 +125,7 @@ class RidePhotoStatsOutputSerializer(serializers.Serializer):
has_primary = serializers.BooleanField() has_primary = serializers.BooleanField()
recent_uploads = serializers.IntegerField() recent_uploads = serializers.IntegerField()
by_type = serializers.DictField( by_type = serializers.DictField(
child=serializers.IntegerField(), child=serializers.IntegerField(), help_text="Photo counts by type"
help_text="Photo counts by type"
) )
@@ -135,13 +134,13 @@ class RidePhotoTypeFilterSerializer(serializers.Serializer):
photo_type = serializers.ChoiceField( photo_type = serializers.ChoiceField(
choices=[ choices=[
('exterior', 'Exterior View'), ("exterior", "Exterior View"),
('queue', 'Queue Area'), ("queue", "Queue Area"),
('station', 'Station'), ("station", "Station"),
('onride', 'On-Ride'), ("onride", "On-Ride"),
('construction', 'Construction'), ("construction", "Construction"),
('other', 'Other'), ("other", "Other"),
], ],
required=False, 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. Ride API URLs for ThrillWiki API v1.
""" """
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter 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. This module contains consolidated ride photo viewset for the centralized API structure.
""" """
import logging import logging
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
@@ -108,28 +109,26 @@ class RidePhotoViewSet(ModelViewSet):
def get_queryset(self): def get_queryset(self):
"""Get photos for the current ride with optimized queries.""" """Get photos for the current ride with optimized queries."""
return RidePhoto.objects.select_related( return (
'ride', RidePhoto.objects.select_related("ride", "ride__park", "uploaded_by")
'ride__park', .filter(ride_id=self.kwargs.get("ride_pk"))
'uploaded_by' .order_by("-created_at")
).filter( )
ride_id=self.kwargs.get('ride_pk')
).order_by('-created_at')
def get_serializer_class(self): def get_serializer_class(self):
"""Return appropriate serializer based on action.""" """Return appropriate serializer based on action."""
if self.action == 'list': if self.action == "list":
return RidePhotoListOutputSerializer return RidePhotoListOutputSerializer
elif self.action == 'create': elif self.action == "create":
return RidePhotoCreateInputSerializer return RidePhotoCreateInputSerializer
elif self.action in ['update', 'partial_update']: elif self.action in ["update", "partial_update"]:
return RidePhotoUpdateInputSerializer return RidePhotoUpdateInputSerializer
else: else:
return RidePhotoOutputSerializer return RidePhotoOutputSerializer
def perform_create(self, serializer): def perform_create(self, serializer):
"""Create a new ride photo using RideMediaService.""" """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: if not ride_id:
raise ValidationError("Ride ID is required") raise ValidationError("Ride ID is required")
@@ -138,7 +137,7 @@ class RidePhotoViewSet(ModelViewSet):
photo = RideMediaService.create_photo( photo = RideMediaService.create_photo(
ride_id=ride_id, ride_id=ride_id,
uploaded_by=self.request.user, uploaded_by=self.request.user,
**serializer.validated_data **serializer.validated_data,
) )
# Set the instance for the serializer response # Set the instance for the serializer response
@@ -153,19 +152,20 @@ class RidePhotoViewSet(ModelViewSet):
instance = self.get_object() instance = self.get_object()
# Check permissions # 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.") raise PermissionDenied("You can only edit your own photos or be an admin.")
# Handle primary photo logic using service # Handle primary photo logic using service
if serializer.validated_data.get('is_primary', False): if serializer.validated_data.get("is_primary", False):
try: try:
RideMediaService.set_primary_photo( RideMediaService.set_primary_photo(
ride_id=instance.ride_id, ride_id=instance.ride_id, photo_id=instance.id
photo_id=instance.id
) )
# Remove is_primary from validated_data since service handles it # Remove is_primary from validated_data since service handles it
if 'is_primary' in serializer.validated_data: if "is_primary" in serializer.validated_data:
del serializer.validated_data['is_primary'] del serializer.validated_data["is_primary"]
except Exception as e: except Exception as e:
logger.error(f"Error setting primary photo: {e}") logger.error(f"Error setting primary photo: {e}")
raise ValidationError(f"Failed to set primary photo: {str(e)}") raise ValidationError(f"Failed to set primary photo: {str(e)}")
@@ -175,9 +175,12 @@ class RidePhotoViewSet(ModelViewSet):
def perform_destroy(self, instance): def perform_destroy(self, instance):
"""Delete ride photo with permission checking.""" """Delete ride photo with permission checking."""
# Check permissions # 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( 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: try:
RideMediaService.delete_photo(instance.id) RideMediaService.delete_photo(instance.id)
@@ -185,7 +188,7 @@ class RidePhotoViewSet(ModelViewSet):
logger.error(f"Error deleting ride photo: {e}") logger.error(f"Error deleting ride photo: {e}")
raise ValidationError(f"Failed to delete photo: {str(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): def set_primary(self, request, **kwargs):
"""Set this photo as the primary photo for the ride.""" """Set this photo as the primary photo for the ride."""
photo = self.get_object() photo = self.get_object()
@@ -193,13 +196,11 @@ class RidePhotoViewSet(ModelViewSet):
# Check permissions # Check permissions
if not (request.user == photo.uploaded_by or request.user.is_staff): if not (request.user == photo.uploaded_by or request.user.is_staff):
raise PermissionDenied( 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: try:
RideMediaService.set_primary_photo( RideMediaService.set_primary_photo(ride_id=photo.ride_id, photo_id=photo.id)
ride_id=photo.ride_id,
photo_id=photo.id
)
# Refresh the photo instance # Refresh the photo instance
photo.refresh_from_db() photo.refresh_from_db()
@@ -207,20 +208,20 @@ class RidePhotoViewSet(ModelViewSet):
return Response( return Response(
{ {
'message': 'Photo set as primary successfully', "message": "Photo set as primary successfully",
'photo': serializer.data "photo": serializer.data,
}, },
status=status.HTTP_200_OK status=status.HTTP_200_OK,
) )
except Exception as e: except Exception as e:
logger.error(f"Error setting primary photo: {e}") logger.error(f"Error setting primary photo: {e}")
return Response( return Response(
{'error': f'Failed to set primary photo: {str(e)}'}, {"error": f"Failed to set primary photo: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST 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): def bulk_approve(self, request, **kwargs):
"""Bulk approve or reject multiple photos (admin only).""" """Bulk approve or reject multiple photos (admin only)."""
if not request.user.is_staff: if not request.user.is_staff:
@@ -229,38 +230,35 @@ class RidePhotoViewSet(ModelViewSet):
serializer = RidePhotoApprovalInputSerializer(data=request.data) serializer = RidePhotoApprovalInputSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
photo_ids = serializer.validated_data['photo_ids'] photo_ids = serializer.validated_data["photo_ids"]
approve = serializer.validated_data['approve'] approve = serializer.validated_data["approve"]
ride_id = self.kwargs.get('ride_pk') ride_id = self.kwargs.get("ride_pk")
try: try:
# Filter photos to only those belonging to this ride # Filter photos to only those belonging to this ride
photos = RidePhoto.objects.filter( photos = RidePhoto.objects.filter(id__in=photo_ids, ride_id=ride_id)
id__in=photo_ids,
ride_id=ride_id
)
updated_count = photos.update(is_approved=approve) updated_count = photos.update(is_approved=approve)
return Response( return Response(
{ {
'message': f'Successfully {"approved" if approve else "rejected"} {updated_count} photos', "message": f"Successfully {'approved' if approve else 'rejected'} {updated_count} photos",
'updated_count': updated_count "updated_count": updated_count,
}, },
status=status.HTTP_200_OK status=status.HTTP_200_OK,
) )
except Exception as e: except Exception as e:
logger.error(f"Error in bulk photo approval: {e}") logger.error(f"Error in bulk photo approval: {e}")
return Response( return Response(
{'error': f'Failed to update photos: {str(e)}'}, {"error": f"Failed to update photos: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST status=status.HTTP_400_BAD_REQUEST,
) )
@action(detail=False, methods=['get']) @action(detail=False, methods=["get"])
def stats(self, request, **kwargs): def stats(self, request, **kwargs):
"""Get photo statistics for the ride.""" """Get photo statistics for the ride."""
ride_id = self.kwargs.get('ride_pk') ride_id = self.kwargs.get("ride_pk")
try: try:
stats = RideMediaService.get_photo_stats(ride_id=ride_id) stats = RideMediaService.get_photo_stats(ride_id=ride_id)
@@ -271,6 +269,6 @@ class RidePhotoViewSet(ModelViewSet):
except Exception as e: except Exception as e:
logger.error(f"Error getting ride photo stats: {e}") logger.error(f"Error getting ride photo stats: {e}")
return Response( return Response(
{'error': f'Failed to get photo statistics: {str(e)}'}, {"error": f"Failed to get photo statistics: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR status=status.HTTP_500_INTERNAL_SERVER_ERROR,
) )

View File

@@ -9,11 +9,10 @@ from django.urls import path, include
urlpatterns = [ urlpatterns = [
# Domain-specific API endpoints # Domain-specific API endpoints
path('rides/', include('api.v1.rides.urls')), path("rides/", include("api.v1.rides.urls")),
path('parks/', include('api.v1.parks.urls')), path("parks/", include("api.v1.parks.urls")),
path('auth/', include('api.v1.auth.urls')), path("auth/", include("api.v1.auth.urls")),
# Media endpoints (for photo management) # Media endpoints (for photo management)
# Will be consolidated from the various media implementations # 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:") self.stdout.write("\nChecking SocialApp table:")
for app in SocialApp.objects.all(): for app in SocialApp.objects.all():
self.stdout.write( self.stdout.write(
f"ID: { f"ID: {app.pk}, Provider: {app.provider}, Name: {app.name}, Client ID: {
app.pk}, Provider: { app.client_id
app.provider}, Name: { }"
app.name}, Client ID: {
app.client_id}"
) )
self.stdout.write("Sites:") self.stdout.write("Sites:")
for site in app.sites.all(): for site in app.sites.all():
@@ -25,10 +23,7 @@ class Command(BaseCommand):
self.stdout.write("\nChecking SocialAccount table:") self.stdout.write("\nChecking SocialAccount table:")
for account in SocialAccount.objects.all(): for account in SocialAccount.objects.all():
self.stdout.write( self.stdout.write(
f"ID: { f"ID: {account.pk}, Provider: {account.provider}, UID: {account.uid}"
account.pk}, Provider: {
account.provider}, UID: {
account.uid}"
) )
# Check SocialToken # Check SocialToken

View File

@@ -13,15 +13,10 @@ class Command(BaseCommand):
return return
for app in social_apps: for app in social_apps:
self.stdout.write( self.stdout.write(self.style.SUCCESS(f"\nProvider: {app.provider}"))
self.style.SUCCESS(
f"\nProvider: {
app.provider}"
)
)
self.stdout.write(f"Name: {app.name}") self.stdout.write(f"Name: {app.name}")
self.stdout.write(f"Client ID: {app.client_id}") self.stdout.write(f"Client ID: {app.client_id}")
self.stdout.write(f"Secret: {app.secret}") self.stdout.write(f"Secret: {app.secret}")
self.stdout.write( 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 # Delete test photos - both park and ride photos
park_photos = ParkPhoto.objects.filter( park_photos = ParkPhoto.objects.filter(
uploader__username__in=["testuser", "moderator"]) uploader__username__in=["testuser", "moderator"]
)
park_count = park_photos.count() park_count = park_photos.count()
park_photos.delete() park_photos.delete()
self.stdout.write(self.style.SUCCESS(f"Deleted {park_count} test park photos")) self.stdout.write(self.style.SUCCESS(f"Deleted {park_count} test park photos"))
ride_photos = RidePhoto.objects.filter( ride_photos = RidePhoto.objects.filter(
uploader__username__in=["testuser", "moderator"]) uploader__username__in=["testuser", "moderator"]
)
ride_count = ride_photos.count() ride_count = ride_photos.count()
ride_photos.delete() ride_photos.delete()
self.stdout.write(self.style.SUCCESS(f"Deleted {ride_count} test ride photos")) 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.secret = "ece7Pe_M4mD4mYzAgcINjTEKL_3ftL11"
discord_app.save() discord_app.save()
discord_app.sites.add(site) 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 # Create Google app
google_app, created = SocialApp.objects.get_or_create( google_app, created = SocialApp.objects.get_or_create(
@@ -52,4 +52,4 @@ class Command(BaseCommand):
google_app.secret = "GOCSPX-Wd_0Ue0Ue0Ue0Ue0Ue0Ue0Ue0Ue" google_app.secret = "GOCSPX-Wd_0Ue0Ue0Ue0Ue0Ue0Ue0Ue0Ue"
google_app.save() google_app.save()
google_app.sites.add(site) 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"), secret=os.getenv("GOOGLE_CLIENT_SECRET"),
) )
google_app.sites.add(site) google_app.sites.add(site)
self.stdout.write( self.stdout.write(f"Created Google app with client_id: {google_app.client_id}")
f"Created Google app with client_id: {
google_app.client_id}"
)
# Create Discord provider # Create Discord provider
discord_app = SocialApp.objects.create( 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 # This will trigger the avatar generation logic in the save method
profile.save() profile.save()
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(f"Regenerated avatar for {profile.user.username}")
f"Regenerated avatar for {
profile.user.username}"
)
) )

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ class Command(BaseCommand):
site.domain = "localhost:8000" site.domain = "localhost:8000"
site.name = "ThrillWiki Development" site.name = "ThrillWiki Development"
site.save() 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 # Create superuser if it doesn't exist
if not User.objects.filter(username="admin").exists(): if not User.objects.filter(username="admin").exists():

View File

@@ -19,5 +19,5 @@ class Command(BaseCommand):
for site in sites: for site in sites:
app.sites.add(site) app.sites.add(site)
self.stdout.write( 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:") self.stdout.write("\nOAuth2 settings in settings.py:")
discord_settings = settings.SOCIALACCOUNT_PROVIDERS.get("discord", {}) discord_settings = settings.SOCIALACCOUNT_PROVIDERS.get("discord", {})
self.stdout.write( self.stdout.write(
f'PKCE Enabled: { f"PKCE Enabled: {discord_settings.get('OAUTH_PKCE_ENABLED', False)}"
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: except SocialApp.DoesNotExist:
self.stdout.write(self.style.ERROR("Discord app not found")) 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): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password 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.crypto import get_random_string
from django.utils import timezone from django.utils import timezone
from datetime import timedelta 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) profile.avatar.save(file_name, File(img_temp), save=True)
except Exception as e: except Exception as e:
print( print(
f"Error downloading avatar for user { f"Error downloading avatar for user {instance.username}: {
instance.username}: { str(e)
str(e)}" }"
) )
except Exception as e: except Exception as e:
print(f"Error creating profile for user {instance.username}: {str(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 pass
except Exception as e: except Exception as e:
print( print(
f"Error syncing role with groups for user { f"Error syncing role with groups for user {instance.username}: {str(e)}"
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. 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.utils import extend_schema, extend_schema_view, OpenApiParameter
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from rest_framework.filters import OrderingFilter from rest_framework.filters import OrderingFilter
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet
from django.shortcuts import get_object_or_404 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 Migrated from apps.core.views.map_views
""" """
import json
import logging import logging
import time
from typing import Dict, Any, Optional
from django.http import JsonResponse, HttpRequest from django.http import 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 rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
@@ -30,15 +21,54 @@ logger = logging.getLogger(__name__)
summary="Get map locations", summary="Get map locations",
description="Get map locations with optional clustering and filtering.", description="Get map locations with optional clustering and filtering.",
parameters=[ parameters=[
{"name": "north", "in": "query", "required": False, "schema": {"type": "number"}}, {
{"name": "south", "in": "query", "required": False, "schema": {"type": "number"}}, "name": "north",
{"name": "east", "in": "query", "required": False, "schema": {"type": "number"}}, "in": "query",
{"name": "west", "in": "query", "required": False, "schema": {"type": "number"}}, "required": False,
{"name": "zoom", "in": "query", "required": False, "schema": {"type": "integer"}}, "schema": {"type": "number"},
{"name": "types", "in": "query", "required": False, "schema": {"type": "string"}}, },
{"name": "cluster", "in": "query", "required": False, {
"schema": {"type": "boolean"}}, "name": "south",
{"name": "q", "in": "query", "required": False, "schema": {"type": "string"}}, "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}, responses={200: OpenApiTypes.OBJECT},
tags=["Maps"], tags=["Maps"],
@@ -54,18 +84,20 @@ class MapLocationsAPIView(APIView):
try: try:
# Simple implementation to fix import error # Simple implementation to fix import error
# TODO: Implement full functionality # TODO: Implement full functionality
return Response({ return Response(
"status": "success", {
"message": "Map locations endpoint - implementation needed", "status": "success",
"data": [] "message": "Map locations endpoint - implementation needed",
}) "data": [],
}
)
except Exception as e: except Exception as e:
logger.error(f"Error in MapLocationsAPIView: {str(e)}", exc_info=True) logger.error(f"Error in MapLocationsAPIView: {str(e)}", exc_info=True)
return Response({ return Response(
"status": "error", {"status": "error", "message": "Failed to retrieve map locations"},
"message": "Failed to retrieve map locations" status=status.HTTP_500_INTERNAL_SERVER_ERROR,
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) )
@extend_schema_view( @extend_schema_view(
@@ -73,10 +105,18 @@ class MapLocationsAPIView(APIView):
summary="Get location details", summary="Get location details",
description="Get detailed information about a specific location.", description="Get detailed information about a specific location.",
parameters=[ parameters=[
{"name": "location_type", "in": "path", {
"required": True, "schema": {"type": "string"}}, "name": "location_type",
{"name": "location_id", "in": "path", "in": "path",
"required": True, "schema": {"type": "integer"}}, "required": True,
"schema": {"type": "string"},
},
{
"name": "location_id",
"in": "path",
"required": True,
"schema": {"type": "integer"},
},
], ],
responses={200: OpenApiTypes.OBJECT, 404: OpenApiTypes.OBJECT}, responses={200: OpenApiTypes.OBJECT, 404: OpenApiTypes.OBJECT},
tags=["Maps"], tags=["Maps"],
@@ -87,25 +127,29 @@ class MapLocationDetailAPIView(APIView):
permission_classes = [AllowAny] 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.""" """Get detailed information for a specific location."""
try: try:
# Simple implementation to fix import error # Simple implementation to fix import error
return Response({ return Response(
"status": "success", {
"message": f"Location detail for {location_type}/{location_id} - implementation needed", "status": "success",
"data": { "message": f"Location detail for {location_type}/{location_id} - implementation needed",
"location_type": location_type, "data": {
"location_id": location_id "location_type": location_type,
"location_id": location_id,
},
} }
}) )
except Exception as e: except Exception as e:
logger.error(f"Error in MapLocationDetailAPIView: {str(e)}", exc_info=True) logger.error(f"Error in MapLocationDetailAPIView: {str(e)}", exc_info=True)
return Response({ return Response(
"status": "error", {"status": "error", "message": "Failed to retrieve location details"},
"message": "Failed to retrieve location details" status=status.HTTP_500_INTERNAL_SERVER_ERROR,
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) )
@extend_schema_view( @extend_schema_view(
@@ -113,7 +157,12 @@ class MapLocationDetailAPIView(APIView):
summary="Search map locations", summary="Search map locations",
description="Search locations by text query with optional bounds filtering.", description="Search locations by text query with optional bounds filtering.",
parameters=[ 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}, responses={200: OpenApiTypes.OBJECT, 400: OpenApiTypes.OBJECT},
tags=["Maps"], tags=["Maps"],
@@ -129,24 +178,29 @@ class MapSearchAPIView(APIView):
try: try:
query = request.GET.get("q", "").strip() query = request.GET.get("q", "").strip()
if not query: if not query:
return Response({ return Response(
"status": "error", {
"message": "Search query 'q' parameter is required" "status": "error",
}, status=status.HTTP_400_BAD_REQUEST) "message": "Search query 'q' parameter is required",
},
status=status.HTTP_400_BAD_REQUEST,
)
# Simple implementation to fix import error # Simple implementation to fix import error
return Response({ return Response(
"status": "success", {
"message": f"Search for '{query}' - implementation needed", "status": "success",
"data": [] "message": f"Search for '{query}' - implementation needed",
}) "data": [],
}
)
except Exception as e: except Exception as e:
logger.error(f"Error in MapSearchAPIView: {str(e)}", exc_info=True) logger.error(f"Error in MapSearchAPIView: {str(e)}", exc_info=True)
return Response({ return Response(
"status": "error", {"status": "error", "message": "Search failed due to internal error"},
"message": "Search failed due to internal error" status=status.HTTP_500_INTERNAL_SERVER_ERROR,
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) )
@extend_schema_view( @extend_schema_view(
@@ -154,10 +208,30 @@ class MapSearchAPIView(APIView):
summary="Get locations within bounds", summary="Get locations within bounds",
description="Get locations within specific geographic bounds.", description="Get locations within specific geographic bounds.",
parameters=[ parameters=[
{"name": "north", "in": "query", "required": True, "schema": {"type": "number"}}, {
{"name": "south", "in": "query", "required": True, "schema": {"type": "number"}}, "name": "north",
{"name": "east", "in": "query", "required": True, "schema": {"type": "number"}}, "in": "query",
{"name": "west", "in": "query", "required": True, "schema": {"type": "number"}}, "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}, responses={200: OpenApiTypes.OBJECT, 400: OpenApiTypes.OBJECT},
tags=["Maps"], tags=["Maps"],
@@ -172,18 +246,23 @@ class MapBoundsAPIView(APIView):
"""Get locations within specific geographic bounds.""" """Get locations within specific geographic bounds."""
try: try:
# Simple implementation to fix import error # Simple implementation to fix import error
return Response({ return Response(
"status": "success", {
"message": "Bounds query - implementation needed", "status": "success",
"data": [] "message": "Bounds query - implementation needed",
}) "data": [],
}
)
except Exception as e: except Exception as e:
logger.error(f"Error in MapBoundsAPIView: {str(e)}", exc_info=True) logger.error(f"Error in MapBoundsAPIView: {str(e)}", exc_info=True)
return Response({ return Response(
"status": "error", {
"message": "Failed to retrieve locations within bounds" "status": "error",
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) "message": "Failed to retrieve locations within bounds",
},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@extend_schema_view( @extend_schema_view(
@@ -203,14 +282,12 @@ class MapStatsAPIView(APIView):
"""Get map service statistics and performance metrics.""" """Get map service statistics and performance metrics."""
try: try:
# Simple implementation to fix import error # Simple implementation to fix import error
return Response({ return Response(
"status": "success", {
"data": { "status": "success",
"total_locations": 0, "data": {"total_locations": 0, "cache_hits": 0, "cache_misses": 0},
"cache_hits": 0,
"cache_misses": 0
} }
}) )
except Exception as e: except Exception as e:
return Response( return Response(
@@ -242,10 +319,9 @@ class MapCacheAPIView(APIView):
"""Clear all map cache (admin only).""" """Clear all map cache (admin only)."""
try: try:
# Simple implementation to fix import error # Simple implementation to fix import error
return Response({ return Response(
"status": "success", {"status": "success", "message": "Map cache cleared successfully"}
"message": "Map cache cleared successfully" )
})
except Exception as e: except Exception as e:
return Response( return Response(
@@ -257,10 +333,9 @@ class MapCacheAPIView(APIView):
"""Invalidate specific cache entries.""" """Invalidate specific cache entries."""
try: try:
# Simple implementation to fix import error # Simple implementation to fix import error
return Response({ return Response(
"status": "success", {"status": "success", "message": "Cache invalidated successfully"}
"message": "Cache invalidated successfully" )
})
except Exception as e: except Exception as e:
return Response( return Response(

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
""" """
Ride API views for ThrillWiki API v1. Ride API views for ThrillWiki API v1.
""" """
import logging import logging
from django.core.exceptions import PermissionDenied 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.openapi import AutoSchema
from drf_spectacular.utils import OpenApiExample
from drf_spectacular.types import OpenApiTypes
# Custom examples for common serializers # Custom examples for common serializers

View File

@@ -12,12 +12,7 @@ from drf_spectacular.utils import (
OpenApiExample, OpenApiExample,
) )
from django.contrib.auth.password_validation import validate_password 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.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 from .shared import UserModel, ModelChoices

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,46 +12,47 @@ class RidePhotoOutputSerializer(serializers.ModelSerializer):
"""Output serializer for ride photos.""" """Output serializer for ride photos."""
uploaded_by_username = serializers.CharField( uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True) source="uploaded_by.username", read_only=True
)
file_size = serializers.ReadOnlyField() file_size = serializers.ReadOnlyField()
dimensions = serializers.ReadOnlyField() dimensions = serializers.ReadOnlyField()
ride_slug = serializers.CharField(source='ride.slug', read_only=True) ride_slug = serializers.CharField(source="ride.slug", read_only=True)
ride_name = serializers.CharField(source='ride.name', 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_slug = serializers.CharField(source="ride.park.slug", read_only=True)
park_name = serializers.CharField(source='ride.park.name', read_only=True) park_name = serializers.CharField(source="ride.park.name", read_only=True)
class Meta: class Meta:
model = RidePhoto model = RidePhoto
fields = [ fields = [
'id', "id",
'image', "image",
'caption', "caption",
'alt_text', "alt_text",
'is_primary', "is_primary",
'is_approved', "is_approved",
'photo_type', "photo_type",
'created_at', "created_at",
'updated_at', "updated_at",
'date_taken', "date_taken",
'uploaded_by_username', "uploaded_by_username",
'file_size', "file_size",
'dimensions', "dimensions",
'ride_slug', "ride_slug",
'ride_name', "ride_name",
'park_slug', "park_slug",
'park_name', "park_name",
] ]
read_only_fields = [ read_only_fields = [
'id', "id",
'created_at', "created_at",
'updated_at', "updated_at",
'uploaded_by_username', "uploaded_by_username",
'file_size', "file_size",
'dimensions', "dimensions",
'ride_slug', "ride_slug",
'ride_name', "ride_name",
'park_slug', "park_slug",
'park_name', "park_name",
] ]
@@ -61,11 +62,11 @@ class RidePhotoCreateInputSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = RidePhoto model = RidePhoto
fields = [ fields = [
'image', "image",
'caption', "caption",
'alt_text', "alt_text",
'photo_type', "photo_type",
'is_primary', "is_primary",
] ]
@@ -75,10 +76,10 @@ class RidePhotoUpdateInputSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = RidePhoto model = RidePhoto
fields = [ fields = [
'caption', "caption",
'alt_text', "alt_text",
'photo_type', "photo_type",
'is_primary', "is_primary",
] ]
@@ -86,19 +87,20 @@ class RidePhotoListOutputSerializer(serializers.ModelSerializer):
"""Simplified output serializer for ride photo lists.""" """Simplified output serializer for ride photo lists."""
uploaded_by_username = serializers.CharField( uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True) source="uploaded_by.username", read_only=True
)
class Meta: class Meta:
model = RidePhoto model = RidePhoto
fields = [ fields = [
'id', "id",
'image', "image",
'caption', "caption",
'photo_type', "photo_type",
'is_primary', "is_primary",
'is_approved', "is_approved",
'created_at', "created_at",
'uploaded_by_username', "uploaded_by_username",
] ]
read_only_fields = fields read_only_fields = fields
@@ -107,12 +109,10 @@ class RidePhotoApprovalInputSerializer(serializers.Serializer):
"""Input serializer for photo approval operations.""" """Input serializer for photo approval operations."""
photo_ids = serializers.ListField( photo_ids = serializers.ListField(
child=serializers.IntegerField(), child=serializers.IntegerField(), help_text="List of photo IDs to approve"
help_text="List of photo IDs to approve"
) )
approve = serializers.BooleanField( approve = serializers.BooleanField(
default=True, default=True, help_text="Whether to approve (True) or reject (False) the photos"
help_text="Whether to approve (True) or reject (False) the photos"
) )
@@ -125,8 +125,7 @@ class RidePhotoStatsOutputSerializer(serializers.Serializer):
has_primary = serializers.BooleanField() has_primary = serializers.BooleanField()
recent_uploads = serializers.IntegerField() recent_uploads = serializers.IntegerField()
by_type = serializers.DictField( by_type = serializers.DictField(
child=serializers.IntegerField(), child=serializers.IntegerField(), help_text="Photo counts by type"
help_text="Photo counts by type"
) )
@@ -135,13 +134,13 @@ class RidePhotoTypeFilterSerializer(serializers.Serializer):
photo_type = serializers.ChoiceField( photo_type = serializers.ChoiceField(
choices=[ choices=[
('exterior', 'Exterior View'), ("exterior", "Exterior View"),
('queue', 'Queue Area'), ("queue", "Queue Area"),
('station', 'Station'), ("station", "Station"),
('onride', 'On-Ride'), ("onride", "On-Ride"),
('construction', 'Construction'), ("construction", "Construction"),
('other', 'Other'), ("other", "Other"),
], ],
required=False, 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 rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_serializer,
extend_schema_field,
OpenApiExample,
)
# === CORE ENTITY SEARCH SERIALIZERS === # === CORE ENTITY SEARCH SERIALIZERS ===

View File

@@ -7,9 +7,7 @@ history tracking, moderation, and roadtrip planning.
from rest_framework import serializers from rest_framework import serializers
from drf_spectacular.utils import ( from drf_spectacular.utils import (
extend_schema_serializer,
extend_schema_field, 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 import get_user_model
from django.contrib.auth.password_validation import validate_password 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.crypto import get_random_string
from django.utils import timezone from django.utils import timezone
from datetime import timedelta from datetime import timedelta
@@ -400,7 +399,8 @@ class ParkFilterInputSerializer(serializers.Serializer):
# Status filter # Status filter
status = serializers.MultipleChoiceField( status = serializers.MultipleChoiceField(
choices=[], required=False # Choices set dynamically choices=[],
required=False, # Choices set dynamically
) )
# Location filters # Location filters
@@ -777,7 +777,8 @@ class RideFilterInputSerializer(serializers.Serializer):
# Status filter # Status filter
status = serializers.MultipleChoiceField( status = serializers.MultipleChoiceField(
choices=[], required=False # Choices set dynamically choices=[],
required=False, # Choices set dynamically
) )
# Park filter # 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. login, signup, logout, password management, and social authentication.
""" """
import time
from django.contrib.auth import authenticate, login, logout, get_user_model from django.contrib.auth import authenticate, login, logout, get_user_model
from django.contrib.sites.shortcuts import get_current_site from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils import timezone
from django.conf import settings
from rest_framework import status from rest_framework import status
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
from allauth.socialaccount import providers
from drf_spectacular.utils import extend_schema, extend_schema_view from drf_spectacular.utils import extend_schema, extend_schema_view
# Import serializers inside methods to avoid Django initialization issues # Import serializers inside methods to avoid Django initialization issues
@@ -274,7 +270,7 @@ class LogoutAPIView(APIView):
{"message": "Logout successful"} {"message": "Logout successful"}
) )
return Response(response_serializer.data) return Response(response_serializer.data)
except Exception as e: except Exception:
return Response( return Response(
{"error": "Logout failed"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR {"error": "Logout failed"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
) )
@@ -385,7 +381,6 @@ class SocialProvidersAPIView(APIView):
def get(self, request: Request) -> Response: def get(self, request: Request) -> Response:
from django.core.cache import cache 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] site = get_current_site(request._request) # type: ignore[attr-defined]

View File

@@ -8,7 +8,6 @@ performance metrics, and database analysis.
import time import time
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
from rest_framework import status
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response 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 datetime import datetime, date
from django.utils import timezone
from rest_framework import status
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response

View File

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

View File

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

View File

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

View File

@@ -57,13 +57,11 @@ class CacheHealthCheck(BaseHealthCheckBackend):
memory_usage_percent = (used_memory / max_memory) * 100 memory_usage_percent = (used_memory / max_memory) * 100
if memory_usage_percent > 90: if memory_usage_percent > 90:
self.add_error( self.add_error(
f"Redis memory usage critical: { f"Redis memory usage critical: {memory_usage_percent:.1f}%"
memory_usage_percent:.1f}%"
) )
elif memory_usage_percent > 80: elif memory_usage_percent > 80:
logger.warning( logger.warning(
f"Redis memory usage high: { f"Redis memory usage high: {memory_usage_percent:.1f}%"
memory_usage_percent:.1f}%"
) )
except ImportError: except ImportError:
@@ -190,10 +188,7 @@ class ApplicationHealthCheck(BaseHealthCheckBackend):
import os import os
if not os.path.exists(settings.MEDIA_ROOT): if not os.path.exists(settings.MEDIA_ROOT):
self.add_error( self.add_error(f"Media directory does not exist: {settings.MEDIA_ROOT}")
f"Media directory does not exist: {
settings.MEDIA_ROOT}"
)
if not os.path.exists(settings.STATIC_ROOT) and not settings.DEBUG: if not os.path.exists(settings.STATIC_ROOT) and not settings.DEBUG:
self.add_error( self.add_error(
@@ -305,8 +300,7 @@ class DiskSpaceHealthCheck(BaseHealthCheckBackend):
) )
elif media_free_percent < 20: elif media_free_percent < 20:
logger.warning( logger.warning(
f"Low disk space: { f"Low disk space: {media_free_percent:.1f}% free in media directory"
media_free_percent:.1f}% free in media directory"
) )
if logs_free_percent < 10: if logs_free_percent < 10:
@@ -316,8 +310,7 @@ class DiskSpaceHealthCheck(BaseHealthCheckBackend):
) )
elif logs_free_percent < 20: elif logs_free_percent < 20:
logger.warning( logger.warning(
f"Low disk space: { f"Low disk space: {logs_free_percent:.1f}% free in logs directory"
logs_free_percent:.1f}% free in logs directory"
) )
except Exception as e: 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. 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.base import BaseCommand
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
@@ -62,7 +60,7 @@ class Command(BaseCommand):
self.stdout.write("") self.stdout.write("")
self.stdout.write( self.stdout.write(
self.style.SUCCESS( 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") self.stdout.write("Press Ctrl+C to stop the server")
@@ -74,12 +72,12 @@ class Command(BaseCommand):
[ [
"manage.py", "manage.py",
"runserver_plus", "runserver_plus",
f'{options["host"]}:{options["port"]}', f"{options['host']}:{options['port']}",
] ]
) )
else: else:
execute_from_command_line( execute_from_command_line(
["manage.py", "runserver", f'{options["host"]}:{options["port"]}'] ["manage.py", "runserver", f"{options['host']}:{options['port']}"]
) )
except KeyboardInterrupt: except KeyboardInterrupt:
self.stdout.write("") 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. allowing the project to run without requiring the shell script.
""" """
import os
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.core.management import execute_from_command_line
from django.conf import settings from django.conf import settings

View File

@@ -5,10 +5,6 @@ Analytics and tracking middleware for Django application.
import pghistory import pghistory
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.handlers.wsgi import WSGIRequest 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): class RequestContextProvider(pghistory.context):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,10 +21,7 @@ class MediaService:
@staticmethod @staticmethod
def generate_upload_path( def generate_upload_path(
domain: str, domain: str, identifier: str, filename: str, subdirectory: Optional[str] = None
identifier: str,
filename: str,
subdirectory: Optional[str] = None
) -> str: ) -> str:
""" """
Generate standardized upload path for media files. Generate standardized upload path for media files.
@@ -86,16 +83,26 @@ class MediaService:
""" """
try: try:
# Check file size # Check file size
max_size = getattr(settings, 'MAX_PHOTO_SIZE', max_size = getattr(
10 * 1024 * 1024) # 10MB default settings, "MAX_PHOTO_SIZE", 10 * 1024 * 1024
) # 10MB default
if image_file.size > max_size: 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 # Check file type
allowed_types = getattr(settings, 'ALLOWED_PHOTO_TYPES', [ allowed_types = getattr(
'image/jpeg', 'image/png', 'image/webp']) settings,
"ALLOWED_PHOTO_TYPES",
["image/jpeg", "image/png", "image/webp"],
)
if image_file.content_type not in allowed_types: 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 # Try to open with PIL to validate it's a real image
with Image.open(image_file) as img: with Image.open(image_file) as img:
@@ -111,7 +118,7 @@ class MediaService:
image_file: UploadedFile, image_file: UploadedFile,
max_width: int = 1920, max_width: int = 1920,
max_height: int = 1080, max_height: int = 1080,
quality: int = 85 quality: int = 85,
) -> UploadedFile: ) -> UploadedFile:
""" """
Process and optimize image file. Process and optimize image file.
@@ -128,8 +135,8 @@ class MediaService:
try: try:
with Image.open(image_file) as img: with Image.open(image_file) as img:
# Convert to RGB if necessary # Convert to RGB if necessary
if img.mode in ('RGBA', 'LA', 'P'): if img.mode in ("RGBA", "LA", "P"):
img = img.convert('RGB') img = img.convert("RGB")
# Resize if necessary # Resize if necessary
if img.width > max_width or img.height > max_height: if img.width > max_width or img.height > max_height:
@@ -140,16 +147,16 @@ class MediaService:
from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.files.uploadedfile import InMemoryUploadedFile
output = BytesIO() output = BytesIO()
img.save(output, format='JPEG', quality=quality, optimize=True) img.save(output, format="JPEG", quality=quality, optimize=True)
output.seek(0) output.seek(0)
return InMemoryUploadedFile( return InMemoryUploadedFile(
output, output,
'ImageField', "ImageField",
f"{os.path.splitext(image_file.name)[0]}.jpg", f"{os.path.splitext(image_file.name)[0]}.jpg",
'image/jpeg', "image/jpeg",
output.getbuffer().nbytes, output.getbuffer().nbytes,
None None,
) )
except Exception as e: except Exception as e:
@@ -168,6 +175,7 @@ class MediaService:
Default caption string Default caption string
""" """
from django.utils import timezone from django.utils import timezone
current_time = timezone.now() current_time = timezone.now()
return f"Uploaded by {username} on {current_time.strftime('%B %d, %Y at %I:%M %p')}" 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_files": 0,
"total_size_bytes": 0, "total_size_bytes": 0,
"storage_backend": "default", "storage_backend": "default",
"available_space": "unknown" "available_space": "unknown",
} }
except Exception as e: except Exception as e:
logger.error(f"Failed to get storage stats: {str(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( logger.log(
log_level, log_level,
f"Performance: {operation_name} completed in { f"Performance: {operation_name} completed in {duration:.3f}s with {
duration:.3f}s with {total_queries} queries", total_queries
} queries",
extra=performance_context, extra=performance_context,
) )
# Log slow operations with additional detail # Log slow operations with additional detail
if duration > 2.0: if duration > 2.0:
logger.warning( logger.warning(
f"Slow operation detected: {operation_name} took { f"Slow operation detected: {operation_name} took {duration:.3f}s",
duration:.3f}s",
extra={ extra={
"slow_operation": True, "slow_operation": True,
"threshold_exceeded": "duration", "threshold_exceeded": "duration",
@@ -246,9 +246,9 @@ class PerformanceProfiler:
log_level = logging.WARNING if total_duration > 1.0 else logging.INFO log_level = logging.WARNING if total_duration > 1.0 else logging.INFO
logger.log( logger.log(
log_level, log_level,
f"Profiling complete: { f"Profiling complete: {self.name} took {total_duration:.3f}s with {
self.name} took { total_queries
total_duration:.3f}s with {total_queries} queries", } queries",
extra=report, extra=report,
) )

View File

@@ -395,7 +395,9 @@ class TrendingService:
"""Calculate popularity score based on total view count.""" """Calculate popularity score based on total view count."""
try: try:
total_views = PageView.get_total_views_count( 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 # Normalize views to 0-1 scale

View File

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

View File

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

View File

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

View File

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

View File

@@ -86,17 +86,14 @@ class Command(BaseCommand):
else: else:
self.stdout.write( self.stdout.write(
self.style.WARNING( self.style.WARNING(
f"Registration returned status { f"Registration returned status {response.status_code}: {
response.status_code}: { response.content.decode()
response.content.decode()}\n" }\n"
) )
) )
except Exception as e: except Exception as e:
self.stdout.write( self.stdout.write(
self.style.ERROR( self.style.ERROR(f"Registration email test failed: {str(e)}\n")
f"Registration email test failed: {
str(e)}\n"
)
) )
def test_password_change(self, user): def test_password_change(self, user):
@@ -120,17 +117,14 @@ class Command(BaseCommand):
else: else:
self.stdout.write( self.stdout.write(
self.style.WARNING( self.style.WARNING(
f"Password change returned status { f"Password change returned status {response.status_code}: {
response.status_code}: { response.content.decode()
response.content.decode()}\n" }\n"
) )
) )
except Exception as e: except Exception as e:
self.stdout.write( self.stdout.write(
self.style.ERROR( self.style.ERROR(f"Password change email test failed: {str(e)}\n")
f"Password change email test failed: {
str(e)}\n"
)
) )
def test_email_change(self, user): def test_email_change(self, user):
@@ -151,17 +145,14 @@ class Command(BaseCommand):
else: else:
self.stdout.write( self.stdout.write(
self.style.WARNING( self.style.WARNING(
f"Email change returned status { f"Email change returned status {response.status_code}: {
response.status_code}: { response.content.decode()
response.content.decode()}\n" }\n"
) )
) )
except Exception as e: except Exception as e:
self.stdout.write( self.stdout.write(
self.style.ERROR( self.style.ERROR(f"Email change verification test failed: {str(e)}\n")
f"Email change verification test failed: {
str(e)}\n"
)
) )
def test_password_reset(self, user): def test_password_reset(self, user):
@@ -182,15 +173,12 @@ class Command(BaseCommand):
else: else:
self.stdout.write( self.stdout.write(
self.style.WARNING( self.style.WARNING(
f"Password reset returned status { f"Password reset returned status {response.status_code}: {
response.status_code}: { response.content.decode()
response.content.decode()}\n" }\n"
) )
) )
except Exception as e: except Exception as e:
self.stdout.write( self.stdout.write(
self.style.ERROR( self.style.ERROR(f"Password reset email test failed: {str(e)}\n")
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(self.style.SUCCESS("Using configuration:"))
self.stdout.write(f" From: {from_email}") self.stdout.write(f" From: {from_email}")
self.stdout.write(f" To: {to_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}") self.stdout.write(f" Site: {site.domain}")
try: try:
@@ -132,10 +132,7 @@ class Command(BaseCommand):
return config return config
except Exception as e: except Exception as e:
self.stdout.write( self.stdout.write(
self.style.ERROR( self.style.ERROR(f"✗ Site configuration failed: {str(e)}")
f"✗ Site configuration failed: {
str(e)}"
)
) )
raise raise
@@ -164,8 +161,8 @@ class Command(BaseCommand):
self.stdout.write( self.stdout.write(
self.style.ERROR( self.style.ERROR(
f"✗ API endpoint test failed with status { f"✗ API endpoint test failed with status {
response.status_code}: { response.status_code
response.text}" }: {response.text}"
) )
) )
raise Exception(f"API test failed: {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") raise Exception("Could not connect to Django server")
except Exception as e: except Exception as e:
self.stdout.write( self.stdout.write(self.style.ERROR(f"✗ API endpoint test failed: {str(e)}"))
self.style.ERROR(
f"✗ API endpoint test failed: {
str(e)}"
)
)
raise raise
def test_email_backend(self, to_email, site): def test_email_backend(self, to_email, site):
@@ -196,8 +188,7 @@ class Command(BaseCommand):
# Debug output # Debug output
self.stdout.write( self.stdout.write(
f" Debug: Using from_email: { f" Debug: Using from_email: {site.email_config.default_from_email}"
site.email_config.default_from_email}"
) )
self.stdout.write(f" Debug: Using to_email: {to_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")) self.stdout.write(self.style.SUCCESS("✓ Email backend test successful"))
except Exception as e: except Exception as e:
self.stdout.write( self.stdout.write(
self.style.ERROR( self.style.ERROR(f"✗ Email backend test failed: {str(e)}")
f"✗ Email backend test failed: {
str(e)}"
)
) )
raise raise
@@ -236,9 +224,6 @@ class Command(BaseCommand):
return response return response
except Exception as e: except Exception as e:
self.stdout.write( self.stdout.write(
self.style.ERROR( self.style.ERROR(f"✗ Direct EmailService test failed: {str(e)}")
f"✗ Direct EmailService test failed: {
str(e)}"
)
) )
raise raise

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -53,7 +53,7 @@ class Command(BaseCommand):
) )
companies[operator.name] = operator companies[operator.name] = operator
self.stdout.write( 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 # Create parks with their locations
@@ -301,7 +301,7 @@ class Command(BaseCommand):
"owner": company, "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 # Create location for park
if created: if created:
@@ -328,7 +328,7 @@ class Command(BaseCommand):
defaults={"description": area_data["description"]}, defaults={"description": area_data["description"]},
) )
self.stdout.write( 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")) self.stdout.write(self.style.SUCCESS("Successfully seeded initial park data"))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("parks", "0006_remove_company_insert_insert_and_more"), ("parks", "0006_remove_company_insert_insert_and_more"),
("pghistory", "0007_auto_20250421_0444"), ("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 .reviews import ParkReview
from .companies import Company, CompanyHeadquarters from .companies import Company, CompanyHeadquarters
from .media import ParkPhoto from .media import ParkPhoto
# Alias Company as Operator for clarity # Alias Company as Operator for clarity
Operator = Company Operator = Company

View File

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

View File

@@ -7,7 +7,6 @@ import pghistory
@pghistory.track() @pghistory.track()
class Company(TrackedModel): class Company(TrackedModel):
# Import managers # Import managers
from ..managers import CompanyManager from ..managers import CompanyManager
@@ -107,13 +106,7 @@ class CompanyHeadquarters(models.Model):
components.append(self.postal_code) components.append(self.postal_code)
if self.country and self.country != "USA": if self.country and self.country != "USA":
components.append(self.country) components.append(self.country)
return ( return ", ".join(components) if components else f"{self.city}, {self.country}"
", ".join(components)
if components
else f"{
self.city}, {
self.country}"
)
@property @property
def location_display(self): 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 typing import Any, Optional, cast
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.utils import timezone
from apps.core.history import TrackedModel from apps.core.history import TrackedModel
from apps.core.services.media_service import MediaService from apps.core.services.media_service import MediaService
import pghistory import pghistory
@@ -15,16 +14,14 @@ import pghistory
def park_photo_upload_path(instance: models.Model, filename: str) -> str: def park_photo_upload_path(instance: models.Model, filename: str) -> str:
"""Generate upload path for park photos.""" """Generate upload path for park photos."""
photo = cast('ParkPhoto', instance) photo = cast("ParkPhoto", instance)
park = photo.park park = photo.park
if park is None: if park is None:
raise ValueError("Park cannot be None") raise ValueError("Park cannot be None")
return MediaService.generate_upload_path( return MediaService.generate_upload_path(
domain="park", domain="park", identifier=park.slug, filename=filename
identifier=park.slug,
filename=filename
) )
@@ -33,9 +30,7 @@ class ParkPhoto(TrackedModel):
"""Photo model specific to parks.""" """Photo model specific to parks."""
park = models.ForeignKey( park = models.ForeignKey(
'parks.Park', "parks.Park", on_delete=models.CASCADE, related_name="photos"
on_delete=models.CASCADE,
related_name='photos'
) )
image = models.ImageField( image = models.ImageField(
@@ -72,9 +67,9 @@ class ParkPhoto(TrackedModel):
constraints = [ constraints = [
# Only one primary photo per park # Only one primary photo per park
models.UniqueConstraint( models.UniqueConstraint(
fields=['park'], fields=["park"],
condition=models.Q(is_primary=True), 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() @pghistory.track()
class Park(TrackedModel): class Park(TrackedModel):
# Import managers # Import managers
from ..managers import ParkManager from ..managers import ParkManager
@@ -226,7 +225,8 @@ class Park(TrackedModel):
if historical: if historical:
print( print(
f"Found historical slug record for object_id: { f"Found historical slug record for object_id: {
historical.object_id}" historical.object_id
}"
) )
try: try:
park = cls.objects.get(pk=historical.object_id) park = cls.objects.get(pk=historical.object_id)
@@ -250,7 +250,8 @@ class Park(TrackedModel):
if historical_event: if historical_event:
print( print(
f"Found pghistory event for pgh_obj_id: { f"Found pghistory event for pgh_obj_id: {
historical_event.pgh_obj_id}" historical_event.pgh_obj_id
}"
) )
try: try:
park = cls.objects.get(pk=historical_event.pgh_obj_id) park = cls.objects.get(pk=historical_event.pgh_obj_id)

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