diff --git a/parks/views.py b/parks/views.py index c0c686fa..a0c4e148 100644 --- a/parks/views.py +++ b/parks/views.py @@ -1,3 +1,24 @@ +from .querysets import get_base_park_queryset +from search.mixins import HTMXFilterableMixin +from reviews.models import Review +from location.models import Location +from media.models import Photo +from moderation.models import EditSubmission +from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin +from core.views import SlugRedirectMixin +from .filters import ParkFilter +from .forms import ParkForm +from .models import Park, ParkArea +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.urls import reverse +from django.shortcuts import get_object_or_404, render +from decimal import InvalidOperation +from django.views.generic import DetailView, ListView, CreateView, UpdateView import requests from decimal import Decimal, ROUND_DOWN from typing import Any, Optional, cast, Literal @@ -8,26 +29,6 @@ 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 (*)." ALLOWED_ROLES = ["MODERATOR", "ADMIN", "SUPERUSER"] -from django.views.generic import DetailView, ListView, CreateView, UpdateView -from decimal import InvalidOperation -from django.shortcuts import get_object_or_404, render -from django.urls import reverse -from django.db.models import Q, Count, QuerySet -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.contenttypes.models import ContentType -from django.contrib import messages -from django.core.exceptions import ObjectDoesNotExist -from django.http import HttpResponseRedirect, HttpResponse, HttpRequest, JsonResponse -from .models import Park, ParkArea -from .forms import ParkForm -from .filters import ParkFilter -from core.views import SlugRedirectMixin -from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin -from moderation.models import EditSubmission -from media.models import Photo -from location.models import Location -from reviews.models import Review -from search.mixins import HTMXFilterableMixin ViewMode = Literal["grid", "list"] @@ -35,19 +36,19 @@ ViewMode = Literal["grid", "list"] def normalize_osm_result(result: dict) -> dict: """Normalize OpenStreetMap result to a consistent format with enhanced address details""" from .location_utils import get_english_name, normalize_coordinate - + # Get address details address = result.get('address', {}) - + # Normalize coordinates 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']) - + # Build street address from available components street_parts = [] if address.get('house_number'): @@ -58,30 +59,30 @@ def normalize_osm_result(result: dict) -> dict: 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', '') - + # Build city from available components 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 '') - + # Get postal code with fallbacks postal_code = (address.get('postcode') or - address.get('postal_code') or - '') - + address.get('postal_code') or + '') + return { 'display_name': name or result.get('display_name', ''), 'lat': lat, @@ -96,29 +97,30 @@ def normalize_osm_result(result: dict) -> dict: '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') -from .querysets import get_base_park_queryset - def add_park_button(request: HttpRequest) -> HttpResponse: """Return the add park button partial template""" return render(request, "parks/partials/add_park_button.html") + def park_actions(request: HttpRequest, slug: str) -> HttpResponse: """Return the park actions partial template""" park = get_object_or_404(Park, slug=slug) return render(request, "parks/partials/park_actions.html", {"park": park}) + def get_park_areas(request: HttpRequest) -> HttpResponse: """Return park areas as options for a select element""" park_id = request.GET.get('park') if not park_id: return HttpResponse('') - + try: park = Park.objects.get(id=park_id) areas = park.areas.all() @@ -131,6 +133,7 @@ def get_park_areas(request: HttpRequest) -> HttpResponse: except Park.DoesNotExist: return HttpResponse('') + def location_search(request: HttpRequest) -> JsonResponse: """Search for locations using OpenStreetMap Nominatim API""" query = request.GET.get("q", "") @@ -153,7 +156,8 @@ def location_search(request: HttpRequest) -> JsonResponse: 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 if r["lat"] is not None and r["lon"] is not None @@ -162,6 +166,7 @@ def location_search(request: HttpRequest) -> JsonResponse: return JsonResponse({"results": []}) + def reverse_geocode(request: HttpRequest) -> JsonResponse: """Reverse geocode coordinates using OpenStreetMap Nominatim API""" try: @@ -217,11 +222,11 @@ class ParkListView(HTMXFilterableMixin, ListView): if self.request.htmx: return ["parks/partials/park_list_item.html"] return [self.template_name] - + def get_view_mode(self) -> ViewMode: """Get the current view mode (grid or list)""" return get_view_mode(self.request) - + def get_queryset(self) -> QuerySet[Park]: """Get base queryset with annotations and apply filters""" try: @@ -229,7 +234,7 @@ class ParkListView(HTMXFilterableMixin, ListView): except Exception as e: messages.error(self.request, f"Error loading parks: {str(e)}") queryset = self.model.objects.none() - + # Always initialize filterset, even if queryset failed self.filterset = self.get_filter_class()(self.request.GET, queryset=queryset) return self.filterset.qs @@ -243,7 +248,7 @@ class ParkListView(HTMXFilterableMixin, ListView): self.request.GET, queryset=self.model.objects.none() ) - + context = super().get_context_data(**kwargs) context.update({ 'view_mode': self.get_view_mode(), @@ -311,6 +316,7 @@ def search_parks(request: HttpRequest) -> HttpResponse: response['HX-Trigger'] = 'searchError' return response + class ParkCreateView(LoginRequiredMixin, CreateView): model = Park form_class = ParkForm @@ -324,7 +330,8 @@ 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]) @@ -375,7 +382,8 @@ class ParkCreateView(LoginRequiredMixin, CreateView): location_type="park", latitude=form.cleaned_data["latitude"], longitude=form.cleaned_data["longitude"], - street_address=form.cleaned_data.get("street_address", ""), + 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", ""), @@ -389,7 +397,8 @@ 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 @@ -444,7 +453,8 @@ 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]) @@ -516,7 +526,8 @@ 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 @@ -651,5 +662,3 @@ class ParkAreaDetailView( def get_redirect_url_kwargs(self) -> dict[str, str]: area = cast(ParkArea, self.object) return {"park_slug": area.park.slug, "area_slug": area.slug} - -