Files
thrillwiki_django_no_react/parks/views.py
2024-11-05 04:10:47 +00:00

481 lines
18 KiB
Python

from decimal import Decimal, ROUND_DOWN, InvalidOperation
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
from location.models import Location
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", "location")
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(location__city__icontains=search) |
Q(location__state__icontains=search) |
Q(location__country__icontains=search)
)
if country:
queryset = queryset.filter(location__country__icontains=country)
if region:
queryset = queryset.filter(location__state__icontains=region)
if city:
queryset = queryset.filter(location__city__icontains=city)
if statuses:
queryset = queryset.filter(status__in=statuses)
return queryset.distinct()
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',
'location'
)
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()
# Create Location record
if form.cleaned_data.get("latitude") and form.cleaned_data.get("longitude"):
Location.objects.create(
content_type=ContentType.objects.get_for_model(Park),
object_id=self.object.id,
name=self.object.name,
location_type='park',
latitude=form.cleaned_data["latitude"],
longitude=form.cleaned_data["longitude"],
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", ""),
postal_code=form.cleaned_data.get("postal_code", "")
)
# 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()
# Update or create Location record
location_data = {
'name': self.object.name,
'location_type': 'park',
'latitude': form.cleaned_data.get("latitude"),
'longitude': form.cleaned_data.get("longitude"),
'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", ""),
'postal_code': form.cleaned_data.get("postal_code", "")
}
if self.object.location.exists():
location = self.object.location.first()
for key, value in location_data.items():
setattr(location, key, value)
location.save()
else:
Location.objects.create(
content_type=ContentType.objects.get_for_model(Park),
object_id=self.object.id,
**location_data
)
# 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}