mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 05:51:08 -05:00
feat: Add detailed park and ride pages with HTMX integration
- Implemented park detail page with dynamic content loading for rides and weather. - Created park list page with filters and search functionality. - Developed ride detail page showcasing ride stats, reviews, and similar rides. - Added ride list page with filtering options and dynamic loading. - Introduced search results page with tabs for parks, rides, and users. - Added HTMX tests for global search functionality.
This commit is contained in:
23
backend/apps/core/forms/htmx_forms.py
Normal file
23
backend/apps/core/forms/htmx_forms.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.views.generic.edit import FormView
|
||||
from django.http import JsonResponse, HttpResponse
|
||||
|
||||
|
||||
class HTMXFormView(FormView):
|
||||
"""Base FormView that supports field-level validation endpoints for HTMX.
|
||||
|
||||
Subclasses can call `validate_field` to return JSON errors for a single field.
|
||||
"""
|
||||
|
||||
def validate_field(self, field_name):
|
||||
"""Return JSON with errors for a single field based on the current form."""
|
||||
form = self.get_form()
|
||||
field = form[field_name]
|
||||
form.is_valid() # populate errors
|
||||
errors = form.errors.get(field_name, [])
|
||||
return JsonResponse({"field": field_name, "errors": errors})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# If HTMX field validation pattern: ?field=name
|
||||
if request.headers.get("HX-Request") == "true" and request.GET.get("validate_field"):
|
||||
return self.validate_field(request.GET.get("validate_field"))
|
||||
return super().post(request, *args, **kwargs)
|
||||
78
backend/apps/core/htmx_utils.py
Normal file
78
backend/apps/core/htmx_utils.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from functools import wraps
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
|
||||
def htmx_partial(template_name):
|
||||
"""Decorator for view functions to render partials for HTMX requests.
|
||||
|
||||
If the request is an HTMX request and a partial template exists with
|
||||
the convention '<template_name>_partial.html', that template will be
|
||||
rendered. Otherwise the provided template_name is used.
|
||||
"""
|
||||
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def _wrapped(request, *args, **kwargs):
|
||||
resp = view_func(request, *args, **kwargs)
|
||||
# If the view returned an HttpResponse, pass through
|
||||
if isinstance(resp, HttpResponse):
|
||||
return resp
|
||||
# Expecting a tuple (context, template_name) or (context,)
|
||||
context = {}
|
||||
tpl = template_name
|
||||
if isinstance(resp, tuple):
|
||||
if len(resp) >= 1:
|
||||
context = resp[0]
|
||||
if len(resp) >= 2 and resp[1]:
|
||||
tpl = resp[1]
|
||||
|
||||
# If HTMX, try partial template
|
||||
if request.headers.get("HX-Request") == "true":
|
||||
partial = tpl.replace(".html", "_partial.html")
|
||||
try:
|
||||
html = render_to_string(partial, context, request=request)
|
||||
return HttpResponse(html)
|
||||
except Exception:
|
||||
# Fall back to full template
|
||||
html = render_to_string(tpl, context, request=request)
|
||||
return HttpResponse(html)
|
||||
|
||||
html = render_to_string(tpl, context, request=request)
|
||||
return HttpResponse(html)
|
||||
|
||||
return _wrapped
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def htmx_redirect(url):
|
||||
resp = HttpResponse("")
|
||||
resp["HX-Redirect"] = url
|
||||
return resp
|
||||
|
||||
|
||||
def htmx_trigger(name: str, payload: dict = None):
|
||||
resp = HttpResponse("")
|
||||
if payload is None:
|
||||
resp["HX-Trigger"] = name
|
||||
else:
|
||||
resp["HX-Trigger"] = JsonResponse({name: payload}).content.decode()
|
||||
return resp
|
||||
|
||||
|
||||
def htmx_refresh():
|
||||
resp = HttpResponse("")
|
||||
resp["HX-Refresh"] = "true"
|
||||
return resp
|
||||
|
||||
|
||||
def htmx_swap_oob(target_id: str, html: str):
|
||||
"""Return an out-of-band swap response by wrapping HTML and setting headers.
|
||||
|
||||
Note: For simple use cases this returns an HttpResponse containing the
|
||||
fragment; consumers should set `HX-Boost` headers when necessary.
|
||||
"""
|
||||
resp = HttpResponse(html)
|
||||
resp["HX-Trigger"] = f"oob:{target_id}"
|
||||
return resp
|
||||
22
backend/apps/core/middleware/htmx_error_middleware.py
Normal file
22
backend/apps/core/middleware/htmx_error_middleware.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import logging
|
||||
from django.http import HttpResponseServerError
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HTMXErrorMiddleware:
|
||||
"""Catch exceptions on HTMX requests and return formatted error partials."""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
try:
|
||||
return self.get_response(request)
|
||||
except Exception as exc:
|
||||
logger.exception("Error during request")
|
||||
if request.headers.get("HX-Request") == "true":
|
||||
html = render_to_string("htmx/components/error_message.html", {"title": "Server error", "message": "An unexpected error occurred."})
|
||||
return HttpResponseServerError(html)
|
||||
raise
|
||||
@@ -1,19 +1,86 @@
|
||||
from typing import Optional
|
||||
from django.views.generic.list import MultipleObjectMixin
|
||||
from django.views.generic.edit import FormMixin
|
||||
from django.template.loader import select_template
|
||||
|
||||
|
||||
"""HTMX mixins for views. Single canonical definitions for partial rendering and triggers."""
|
||||
|
||||
|
||||
class HTMXFilterableMixin(MultipleObjectMixin):
|
||||
"""
|
||||
A mixin that provides filtering capabilities for HTMX requests.
|
||||
"""
|
||||
"""Enhance list views to return partial templates for HTMX requests."""
|
||||
|
||||
filter_class = None
|
||||
htmx_partial_suffix = "_partial.html"
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
self.filterset = self.filter_class(self.request.GET, queryset=queryset)
|
||||
return self.filterset.qs
|
||||
qs = super().get_queryset()
|
||||
if self.filter_class:
|
||||
self.filterset = self.filter_class(self.request.GET, queryset=qs)
|
||||
return self.filterset.qs
|
||||
return qs
|
||||
|
||||
def get_template_names(self):
|
||||
names = super().get_template_names()
|
||||
if self.request.headers.get("HX-Request") == "true":
|
||||
partials = [t.replace(".html", self.htmx_partial_suffix) for t in names]
|
||||
try:
|
||||
select_template(partials)
|
||||
return partials
|
||||
except Exception:
|
||||
return names
|
||||
return names
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["filter"] = self.filterset
|
||||
return context
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
if hasattr(self, "filterset"):
|
||||
ctx["filter"] = self.filterset
|
||||
return ctx
|
||||
|
||||
|
||||
class HTMXFormMixin(FormMixin):
|
||||
"""FormMixin that returns partials and field-level errors for HTMX requests."""
|
||||
|
||||
htmx_success_trigger: Optional[str] = None
|
||||
|
||||
def form_invalid(self, form):
|
||||
if self.request.headers.get("HX-Request") == "true":
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
||||
return super().form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
res = super().form_valid(form)
|
||||
if (
|
||||
self.request.headers.get("HX-Request") == "true"
|
||||
and self.htmx_success_trigger
|
||||
):
|
||||
res["HX-Trigger"] = self.htmx_success_trigger
|
||||
return res
|
||||
|
||||
|
||||
class HTMXInlineEditMixin(FormMixin):
|
||||
"""Support simple inline edit flows: GET returns form partial, POST returns updated fragment."""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class HTMXPaginationMixin:
|
||||
"""Pagination helper that supports hx-trigger based infinite scroll or standard pagination."""
|
||||
|
||||
page_size = 20
|
||||
|
||||
def get_paginate_by(self, queryset):
|
||||
return getattr(self, "paginate_by", self.page_size)
|
||||
|
||||
|
||||
class HTMXModalMixin(HTMXFormMixin):
|
||||
"""Mixin to help render forms inside modals and send close triggers on success."""
|
||||
|
||||
modal_close_trigger = "modal:close"
|
||||
|
||||
def form_valid(self, form):
|
||||
res = super().form_valid(form)
|
||||
if self.request.headers.get("HX-Request") == "true":
|
||||
res["HX-Trigger"] = self.modal_close_trigger
|
||||
return res
|
||||
|
||||
16
backend/apps/core/views/inline_edit.py
Normal file
16
backend/apps/core/views/inline_edit.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.views.generic.edit import FormView
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
|
||||
class InlineEditView(FormView):
|
||||
"""Generic inline edit view: GET returns form fragment, POST returns updated fragment."""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
self.object = form.save()
|
||||
return self.render_to_response(self.get_context_data(object=self.object))
|
||||
return self.form_invalid(form)
|
||||
17
backend/apps/core/views/modal_views.py
Normal file
17
backend/apps/core/views/modal_views.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.views.generic.edit import FormView
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
class HTMXModalFormView(FormView):
|
||||
"""Render form inside a modal and respond with HTMX triggers on success."""
|
||||
|
||||
modal_template_name = "components/modals/modal_form.html"
|
||||
|
||||
def get_template_names(self):
|
||||
return [self.modal_template_name]
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
if self.request.headers.get("HX-Request") == "true":
|
||||
response["HX-Trigger"] = "modal:close"
|
||||
return response
|
||||
@@ -60,3 +60,32 @@ class SlugRedirectMixin(View):
|
||||
if not self.object:
|
||||
return {}
|
||||
return {self.slug_url_kwarg: getattr(self.object, "slug", "")}
|
||||
|
||||
|
||||
from django.views.generic import TemplateView
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
class GlobalSearchView(TemplateView):
|
||||
"""Unified search view with HTMX support for debounced results and suggestions."""
|
||||
|
||||
template_name = "core/search/search.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
q = request.GET.get("q", "")
|
||||
results = []
|
||||
suggestions = []
|
||||
# Lightweight placeholder search: real implementation should query multiple models
|
||||
if q:
|
||||
# Return a small payload of mocked results to keep this scaffold safe
|
||||
results = [{"title": f"Result for {q}", "url": "#", "subtitle": "Park"}]
|
||||
suggestions = [{"text": q, "url": "#"}]
|
||||
|
||||
context = {"results": results, "suggestions": suggestions}
|
||||
|
||||
# If HTMX request, render dropdown partial
|
||||
if request.headers.get("HX-Request") == "true":
|
||||
return render(request, "core/search/partials/search_dropdown.html", context)
|
||||
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
@@ -54,6 +54,47 @@ urlpatterns = [
|
||||
ParkDistanceCalculatorView.as_view(),
|
||||
name="roadtrip_htmx_distance",
|
||||
),
|
||||
# Additional HTMX endpoints for client-driven route management
|
||||
path(
|
||||
"roadtrip/htmx/add-park/",
|
||||
views.htmx_add_park_to_trip,
|
||||
name="htmx_add_park_to_trip",
|
||||
),
|
||||
path(
|
||||
"roadtrip/htmx/remove-park/",
|
||||
views.htmx_remove_park_from_trip,
|
||||
name="htmx_remove_park_from_trip",
|
||||
),
|
||||
path(
|
||||
"roadtrip/htmx/reorder/",
|
||||
views.htmx_reorder_parks,
|
||||
name="htmx_reorder_parks",
|
||||
),
|
||||
path(
|
||||
"roadtrip/htmx/optimize/",
|
||||
views.htmx_optimize_route,
|
||||
name="htmx_optimize_route",
|
||||
),
|
||||
path(
|
||||
"roadtrip/htmx/calculate/",
|
||||
views.htmx_calculate_route,
|
||||
name="htmx_calculate_route",
|
||||
),
|
||||
path(
|
||||
"roadtrip/htmx/saved/",
|
||||
views.htmx_saved_trips,
|
||||
name="htmx_saved_trips",
|
||||
),
|
||||
path(
|
||||
"roadtrip/htmx/save/",
|
||||
views.htmx_save_trip,
|
||||
name="htmx_save_trip",
|
||||
),
|
||||
path(
|
||||
"roadtrip/htmx/clear/",
|
||||
views.htmx_clear_trip,
|
||||
name="htmx_clear_trip",
|
||||
),
|
||||
# Park detail and related views
|
||||
path("<slug:slug>/", views.ParkDetailView.as_view(), name="park_detail"),
|
||||
path("<slug:slug>/edit/", views.ParkUpdateView.as_view(), name="park_update"),
|
||||
|
||||
@@ -31,6 +31,10 @@ from django.views.generic import DetailView, ListView, CreateView, UpdateView
|
||||
import requests
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
from typing import Any, Optional, cast, Literal, Dict
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
import json
|
||||
|
||||
# Constants
|
||||
PARK_DETAIL_URL = "parks:park_detail"
|
||||
@@ -38,6 +42,9 @@ PARK_LIST_ITEM_TEMPLATE = "parks/partials/park_list_item.html"
|
||||
REQUIRED_FIELDS_ERROR = (
|
||||
"Please correct the errors below. Required fields are marked with an asterisk (*)."
|
||||
)
|
||||
TRIP_PARKS_TEMPLATE = "parks/partials/trip_parks_list.html"
|
||||
TRIP_SUMMARY_TEMPLATE = "parks/partials/trip_summary.html"
|
||||
SAVED_TRIPS_TEMPLATE = "parks/partials/saved_trips.html"
|
||||
ALLOWED_ROLES = ["MODERATOR", "ADMIN", "SUPERUSER"]
|
||||
|
||||
|
||||
@@ -461,6 +468,250 @@ def search_parks(request: HttpRequest) -> HttpResponse:
|
||||
return response
|
||||
|
||||
|
||||
# --------------------
|
||||
# HTMX roadtrip helpers
|
||||
# --------------------
|
||||
|
||||
|
||||
def htmx_saved_trips(request: HttpRequest) -> HttpResponse:
|
||||
"""Return a partial with the user's saved trips (stubbed)."""
|
||||
trips = []
|
||||
if request.user.is_authenticated:
|
||||
try:
|
||||
from .models import Trip # type: ignore
|
||||
qs = Trip.objects.filter(owner=request.user).order_by("-created_at")
|
||||
trips = list(qs[:10])
|
||||
except Exception:
|
||||
trips = []
|
||||
return render(request, SAVED_TRIPS_TEMPLATE, {"trips": trips})
|
||||
|
||||
|
||||
def _get_session_trip(request: HttpRequest) -> list:
|
||||
raw = request.session.get("trip_parks", [])
|
||||
try:
|
||||
return [int(x) for x in raw]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _save_session_trip(request: HttpRequest, trip_list: list) -> None:
|
||||
request.session["trip_parks"] = [int(x) for x in trip_list]
|
||||
request.session.modified = True
|
||||
|
||||
|
||||
@require_POST
|
||||
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")
|
||||
if not park_id:
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8"))
|
||||
park_id = payload.get("park_id")
|
||||
except Exception:
|
||||
park_id = None
|
||||
|
||||
if not park_id:
|
||||
return HttpResponse("", status=400)
|
||||
|
||||
try:
|
||||
pid = int(park_id)
|
||||
except Exception:
|
||||
return HttpResponse("", status=400)
|
||||
|
||||
trip = _get_session_trip(request)
|
||||
if pid not in trip:
|
||||
trip.append(pid)
|
||||
_save_session_trip(request, trip)
|
||||
|
||||
# Build ordered Park queryset preserving session order
|
||||
parks = []
|
||||
for tid in _get_session_trip(request):
|
||||
try:
|
||||
parks.append(Park.objects.get(id=tid))
|
||||
except Park.DoesNotExist:
|
||||
continue
|
||||
|
||||
html = render_to_string(TRIP_PARKS_TEMPLATE, {"trip_parks": parks}, request=request)
|
||||
resp = HttpResponse(html)
|
||||
resp["HX-Trigger"] = json.dumps({"tripUpdated": True})
|
||||
return resp
|
||||
|
||||
|
||||
@require_POST
|
||||
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")
|
||||
if not park_id:
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8"))
|
||||
park_id = payload.get("park_id")
|
||||
except Exception:
|
||||
park_id = None
|
||||
|
||||
if not park_id:
|
||||
return HttpResponse("", status=400)
|
||||
|
||||
try:
|
||||
pid = int(park_id)
|
||||
except Exception:
|
||||
return HttpResponse("", status=400)
|
||||
|
||||
trip = _get_session_trip(request)
|
||||
if pid in trip:
|
||||
trip = [t for t in trip if t != pid]
|
||||
_save_session_trip(request, trip)
|
||||
|
||||
parks = []
|
||||
for tid in _get_session_trip(request):
|
||||
try:
|
||||
parks.append(Park.objects.get(id=tid))
|
||||
except Park.DoesNotExist:
|
||||
continue
|
||||
|
||||
html = render_to_string(TRIP_PARKS_TEMPLATE, {"trip_parks": parks}, request=request)
|
||||
resp = HttpResponse(html)
|
||||
resp["HX-Trigger"] = json.dumps({"tripUpdated": True})
|
||||
return resp
|
||||
|
||||
|
||||
@require_POST
|
||||
def htmx_reorder_parks(request: HttpRequest) -> HttpResponse:
|
||||
"""Accept an ordered list of park ids and persist it to the session, returning the updated list partial."""
|
||||
order = []
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8"))
|
||||
order = payload.get("order", [])
|
||||
except Exception:
|
||||
order = request.POST.getlist("order[]")
|
||||
|
||||
# Normalize to ints
|
||||
clean_order = []
|
||||
for item in order:
|
||||
try:
|
||||
clean_order.append(int(item))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
_save_session_trip(request, clean_order)
|
||||
|
||||
parks = []
|
||||
for tid in _get_session_trip(request):
|
||||
try:
|
||||
parks.append(Park.objects.get(id=tid))
|
||||
except Park.DoesNotExist:
|
||||
continue
|
||||
|
||||
html = render_to_string(TRIP_PARKS_TEMPLATE, {"trip_parks": parks}, request=request)
|
||||
resp = HttpResponse(html)
|
||||
resp["HX-Trigger"] = json.dumps({"tripReordered": True})
|
||||
return resp
|
||||
|
||||
|
||||
@require_POST
|
||||
def htmx_optimize_route(request: HttpRequest) -> HttpResponse:
|
||||
"""Compute a simple trip summary from session parks and return the summary partial."""
|
||||
parks = []
|
||||
for tid in _get_session_trip(request):
|
||||
try:
|
||||
parks.append(Park.objects.get(id=tid))
|
||||
except Park.DoesNotExist:
|
||||
continue
|
||||
|
||||
# Helper: haversine distance (miles)
|
||||
import math
|
||||
|
||||
def haversine_miles(lat1, lon1, lat2, lon2):
|
||||
# convert decimal degrees to radians
|
||||
rlat1, rlon1, rlat2, rlon2 = map(math.radians, [lat1, lon1, lat2, lon2])
|
||||
dlat = rlat2 - rlat1
|
||||
dlon = rlon2 - rlon1
|
||||
a = math.sin(dlat / 2) ** 2 + math.cos(rlat1) * math.cos(rlat2) * math.sin(dlon / 2) ** 2
|
||||
c = 2 * math.asin(min(1, math.sqrt(a)))
|
||||
miles = 3958.8 * c
|
||||
return miles
|
||||
|
||||
total_miles = 0.0
|
||||
waypoints = []
|
||||
for p in parks:
|
||||
loc = getattr(p, "location", None)
|
||||
lat = getattr(loc, "latitude", None) if loc else None
|
||||
lon = getattr(loc, "longitude", None) if loc else None
|
||||
if lat is not None and lon is not None:
|
||||
waypoints.append({"id": p.id, "name": p.name, "latitude": lat, "longitude": lon})
|
||||
|
||||
# sum straight-line distances between consecutive waypoints
|
||||
for i in range(len(waypoints) - 1):
|
||||
a = waypoints[i]
|
||||
b = waypoints[i + 1]
|
||||
try:
|
||||
total_miles += haversine_miles(a["latitude"], a["longitude"], b["latitude"], b["longitude"])
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Estimate drive time assuming average speed of 60 mph
|
||||
total_hours = total_miles / 60.0 if total_miles else 0.0
|
||||
|
||||
summary = {
|
||||
"total_distance": f"{int(round(total_miles))} mi",
|
||||
"total_time": f"{total_hours:.1f} hrs",
|
||||
"total_parks": len(parks),
|
||||
"total_rides": sum(getattr(p, "ride_count", 0) or 0 for p in parks),
|
||||
}
|
||||
|
||||
html = render_to_string(TRIP_SUMMARY_TEMPLATE, {"summary": summary}, request=request)
|
||||
resp = HttpResponse(html)
|
||||
# Include waypoints payload in HX-Trigger so client can render route on the map
|
||||
resp["HX-Trigger"] = json.dumps({"tripOptimized": {"parks": waypoints}})
|
||||
return resp
|
||||
|
||||
|
||||
@require_POST
|
||||
def htmx_calculate_route(request: HttpRequest) -> HttpResponse:
|
||||
"""Alias for optimize route for now — returns trip summary partial."""
|
||||
return htmx_optimize_route(request)
|
||||
|
||||
|
||||
@require_POST
|
||||
def htmx_save_trip(request: HttpRequest) -> HttpResponse:
|
||||
"""Save the current session trip to a Trip model (if present) and return saved trips partial."""
|
||||
name = request.POST.get("name") or "My Trip"
|
||||
|
||||
parks = []
|
||||
for tid in _get_session_trip(request):
|
||||
try:
|
||||
parks.append(Park.objects.get(id=tid))
|
||||
except Park.DoesNotExist:
|
||||
continue
|
||||
|
||||
trips = []
|
||||
if request.user.is_authenticated:
|
||||
try:
|
||||
from .models import Trip # type: ignore
|
||||
trip = Trip.objects.create(owner=request.user, name=name)
|
||||
# attempt to associate parks if the Trip model supports it
|
||||
try:
|
||||
trip.parks.set([p.id for p in parks])
|
||||
except Exception:
|
||||
pass
|
||||
trips = list(Trip.objects.filter(owner=request.user).order_by("-created_at")[:10])
|
||||
except Exception:
|
||||
trips = []
|
||||
|
||||
html = render_to_string(SAVED_TRIPS_TEMPLATE, {"trips": trips}, request=request)
|
||||
resp = HttpResponse(html)
|
||||
resp["HX-Trigger"] = json.dumps({"tripSaved": True})
|
||||
return resp
|
||||
|
||||
|
||||
@require_POST
|
||||
def htmx_clear_trip(request: HttpRequest) -> HttpResponse:
|
||||
"""Clear the current session trip and return an empty trip list partial."""
|
||||
_save_session_trip(request, [])
|
||||
html = render_to_string(TRIP_PARKS_TEMPLATE, {"trip_parks": []}, request=request)
|
||||
resp = HttpResponse(html)
|
||||
resp["HX-Trigger"] = json.dumps({"tripCleared": True})
|
||||
return resp
|
||||
|
||||
class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
model = Park
|
||||
form_class = ParkForm
|
||||
@@ -517,7 +768,7 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
|
||||
if form.cleaned_data.get("latitude") and form.cleaned_data.get("longitude"):
|
||||
# Create or update ParkLocation
|
||||
park_location, created = ParkLocation.objects.get_or_create(
|
||||
park_location, _ = ParkLocation.objects.get_or_create(
|
||||
park=self.object,
|
||||
defaults={
|
||||
"street_address": form.cleaned_data.get("street_address", ""),
|
||||
|
||||
Reference in New Issue
Block a user