from decimal import Decimal, ROUND_DOWN from django.views.generic import DetailView, ListView, CreateView, UpdateView from django.shortcuts import get_object_or_404, render from django.core.serializers.json import DjangoJSONEncoder from django.urls import reverse from django.db.models import Q from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType from django.contrib import messages from django.http import JsonResponse, HttpResponseRedirect, HttpResponse import requests from .models import Park, ParkArea from .forms import ParkForm from .location_utils import normalize_coordinate, normalize_osm_result from core.views import SlugRedirectMixin from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin from moderation.models import EditSubmission from media.models import Photo def location_search(request): """Search for locations using OpenStreetMap Nominatim API""" query = request.GET.get("q", "") if not query: return JsonResponse({"results": []}) # Call Nominatim API response = requests.get( "https://nominatim.openstreetmap.org/search", params={ "q": query, "format": "json", "addressdetails": 1, "namedetails": 1, # Include name tags "accept-language": "en", # Prefer English results "limit": 10, }, headers={"User-Agent": "ThrillWiki/1.0"}, ) if response.status_code == 200: results = response.json() # Normalize each result normalized_results = [normalize_osm_result(result) for result in results] # Filter out any results with invalid coordinates valid_results = [r for r in normalized_results if r['lat'] is not None and r['lon'] is not None] return JsonResponse({"results": valid_results}) return JsonResponse({"results": []}) def reverse_geocode(request): """Reverse geocode coordinates using OpenStreetMap Nominatim API""" try: lat = Decimal(request.GET.get("lat", "")) lon = Decimal(request.GET.get("lon", "")) except (TypeError, ValueError, InvalidOperation): return JsonResponse({"error": "Invalid coordinates"}, status=400) if not lat or not lon: return JsonResponse({"error": "Missing coordinates"}, status=400) # Normalize coordinates before geocoding lat = lat.quantize(Decimal('0.000001'), rounding=ROUND_DOWN) lon = lon.quantize(Decimal('0.000001'), rounding=ROUND_DOWN) # Validate ranges if lat < -90 or lat > 90: 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) # Call Nominatim API response = requests.get( "https://nominatim.openstreetmap.org/reverse", params={ "lat": str(lat), "lon": str(lon), "format": "json", "addressdetails": 1, "namedetails": 1, # Include name tags "accept-language": "en", # Prefer English results }, headers={"User-Agent": "ThrillWiki/1.0"}, ) if response.status_code == 200: result = response.json() # Normalize the result normalized_result = normalize_osm_result(result) if normalized_result['lat'] is None or normalized_result['lon'] is None: return JsonResponse({"error": "Invalid coordinates"}, status=400) return JsonResponse(normalized_result) return JsonResponse({"error": "Geocoding failed"}, status=500) class ParkListView(ListView): model = Park template_name = "parks/park_list.html" context_object_name = "parks" def get_queryset(self): queryset = Park.objects.select_related("owner").prefetch_related("photos") search = self.request.GET.get("search", "").strip() country = self.request.GET.get("country", "").strip() region = self.request.GET.get("region", "").strip() city = self.request.GET.get("city", "").strip() statuses = self.request.GET.getlist("status") if search: queryset = queryset.filter( Q(name__icontains=search) | Q(city__icontains=search) | Q(state__icontains=search) | Q(country__icontains=search) ) if country: queryset = queryset.filter(country__icontains=country) if region: queryset = queryset.filter(state__icontains=region) if city: queryset = queryset.filter(city__icontains=city) if statuses: queryset = queryset.filter(status__in=statuses) return queryset def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["current_filters"] = { "search": self.request.GET.get("search", ""), "country": self.request.GET.get("country", ""), "region": self.request.GET.get("region", ""), "city": self.request.GET.get("city", ""), "statuses": self.request.GET.getlist("status"), } return context def get(self, request, *args, **kwargs): # Check if this is an HTMX request if request.htmx: # If it is, return just the parks list partial self.template_name = "parks/partials/park_list.html" return super().get(request, *args, **kwargs) class ParkDetailView( SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin, DetailView, ): model = Park template_name = "parks/park_detail.html" context_object_name = "park" def get_object(self, queryset=None): if queryset is None: queryset = self.get_queryset() slug = self.kwargs.get(self.slug_url_kwarg) # Try to get by current or historical slug return self.model.get_by_slug(slug)[0] def get_queryset(self): return super().get_queryset().prefetch_related( 'rides', 'rides__manufacturer', 'photos', 'areas' ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["areas"] = self.object.areas.all() # Get rides ordered by status (operating first) and name context["rides"] = self.object.rides.all().order_by( '-status', # OPERATING will come before others 'name' ) return context def get_redirect_url_pattern(self): return "parks:park_detail" class ParkCreateView(LoginRequiredMixin, CreateView): model = Park form_class = ParkForm template_name = "parks/park_form.html" def prepare_changes_data(self, cleaned_data): data = cleaned_data.copy() # Convert model instances to IDs for JSON serialization if data.get("owner"): data["owner"] = data["owner"].id # Convert dates to ISO format strings if data.get("opening_date"): data["opening_date"] = data["opening_date"].isoformat() if data.get("closing_date"): data["closing_date"] = data["closing_date"].isoformat() # Convert Decimal fields to strings decimal_fields = ["latitude", "longitude", "size_acres", "average_rating"] for field in decimal_fields: if data.get(field): data[field] = str(data[field]) return data def form_valid(self, form): # Normalize coordinates before saving if form.cleaned_data.get("latitude"): lat = Decimal(str(form.cleaned_data["latitude"])) form.cleaned_data["latitude"] = lat.quantize(Decimal('0.000001'), rounding=ROUND_DOWN) if form.cleaned_data.get("longitude"): lon = Decimal(str(form.cleaned_data["longitude"])) form.cleaned_data["longitude"] = lon.quantize(Decimal('0.000001'), rounding=ROUND_DOWN) changes = self.prepare_changes_data(form.cleaned_data) # Create submission record submission = EditSubmission.objects.create( user=self.request.user, content_type=ContentType.objects.get_for_model(Park), submission_type="CREATE", changes=changes, reason=self.request.POST.get("reason", ""), source=self.request.POST.get("source", ""), ) # If user is moderator or above, auto-approve if self.request.user.role in ["MODERATOR", "ADMIN", "SUPERUSER"]: try: self.object = form.save() submission.object_id = self.object.id submission.status = "APPROVED" submission.handled_by = self.request.user submission.save() # Handle photo uploads photos = self.request.FILES.getlist("photos") for photo_file in photos: try: Photo.objects.create( image=photo_file, uploaded_by=self.request.user, content_type=ContentType.objects.get_for_model(Park), object_id=self.object.id, ) except Exception as e: messages.error( self.request, f"Error uploading photo {photo_file.name}: {str(e)}", ) messages.success( self.request, f"Successfully created {self.object.name}. " f"Added {len(photos)} photo(s).", ) return HttpResponseRedirect(self.get_success_url()) except Exception as e: messages.error( self.request, 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.", ) return HttpResponseRedirect(reverse("parks:park_list")) def form_invalid(self, form): messages.error( self.request, "Please correct the errors below. Required fields are marked with an asterisk (*).", ) for field, errors in form.errors.items(): for error in errors: messages.error(self.request, f"{field}: {error}") return super().form_invalid(form) def get_success_url(self): return reverse("parks:park_detail", kwargs={"slug": self.object.slug}) class ParkUpdateView(LoginRequiredMixin, UpdateView): model = Park form_class = ParkForm template_name = "parks/park_form.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["is_edit"] = True return context def prepare_changes_data(self, cleaned_data): data = cleaned_data.copy() # Convert model instances to IDs for JSON serialization if data.get("owner"): data["owner"] = data["owner"].id # Convert dates to ISO format strings if data.get("opening_date"): data["opening_date"] = data["opening_date"].isoformat() if data.get("closing_date"): data["closing_date"] = data["closing_date"].isoformat() # Convert Decimal fields to strings decimal_fields = ["latitude", "longitude", "size_acres", "average_rating"] for field in decimal_fields: if data.get(field): data[field] = str(data[field]) return data def form_valid(self, form): # Normalize coordinates before saving if form.cleaned_data.get("latitude"): lat = Decimal(str(form.cleaned_data["latitude"])) form.cleaned_data["latitude"] = lat.quantize(Decimal('0.000001'), rounding=ROUND_DOWN) if form.cleaned_data.get("longitude"): lon = Decimal(str(form.cleaned_data["longitude"])) form.cleaned_data["longitude"] = lon.quantize(Decimal('0.000001'), rounding=ROUND_DOWN) changes = self.prepare_changes_data(form.cleaned_data) # Create submission record submission = EditSubmission.objects.create( user=self.request.user, content_type=ContentType.objects.get_for_model(Park), object_id=self.object.id, submission_type="EDIT", changes=changes, reason=self.request.POST.get("reason", ""), source=self.request.POST.get("source", ""), ) # If user is moderator or above, auto-approve if self.request.user.role in ["MODERATOR", "ADMIN", "SUPERUSER"]: try: self.object = form.save() submission.status = "APPROVED" submission.handled_by = self.request.user submission.save() # Handle photo uploads photos = self.request.FILES.getlist("photos") uploaded_count = 0 for photo_file in photos: try: Photo.objects.create( image=photo_file, uploaded_by=self.request.user, 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)}", ) messages.success( self.request, f"Successfully updated {self.object.name}. " f"Added {uploaded_count} new photo(s).", ) return HttpResponseRedirect(self.get_success_url()) except Exception as e: messages.error( self.request, f"Error updating park: {str(e)}. Please check your input and try again.", ) return self.form_invalid(form) messages.success( self.request, f"Your changes to {self.object.name} have been sent for review. " "You will be notified when they are approved.", ) return HttpResponseRedirect( reverse("parks:park_detail", kwargs={"slug": self.object.slug}) ) def form_invalid(self, form): messages.error( self.request, "Please correct the errors below. Required fields are marked with an asterisk (*).", ) for field, errors in form.errors.items(): for error in errors: messages.error(self.request, f"{field}: {error}") return super().form_invalid(form) def get_success_url(self): return reverse("parks:park_detail", kwargs={"slug": self.object.slug}) class ParkAreaDetailView( SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin, DetailView, ): model = ParkArea template_name = "parks/area_detail.html" context_object_name = "area" slug_url_kwarg = "area_slug" def get_object(self, queryset=None): if queryset is None: queryset = self.get_queryset() park_slug = self.kwargs.get("park_slug") area_slug = self.kwargs.get("area_slug") # Try to get by current or historical slug obj, is_old_slug = self.model.get_by_slug(area_slug) if obj.park.slug != park_slug: raise self.model.DoesNotExist("Park slug doesn't match") return obj def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) return context def get_redirect_url_pattern(self): return "parks:park_detail" def get_redirect_url_kwargs(self): return {"park_slug": self.object.park.slug, "area_slug": self.object.slug}