mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 10:31:09 -05:00
438 lines
16 KiB
Python
438 lines
16 KiB
Python
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}
|