mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 02:31:08 -05:00
- Cleaned up and standardized assertions in ApiTestMixin for API response validation. - Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE. - Removed unused imports and improved formatting in settings.py. - Refactored URL patterns in urls.py for better readability and organization. - Enhanced view functions in views.py for consistency and clarity. - Added .flake8 configuration for linting and style enforcement. - Introduced type stubs for django-environ to improve type checking with Pylance.
422 lines
13 KiB
Python
422 lines
13 KiB
Python
"""
|
|
HTML views for the unified map service.
|
|
Provides web interfaces for map functionality with HTMX integration.
|
|
"""
|
|
|
|
import json
|
|
from typing import Dict, Any, Optional, Set
|
|
from django.shortcuts import render
|
|
from django.http import JsonResponse, HttpRequest, HttpResponse
|
|
from django.views.generic import TemplateView, View
|
|
from django.core.paginator import Paginator
|
|
|
|
from ..services.map_service import unified_map_service
|
|
from ..services.data_structures import GeoBounds, MapFilters, LocationType
|
|
|
|
|
|
class MapViewMixin:
|
|
"""Mixin providing common functionality for map views."""
|
|
|
|
def get_map_context(self, request: HttpRequest) -> Dict[str, Any]:
|
|
"""Get common context data for map views."""
|
|
return {
|
|
"map_api_urls": {
|
|
"locations": "/api/map/locations/",
|
|
"search": "/api/map/search/",
|
|
"bounds": "/api/map/bounds/",
|
|
"location_detail": "/api/map/locations/",
|
|
},
|
|
"location_types": [lt.value for lt in LocationType],
|
|
"default_zoom": 10,
|
|
"enable_clustering": True,
|
|
"enable_search": True,
|
|
}
|
|
|
|
def parse_location_types(self, request: HttpRequest) -> Optional[Set[LocationType]]:
|
|
"""Parse location types from request parameters."""
|
|
types_param = request.GET.get("types")
|
|
if types_param:
|
|
try:
|
|
return {
|
|
LocationType(t.strip())
|
|
for t in types_param.split(",")
|
|
if t.strip() in [lt.value for lt in LocationType]
|
|
}
|
|
except ValueError:
|
|
return None
|
|
return None
|
|
|
|
|
|
class UniversalMapView(MapViewMixin, TemplateView):
|
|
"""
|
|
Main universal map view showing all location types.
|
|
|
|
URL: /maps/
|
|
"""
|
|
|
|
template_name = "maps/universal_map.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update(self.get_map_context(self.request))
|
|
|
|
# Additional context for universal map
|
|
context.update(
|
|
{
|
|
"page_title": "Interactive Map - All Locations",
|
|
"map_type": "universal",
|
|
"show_all_types": True,
|
|
"initial_location_types": [lt.value for lt in LocationType],
|
|
"filters_enabled": True,
|
|
}
|
|
)
|
|
|
|
# Handle initial bounds from query parameters
|
|
if all(
|
|
param in self.request.GET for param in ["north", "south", "east", "west"]
|
|
):
|
|
try:
|
|
context["initial_bounds"] = {
|
|
"north": float(self.request.GET["north"]),
|
|
"south": float(self.request.GET["south"]),
|
|
"east": float(self.request.GET["east"]),
|
|
"west": float(self.request.GET["west"]),
|
|
}
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
return context
|
|
|
|
|
|
class ParkMapView(MapViewMixin, TemplateView):
|
|
"""
|
|
Map view focused specifically on parks.
|
|
|
|
URL: /maps/parks/
|
|
"""
|
|
|
|
template_name = "maps/park_map.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update(self.get_map_context(self.request))
|
|
|
|
# Park-specific context
|
|
context.update(
|
|
{
|
|
"page_title": "Theme Parks Map",
|
|
"map_type": "parks",
|
|
"show_all_types": False,
|
|
"initial_location_types": [LocationType.PARK.value],
|
|
"filters_enabled": True,
|
|
"park_specific_filters": True,
|
|
}
|
|
)
|
|
|
|
return context
|
|
|
|
|
|
class NearbyLocationsView(MapViewMixin, TemplateView):
|
|
"""
|
|
View for showing locations near a specific point.
|
|
|
|
URL: /maps/nearby/
|
|
"""
|
|
|
|
template_name = "maps/nearby_locations.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update(self.get_map_context(self.request))
|
|
|
|
# Parse coordinates from query parameters
|
|
lat = self.request.GET.get("lat")
|
|
lng = self.request.GET.get("lng")
|
|
radius = self.request.GET.get("radius", "50") # Default 50km radius
|
|
|
|
if lat and lng:
|
|
try:
|
|
center_lat = float(lat)
|
|
center_lng = float(lng)
|
|
# Clamp between 1-200km
|
|
search_radius = min(200, max(1, float(radius)))
|
|
|
|
context.update(
|
|
{
|
|
"page_title": f"Locations Near {
|
|
center_lat:.4f}, {
|
|
center_lng:.4f}",
|
|
"map_type": "nearby",
|
|
"center_coordinates": {
|
|
"lat": center_lat,
|
|
"lng": center_lng,
|
|
},
|
|
"search_radius": search_radius,
|
|
"show_radius_circle": True,
|
|
}
|
|
)
|
|
except (ValueError, TypeError):
|
|
context["error"] = "Invalid coordinates provided"
|
|
else:
|
|
context.update(
|
|
{
|
|
"page_title": "Nearby Locations",
|
|
"map_type": "nearby",
|
|
"prompt_for_location": True,
|
|
}
|
|
)
|
|
|
|
return context
|
|
|
|
|
|
class LocationFilterView(MapViewMixin, View):
|
|
"""
|
|
HTMX endpoint for updating map when filters change.
|
|
|
|
URL: /maps/htmx/filter/
|
|
"""
|
|
|
|
def get(self, request: HttpRequest) -> HttpResponse:
|
|
"""Return filtered location data for HTMX updates."""
|
|
try:
|
|
# Parse filter parameters
|
|
location_types = self.parse_location_types(request)
|
|
search_query = request.GET.get("q", "").strip()
|
|
country = request.GET.get("country", "").strip()
|
|
state = request.GET.get("state", "").strip()
|
|
|
|
# Create filters
|
|
filters = None
|
|
if any([location_types, search_query, country, state]):
|
|
filters = MapFilters(
|
|
location_types=location_types,
|
|
search_query=search_query or None,
|
|
country=country or None,
|
|
state=state or None,
|
|
has_coordinates=True,
|
|
)
|
|
|
|
# Get filtered locations
|
|
map_response = unified_map_service.get_map_data(
|
|
filters=filters,
|
|
zoom_level=int(request.GET.get("zoom", "10")),
|
|
cluster=request.GET.get("cluster", "true").lower() == "true",
|
|
)
|
|
|
|
# Return JSON response for HTMX
|
|
return JsonResponse(
|
|
{
|
|
"status": "success",
|
|
"data": map_response.to_dict(),
|
|
"filters_applied": map_response.filters_applied,
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
return JsonResponse({"status": "error", "message": str(e)}, status=400)
|
|
|
|
|
|
class LocationSearchView(MapViewMixin, View):
|
|
"""
|
|
HTMX endpoint for real-time location search.
|
|
|
|
URL: /maps/htmx/search/
|
|
"""
|
|
|
|
def get(self, request: HttpRequest) -> HttpResponse:
|
|
"""Return search results for HTMX updates."""
|
|
query = request.GET.get("q", "").strip()
|
|
|
|
if not query or len(query) < 3:
|
|
return render(
|
|
request,
|
|
"maps/partials/search_results.html",
|
|
{
|
|
"results": [],
|
|
"query": query,
|
|
"message": "Enter at least 3 characters to search",
|
|
},
|
|
)
|
|
|
|
try:
|
|
# Parse optional location types
|
|
location_types = self.parse_location_types(request)
|
|
limit = min(20, max(5, int(request.GET.get("limit", "10"))))
|
|
|
|
# Perform search
|
|
results = unified_map_service.search_locations(
|
|
query=query, location_types=location_types, limit=limit
|
|
)
|
|
|
|
return render(
|
|
request,
|
|
"maps/partials/search_results.html",
|
|
{"results": results, "query": query, "count": len(results)},
|
|
)
|
|
|
|
except Exception as e:
|
|
return render(
|
|
request,
|
|
"maps/partials/search_results.html",
|
|
{"results": [], "query": query, "error": str(e)},
|
|
)
|
|
|
|
|
|
class MapBoundsUpdateView(MapViewMixin, View):
|
|
"""
|
|
HTMX endpoint for updating locations when map bounds change.
|
|
|
|
URL: /maps/htmx/bounds/
|
|
"""
|
|
|
|
def post(self, request: HttpRequest) -> HttpResponse:
|
|
"""Update map data when bounds change."""
|
|
try:
|
|
data = json.loads(request.body)
|
|
|
|
# Parse bounds
|
|
bounds = GeoBounds(
|
|
north=float(data["north"]),
|
|
south=float(data["south"]),
|
|
east=float(data["east"]),
|
|
west=float(data["west"]),
|
|
)
|
|
|
|
# Parse additional parameters
|
|
zoom_level = int(data.get("zoom", 10))
|
|
location_types = None
|
|
if "types" in data:
|
|
location_types = {
|
|
LocationType(t)
|
|
for t in data["types"]
|
|
if t in [lt.value for lt in LocationType]
|
|
}
|
|
|
|
# Location types are used directly in the service call
|
|
|
|
# Get updated map data
|
|
map_response = unified_map_service.get_locations_by_bounds(
|
|
north=bounds.north,
|
|
south=bounds.south,
|
|
east=bounds.east,
|
|
west=bounds.west,
|
|
location_types=location_types,
|
|
zoom_level=zoom_level,
|
|
)
|
|
|
|
return JsonResponse({"status": "success", "data": map_response.to_dict()})
|
|
|
|
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
|
return JsonResponse(
|
|
{
|
|
"status": "error",
|
|
"message": f"Invalid request data: {str(e)}",
|
|
},
|
|
status=400,
|
|
)
|
|
except Exception as e:
|
|
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
|
|
|
|
|
class LocationDetailModalView(MapViewMixin, View):
|
|
"""
|
|
HTMX endpoint for showing location details in modal.
|
|
|
|
URL: /maps/htmx/location/<type>/<id>/
|
|
"""
|
|
|
|
def get(
|
|
self, request: HttpRequest, location_type: str, location_id: int
|
|
) -> HttpResponse:
|
|
"""Return location detail modal content."""
|
|
try:
|
|
# Validate location type
|
|
if location_type not in [lt.value for lt in LocationType]:
|
|
return render(
|
|
request,
|
|
"maps/partials/location_modal.html",
|
|
{"error": f"Invalid location type: {location_type}"},
|
|
)
|
|
|
|
# Get location details
|
|
location = unified_map_service.get_location_details(
|
|
location_type, location_id
|
|
)
|
|
|
|
if not location:
|
|
return render(
|
|
request,
|
|
"maps/partials/location_modal.html",
|
|
{"error": "Location not found"},
|
|
)
|
|
|
|
return render(
|
|
request,
|
|
"maps/partials/location_modal.html",
|
|
{"location": location, "location_type": location_type},
|
|
)
|
|
|
|
except Exception as e:
|
|
return render(
|
|
request, "maps/partials/location_modal.html", {"error": str(e)}
|
|
)
|
|
|
|
|
|
class LocationListView(MapViewMixin, TemplateView):
|
|
"""
|
|
View for listing locations with pagination (non-map view).
|
|
|
|
URL: /maps/list/
|
|
"""
|
|
|
|
template_name = "maps/location_list.html"
|
|
paginate_by = 20
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
# Parse filters
|
|
location_types = self.parse_location_types(self.request)
|
|
search_query = self.request.GET.get("q", "").strip()
|
|
country = self.request.GET.get("country", "").strip()
|
|
state = self.request.GET.get("state", "").strip()
|
|
|
|
# Create filters
|
|
filters = None
|
|
if any([location_types, search_query, country, state]):
|
|
filters = MapFilters(
|
|
location_types=location_types,
|
|
search_query=search_query or None,
|
|
country=country or None,
|
|
state=state or None,
|
|
has_coordinates=True,
|
|
)
|
|
|
|
# Get locations without clustering
|
|
map_response = unified_map_service.get_map_data(
|
|
filters=filters, cluster=False, use_cache=True
|
|
)
|
|
|
|
# Paginate results
|
|
paginator = Paginator(map_response.locations, self.paginate_by)
|
|
page_number = self.request.GET.get("page")
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
context.update(
|
|
{
|
|
"page_title": "All Locations",
|
|
"locations": page_obj,
|
|
"total_count": map_response.total_count,
|
|
"applied_filters": filters,
|
|
"location_types": [lt.value for lt in LocationType],
|
|
"current_filters": {
|
|
"types": self.request.GET.getlist("types"),
|
|
"q": search_query,
|
|
"country": country,
|
|
"state": state,
|
|
},
|
|
}
|
|
)
|
|
|
|
return context
|