diff --git a/.gitignore b/.gitignore index 8735f41e..90c51d31 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,5 @@ temp/ /uploads/ /backups/ .django_tailwind_cli/ +backend/.env +frontend/.env \ No newline at end of file diff --git a/.roo/rules/api_architecture_enforcement b/.roo/rules/api_architecture_enforcement index 51c8d249..4108f3c3 100644 --- a/.roo/rules/api_architecture_enforcement +++ b/.roo/rules/api_architecture_enforcement @@ -39,15 +39,15 @@ backend/ ### Required URL Pattern - **Frontend requests:** `/api/{endpoint}` - **Vite proxy rewrites to:** `/api/v1/{endpoint}` -- **Django serves from:** `backend/api/v1/{endpoint}` +- **Django serves from:** `backend/apps/api/v1/{endpoint}` ### Migration Requirements When consolidating rogue API endpoints: -1. **BEFORE REMOVAL:** Ensure ALL functionality exists in `backend/api/v1/` -2. **Move views:** Transfer all API views to appropriate `backend/api/v1/{domain}/views.py` -3. **Move serializers:** Transfer to `backend/api/v1/{domain}/serializers.py` -4. **Update URLs:** Consolidate routes in `backend/api/v1/{domain}/urls.py` +1. **BEFORE REMOVAL:** Ensure ALL functionality exists in `backend/apps/api/v1/` +2. **Move views:** Transfer all API views to appropriate `backend/apps/api/v1/{domain}/views.py` +3. **Move serializers:** Transfer to `backend/apps/api/v1/{domain}/serializers.py` +4. **Update URLs:** Consolidate routes in `backend/apps/api/v1/{domain}/urls.py` 5. **Test thoroughly:** Verify all endpoints work via central API 6. **Only then remove:** Delete the rogue `api_urls.py` and `api_views.py` files @@ -61,8 +61,8 @@ If rogue API files are discovered: 5. **Remove rogue files only after verification** ### URL Routing Rules -- **Main API router:** `backend/api/urls.py` includes `api/v1/urls.py` -- **Version router:** `backend/api/v1/urls.py` includes domain-specific routes +- **Main API router:** `backend/apps/api/urls.py` includes `apps/api/v1/urls.py` +- **Version router:** `backend/apps/api/v1/urls.py` includes domain-specific routes - **Domain routers:** `backend/api/v1/{domain}/urls.py` defines actual endpoints - **No direct app routing:** Apps CANNOT define their own API URLs @@ -72,21 +72,21 @@ If rogue API files are discovered: - **URL consistency:** All frontend API calls follow this pattern ### Quality Assurance -- **No API endpoints** may exist outside `backend/api/v1/` +- **No API endpoints** may exist outside `backend/apps/api/v1/` - **All API responses** must use proper DRF serializers - **Consistent error handling** across all endpoints - **Proper authentication** and permissions on all routes ### Examples of Proper Structure ```python -# backend/api/urls.py +# backend/apps/api/urls.py from django.urls import path, include urlpatterns = [ path('v1/', include('api.v1.urls')), ] -# backend/api/v1/urls.py +# backend/apps/api/v1/urls.py from django.urls import path, include urlpatterns = [ @@ -95,7 +95,7 @@ urlpatterns = [ path('auth/', include('api.v1.auth.urls')), ] -# backend/api/v1/rides/urls.py +# backend/apps/api/v1/rides/urls.py from django.urls import path from . import views diff --git a/backend/api/__init__.py b/backend/api/__init__.py index 41e9715b..02ddf071 100644 --- a/backend/api/__init__.py +++ b/backend/api/__init__.py @@ -3,4 +3,4 @@ Centralized API package for ThrillWiki. This package contains all API endpoints organized by version. All API routes must be routed through this centralized structure. -""" \ No newline at end of file +""" diff --git a/backend/api/urls.py b/backend/api/urls.py index 7c219133..0131e40e 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -8,5 +8,5 @@ Currently supports v1 API endpoints. from django.urls import path, include urlpatterns = [ - path('v1/', include('api.v1.urls')), + path("v1/", include("api.v1.urls")), ] diff --git a/backend/api/v1/auth/serializers.py b/backend/api/v1/auth/serializers.py index 105b48c9..3479ff66 100644 --- a/backend/api/v1/auth/serializers.py +++ b/backend/api/v1/auth/serializers.py @@ -12,15 +12,9 @@ from drf_spectacular.utils import ( OpenApiExample, ) from django.contrib.auth.password_validation import validate_password -from django.core.exceptions import ValidationError as DjangoValidationError from django.utils.crypto import get_random_string -from django.utils import timezone -from datetime import timedelta -from django.contrib.sites.shortcuts import get_current_site -from django.template.loader import render_to_string from django.contrib.auth import get_user_model -from apps.accounts.models import UserProfile, TopList, TopListItem UserModel = get_user_model() @@ -44,6 +38,7 @@ class ModelChoices: # === AUTHENTICATION SERIALIZERS === + @extend_schema_serializer( examples=[ OpenApiExample( @@ -298,6 +293,7 @@ class AuthStatusOutputSerializer(serializers.Serializer): # === USER PROFILE SERIALIZERS === + @extend_schema_serializer( examples=[ OpenApiExample( @@ -388,6 +384,7 @@ class UserProfileUpdateInputSerializer(serializers.Serializer): # === TOP LIST SERIALIZERS === + @extend_schema_serializer( examples=[ OpenApiExample( @@ -447,6 +444,7 @@ class TopListUpdateInputSerializer(serializers.Serializer): # === TOP LIST ITEM SERIALIZERS === + @extend_schema_serializer( examples=[ OpenApiExample( diff --git a/backend/api/v1/auth/urls.py b/backend/api/v1/auth/urls.py index 7a4cd74d..c32edbb2 100644 --- a/backend/api/v1/auth/urls.py +++ b/backend/api/v1/auth/urls.py @@ -21,13 +21,22 @@ urlpatterns = [ path("signup/", views.SignupAPIView.as_view(), name="auth-signup"), path("logout/", views.LogoutAPIView.as_view(), name="auth-logout"), path("user/", views.CurrentUserAPIView.as_view(), name="auth-current-user"), - path("password/reset/", views.PasswordResetAPIView.as_view(), name="auth-password-reset"), - path("password/change/", views.PasswordChangeAPIView.as_view(), - name="auth-password-change"), - path("social/providers/", views.SocialProvidersAPIView.as_view(), - name="auth-social-providers"), + path( + "password/reset/", + views.PasswordResetAPIView.as_view(), + name="auth-password-reset", + ), + path( + "password/change/", + views.PasswordChangeAPIView.as_view(), + name="auth-password-change", + ), + path( + "social/providers/", + views.SocialProvidersAPIView.as_view(), + name="auth-social-providers", + ), path("status/", views.AuthStatusAPIView.as_view(), name="auth-status"), - # Include router URLs for ViewSets (profiles, top lists) path("", include(router.urls)), ] diff --git a/backend/api/v1/auth/views.py b/backend/api/v1/auth/views.py index 95a4ce85..ec87e44b 100644 --- a/backend/api/v1/auth/views.py +++ b/backend/api/v1/auth/views.py @@ -6,12 +6,9 @@ login, signup, logout, password management, social authentication, user profiles, and top lists. """ -import time from django.contrib.auth import authenticate, login, logout, get_user_model from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import ValidationError -from django.utils import timezone -from django.conf import settings from django.db.models import Q from rest_framework import status from rest_framework.views import APIView @@ -20,7 +17,6 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.decorators import action -from allauth.socialaccount import providers from drf_spectacular.utils import extend_schema, extend_schema_view from apps.accounts.models import UserProfile, TopList, TopListItem @@ -72,6 +68,7 @@ UserModel = get_user_model() # === AUTHENTICATION API VIEWS === + @extend_schema_view( post=extend_schema( summary="User login", @@ -250,7 +247,7 @@ class LogoutAPIView(APIView): {"message": "Logout successful"} ) return Response(response_serializer.data) - except Exception as e: + except Exception: return Response( {"error": "Logout failed"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) @@ -361,7 +358,6 @@ class SocialProvidersAPIView(APIView): def get(self, request: Request) -> Response: from django.core.cache import cache - from django.contrib.sites.shortcuts import get_current_site site = get_current_site(request._request) # type: ignore[attr-defined] @@ -446,6 +442,7 @@ class AuthStatusAPIView(APIView): # === USER PROFILE API VIEWS === + class UserProfileViewSet(ModelViewSet): """ViewSet for managing user profiles.""" @@ -481,6 +478,7 @@ class UserProfileViewSet(ModelViewSet): # === TOP LIST API VIEWS === + class TopListViewSet(ModelViewSet): """ViewSet for managing user top lists.""" diff --git a/backend/api/v1/parks/serializers.py b/backend/api/v1/parks/serializers.py index a32448f0..271ff82c 100644 --- a/backend/api/v1/parks/serializers.py +++ b/backend/api/v1/parks/serializers.py @@ -12,39 +12,40 @@ class ParkPhotoOutputSerializer(serializers.ModelSerializer): """Output serializer for park photos.""" uploaded_by_username = serializers.CharField( - source='uploaded_by.username', read_only=True) + source="uploaded_by.username", read_only=True + ) file_size = serializers.ReadOnlyField() dimensions = serializers.ReadOnlyField() - park_slug = serializers.CharField(source='park.slug', read_only=True) - park_name = serializers.CharField(source='park.name', read_only=True) + park_slug = serializers.CharField(source="park.slug", read_only=True) + park_name = serializers.CharField(source="park.name", read_only=True) class Meta: model = ParkPhoto fields = [ - 'id', - 'image', - 'caption', - 'alt_text', - 'is_primary', - 'is_approved', - 'created_at', - 'updated_at', - 'date_taken', - 'uploaded_by_username', - 'file_size', - 'dimensions', - 'park_slug', - 'park_name', + "id", + "image", + "caption", + "alt_text", + "is_primary", + "is_approved", + "created_at", + "updated_at", + "date_taken", + "uploaded_by_username", + "file_size", + "dimensions", + "park_slug", + "park_name", ] read_only_fields = [ - 'id', - 'created_at', - 'updated_at', - 'uploaded_by_username', - 'file_size', - 'dimensions', - 'park_slug', - 'park_name', + "id", + "created_at", + "updated_at", + "uploaded_by_username", + "file_size", + "dimensions", + "park_slug", + "park_name", ] @@ -54,10 +55,10 @@ class ParkPhotoCreateInputSerializer(serializers.ModelSerializer): class Meta: model = ParkPhoto fields = [ - 'image', - 'caption', - 'alt_text', - 'is_primary', + "image", + "caption", + "alt_text", + "is_primary", ] @@ -67,9 +68,9 @@ class ParkPhotoUpdateInputSerializer(serializers.ModelSerializer): class Meta: model = ParkPhoto fields = [ - 'caption', - 'alt_text', - 'is_primary', + "caption", + "alt_text", + "is_primary", ] @@ -77,18 +78,19 @@ class ParkPhotoListOutputSerializer(serializers.ModelSerializer): """Simplified output serializer for park photo lists.""" uploaded_by_username = serializers.CharField( - source='uploaded_by.username', read_only=True) + source="uploaded_by.username", read_only=True + ) class Meta: model = ParkPhoto fields = [ - 'id', - 'image', - 'caption', - 'is_primary', - 'is_approved', - 'created_at', - 'uploaded_by_username', + "id", + "image", + "caption", + "is_primary", + "is_approved", + "created_at", + "uploaded_by_username", ] read_only_fields = fields @@ -97,12 +99,10 @@ class ParkPhotoApprovalInputSerializer(serializers.Serializer): """Input serializer for photo approval operations.""" photo_ids = serializers.ListField( - child=serializers.IntegerField(), - help_text="List of photo IDs to approve" + child=serializers.IntegerField(), help_text="List of photo IDs to approve" ) approve = serializers.BooleanField( - default=True, - help_text="Whether to approve (True) or reject (False) the photos" + default=True, help_text="Whether to approve (True) or reject (False) the photos" ) diff --git a/backend/api/v1/parks/urls.py b/backend/api/v1/parks/urls.py index 44b21dee..20ccc907 100644 --- a/backend/api/v1/parks/urls.py +++ b/backend/api/v1/parks/urls.py @@ -1,6 +1,7 @@ """ Park API URLs for ThrillWiki API v1. """ + from django.urls import path, include from rest_framework.routers import DefaultRouter diff --git a/backend/api/v1/parks/views.py b/backend/api/v1/parks/views.py index 2e211edf..c13d15f0 100644 --- a/backend/api/v1/parks/views.py +++ b/backend/api/v1/parks/views.py @@ -3,6 +3,7 @@ Park API views for ThrillWiki API v1. This module contains consolidated park photo viewset for the centralized API structure. """ + import logging from django.core.exceptions import PermissionDenied @@ -108,28 +109,26 @@ class ParkPhotoViewSet(ModelViewSet): def get_queryset(self): """Get photos for the current park with optimized queries.""" - return ParkPhoto.objects.select_related( - 'park', - 'park__operator', - 'uploaded_by' - ).filter( - park_id=self.kwargs.get('park_pk') - ).order_by('-created_at') + return ( + ParkPhoto.objects.select_related("park", "park__operator", "uploaded_by") + .filter(park_id=self.kwargs.get("park_pk")) + .order_by("-created_at") + ) def get_serializer_class(self): """Return appropriate serializer based on action.""" - if self.action == 'list': + if self.action == "list": return ParkPhotoListOutputSerializer - elif self.action == 'create': + elif self.action == "create": return ParkPhotoCreateInputSerializer - elif self.action in ['update', 'partial_update']: + elif self.action in ["update", "partial_update"]: return ParkPhotoUpdateInputSerializer else: return ParkPhotoOutputSerializer def perform_create(self, serializer): """Create a new park photo using ParkMediaService.""" - park_id = self.kwargs.get('park_pk') + park_id = self.kwargs.get("park_pk") if not park_id: raise ValidationError("Park ID is required") @@ -138,7 +137,7 @@ class ParkPhotoViewSet(ModelViewSet): photo = ParkMediaService.create_photo( park_id=park_id, uploaded_by=self.request.user, - **serializer.validated_data + **serializer.validated_data, ) # Set the instance for the serializer response @@ -153,19 +152,20 @@ class ParkPhotoViewSet(ModelViewSet): instance = self.get_object() # Check permissions - if not (self.request.user == instance.uploaded_by or self.request.user.is_staff): + if not ( + self.request.user == instance.uploaded_by or self.request.user.is_staff + ): raise PermissionDenied("You can only edit your own photos or be an admin.") # Handle primary photo logic using service - if serializer.validated_data.get('is_primary', False): + if serializer.validated_data.get("is_primary", False): try: ParkMediaService.set_primary_photo( - park_id=instance.park_id, - photo_id=instance.id + park_id=instance.park_id, photo_id=instance.id ) # Remove is_primary from validated_data since service handles it - if 'is_primary' in serializer.validated_data: - del serializer.validated_data['is_primary'] + if "is_primary" in serializer.validated_data: + del serializer.validated_data["is_primary"] except Exception as e: logger.error(f"Error setting primary photo: {e}") raise ValidationError(f"Failed to set primary photo: {str(e)}") @@ -175,9 +175,12 @@ class ParkPhotoViewSet(ModelViewSet): def perform_destroy(self, instance): """Delete park photo with permission checking.""" # Check permissions - if not (self.request.user == instance.uploaded_by or self.request.user.is_staff): + if not ( + self.request.user == instance.uploaded_by or self.request.user.is_staff + ): raise PermissionDenied( - "You can only delete your own photos or be an admin.") + "You can only delete your own photos or be an admin." + ) try: ParkMediaService.delete_photo(instance.id) @@ -185,7 +188,7 @@ class ParkPhotoViewSet(ModelViewSet): logger.error(f"Error deleting park photo: {e}") raise ValidationError(f"Failed to delete photo: {str(e)}") - @action(detail=True, methods=['post']) + @action(detail=True, methods=["post"]) def set_primary(self, request, **kwargs): """Set this photo as the primary photo for the park.""" photo = self.get_object() @@ -193,13 +196,11 @@ class ParkPhotoViewSet(ModelViewSet): # Check permissions if not (request.user == photo.uploaded_by or request.user.is_staff): raise PermissionDenied( - "You can only modify your own photos or be an admin.") + "You can only modify your own photos or be an admin." + ) try: - ParkMediaService.set_primary_photo( - park_id=photo.park_id, - photo_id=photo.id - ) + ParkMediaService.set_primary_photo(park_id=photo.park_id, photo_id=photo.id) # Refresh the photo instance photo.refresh_from_db() @@ -207,20 +208,20 @@ class ParkPhotoViewSet(ModelViewSet): return Response( { - 'message': 'Photo set as primary successfully', - 'photo': serializer.data + "message": "Photo set as primary successfully", + "photo": serializer.data, }, - status=status.HTTP_200_OK + status=status.HTTP_200_OK, ) except Exception as e: logger.error(f"Error setting primary photo: {e}") return Response( - {'error': f'Failed to set primary photo: {str(e)}'}, - status=status.HTTP_400_BAD_REQUEST + {"error": f"Failed to set primary photo: {str(e)}"}, + status=status.HTTP_400_BAD_REQUEST, ) - @action(detail=False, methods=['post'], permission_classes=[IsAuthenticated]) + @action(detail=False, methods=["post"], permission_classes=[IsAuthenticated]) def bulk_approve(self, request, **kwargs): """Bulk approve or reject multiple photos (admin only).""" if not request.user.is_staff: @@ -229,38 +230,35 @@ class ParkPhotoViewSet(ModelViewSet): serializer = ParkPhotoApprovalInputSerializer(data=request.data) serializer.is_valid(raise_exception=True) - photo_ids = serializer.validated_data['photo_ids'] - approve = serializer.validated_data['approve'] - park_id = self.kwargs.get('park_pk') + photo_ids = serializer.validated_data["photo_ids"] + approve = serializer.validated_data["approve"] + park_id = self.kwargs.get("park_pk") try: # Filter photos to only those belonging to this park - photos = ParkPhoto.objects.filter( - id__in=photo_ids, - park_id=park_id - ) + photos = ParkPhoto.objects.filter(id__in=photo_ids, park_id=park_id) updated_count = photos.update(is_approved=approve) return Response( { - 'message': f'Successfully {"approved" if approve else "rejected"} {updated_count} photos', - 'updated_count': updated_count + "message": f"Successfully {'approved' if approve else 'rejected'} {updated_count} photos", + "updated_count": updated_count, }, - status=status.HTTP_200_OK + status=status.HTTP_200_OK, ) except Exception as e: logger.error(f"Error in bulk photo approval: {e}") return Response( - {'error': f'Failed to update photos: {str(e)}'}, - status=status.HTTP_400_BAD_REQUEST + {"error": f"Failed to update photos: {str(e)}"}, + status=status.HTTP_400_BAD_REQUEST, ) - @action(detail=False, methods=['get']) + @action(detail=False, methods=["get"]) def stats(self, request, **kwargs): """Get photo statistics for the park.""" - park_id = self.kwargs.get('park_pk') + park_id = self.kwargs.get("park_pk") try: stats = ParkMediaService.get_photo_stats(park_id=park_id) @@ -271,6 +269,6 @@ class ParkPhotoViewSet(ModelViewSet): except Exception as e: logger.error(f"Error getting park photo stats: {e}") return Response( - {'error': f'Failed to get photo statistics: {str(e)}'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR + {"error": f"Failed to get photo statistics: {str(e)}"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) diff --git a/backend/api/v1/rides/serializers.py b/backend/api/v1/rides/serializers.py index c6cd2a16..f3b63e07 100644 --- a/backend/api/v1/rides/serializers.py +++ b/backend/api/v1/rides/serializers.py @@ -12,46 +12,47 @@ class RidePhotoOutputSerializer(serializers.ModelSerializer): """Output serializer for ride photos.""" uploaded_by_username = serializers.CharField( - source='uploaded_by.username', read_only=True) + source="uploaded_by.username", read_only=True + ) file_size = serializers.ReadOnlyField() dimensions = serializers.ReadOnlyField() - ride_slug = serializers.CharField(source='ride.slug', read_only=True) - ride_name = serializers.CharField(source='ride.name', read_only=True) - park_slug = serializers.CharField(source='ride.park.slug', read_only=True) - park_name = serializers.CharField(source='ride.park.name', read_only=True) + ride_slug = serializers.CharField(source="ride.slug", read_only=True) + ride_name = serializers.CharField(source="ride.name", read_only=True) + park_slug = serializers.CharField(source="ride.park.slug", read_only=True) + park_name = serializers.CharField(source="ride.park.name", read_only=True) class Meta: model = RidePhoto fields = [ - 'id', - 'image', - 'caption', - 'alt_text', - 'is_primary', - 'is_approved', - 'photo_type', - 'created_at', - 'updated_at', - 'date_taken', - 'uploaded_by_username', - 'file_size', - 'dimensions', - 'ride_slug', - 'ride_name', - 'park_slug', - 'park_name', + "id", + "image", + "caption", + "alt_text", + "is_primary", + "is_approved", + "photo_type", + "created_at", + "updated_at", + "date_taken", + "uploaded_by_username", + "file_size", + "dimensions", + "ride_slug", + "ride_name", + "park_slug", + "park_name", ] read_only_fields = [ - 'id', - 'created_at', - 'updated_at', - 'uploaded_by_username', - 'file_size', - 'dimensions', - 'ride_slug', - 'ride_name', - 'park_slug', - 'park_name', + "id", + "created_at", + "updated_at", + "uploaded_by_username", + "file_size", + "dimensions", + "ride_slug", + "ride_name", + "park_slug", + "park_name", ] @@ -61,11 +62,11 @@ class RidePhotoCreateInputSerializer(serializers.ModelSerializer): class Meta: model = RidePhoto fields = [ - 'image', - 'caption', - 'alt_text', - 'photo_type', - 'is_primary', + "image", + "caption", + "alt_text", + "photo_type", + "is_primary", ] @@ -75,10 +76,10 @@ class RidePhotoUpdateInputSerializer(serializers.ModelSerializer): class Meta: model = RidePhoto fields = [ - 'caption', - 'alt_text', - 'photo_type', - 'is_primary', + "caption", + "alt_text", + "photo_type", + "is_primary", ] @@ -86,19 +87,20 @@ class RidePhotoListOutputSerializer(serializers.ModelSerializer): """Simplified output serializer for ride photo lists.""" uploaded_by_username = serializers.CharField( - source='uploaded_by.username', read_only=True) + source="uploaded_by.username", read_only=True + ) class Meta: model = RidePhoto fields = [ - 'id', - 'image', - 'caption', - 'photo_type', - 'is_primary', - 'is_approved', - 'created_at', - 'uploaded_by_username', + "id", + "image", + "caption", + "photo_type", + "is_primary", + "is_approved", + "created_at", + "uploaded_by_username", ] read_only_fields = fields @@ -107,12 +109,10 @@ class RidePhotoApprovalInputSerializer(serializers.Serializer): """Input serializer for photo approval operations.""" photo_ids = serializers.ListField( - child=serializers.IntegerField(), - help_text="List of photo IDs to approve" + child=serializers.IntegerField(), help_text="List of photo IDs to approve" ) approve = serializers.BooleanField( - default=True, - help_text="Whether to approve (True) or reject (False) the photos" + default=True, help_text="Whether to approve (True) or reject (False) the photos" ) @@ -125,8 +125,7 @@ class RidePhotoStatsOutputSerializer(serializers.Serializer): has_primary = serializers.BooleanField() recent_uploads = serializers.IntegerField() by_type = serializers.DictField( - child=serializers.IntegerField(), - help_text="Photo counts by type" + child=serializers.IntegerField(), help_text="Photo counts by type" ) @@ -135,13 +134,13 @@ class RidePhotoTypeFilterSerializer(serializers.Serializer): photo_type = serializers.ChoiceField( choices=[ - ('exterior', 'Exterior View'), - ('queue', 'Queue Area'), - ('station', 'Station'), - ('onride', 'On-Ride'), - ('construction', 'Construction'), - ('other', 'Other'), + ("exterior", "Exterior View"), + ("queue", "Queue Area"), + ("station", "Station"), + ("onride", "On-Ride"), + ("construction", "Construction"), + ("other", "Other"), ], required=False, - help_text="Filter photos by type" + help_text="Filter photos by type", ) diff --git a/backend/api/v1/rides/urls.py b/backend/api/v1/rides/urls.py index 7c84459d..9086ced4 100644 --- a/backend/api/v1/rides/urls.py +++ b/backend/api/v1/rides/urls.py @@ -1,6 +1,7 @@ """ Ride API URLs for ThrillWiki API v1. """ + from django.urls import path, include from rest_framework.routers import DefaultRouter diff --git a/backend/api/v1/rides/views.py b/backend/api/v1/rides/views.py index bce9cf2a..05288c2a 100644 --- a/backend/api/v1/rides/views.py +++ b/backend/api/v1/rides/views.py @@ -3,6 +3,7 @@ Ride API views for ThrillWiki API v1. This module contains consolidated ride photo viewset for the centralized API structure. """ + import logging from django.core.exceptions import PermissionDenied @@ -108,28 +109,26 @@ class RidePhotoViewSet(ModelViewSet): def get_queryset(self): """Get photos for the current ride with optimized queries.""" - return RidePhoto.objects.select_related( - 'ride', - 'ride__park', - 'uploaded_by' - ).filter( - ride_id=self.kwargs.get('ride_pk') - ).order_by('-created_at') + return ( + RidePhoto.objects.select_related("ride", "ride__park", "uploaded_by") + .filter(ride_id=self.kwargs.get("ride_pk")) + .order_by("-created_at") + ) def get_serializer_class(self): """Return appropriate serializer based on action.""" - if self.action == 'list': + if self.action == "list": return RidePhotoListOutputSerializer - elif self.action == 'create': + elif self.action == "create": return RidePhotoCreateInputSerializer - elif self.action in ['update', 'partial_update']: + elif self.action in ["update", "partial_update"]: return RidePhotoUpdateInputSerializer else: return RidePhotoOutputSerializer def perform_create(self, serializer): """Create a new ride photo using RideMediaService.""" - ride_id = self.kwargs.get('ride_pk') + ride_id = self.kwargs.get("ride_pk") if not ride_id: raise ValidationError("Ride ID is required") @@ -138,7 +137,7 @@ class RidePhotoViewSet(ModelViewSet): photo = RideMediaService.create_photo( ride_id=ride_id, uploaded_by=self.request.user, - **serializer.validated_data + **serializer.validated_data, ) # Set the instance for the serializer response @@ -153,19 +152,20 @@ class RidePhotoViewSet(ModelViewSet): instance = self.get_object() # Check permissions - if not (self.request.user == instance.uploaded_by or self.request.user.is_staff): + if not ( + self.request.user == instance.uploaded_by or self.request.user.is_staff + ): raise PermissionDenied("You can only edit your own photos or be an admin.") # Handle primary photo logic using service - if serializer.validated_data.get('is_primary', False): + if serializer.validated_data.get("is_primary", False): try: RideMediaService.set_primary_photo( - ride_id=instance.ride_id, - photo_id=instance.id + ride_id=instance.ride_id, photo_id=instance.id ) # Remove is_primary from validated_data since service handles it - if 'is_primary' in serializer.validated_data: - del serializer.validated_data['is_primary'] + if "is_primary" in serializer.validated_data: + del serializer.validated_data["is_primary"] except Exception as e: logger.error(f"Error setting primary photo: {e}") raise ValidationError(f"Failed to set primary photo: {str(e)}") @@ -175,9 +175,12 @@ class RidePhotoViewSet(ModelViewSet): def perform_destroy(self, instance): """Delete ride photo with permission checking.""" # Check permissions - if not (self.request.user == instance.uploaded_by or self.request.user.is_staff): + if not ( + self.request.user == instance.uploaded_by or self.request.user.is_staff + ): raise PermissionDenied( - "You can only delete your own photos or be an admin.") + "You can only delete your own photos or be an admin." + ) try: RideMediaService.delete_photo(instance.id) @@ -185,7 +188,7 @@ class RidePhotoViewSet(ModelViewSet): logger.error(f"Error deleting ride photo: {e}") raise ValidationError(f"Failed to delete photo: {str(e)}") - @action(detail=True, methods=['post']) + @action(detail=True, methods=["post"]) def set_primary(self, request, **kwargs): """Set this photo as the primary photo for the ride.""" photo = self.get_object() @@ -193,13 +196,11 @@ class RidePhotoViewSet(ModelViewSet): # Check permissions if not (request.user == photo.uploaded_by or request.user.is_staff): raise PermissionDenied( - "You can only modify your own photos or be an admin.") + "You can only modify your own photos or be an admin." + ) try: - RideMediaService.set_primary_photo( - ride_id=photo.ride_id, - photo_id=photo.id - ) + RideMediaService.set_primary_photo(ride_id=photo.ride_id, photo_id=photo.id) # Refresh the photo instance photo.refresh_from_db() @@ -207,20 +208,20 @@ class RidePhotoViewSet(ModelViewSet): return Response( { - 'message': 'Photo set as primary successfully', - 'photo': serializer.data + "message": "Photo set as primary successfully", + "photo": serializer.data, }, - status=status.HTTP_200_OK + status=status.HTTP_200_OK, ) except Exception as e: logger.error(f"Error setting primary photo: {e}") return Response( - {'error': f'Failed to set primary photo: {str(e)}'}, - status=status.HTTP_400_BAD_REQUEST + {"error": f"Failed to set primary photo: {str(e)}"}, + status=status.HTTP_400_BAD_REQUEST, ) - @action(detail=False, methods=['post'], permission_classes=[IsAuthenticated]) + @action(detail=False, methods=["post"], permission_classes=[IsAuthenticated]) def bulk_approve(self, request, **kwargs): """Bulk approve or reject multiple photos (admin only).""" if not request.user.is_staff: @@ -229,38 +230,35 @@ class RidePhotoViewSet(ModelViewSet): serializer = RidePhotoApprovalInputSerializer(data=request.data) serializer.is_valid(raise_exception=True) - photo_ids = serializer.validated_data['photo_ids'] - approve = serializer.validated_data['approve'] - ride_id = self.kwargs.get('ride_pk') + photo_ids = serializer.validated_data["photo_ids"] + approve = serializer.validated_data["approve"] + ride_id = self.kwargs.get("ride_pk") try: # Filter photos to only those belonging to this ride - photos = RidePhoto.objects.filter( - id__in=photo_ids, - ride_id=ride_id - ) + photos = RidePhoto.objects.filter(id__in=photo_ids, ride_id=ride_id) updated_count = photos.update(is_approved=approve) return Response( { - 'message': f'Successfully {"approved" if approve else "rejected"} {updated_count} photos', - 'updated_count': updated_count + "message": f"Successfully {'approved' if approve else 'rejected'} {updated_count} photos", + "updated_count": updated_count, }, - status=status.HTTP_200_OK + status=status.HTTP_200_OK, ) except Exception as e: logger.error(f"Error in bulk photo approval: {e}") return Response( - {'error': f'Failed to update photos: {str(e)}'}, - status=status.HTTP_400_BAD_REQUEST + {"error": f"Failed to update photos: {str(e)}"}, + status=status.HTTP_400_BAD_REQUEST, ) - @action(detail=False, methods=['get']) + @action(detail=False, methods=["get"]) def stats(self, request, **kwargs): """Get photo statistics for the ride.""" - ride_id = self.kwargs.get('ride_pk') + ride_id = self.kwargs.get("ride_pk") try: stats = RideMediaService.get_photo_stats(ride_id=ride_id) @@ -271,6 +269,6 @@ class RidePhotoViewSet(ModelViewSet): except Exception as e: logger.error(f"Error getting ride photo stats: {e}") return Response( - {'error': f'Failed to get photo statistics: {str(e)}'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR + {"error": f"Failed to get photo statistics: {str(e)}"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) diff --git a/backend/api/v1/urls.py b/backend/api/v1/urls.py index d806fada..c3382858 100644 --- a/backend/api/v1/urls.py +++ b/backend/api/v1/urls.py @@ -9,11 +9,10 @@ from django.urls import path, include urlpatterns = [ # Domain-specific API endpoints - path('rides/', include('api.v1.rides.urls')), - path('parks/', include('api.v1.parks.urls')), - path('auth/', include('api.v1.auth.urls')), - + path("rides/", include("api.v1.rides.urls")), + path("parks/", include("api.v1.parks.urls")), + path("auth/", include("api.v1.auth.urls")), # Media endpoints (for photo management) # Will be consolidated from the various media implementations - path('media/', include('api.v1.media.urls')), + path("media/", include("api.v1.media.urls")), ] diff --git a/backend/apps/accounts/management/commands/check_all_social_tables.py b/backend/apps/accounts/management/commands/check_all_social_tables.py index fedeeaf8..7e65f416 100644 --- a/backend/apps/accounts/management/commands/check_all_social_tables.py +++ b/backend/apps/accounts/management/commands/check_all_social_tables.py @@ -11,11 +11,9 @@ class Command(BaseCommand): self.stdout.write("\nChecking SocialApp table:") for app in SocialApp.objects.all(): self.stdout.write( - f"ID: { - app.pk}, Provider: { - app.provider}, Name: { - app.name}, Client ID: { - app.client_id}" + f"ID: {app.pk}, Provider: {app.provider}, Name: {app.name}, Client ID: { + app.client_id + }" ) self.stdout.write("Sites:") for site in app.sites.all(): @@ -25,10 +23,7 @@ class Command(BaseCommand): self.stdout.write("\nChecking SocialAccount table:") for account in SocialAccount.objects.all(): self.stdout.write( - f"ID: { - account.pk}, Provider: { - account.provider}, UID: { - account.uid}" + f"ID: {account.pk}, Provider: {account.provider}, UID: {account.uid}" ) # Check SocialToken diff --git a/backend/apps/accounts/management/commands/check_social_apps.py b/backend/apps/accounts/management/commands/check_social_apps.py index 33a66011..473f99a4 100644 --- a/backend/apps/accounts/management/commands/check_social_apps.py +++ b/backend/apps/accounts/management/commands/check_social_apps.py @@ -13,15 +13,10 @@ class Command(BaseCommand): return for app in social_apps: - self.stdout.write( - self.style.SUCCESS( - f"\nProvider: { - app.provider}" - ) - ) + self.stdout.write(self.style.SUCCESS(f"\nProvider: {app.provider}")) self.stdout.write(f"Name: {app.name}") self.stdout.write(f"Client ID: {app.client_id}") self.stdout.write(f"Secret: {app.secret}") self.stdout.write( - f'Sites: {", ".join(str(site.domain) for site in app.sites.all())}' + f"Sites: {', '.join(str(site.domain) for site in app.sites.all())}" ) diff --git a/backend/apps/accounts/management/commands/cleanup_test_data.py b/backend/apps/accounts/management/commands/cleanup_test_data.py index 6b4cf65a..2bb7dee5 100644 --- a/backend/apps/accounts/management/commands/cleanup_test_data.py +++ b/backend/apps/accounts/management/commands/cleanup_test_data.py @@ -26,13 +26,15 @@ class Command(BaseCommand): # Delete test photos - both park and ride photos park_photos = ParkPhoto.objects.filter( - uploader__username__in=["testuser", "moderator"]) + uploader__username__in=["testuser", "moderator"] + ) park_count = park_photos.count() park_photos.delete() self.stdout.write(self.style.SUCCESS(f"Deleted {park_count} test park photos")) ride_photos = RidePhoto.objects.filter( - uploader__username__in=["testuser", "moderator"]) + uploader__username__in=["testuser", "moderator"] + ) ride_count = ride_photos.count() ride_photos.delete() self.stdout.write(self.style.SUCCESS(f"Deleted {ride_count} test ride photos")) diff --git a/backend/apps/accounts/management/commands/create_social_apps.py b/backend/apps/accounts/management/commands/create_social_apps.py index b45e9e63..ef858549 100644 --- a/backend/apps/accounts/management/commands/create_social_apps.py +++ b/backend/apps/accounts/management/commands/create_social_apps.py @@ -30,7 +30,7 @@ class Command(BaseCommand): discord_app.secret = "ece7Pe_M4mD4mYzAgcINjTEKL_3ftL11" discord_app.save() discord_app.sites.add(site) - self.stdout.write(f'{"Created" if created else "Updated"} Discord app') + self.stdout.write(f"{'Created' if created else 'Updated'} Discord app") # Create Google app google_app, created = SocialApp.objects.get_or_create( @@ -52,4 +52,4 @@ class Command(BaseCommand): google_app.secret = "GOCSPX-Wd_0Ue0Ue0Ue0Ue0Ue0Ue0Ue0Ue" google_app.save() google_app.sites.add(site) - self.stdout.write(f'{"Created" if created else "Updated"} Google app') + self.stdout.write(f"{'Created' if created else 'Updated'} Google app") diff --git a/backend/apps/accounts/management/commands/fix_social_apps.py b/backend/apps/accounts/management/commands/fix_social_apps.py index 8bbc4372..c5032c1c 100644 --- a/backend/apps/accounts/management/commands/fix_social_apps.py +++ b/backend/apps/accounts/management/commands/fix_social_apps.py @@ -23,10 +23,7 @@ class Command(BaseCommand): secret=os.getenv("GOOGLE_CLIENT_SECRET"), ) google_app.sites.add(site) - self.stdout.write( - f"Created Google app with client_id: { - google_app.client_id}" - ) + self.stdout.write(f"Created Google app with client_id: {google_app.client_id}") # Create Discord provider discord_app = SocialApp.objects.create( diff --git a/backend/apps/accounts/management/commands/regenerate_avatars.py b/backend/apps/accounts/management/commands/regenerate_avatars.py index 7ee58fc8..be75d04e 100644 --- a/backend/apps/accounts/management/commands/regenerate_avatars.py +++ b/backend/apps/accounts/management/commands/regenerate_avatars.py @@ -11,8 +11,5 @@ class Command(BaseCommand): # This will trigger the avatar generation logic in the save method profile.save() self.stdout.write( - self.style.SUCCESS( - f"Regenerated avatar for { - profile.user.username}" - ) + self.style.SUCCESS(f"Regenerated avatar for {profile.user.username}") ) diff --git a/backend/apps/accounts/management/commands/reset_db.py b/backend/apps/accounts/management/commands/reset_db.py index b08ffcdb..ea2a105a 100644 --- a/backend/apps/accounts/management/commands/reset_db.py +++ b/backend/apps/accounts/management/commands/reset_db.py @@ -102,12 +102,7 @@ class Command(BaseCommand): self.stdout.write("Superuser created.") except Exception as e: - self.stdout.write( - self.style.ERROR( - f"Error creating superuser: { - str(e)}" - ) - ) + self.stdout.write(self.style.ERROR(f"Error creating superuser: {str(e)}")) raise self.stdout.write(self.style.SUCCESS("Database reset complete.")) diff --git a/backend/apps/accounts/management/commands/setup_groups.py b/backend/apps/accounts/management/commands/setup_groups.py index 1fb86d11..d4081cbd 100644 --- a/backend/apps/accounts/management/commands/setup_groups.py +++ b/backend/apps/accounts/management/commands/setup_groups.py @@ -41,9 +41,4 @@ class Command(BaseCommand): self.stdout.write(f" - {perm.codename}") except Exception as e: - self.stdout.write( - self.style.ERROR( - f"Error setting up groups: { - str(e)}" - ) - ) + self.stdout.write(self.style.ERROR(f"Error setting up groups: {str(e)}")) diff --git a/backend/apps/accounts/management/commands/setup_social_auth.py b/backend/apps/accounts/management/commands/setup_social_auth.py index a0e0fb90..e6333d91 100644 --- a/backend/apps/accounts/management/commands/setup_social_auth.py +++ b/backend/apps/accounts/management/commands/setup_social_auth.py @@ -20,20 +20,24 @@ class Command(BaseCommand): # DEBUG: Log environment variable values self.stdout.write( - f"DEBUG: google_client_id type: { - type(google_client_id)}, value: {google_client_id}" + f"DEBUG: google_client_id type: {type(google_client_id)}, value: { + google_client_id + }" ) self.stdout.write( - f"DEBUG: google_client_secret type: { - type(google_client_secret)}, value: {google_client_secret}" + f"DEBUG: google_client_secret type: {type(google_client_secret)}, value: { + google_client_secret + }" ) self.stdout.write( - f"DEBUG: discord_client_id type: { - type(discord_client_id)}, value: {discord_client_id}" + f"DEBUG: discord_client_id type: {type(discord_client_id)}, value: { + discord_client_id + }" ) self.stdout.write( - f"DEBUG: discord_client_secret type: { - type(discord_client_secret)}, value: {discord_client_secret}" + f"DEBUG: discord_client_secret type: {type(discord_client_secret)}, value: { + discord_client_secret + }" ) if not all( @@ -51,16 +55,13 @@ class Command(BaseCommand): f"DEBUG: google_client_id is None: {google_client_id is None}" ) self.stdout.write( - f"DEBUG: google_client_secret is None: { - google_client_secret is None}" + f"DEBUG: google_client_secret is None: {google_client_secret is None}" ) self.stdout.write( - f"DEBUG: discord_client_id is None: { - discord_client_id is None}" + f"DEBUG: discord_client_id is None: {discord_client_id is None}" ) self.stdout.write( - f"DEBUG: discord_client_secret is None: { - discord_client_secret is None}" + f"DEBUG: discord_client_secret is None: {discord_client_secret is None}" ) return @@ -81,7 +82,8 @@ class Command(BaseCommand): if not created: self.stdout.write( f"DEBUG: About to assign google_client_id: {google_client_id} (type: { - type(google_client_id)})" + type(google_client_id) + })" ) if google_client_id is not None and google_client_secret is not None: google_app.client_id = google_client_id @@ -108,7 +110,8 @@ class Command(BaseCommand): if not created: self.stdout.write( f"DEBUG: About to assign discord_client_id: {discord_client_id} (type: { - type(discord_client_id)})" + type(discord_client_id) + })" ) if discord_client_id is not None and discord_client_secret is not None: discord_app.client_id = discord_client_id diff --git a/backend/apps/accounts/management/commands/setup_social_auth_admin.py b/backend/apps/accounts/management/commands/setup_social_auth_admin.py index bb030798..47523442 100644 --- a/backend/apps/accounts/management/commands/setup_social_auth_admin.py +++ b/backend/apps/accounts/management/commands/setup_social_auth_admin.py @@ -21,7 +21,7 @@ class Command(BaseCommand): site.domain = "localhost:8000" site.name = "ThrillWiki Development" site.save() - self.stdout.write(f'{"Created" if _ else "Updated"} site: {site.domain}') + self.stdout.write(f"{'Created' if _ else 'Updated'} site: {site.domain}") # Create superuser if it doesn't exist if not User.objects.filter(username="admin").exists(): diff --git a/backend/apps/accounts/management/commands/update_social_apps_sites.py b/backend/apps/accounts/management/commands/update_social_apps_sites.py index 2e493170..61ecde08 100644 --- a/backend/apps/accounts/management/commands/update_social_apps_sites.py +++ b/backend/apps/accounts/management/commands/update_social_apps_sites.py @@ -19,5 +19,5 @@ class Command(BaseCommand): for site in sites: app.sites.add(site) self.stdout.write( - f'Added sites: {", ".join(site.domain for site in sites)}' + f"Added sites: {', '.join(site.domain for site in sites)}" ) diff --git a/backend/apps/accounts/management/commands/verify_discord_settings.py b/backend/apps/accounts/management/commands/verify_discord_settings.py index c83fe6da..50f5c379 100644 --- a/backend/apps/accounts/management/commands/verify_discord_settings.py +++ b/backend/apps/accounts/management/commands/verify_discord_settings.py @@ -31,12 +31,9 @@ class Command(BaseCommand): self.stdout.write("\nOAuth2 settings in settings.py:") discord_settings = settings.SOCIALACCOUNT_PROVIDERS.get("discord", {}) self.stdout.write( - f'PKCE Enabled: { - discord_settings.get( - "OAUTH_PKCE_ENABLED", - False)}' + f"PKCE Enabled: {discord_settings.get('OAUTH_PKCE_ENABLED', False)}" ) - self.stdout.write(f'Scopes: {discord_settings.get("SCOPE", [])}') + self.stdout.write(f"Scopes: {discord_settings.get('SCOPE', [])}") except SocialApp.DoesNotExist: self.stdout.write(self.style.ERROR("Discord app not found")) diff --git a/backend/apps/accounts/migrations/0001_initial.py b/backend/apps/accounts/migrations/0001_initial.py index 61ba3912..544048ac 100644 --- a/backend/apps/accounts/migrations/0001_initial.py +++ b/backend/apps/accounts/migrations/0001_initial.py @@ -11,7 +11,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/backend/apps/accounts/migrations/0002_remove_toplistevent_pgh_context_and_more.py b/backend/apps/accounts/migrations/0002_remove_toplistevent_pgh_context_and_more.py index 292c0890..20709618 100644 --- a/backend/apps/accounts/migrations/0002_remove_toplistevent_pgh_context_and_more.py +++ b/backend/apps/accounts/migrations/0002_remove_toplistevent_pgh_context_and_more.py @@ -5,7 +5,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("accounts", "0001_initial"), ] diff --git a/backend/apps/accounts/migrations/0003_emailverificationevent_passwordresetevent_userevent_and_more.py b/backend/apps/accounts/migrations/0003_emailverificationevent_passwordresetevent_userevent_and_more.py index d0a8fb72..73374205 100644 --- a/backend/apps/accounts/migrations/0003_emailverificationevent_passwordresetevent_userevent_and_more.py +++ b/backend/apps/accounts/migrations/0003_emailverificationevent_passwordresetevent_userevent_and_more.py @@ -10,7 +10,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("accounts", "0002_remove_toplistevent_pgh_context_and_more"), ("pghistory", "0007_auto_20250421_0444"), diff --git a/backend/apps/accounts/serializers.py b/backend/apps/accounts/serializers.py index 152a9e77..b80ed896 100644 --- a/backend/apps/accounts/serializers.py +++ b/backend/apps/accounts/serializers.py @@ -1,7 +1,6 @@ from rest_framework import serializers from django.contrib.auth import get_user_model from django.contrib.auth.password_validation import validate_password -from django.core.exceptions import ValidationError from django.utils.crypto import get_random_string from django.utils import timezone from datetime import timedelta diff --git a/backend/apps/accounts/signals.py b/backend/apps/accounts/signals.py index e7f9dc7b..a173e4d8 100644 --- a/backend/apps/accounts/signals.py +++ b/backend/apps/accounts/signals.py @@ -42,9 +42,9 @@ def create_user_profile(sender, instance, created, **kwargs): profile.avatar.save(file_name, File(img_temp), save=True) except Exception as e: print( - f"Error downloading avatar for user { - instance.username}: { - str(e)}" + f"Error downloading avatar for user {instance.username}: { + str(e) + }" ) except Exception as e: print(f"Error creating profile for user {instance.username}: {str(e)}") @@ -117,9 +117,7 @@ def sync_user_role_with_groups(sender, instance, **kwargs): pass except Exception as e: print( - f"Error syncing role with groups for user { - instance.username}: { - str(e)}" + f"Error syncing role with groups for user {instance.username}: {str(e)}" ) diff --git a/backend/apps/api/v1/history/views.py b/backend/apps/api/v1/history/views.py index d2a44326..0c8cc35f 100644 --- a/backend/apps/api/v1/history/views.py +++ b/backend/apps/api/v1/history/views.py @@ -5,12 +5,10 @@ This module provides ViewSets for accessing historical data and change tracking across all models in the ThrillWiki system using django-pghistory. """ -from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter from drf_spectacular.types import OpenApiTypes from rest_framework.filters import OrderingFilter from rest_framework.permissions import AllowAny -from rest_framework.request import Request from rest_framework.response import Response from rest_framework.viewsets import ReadOnlyModelViewSet from django.shortcuts import get_object_or_404 diff --git a/backend/apps/api/v1/maps/views.py b/backend/apps/api/v1/maps/views.py index 2c2a8e47..b7375484 100644 --- a/backend/apps/api/v1/maps/views.py +++ b/backend/apps/api/v1/maps/views.py @@ -3,18 +3,9 @@ Centralized map API views. Migrated from apps.core.views.map_views """ -import json import logging -import time -from typing import Dict, Any, Optional -from django.http import JsonResponse, HttpRequest -from django.views.decorators.cache import cache_page -from django.views.decorators.gzip import gzip_page -from django.utils.decorators import method_decorator -from django.views import View -from django.core.exceptions import ValidationError -from django.conf import settings +from django.http import HttpRequest from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status @@ -30,15 +21,54 @@ logger = logging.getLogger(__name__) summary="Get map locations", description="Get map locations with optional clustering and filtering.", parameters=[ - {"name": "north", "in": "query", "required": False, "schema": {"type": "number"}}, - {"name": "south", "in": "query", "required": False, "schema": {"type": "number"}}, - {"name": "east", "in": "query", "required": False, "schema": {"type": "number"}}, - {"name": "west", "in": "query", "required": False, "schema": {"type": "number"}}, - {"name": "zoom", "in": "query", "required": False, "schema": {"type": "integer"}}, - {"name": "types", "in": "query", "required": False, "schema": {"type": "string"}}, - {"name": "cluster", "in": "query", "required": False, - "schema": {"type": "boolean"}}, - {"name": "q", "in": "query", "required": False, "schema": {"type": "string"}}, + { + "name": "north", + "in": "query", + "required": False, + "schema": {"type": "number"}, + }, + { + "name": "south", + "in": "query", + "required": False, + "schema": {"type": "number"}, + }, + { + "name": "east", + "in": "query", + "required": False, + "schema": {"type": "number"}, + }, + { + "name": "west", + "in": "query", + "required": False, + "schema": {"type": "number"}, + }, + { + "name": "zoom", + "in": "query", + "required": False, + "schema": {"type": "integer"}, + }, + { + "name": "types", + "in": "query", + "required": False, + "schema": {"type": "string"}, + }, + { + "name": "cluster", + "in": "query", + "required": False, + "schema": {"type": "boolean"}, + }, + { + "name": "q", + "in": "query", + "required": False, + "schema": {"type": "string"}, + }, ], responses={200: OpenApiTypes.OBJECT}, tags=["Maps"], @@ -54,18 +84,20 @@ class MapLocationsAPIView(APIView): try: # Simple implementation to fix import error # TODO: Implement full functionality - return Response({ - "status": "success", - "message": "Map locations endpoint - implementation needed", - "data": [] - }) + return Response( + { + "status": "success", + "message": "Map locations endpoint - implementation needed", + "data": [], + } + ) except Exception as e: logger.error(f"Error in MapLocationsAPIView: {str(e)}", exc_info=True) - return Response({ - "status": "error", - "message": "Failed to retrieve map locations" - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response( + {"status": "error", "message": "Failed to retrieve map locations"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) @extend_schema_view( @@ -73,10 +105,18 @@ class MapLocationsAPIView(APIView): summary="Get location details", description="Get detailed information about a specific location.", parameters=[ - {"name": "location_type", "in": "path", - "required": True, "schema": {"type": "string"}}, - {"name": "location_id", "in": "path", - "required": True, "schema": {"type": "integer"}}, + { + "name": "location_type", + "in": "path", + "required": True, + "schema": {"type": "string"}, + }, + { + "name": "location_id", + "in": "path", + "required": True, + "schema": {"type": "integer"}, + }, ], responses={200: OpenApiTypes.OBJECT, 404: OpenApiTypes.OBJECT}, tags=["Maps"], @@ -87,25 +127,29 @@ class MapLocationDetailAPIView(APIView): permission_classes = [AllowAny] - def get(self, request: HttpRequest, location_type: str, location_id: int) -> Response: + def get( + self, request: HttpRequest, location_type: str, location_id: int + ) -> Response: """Get detailed information for a specific location.""" try: # Simple implementation to fix import error - return Response({ - "status": "success", - "message": f"Location detail for {location_type}/{location_id} - implementation needed", - "data": { - "location_type": location_type, - "location_id": location_id + return Response( + { + "status": "success", + "message": f"Location detail for {location_type}/{location_id} - implementation needed", + "data": { + "location_type": location_type, + "location_id": location_id, + }, } - }) + ) except Exception as e: logger.error(f"Error in MapLocationDetailAPIView: {str(e)}", exc_info=True) - return Response({ - "status": "error", - "message": "Failed to retrieve location details" - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response( + {"status": "error", "message": "Failed to retrieve location details"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) @extend_schema_view( @@ -113,7 +157,12 @@ class MapLocationDetailAPIView(APIView): summary="Search map locations", description="Search locations by text query with optional bounds filtering.", parameters=[ - {"name": "q", "in": "query", "required": True, "schema": {"type": "string"}}, + { + "name": "q", + "in": "query", + "required": True, + "schema": {"type": "string"}, + }, ], responses={200: OpenApiTypes.OBJECT, 400: OpenApiTypes.OBJECT}, tags=["Maps"], @@ -129,24 +178,29 @@ class MapSearchAPIView(APIView): try: query = request.GET.get("q", "").strip() if not query: - return Response({ - "status": "error", - "message": "Search query 'q' parameter is required" - }, status=status.HTTP_400_BAD_REQUEST) + return Response( + { + "status": "error", + "message": "Search query 'q' parameter is required", + }, + status=status.HTTP_400_BAD_REQUEST, + ) # Simple implementation to fix import error - return Response({ - "status": "success", - "message": f"Search for '{query}' - implementation needed", - "data": [] - }) + return Response( + { + "status": "success", + "message": f"Search for '{query}' - implementation needed", + "data": [], + } + ) except Exception as e: logger.error(f"Error in MapSearchAPIView: {str(e)}", exc_info=True) - return Response({ - "status": "error", - "message": "Search failed due to internal error" - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response( + {"status": "error", "message": "Search failed due to internal error"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) @extend_schema_view( @@ -154,10 +208,30 @@ class MapSearchAPIView(APIView): summary="Get locations within bounds", description="Get locations within specific geographic bounds.", parameters=[ - {"name": "north", "in": "query", "required": True, "schema": {"type": "number"}}, - {"name": "south", "in": "query", "required": True, "schema": {"type": "number"}}, - {"name": "east", "in": "query", "required": True, "schema": {"type": "number"}}, - {"name": "west", "in": "query", "required": True, "schema": {"type": "number"}}, + { + "name": "north", + "in": "query", + "required": True, + "schema": {"type": "number"}, + }, + { + "name": "south", + "in": "query", + "required": True, + "schema": {"type": "number"}, + }, + { + "name": "east", + "in": "query", + "required": True, + "schema": {"type": "number"}, + }, + { + "name": "west", + "in": "query", + "required": True, + "schema": {"type": "number"}, + }, ], responses={200: OpenApiTypes.OBJECT, 400: OpenApiTypes.OBJECT}, tags=["Maps"], @@ -172,18 +246,23 @@ class MapBoundsAPIView(APIView): """Get locations within specific geographic bounds.""" try: # Simple implementation to fix import error - return Response({ - "status": "success", - "message": "Bounds query - implementation needed", - "data": [] - }) + return Response( + { + "status": "success", + "message": "Bounds query - implementation needed", + "data": [], + } + ) except Exception as e: logger.error(f"Error in MapBoundsAPIView: {str(e)}", exc_info=True) - return Response({ - "status": "error", - "message": "Failed to retrieve locations within bounds" - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response( + { + "status": "error", + "message": "Failed to retrieve locations within bounds", + }, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) @extend_schema_view( @@ -203,14 +282,12 @@ class MapStatsAPIView(APIView): """Get map service statistics and performance metrics.""" try: # Simple implementation to fix import error - return Response({ - "status": "success", - "data": { - "total_locations": 0, - "cache_hits": 0, - "cache_misses": 0 + return Response( + { + "status": "success", + "data": {"total_locations": 0, "cache_hits": 0, "cache_misses": 0}, } - }) + ) except Exception as e: return Response( @@ -242,10 +319,9 @@ class MapCacheAPIView(APIView): """Clear all map cache (admin only).""" try: # Simple implementation to fix import error - return Response({ - "status": "success", - "message": "Map cache cleared successfully" - }) + return Response( + {"status": "success", "message": "Map cache cleared successfully"} + ) except Exception as e: return Response( @@ -257,10 +333,9 @@ class MapCacheAPIView(APIView): """Invalidate specific cache entries.""" try: # Simple implementation to fix import error - return Response({ - "status": "success", - "message": "Cache invalidated successfully" - }) + return Response( + {"status": "success", "message": "Cache invalidated successfully"} + ) except Exception as e: return Response( diff --git a/backend/apps/api/v1/parks/urls.py b/backend/apps/api/v1/parks/urls.py index 44b21dee..20ccc907 100644 --- a/backend/apps/api/v1/parks/urls.py +++ b/backend/apps/api/v1/parks/urls.py @@ -1,6 +1,7 @@ """ Park API URLs for ThrillWiki API v1. """ + from django.urls import path, include from rest_framework.routers import DefaultRouter diff --git a/backend/apps/api/v1/parks/views.py b/backend/apps/api/v1/parks/views.py index bf7c06f7..0c8fde4e 100644 --- a/backend/apps/api/v1/parks/views.py +++ b/backend/apps/api/v1/parks/views.py @@ -1,6 +1,7 @@ """ Park API views for ThrillWiki API v1. """ + import logging from django.core.exceptions import PermissionDenied diff --git a/backend/apps/api/v1/rides/urls.py b/backend/apps/api/v1/rides/urls.py index 7c84459d..9086ced4 100644 --- a/backend/apps/api/v1/rides/urls.py +++ b/backend/apps/api/v1/rides/urls.py @@ -1,6 +1,7 @@ """ Ride API URLs for ThrillWiki API v1. """ + from django.urls import path, include from rest_framework.routers import DefaultRouter diff --git a/backend/apps/api/v1/rides/views.py b/backend/apps/api/v1/rides/views.py index 1c0efe0f..c4c509ae 100644 --- a/backend/apps/api/v1/rides/views.py +++ b/backend/apps/api/v1/rides/views.py @@ -1,6 +1,7 @@ """ Ride API views for ThrillWiki API v1. """ + import logging from django.core.exceptions import PermissionDenied diff --git a/backend/apps/api/v1/schema.py b/backend/apps/api/v1/schema.py index 11a512fe..1a5f32c4 100644 --- a/backend/apps/api/v1/schema.py +++ b/backend/apps/api/v1/schema.py @@ -6,8 +6,6 @@ for the ThrillWiki API, including better documentation and examples. """ from drf_spectacular.openapi import AutoSchema -from drf_spectacular.utils import OpenApiExample -from drf_spectacular.types import OpenApiTypes # Custom examples for common serializers diff --git a/backend/apps/api/v1/serializers/accounts.py b/backend/apps/api/v1/serializers/accounts.py index 85f8993c..6d19305f 100644 --- a/backend/apps/api/v1/serializers/accounts.py +++ b/backend/apps/api/v1/serializers/accounts.py @@ -12,12 +12,7 @@ from drf_spectacular.utils import ( OpenApiExample, ) from django.contrib.auth.password_validation import validate_password -from django.core.exceptions import ValidationError as DjangoValidationError from django.utils.crypto import get_random_string -from django.utils import timezone -from datetime import timedelta -from django.contrib.sites.shortcuts import get_current_site -from django.template.loader import render_to_string from .shared import UserModel, ModelChoices diff --git a/backend/apps/api/v1/serializers/history.py b/backend/apps/api/v1/serializers/history.py index fab7d9ed..4aeb4e1c 100644 --- a/backend/apps/api/v1/serializers/history.py +++ b/backend/apps/api/v1/serializers/history.py @@ -6,8 +6,7 @@ using django-pghistory. """ from rest_framework import serializers -from drf_spectacular.utils import extend_schema_serializer, extend_schema_field -import pghistory.models +from drf_spectacular.utils import extend_schema_field class ParkHistoryEventSerializer(serializers.Serializer): diff --git a/backend/apps/api/v1/serializers/other.py b/backend/apps/api/v1/serializers/other.py index 62c60815..82c88159 100644 --- a/backend/apps/api/v1/serializers/other.py +++ b/backend/apps/api/v1/serializers/other.py @@ -7,9 +7,7 @@ miscellaneous functionality. from rest_framework import serializers from drf_spectacular.utils import ( - extend_schema_serializer, extend_schema_field, - OpenApiExample, ) diff --git a/backend/apps/api/v1/serializers/parks.py b/backend/apps/api/v1/serializers/parks.py index bf91117e..4ad273c8 100644 --- a/backend/apps/api/v1/serializers/parks.py +++ b/backend/apps/api/v1/serializers/parks.py @@ -239,7 +239,8 @@ class ParkFilterInputSerializer(serializers.Serializer): # Status filter status = serializers.MultipleChoiceField( - choices=[], required=False # Choices set dynamically + choices=[], + required=False, # Choices set dynamically ) # Location filters diff --git a/backend/apps/api/v1/serializers/parks_media.py b/backend/apps/api/v1/serializers/parks_media.py index 32c12382..71fc0d64 100644 --- a/backend/apps/api/v1/serializers/parks_media.py +++ b/backend/apps/api/v1/serializers/parks_media.py @@ -12,39 +12,40 @@ class ParkPhotoOutputSerializer(serializers.ModelSerializer): """Output serializer for park photos.""" uploaded_by_username = serializers.CharField( - source='uploaded_by.username', read_only=True) + source="uploaded_by.username", read_only=True + ) file_size = serializers.ReadOnlyField() dimensions = serializers.ReadOnlyField() - park_slug = serializers.CharField(source='park.slug', read_only=True) - park_name = serializers.CharField(source='park.name', read_only=True) + park_slug = serializers.CharField(source="park.slug", read_only=True) + park_name = serializers.CharField(source="park.name", read_only=True) class Meta: model = ParkPhoto fields = [ - 'id', - 'image', - 'caption', - 'alt_text', - 'is_primary', - 'is_approved', - 'created_at', - 'updated_at', - 'date_taken', - 'uploaded_by_username', - 'file_size', - 'dimensions', - 'park_slug', - 'park_name', + "id", + "image", + "caption", + "alt_text", + "is_primary", + "is_approved", + "created_at", + "updated_at", + "date_taken", + "uploaded_by_username", + "file_size", + "dimensions", + "park_slug", + "park_name", ] read_only_fields = [ - 'id', - 'created_at', - 'updated_at', - 'uploaded_by_username', - 'file_size', - 'dimensions', - 'park_slug', - 'park_name', + "id", + "created_at", + "updated_at", + "uploaded_by_username", + "file_size", + "dimensions", + "park_slug", + "park_name", ] @@ -54,10 +55,10 @@ class ParkPhotoCreateInputSerializer(serializers.ModelSerializer): class Meta: model = ParkPhoto fields = [ - 'image', - 'caption', - 'alt_text', - 'is_primary', + "image", + "caption", + "alt_text", + "is_primary", ] @@ -67,9 +68,9 @@ class ParkPhotoUpdateInputSerializer(serializers.ModelSerializer): class Meta: model = ParkPhoto fields = [ - 'caption', - 'alt_text', - 'is_primary', + "caption", + "alt_text", + "is_primary", ] @@ -77,18 +78,19 @@ class ParkPhotoListOutputSerializer(serializers.ModelSerializer): """Simplified output serializer for park photo lists.""" uploaded_by_username = serializers.CharField( - source='uploaded_by.username', read_only=True) + source="uploaded_by.username", read_only=True + ) class Meta: model = ParkPhoto fields = [ - 'id', - 'image', - 'caption', - 'is_primary', - 'is_approved', - 'created_at', - 'uploaded_by_username', + "id", + "image", + "caption", + "is_primary", + "is_approved", + "created_at", + "uploaded_by_username", ] read_only_fields = fields @@ -97,12 +99,10 @@ class ParkPhotoApprovalInputSerializer(serializers.Serializer): """Input serializer for photo approval operations.""" photo_ids = serializers.ListField( - child=serializers.IntegerField(), - help_text="List of photo IDs to approve" + child=serializers.IntegerField(), help_text="List of photo IDs to approve" ) approve = serializers.BooleanField( - default=True, - help_text="Whether to approve (True) or reject (False) the photos" + default=True, help_text="Whether to approve (True) or reject (False) the photos" ) diff --git a/backend/apps/api/v1/serializers/rides.py b/backend/apps/api/v1/serializers/rides.py index 02c557b0..cc7790fb 100644 --- a/backend/apps/api/v1/serializers/rides.py +++ b/backend/apps/api/v1/serializers/rides.py @@ -344,7 +344,8 @@ class RideFilterInputSerializer(serializers.Serializer): # Status filter status = serializers.MultipleChoiceField( - choices=[], required=False # Choices set dynamically + choices=[], + required=False, # Choices set dynamically ) # Park filter diff --git a/backend/apps/api/v1/serializers/rides_media.py b/backend/apps/api/v1/serializers/rides_media.py index 39d58f2a..1cc01306 100644 --- a/backend/apps/api/v1/serializers/rides_media.py +++ b/backend/apps/api/v1/serializers/rides_media.py @@ -12,46 +12,47 @@ class RidePhotoOutputSerializer(serializers.ModelSerializer): """Output serializer for ride photos.""" uploaded_by_username = serializers.CharField( - source='uploaded_by.username', read_only=True) + source="uploaded_by.username", read_only=True + ) file_size = serializers.ReadOnlyField() dimensions = serializers.ReadOnlyField() - ride_slug = serializers.CharField(source='ride.slug', read_only=True) - ride_name = serializers.CharField(source='ride.name', read_only=True) - park_slug = serializers.CharField(source='ride.park.slug', read_only=True) - park_name = serializers.CharField(source='ride.park.name', read_only=True) + ride_slug = serializers.CharField(source="ride.slug", read_only=True) + ride_name = serializers.CharField(source="ride.name", read_only=True) + park_slug = serializers.CharField(source="ride.park.slug", read_only=True) + park_name = serializers.CharField(source="ride.park.name", read_only=True) class Meta: model = RidePhoto fields = [ - 'id', - 'image', - 'caption', - 'alt_text', - 'is_primary', - 'is_approved', - 'photo_type', - 'created_at', - 'updated_at', - 'date_taken', - 'uploaded_by_username', - 'file_size', - 'dimensions', - 'ride_slug', - 'ride_name', - 'park_slug', - 'park_name', + "id", + "image", + "caption", + "alt_text", + "is_primary", + "is_approved", + "photo_type", + "created_at", + "updated_at", + "date_taken", + "uploaded_by_username", + "file_size", + "dimensions", + "ride_slug", + "ride_name", + "park_slug", + "park_name", ] read_only_fields = [ - 'id', - 'created_at', - 'updated_at', - 'uploaded_by_username', - 'file_size', - 'dimensions', - 'ride_slug', - 'ride_name', - 'park_slug', - 'park_name', + "id", + "created_at", + "updated_at", + "uploaded_by_username", + "file_size", + "dimensions", + "ride_slug", + "ride_name", + "park_slug", + "park_name", ] @@ -61,11 +62,11 @@ class RidePhotoCreateInputSerializer(serializers.ModelSerializer): class Meta: model = RidePhoto fields = [ - 'image', - 'caption', - 'alt_text', - 'photo_type', - 'is_primary', + "image", + "caption", + "alt_text", + "photo_type", + "is_primary", ] @@ -75,10 +76,10 @@ class RidePhotoUpdateInputSerializer(serializers.ModelSerializer): class Meta: model = RidePhoto fields = [ - 'caption', - 'alt_text', - 'photo_type', - 'is_primary', + "caption", + "alt_text", + "photo_type", + "is_primary", ] @@ -86,19 +87,20 @@ class RidePhotoListOutputSerializer(serializers.ModelSerializer): """Simplified output serializer for ride photo lists.""" uploaded_by_username = serializers.CharField( - source='uploaded_by.username', read_only=True) + source="uploaded_by.username", read_only=True + ) class Meta: model = RidePhoto fields = [ - 'id', - 'image', - 'caption', - 'photo_type', - 'is_primary', - 'is_approved', - 'created_at', - 'uploaded_by_username', + "id", + "image", + "caption", + "photo_type", + "is_primary", + "is_approved", + "created_at", + "uploaded_by_username", ] read_only_fields = fields @@ -107,12 +109,10 @@ class RidePhotoApprovalInputSerializer(serializers.Serializer): """Input serializer for photo approval operations.""" photo_ids = serializers.ListField( - child=serializers.IntegerField(), - help_text="List of photo IDs to approve" + child=serializers.IntegerField(), help_text="List of photo IDs to approve" ) approve = serializers.BooleanField( - default=True, - help_text="Whether to approve (True) or reject (False) the photos" + default=True, help_text="Whether to approve (True) or reject (False) the photos" ) @@ -125,8 +125,7 @@ class RidePhotoStatsOutputSerializer(serializers.Serializer): has_primary = serializers.BooleanField() recent_uploads = serializers.IntegerField() by_type = serializers.DictField( - child=serializers.IntegerField(), - help_text="Photo counts by type" + child=serializers.IntegerField(), help_text="Photo counts by type" ) @@ -135,13 +134,13 @@ class RidePhotoTypeFilterSerializer(serializers.Serializer): photo_type = serializers.ChoiceField( choices=[ - ('exterior', 'Exterior View'), - ('queue', 'Queue Area'), - ('station', 'Station'), - ('onride', 'On-Ride'), - ('construction', 'Construction'), - ('other', 'Other'), + ("exterior", "Exterior View"), + ("queue", "Queue Area"), + ("station", "Station"), + ("onride", "On-Ride"), + ("construction", "Construction"), + ("other", "Other"), ], required=False, - help_text="Filter photos by type" + help_text="Filter photos by type", ) diff --git a/backend/apps/api/v1/serializers/search.py b/backend/apps/api/v1/serializers/search.py index a4907a45..331ab208 100644 --- a/backend/apps/api/v1/serializers/search.py +++ b/backend/apps/api/v1/serializers/search.py @@ -6,11 +6,6 @@ and other search functionality. """ from rest_framework import serializers -from drf_spectacular.utils import ( - extend_schema_serializer, - extend_schema_field, - OpenApiExample, -) # === CORE ENTITY SEARCH SERIALIZERS === diff --git a/backend/apps/api/v1/serializers/services.py b/backend/apps/api/v1/serializers/services.py index c87ef629..3eb46845 100644 --- a/backend/apps/api/v1/serializers/services.py +++ b/backend/apps/api/v1/serializers/services.py @@ -7,9 +7,7 @@ history tracking, moderation, and roadtrip planning. from rest_framework import serializers from drf_spectacular.utils import ( - extend_schema_serializer, extend_schema_field, - OpenApiExample, ) diff --git a/backend/apps/api/v1/serializers_original_backup.py b/backend/apps/api/v1/serializers_original_backup.py index 0e1f11bc..2f796ae5 100644 --- a/backend/apps/api/v1/serializers_original_backup.py +++ b/backend/apps/api/v1/serializers_original_backup.py @@ -13,7 +13,6 @@ from drf_spectacular.utils import ( ) from django.contrib.auth import get_user_model from django.contrib.auth.password_validation import validate_password -from django.core.exceptions import ValidationError as DjangoValidationError from django.utils.crypto import get_random_string from django.utils import timezone from datetime import timedelta @@ -400,7 +399,8 @@ class ParkFilterInputSerializer(serializers.Serializer): # Status filter status = serializers.MultipleChoiceField( - choices=[], required=False # Choices set dynamically + choices=[], + required=False, # Choices set dynamically ) # Location filters @@ -777,7 +777,8 @@ class RideFilterInputSerializer(serializers.Serializer): # Status filter status = serializers.MultipleChoiceField( - choices=[], required=False # Choices set dynamically + choices=[], + required=False, # Choices set dynamically ) # Park filter diff --git a/backend/apps/api/v1/views/auth.py b/backend/apps/api/v1/views/auth.py index d621015d..be41c45d 100644 --- a/backend/apps/api/v1/views/auth.py +++ b/backend/apps/api/v1/views/auth.py @@ -5,18 +5,14 @@ This module contains all authentication-related API endpoints including login, signup, logout, password management, and social authentication. """ -import time from django.contrib.auth import authenticate, login, logout, get_user_model from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import ValidationError -from django.utils import timezone -from django.conf import settings from rest_framework import status from rest_framework.views import APIView from rest_framework.request import Request from rest_framework.response import Response from rest_framework.permissions import AllowAny, IsAuthenticated -from allauth.socialaccount import providers from drf_spectacular.utils import extend_schema, extend_schema_view # Import serializers inside methods to avoid Django initialization issues @@ -274,7 +270,7 @@ class LogoutAPIView(APIView): {"message": "Logout successful"} ) return Response(response_serializer.data) - except Exception as e: + except Exception: return Response( {"error": "Logout failed"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) @@ -385,7 +381,6 @@ class SocialProvidersAPIView(APIView): def get(self, request: Request) -> Response: from django.core.cache import cache - from django.contrib.sites.shortcuts import get_current_site site = get_current_site(request._request) # type: ignore[attr-defined] diff --git a/backend/apps/api/v1/views/health.py b/backend/apps/api/v1/views/health.py index 4f934404..42667733 100644 --- a/backend/apps/api/v1/views/health.py +++ b/backend/apps/api/v1/views/health.py @@ -8,7 +8,6 @@ performance metrics, and database analysis. import time from django.utils import timezone from django.conf import settings -from rest_framework import status from rest_framework.views import APIView from rest_framework.request import Request from rest_framework.response import Response diff --git a/backend/apps/api/v1/views/trending.py b/backend/apps/api/v1/views/trending.py index 08207015..ae0ac177 100644 --- a/backend/apps/api/v1/views/trending.py +++ b/backend/apps/api/v1/views/trending.py @@ -6,8 +6,6 @@ including trending parks, rides, and recently added content. """ from datetime import datetime, date -from django.utils import timezone -from rest_framework import status from rest_framework.views import APIView from rest_framework.request import Request from rest_framework.response import Response diff --git a/backend/apps/api/v1/viewsets_rankings.py b/backend/apps/api/v1/viewsets_rankings.py index b92d06d6..8be2dacf 100644 --- a/backend/apps/api/v1/viewsets_rankings.py +++ b/backend/apps/api/v1/viewsets_rankings.py @@ -2,7 +2,7 @@ API viewsets for the ride ranking system. """ -from django.db.models import Q, Count, Max +from django.db.models import Q from django.utils import timezone from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter @@ -147,9 +147,7 @@ class RideRankingViewSet(ReadOnlyModelViewSet): ranking = self.get_object() history = RankingSnapshot.objects.filter(ride=ranking.ride).order_by( "-snapshot_date" - )[ - :90 - ] # Last 3 months + )[:90] # Last 3 months serializer = self.get_serializer(history, many=True) return Response(serializer.data) diff --git a/backend/apps/context_portal/alembic/versions/2025_06_17_initial_schema.py b/backend/apps/context_portal/alembic/versions/2025_06_17_initial_schema.py index 01b8f1b5..b7cdc14e 100644 --- a/backend/apps/context_portal/alembic/versions/2025_06_17_initial_schema.py +++ b/backend/apps/context_portal/alembic/versions/2025_06_17_initial_schema.py @@ -8,7 +8,6 @@ Create Date: 2025-06-17 15:00:00.000000 from alembic import op import sqlalchemy as sa -import json # revision identifiers, used by Alembic. revision = "20250617" diff --git a/backend/apps/core/decorators/cache_decorators.py b/backend/apps/core/decorators/cache_decorators.py index 818e538d..32bbc3cb 100644 --- a/backend/apps/core/decorators/cache_decorators.py +++ b/backend/apps/core/decorators/cache_decorators.py @@ -100,12 +100,9 @@ def cache_api_response( ) else: logger.debug( - f"Not caching response for view { - view_func.__name__} (status: { - getattr( - response, - 'status_code', - 'unknown')})" + f"Not caching response for view {view_func.__name__} (status: { + getattr(response, 'status_code', 'unknown') + })" ) return response @@ -135,10 +132,7 @@ def cache_queryset_result( cache_key = cache_key_template.format(*args, **kwargs) except (KeyError, IndexError): # Fallback to simpler key generation - cache_key = f"{cache_key_template}:{ - hash( - str(args) + - str(kwargs))}" + cache_key = f"{cache_key_template}:{hash(str(args) + str(kwargs))}" cache_service = EnhancedCacheService() cached_result = getattr(cache_service, cache_backend + "_cache").get( @@ -146,10 +140,7 @@ def cache_queryset_result( ) if cached_result is not None: - logger.debug( - f"Cache hit for queryset operation: { - func.__name__}" - ) + logger.debug(f"Cache hit for queryset operation: {func.__name__}") return cached_result # Execute function and cache result @@ -314,9 +305,9 @@ def smart_cache( "kwargs": json.dumps(kwargs, sort_keys=True, default=str), } key_string = json.dumps(key_data, sort_keys=True) - cache_key = f"smart_cache:{ - hashlib.md5( - key_string.encode()).hexdigest()}" + cache_key = ( + f"smart_cache:{hashlib.md5(key_string.encode()).hexdigest()}" + ) # Try to get from cache cache_service = EnhancedCacheService() diff --git a/backend/apps/core/health_checks/custom_checks.py b/backend/apps/core/health_checks/custom_checks.py index 8ae74607..f3f72a1c 100644 --- a/backend/apps/core/health_checks/custom_checks.py +++ b/backend/apps/core/health_checks/custom_checks.py @@ -57,13 +57,11 @@ class CacheHealthCheck(BaseHealthCheckBackend): memory_usage_percent = (used_memory / max_memory) * 100 if memory_usage_percent > 90: self.add_error( - f"Redis memory usage critical: { - memory_usage_percent:.1f}%" + f"Redis memory usage critical: {memory_usage_percent:.1f}%" ) elif memory_usage_percent > 80: logger.warning( - f"Redis memory usage high: { - memory_usage_percent:.1f}%" + f"Redis memory usage high: {memory_usage_percent:.1f}%" ) except ImportError: @@ -190,10 +188,7 @@ class ApplicationHealthCheck(BaseHealthCheckBackend): import os if not os.path.exists(settings.MEDIA_ROOT): - self.add_error( - f"Media directory does not exist: { - settings.MEDIA_ROOT}" - ) + self.add_error(f"Media directory does not exist: {settings.MEDIA_ROOT}") if not os.path.exists(settings.STATIC_ROOT) and not settings.DEBUG: self.add_error( @@ -305,8 +300,7 @@ class DiskSpaceHealthCheck(BaseHealthCheckBackend): ) elif media_free_percent < 20: logger.warning( - f"Low disk space: { - media_free_percent:.1f}% free in media directory" + f"Low disk space: {media_free_percent:.1f}% free in media directory" ) if logs_free_percent < 10: @@ -316,8 +310,7 @@ class DiskSpaceHealthCheck(BaseHealthCheckBackend): ) elif logs_free_percent < 20: logger.warning( - f"Low disk space: { - logs_free_percent:.1f}% free in logs directory" + f"Low disk space: {logs_free_percent:.1f}% free in logs directory" ) except Exception as e: diff --git a/backend/apps/core/management/commands/rundev.py b/backend/apps/core/management/commands/rundev.py index 6871d656..23aaa783 100644 --- a/backend/apps/core/management/commands/rundev.py +++ b/backend/apps/core/management/commands/rundev.py @@ -5,8 +5,6 @@ This command automatically sets up the development environment and starts the server, replacing the need for the dev_server.sh script. """ -import subprocess -import sys from django.core.management.base import BaseCommand from django.core.management import execute_from_command_line @@ -62,7 +60,7 @@ class Command(BaseCommand): self.stdout.write("") self.stdout.write( self.style.SUCCESS( - f'🌟 Starting Django development server on http://{options["host"]}:{options["port"]}' + f"🌟 Starting Django development server on http://{options['host']}:{options['port']}" ) ) self.stdout.write("Press Ctrl+C to stop the server") @@ -74,12 +72,12 @@ class Command(BaseCommand): [ "manage.py", "runserver_plus", - f'{options["host"]}:{options["port"]}', + f"{options['host']}:{options['port']}", ] ) else: execute_from_command_line( - ["manage.py", "runserver", f'{options["host"]}:{options["port"]}'] + ["manage.py", "runserver", f"{options['host']}:{options['port']}"] ) except KeyboardInterrupt: self.stdout.write("") diff --git a/backend/apps/core/management/commands/setup_dev.py b/backend/apps/core/management/commands/setup_dev.py index 5e3561b5..4b3b7c4c 100644 --- a/backend/apps/core/management/commands/setup_dev.py +++ b/backend/apps/core/management/commands/setup_dev.py @@ -5,13 +5,11 @@ This command performs all the setup tasks that the dev_server.sh script does, allowing the project to run without requiring the shell script. """ -import os import subprocess import sys from pathlib import Path from django.core.management.base import BaseCommand -from django.core.management import execute_from_command_line from django.conf import settings diff --git a/backend/apps/core/middleware/analytics.py b/backend/apps/core/middleware/analytics.py index d480d636..72b7af36 100644 --- a/backend/apps/core/middleware/analytics.py +++ b/backend/apps/core/middleware/analytics.py @@ -5,10 +5,6 @@ Analytics and tracking middleware for Django application. import pghistory from django.contrib.auth.models import AnonymousUser from django.core.handlers.wsgi import WSGIRequest -from django.utils.deprecation import MiddlewareMixin -from django.contrib.contenttypes.models import ContentType -from django.views.generic.detail import DetailView -from apps.core.analytics import PageView class RequestContextProvider(pghistory.context): diff --git a/backend/apps/core/middleware/performance_middleware.py b/backend/apps/core/middleware/performance_middleware.py index 09d8bccd..f2ac5a0f 100644 --- a/backend/apps/core/middleware/performance_middleware.py +++ b/backend/apps/core/middleware/performance_middleware.py @@ -151,12 +151,10 @@ class PerformanceMiddleware(MiddlewareMixin): } performance_logger.error( - f"Request exception: { - request.method} { - request.path} - " - f"{ - duration:.3f}s, {total_queries} queries, { - type(exception).__name__}: {exception}", + f"Request exception: {request.method} {request.path} - " + f"{duration:.3f}s, {total_queries} queries, {type(exception).__name__}: { + exception + }", extra=performance_data, ) @@ -216,10 +214,10 @@ class QueryCountMiddleware(MiddlewareMixin): if request_query_count > self.query_limit: logger.warning( - f"Excessive query count: { - request.path} executed {request_query_count} queries " - f"(limit: { - self.query_limit})", + f"Excessive query count: {request.path} executed { + request_query_count + } queries " + f"(limit: {self.query_limit})", extra={ "path": request.path, "method": request.method, @@ -308,9 +306,7 @@ class CachePerformanceMiddleware(MiddlewareMixin): ) else: logger.debug( - f"Cache performance for { - request.path}: { - hit_rate:.1f}% hit rate", + f"Cache performance for {request.path}: {hit_rate:.1f}% hit rate", extra=cache_data, ) diff --git a/backend/apps/core/middleware/view_tracking.py b/backend/apps/core/middleware/view_tracking.py index 7a4881f1..041aac37 100644 --- a/backend/apps/core/middleware/view_tracking.py +++ b/backend/apps/core/middleware/view_tracking.py @@ -8,14 +8,13 @@ analytics for the trending algorithm. import logging import re -from datetime import datetime, timedelta +from datetime import timedelta from typing import Optional, Union from django.http import HttpRequest, HttpResponse from django.utils import timezone from django.contrib.contenttypes.models import ContentType from django.core.cache import cache from django.conf import settings -from django.db import models from apps.core.analytics import PageView from apps.parks.models import Park @@ -68,7 +67,6 @@ class ViewTrackingMiddleware: and 200 <= response.status_code < 300 and not self._should_skip_tracking(request) ): - try: self._track_view_if_applicable(request) except Exception as e: diff --git a/backend/apps/core/migrations/0001_initial.py b/backend/apps/core/migrations/0001_initial.py index 8ac879ad..9593ad64 100644 --- a/backend/apps/core/migrations/0001_initial.py +++ b/backend/apps/core/migrations/0001_initial.py @@ -5,7 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/backend/apps/core/migrations/0002_historicalslug_pageview.py b/backend/apps/core/migrations/0002_historicalslug_pageview.py index 7e882ddd..56bc2378 100644 --- a/backend/apps/core/migrations/0002_historicalslug_pageview.py +++ b/backend/apps/core/migrations/0002_historicalslug_pageview.py @@ -6,7 +6,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("contenttypes", "0002_remove_content_type_name"), ("core", "0001_initial"), diff --git a/backend/apps/core/migrations/0003_pageviewevent_slughistoryevent_and_more.py b/backend/apps/core/migrations/0003_pageviewevent_slughistoryevent_and_more.py index f7820478..261ff403 100644 --- a/backend/apps/core/migrations/0003_pageviewevent_slughistoryevent_and_more.py +++ b/backend/apps/core/migrations/0003_pageviewevent_slughistoryevent_and_more.py @@ -7,7 +7,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("contenttypes", "0002_remove_content_type_name"), ("core", "0002_historicalslug_pageview"), diff --git a/backend/apps/core/services/enhanced_cache_service.py b/backend/apps/core/services/enhanced_cache_service.py index 874e4bac..c998346b 100644 --- a/backend/apps/core/services/enhanced_cache_service.py +++ b/backend/apps/core/services/enhanced_cache_service.py @@ -50,8 +50,7 @@ class EnhancedCacheService: # Log cache miss and function execution time logger.info( - f"Cache miss for key '{cache_key}', executed in { - duration:.3f}s", + f"Cache miss for key '{cache_key}', executed in {duration:.3f}s", extra={"cache_key": cache_key, "execution_time": duration}, ) @@ -96,11 +95,9 @@ class EnhancedCacheService: ): """Cache geographic data with spatial keys""" # Generate spatial cache key based on bounds and zoom level - cache_key = f"geo:{ - bounds.min_lat}:{ - bounds.min_lng}:{ - bounds.max_lat}:{ - bounds.max_lng}:z{zoom_level}" + cache_key = f"geo:{bounds.min_lat}:{bounds.min_lng}:{bounds.max_lat}:{ + bounds.max_lng + }:z{zoom_level}" self.default_cache.set(cache_key, data, timeout) logger.debug(f"Cached geographic data for bounds {bounds}") @@ -108,11 +105,9 @@ class EnhancedCacheService: self, bounds: "GeoBounds", zoom_level: int ) -> Optional[Any]: """Retrieve cached geographic data""" - cache_key = f"geo:{ - bounds.min_lat}:{ - bounds.min_lng}:{ - bounds.max_lat}:{ - bounds.max_lng}:z{zoom_level}" + cache_key = f"geo:{bounds.min_lat}:{bounds.min_lng}:{bounds.max_lat}:{ + bounds.max_lng + }:z{zoom_level}" return self.default_cache.get(cache_key) # Cache invalidation utilities @@ -206,10 +201,7 @@ def cache_api_response(timeout=1800, vary_on=None, key_prefix=""): response = view_func(self, request, *args, **kwargs) if hasattr(response, "status_code") and response.status_code == 200: cache_service.api_cache.set(cache_key, response, timeout) - logger.debug( - f"Cached API response for view { - view_func.__name__}" - ) + logger.debug(f"Cached API response for view {view_func.__name__}") return response @@ -273,10 +265,7 @@ class CacheWarmer: try: self.cache_service.warm_cache(**operation) except Exception as e: - logger.error( - f"Error warming cache for { - operation['cache_key']}: {e}" - ) + logger.error(f"Error warming cache for {operation['cache_key']}: {e}") # Cache statistics and monitoring diff --git a/backend/apps/core/services/location_adapters.py b/backend/apps/core/services/location_adapters.py index 14db0273..9922f2a8 100644 --- a/backend/apps/core/services/location_adapters.py +++ b/backend/apps/core/services/location_adapters.py @@ -2,7 +2,6 @@ Location adapters for converting between domain-specific models and UnifiedLocation. """ -from django.db import models from typing import List, Optional from django.db.models import QuerySet from django.urls import reverse diff --git a/backend/apps/core/services/location_search.py b/backend/apps/core/services/location_search.py index e4801797..59152bc7 100644 --- a/backend/apps/core/services/location_search.py +++ b/backend/apps/core/services/location_search.py @@ -447,13 +447,10 @@ class LocationSearchService: suggestions.append( { "type": "city", - "name": f"{ - city_data['city']}, { - city_data['state']}", - "address": f"{ - city_data['city']}, { - city_data['state']}, { - city_data['country']}", + "name": f"{city_data['city']}, {city_data['state']}", + "address": f"{city_data['city']}, {city_data['state']}, { + city_data['country'] + }", "coordinates": None, } ) diff --git a/backend/apps/core/services/map_cache_service.py b/backend/apps/core/services/map_cache_service.py index 5bebd9c8..34a7e799 100644 --- a/backend/apps/core/services/map_cache_service.py +++ b/backend/apps/core/services/map_cache_service.py @@ -289,11 +289,7 @@ class MapCacheService: """Record query performance metrics for analysis.""" try: # 5-minute buckets - stats_key = f"{ - self.STATS_PREFIX}:performance:{ - int( - time.time() // - 300)}" + stats_key = f"{self.STATS_PREFIX}:performance:{int(time.time() // 300)}" current_stats = cache.get( stats_key, diff --git a/backend/apps/core/services/media_service.py b/backend/apps/core/services/media_service.py index dad947da..447cf082 100644 --- a/backend/apps/core/services/media_service.py +++ b/backend/apps/core/services/media_service.py @@ -21,10 +21,7 @@ class MediaService: @staticmethod def generate_upload_path( - domain: str, - identifier: str, - filename: str, - subdirectory: Optional[str] = None + domain: str, identifier: str, filename: str, subdirectory: Optional[str] = None ) -> str: """ Generate standardized upload path for media files. @@ -86,16 +83,26 @@ class MediaService: """ try: # Check file size - max_size = getattr(settings, 'MAX_PHOTO_SIZE', - 10 * 1024 * 1024) # 10MB default + max_size = getattr( + settings, "MAX_PHOTO_SIZE", 10 * 1024 * 1024 + ) # 10MB default if image_file.size > max_size: - return False, f"File size too large. Maximum size is {max_size // (1024 * 1024)}MB" + return ( + False, + f"File size too large. Maximum size is {max_size // (1024 * 1024)}MB", + ) # Check file type - allowed_types = getattr(settings, 'ALLOWED_PHOTO_TYPES', [ - 'image/jpeg', 'image/png', 'image/webp']) + allowed_types = getattr( + settings, + "ALLOWED_PHOTO_TYPES", + ["image/jpeg", "image/png", "image/webp"], + ) if image_file.content_type not in allowed_types: - return False, f"File type not allowed. Allowed types: {', '.join(allowed_types)}" + return ( + False, + f"File type not allowed. Allowed types: {', '.join(allowed_types)}", + ) # Try to open with PIL to validate it's a real image with Image.open(image_file) as img: @@ -111,7 +118,7 @@ class MediaService: image_file: UploadedFile, max_width: int = 1920, max_height: int = 1080, - quality: int = 85 + quality: int = 85, ) -> UploadedFile: """ Process and optimize image file. @@ -128,8 +135,8 @@ class MediaService: try: with Image.open(image_file) as img: # Convert to RGB if necessary - if img.mode in ('RGBA', 'LA', 'P'): - img = img.convert('RGB') + if img.mode in ("RGBA", "LA", "P"): + img = img.convert("RGB") # Resize if necessary if img.width > max_width or img.height > max_height: @@ -140,16 +147,16 @@ class MediaService: from django.core.files.uploadedfile import InMemoryUploadedFile output = BytesIO() - img.save(output, format='JPEG', quality=quality, optimize=True) + img.save(output, format="JPEG", quality=quality, optimize=True) output.seek(0) return InMemoryUploadedFile( output, - 'ImageField', + "ImageField", f"{os.path.splitext(image_file.name)[0]}.jpg", - 'image/jpeg', + "image/jpeg", output.getbuffer().nbytes, - None + None, ) except Exception as e: @@ -168,6 +175,7 @@ class MediaService: Default caption string """ from django.utils import timezone + current_time = timezone.now() return f"Uploaded by {username} on {current_time.strftime('%B %d, %Y at %I:%M %p')}" @@ -185,7 +193,7 @@ class MediaService: "total_files": 0, "total_size_bytes": 0, "storage_backend": "default", - "available_space": "unknown" + "available_space": "unknown", } except Exception as e: logger.error(f"Failed to get storage stats: {str(e)}") diff --git a/backend/apps/core/services/performance_monitoring.py b/backend/apps/core/services/performance_monitoring.py index e07a1524..6c5500dd 100644 --- a/backend/apps/core/services/performance_monitoring.py +++ b/backend/apps/core/services/performance_monitoring.py @@ -57,16 +57,16 @@ def monitor_performance(operation_name: str, **tags): ) logger.log( log_level, - f"Performance: {operation_name} completed in { - duration:.3f}s with {total_queries} queries", + f"Performance: {operation_name} completed in {duration:.3f}s with { + total_queries + } queries", extra=performance_context, ) # Log slow operations with additional detail if duration > 2.0: logger.warning( - f"Slow operation detected: {operation_name} took { - duration:.3f}s", + f"Slow operation detected: {operation_name} took {duration:.3f}s", extra={ "slow_operation": True, "threshold_exceeded": "duration", @@ -246,9 +246,9 @@ class PerformanceProfiler: log_level = logging.WARNING if total_duration > 1.0 else logging.INFO logger.log( log_level, - f"Profiling complete: { - self.name} took { - total_duration:.3f}s with {total_queries} queries", + f"Profiling complete: {self.name} took {total_duration:.3f}s with { + total_queries + } queries", extra=report, ) diff --git a/backend/apps/core/services/trending_service.py b/backend/apps/core/services/trending_service.py index 20bfa149..fdd0eadb 100644 --- a/backend/apps/core/services/trending_service.py +++ b/backend/apps/core/services/trending_service.py @@ -395,7 +395,9 @@ class TrendingService: """Calculate popularity score based on total view count.""" try: total_views = PageView.get_total_views_count( - content_type, object_id, hours=168 # Last 7 days + content_type, + object_id, + hours=168, # Last 7 days ) # Normalize views to 0-1 scale diff --git a/backend/apps/core/utils/query_optimization.py b/backend/apps/core/utils/query_optimization.py index 292c7dbc..5e180e1f 100644 --- a/backend/apps/core/utils/query_optimization.py +++ b/backend/apps/core/utils/query_optimization.py @@ -323,10 +323,7 @@ class IndexAnalyzer: common_filter_fields = ["slug", "name", "created_at", "updated_at"] for field in opts.fields: if field.name in common_filter_fields and not field.db_index: - suggestions.append( - f"Consider adding db_index=True to { - field.name}" - ) + suggestions.append(f"Consider adding db_index=True to {field.name}") return suggestions @@ -419,9 +416,9 @@ def monitor_db_performance(operation_name: str): if duration > 1.0 or total_queries > 15 or slow_queries: logger.warning( f"Performance issue in {operation_name}: " - f"{ - duration:.3f}s, {total_queries} queries, { - len(slow_queries)} slow", + f"{duration:.3f}s, {total_queries} queries, { + len(slow_queries) + } slow", extra=performance_data, ) else: diff --git a/backend/apps/core/views/map_views.py b/backend/apps/core/views/map_views.py index b46dffc2..4385da2c 100644 --- a/backend/apps/core/views/map_views.py +++ b/backend/apps/core/views/map_views.py @@ -41,11 +41,7 @@ class MapAPIView(View): response["Access-Control-Allow-Headers"] = "Content-Type, Authorization" # Add performance headers - response["X-Response-Time"] = ( - f"{(time.time() - - start_time) * - 1000:.2f}ms" - ) + response["X-Response-Time"] = f"{(time.time() - start_time) * 1000:.2f}ms" # Add compression hint for large responses if hasattr(response, "content") and len(response.content) > 1024: @@ -55,9 +51,7 @@ class MapAPIView(View): except Exception as e: logger.error( - f"API error in { - request.path}: { - str(e)}", + f"API error in {request.path}: {str(e)}", exc_info=True, ) return self._error_response("An internal server error occurred", status=500) @@ -412,7 +406,8 @@ class MapLocationDetailView(MapAPIView): if location_type not in valid_types: return self._error_response( f"Invalid location type: {location_type}. Valid types: { - ', '.join(valid_types)}", + ', '.join(valid_types) + }", 400, error_code="INVALID_LOCATION_TYPE", ) @@ -450,8 +445,7 @@ class MapLocationDetailView(MapAPIView): return self._error_response(str(e), 400, error_code="INVALID_PARAMETER") except Exception as e: logger.error( - f"Error in MapLocationDetailView: { - str(e)}", + f"Error in MapLocationDetailView: {str(e)}", exc_info=True, ) return self._error_response( @@ -606,8 +600,7 @@ class MapBoundsView(MapAPIView): return self._error_response(str(e), 400) except Exception as e: return self._error_response( - f"Internal server error: { - str(e)}", + f"Internal server error: {str(e)}", 500, ) @@ -628,8 +621,7 @@ class MapStatsView(MapAPIView): except Exception as e: return self._error_response( - f"Internal server error: { - str(e)}", + f"Internal server error: {str(e)}", 500, ) @@ -657,8 +649,7 @@ class MapCacheView(MapAPIView): except Exception as e: return self._error_response( - f"Internal server error: { - str(e)}", + f"Internal server error: {str(e)}", 500, ) @@ -693,7 +684,6 @@ class MapCacheView(MapAPIView): return self._error_response(f"Invalid request data: {str(e)}", 400) except Exception as e: return self._error_response( - f"Internal server error: { - str(e)}", + f"Internal server error: {str(e)}", 500, ) diff --git a/backend/apps/core/views/maps.py b/backend/apps/core/views/maps.py index ca059164..d3f7781d 100644 --- a/backend/apps/core/views/maps.py +++ b/backend/apps/core/views/maps.py @@ -143,8 +143,7 @@ class NearbyLocationsView(MapViewMixin, TemplateView): context.update( { - "page_title": f"Locations Near { - center_lat:.4f}, { + "page_title": f"Locations Near {center_lat:.4f}, { center_lng:.4f}", "map_type": "nearby", "center_coordinates": { diff --git a/backend/apps/email_service/backends.py b/backend/apps/email_service/backends.py index 1731d01c..5a4786f2 100644 --- a/backend/apps/email_service/backends.py +++ b/backend/apps/email_service/backends.py @@ -52,10 +52,7 @@ class ForwardEmailBackend(BaseEmailBackend): try: config = EmailConfiguration.objects.get(site=site) except EmailConfiguration.DoesNotExist: - raise ValueError( - f"Email configuration not found for site: { - site.domain}" - ) + raise ValueError(f"Email configuration not found for site: {site.domain}") # Get the from email, falling back to site's default if not provided if email_message.from_email: diff --git a/backend/apps/email_service/management/commands/test_email_flows.py b/backend/apps/email_service/management/commands/test_email_flows.py index fada281c..841194fe 100644 --- a/backend/apps/email_service/management/commands/test_email_flows.py +++ b/backend/apps/email_service/management/commands/test_email_flows.py @@ -86,17 +86,14 @@ class Command(BaseCommand): else: self.stdout.write( self.style.WARNING( - f"Registration returned status { - response.status_code}: { - response.content.decode()}\n" + f"Registration returned status {response.status_code}: { + response.content.decode() + }\n" ) ) except Exception as e: self.stdout.write( - self.style.ERROR( - f"Registration email test failed: { - str(e)}\n" - ) + self.style.ERROR(f"Registration email test failed: {str(e)}\n") ) def test_password_change(self, user): @@ -120,17 +117,14 @@ class Command(BaseCommand): else: self.stdout.write( self.style.WARNING( - f"Password change returned status { - response.status_code}: { - response.content.decode()}\n" + f"Password change returned status {response.status_code}: { + response.content.decode() + }\n" ) ) except Exception as e: self.stdout.write( - self.style.ERROR( - f"Password change email test failed: { - str(e)}\n" - ) + self.style.ERROR(f"Password change email test failed: {str(e)}\n") ) def test_email_change(self, user): @@ -151,17 +145,14 @@ class Command(BaseCommand): else: self.stdout.write( self.style.WARNING( - f"Email change returned status { - response.status_code}: { - response.content.decode()}\n" + f"Email change returned status {response.status_code}: { + response.content.decode() + }\n" ) ) except Exception as e: self.stdout.write( - self.style.ERROR( - f"Email change verification test failed: { - str(e)}\n" - ) + self.style.ERROR(f"Email change verification test failed: {str(e)}\n") ) def test_password_reset(self, user): @@ -182,15 +173,12 @@ class Command(BaseCommand): else: self.stdout.write( self.style.WARNING( - f"Password reset returned status { - response.status_code}: { - response.content.decode()}\n" + f"Password reset returned status {response.status_code}: { + response.content.decode() + }\n" ) ) except Exception as e: self.stdout.write( - self.style.ERROR( - f"Password reset email test failed: { - str(e)}\n" - ) + self.style.ERROR(f"Password reset email test failed: {str(e)}\n") ) diff --git a/backend/apps/email_service/management/commands/test_email_service.py b/backend/apps/email_service/management/commands/test_email_service.py index f56ac3f0..a675d913 100644 --- a/backend/apps/email_service/management/commands/test_email_service.py +++ b/backend/apps/email_service/management/commands/test_email_service.py @@ -84,7 +84,7 @@ class Command(BaseCommand): self.stdout.write(self.style.SUCCESS("Using configuration:")) self.stdout.write(f" From: {from_email}") self.stdout.write(f" To: {to_email}") - self.stdout.write(f' API Key: {"*" * len(api_key)}') + self.stdout.write(f" API Key: {'*' * len(api_key)}") self.stdout.write(f" Site: {site.domain}") try: @@ -132,10 +132,7 @@ class Command(BaseCommand): return config except Exception as e: self.stdout.write( - self.style.ERROR( - f"✗ Site configuration failed: { - str(e)}" - ) + self.style.ERROR(f"✗ Site configuration failed: {str(e)}") ) raise @@ -164,8 +161,8 @@ class Command(BaseCommand): self.stdout.write( self.style.ERROR( f"✗ API endpoint test failed with status { - response.status_code}: { - response.text}" + response.status_code + }: {response.text}" ) ) raise Exception(f"API test failed: {response.text}") @@ -178,12 +175,7 @@ class Command(BaseCommand): ) raise Exception("Could not connect to Django server") except Exception as e: - self.stdout.write( - self.style.ERROR( - f"✗ API endpoint test failed: { - str(e)}" - ) - ) + self.stdout.write(self.style.ERROR(f"✗ API endpoint test failed: {str(e)}")) raise def test_email_backend(self, to_email, site): @@ -196,8 +188,7 @@ class Command(BaseCommand): # Debug output self.stdout.write( - f" Debug: Using from_email: { - site.email_config.default_from_email}" + f" Debug: Using from_email: {site.email_config.default_from_email}" ) self.stdout.write(f" Debug: Using to_email: {to_email}") @@ -212,10 +203,7 @@ class Command(BaseCommand): self.stdout.write(self.style.SUCCESS("✓ Email backend test successful")) except Exception as e: self.stdout.write( - self.style.ERROR( - f"✗ Email backend test failed: { - str(e)}" - ) + self.style.ERROR(f"✗ Email backend test failed: {str(e)}") ) raise @@ -236,9 +224,6 @@ class Command(BaseCommand): return response except Exception as e: self.stdout.write( - self.style.ERROR( - f"✗ Direct EmailService test failed: { - str(e)}" - ) + self.style.ERROR(f"✗ Direct EmailService test failed: {str(e)}") ) raise diff --git a/backend/apps/email_service/migrations/0001_initial.py b/backend/apps/email_service/migrations/0001_initial.py index 00c7e8e7..bd0dd96f 100644 --- a/backend/apps/email_service/migrations/0001_initial.py +++ b/backend/apps/email_service/migrations/0001_initial.py @@ -7,7 +7,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/backend/apps/email_service/migrations/0002_remove_emailconfiguration_insert_insert_and_more.py b/backend/apps/email_service/migrations/0002_remove_emailconfiguration_insert_insert_and_more.py index d89500d4..81f5cfa2 100644 --- a/backend/apps/email_service/migrations/0002_remove_emailconfiguration_insert_insert_and_more.py +++ b/backend/apps/email_service/migrations/0002_remove_emailconfiguration_insert_insert_and_more.py @@ -6,7 +6,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("email_service", "0001_initial"), ] diff --git a/backend/apps/email_service/services.py b/backend/apps/email_service/services.py index 7ab01cf5..f8e98e00 100644 --- a/backend/apps/email_service/services.py +++ b/backend/apps/email_service/services.py @@ -34,9 +34,7 @@ class EmailService: # Use provided from_email or construct from config if not from_email: - from_email = f"{ - email_config.from_name} <{ - email_config.from_email}>" + from_email = f"{email_config.from_name} <{email_config.from_email}>" elif "<" not in from_email: # If from_email is provided but doesn't include a name, add the # configured name @@ -101,8 +99,9 @@ class EmailService: if response.status_code != 200: error_message = response.text if response.text else "Unknown error" raise Exception( - f"Failed to send email (Status { - response.status_code}): {error_message}" + f"Failed to send email (Status {response.status_code}): { + error_message + }" ) return response.json() diff --git a/backend/apps/moderation/migrations/0001_initial.py b/backend/apps/moderation/migrations/0001_initial.py index adb72d72..0553be8b 100644 --- a/backend/apps/moderation/migrations/0001_initial.py +++ b/backend/apps/moderation/migrations/0001_initial.py @@ -8,7 +8,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/backend/apps/moderation/migrations/0002_remove_editsubmission_insert_insert_and_more.py b/backend/apps/moderation/migrations/0002_remove_editsubmission_insert_insert_and_more.py index 97a74833..dfc0b43d 100644 --- a/backend/apps/moderation/migrations/0002_remove_editsubmission_insert_insert_and_more.py +++ b/backend/apps/moderation/migrations/0002_remove_editsubmission_insert_insert_and_more.py @@ -6,7 +6,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("moderation", "0001_initial"), ] diff --git a/backend/apps/moderation/models.py b/backend/apps/moderation/models.py index 42f646ff..894e193e 100644 --- a/backend/apps/moderation/models.py +++ b/backend/apps/moderation/models.py @@ -171,10 +171,9 @@ class EditSubmission(TrackedModel): self.status = "REJECTED" self.handled_by = user # type: ignore self.handled_at = timezone.now() - self.notes = f"A { - model_class.__name__} with the name '{ - prepared_data['name']}' already exists (ID: { - existing_obj.pk})" + self.notes = f"A {model_class.__name__} with the name '{ + prepared_data['name'] + }' already exists (ID: {existing_obj.pk})" self.save() raise ValueError(self.notes) @@ -279,9 +278,7 @@ class PhotoSubmission(TrackedModel): ] def __str__(self) -> str: - return f"Photo submission by { - self.user.username} for { - self.content_object}" + return f"Photo submission by {self.user.username} for {self.content_object}" def approve(self, moderator: UserType, notes: str = "") -> None: """Approve the photo submission""" diff --git a/backend/apps/parks/management/commands/create_sample_data.py b/backend/apps/parks/management/commands/create_sample_data.py index 65aa6e72..ab10b0ad 100644 --- a/backend/apps/parks/management/commands/create_sample_data.py +++ b/backend/apps/parks/management/commands/create_sample_data.py @@ -106,9 +106,7 @@ class Command(BaseCommand): ) self.created_companies[company.slug] = company self.stdout.write( - f' { - "Created" if created else "Found"} park company: { - company.name}' + f" {'Created' if created else 'Found'} park company: {company.name}" ) # Ride manufacturers and designers (using rides.models.Company) @@ -201,9 +199,7 @@ class Command(BaseCommand): ) self.created_companies[company.slug] = company self.stdout.write( - f' { - "Created" if created else "Found"} ride company: { - company.name}' + f" {'Created' if created else 'Found'} ride company: {company.name}" ) def create_parks(self): diff --git a/backend/apps/parks/management/commands/seed_initial_data.py b/backend/apps/parks/management/commands/seed_initial_data.py index 2ee36efb..d96aa255 100644 --- a/backend/apps/parks/management/commands/seed_initial_data.py +++ b/backend/apps/parks/management/commands/seed_initial_data.py @@ -53,7 +53,7 @@ class Command(BaseCommand): ) companies[operator.name] = operator self.stdout.write( - f'{"Created" if created else "Found"} company: {operator.name}' + f"{'Created' if created else 'Found'} company: {operator.name}" ) # Create parks with their locations @@ -301,7 +301,7 @@ class Command(BaseCommand): "owner": company, }, ) - self.stdout.write(f'{"Created" if created else "Found"} park: {park.name}') + self.stdout.write(f"{'Created' if created else 'Found'} park: {park.name}") # Create location for park if created: @@ -328,7 +328,7 @@ class Command(BaseCommand): defaults={"description": area_data["description"]}, ) self.stdout.write( - f'{"Created" if created else "Found"} area: {area.name} in {park.name}' + f"{'Created' if created else 'Found'} area: {area.name} in {park.name}" ) self.stdout.write(self.style.SUCCESS("Successfully seeded initial park data")) diff --git a/backend/apps/parks/management/commands/seed_sample_data.py b/backend/apps/parks/management/commands/seed_sample_data.py index e6be2536..66aa94c7 100644 --- a/backend/apps/parks/management/commands/seed_sample_data.py +++ b/backend/apps/parks/management/commands/seed_sample_data.py @@ -121,8 +121,7 @@ class Command(BaseCommand): except Exception as e: self.logger.error( - f"Error during data cleanup: { - str(e)}", + f"Error during data cleanup: {str(e)}", exc_info=True, ) self.stdout.write( @@ -205,7 +204,7 @@ class Command(BaseCommand): if missing_tables: self.stdout.write( self.style.WARNING( - f'Missing tables for models: {", ".join(missing_tables)}' + f"Missing tables for models: {', '.join(missing_tables)}" ) ) return False @@ -353,13 +352,13 @@ class Command(BaseCommand): ) self.park_companies[data["name"]] = company self.stdout.write( - f' { - "Created" if created else "Found"} park company: { - company.name}' + f" {'Created' if created else 'Found'} park company: { + company.name + }" ) except Exception as e: self.logger.error( - f'Error creating park company {data["name"]}: {str(e)}' + f"Error creating park company {data['name']}: {str(e)}" ) raise @@ -378,13 +377,13 @@ class Command(BaseCommand): ) self.ride_companies[data["name"]] = company self.stdout.write( - f' { - "Created" if created else "Found"} ride company: { - company.name}' + f" {'Created' if created else 'Found'} ride company: { + company.name + }" ) except Exception as e: self.logger.error( - f'Error creating ride company {data["name"]}: {str(e)}' + f"Error creating ride company {data['name']}: {str(e)}" ) raise @@ -532,9 +531,7 @@ class Command(BaseCommand): ) self.parks[park_data["name"]] = park self.stdout.write( - f' { - "Created" if created else "Found"} park: { - park.name}' + f" {'Created' if created else 'Found'} park: {park.name}" ) # Create location for park @@ -556,15 +553,15 @@ class Command(BaseCommand): park_location.save() except Exception as e: self.logger.error( - f'Error creating location for park { - park_data["name"]}: { - str(e)}' + f"Error creating location for park { + park_data['name'] + }: {str(e)}" ) raise except Exception as e: self.logger.error( - f'Error creating park {park_data["name"]}: {str(e)}' + f"Error creating park {park_data['name']}: {str(e)}" ) raise @@ -631,15 +628,13 @@ class Command(BaseCommand): ) self.ride_models[model_data["name"]] = model self.stdout.write( - f' { - "Created" if created else "Found"} ride model: { - model.name}' + f" {'Created' if created else 'Found'} ride model: { + model.name + }" ) except Exception as e: self.logger.error( - f'Error creating ride model { - model_data["name"]}: { - str(e)}' + f"Error creating ride model {model_data['name']}: {str(e)}" ) raise @@ -860,9 +855,7 @@ class Command(BaseCommand): ) self.rides[ride_data["name"]] = ride self.stdout.write( - f' { - "Created" if created else "Found"} ride: { - ride.name}' + f" {'Created' if created else 'Found'} ride: {ride.name}" ) # Create roller coaster stats if provided @@ -872,15 +865,15 @@ class Command(BaseCommand): RollerCoasterStats.objects.create(ride=ride, **stats_data) except Exception as e: self.logger.error( - f'Error creating stats for ride { - ride_data["name"]}: { - str(e)}' + f"Error creating stats for ride {ride_data['name']}: { + str(e) + }" ) raise except Exception as e: self.logger.error( - f'Error creating ride {ride_data["name"]}: {str(e)}' + f"Error creating ride {ride_data['name']}: {str(e)}" ) raise @@ -1013,16 +1006,13 @@ class Command(BaseCommand): }, ) self.stdout.write( - f' { - "Created" if created else "Found"} area: { - area.name} in { - park.name}' + f" {'Created' if created else 'Found'} area: { + area.name + } in {park.name}" ) except Exception as e: self.logger.error( - f'Error creating areas for park { - area_group["park"]}: { - str(e)}' + f"Error creating areas for park {area_group['park']}: {str(e)}" ) raise @@ -1095,15 +1085,15 @@ class Command(BaseCommand): }, ) self.stdout.write( - f' { - "Created" if created else "Found"} park review: { - review.title}' + f" {'Created' if created else 'Found'} park review: { + review.title + }" ) except Exception as e: self.logger.error( - f'Error creating park review for { - review_data["park"]}: { - str(e)}' + f"Error creating park review for {review_data['park']}: { + str(e) + }" ) raise @@ -1154,15 +1144,15 @@ class Command(BaseCommand): }, ) self.stdout.write( - f' { - "Created" if created else "Found"} ride review: { - review.title}' + f" {'Created' if created else 'Found'} ride review: { + review.title + }" ) except Exception as e: self.logger.error( - f'Error creating ride review for { - review_data["ride"]}: { - str(e)}' + f"Error creating ride review for {review_data['ride']}: { + str(e) + }" ) raise diff --git a/backend/apps/parks/management/commands/test_location.py b/backend/apps/parks/management/commands/test_location.py index 8834dbeb..47e68fba 100644 --- a/backend/apps/parks/management/commands/test_location.py +++ b/backend/apps/parks/management/commands/test_location.py @@ -55,10 +55,7 @@ class Command(BaseCommand): # Test Park model integration self.stdout.write("\n🔍 Testing Park model integration:") - self.stdout.write( - f" Park formatted location: { - park.formatted_location}" - ) + self.stdout.write(f" Park formatted location: {park.formatted_location}") self.stdout.write(f" Park coordinates: {park.coordinates}") # Create another location for distance testing @@ -112,10 +109,7 @@ class Command(BaseCommand): nearby_locations = ParkLocation.objects.filter( point__distance_lte=(search_point, D(km=100)) ) - self.stdout.write( - f" Found { - nearby_locations.count()} parks within 100km" - ) + self.stdout.write(f" Found {nearby_locations.count()} parks within 100km") for loc in nearby_locations: self.stdout.write(f" - {loc.park.name} in {loc.city}, {loc.state}") except Exception as e: diff --git a/backend/apps/parks/migrations/0001_add_filter_indexes.py b/backend/apps/parks/migrations/0001_add_filter_indexes.py index 24ad748c..00d4f077 100644 --- a/backend/apps/parks/migrations/0001_add_filter_indexes.py +++ b/backend/apps/parks/migrations/0001_add_filter_indexes.py @@ -1,6 +1,6 @@ # Generated manually for enhanced filtering performance -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): diff --git a/backend/apps/parks/migrations/0001_initial.py b/backend/apps/parks/migrations/0001_initial.py index 95d0849c..34728a1f 100644 --- a/backend/apps/parks/migrations/0001_initial.py +++ b/backend/apps/parks/migrations/0001_initial.py @@ -11,7 +11,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/backend/apps/parks/migrations/0002_alter_parkarea_unique_together.py b/backend/apps/parks/migrations/0002_alter_parkarea_unique_together.py index ca7cfacb..86f1d446 100644 --- a/backend/apps/parks/migrations/0002_alter_parkarea_unique_together.py +++ b/backend/apps/parks/migrations/0002_alter_parkarea_unique_together.py @@ -4,7 +4,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("parks", "0001_initial"), ] diff --git a/backend/apps/parks/migrations/0003_add_business_constraints.py b/backend/apps/parks/migrations/0003_add_business_constraints.py index 60f3dd47..5022cbfc 100644 --- a/backend/apps/parks/migrations/0003_add_business_constraints.py +++ b/backend/apps/parks/migrations/0003_add_business_constraints.py @@ -6,7 +6,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("parks", "0002_alter_parkarea_unique_together"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), diff --git a/backend/apps/parks/migrations/0004_fix_pghistory_triggers.py b/backend/apps/parks/migrations/0004_fix_pghistory_triggers.py index 42178a88..110b0bdd 100644 --- a/backend/apps/parks/migrations/0004_fix_pghistory_triggers.py +++ b/backend/apps/parks/migrations/0004_fix_pghistory_triggers.py @@ -8,7 +8,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("parks", "0003_add_business_constraints"), ("pghistory", "0007_auto_20250421_0444"), diff --git a/backend/apps/parks/migrations/0005_merge_20250820_2020.py b/backend/apps/parks/migrations/0005_merge_20250820_2020.py index 54b92f6f..8210d476 100644 --- a/backend/apps/parks/migrations/0005_merge_20250820_2020.py +++ b/backend/apps/parks/migrations/0005_merge_20250820_2020.py @@ -4,7 +4,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("parks", "0001_add_filter_indexes"), ("parks", "0004_fix_pghistory_triggers"), diff --git a/backend/apps/parks/migrations/0006_remove_company_insert_insert_and_more.py b/backend/apps/parks/migrations/0006_remove_company_insert_insert_and_more.py index 42bb0859..495891c4 100644 --- a/backend/apps/parks/migrations/0006_remove_company_insert_insert_and_more.py +++ b/backend/apps/parks/migrations/0006_remove_company_insert_insert_and_more.py @@ -6,7 +6,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("parks", "0005_merge_20250820_2020"), ] diff --git a/backend/apps/parks/migrations/0007_companyheadquartersevent_parklocationevent_and_more.py b/backend/apps/parks/migrations/0007_companyheadquartersevent_parklocationevent_and_more.py index 0e416559..ec38979c 100644 --- a/backend/apps/parks/migrations/0007_companyheadquartersevent_parklocationevent_and_more.py +++ b/backend/apps/parks/migrations/0007_companyheadquartersevent_parklocationevent_and_more.py @@ -8,7 +8,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("parks", "0006_remove_company_insert_insert_and_more"), ("pghistory", "0007_auto_20250421_0444"), diff --git a/backend/apps/parks/migrations/0008_parkphoto_parkphotoevent_and_more.py b/backend/apps/parks/migrations/0008_parkphoto_parkphotoevent_and_more.py new file mode 100644 index 00000000..42880903 --- /dev/null +++ b/backend/apps/parks/migrations/0008_parkphoto_parkphotoevent_and_more.py @@ -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", + ), + ), + ), + ] diff --git a/backend/apps/parks/models/__init__.py b/backend/apps/parks/models/__init__.py index 8844497a..ec06b470 100644 --- a/backend/apps/parks/models/__init__.py +++ b/backend/apps/parks/models/__init__.py @@ -14,6 +14,7 @@ from .location import ParkLocation from .reviews import ParkReview from .companies import Company, CompanyHeadquarters from .media import ParkPhoto + # Alias Company as Operator for clarity Operator = Company diff --git a/backend/apps/parks/models/areas.py b/backend/apps/parks/models/areas.py index de3bdcb4..497e4542 100644 --- a/backend/apps/parks/models/areas.py +++ b/backend/apps/parks/models/areas.py @@ -8,7 +8,6 @@ from .parks import Park @pghistory.track() class ParkArea(TrackedModel): - # Import managers from ..managers import ParkAreaManager diff --git a/backend/apps/parks/models/companies.py b/backend/apps/parks/models/companies.py index 773c2742..d2f9ab9f 100644 --- a/backend/apps/parks/models/companies.py +++ b/backend/apps/parks/models/companies.py @@ -7,7 +7,6 @@ import pghistory @pghistory.track() class Company(TrackedModel): - # Import managers from ..managers import CompanyManager @@ -107,13 +106,7 @@ class CompanyHeadquarters(models.Model): components.append(self.postal_code) if self.country and self.country != "USA": components.append(self.country) - return ( - ", ".join(components) - if components - else f"{ - self.city}, { - self.country}" - ) + return ", ".join(components) if components else f"{self.city}, {self.country}" @property def location_display(self): diff --git a/backend/apps/parks/models/media.py b/backend/apps/parks/models/media.py index 9a109e94..765e1a56 100644 --- a/backend/apps/parks/models/media.py +++ b/backend/apps/parks/models/media.py @@ -7,7 +7,6 @@ This module contains media models specific to parks domain. from typing import Any, Optional, cast from django.db import models from django.conf import settings -from django.utils import timezone from apps.core.history import TrackedModel from apps.core.services.media_service import MediaService import pghistory @@ -15,16 +14,14 @@ import pghistory def park_photo_upload_path(instance: models.Model, filename: str) -> str: """Generate upload path for park photos.""" - photo = cast('ParkPhoto', instance) + photo = cast("ParkPhoto", instance) park = photo.park if park is None: raise ValueError("Park cannot be None") return MediaService.generate_upload_path( - domain="park", - identifier=park.slug, - filename=filename + domain="park", identifier=park.slug, filename=filename ) @@ -33,9 +30,7 @@ class ParkPhoto(TrackedModel): """Photo model specific to parks.""" park = models.ForeignKey( - 'parks.Park', - on_delete=models.CASCADE, - related_name='photos' + "parks.Park", on_delete=models.CASCADE, related_name="photos" ) image = models.ImageField( @@ -72,9 +67,9 @@ class ParkPhoto(TrackedModel): constraints = [ # Only one primary photo per park models.UniqueConstraint( - fields=['park'], + fields=["park"], condition=models.Q(is_primary=True), - name='unique_primary_park_photo' + name="unique_primary_park_photo", ) ] diff --git a/backend/apps/parks/models/parks.py b/backend/apps/parks/models/parks.py index 95baa36d..6b9e274c 100644 --- a/backend/apps/parks/models/parks.py +++ b/backend/apps/parks/models/parks.py @@ -13,7 +13,6 @@ if TYPE_CHECKING: @pghistory.track() class Park(TrackedModel): - # Import managers from ..managers import ParkManager @@ -226,7 +225,8 @@ class Park(TrackedModel): if historical: print( f"Found historical slug record for object_id: { - historical.object_id}" + historical.object_id + }" ) try: park = cls.objects.get(pk=historical.object_id) @@ -250,7 +250,8 @@ class Park(TrackedModel): if historical_event: print( f"Found pghistory event for pgh_obj_id: { - historical_event.pgh_obj_id}" + historical_event.pgh_obj_id + }" ) try: park = cls.objects.get(pk=historical_event.pgh_obj_id) diff --git a/backend/apps/parks/models/reviews.py b/backend/apps/parks/models/reviews.py index fcd127e6..0bc6c57e 100644 --- a/backend/apps/parks/models/reviews.py +++ b/backend/apps/parks/models/reviews.py @@ -7,7 +7,6 @@ import pghistory @pghistory.track() class ParkReview(TrackedModel): - # Import managers from ..managers import ParkReviewManager diff --git a/backend/apps/parks/services/__init__.py b/backend/apps/parks/services/__init__.py index 24717d8e..f2ee4bf7 100644 --- a/backend/apps/parks/services/__init__.py +++ b/backend/apps/parks/services/__init__.py @@ -3,5 +3,11 @@ from .park_management import ParkService from .location_service import ParkLocationService from .filter_service import ParkFilterService from .media_service import ParkMediaService -__all__ = ["RoadTripService", "ParkService", - "ParkLocationService", "ParkFilterService", "ParkMediaService"] + +__all__ = [ + "RoadTripService", + "ParkService", + "ParkLocationService", + "ParkFilterService", + "ParkMediaService", +] diff --git a/backend/apps/parks/services/location_service.py b/backend/apps/parks/services/location_service.py index 6b947e83..3a721427 100644 --- a/backend/apps/parks/services/location_service.py +++ b/backend/apps/parks/services/location_service.py @@ -4,8 +4,7 @@ Handles geocoding, reverse geocoding, and location search for parks. """ import requests -from typing import List, Dict, Any, Optional, Tuple -from django.conf import settings +from typing import List, Dict, Any, Optional from django.core.cache import cache from django.db import transaction import logging diff --git a/backend/apps/parks/services/media_service.py b/backend/apps/parks/services/media_service.py index 9b9dc006..1afbaae0 100644 --- a/backend/apps/parks/services/media_service.py +++ b/backend/apps/parks/services/media_service.py @@ -27,7 +27,7 @@ class ParkMediaService: caption: str = "", alt_text: str = "", is_primary: bool = False, - auto_approve: bool = False + auto_approve: bool = False, ) -> ParkPhoto: """ Upload a photo for a park. @@ -64,7 +64,7 @@ class ParkMediaService: alt_text=alt_text, is_primary=is_primary, is_approved=auto_approve, - uploaded_by=user + uploaded_by=user, ) # Extract EXIF date @@ -77,9 +77,7 @@ class ParkMediaService: @staticmethod def get_park_photos( - park: Park, - approved_only: bool = True, - primary_first: bool = True + park: Park, approved_only: bool = True, primary_first: bool = True ) -> List[ParkPhoto]: """ Get photos for a park. @@ -98,9 +96,9 @@ class ParkMediaService: queryset = queryset.filter(is_approved=True) if primary_first: - queryset = queryset.order_by('-is_primary', '-created_at') + queryset = queryset.order_by("-is_primary", "-created_at") else: - queryset = queryset.order_by('-created_at') + queryset = queryset.order_by("-created_at") return list(queryset) @@ -190,7 +188,8 @@ class ParkMediaService: photo.delete() logger.info( - f"Photo {photo_id} deleted from park {park_slug} by user {deleted_by.username}") + f"Photo {photo_id} deleted from park {park_slug} by user {deleted_by.username}" + ) return True except Exception as e: logger.error(f"Failed to delete photo {photo.pk}: {str(e)}") @@ -214,7 +213,7 @@ class ParkMediaService: "approved_photos": photos.filter(is_approved=True).count(), "pending_photos": photos.filter(is_approved=False).count(), "has_primary": photos.filter(is_primary=True).exists(), - "recent_uploads": photos.order_by('-created_at')[:5].count() + "recent_uploads": photos.order_by("-created_at")[:5].count(), } @staticmethod @@ -237,5 +236,6 @@ class ParkMediaService: approved_count += 1 logger.info( - f"Bulk approved {approved_count} photos by user {approved_by.username}") + f"Bulk approved {approved_count} photos by user {approved_by.username}" + ) return approved_count diff --git a/backend/apps/parks/services/roadtrip.py b/backend/apps/parks/services/roadtrip.py index 3cc66e75..e5364f0d 100644 --- a/backend/apps/parks/services/roadtrip.py +++ b/backend/apps/parks/services/roadtrip.py @@ -192,8 +192,7 @@ class RoadTripService: time.sleep(wait_time) else: raise OSMAPIException( - f"Failed to make request after { - self.max_retries} attempts: {e}" + f"Failed to make request after {self.max_retries} attempts: {e}" ) def geocode_address(self, address: str) -> Optional[Coordinates]: @@ -244,9 +243,7 @@ class RoadTripService: ) logger.info( - f"Geocoded '{address}' to { - coords.latitude}, { - coords.longitude}" + f"Geocoded '{address}' to {coords.latitude}, {coords.longitude}" ) return coords else: @@ -274,22 +271,18 @@ class RoadTripService: return None # Check cache first - cache_key = f"roadtrip:route:{ - start_coords.latitude},{ - start_coords.longitude}:{ - end_coords.latitude},{ - end_coords.longitude}" + cache_key = f"roadtrip:route:{start_coords.latitude},{start_coords.longitude}:{ + end_coords.latitude + },{end_coords.longitude}" cached_result = cache.get(cache_key) if cached_result: return RouteInfo(**cached_result) try: # Format coordinates for OSRM (lon,lat format) - coords_string = f"{ - start_coords.longitude},{ - start_coords.latitude};{ - end_coords.longitude},{ - end_coords.latitude}" + coords_string = f"{start_coords.longitude},{start_coords.latitude};{ + end_coords.longitude + },{end_coords.latitude}" url = f"{self.osrm_base_url}/{coords_string}" params = { @@ -326,9 +319,9 @@ class RoadTripService: ) logger.info( - f"Route calculated: { - route_info.formatted_distance}, { - route_info.formatted_duration}" + f"Route calculated: {route_info.formatted_distance}, { + route_info.formatted_duration + }" ) return route_info else: @@ -350,11 +343,13 @@ class RoadTripService: Calculate straight-line distance as fallback when routing fails. """ # Haversine formula for great-circle distance - lat1, lon1 = math.radians(start_coords.latitude), math.radians( - start_coords.longitude + lat1, lon1 = ( + math.radians(start_coords.latitude), + math.radians(start_coords.longitude), ) - lat2, lon2 = math.radians(end_coords.latitude), math.radians( - end_coords.longitude + lat2, lon2 = ( + math.radians(end_coords.latitude), + math.radians(end_coords.longitude), ) dlat = lat2 - lat1 @@ -696,10 +691,7 @@ class RoadTripService: location.set_coordinates(coords.latitude, coords.longitude) location.save() logger.info( - f"Geocoded park '{ - park.name}' to { - coords.latitude}, { - coords.longitude}" + f"Geocoded park '{park.name}' to {coords.latitude}, {coords.longitude}" ) return True diff --git a/backend/apps/parks/tests_disabled/test_models.py b/backend/apps/parks/tests_disabled/test_models.py index 429e24a8..862689f2 100644 --- a/backend/apps/parks/tests_disabled/test_models.py +++ b/backend/apps/parks/tests_disabled/test_models.py @@ -165,7 +165,8 @@ class ParkAreaModelTests(TestCase): with transaction.atomic(): with self.assertRaises(IntegrityError): ParkArea.objects.create( - park=self.park, name="Test Area" # Will generate same slug + park=self.park, + name="Test Area", # Will generate same slug ) # Should be able to use same name in different park diff --git a/backend/apps/parks/views.py b/backend/apps/parks/views.py index 3ee4356f..b914e7d4 100644 --- a/backend/apps/parks/views.py +++ b/backend/apps/parks/views.py @@ -551,14 +551,12 @@ class ParkCreateView(LoginRequiredMixin, CreateView): image=photo_file, uploaded_by=self.request.user, park=self.object, - ) ) + ) uploaded_count += 1 except Exception as e: messages.error( self.request, - f"Error uploading photo { - photo_file.name}: { - str(e)}", + f"Error uploading photo {photo_file.name}: {str(e)}", ) messages.success( @@ -571,7 +569,8 @@ class ParkCreateView(LoginRequiredMixin, CreateView): messages.error( self.request, f"Error creating park: { - str(e)}. Please check your input and try again.", + str(e) + }. Please check your input and try again.", ) return self.form_invalid(form) @@ -727,9 +726,7 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView): except Exception as e: messages.error( self.request, - f"Error uploading photo { - photo_file.name}: { - str(e)}", + f"Error uploading photo {photo_file.name}: {str(e)}", ) messages.success( @@ -742,7 +739,8 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView): messages.error( self.request, f"Error updating park: { - str(e)}. Please check your input and try again.", + str(e) + }. Please check your input and try again.", ) return self.form_invalid(form) diff --git a/backend/apps/parks/views_roadtrip.py b/backend/apps/parks/views_roadtrip.py index 28c52d52..7d6de084 100644 --- a/backend/apps/parks/views_roadtrip.py +++ b/backend/apps/parks/views_roadtrip.py @@ -13,7 +13,7 @@ from django.urls import reverse from .models import Park from .services.roadtrip import RoadTripService from apps.core.services.map_service import unified_map_service -from apps.core.services.data_structures import LocationType, MapFilters +from apps.core.services.data_structures import LocationType JSON_DECODE_ERROR_MSG = "Invalid JSON data" PARKS_ALONG_ROUTE_HTML = "parks/partials/parks_along_route.html" diff --git a/backend/apps/rides/forms.py b/backend/apps/rides/forms.py index 546ed9ff..42da413d 100644 --- a/backend/apps/rides/forms.py +++ b/backend/apps/rides/forms.py @@ -346,9 +346,9 @@ class RideForm(forms.ModelForm): # editing if self.instance and self.instance.pk: if self.instance.manufacturer: - self.fields["manufacturer_search"].initial = ( - self.instance.manufacturer.name - ) + self.fields[ + "manufacturer_search" + ].initial = self.instance.manufacturer.name self.fields["manufacturer"].initial = self.instance.manufacturer if self.instance.designer: self.fields["designer_search"].initial = self.instance.designer.name diff --git a/backend/apps/rides/forms/base.py b/backend/apps/rides/forms/base.py index b826f641..c4fdff39 100644 --- a/backend/apps/rides/forms/base.py +++ b/backend/apps/rides/forms/base.py @@ -346,9 +346,9 @@ class RideForm(forms.ModelForm): # editing if self.instance and self.instance.pk: if self.instance.manufacturer: - self.fields["manufacturer_search"].initial = ( - self.instance.manufacturer.name - ) + self.fields[ + "manufacturer_search" + ].initial = self.instance.manufacturer.name self.fields["manufacturer"].initial = self.instance.manufacturer if self.instance.designer: self.fields["designer_search"].initial = self.instance.designer.name diff --git a/backend/apps/rides/migrations/0001_initial.py b/backend/apps/rides/migrations/0001_initial.py index 8287dda2..2eb8563d 100644 --- a/backend/apps/rides/migrations/0001_initial.py +++ b/backend/apps/rides/migrations/0001_initial.py @@ -11,7 +11,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/backend/apps/rides/migrations/0002_add_business_constraints.py b/backend/apps/rides/migrations/0002_add_business_constraints.py index 8359bd7f..d661a3a3 100644 --- a/backend/apps/rides/migrations/0002_add_business_constraints.py +++ b/backend/apps/rides/migrations/0002_add_business_constraints.py @@ -6,7 +6,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("parks", "0003_add_business_constraints"), ("rides", "0001_initial"), diff --git a/backend/apps/rides/migrations/0003_remove_company_insert_insert_and_more.py b/backend/apps/rides/migrations/0003_remove_company_insert_insert_and_more.py index 815a3621..c33d3a4b 100644 --- a/backend/apps/rides/migrations/0003_remove_company_insert_insert_and_more.py +++ b/backend/apps/rides/migrations/0003_remove_company_insert_insert_and_more.py @@ -6,7 +6,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("rides", "0002_add_business_constraints"), ] diff --git a/backend/apps/rides/migrations/0004_rideevent_ridemodelevent_rollercoasterstatsevent_and_more.py b/backend/apps/rides/migrations/0004_rideevent_ridemodelevent_rollercoasterstatsevent_and_more.py index 9be1bdf0..c40ea9b2 100644 --- a/backend/apps/rides/migrations/0004_rideevent_ridemodelevent_rollercoasterstatsevent_and_more.py +++ b/backend/apps/rides/migrations/0004_rideevent_ridemodelevent_rollercoasterstatsevent_and_more.py @@ -7,7 +7,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("parks", "0006_remove_company_insert_insert_and_more"), ("pghistory", "0007_auto_20250421_0444"), diff --git a/backend/apps/rides/migrations/0005_ridelocationevent_ridelocation_insert_insert_and_more.py b/backend/apps/rides/migrations/0005_ridelocationevent_ridelocation_insert_insert_and_more.py index 7886399c..e4f58f20 100644 --- a/backend/apps/rides/migrations/0005_ridelocationevent_ridelocation_insert_insert_and_more.py +++ b/backend/apps/rides/migrations/0005_ridelocationevent_ridelocation_insert_insert_and_more.py @@ -8,7 +8,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("pghistory", "0007_auto_20250421_0444"), ("rides", "0004_rideevent_ridemodelevent_rollercoasterstatsevent_and_more"), diff --git a/backend/apps/rides/migrations/0006_add_ride_rankings.py b/backend/apps/rides/migrations/0006_add_ride_rankings.py index 1f092f50..4ba996b7 100644 --- a/backend/apps/rides/migrations/0006_add_ride_rankings.py +++ b/backend/apps/rides/migrations/0006_add_ride_rankings.py @@ -9,7 +9,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("pghistory", "0007_auto_20250421_0444"), ("rides", "0005_ridelocationevent_ridelocation_insert_insert_and_more"), diff --git a/backend/apps/rides/migrations/0007_ridephoto_ridephotoevent_and_more.py b/backend/apps/rides/migrations/0007_ridephoto_ridephotoevent_and_more.py new file mode 100644 index 00000000..51523baf --- /dev/null +++ b/backend/apps/rides/migrations/0007_ridephoto_ridephotoevent_and_more.py @@ -0,0 +1,224 @@ +# Generated by Django 5.2.5 on 2025-08-26 17:39 + +import apps.rides.models.media +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("pghistory", "0007_auto_20250421_0444"), + ("rides", "0006_add_ride_rankings"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="RidePhoto", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "image", + models.ImageField( + max_length=255, + upload_to=apps.rides.models.media.ride_photo_upload_path, + ), + ), + ("caption", models.CharField(blank=True, max_length=255)), + ("alt_text", models.CharField(blank=True, max_length=255)), + ("is_primary", models.BooleanField(default=False)), + ("is_approved", models.BooleanField(default=False)), + ( + "photo_type", + models.CharField( + choices=[ + ("exterior", "Exterior View"), + ("queue", "Queue Area"), + ("station", "Station"), + ("onride", "On-Ride"), + ("construction", "Construction"), + ("other", "Other"), + ], + default="exterior", + max_length=50, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("date_taken", models.DateTimeField(blank=True, null=True)), + ( + "ride", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="photos", + to="rides.ride", + ), + ), + ( + "uploaded_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="uploaded_ride_photos", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["-is_primary", "-created_at"], + }, + ), + migrations.CreateModel( + name="RidePhotoEvent", + fields=[ + ("pgh_id", models.AutoField(primary_key=True, serialize=False)), + ("pgh_created_at", models.DateTimeField(auto_now_add=True)), + ("pgh_label", models.TextField(help_text="The event label.")), + ("id", models.BigIntegerField()), + ( + "image", + models.ImageField( + max_length=255, + upload_to=apps.rides.models.media.ride_photo_upload_path, + ), + ), + ("caption", models.CharField(blank=True, max_length=255)), + ("alt_text", models.CharField(blank=True, max_length=255)), + ("is_primary", models.BooleanField(default=False)), + ("is_approved", models.BooleanField(default=False)), + ( + "photo_type", + models.CharField( + choices=[ + ("exterior", "Exterior View"), + ("queue", "Queue Area"), + ("station", "Station"), + ("onride", "On-Ride"), + ("construction", "Construction"), + ("other", "Other"), + ], + default="exterior", + max_length=50, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("date_taken", models.DateTimeField(blank=True, null=True)), + ( + "pgh_context", + models.ForeignKey( + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="pghistory.context", + ), + ), + ( + "pgh_obj", + models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.ridephoto", + ), + ), + ( + "ride", + models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ride", + ), + ), + ( + "uploaded_by", + models.ForeignKey( + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddIndex( + model_name="ridephoto", + index=models.Index( + fields=["ride", "is_primary"], name="rides_ridep_ride_id_aa49f1_idx" + ), + ), + migrations.AddIndex( + model_name="ridephoto", + index=models.Index( + fields=["ride", "is_approved"], name="rides_ridep_ride_id_f1eddc_idx" + ), + ), + migrations.AddIndex( + model_name="ridephoto", + index=models.Index( + fields=["ride", "photo_type"], name="rides_ridep_ride_id_49e7ec_idx" + ), + ), + migrations.AddIndex( + model_name="ridephoto", + index=models.Index( + fields=["created_at"], name="rides_ridep_created_106e02_idx" + ), + ), + migrations.AddConstraint( + model_name="ridephoto", + constraint=models.UniqueConstraint( + condition=models.Q(("is_primary", True)), + fields=("ride",), + name="unique_primary_ride_photo", + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ridephoto", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "rides_ridephotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image", "is_approved", "is_primary", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo_type", "ride_id", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."created_at", NEW."date_taken", NEW."id", NEW."image", NEW."is_approved", NEW."is_primary", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."photo_type", NEW."ride_id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;', + hash="8027f17cac76b8301927e468ab4873ae9f38f27a", + operation="INSERT", + pgid="pgtrigger_insert_insert_0043a", + table="rides_ridephoto", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ridephoto", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "rides_ridephotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image", "is_approved", "is_primary", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo_type", "ride_id", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."created_at", NEW."date_taken", NEW."id", NEW."image", NEW."is_approved", NEW."is_primary", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."photo_type", NEW."ride_id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;', + hash="54562f9a78754cac359f1efd5c0e8d6d144d1806", + operation="UPDATE", + pgid="pgtrigger_update_update_93a7e", + table="rides_ridephoto", + when="AFTER", + ), + ), + ), + ] diff --git a/backend/apps/rides/models/__init__.py b/backend/apps/rides/models/__init__.py index 1e40bfdb..54d45a51 100644 --- a/backend/apps/rides/models/__init__.py +++ b/backend/apps/rides/models/__init__.py @@ -7,6 +7,7 @@ enabling imports like: from rides.models import Ride, Manufacturer The Company model is aliased as Manufacturer to clarify its role as ride manufacturers, while maintaining backward compatibility through the Company alias. """ + from .rides import Ride, RideModel, RollerCoasterStats, Categories, CATEGORY_CHOICES from .location import RideLocation from .reviews import RideReview diff --git a/backend/apps/rides/models/media.py b/backend/apps/rides/models/media.py index dd7fab00..d0495273 100644 --- a/backend/apps/rides/models/media.py +++ b/backend/apps/rides/models/media.py @@ -7,7 +7,6 @@ This module contains media models specific to rides domain. from typing import Any, Optional, cast from django.db import models from django.conf import settings -from django.utils import timezone from apps.core.history import TrackedModel from apps.core.services.media_service import MediaService import pghistory @@ -15,7 +14,7 @@ import pghistory def ride_photo_upload_path(instance: models.Model, filename: str) -> str: """Generate upload path for ride photos.""" - photo = cast('RidePhoto', instance) + photo = cast("RidePhoto", instance) ride = photo.ride if ride is None: @@ -25,7 +24,7 @@ def ride_photo_upload_path(instance: models.Model, filename: str) -> str: domain="park", identifier=ride.slug, filename=filename, - subdirectory=ride.park.slug + subdirectory=ride.park.slug, ) @@ -34,9 +33,7 @@ class RidePhoto(TrackedModel): """Photo model specific to rides.""" ride = models.ForeignKey( - 'rides.Ride', - on_delete=models.CASCADE, - related_name='photos' + "rides.Ride", on_delete=models.CASCADE, related_name="photos" ) image = models.ImageField( @@ -53,14 +50,14 @@ class RidePhoto(TrackedModel): photo_type = models.CharField( max_length=50, choices=[ - ('exterior', 'Exterior View'), - ('queue', 'Queue Area'), - ('station', 'Station'), - ('onride', 'On-Ride'), - ('construction', 'Construction'), - ('other', 'Other'), + ("exterior", "Exterior View"), + ("queue", "Queue Area"), + ("station", "Station"), + ("onride", "On-Ride"), + ("construction", "Construction"), + ("other", "Other"), ], - default='exterior' + default="exterior", ) # Metadata @@ -88,9 +85,9 @@ class RidePhoto(TrackedModel): constraints = [ # Only one primary photo per ride models.UniqueConstraint( - fields=['ride'], + fields=["ride"], condition=models.Q(is_primary=True), - name='unique_primary_ride_photo' + name="unique_primary_ride_photo", ) ] diff --git a/backend/apps/rides/models/rides.py b/backend/apps/rides/models/rides.py index 63404427..478f79dd 100644 --- a/backend/apps/rides/models/rides.py +++ b/backend/apps/rides/models/rides.py @@ -1,7 +1,5 @@ from django.db import models from django.utils.text import slugify -from django.contrib.contenttypes.fields import GenericRelation -from django.db.models import Avg from apps.core.models import TrackedModel from .company import Company import pghistory @@ -140,7 +138,6 @@ class Ride(TrackedModel): average_rating = models.DecimalField( max_digits=3, decimal_places=2, null=True, blank=True ) - photos = GenericRelation("media.Photo") class Meta(TrackedModel.Meta): ordering = ["name"] diff --git a/backend/apps/rides/selectors.py b/backend/apps/rides/selectors.py index 017734bf..2e6e4aae 100644 --- a/backend/apps/rides/selectors.py +++ b/backend/apps/rides/selectors.py @@ -273,7 +273,6 @@ def ride_statistics_by_category() -> Dict[str, Any]: Returns: Dictionary containing ride statistics by category """ - from .models import CATEGORY_CHOICES stats = {} for category_code, category_name in CATEGORY_CHOICES: diff --git a/backend/apps/rides/services/location_service.py b/backend/apps/rides/services/location_service.py index d3a591e6..09dccc97 100644 --- a/backend/apps/rides/services/location_service.py +++ b/backend/apps/rides/services/location_service.py @@ -5,8 +5,6 @@ Handles location management for individual rides within parks. import requests from typing import List, Dict, Any, Optional, Tuple -from django.conf import settings -from django.core.cache import cache from django.db import transaction import logging @@ -317,7 +315,6 @@ class RideLocationService: ride_location.ride.name.lower() in display_name and park.name.lower() in display_name ): - # Update the ride location ride_location.set_coordinates( float(result["lat"]), float(result["lon"]) diff --git a/backend/apps/rides/services/media_service.py b/backend/apps/rides/services/media_service.py index 42c2fbc6..aaec46ef 100644 --- a/backend/apps/rides/services/media_service.py +++ b/backend/apps/rides/services/media_service.py @@ -28,7 +28,7 @@ class RideMediaService: alt_text: str = "", photo_type: str = "exterior", is_primary: bool = False, - auto_approve: bool = False + auto_approve: bool = False, ) -> RidePhoto: """ Upload a photo for a ride. @@ -67,7 +67,7 @@ class RideMediaService: photo_type=photo_type, is_primary=is_primary, is_approved=auto_approve, - uploaded_by=user + uploaded_by=user, ) # Extract EXIF date @@ -83,7 +83,7 @@ class RideMediaService: ride: Ride, approved_only: bool = True, primary_first: bool = True, - photo_type: Optional[str] = None + photo_type: Optional[str] = None, ) -> List[RidePhoto]: """ Get photos for a ride. @@ -106,9 +106,9 @@ class RideMediaService: queryset = queryset.filter(photo_type=photo_type) if primary_first: - queryset = queryset.order_by('-is_primary', '-created_at') + queryset = queryset.order_by("-is_primary", "-created_at") else: - queryset = queryset.order_by('-created_at') + queryset = queryset.order_by("-created_at") return list(queryset) @@ -141,10 +141,9 @@ class RideMediaService: List of RidePhoto instances """ return list( - ride.photos.filter( - photo_type=photo_type, - is_approved=True - ).order_by('-created_at') + ride.photos.filter(photo_type=photo_type, is_approved=True).order_by( + "-created_at" + ) ) @staticmethod @@ -217,7 +216,8 @@ class RideMediaService: photo.delete() logger.info( - f"Photo {photo_id} deleted from ride {ride_slug} by user {deleted_by.username}") + f"Photo {photo_id} deleted from ride {ride_slug} by user {deleted_by.username}" + ) return True except Exception as e: logger.error(f"Failed to delete photo {photo.pk}: {str(e)}") @@ -238,7 +238,7 @@ class RideMediaService: # Get counts by photo type type_counts = {} - for photo_type, _ in RidePhoto._meta.get_field('photo_type').choices: + for photo_type, _ in RidePhoto._meta.get_field("photo_type").choices: type_counts[photo_type] = photos.filter(photo_type=photo_type).count() return { @@ -246,8 +246,8 @@ class RideMediaService: "approved_photos": photos.filter(is_approved=True).count(), "pending_photos": photos.filter(is_approved=False).count(), "has_primary": photos.filter(is_primary=True).exists(), - "recent_uploads": photos.order_by('-created_at')[:5].count(), - "by_type": type_counts + "recent_uploads": photos.order_by("-created_at")[:5].count(), + "by_type": type_counts, } @staticmethod @@ -270,7 +270,8 @@ class RideMediaService: approved_count += 1 logger.info( - f"Bulk approved {approved_count} photos by user {approved_by.username}") + f"Bulk approved {approved_count} photos by user {approved_by.username}" + ) return approved_count @staticmethod @@ -285,10 +286,9 @@ class RideMediaService: List of construction RidePhoto instances ordered by date taken """ return list( - ride.photos.filter( - photo_type='construction', - is_approved=True - ).order_by('date_taken', 'created_at') + ride.photos.filter(photo_type="construction", is_approved=True).order_by( + "date_taken", "created_at" + ) ) @staticmethod @@ -302,4 +302,4 @@ class RideMediaService: Returns: List of on-ride RidePhoto instances """ - return RideMediaService.get_photos_by_type(ride, 'onride') + return RideMediaService.get_photos_by_type(ride, "onride") diff --git a/backend/apps/rides/services/ranking_service.py b/backend/apps/rides/services/ranking_service.py index 70f2bf38..61b18db5 100644 --- a/backend/apps/rides/services/ranking_service.py +++ b/backend/apps/rides/services/ranking_service.py @@ -12,7 +12,7 @@ from decimal import Decimal from datetime import date from django.db import transaction -from django.db.models import Avg, Count, Q, F +from django.db.models import Avg, Count, Q from django.utils import timezone from apps.rides.models import ( diff --git a/backend/apps/rides/urls.py b/backend/apps/rides/urls.py index 8ad00d2a..93dbf050 100644 --- a/backend/apps/rides/urls.py +++ b/backend/apps/rides/urls.py @@ -1,4 +1,4 @@ -from django.urls import path, include +from django.urls import path from . import views app_name = "rides" diff --git a/backend/config/django/base.py b/backend/config/django/base.py index effe4651..abab1d50 100644 --- a/backend/config/django/base.py +++ b/backend/config/django/base.py @@ -6,6 +6,7 @@ Common settings shared across all environments. import environ # type: ignore[import] import sys from pathlib import Path +from decouple import config # Initialize environment variables with better defaults env = environ.Env( @@ -37,10 +38,6 @@ apps_dir = BASE_DIR / "apps" if apps_dir.exists() and str(apps_dir) not in sys.path: sys.path.insert(0, str(apps_dir)) -# Add backend directory to sys.path so Django can find the api module -if str(BASE_DIR) not in sys.path: - sys.path.insert(0, str(BASE_DIR)) - # Read environment file if it exists environ.Env.read_env(BASE_DIR / ".env") @@ -144,6 +141,15 @@ TEMPLATES = [ WSGI_APPLICATION = "thrillwiki.wsgi.application" +# Cloudflare Images Settings +STORAGES = {"default": {"BACKEND": "cloudflare_images.storage.CloudflareImagesStorage"}} +CLOUDFLARE_IMAGES_ACCOUNT_ID = config('CLOUDFLARE_IMAGES_ACCOUNT_ID') +CLOUDFLARE_IMAGES_API_KEY = config('CLOUDFLARE_IMAGES_API_KEY') +CLOUDFLARE_IMAGES_ACCOUNT_HASH = config('CLOUDFLARE_IMAGES_ACCOUNT_HASH') +CLOUDFLARE_IMAGES_DOMAIN = config( + 'CLOUDFLARE_IMAGES_DOMAIN', default='imagedelivery.net') + +CLOUDFLARE_EMAIL = config('CLOUDFLARE_EMAIL') # Password validation AUTH_PASSWORD_VALIDATORS = [ { @@ -278,17 +284,14 @@ REST_FRAMEWORK = { # CORS Settings for API CORS_ALLOWED_ORIGINS = env("CORS_ALLOWED_ORIGINS", default=[]) # type: ignore[arg-type] CORS_ALLOW_CREDENTIALS = True -CORS_ALLOW_ALL_ORIGINS = env( - "CORS_ALLOW_ALL_ORIGINS", default=False -) # type: ignore[arg-type] +CORS_ALLOW_ALL_ORIGINS = env("CORS_ALLOW_ALL_ORIGINS", + default=False) # type: ignore[arg-type] # API-specific settings API_RATE_LIMIT_PER_MINUTE = env.int( - "API_RATE_LIMIT_PER_MINUTE", default=60 -) # type: ignore[arg-type] + "API_RATE_LIMIT_PER_MINUTE", default=60) # type: ignore[arg-type] API_RATE_LIMIT_PER_HOUR = env.int( - "API_RATE_LIMIT_PER_HOUR", default=1000 -) # type: ignore[arg-type] + "API_RATE_LIMIT_PER_HOUR", default=1000) # type: ignore[arg-type] # drf-spectacular settings SPECTACULAR_SETTINGS = { diff --git a/backend/config/django/local.py b/backend/config/django/local.py index 66b220c5..77a2e086 100644 --- a/backend/config/django/local.py +++ b/backend/config/django/local.py @@ -5,7 +5,6 @@ Local development settings for thrillwiki project. import logging from .base import * from ..settings import database -from pythonjsonlogger import jsonlogger # Import the module and use its members, e.g., email.EMAIL_HOST diff --git a/backend/manage.py b/backend/manage.py index 11647886..0c76f603 100755 --- a/backend/manage.py +++ b/backend/manage.py @@ -1,13 +1,8 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys -from pathlib import Path - -# Add the backend directory to Python path so 'api' module can be found -backend_dir = Path(__file__).resolve().parent -if str(backend_dir) not in sys.path: - sys.path.insert(0, str(backend_dir)) def main(): diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 929a1d98..39da8a8b 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -54,6 +54,8 @@ dependencies = [ "werkzeug>=3.1.3", "django-widget-tweaks>=1.5.0", "redis>=6.4.0", + "ruff>=0.12.10", + "python-decouple>=3.8", ] [dependency-groups] diff --git a/backend/tests/test_utils.py b/backend/tests/test_utils.py index dae693e1..ca5f2733 100644 --- a/backend/tests/test_utils.py +++ b/backend/tests/test_utils.py @@ -315,8 +315,7 @@ class GeographyTestMixin: self.assertLessEqual( distance_km, max_distance_km, - f"Points are { - distance_km:.2f}km apart, exceeds {max_distance_km}km", + f"Points are {distance_km:.2f}km apart, exceeds {max_distance_km}km", ) diff --git a/backend/thrillwiki/urls.py b/backend/thrillwiki/urls.py index 62793571..36dbdb2d 100644 --- a/backend/thrillwiki/urls.py +++ b/backend/thrillwiki/urls.py @@ -151,7 +151,6 @@ if settings.DEBUG: pass try: - urlpatterns += [path("silk/", include("silk.urls", namespace="silk"))] except ImportError: pass diff --git a/backend/uv.lock b/backend/uv.lock index 8aa7423e..e8070faa 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -614,16 +614,16 @@ wheels = [ [[package]] name = "django-silk" -version = "5.4.1" +version = "5.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, { name = "gprof2dot" }, { name = "sqlparse" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/3d/0d2b235ff915599ca64255a7ca496ff863e7640a9708955f162187255ea3/django_silk-5.4.1.tar.gz", hash = "sha256:4c09b2ff37a45af70553cb04ff507f512abfc768646de13373b9741fe8d538da", size = 4495005, upload-time = "2025-08-13T06:10:01.034Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/03/6094b030f7f874bedc5726f36ebd23d6ad6ad4596b199ad850ecf887f9a6/django_silk-5.4.2.tar.gz", hash = "sha256:f3b2be63c0b92a660b7aaee591c8a505bb0757e2de55f6914cb95c1ee9ea2138", size = 4495504, upload-time = "2025-08-26T06:55:07.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/8a/3b8128a6404a658ffdb348c1e0e1c575f736f5a0d4deaba9700b31cfa6c1/django_silk-5.4.1-py3-none-any.whl", hash = "sha256:3625acc029f172bd94fd32284ec49a07552c5e58f02ede26db90a2fa33e9507e", size = 1944345, upload-time = "2025-08-13T06:10:21.994Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e8/b45d7b141afe735ee10244ec69b5d43e25755281ed02196126ad14ee2d71/django_silk-5.4.2-py3-none-any.whl", hash = "sha256:8154203fae904dc66e99ede75a2bad9410abd672dbba6cb5b07b22f8b5577253", size = 1944482, upload-time = "2025-08-26T06:55:33.786Z" }, ] [[package]] @@ -775,14 +775,14 @@ wheels = [ [[package]] name = "faker" -version = "37.5.3" +version = "37.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/5d/7797a74e8e31fa227f0303239802c5f09b6722bdb6638359e7b6c8f30004/faker-37.5.3.tar.gz", hash = "sha256:8315d8ff4d6f4f588bd42ffe63abd599886c785073e26a44707e10eeba5713dc", size = 1907147, upload-time = "2025-07-30T15:52:19.528Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/cd/f7679c20f07d9e2013123b7f7e13809a3450a18d938d58e86081a486ea15/faker-37.6.0.tar.gz", hash = "sha256:0f8cc34f30095184adf87c3c24c45b38b33ad81c35ef6eb0a3118f301143012c", size = 1907960, upload-time = "2025-08-26T15:56:27.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/bf/d06dd96e7afa72069dbdd26ed0853b5e8bd7941e2c0819a9b21d6e6fc052/faker-37.5.3-py3-none-any.whl", hash = "sha256:386fe9d5e6132a915984bf887fcebcc72d6366a25dd5952905b31b141a17016d", size = 1949261, upload-time = "2025-07-30T15:52:17.729Z" }, + { url = "https://files.pythonhosted.org/packages/61/7d/8b50e4ac772719777be33661f4bde320793400a706f5eb214e4de46f093c/faker-37.6.0-py3-none-any.whl", hash = "sha256:3c5209b23d7049d596a51db5d76403a0ccfea6fc294ffa2ecfef6a8843b1e6a7", size = 1949837, upload-time = "2025-08-26T15:56:25.33Z" }, ] [[package]] @@ -1263,11 +1263,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.8" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, ] [[package]] @@ -1537,6 +1537,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/96/5f8a4545d783674f3de33f0ebc4db16cc76ce77a4c404d284f43f09125e3/pytest_playwright-0.7.0-py3-none-any.whl", hash = "sha256:2516d0871fa606634bfe32afbcc0342d68da2dbff97fe3459849e9c428486da2", size = 16618, upload-time = "2025-01-31T11:06:08.075Z" }, ] +[[package]] +name = "python-decouple" +version = "3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/97/373dcd5844ec0ea5893e13c39a2c67e7537987ad8de3842fe078db4582fa/python-decouple-3.8.tar.gz", hash = "sha256:ba6e2657d4f376ecc46f77a3a615e058d93ba5e465c01bbe57289bfb7cce680f", size = 9612, upload-time = "2023-03-01T19:38:38.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/d4/9193206c4563ec771faf2ccf54815ca7918529fe81f6adb22ee6d0e06622/python_decouple-3.8-py3-none-any.whl", hash = "sha256:d0d45340815b25f4de59c974b855bb38d03151d81b037d9e3f463b0c9f8cbd66", size = 9947, upload-time = "2023-03-01T19:38:36.015Z" }, +] + [[package]] name = "python-dotenv" version = "1.1.1" @@ -1769,6 +1778,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/3f/d6c216ed5199c9ef79e2a33955601f454ed1e7420a93b89670133bca5ace/rpds_py-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391", size = 230993, upload-time = "2025-08-07T08:25:23.34Z" }, ] +[[package]] +name = "ruff" +version = "0.12.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" }, + { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783, upload-time = "2025-08-21T18:22:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" }, + { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" }, + { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" }, + { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603, upload-time = "2025-08-21T18:23:06.935Z" }, + { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089, upload-time = "2025-08-21T18:23:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616, upload-time = "2025-08-21T18:23:17.422Z" }, + { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225, upload-time = "2025-08-21T18:23:20.137Z" }, +] + [[package]] name = "secretstorage" version = "3.3.3" @@ -1793,15 +1828,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.35.0" +version = "2.35.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/83/055dc157b719651ef13db569bb8cf2103df11174478649735c1b2bf3f6bc/sentry_sdk-2.35.0.tar.gz", hash = "sha256:5ea58d352779ce45d17bc2fa71ec7185205295b83a9dbb5707273deb64720092", size = 343014, upload-time = "2025-08-14T17:11:20.223Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/75/6223b9ffa0bf5a79ece08055469be73c18034e46ed082742a0899cc58351/sentry_sdk-2.35.1.tar.gz", hash = "sha256:241b41e059632fe1f7c54ae6e1b93af9456aebdfc297be9cf7ecfd6da5167e8e", size = 343145, upload-time = "2025-08-26T08:23:32.429Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/3d/742617a7c644deb0c1628dcf6bb2d2165ab7c6aab56fe5222758994007f8/sentry_sdk-2.35.0-py2.py3-none-any.whl", hash = "sha256:6e0c29b9a5d34de8575ffb04d289a987ff3053cf2c98ede445bea995e3830263", size = 363806, upload-time = "2025-08-14T17:11:18.29Z" }, + { url = "https://files.pythonhosted.org/packages/62/1f/5feb6c42cc30126e9574eabc28139f8c626b483a47c537f648d133628df0/sentry_sdk-2.35.1-py2.py3-none-any.whl", hash = "sha256:13b6d6cfdae65d61fe1396a061cf9113b20f0ec1bcb257f3826b88f01bb55720", size = 363887, upload-time = "2025-08-26T08:23:30.335Z" }, ] [[package]] @@ -1921,10 +1956,12 @@ dependencies = [ { name = "pytest" }, { name = "pytest-django" }, { name = "pytest-playwright" }, + { name = "python-decouple" }, { name = "python-dotenv" }, { name = "python-json-logger" }, { name = "redis" }, { name = "requests" }, + { name = "ruff" }, { name = "sentry-sdk" }, { name = "werkzeug" }, { name = "whitenoise" }, @@ -1984,10 +2021,12 @@ requires-dist = [ { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-django", specifier = ">=4.9.0" }, { name = "pytest-playwright", specifier = ">=0.4.3" }, + { name = "python-decouple", specifier = ">=3.8" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "python-json-logger", url = "https://github.com/nhairs/python-json-logger/releases/download/v3.0.0/python_json_logger-3.0.0-py3-none-any.whl" }, { name = "redis", specifier = ">=6.4.0" }, { name = "requests", specifier = ">=2.32.3" }, + { name = "ruff", specifier = ">=0.12.10" }, { name = "sentry-sdk", specifier = ">=1.40.0" }, { name = "werkzeug", specifier = ">=3.1.3" }, { name = "whitenoise", specifier = ">=6.6.0" }, @@ -2013,11 +2052,11 @@ wheels = [ [[package]] name = "trove-classifiers" -version = "2025.8.6.13" +version = "2025.8.26.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/21/707af14daa638b0df15b5d5700349e0abdd3e5140069f9ab6e0ccb922806/trove_classifiers-2025.8.6.13.tar.gz", hash = "sha256:5a0abad839d2ed810f213ab133d555d267124ddea29f1d8a50d6eca12a50ae6e", size = 16932, upload-time = "2025-08-06T13:26:26.479Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/7c/78ea329adc8be4353f9ef8ee5b7498450fcbd1a02fed6cd444344eb0bf63/trove_classifiers-2025.8.26.11.tar.gz", hash = "sha256:e73efff317c492a7990092f9c12676c705bf6cfe40a258a93f63f4b4c9941432", size = 16960, upload-time = "2025-08-26T11:30:12.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/44/323a87d78f04d5329092aada803af3612dd004a64b69ba8b13046601a8c9/trove_classifiers-2025.8.6.13-py3-none-any.whl", hash = "sha256:c4e7fc83012770d80b3ae95816111c32b085716374dccee0d3fbf5c235495f9f", size = 14121, upload-time = "2025-08-06T13:26:25.063Z" }, + { url = "https://files.pythonhosted.org/packages/4a/40/d54944eeb5646fb4b1c98d4601fe5e0812dd2e7c0aa94d53fc46457effc8/trove_classifiers-2025.8.26.11-py3-none-any.whl", hash = "sha256:887fb0a402bdbecd4415a52c06e6728f8bdaa506a7143372d2b893e2c5e2d859", size = 14140, upload-time = "2025-08-26T11:30:11.427Z" }, ] [[package]] diff --git a/context_portal/context.db b/context_portal/context.db index 644be008..82d337c7 100644 Binary files a/context_portal/context.db and b/context_portal/context.db differ