mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 15:51:08 -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:
@@ -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)
|
||||
|
||||
@@ -7,7 +7,6 @@ import pghistory
|
||||
|
||||
@pghistory.track()
|
||||
class ParkReview(TrackedModel):
|
||||
|
||||
# Import managers
|
||||
from ..managers import ParkReviewManager
|
||||
|
||||
|
||||
@@ -3,5 +3,11 @@ from .park_management import ParkService
|
||||
from .location_service import ParkLocationService
|
||||
from .filter_service import ParkFilterService
|
||||
from .media_service import ParkMediaService
|
||||
__all__ = ["RoadTripService", "ParkService",
|
||||
"ParkLocationService", "ParkFilterService", "ParkMediaService"]
|
||||
|
||||
__all__ = [
|
||||
"RoadTripService",
|
||||
"ParkService",
|
||||
"ParkLocationService",
|
||||
"ParkFilterService",
|
||||
"ParkMediaService",
|
||||
]
|
||||
|
||||
@@ -4,8 +4,7 @@ Handles geocoding, reverse geocoding, and location search for parks.
|
||||
"""
|
||||
|
||||
import requests
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from django.conf import settings
|
||||
from typing import List, Dict, Any, Optional
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
import logging
|
||||
|
||||
@@ -27,7 +27,7 @@ class ParkMediaService:
|
||||
caption: str = "",
|
||||
alt_text: str = "",
|
||||
is_primary: bool = False,
|
||||
auto_approve: bool = False
|
||||
auto_approve: bool = False,
|
||||
) -> ParkPhoto:
|
||||
"""
|
||||
Upload a photo for a park.
|
||||
@@ -64,7 +64,7 @@ class ParkMediaService:
|
||||
alt_text=alt_text,
|
||||
is_primary=is_primary,
|
||||
is_approved=auto_approve,
|
||||
uploaded_by=user
|
||||
uploaded_by=user,
|
||||
)
|
||||
|
||||
# Extract EXIF date
|
||||
@@ -77,9 +77,7 @@ class ParkMediaService:
|
||||
|
||||
@staticmethod
|
||||
def get_park_photos(
|
||||
park: Park,
|
||||
approved_only: bool = True,
|
||||
primary_first: bool = True
|
||||
park: Park, approved_only: bool = True, primary_first: bool = True
|
||||
) -> List[ParkPhoto]:
|
||||
"""
|
||||
Get photos for a park.
|
||||
@@ -98,9 +96,9 @@ class ParkMediaService:
|
||||
queryset = queryset.filter(is_approved=True)
|
||||
|
||||
if primary_first:
|
||||
queryset = queryset.order_by('-is_primary', '-created_at')
|
||||
queryset = queryset.order_by("-is_primary", "-created_at")
|
||||
else:
|
||||
queryset = queryset.order_by('-created_at')
|
||||
queryset = queryset.order_by("-created_at")
|
||||
|
||||
return list(queryset)
|
||||
|
||||
@@ -190,7 +188,8 @@ class ParkMediaService:
|
||||
photo.delete()
|
||||
|
||||
logger.info(
|
||||
f"Photo {photo_id} deleted from park {park_slug} by user {deleted_by.username}")
|
||||
f"Photo {photo_id} deleted from park {park_slug} by user {deleted_by.username}"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete photo {photo.pk}: {str(e)}")
|
||||
@@ -214,7 +213,7 @@ class ParkMediaService:
|
||||
"approved_photos": photos.filter(is_approved=True).count(),
|
||||
"pending_photos": photos.filter(is_approved=False).count(),
|
||||
"has_primary": photos.filter(is_primary=True).exists(),
|
||||
"recent_uploads": photos.order_by('-created_at')[:5].count()
|
||||
"recent_uploads": photos.order_by("-created_at")[:5].count(),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -237,5 +236,6 @@ class ParkMediaService:
|
||||
approved_count += 1
|
||||
|
||||
logger.info(
|
||||
f"Bulk approved {approved_count} photos by user {approved_by.username}")
|
||||
f"Bulk approved {approved_count} photos by user {approved_by.username}"
|
||||
)
|
||||
return approved_count
|
||||
|
||||
@@ -192,8 +192,7 @@ class RoadTripService:
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
raise OSMAPIException(
|
||||
f"Failed to make request after {
|
||||
self.max_retries} attempts: {e}"
|
||||
f"Failed to make request after {self.max_retries} attempts: {e}"
|
||||
)
|
||||
|
||||
def geocode_address(self, address: str) -> Optional[Coordinates]:
|
||||
@@ -244,9 +243,7 @@ class RoadTripService:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Geocoded '{address}' to {
|
||||
coords.latitude}, {
|
||||
coords.longitude}"
|
||||
f"Geocoded '{address}' to {coords.latitude}, {coords.longitude}"
|
||||
)
|
||||
return coords
|
||||
else:
|
||||
@@ -274,22 +271,18 @@ class RoadTripService:
|
||||
return None
|
||||
|
||||
# Check cache first
|
||||
cache_key = f"roadtrip:route:{
|
||||
start_coords.latitude},{
|
||||
start_coords.longitude}:{
|
||||
end_coords.latitude},{
|
||||
end_coords.longitude}"
|
||||
cache_key = f"roadtrip:route:{start_coords.latitude},{start_coords.longitude}:{
|
||||
end_coords.latitude
|
||||
},{end_coords.longitude}"
|
||||
cached_result = cache.get(cache_key)
|
||||
if cached_result:
|
||||
return RouteInfo(**cached_result)
|
||||
|
||||
try:
|
||||
# Format coordinates for OSRM (lon,lat format)
|
||||
coords_string = f"{
|
||||
start_coords.longitude},{
|
||||
start_coords.latitude};{
|
||||
end_coords.longitude},{
|
||||
end_coords.latitude}"
|
||||
coords_string = f"{start_coords.longitude},{start_coords.latitude};{
|
||||
end_coords.longitude
|
||||
},{end_coords.latitude}"
|
||||
url = f"{self.osrm_base_url}/{coords_string}"
|
||||
|
||||
params = {
|
||||
@@ -326,9 +319,9 @@ class RoadTripService:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Route calculated: {
|
||||
route_info.formatted_distance}, {
|
||||
route_info.formatted_duration}"
|
||||
f"Route calculated: {route_info.formatted_distance}, {
|
||||
route_info.formatted_duration
|
||||
}"
|
||||
)
|
||||
return route_info
|
||||
else:
|
||||
@@ -350,11 +343,13 @@ class RoadTripService:
|
||||
Calculate straight-line distance as fallback when routing fails.
|
||||
"""
|
||||
# Haversine formula for great-circle distance
|
||||
lat1, lon1 = math.radians(start_coords.latitude), math.radians(
|
||||
start_coords.longitude
|
||||
lat1, lon1 = (
|
||||
math.radians(start_coords.latitude),
|
||||
math.radians(start_coords.longitude),
|
||||
)
|
||||
lat2, lon2 = math.radians(end_coords.latitude), math.radians(
|
||||
end_coords.longitude
|
||||
lat2, lon2 = (
|
||||
math.radians(end_coords.latitude),
|
||||
math.radians(end_coords.longitude),
|
||||
)
|
||||
|
||||
dlat = lat2 - lat1
|
||||
@@ -696,10 +691,7 @@ class RoadTripService:
|
||||
location.set_coordinates(coords.latitude, coords.longitude)
|
||||
location.save()
|
||||
logger.info(
|
||||
f"Geocoded park '{
|
||||
park.name}' to {
|
||||
coords.latitude}, {
|
||||
coords.longitude}"
|
||||
f"Geocoded park '{park.name}' to {coords.latitude}, {coords.longitude}"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -165,7 +165,8 @@ class ParkAreaModelTests(TestCase):
|
||||
with transaction.atomic():
|
||||
with self.assertRaises(IntegrityError):
|
||||
ParkArea.objects.create(
|
||||
park=self.park, name="Test Area" # Will generate same slug
|
||||
park=self.park,
|
||||
name="Test Area", # Will generate same slug
|
||||
)
|
||||
|
||||
# Should be able to use same name in different park
|
||||
|
||||
@@ -551,14 +551,12 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
image=photo_file,
|
||||
uploaded_by=self.request.user,
|
||||
park=self.object,
|
||||
) )
|
||||
)
|
||||
uploaded_count += 1
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
self.request,
|
||||
f"Error uploading photo {
|
||||
photo_file.name}: {
|
||||
str(e)}",
|
||||
f"Error uploading photo {photo_file.name}: {str(e)}",
|
||||
)
|
||||
|
||||
messages.success(
|
||||
@@ -571,7 +569,8 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
messages.error(
|
||||
self.request,
|
||||
f"Error creating park: {
|
||||
str(e)}. Please check your input and try again.",
|
||||
str(e)
|
||||
}. Please check your input and try again.",
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
|
||||
@@ -727,9 +726,7 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
self.request,
|
||||
f"Error uploading photo {
|
||||
photo_file.name}: {
|
||||
str(e)}",
|
||||
f"Error uploading photo {photo_file.name}: {str(e)}",
|
||||
)
|
||||
|
||||
messages.success(
|
||||
@@ -742,7 +739,8 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
|
||||
messages.error(
|
||||
self.request,
|
||||
f"Error updating park: {
|
||||
str(e)}. Please check your input and try again.",
|
||||
str(e)
|
||||
}. Please check your input and try again.",
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.urls import reverse
|
||||
from .models import Park
|
||||
from .services.roadtrip import RoadTripService
|
||||
from apps.core.services.map_service import unified_map_service
|
||||
from apps.core.services.data_structures import LocationType, MapFilters
|
||||
from apps.core.services.data_structures import LocationType
|
||||
|
||||
JSON_DECODE_ERROR_MSG = "Invalid JSON data"
|
||||
PARKS_ALONG_ROUTE_HTML = "parks/partials/parks_along_route.html"
|
||||
|
||||
@@ -346,9 +346,9 @@ class RideForm(forms.ModelForm):
|
||||
# editing
|
||||
if self.instance and self.instance.pk:
|
||||
if self.instance.manufacturer:
|
||||
self.fields["manufacturer_search"].initial = (
|
||||
self.instance.manufacturer.name
|
||||
)
|
||||
self.fields[
|
||||
"manufacturer_search"
|
||||
].initial = self.instance.manufacturer.name
|
||||
self.fields["manufacturer"].initial = self.instance.manufacturer
|
||||
if self.instance.designer:
|
||||
self.fields["designer_search"].initial = self.instance.designer.name
|
||||
|
||||
@@ -346,9 +346,9 @@ class RideForm(forms.ModelForm):
|
||||
# editing
|
||||
if self.instance and self.instance.pk:
|
||||
if self.instance.manufacturer:
|
||||
self.fields["manufacturer_search"].initial = (
|
||||
self.instance.manufacturer.name
|
||||
)
|
||||
self.fields[
|
||||
"manufacturer_search"
|
||||
].initial = self.instance.manufacturer.name
|
||||
self.fields["manufacturer"].initial = self.instance.manufacturer
|
||||
if self.instance.designer:
|
||||
self.fields["designer_search"].initial = self.instance.designer.name
|
||||
|
||||
@@ -11,7 +11,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 = [
|
||||
("parks", "0003_add_business_constraints"),
|
||||
("rides", "0001_initial"),
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("rides", "0002_add_business_constraints"),
|
||||
]
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0006_remove_company_insert_insert_and_more"),
|
||||
("pghistory", "0007_auto_20250421_0444"),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user