Refactor test utilities and enhance ASGI settings

- 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.
This commit is contained in:
pacnpal
2025-08-20 19:51:59 -04:00
parent 69c07d1381
commit 66ed4347a9
230 changed files with 15094 additions and 11578 deletions

View File

@@ -5,15 +5,10 @@ Provides web interfaces for map functionality with HTMX integration.
import json
from typing import Dict, Any, Optional, Set
from django.shortcuts import render, get_object_or_404
from django.shortcuts import render
from django.http import JsonResponse, HttpRequest, HttpResponse
from django.views.generic import TemplateView, View
from django.views.decorators.http import require_http_methods
from django.utils.decorators import method_decorator
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.paginator import Paginator
from django.core.exceptions import ValidationError
from django.db.models import Q
from ..services.map_service import unified_map_service
from ..services.data_structures import GeoBounds, MapFilters, LocationType
@@ -21,29 +16,30 @@ 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/',
"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,
"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')
types_param = request.GET.get("types")
if types_param:
try:
return {
LocationType(t.strip()) for t in types_param.split(',')
LocationType(t.strip())
for t in types_param.split(",")
if t.strip() in [lt.value for lt in LocationType]
}
except ValueError:
@@ -54,122 +50,141 @@ class MapViewMixin:
class UniversalMapView(MapViewMixin, TemplateView):
"""
Main universal map view showing all location types.
URL: /maps/
"""
template_name = 'maps/universal_map.html'
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,
})
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']):
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']),
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'
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,
})
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'
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
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)
search_radius = min(200, max(1, float(radius))) # Clamp between 1-200km
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,
})
# 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'
context["error"] = "Invalid coordinates provided"
else:
context.update({
'page_title': 'Nearby Locations',
'map_type': 'nearby',
'prompt_for_location': True,
})
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()
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]):
@@ -178,108 +193,107 @@ class LocationFilterView(MapViewMixin, View):
search_query=search_query or None,
country=country or None,
state=state or None,
has_coordinates=True
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'
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
})
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)
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()
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'
})
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'))))
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
query=query, location_types=location_types, limit=limit
)
return render(request, 'maps/partials/search_results.html', {
'results': results,
'query': query,
'count': len(results)
})
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)
})
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'])
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))
zoom_level = int(data.get("zoom", 10))
location_types = None
if 'types' in data:
if "types" in data:
location_types = {
LocationType(t) for t in data['types']
LocationType(t)
for t in data["types"]
if t in [lt.value for lt in LocationType]
}
# Create filters if needed
filters = None
if location_types:
filters = MapFilters(location_types=location_types)
# 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,
@@ -287,79 +301,86 @@ class MapBoundsUpdateView(MapViewMixin, View):
east=bounds.east,
west=bounds.west,
location_types=location_types,
zoom_level=zoom_level
zoom_level=zoom_level,
)
return JsonResponse({
'status': 'success',
'data': map_response.to_dict()
})
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)
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)
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:
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}'
})
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)
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
})
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)
})
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'
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()
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]):
@@ -368,33 +389,33 @@ class LocationListView(MapViewMixin, TemplateView):
search_query=search_query or None,
country=country or None,
state=state or None,
has_coordinates=True
has_coordinates=True,
)
# Get locations without clustering
map_response = unified_map_service.get_map_data(
filters=filters,
cluster=False,
use_cache=True
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_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,
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
)
return context