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

@@ -3,17 +3,26 @@ from core.mixins import HTMXFilterableMixin
from .models.location import ParkLocation
from media.models import Photo
from moderation.models import EditSubmission
from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin
from moderation.mixins import (
EditSubmissionMixin,
PhotoSubmissionMixin,
HistoryMixin,
)
from core.views.views import SlugRedirectMixin
from .filters import ParkFilter
from .forms import ParkForm
from .models import Park, ParkArea, ParkReview as Review
from django.http import HttpResponseRedirect, HttpResponse, HttpRequest, JsonResponse
from django.http import (
HttpResponseRedirect,
HttpResponse,
HttpRequest,
JsonResponse,
)
from django.core.exceptions import ObjectDoesNotExist
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q, Count, QuerySet
from django.db.models import QuerySet
from django.urls import reverse
from django.shortcuts import get_object_or_404, render
from decimal import InvalidOperation
@@ -25,7 +34,9 @@ from typing import Any, Optional, cast, Literal
# Constants
PARK_DETAIL_URL = "parks:park_detail"
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 (*)."
REQUIRED_FIELDS_ERROR = (
"Please correct the errors below. Required fields are marked with an asterisk (*)."
)
ALLOWED_ROLES = ["MODERATOR", "ADMIN", "SUPERUSER"]
@@ -37,70 +48,69 @@ def normalize_osm_result(result: dict) -> dict:
from .location_utils import get_english_name, normalize_coordinate
# Get address details
address = result.get('address', {})
address = result.get("address", {})
# Normalize coordinates
lat = normalize_coordinate(float(result.get('lat')), 9, 6)
lon = normalize_coordinate(float(result.get('lon')), 10, 6)
lat = normalize_coordinate(float(result.get("lat")), 9, 6)
lon = normalize_coordinate(float(result.get("lon")), 10, 6)
# Get English names where possible
name = ''
if 'namedetails' in result:
name = get_english_name(result['namedetails'])
name = ""
if "namedetails" in result:
name = get_english_name(result["namedetails"])
# Build street address from available components
street_parts = []
if address.get('house_number'):
street_parts.append(address['house_number'])
if address.get('road') or address.get('street'):
street_parts.append(address.get('road') or address.get('street'))
elif address.get('pedestrian'):
street_parts.append(address['pedestrian'])
elif address.get('footway'):
street_parts.append(address['footway'])
if address.get("house_number"):
street_parts.append(address["house_number"])
if address.get("road") or address.get("street"):
street_parts.append(address.get("road") or address.get("street"))
elif address.get("pedestrian"):
street_parts.append(address["pedestrian"])
elif address.get("footway"):
street_parts.append(address["footway"])
# Handle additional address components
suburb = address.get('suburb', '')
district = address.get('district', '')
neighborhood = address.get('neighbourhood', '')
suburb = address.get("suburb", "")
district = address.get("district", "")
neighborhood = address.get("neighbourhood", "")
# Build city from available components
city = (address.get('city') or
address.get('town') or
address.get('village') or
address.get('municipality') or
'')
city = (
address.get("city")
or address.get("town")
or address.get("village")
or address.get("municipality")
or ""
)
# Get detailed state/region information
state = (address.get('state') or
address.get('province') or
address.get('region') or
'')
state = (
address.get("state") or address.get("province") or address.get("region") or ""
)
# Get postal code with fallbacks
postal_code = (address.get('postcode') or
address.get('postal_code') or
'')
postal_code = address.get("postcode") or address.get("postal_code") or ""
return {
'display_name': name or result.get('display_name', ''),
'lat': lat,
'lon': lon,
'street': ' '.join(street_parts).strip(),
'suburb': suburb,
'district': district,
'neighborhood': neighborhood,
'city': city,
'state': state,
'country': address.get('country', ''),
'postal_code': postal_code,
"display_name": name or result.get("display_name", ""),
"lat": lat,
"lon": lon,
"street": " ".join(street_parts).strip(),
"suburb": suburb,
"district": district,
"neighborhood": neighborhood,
"city": city,
"state": state,
"country": address.get("country", ""),
"postal_code": postal_code,
}
def get_view_mode(request: HttpRequest) -> ViewMode:
"""Get the current view mode from request, defaulting to grid"""
view_mode = request.GET.get('view_mode', 'grid')
return cast(ViewMode, 'list' if view_mode == 'list' else 'grid')
view_mode = request.GET.get("view_mode", "grid")
return cast(ViewMode, "list" if view_mode == "list" else "grid")
def add_park_button(request: HttpRequest) -> HttpResponse:
@@ -116,7 +126,7 @@ def park_actions(request: HttpRequest, slug: str) -> HttpResponse:
def get_park_areas(request: HttpRequest) -> HttpResponse:
"""Return park areas as options for a select element"""
park_id = request.GET.get('park')
park_id = request.GET.get("park")
if not park_id:
return HttpResponse('<option value="">Select a park first</option>')
@@ -124,11 +134,10 @@ def get_park_areas(request: HttpRequest) -> HttpResponse:
park = Park.objects.get(id=park_id)
areas = park.areas.all()
options = ['<option value="">No specific area</option>']
options.extend([
f'<option value="{area.id}">{area.name}</option>'
for area in areas
])
return HttpResponse('\n'.join(options))
options.extend(
[f'<option value="{area.id}">{area.name}</option>' for area in areas]
)
return HttpResponse("\n".join(options))
except Park.DoesNotExist:
return HttpResponse('<option value="">Invalid park selected</option>')
@@ -150,15 +159,15 @@ def location_search(request: HttpRequest) -> JsonResponse:
"limit": 10,
},
headers={"User-Agent": "ThrillWiki/1.0"},
timeout=60
timeout=60,
)
if response.status_code == 200:
results = response.json()
normalized_results = [normalize_osm_result(
result) for result in results]
normalized_results = [normalize_osm_result(result) for result in results]
valid_results = [
r for r in normalized_results
r
for r in normalized_results
if r["lat"] is not None and r["lon"] is not None
]
return JsonResponse({"results": valid_results})
@@ -181,9 +190,13 @@ def reverse_geocode(request: HttpRequest) -> JsonResponse:
lon = lon.quantize(Decimal("0.000001"), rounding=ROUND_DOWN)
if lat < -90 or lat > 90:
return JsonResponse({"error": "Latitude must be between -90 and 90"}, status=400)
return JsonResponse(
{"error": "Latitude must be between -90 and 90"}, status=400
)
if lon < -180 or lon > 180:
return JsonResponse({"error": "Longitude must be between -180 and 180"}, status=400)
return JsonResponse(
{"error": "Longitude must be between -180 and 180"}, status=400
)
response = requests.get(
"https://nominatim.openstreetmap.org/reverse",
@@ -196,7 +209,7 @@ def reverse_geocode(request: HttpRequest) -> JsonResponse:
"accept-language": "en",
},
headers={"User-Agent": "ThrillWiki/1.0"},
timeout=60
timeout=60,
)
if response.status_code == 200:
@@ -242,51 +255,51 @@ class ParkListView(HTMXFilterableMixin, ListView):
"""Add view_mode and other context data"""
try:
# Initialize filterset even if queryset fails
if not hasattr(self, 'filterset'):
if not hasattr(self, "filterset"):
self.filterset = self.filter_class(
self.request.GET,
queryset=self.model.objects.none()
self.request.GET, queryset=self.model.objects.none()
)
context = super().get_context_data(**kwargs)
context.update({
'view_mode': self.get_view_mode(),
'is_search': bool(self.request.GET.get('search')),
'search_query': self.request.GET.get('search', '')
})
context.update(
{
"view_mode": self.get_view_mode(),
"is_search": bool(self.request.GET.get("search")),
"search_query": self.request.GET.get("search", ""),
}
)
return context
except Exception as e:
messages.error(self.request, f"Error applying filters: {str(e)}")
# Ensure filterset exists in error case
if not hasattr(self, 'filterset'):
if not hasattr(self, "filterset"):
self.filterset = self.filter_class(
self.request.GET,
queryset=self.model.objects.none()
self.request.GET, queryset=self.model.objects.none()
)
return {
'filter': self.filterset,
'error': "Unable to apply filters. Please try adjusting your criteria.",
'view_mode': self.get_view_mode(),
'is_search': bool(self.request.GET.get('search')),
'search_query': self.request.GET.get('search', '')
"filter": self.filterset,
"error": "Unable to apply filters. Please try adjusting your criteria.",
"view_mode": self.get_view_mode(),
"is_search": bool(self.request.GET.get("search")),
"search_query": self.request.GET.get("search", ""),
}
def search_parks(request: HttpRequest) -> HttpResponse:
"""Search parks and return results using park_list_item.html"""
try:
search_query = request.GET.get('search', '').strip()
search_query = request.GET.get("search", "").strip()
if not search_query:
return HttpResponse('')
return HttpResponse("")
# Get current view mode from request
current_view_mode = request.GET.get('view_mode', 'grid')
park_filter = ParkFilter({
'search': search_query
}, queryset=get_base_park_queryset())
current_view_mode = request.GET.get("view_mode", "grid")
park_filter = ParkFilter(
{"search": search_query}, queryset=get_base_park_queryset()
)
parks = park_filter.qs
if request.GET.get('quick_search'):
if request.GET.get("quick_search"):
parks = parks[:8] # Limit quick search results
response = render(
@@ -296,10 +309,10 @@ def search_parks(request: HttpRequest) -> HttpResponse:
"parks": parks,
"view_mode": current_view_mode,
"search_query": search_query,
"is_search": True
}
"is_search": True,
},
)
response['HX-Trigger'] = 'searchComplete'
response["HX-Trigger"] = "searchComplete"
return response
except Exception as e:
@@ -309,10 +322,10 @@ def search_parks(request: HttpRequest) -> HttpResponse:
{
"parks": [],
"error": f"Error performing search: {str(e)}",
"is_search": True
}
"is_search": True,
},
)
response['HX-Trigger'] = 'searchError'
response["HX-Trigger"] = "searchError"
return response
@@ -329,8 +342,12 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
data["opening_date"] = data["opening_date"].isoformat()
if data.get("closing_date"):
data["closing_date"] = data["closing_date"].isoformat()
decimal_fields = ["latitude", "longitude",
"size_acres", "average_rating"]
decimal_fields = [
"latitude",
"longitude",
"size_acres",
"average_rating",
]
for field in decimal_fields:
if data.get(field):
data[field] = str(data[field])
@@ -361,9 +378,10 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
source=self.request.POST.get("source", ""),
)
if hasattr(self.request.user, "role") and getattr(
self.request.user, "role", None
) in ALLOWED_ROLES:
if (
hasattr(self.request.user, "role")
and getattr(self.request.user, "role", None) in ALLOWED_ROLES
):
try:
self.object = form.save()
submission.object_id = self.object.id
@@ -378,16 +396,18 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
park_location, created = ParkLocation.objects.get_or_create(
park=self.object,
defaults={
'street_address': form.cleaned_data.get("street_address", ""),
'city': form.cleaned_data.get("city", ""),
'state': form.cleaned_data.get("state", ""),
'country': form.cleaned_data.get("country", "USA"),
'postal_code': form.cleaned_data.get("postal_code", ""),
}
"street_address": form.cleaned_data.get(
"street_address", ""
),
"city": form.cleaned_data.get("city", ""),
"state": form.cleaned_data.get("state", ""),
"country": form.cleaned_data.get("country", "USA"),
"postal_code": form.cleaned_data.get("postal_code", ""),
},
)
park_location.set_coordinates(
form.cleaned_data["latitude"],
form.cleaned_data["longitude"]
form.cleaned_data["longitude"],
)
park_location.save()
@@ -398,15 +418,16 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
Photo.objects.create(
image=photo_file,
uploaded_by=self.request.user,
content_type=ContentType.objects.get_for_model(
Park),
content_type=ContentType.objects.get_for_model(Park),
object_id=self.object.id,
)
uploaded_count += 1
except Exception as e:
messages.error(
self.request,
f"Error uploading photo {photo_file.name}: {str(e)}",
f"Error uploading photo {
photo_file.name}: {
str(e)}",
)
messages.success(
@@ -418,14 +439,15 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
except Exception as e:
messages.error(
self.request,
f"Error creating park: {str(e)}. Please check your input and try again.",
f"Error creating park: {
str(e)}. Please check your input and try again.",
)
return self.form_invalid(form)
messages.success(
self.request,
"Your park submission has been sent for review. "
"You will be notified when it is approved."
"You will be notified when it is approved.",
)
for field, errors in form.errors.items():
for error in errors:
@@ -454,8 +476,12 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
data["opening_date"] = data["opening_date"].isoformat()
if data.get("closing_date"):
data["closing_date"] = data["closing_date"].isoformat()
decimal_fields = ["latitude", "longitude",
"size_acres", "average_rating"]
decimal_fields = [
"latitude",
"longitude",
"size_acres",
"average_rating",
]
for field in decimal_fields:
if data.get(field):
data[field] = str(data[field])
@@ -487,9 +513,10 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
source=self.request.POST.get("source", ""),
)
if hasattr(self.request.user, "role") and getattr(
self.request.user, "role", None
) in ALLOWED_ROLES:
if (
hasattr(self.request.user, "role")
and getattr(self.request.user, "role", None) in ALLOWED_ROLES
):
try:
self.object = form.save()
submission.status = "APPROVED"
@@ -513,43 +540,45 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
park_location = self.object.location
# Update existing location
for key, value in location_data.items():
if key in ['latitude', 'longitude'] and value:
if key in ["latitude", "longitude"] and value:
continue # Handle coordinates separately
if hasattr(park_location, key):
setattr(park_location, key, value)
# Handle coordinates if provided
if 'latitude' in location_data and 'longitude' in location_data:
if location_data['latitude'] and location_data['longitude']:
if "latitude" in location_data and "longitude" in location_data:
if location_data["latitude"] and location_data["longitude"]:
park_location.set_coordinates(
float(location_data['latitude']),
float(location_data['longitude'])
float(location_data["latitude"]),
float(location_data["longitude"]),
)
park_location.save()
except ParkLocation.DoesNotExist:
# Create new ParkLocation
coordinates_data = {}
if 'latitude' in location_data and 'longitude' in location_data:
if location_data['latitude'] and location_data['longitude']:
if "latitude" in location_data and "longitude" in location_data:
if location_data["latitude"] and location_data["longitude"]:
coordinates_data = {
'latitude': float(location_data['latitude']),
'longitude': float(location_data['longitude'])
"latitude": float(location_data["latitude"]),
"longitude": float(location_data["longitude"]),
}
# Remove coordinate fields from location_data for creation
creation_data = {k: v for k, v in location_data.items()
if k not in ['latitude', 'longitude']}
creation_data.setdefault('country', 'USA')
creation_data = {
k: v
for k, v in location_data.items()
if k not in ["latitude", "longitude"]
}
creation_data.setdefault("country", "USA")
park_location = ParkLocation.objects.create(
park=self.object,
**creation_data
park=self.object, **creation_data
)
if coordinates_data:
park_location.set_coordinates(
coordinates_data['latitude'],
coordinates_data['longitude']
coordinates_data["latitude"],
coordinates_data["longitude"],
)
park_location.save()
@@ -560,15 +589,16 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
Photo.objects.create(
image=photo_file,
uploaded_by=self.request.user,
content_type=ContentType.objects.get_for_model(
Park),
content_type=ContentType.objects.get_for_model(Park),
object_id=self.object.id,
)
uploaded_count += 1
except Exception as e:
messages.error(
self.request,
f"Error uploading photo {photo_file.name}: {str(e)}",
f"Error uploading photo {
photo_file.name}: {
str(e)}",
)
messages.success(
@@ -580,7 +610,8 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
except Exception as e:
messages.error(
self.request,
f"Error updating park: {str(e)}. Please check your input and try again.",
f"Error updating park: {
str(e)}. Please check your input and try again.",
)
return self.form_invalid(form)
@@ -594,10 +625,7 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
)
def form_invalid(self, form: ParkForm) -> HttpResponse:
messages.error(
self.request,
REQUIRED_FIELDS_ERROR
)
messages.error(self.request, REQUIRED_FIELDS_ERROR)
for field, errors in form.errors.items():
for error in errors:
messages.error(self.request, f"{field}: {error}")
@@ -612,7 +640,7 @@ class ParkDetailView(
EditSubmissionMixin,
PhotoSubmissionMixin,
HistoryMixin,
DetailView
DetailView,
):
model = Park
template_name = "parks/park_detail.html"
@@ -633,11 +661,7 @@ class ParkDetailView(
super()
.get_queryset()
.prefetch_related(
"rides",
"rides__manufacturer",
"photos",
"areas",
"location"
"rides", "rides__manufacturer", "photos", "areas", "location"
),
)
@@ -667,7 +691,7 @@ class ParkAreaDetailView(
EditSubmissionMixin,
PhotoSubmissionMixin,
HistoryMixin,
DetailView
DetailView,
):
model = ParkArea
template_name = "parks/area_detail.html"