Add migrations for ParkPhoto and RidePhoto models with associated events

- Created ParkPhoto and ParkPhotoEvent models in the parks app, including fields for image, caption, alt text, and relationships to the Park model.
- Implemented triggers for insert and update operations on ParkPhoto to log changes in ParkPhotoEvent.
- Created RidePhoto and RidePhotoEvent models in the rides app, with similar structure and functionality as ParkPhoto.
- Added fields for photo type in RidePhoto and implemented corresponding triggers for logging changes.
- Established necessary indexes and unique constraints for both models to ensure data integrity and optimize queries.
This commit is contained in:
pacnpal
2025-08-26 14:40:46 -04:00
parent 831be6a2ee
commit e4e36c7899
133 changed files with 1321 additions and 1001 deletions

View File

@@ -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",
]

View File

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

View File

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

View File

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