major changes, including tailwind v4

This commit is contained in:
pacnpal
2025-08-15 12:24:20 -04:00
parent f6c8e0e25c
commit da7c7e3381
261 changed files with 22783 additions and 10465 deletions

2
core/views/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .search import *
from .views import *

394
core/views/map_views.py Normal file
View File

@@ -0,0 +1,394 @@
"""
API views for the unified map service.
"""
import json
from typing import Dict, Any, Optional, Set
from django.http import JsonResponse, HttpRequest, Http404
from django.views.decorators.http import require_http_methods
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from django.views import View
from django.core.exceptions import ValidationError
from ..services.map_service import unified_map_service
from ..services.data_structures import GeoBounds, MapFilters, LocationType
class MapAPIView(View):
"""Base view for map API endpoints with common functionality."""
def dispatch(self, request, *args, **kwargs):
"""Add CORS headers and handle preflight requests."""
response = super().dispatch(request, *args, **kwargs)
# Add CORS headers for API access
response['Access-Control-Allow-Origin'] = '*'
response['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
return response
def options(self, request, *args, **kwargs):
"""Handle preflight CORS requests."""
return JsonResponse({}, status=200)
def _parse_bounds(self, request: HttpRequest) -> Optional[GeoBounds]:
"""Parse geographic bounds from request parameters."""
try:
north = request.GET.get('north')
south = request.GET.get('south')
east = request.GET.get('east')
west = request.GET.get('west')
if all(param is not None for param in [north, south, east, west]):
return GeoBounds(
north=float(north),
south=float(south),
east=float(east),
west=float(west)
)
return None
except (ValueError, TypeError) as e:
raise ValidationError(f"Invalid bounds parameters: {e}")
def _parse_filters(self, request: HttpRequest) -> Optional[MapFilters]:
"""Parse filtering parameters from request."""
try:
filters = MapFilters()
# Location types
location_types_param = request.GET.get('types')
if location_types_param:
type_strings = location_types_param.split(',')
filters.location_types = {
LocationType(t.strip()) for t in type_strings
if t.strip() in [lt.value for lt in LocationType]
}
# Park status
park_status_param = request.GET.get('park_status')
if park_status_param:
filters.park_status = set(park_status_param.split(','))
# Ride types
ride_types_param = request.GET.get('ride_types')
if ride_types_param:
filters.ride_types = set(ride_types_param.split(','))
# Company roles
company_roles_param = request.GET.get('company_roles')
if company_roles_param:
filters.company_roles = set(company_roles_param.split(','))
# Search query
filters.search_query = request.GET.get('q') or request.GET.get('search')
# Rating filter
min_rating_param = request.GET.get('min_rating')
if min_rating_param:
filters.min_rating = float(min_rating_param)
# Geographic filters
filters.country = request.GET.get('country')
filters.state = request.GET.get('state')
filters.city = request.GET.get('city')
# Coordinates requirement
has_coordinates_param = request.GET.get('has_coordinates')
if has_coordinates_param is not None:
filters.has_coordinates = has_coordinates_param.lower() in ['true', '1', 'yes']
return filters if any([
filters.location_types, filters.park_status, filters.ride_types,
filters.company_roles, filters.search_query, filters.min_rating,
filters.country, filters.state, filters.city
]) else None
except (ValueError, TypeError) as e:
raise ValidationError(f"Invalid filter parameters: {e}")
def _parse_zoom_level(self, request: HttpRequest) -> int:
"""Parse zoom level from request with default."""
try:
zoom_param = request.GET.get('zoom', '10')
zoom_level = int(zoom_param)
return max(1, min(20, zoom_level)) # Clamp between 1 and 20
except (ValueError, TypeError):
return 10 # Default zoom level
def _error_response(self, message: str, status: int = 400) -> JsonResponse:
"""Return standardized error response."""
return JsonResponse({
'status': 'error',
'message': message,
'data': None
}, status=status)
class MapLocationsView(MapAPIView):
"""
API endpoint for getting map locations with optional clustering.
GET /api/map/locations/
Parameters:
- north, south, east, west: Bounding box coordinates
- zoom: Zoom level (1-20)
- types: Comma-separated location types (park,ride,company,generic)
- cluster: Whether to enable clustering (true/false)
- q: Search query
- park_status: Park status filter
- ride_types: Ride type filter
- min_rating: Minimum rating filter
- country, state, city: Geographic filters
"""
@method_decorator(cache_page(300)) # Cache for 5 minutes
def get(self, request: HttpRequest) -> JsonResponse:
"""Get map locations with optional clustering and filtering."""
try:
# Parse parameters
bounds = self._parse_bounds(request)
filters = self._parse_filters(request)
zoom_level = self._parse_zoom_level(request)
# Clustering preference
cluster_param = request.GET.get('cluster', 'true')
enable_clustering = cluster_param.lower() in ['true', '1', 'yes']
# Cache preference
use_cache_param = request.GET.get('cache', 'true')
use_cache = use_cache_param.lower() in ['true', '1', 'yes']
# Get map data
response = unified_map_service.get_map_data(
bounds=bounds,
filters=filters,
zoom_level=zoom_level,
cluster=enable_clustering,
use_cache=use_cache
)
return JsonResponse(response.to_dict())
except ValidationError as e:
return self._error_response(str(e), 400)
except Exception as e:
return self._error_response(f"Internal server error: {str(e)}", 500)
class MapLocationDetailView(MapAPIView):
"""
API endpoint for getting detailed information about a specific location.
GET /api/map/locations/<type>/<id>/
"""
@method_decorator(cache_page(600)) # Cache for 10 minutes
def get(self, request: HttpRequest, location_type: str, location_id: int) -> JsonResponse:
"""Get detailed information for a specific location."""
try:
# Validate location type
if location_type not in [lt.value for lt in LocationType]:
return self._error_response(f"Invalid location type: {location_type}", 400)
# Get location details
location = unified_map_service.get_location_details(location_type, location_id)
if not location:
return self._error_response("Location not found", 404)
return JsonResponse({
'status': 'success',
'data': location.to_dict()
})
except Exception as e:
return self._error_response(f"Internal server error: {str(e)}", 500)
class MapSearchView(MapAPIView):
"""
API endpoint for searching locations by text query.
GET /api/map/search/
Parameters:
- q: Search query (required)
- north, south, east, west: Optional bounding box
- types: Comma-separated location types
- limit: Maximum results (default 50)
"""
def get(self, request: HttpRequest) -> JsonResponse:
"""Search locations by text query."""
try:
# Get search query
query = request.GET.get('q')
if not query:
return self._error_response("Search query 'q' parameter is required", 400)
# Parse optional parameters
bounds = self._parse_bounds(request)
# Parse location types
location_types = None
types_param = request.GET.get('types')
if types_param:
try:
location_types = {
LocationType(t.strip()) for t in types_param.split(',')
if t.strip() in [lt.value for lt in LocationType]
}
except ValueError:
return self._error_response("Invalid location types", 400)
# Parse limit
limit = min(100, max(1, int(request.GET.get('limit', '50'))))
# Perform search
locations = unified_map_service.search_locations(
query=query,
bounds=bounds,
location_types=location_types,
limit=limit
)
return JsonResponse({
'status': 'success',
'data': {
'locations': [loc.to_dict() for loc in locations],
'query': query,
'count': len(locations),
'limit': limit
}
})
except ValueError as e:
return self._error_response(str(e), 400)
except Exception as e:
return self._error_response(f"Internal server error: {str(e)}", 500)
class MapBoundsView(MapAPIView):
"""
API endpoint for getting locations within specific bounds.
GET /api/map/bounds/
Parameters:
- north, south, east, west: Bounding box coordinates (required)
- types: Comma-separated location types
- zoom: Zoom level
"""
@method_decorator(cache_page(300)) # Cache for 5 minutes
def get(self, request: HttpRequest) -> JsonResponse:
"""Get locations within specific geographic bounds."""
try:
# Parse required bounds
bounds = self._parse_bounds(request)
if not bounds:
return self._error_response(
"Bounds parameters required: north, south, east, west", 400
)
# Parse optional filters
location_types = None
types_param = request.GET.get('types')
if types_param:
location_types = {
LocationType(t.strip()) for t in types_param.split(',')
if t.strip() in [lt.value for lt in LocationType]
}
zoom_level = self._parse_zoom_level(request)
# Get locations within bounds
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(response.to_dict())
except ValidationError as e:
return self._error_response(str(e), 400)
except Exception as e:
return self._error_response(f"Internal server error: {str(e)}", 500)
class MapStatsView(MapAPIView):
"""
API endpoint for getting map service statistics and health information.
GET /api/map/stats/
"""
def get(self, request: HttpRequest) -> JsonResponse:
"""Get map service statistics and performance metrics."""
try:
stats = unified_map_service.get_service_stats()
return JsonResponse({
'status': 'success',
'data': stats
})
except Exception as e:
return self._error_response(f"Internal server error: {str(e)}", 500)
class MapCacheView(MapAPIView):
"""
API endpoint for cache management (admin only).
DELETE /api/map/cache/
POST /api/map/cache/invalidate/
"""
def delete(self, request: HttpRequest) -> JsonResponse:
"""Clear all map cache (admin only)."""
# TODO: Add admin permission check
try:
unified_map_service.invalidate_cache()
return JsonResponse({
'status': 'success',
'message': 'Map cache cleared successfully'
})
except Exception as e:
return self._error_response(f"Internal server error: {str(e)}", 500)
def post(self, request: HttpRequest) -> JsonResponse:
"""Invalidate specific cache entries."""
# TODO: Add admin permission check
try:
data = json.loads(request.body)
location_type = data.get('location_type')
location_id = data.get('location_id')
bounds_data = data.get('bounds')
bounds = None
if bounds_data:
bounds = GeoBounds(**bounds_data)
unified_map_service.invalidate_cache(
location_type=location_type,
location_id=location_id,
bounds=bounds
)
return JsonResponse({
'status': 'success',
'message': 'Cache invalidated successfully'
})
except (json.JSONDecodeError, TypeError, ValueError) as e:
return self._error_response(f"Invalid request data: {str(e)}", 400)
except Exception as e:
return self._error_response(f"Internal server error: {str(e)}", 500)

48
core/views/search.py Normal file
View File

@@ -0,0 +1,48 @@
from django.views.generic import TemplateView
from parks.models import Park
from parks.filters import ParkFilter
class AdaptiveSearchView(TemplateView):
template_name = "core/search/results.html"
def get_queryset(self):
"""
Get the base queryset, optimized with select_related and prefetch_related
"""
return Park.objects.select_related('owner').prefetch_related(
'location',
'photos'
).all()
def get_filterset(self):
"""
Get the filterset instance
"""
return ParkFilter(self.request.GET, queryset=self.get_queryset())
def get_context_data(self, **kwargs):
"""
Add filtered results and filter form to context
"""
context = super().get_context_data(**kwargs)
filterset = self.get_filterset()
context.update({
'results': filterset.qs,
'filters': filterset,
'applied_filters': bool(self.request.GET), # Check if any filters are applied
})
return context
class FilterFormView(TemplateView):
"""
View for rendering just the filter form for HTMX updates
"""
template_name = "core/search/filters.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
filterset = ParkFilter(self.request.GET, queryset=Park.objects.all())
context['filters'] = filterset
return context

61
core/views/views.py Normal file
View File

@@ -0,0 +1,61 @@
from typing import Any, Dict, Optional, Type, cast
from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic import DetailView
from django.views import View
from django.http import HttpRequest, HttpResponse
from django.db.models import Model
class SlugRedirectMixin(View):
"""
Mixin that handles redirects for old slugs.
Requires the model to inherit from SluggedModel and view to inherit from DetailView.
"""
model: Optional[Type[Model]] = None
slug_url_kwarg: str = 'slug'
object: Optional[Model] = None
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
# Only apply slug redirect logic to DetailViews
if not isinstance(self, DetailView):
return super().dispatch(request, *args, **kwargs)
# Get the object using current or historical slug
try:
self.object = self.get_object() # type: ignore
# Check if we used an old slug
current_slug = kwargs.get(self.slug_url_kwarg)
if current_slug and current_slug != getattr(self.object, 'slug', None):
# Get the URL pattern name from the view
url_pattern = self.get_redirect_url_pattern()
# Build kwargs for reverse()
reverse_kwargs = self.get_redirect_url_kwargs()
# Redirect to the current slug URL
return redirect(
reverse(url_pattern, kwargs=reverse_kwargs),
permanent=True
)
return super().dispatch(request, *args, **kwargs)
except (AttributeError, Exception) as e: # type: ignore
if self.model and hasattr(self.model, 'DoesNotExist'):
if isinstance(e, self.model.DoesNotExist): # type: ignore
return super().dispatch(request, *args, **kwargs)
return super().dispatch(request, *args, **kwargs)
def get_redirect_url_pattern(self) -> str:
"""
Get the URL pattern name for redirects.
Should be overridden by subclasses.
"""
raise NotImplementedError(
"Subclasses must implement get_redirect_url_pattern()"
)
def get_redirect_url_kwargs(self) -> Dict[str, Any]:
"""
Get the kwargs for reverse() when redirecting.
Should be overridden by subclasses if they need custom kwargs.
"""
if not self.object:
return {}
return {self.slug_url_kwarg: getattr(self.object, 'slug', '')}