mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 05:11:09 -05:00
Add migrations for ParkPhoto and RidePhoto models with associated events
- Created ParkPhoto and ParkPhotoEvent models in the parks app, including fields for image, caption, alt text, and relationships to the Park model. - Implemented triggers for insert and update operations on ParkPhoto to log changes in ParkPhotoEvent. - Created RidePhoto and RidePhotoEvent models in the rides app, with similar structure and functionality as ParkPhoto. - Added fields for photo type in RidePhoto and implemented corresponding triggers for logging changes. - Established necessary indexes and unique constraints for both models to ensure data integrity and optimize queries.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -115,3 +115,5 @@ temp/
|
||||
/uploads/
|
||||
/backups/
|
||||
.django_tailwind_cli/
|
||||
backend/.env
|
||||
frontend/.env
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
"""
|
||||
|
||||
@@ -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")),
|
||||
]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)),
|
||||
]
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Park API URLs for ThrillWiki API v1.
|
||||
"""
|
||||
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Ride API URLs for ThrillWiki API v1.
|
||||
"""
|
||||
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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")),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())}"
|
||||
)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}")
|
||||
)
|
||||
|
||||
@@ -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."))
|
||||
|
||||
@@ -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)}"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)}"
|
||||
)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -11,7 +11,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0001_initial"),
|
||||
]
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Park API URLs for ThrillWiki API v1.
|
||||
"""
|
||||
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Park API views for ThrillWiki API v1.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Ride API URLs for ThrillWiki API v1.
|
||||
"""
|
||||
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Ride API views for ThrillWiki API v1.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -7,9 +7,7 @@ miscellaneous functionality.
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema_serializer,
|
||||
extend_schema_field,
|
||||
OpenApiExample,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("core", "0001_initial"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("email_service", "0001_initial"),
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -8,7 +8,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("moderation", "0001_initial"),
|
||||
]
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -11,7 +11,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0001_initial"),
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0001_add_filter_indexes"),
|
||||
("parks", "0004_fix_pghistory_triggers"),
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0005_merge_20250820_2020"),
|
||||
]
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from .parks import Park
|
||||
|
||||
@pghistory.track()
|
||||
class ParkArea(TrackedModel):
|
||||
|
||||
# Import managers
|
||||
from ..managers import ParkAreaManager
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user