feat: Add blog, media, and support apps, implement ride credits and image API, and remove toplist feature.

This commit is contained in:
pacnpal
2025-12-26 15:15:28 -05:00
parent cd8868a591
commit 00699d53b4
77 changed files with 7274 additions and 538 deletions

View File

@@ -11,6 +11,7 @@ from django.http import HttpRequest, HttpResponseBase
from django.utils.decorators import method_decorator
from django.views.decorators.vary import vary_on_headers
from django.views import View
from rest_framework.response import Response as DRFResponse
from apps.core.services.enhanced_cache_service import EnhancedCacheService
import logging
@@ -81,6 +82,14 @@ def cache_api_response(
"cache_hit": True,
},
)
# If cached data is our dict format for DRF responses, reconstruct it
if isinstance(cached_response, dict) and '__drf_data__' in cached_response:
return DRFResponse(
data=cached_response['__drf_data__'],
status=cached_response.get('status', 200)
)
return cached_response
# Execute view and cache result
@@ -90,8 +99,18 @@ def cache_api_response(
# Only cache successful responses
if hasattr(response, "status_code") and response.status_code == 200:
# For DRF responses, we must cache the data, not the response object
# because the response object is not rendered yet and cannot be pickled
if hasattr(response, 'data'):
cache_payload = {
'__drf_data__': response.data,
'status': response.status_code
}
else:
cache_payload = response
getattr(cache_service, cache_backend + "_cache").set(
cache_key, response, timeout
cache_key, cache_payload, timeout
)
logger.debug(
f"Cached API response for view {view_func.__name__}",

View File

@@ -16,3 +16,13 @@ class IsOwnerOrReadOnly(permissions.BasePermission):
if hasattr(obj, 'user'):
return obj.user == request.user
return False
class IsStaffOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow staff to edit it.
"""
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return request.user and request.user.is_staff

View File

@@ -229,7 +229,6 @@ class EntityFuzzyMatcher:
parks = Park.objects.filter(
Q(name__icontains=query)
| Q(slug__icontains=query.lower().replace(" ", "-"))
| Q(former_names__icontains=query)
)[: self.MAX_CANDIDATES]
for park in parks:
@@ -249,7 +248,6 @@ class EntityFuzzyMatcher:
rides = Ride.objects.select_related("park").filter(
Q(name__icontains=query)
| Q(slug__icontains=query.lower().replace(" ", "-"))
| Q(former_names__icontains=query)
| Q(park__name__icontains=query)
)[: self.MAX_CANDIDATES]

View File

@@ -0,0 +1,54 @@
import pytest
from django.contrib.auth import get_user_model
from apps.parks.models import Park, Company
import pghistory
User = get_user_model()
@pytest.mark.django_db
class TestTrackedModel:
"""
Tests for the TrackedModel base class and pghistory integration.
"""
def test_create_history_tracking(self):
"""Test that creating a model instance creates a history event."""
user = User.objects.create_user(username="testuser", password="password")
company = Company.objects.create(name="Test Operator", roles=["OPERATOR"])
with pghistory.context(user=user.id):
park = Park.objects.create(
name="History Test Park",
description="Testing history",
operating_season="Summer",
operator=company
)
# Verify history using the helper method from TrackedModel
events = park.get_history()
assert events.count() == 1
event = events.first()
assert event.pgh_obj_id == park.pk
# Verify context was captured
# The middleware isn't running here, so we used pghistory.context explicitly
# But pghistory.context stores data in pgh_context field if configured?
# Let's check if the event has pgh_context
assert event.pgh_context["user"] == user.id
def test_update_tracking(self):
company = Company.objects.create(name="Test Operator 2", roles=["OPERATOR"])
park = Park.objects.create(name="Original", operator=company)
# Initial create event
assert park.get_history().count() == 1
# Update
park.name = "Updated"
park.save()
assert park.get_history().count() == 2
latest = park.get_history().first() # Ordered by -pgh_created_at
assert latest.name == "Updated"

View File

@@ -0,0 +1,53 @@
import requests
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
import logging
logger = logging.getLogger(__name__)
def get_direct_upload_url(user_id=None):
"""
Generates a direct upload URL for Cloudflare Images.
Args:
user_id (str, optional): The user ID to associate with the upload.
Returns:
dict: A dictionary containing 'id' and 'uploadURL'.
Raises:
ImproperlyConfigured: If Cloudflare settings are missing.
requests.RequestException: If the Cloudflare API request fails.
"""
account_id = getattr(settings, 'CLOUDFLARE_IMAGES_ACCOUNT_ID', None)
api_token = getattr(settings, 'CLOUDFLARE_IMAGES_API_TOKEN', None)
if not account_id or not api_token:
raise ImproperlyConfigured(
"CLOUDFLARE_IMAGES_ACCOUNT_ID and CLOUDFLARE_IMAGES_API_TOKEN must be set."
)
url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v2/direct_upload"
headers = {
"Authorization": f"Bearer {api_token}",
}
data = {
"requireSignedURLs": "false",
}
if user_id:
data["metadata"] = f'{{"user_id": "{user_id}"}}'
response = requests.post(url, headers=headers, data=data)
response.raise_for_status()
result = response.json()
if not result.get("success"):
error_msg = result.get("errors", [{"message": "Unknown error"}])[0].get("message")
logger.error(f"Cloudflare Direct Upload Error: {error_msg}")
raise requests.RequestException(f"Cloudflare Error: {error_msg}")
return result.get("result", {})