mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 10:11:08 -05:00
Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -207,7 +207,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write("Creating parks...")
|
||||
|
||||
# Park creation data - will be used to create parks in the database
|
||||
# TODO(THRILLWIKI-111): Complete park creation implementation
|
||||
# See FUTURE_WORK.md - THRILLWIKI-111 for implementation plan
|
||||
parks_data = [
|
||||
{
|
||||
"name": "Magic Kingdom",
|
||||
|
||||
@@ -13,12 +13,23 @@ class ParkArea(TrackedModel):
|
||||
|
||||
objects = ParkAreaManager()
|
||||
id: int # Type hint for Django's automatic id field
|
||||
park = models.ForeignKey(Park, on_delete=models.CASCADE, related_name="areas")
|
||||
name = models.CharField(max_length=255)
|
||||
slug = models.SlugField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
opening_date = models.DateField(null=True, blank=True)
|
||||
closing_date = models.DateField(null=True, blank=True)
|
||||
park = models.ForeignKey(
|
||||
Park,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="areas",
|
||||
help_text="Park this area belongs to",
|
||||
)
|
||||
name = models.CharField(max_length=255, help_text="Name of the park area")
|
||||
slug = models.SlugField(
|
||||
max_length=255, help_text="URL-friendly identifier (unique within park)"
|
||||
)
|
||||
description = models.TextField(blank=True, help_text="Detailed description of the area")
|
||||
opening_date = models.DateField(
|
||||
null=True, blank=True, help_text="Date this area opened"
|
||||
)
|
||||
closing_date = models.DateField(
|
||||
null=True, blank=True, help_text="Date this area closed (if applicable)"
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
@@ -28,5 +39,8 @@ class ParkArea(TrackedModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
class Meta(TrackedModel.Meta):
|
||||
verbose_name = "Park Area"
|
||||
verbose_name_plural = "Park Areas"
|
||||
ordering = ["park", "name"]
|
||||
unique_together = ("park", "slug")
|
||||
|
||||
@@ -13,20 +13,27 @@ class Company(TrackedModel):
|
||||
|
||||
objects = CompanyManager()
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
slug = models.SlugField(max_length=255, unique=True)
|
||||
name = models.CharField(max_length=255, help_text="Company name")
|
||||
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
|
||||
roles = ArrayField(
|
||||
RichChoiceField(choice_group="company_roles", domain="parks", max_length=20),
|
||||
default=list,
|
||||
blank=True,
|
||||
help_text="Company roles (operator, manufacturer, etc.)",
|
||||
)
|
||||
description = models.TextField(blank=True)
|
||||
website = models.URLField(blank=True)
|
||||
description = models.TextField(blank=True, help_text="Detailed company description")
|
||||
website = models.URLField(blank=True, help_text="Company website URL")
|
||||
|
||||
# Operator-specific fields
|
||||
founded_year = models.PositiveIntegerField(blank=True, null=True)
|
||||
parks_count = models.IntegerField(default=0)
|
||||
rides_count = models.IntegerField(default=0)
|
||||
founded_year = models.PositiveIntegerField(
|
||||
blank=True, null=True, help_text="Year the company was founded"
|
||||
)
|
||||
parks_count = models.IntegerField(
|
||||
default=0, help_text="Number of parks operated (auto-calculated)"
|
||||
)
|
||||
rides_count = models.IntegerField(
|
||||
default=0, help_text="Number of rides manufactured (auto-calculated)"
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
@@ -38,8 +45,9 @@ class Company(TrackedModel):
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
app_label = "parks"
|
||||
ordering = ["name"]
|
||||
verbose_name = "Company"
|
||||
verbose_name_plural = "Companies"
|
||||
ordering = ["name"]
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
@@ -51,7 +59,10 @@ class CompanyHeadquarters(models.Model):
|
||||
|
||||
# Relationships
|
||||
company = models.OneToOneField(
|
||||
"Company", on_delete=models.CASCADE, related_name="headquarters"
|
||||
"Company",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="headquarters",
|
||||
help_text="Company this headquarters belongs to",
|
||||
)
|
||||
|
||||
# Address Fields (No coordinates needed)
|
||||
|
||||
@@ -30,7 +30,10 @@ 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",
|
||||
help_text="Park this photo belongs to",
|
||||
)
|
||||
|
||||
image = models.ForeignKey(
|
||||
@@ -39,10 +42,18 @@ class ParkPhoto(TrackedModel):
|
||||
help_text="Park photo stored on Cloudflare Images"
|
||||
)
|
||||
|
||||
caption = models.CharField(max_length=255, blank=True)
|
||||
alt_text = models.CharField(max_length=255, blank=True)
|
||||
is_primary = models.BooleanField(default=False)
|
||||
is_approved = models.BooleanField(default=False)
|
||||
caption = models.CharField(
|
||||
max_length=255, blank=True, help_text="Photo caption or description"
|
||||
)
|
||||
alt_text = models.CharField(
|
||||
max_length=255, blank=True, help_text="Alternative text for accessibility"
|
||||
)
|
||||
is_primary = models.BooleanField(
|
||||
default=False, help_text="Whether this is the primary photo for the park"
|
||||
)
|
||||
is_approved = models.BooleanField(
|
||||
default=False, help_text="Whether this photo has been approved by moderators"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
@@ -55,10 +66,13 @@ class ParkPhoto(TrackedModel):
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name="uploaded_park_photos",
|
||||
help_text="User who uploaded this photo",
|
||||
)
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
app_label = "parks"
|
||||
verbose_name = "Park Photo"
|
||||
verbose_name_plural = "Park Photos"
|
||||
ordering = ["-is_primary", "-created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=["park", "is_primary"]),
|
||||
|
||||
@@ -24,9 +24,9 @@ class Park(StateMachineMixin, TrackedModel):
|
||||
objects = ParkManager()
|
||||
id: int # Type hint for Django's automatic id field
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
slug = models.SlugField(max_length=255, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
name = models.CharField(max_length=255, help_text="Park name")
|
||||
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
|
||||
description = models.TextField(blank=True, help_text="Park description")
|
||||
state_field_name = "status"
|
||||
|
||||
status = RichFSMField(
|
||||
@@ -50,20 +50,20 @@ class Park(StateMachineMixin, TrackedModel):
|
||||
# ParkLocation
|
||||
|
||||
# Details
|
||||
opening_date = models.DateField(null=True, blank=True)
|
||||
closing_date = models.DateField(null=True, blank=True)
|
||||
operating_season = models.CharField(max_length=255, blank=True)
|
||||
opening_date = models.DateField(null=True, blank=True, help_text="Opening date")
|
||||
closing_date = models.DateField(null=True, blank=True, help_text="Closing date")
|
||||
operating_season = models.CharField(max_length=255, blank=True, help_text="Operating season")
|
||||
size_acres = models.DecimalField(
|
||||
max_digits=10, decimal_places=2, null=True, blank=True
|
||||
max_digits=10, decimal_places=2, null=True, blank=True, help_text="Park size in acres"
|
||||
)
|
||||
website = models.URLField(blank=True)
|
||||
website = models.URLField(blank=True, help_text="Official website URL")
|
||||
|
||||
# Statistics
|
||||
average_rating = models.DecimalField(
|
||||
max_digits=3, decimal_places=2, null=True, blank=True
|
||||
max_digits=3, decimal_places=2, null=True, blank=True, help_text="Average user rating (1–10)"
|
||||
)
|
||||
ride_count = models.IntegerField(null=True, blank=True)
|
||||
coaster_count = models.IntegerField(null=True, blank=True)
|
||||
ride_count = models.IntegerField(null=True, blank=True, help_text="Total ride count")
|
||||
coaster_count = models.IntegerField(null=True, blank=True, help_text="Total coaster count")
|
||||
|
||||
# Image settings - references to existing photos
|
||||
banner_image = models.ForeignKey(
|
||||
@@ -133,6 +133,8 @@ class Park(StateMachineMixin, TrackedModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Park"
|
||||
verbose_name_plural = "Parks"
|
||||
ordering = ["name"]
|
||||
constraints = [
|
||||
# Business rule: Closing date must be after opening date
|
||||
|
||||
@@ -15,35 +15,51 @@ class ParkReview(TrackedModel):
|
||||
A review of a park.
|
||||
"""
|
||||
park = models.ForeignKey(
|
||||
"parks.Park", on_delete=models.CASCADE, related_name="reviews"
|
||||
"parks.Park",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="reviews",
|
||||
help_text="Park being reviewed",
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
"accounts.User", on_delete=models.CASCADE, related_name="park_reviews"
|
||||
"accounts.User",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="park_reviews",
|
||||
help_text="User who wrote the review",
|
||||
)
|
||||
rating = models.PositiveSmallIntegerField(
|
||||
validators=[MinValueValidator(1), MaxValueValidator(10)]
|
||||
validators=[MinValueValidator(1), MaxValueValidator(10)],
|
||||
help_text="Rating from 1-10",
|
||||
)
|
||||
title = models.CharField(max_length=200)
|
||||
content = models.TextField()
|
||||
visit_date = models.DateField()
|
||||
title = models.CharField(max_length=200, help_text="Review title")
|
||||
content = models.TextField(help_text="Review content")
|
||||
visit_date = models.DateField(help_text="Date the user visited the park")
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
# Moderation
|
||||
is_published = models.BooleanField(default=True)
|
||||
moderation_notes = models.TextField(blank=True)
|
||||
is_published = models.BooleanField(
|
||||
default=True, help_text="Whether this review is publicly visible"
|
||||
)
|
||||
moderation_notes = models.TextField(
|
||||
blank=True, help_text="Internal notes from moderators"
|
||||
)
|
||||
moderated_by = models.ForeignKey(
|
||||
"accounts.User",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="moderated_park_reviews",
|
||||
help_text="Moderator who reviewed this",
|
||||
)
|
||||
moderated_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When this review was moderated"
|
||||
)
|
||||
moderated_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
class Meta(TrackedModel.Meta):
|
||||
verbose_name = "Park Review"
|
||||
verbose_name_plural = "Park Reviews"
|
||||
ordering = ["-created_at"]
|
||||
unique_together = ["park", "user"]
|
||||
constraints = [
|
||||
|
||||
156
backend/apps/parks/tests/test_admin.py
Normal file
156
backend/apps/parks/tests/test_admin.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
Tests for parks admin interfaces.
|
||||
|
||||
These tests verify the functionality of park, area, company, location,
|
||||
and review admin classes including query optimization and custom actions.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from apps.parks.admin import (
|
||||
CompanyAdmin,
|
||||
CompanyHeadquartersAdmin,
|
||||
ParkAdmin,
|
||||
ParkAreaAdmin,
|
||||
ParkLocationAdmin,
|
||||
ParkReviewAdmin,
|
||||
)
|
||||
from apps.parks.models import Company, CompanyHeadquarters, Park, ParkArea, ParkLocation, ParkReview
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class TestParkAdmin(TestCase):
|
||||
"""Tests for ParkAdmin class."""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.site = AdminSite()
|
||||
self.admin = ParkAdmin(model=Park, admin_site=self.site)
|
||||
|
||||
def test_list_display_fields(self):
|
||||
"""Verify all required fields are in list_display."""
|
||||
required_fields = [
|
||||
"name",
|
||||
"formatted_location",
|
||||
"status_badge",
|
||||
"operator_link",
|
||||
"ride_count",
|
||||
"average_rating",
|
||||
"created_at",
|
||||
]
|
||||
for field in required_fields:
|
||||
assert field in self.admin.list_display
|
||||
|
||||
def test_list_select_related(self):
|
||||
"""Verify select_related is configured for ForeignKeys."""
|
||||
assert "operator" in self.admin.list_select_related
|
||||
assert "property_owner" in self.admin.list_select_related
|
||||
assert "location" in self.admin.list_select_related
|
||||
|
||||
def test_list_prefetch_related(self):
|
||||
"""Verify prefetch_related is configured for reverse relations."""
|
||||
assert "areas" in self.admin.list_prefetch_related
|
||||
assert "rides" in self.admin.list_prefetch_related
|
||||
|
||||
def test_search_fields_include_relations(self):
|
||||
"""Verify search includes related object fields."""
|
||||
assert "location__city" in self.admin.search_fields
|
||||
assert "operator__name" in self.admin.search_fields
|
||||
|
||||
def test_export_fields_configured(self):
|
||||
"""Verify export fields are configured."""
|
||||
assert hasattr(self.admin, "export_fields")
|
||||
assert "id" in self.admin.export_fields
|
||||
assert "name" in self.admin.export_fields
|
||||
|
||||
def test_actions_registered(self):
|
||||
"""Verify custom actions are registered."""
|
||||
request = self.factory.get("/admin/")
|
||||
request.user = User(is_superuser=True)
|
||||
|
||||
actions = self.admin.get_actions(request)
|
||||
assert "bulk_activate" in actions
|
||||
assert "bulk_deactivate" in actions
|
||||
assert "export_to_csv" in actions
|
||||
|
||||
|
||||
class TestParkAreaAdmin(TestCase):
|
||||
"""Tests for ParkAreaAdmin class."""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.site = AdminSite()
|
||||
self.admin = ParkAreaAdmin(model=ParkArea, admin_site=self.site)
|
||||
|
||||
def test_list_select_related(self):
|
||||
"""Verify select_related for park."""
|
||||
assert "park" in self.admin.list_select_related
|
||||
|
||||
def test_list_prefetch_related(self):
|
||||
"""Verify prefetch_related for rides."""
|
||||
assert "rides" in self.admin.list_prefetch_related
|
||||
|
||||
|
||||
class TestParkLocationAdmin(TestCase):
|
||||
"""Tests for ParkLocationAdmin class."""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.site = AdminSite()
|
||||
self.admin = ParkLocationAdmin(model=ParkLocation, admin_site=self.site)
|
||||
|
||||
def test_list_select_related(self):
|
||||
"""Verify select_related for park."""
|
||||
assert "park" in self.admin.list_select_related
|
||||
|
||||
def test_readonly_coordinates(self):
|
||||
"""Verify coordinate fields are readonly."""
|
||||
assert "latitude" in self.admin.readonly_fields
|
||||
assert "longitude" in self.admin.readonly_fields
|
||||
|
||||
|
||||
class TestCompanyAdmin(TestCase):
|
||||
"""Tests for CompanyAdmin class."""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.site = AdminSite()
|
||||
self.admin = CompanyAdmin(model=Company, admin_site=self.site)
|
||||
|
||||
def test_list_prefetch_related(self):
|
||||
"""Verify prefetch_related for related parks."""
|
||||
assert "operated_parks" in self.admin.list_prefetch_related
|
||||
assert "owned_parks" in self.admin.list_prefetch_related
|
||||
|
||||
|
||||
class TestParkReviewAdmin(TestCase):
|
||||
"""Tests for ParkReviewAdmin class."""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.site = AdminSite()
|
||||
self.admin = ParkReviewAdmin(model=ParkReview, admin_site=self.site)
|
||||
|
||||
def test_list_select_related(self):
|
||||
"""Verify select_related for user and park."""
|
||||
assert "park" in self.admin.list_select_related
|
||||
assert "user" in self.admin.list_select_related
|
||||
assert "moderated_by" in self.admin.list_select_related
|
||||
|
||||
def test_moderation_actions_registered(self):
|
||||
"""Verify moderation actions are registered."""
|
||||
request = self.factory.get("/admin/")
|
||||
request.user = User(is_superuser=True)
|
||||
|
||||
actions = self.admin.get_actions(request)
|
||||
assert "bulk_approve" in actions
|
||||
assert "bulk_reject" in actions
|
||||
|
||||
def test_readonly_moderation_fields(self):
|
||||
"""Verify moderation fields are readonly."""
|
||||
assert "moderated_by" in self.admin.readonly_fields
|
||||
assert "moderated_at" in self.admin.readonly_fields
|
||||
@@ -1,5 +1,39 @@
|
||||
# Park Search Tests
|
||||
|
||||
## Why These Tests Are Disabled
|
||||
|
||||
These tests were disabled because they need updating to work with the new `ParkLocation` model instead of the generic `Location` model. The model refactoring changed how location data is stored and accessed for parks.
|
||||
|
||||
## Re-enabling These Tests
|
||||
|
||||
To re-enable these tests, follow these steps:
|
||||
|
||||
1. **Update model imports** in `test_filters.py` and `test_models.py`:
|
||||
- Replace `from apps.locations.models import Location` with `from apps.parks.models import ParkLocation`
|
||||
- Update any other location-related imports
|
||||
|
||||
2. **Update test fixtures** to use `ParkLocation` instead of `Location`:
|
||||
- Change factory classes to create `ParkLocation` instances
|
||||
- Update fixture data to match the new model structure
|
||||
|
||||
3. **Update assertions** to match new model structure:
|
||||
- Adjust field references (e.g., `park.location` may now be `park.park_location`)
|
||||
- Update any serializer-based assertions
|
||||
|
||||
4. **Move files** back to the active test directory:
|
||||
```bash
|
||||
mv backend/apps/parks/tests_disabled/*.py backend/apps/parks/tests/
|
||||
```
|
||||
|
||||
5. **Run tests** to verify they pass:
|
||||
```bash
|
||||
uv run pytest backend/apps/parks/tests/
|
||||
```
|
||||
|
||||
**Tracking**: See TODO(THRILLWIKI-XXX) for tracking issue
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Test suite for the park search functionality including:
|
||||
|
||||
@@ -33,6 +33,11 @@ from django.views.decorators.http import require_POST
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from apps.core.logging import log_exception, log_business_event
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Constants
|
||||
PARK_DETAIL_URL = "parks:park_detail"
|
||||
@@ -285,6 +290,12 @@ class ParkListView(HTMXFilterableMixin, ListView):
|
||||
self.filterset = self.filter_class(self.request.GET, queryset=queryset)
|
||||
return self.filterset.qs
|
||||
except Exception as e:
|
||||
log_exception(
|
||||
logger,
|
||||
e,
|
||||
context={"operation": "get_filtered_queryset", "filters": filter_params},
|
||||
request=self.request,
|
||||
)
|
||||
messages.error(self.request, f"Error loading parks: {str(e)}")
|
||||
queryset = self.model.objects.none()
|
||||
self.filterset = self.filter_class(self.request.GET, queryset=queryset)
|
||||
@@ -330,6 +341,15 @@ class ParkListView(HTMXFilterableMixin, ListView):
|
||||
return context
|
||||
|
||||
except Exception as e:
|
||||
log_exception(
|
||||
logger,
|
||||
e,
|
||||
context={
|
||||
"operation": "get_context_data",
|
||||
"search_query": self.request.GET.get("search", ""),
|
||||
},
|
||||
request=self.request,
|
||||
)
|
||||
messages.error(self.request, f"Error applying filters: {str(e)}")
|
||||
# Ensure filterset exists in error case
|
||||
if not hasattr(self, "filterset"):
|
||||
@@ -478,6 +498,16 @@ def search_parks(request: HttpRequest) -> HttpResponse:
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
log_exception(
|
||||
logger,
|
||||
e,
|
||||
context={
|
||||
"operation": "search_parks",
|
||||
"search_query": request.GET.get("search", ""),
|
||||
"view_mode": request.GET.get("view_mode", "grid"),
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
response = render(
|
||||
request,
|
||||
PARK_LIST_ITEM_TEMPLATE,
|
||||
@@ -505,7 +535,13 @@ def htmx_saved_trips(request: HttpRequest) -> HttpResponse:
|
||||
|
||||
qs = Trip.objects.filter(owner=request.user).order_by("-created_at")
|
||||
trips = list(qs[:10])
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
log_exception(
|
||||
logger,
|
||||
e,
|
||||
context={"operation": "htmx_saved_trips"},
|
||||
request=request,
|
||||
)
|
||||
trips = []
|
||||
return render(request, SAVED_TRIPS_TEMPLATE, {"trips": trips})
|
||||
|
||||
@@ -514,7 +550,13 @@ def _get_session_trip(request: HttpRequest) -> list:
|
||||
raw = request.session.get("trip_parks", [])
|
||||
try:
|
||||
return [int(x) for x in raw]
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
log_exception(
|
||||
logger,
|
||||
e,
|
||||
context={"operation": "get_session_trip", "raw": raw},
|
||||
request=request,
|
||||
)
|
||||
return []
|
||||
|
||||
|
||||
@@ -527,11 +569,21 @@ def _save_session_trip(request: HttpRequest, trip_list: list) -> None:
|
||||
def htmx_add_park_to_trip(request: HttpRequest) -> HttpResponse:
|
||||
"""Add a park id to `request.session['trip_parks']` and return the full trip list partial."""
|
||||
park_id = request.POST.get("park_id")
|
||||
payload = None
|
||||
if not park_id:
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8"))
|
||||
park_id = payload.get("park_id")
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
log_exception(
|
||||
logger,
|
||||
e,
|
||||
context={
|
||||
"operation": "htmx_add_park_to_trip",
|
||||
"payload": request.body.decode("utf-8", errors="replace")[:500],
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
park_id = None
|
||||
|
||||
if not park_id:
|
||||
@@ -539,7 +591,16 @@ def htmx_add_park_to_trip(request: HttpRequest) -> HttpResponse:
|
||||
|
||||
try:
|
||||
pid = int(park_id)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
log_exception(
|
||||
logger,
|
||||
e,
|
||||
context={
|
||||
"operation": "htmx_add_park_to_trip",
|
||||
"park_id": park_id,
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
return HttpResponse("", status=400)
|
||||
|
||||
trip = _get_session_trip(request)
|
||||
@@ -565,11 +626,21 @@ def htmx_add_park_to_trip(request: HttpRequest) -> HttpResponse:
|
||||
def htmx_remove_park_from_trip(request: HttpRequest) -> HttpResponse:
|
||||
"""Remove a park id from `request.session['trip_parks']` and return the updated trip list partial."""
|
||||
park_id = request.POST.get("park_id")
|
||||
payload = None
|
||||
if not park_id:
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8"))
|
||||
park_id = payload.get("park_id")
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
log_exception(
|
||||
logger,
|
||||
e,
|
||||
context={
|
||||
"operation": "htmx_remove_park_from_trip",
|
||||
"payload": request.body.decode("utf-8", errors="replace")[:500],
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
park_id = None
|
||||
|
||||
if not park_id:
|
||||
@@ -577,7 +648,16 @@ def htmx_remove_park_from_trip(request: HttpRequest) -> HttpResponse:
|
||||
|
||||
try:
|
||||
pid = int(park_id)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
log_exception(
|
||||
logger,
|
||||
e,
|
||||
context={
|
||||
"operation": "htmx_remove_park_from_trip",
|
||||
"park_id": park_id,
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
return HttpResponse("", status=400)
|
||||
|
||||
trip = _get_session_trip(request)
|
||||
@@ -605,7 +685,16 @@ def htmx_reorder_parks(request: HttpRequest) -> HttpResponse:
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8"))
|
||||
order = payload.get("order", [])
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
log_exception(
|
||||
logger,
|
||||
e,
|
||||
context={
|
||||
"operation": "htmx_reorder_parks",
|
||||
"payload": request.body.decode("utf-8", errors="replace")[:500],
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
order = request.POST.getlist("order[]")
|
||||
|
||||
# Normalize to ints
|
||||
@@ -613,7 +702,16 @@ def htmx_reorder_parks(request: HttpRequest) -> HttpResponse:
|
||||
for item in order:
|
||||
try:
|
||||
clean_order.append(int(item))
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
log_exception(
|
||||
logger,
|
||||
e,
|
||||
context={
|
||||
"operation": "htmx_reorder_parks",
|
||||
"order_item": item,
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
continue
|
||||
|
||||
_save_session_trip(request, clean_order)
|
||||
@@ -676,7 +774,27 @@ def htmx_optimize_route(request: HttpRequest) -> HttpResponse:
|
||||
total_miles += haversine_miles(
|
||||
a["latitude"], a["longitude"], b["latitude"], b["longitude"]
|
||||
)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
log_exception(
|
||||
logger,
|
||||
e,
|
||||
context={
|
||||
"operation": "htmx_optimize_route",
|
||||
"waypoint_index_a": i,
|
||||
"waypoint_index_b": i + 1,
|
||||
"waypoint_a": {
|
||||
"id": a.get("id"),
|
||||
"latitude": a.get("latitude"),
|
||||
"longitude": a.get("longitude"),
|
||||
},
|
||||
"waypoint_b": {
|
||||
"id": b.get("id"),
|
||||
"latitude": b.get("latitude"),
|
||||
"longitude": b.get("longitude"),
|
||||
},
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
continue
|
||||
|
||||
# Estimate drive time assuming average speed of 60 mph
|
||||
@@ -812,6 +930,18 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
|
||||
if service_result["status"] == "auto_approved":
|
||||
self.object = service_result["park"]
|
||||
log_business_event(
|
||||
logger,
|
||||
event_type="park_created",
|
||||
message=f"Park created: {self.object.name} (auto-approved)",
|
||||
context={
|
||||
"park_id": self.object.id,
|
||||
"park_name": self.object.name,
|
||||
"status": "auto_approved",
|
||||
"photo_count": service_result["uploaded_count"],
|
||||
},
|
||||
request=self.request,
|
||||
)
|
||||
messages.success(
|
||||
self.request,
|
||||
f"Successfully created {self.object.name}. "
|
||||
@@ -820,6 +950,16 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
elif service_result["status"] == "queued":
|
||||
log_business_event(
|
||||
logger,
|
||||
event_type="park_created",
|
||||
message="Park submission queued for moderation",
|
||||
context={
|
||||
"status": "queued",
|
||||
"park_name": form.cleaned_data.get("name"),
|
||||
},
|
||||
request=self.request,
|
||||
)
|
||||
messages.success(
|
||||
self.request,
|
||||
"Your park submission has been sent for review. "
|
||||
@@ -916,6 +1056,18 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
|
||||
|
||||
if service_result["status"] == "auto_approved":
|
||||
self.object = service_result["park"]
|
||||
log_business_event(
|
||||
logger,
|
||||
event_type="park_updated",
|
||||
message=f"Park updated: {self.object.name} (auto-approved)",
|
||||
context={
|
||||
"park_id": self.object.id,
|
||||
"park_name": self.object.name,
|
||||
"status": "auto_approved",
|
||||
"photo_count": service_result["uploaded_count"],
|
||||
},
|
||||
request=self.request,
|
||||
)
|
||||
messages.success(
|
||||
self.request,
|
||||
f"Successfully updated {self.object.name}. "
|
||||
@@ -924,6 +1076,17 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
elif service_result["status"] == "queued":
|
||||
log_business_event(
|
||||
logger,
|
||||
event_type="park_updated",
|
||||
message=f"Park update queued for moderation: {self.object.name}",
|
||||
context={
|
||||
"park_id": self.object.id,
|
||||
"park_name": self.object.name,
|
||||
"status": "queued",
|
||||
},
|
||||
request=self.request,
|
||||
)
|
||||
messages.success(
|
||||
self.request,
|
||||
f"Your changes to {self.object.name} have been sent for review. "
|
||||
|
||||
Reference in New Issue
Block a user