This commit is contained in:
pacnpal
2024-11-14 04:15:24 +00:00
parent e63677e8c0
commit dfe6194039
5 changed files with 370 additions and 325 deletions

View File

@@ -1,26 +1,31 @@
from django import template
from django.utils.safestring import mark_safe
from django.contrib.contenttypes.models import ContentType
from django.db.models import Model
from typing import Optional, Dict, Any, List, Union
register = template.Library()
@register.filter
def get_object_name(value, model_path):
def get_object_name(value: Optional[int], model_path: str) -> Optional[str]:
"""Get object name from ID and model path."""
if not value:
if not value or not model_path or '.' not in model_path:
return None
app_label, model = model_path.split('.')
try:
content_type = ContentType.objects.get(app_label=app_label.lower(), model=model.lower())
model_class = content_type.model_class()
obj = model_class.objects.get(id=value)
return str(obj)
if not model_class:
return None
obj = model_class.objects.filter(id=value).first()
return str(obj) if obj else None
except Exception:
return None
@register.filter
def get_category_display(value):
def get_category_display(value: Optional[str]) -> Optional[str]:
"""Get display value for ride category."""
if not value:
return None
@@ -33,24 +38,25 @@ def get_category_display(value):
'TR': 'Transport',
'OT': 'Other'
}
return categories.get(value, value)
return categories.get(value)
@register.filter
def get_park_area_name(value, park_id):
def get_park_area_name(value: Optional[int], park_id: Optional[int]) -> Optional[str]:
"""Get park area name from ID and park ID."""
if not value or not park_id:
return None
try:
from parks.models import ParkArea
area = ParkArea.objects.get(id=value, park_id=park_id)
return str(area)
area = ParkArea.objects.filter(id=value, park_id=park_id).first()
return str(area) if area else None
except Exception:
return None
@register.filter
def get_item(dictionary, key):
def get_item(dictionary: Optional[Dict[str, Any]], key: Optional[Union[str, int]]) -> List[Any]:
"""Get item from dictionary by key."""
if not dictionary or not key:
if not dictionary or not isinstance(dictionary, dict) or not key:
return []
return dictionary.get(str(key), [])

View File

@@ -4,9 +4,12 @@ from django.http import HttpResponse, JsonResponse, HttpRequest
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.decorators import login_required
from django.template.loader import render_to_string
from django.db.models import Q
from django.db.models import Q, QuerySet
from django.core.exceptions import PermissionDenied
from typing import Optional, Any, cast
from typing import Optional, Any, Dict, List, Tuple, Union, cast
from django.db import models
from django.core.serializers.json import DjangoJSONEncoder
import json
from accounts.models import User
from .models import EditSubmission, PhotoSubmission
@@ -14,6 +17,7 @@ from parks.models import Park, ParkArea
from designers.models import Designer
from companies.models import Manufacturer
from rides.models import RideModel
from location.models import Location
MODERATOR_ROLES = ['MODERATOR', 'ADMIN', 'SUPERUSER']
@@ -23,16 +27,53 @@ class ModeratorRequiredMixin(UserPassesTestMixin):
def test_func(self) -> bool:
"""Check if user has moderator permissions."""
user = cast(User, self.request.user)
return (
user.is_authenticated and
(getattr(user, 'role', None) in MODERATOR_ROLES or user.is_superuser)
)
return user.is_authenticated and (user.role in MODERATOR_ROLES or user.is_superuser)
def handle_no_permission(self) -> HttpResponse:
if not self.request.user.is_authenticated:
return super().handle_no_permission()
raise PermissionDenied("You do not have moderator permissions.")
def get_filtered_queryset(request: HttpRequest, status: str, submission_type: str) -> QuerySet:
"""Get filtered queryset based on request parameters."""
if submission_type == 'photo':
return PhotoSubmission.objects.filter(status=status).order_by('-created_at')
queryset = EditSubmission.objects.filter(status=status).order_by('-created_at')
if type_filter := request.GET.get('type'):
queryset = queryset.filter(submission_type=type_filter)
if content_type := request.GET.get('content_type'):
queryset = queryset.filter(content_type__model=content_type)
return queryset
def get_context_data(request: HttpRequest, queryset: QuerySet) -> Dict[str, Any]:
"""Get common context data for views."""
park_areas_by_park: Dict[int, List[Tuple[int, str]]] = {}
if isinstance(queryset.first(), EditSubmission):
for submission in queryset:
if (submission.content_type.model == 'park' and
isinstance(submission.changes, dict) and
'park' in submission.changes):
park_id = submission.changes['park']
if park_id not in park_areas_by_park:
areas = ParkArea.objects.filter(park_id=park_id)
park_areas_by_park[park_id] = [(area.pk, str(area)) for area in areas]
return {
'submissions': queryset,
'user': request.user,
'parks': [(park.pk, str(park)) for park in Park.objects.all()],
'designers': [(designer.pk, str(designer)) for designer in Designer.objects.all()],
'manufacturers': [(manufacturer.pk, str(manufacturer)) for manufacturer in Manufacturer.objects.all()],
'ride_models': [(model.pk, str(model)) for model in RideModel.objects.all()],
'owners': [(user.pk, str(user)) for user in User.objects.filter(role__in=['OWNER', 'ADMIN', 'SUPERUSER'])],
'park_areas_by_park': park_areas_by_park
}
@login_required
def search_parks(request: HttpRequest) -> HttpResponse:
"""HTMX endpoint for searching parks in moderation dashboard"""
@@ -43,19 +84,16 @@ def search_parks(request: HttpRequest) -> HttpResponse:
query = request.GET.get('q', '').strip()
submission_id = request.GET.get('submission_id')
# If no query, show first 10 parks
if not query:
parks = Park.objects.all().order_by('name')[:10]
else:
parks = Park.objects.filter(name__icontains=query).order_by('name')[:10]
parks = Park.objects.all().order_by('name')
if query:
parks = parks.filter(name__icontains=query)
parks = parks[:10]
context = {
return render(request, 'moderation/partials/park_search_results.html', {
'parks': parks,
'search_term': query,
'submission_id': submission_id
}
return render(request, 'moderation/partials/park_search_results.html', context)
})
@login_required
def search_manufacturers(request: HttpRequest) -> HttpResponse:
@@ -67,19 +105,16 @@ def search_manufacturers(request: HttpRequest) -> HttpResponse:
query = request.GET.get('q', '').strip()
submission_id = request.GET.get('submission_id')
# If no query, show first 10 manufacturers
if not query:
manufacturers = Manufacturer.objects.all().order_by('name')[:10]
else:
manufacturers = Manufacturer.objects.filter(name__icontains=query).order_by('name')[:10]
manufacturers = Manufacturer.objects.all().order_by('name')
if query:
manufacturers = manufacturers.filter(name__icontains=query)
manufacturers = manufacturers[:10]
context = {
return render(request, 'moderation/partials/manufacturer_search_results.html', {
'manufacturers': manufacturers,
'search_term': query,
'submission_id': submission_id
}
return render(request, 'moderation/partials/manufacturer_search_results.html', context)
})
@login_required
def search_designers(request: HttpRequest) -> HttpResponse:
@@ -91,19 +126,16 @@ def search_designers(request: HttpRequest) -> HttpResponse:
query = request.GET.get('q', '').strip()
submission_id = request.GET.get('submission_id')
# If no query, show first 10 designers
if not query:
designers = Designer.objects.all().order_by('name')[:10]
else:
designers = Designer.objects.filter(name__icontains=query).order_by('name')[:10]
designers = Designer.objects.all().order_by('name')
if query:
designers = designers.filter(name__icontains=query)
designers = designers[:10]
context = {
return render(request, 'moderation/partials/designer_search_results.html', {
'designers': designers,
'search_term': query,
'submission_id': submission_id
}
return render(request, 'moderation/partials/designer_search_results.html', context)
})
@login_required
def search_ride_models(request: HttpRequest) -> HttpResponse:
@@ -117,50 +149,32 @@ def search_ride_models(request: HttpRequest) -> HttpResponse:
manufacturer_id = request.GET.get('manufacturer')
queryset = RideModel.objects.all()
if manufacturer_id:
queryset = queryset.filter(manufacturer_id=manufacturer_id)
if query:
queryset = queryset.filter(name__icontains=query)
queryset = queryset.order_by('name')[:10]
# If no query, show first 10 models
if not query:
ride_models = queryset.order_by('name')[:10]
else:
ride_models = queryset.filter(name__icontains=query).order_by('name')[:10]
context = {
'ride_models': ride_models,
return render(request, 'moderation/partials/ride_model_search_results.html', {
'ride_models': queryset,
'search_term': query,
'submission_id': submission_id
}
return render(request, 'moderation/partials/ride_model_search_results.html', context)
})
class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, ListView):
template_name = 'moderation/dashboard.html'
context_object_name = 'submissions'
paginate_by = 10
def get_template_names(self):
def get_template_names(self) -> List[str]:
if self.request.headers.get('HX-Request'):
return ['moderation/partials/dashboard_content.html']
return [self.template_name]
def get_queryset(self):
def get_queryset(self) -> QuerySet:
status = self.request.GET.get('status', 'PENDING')
submission_type = self.request.GET.get('submission_type', '')
if submission_type == 'photo':
queryset = PhotoSubmission.objects.filter(status=status).order_by('-created_at')
else:
queryset = EditSubmission.objects.filter(status=status).order_by('-created_at')
if type_filter := self.request.GET.get('type'):
queryset = queryset.filter(submission_type=type_filter)
if content_type := self.request.GET.get('content_type'):
queryset = queryset.filter(content_type__model=content_type)
return queryset
return get_filtered_queryset(self.request, status, submission_type)
@login_required
def submission_list(request: HttpRequest) -> HttpResponse:
@@ -172,42 +186,25 @@ def submission_list(request: HttpRequest) -> HttpResponse:
status = request.GET.get('status', 'PENDING')
submission_type = request.GET.get('submission_type', '')
if submission_type == 'photo':
queryset = PhotoSubmission.objects.filter(status=status).order_by('-created_at')
else:
queryset = EditSubmission.objects.filter(status=status).order_by('-created_at')
queryset = get_filtered_queryset(request, status, submission_type)
if type_filter := request.GET.get('type'):
queryset = queryset.filter(submission_type=type_filter)
if content_type := request.GET.get('content_type'):
queryset = queryset.filter(content_type__model=content_type)
# Get park areas for each park submission
park_areas_by_park = {}
# Process location data for park submissions
for submission in queryset:
if submission.content_type.model == 'park' and submission.changes.get('park'):
park_id = submission.changes['park']
if park_id not in park_areas_by_park:
park_areas_by_park[park_id] = [(a.id, str(a)) for a in ParkArea.objects.filter(park_id=park_id)]
if (submission.content_type.model == 'park' and
isinstance(submission.changes, dict)):
# Extract location fields into a location object
location_fields = ['latitude', 'longitude', 'street_address', 'city', 'state', 'postal_code', 'country']
location_data = {field: submission.changes.get(field) for field in location_fields}
# Add location data back as a single object
submission.changes['location'] = location_data
context = {
'submissions': queryset,
'user': request.user,
'parks': [(p.id, str(p)) for p in Park.objects.all()],
'designers': [(d.id, str(d)) for d in Designer.objects.all()],
'manufacturers': [(m.id, str(m)) for m in Manufacturer.objects.all()],
'ride_models': [(m.id, str(m)) for m in RideModel.objects.all()],
'owners': [(u.id, str(u)) for u in User.objects.filter(role__in=['OWNER', 'ADMIN', 'SUPERUSER'])],
'park_areas_by_park': park_areas_by_park
}
context = get_context_data(request, queryset)
# If it's an HTMX request, return just the content
if request.headers.get('HX-Request'):
return render(request, 'moderation/partials/dashboard_content.html', context)
template_name = ('moderation/partials/dashboard_content.html'
if request.headers.get('HX-Request')
else 'moderation/dashboard.html')
# For direct URL access, return the full dashboard template
return render(request, 'moderation/dashboard.html', context)
return render(request, template_name, context)
@login_required
def edit_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
@@ -218,26 +215,47 @@ def edit_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
submission = get_object_or_404(EditSubmission, id=submission_id)
if request.method == 'POST':
# Handle the edit submission
if request.method != 'POST':
return HttpResponse("Invalid request method", status=405)
notes = request.POST.get('notes')
if not notes:
return HttpResponse("Notes are required when editing a submission", status=400)
try:
# Update the moderator_changes with the edited values
edited_changes = submission.changes.copy()
for field in submission.changes.keys():
if field == 'stats':
edited_changes = dict(submission.changes) if submission.changes else {}
# Update stats if present
if 'stats' in edited_changes:
edited_stats = {}
for key in submission.changes['stats'].keys():
for key in edited_changes['stats']:
if new_value := request.POST.get(f'stats.{key}'):
edited_stats[key] = new_value
edited_changes['stats'] = edited_stats
else:
# Update location fields if present
if submission.content_type.model == 'park':
location_fields = ['latitude', 'longitude', 'street_address', 'city', 'state', 'postal_code', 'country']
location_data = {}
for field in location_fields:
if new_value := request.POST.get(field):
# Handle special field types
if field in ['latitude', 'longitude', 'size_acres']:
if field in ['latitude', 'longitude']:
try:
location_data[field] = float(new_value)
except ValueError:
return HttpResponse(f"Invalid value for {field}", status=400)
else:
location_data[field] = new_value
if location_data:
edited_changes.update(location_data)
# Update other fields
for field in edited_changes:
if field == 'stats' or field in ['latitude', 'longitude', 'street_address', 'city', 'state', 'postal_code', 'country']:
continue
if new_value := request.POST.get(field):
if field in ['size_acres']:
try:
edited_changes[field] = float(new_value)
except ValueError:
@@ -245,64 +263,48 @@ def edit_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
else:
edited_changes[field] = new_value
submission.moderator_changes = edited_changes
# Convert to JSON-serializable format
json_changes = json.loads(json.dumps(edited_changes, cls=DjangoJSONEncoder))
submission.moderator_changes = json_changes
submission.notes = notes
submission.save()
# Return the updated submission
context = {
'submission': submission,
'user': request.user,
'parks': [(p.id, str(p)) for p in Park.objects.all()],
'designers': [(d.id, str(d)) for d in Designer.objects.all()],
'manufacturers': [(m.id, str(m)) for m in Manufacturer.objects.all()],
'ride_models': [(m.id, str(m)) for m in RideModel.objects.all()],
'owners': [(u.id, str(u)) for u in User.objects.filter(role__in=['OWNER', 'ADMIN', 'SUPERUSER'])],
'park_areas': [(a.id, str(a)) for a in ParkArea.objects.filter(park_id=edited_changes.get('park'))] if edited_changes.get('park') else []
}
# Process location data for display
if submission.content_type.model == 'park':
location_fields = ['latitude', 'longitude', 'street_address', 'city', 'state', 'postal_code', 'country']
location_data = {field: json_changes.get(field) for field in location_fields}
# Add location data back as a single object
json_changes['location'] = location_data
submission.changes = json_changes
context = get_context_data(request, EditSubmission.objects.filter(id=submission_id))
return render(request, 'moderation/partials/submission_list.html', context)
except Exception as e:
return HttpResponse(str(e), status=400)
return HttpResponse("Invalid request method", status=405)
@login_required
def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
"""HTMX endpoint for approving a submission"""
user = cast(User, request.user)
submission = get_object_or_404(EditSubmission, id=submission_id)
if submission.status == 'ESCALATED' and not (user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser):
return HttpResponse("Only admins can handle escalated submissions", status=403)
if not (user.role in MODERATOR_ROLES or user.is_superuser):
return HttpResponse(status=403)
if not ((submission.status != 'ESCALATED' and user.role in MODERATOR_ROLES) or
user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser):
return HttpResponse("Insufficient permissions", status=403)
try:
submission.approve(user)
_update_submission_notes(submission, request.POST.get('notes'))
# Get updated queryset with filters
status = request.GET.get('status', 'PENDING')
submission_type = request.GET.get('submission_type', '')
queryset = get_filtered_queryset(request, status, submission_type)
if submission_type == 'photo':
queryset = PhotoSubmission.objects.filter(status=status).order_by('-created_at')
else:
queryset = EditSubmission.objects.filter(status=status).order_by('-created_at')
if type_filter := request.GET.get('type'):
queryset = queryset.filter(submission_type=type_filter)
if content_type := request.GET.get('content_type'):
queryset = queryset.filter(content_type__model=content_type)
context = {
return render(request, 'moderation/partials/dashboard_content.html', {
'submissions': queryset,
'user': request.user,
}
return render(request, 'moderation/partials/dashboard_content.html', context)
})
except ValueError as e:
return HttpResponse(str(e), status=400)
@@ -312,42 +314,20 @@ def reject_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
user = cast(User, request.user)
submission = get_object_or_404(EditSubmission, id=submission_id)
if submission.status == 'ESCALATED' and not (user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser):
return HttpResponse("Only admins can handle escalated submissions", status=403)
if not (user.role in MODERATOR_ROLES or user.is_superuser):
return HttpResponse(status=403)
if not ((submission.status != 'ESCALATED' and user.role in MODERATOR_ROLES) or
user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser):
return HttpResponse("Insufficient permissions", status=403)
submission.reject(user)
_update_submission_notes(submission, request.POST.get('notes'))
# Get updated queryset with filters
status = request.GET.get('status', 'PENDING')
submission_type = request.GET.get('submission_type', '')
queryset = get_filtered_queryset(request, status, submission_type)
context = get_context_data(request, queryset)
if submission_type == 'photo':
queryset = PhotoSubmission.objects.filter(status=status).order_by('-created_at')
else:
queryset = EditSubmission.objects.filter(status=status).order_by('-created_at')
if type_filter := request.GET.get('type'):
queryset = queryset.filter(submission_type=type_filter)
if content_type := request.GET.get('content_type'):
queryset = queryset.filter(content_type__model=content_type)
context = {
'submission': submission,
'user': request.user,
'parks': [(p.id, str(p)) for p in Park.objects.all()],
'designers': [(d.id, str(d)) for d in Designer.objects.all()],
'manufacturers': [(m.id, str(m)) for m in Manufacturer.objects.all()],
'ride_models': [(m.id, str(m)) for m in RideModel.objects.all()],
'owners': [(u.id, str(u)) for u in User.objects.filter(role__in=['OWNER', 'ADMIN', 'SUPERUSER'])],
'park_areas': [(a.id, str(a)) for a in ParkArea.objects.filter(park_id=submission.changes.get('park'))] if submission.changes.get('park') else []
}
return render(request, 'moderation/partials/submission_list.html', context)
@login_required
def escalate_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
"""HTMX endpoint for escalating a submission"""
@@ -362,28 +342,14 @@ def escalate_submission(request: HttpRequest, submission_id: int) -> HttpRespons
submission.escalate(user)
_update_submission_notes(submission, request.POST.get("notes"))
# Get updated queryset with filters
status = request.GET.get("status", "PENDING")
submission_type = request.GET.get("submission_type", "")
queryset = get_filtered_queryset(request, status, submission_type)
if submission_type == "photo":
queryset = PhotoSubmission.objects.filter(status=status).order_by("-created_at")
else:
queryset = EditSubmission.objects.filter(status=status).order_by("-created_at")
if type_filter := request.GET.get("type"):
queryset = queryset.filter(submission_type=type_filter)
if content_type := request.GET.get("content_type"):
queryset = queryset.filter(content_type__model=content_type)
context = {
return render(request, "moderation/partials/dashboard_content.html", {
"submissions": queryset,
"user": request.user,
}
return render(request, "moderation/partials/dashboard_content.html", context)
})
@login_required
def approve_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
@@ -393,18 +359,13 @@ def approve_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
return HttpResponse(status=403)
submission = get_object_or_404(PhotoSubmission, id=submission_id)
try:
submission.approve(user, request.POST.get("notes", ""))
return render(
request,
"moderation/partials/photo_submission.html",
{"submission": submission},
)
return render(request, "moderation/partials/photo_submission.html",
{"submission": submission})
except Exception as e:
return HttpResponse(str(e), status=400)
@login_required
def reject_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
"""HTMX endpoint for rejecting a photo submission"""
@@ -415,10 +376,8 @@ def reject_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
submission = get_object_or_404(PhotoSubmission, id=submission_id)
submission.reject(user, request.POST.get("notes", ""))
return render(
request, "moderation/partials/photo_submission.html", {"submission": submission}
)
return render(request, "moderation/partials/photo_submission.html",
{"submission": submission})
def _update_submission_notes(submission: EditSubmission, notes: Optional[str]) -> None:
"""Update submission notes if provided."""

View File

@@ -1,23 +1,48 @@
{% load moderation_tags %}
<div class="p-4 bg-gray-100 border rounded-lg dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<h3 class="mb-4 text-lg font-semibold">Location</h3>
<div class="p-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-300">Location</h3>
<!-- Map Container -->
<div class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"
id="viewMap-{{ submission.id }}"
x-init="setTimeout(() => {
const map = L.map('viewMap-{{ submission.id }}').setView([{{ location.latitude }}, {{ location.longitude }}], 13);
const map = L.map('viewMap-{{ submission.id }}');
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
L.marker([{{ location.latitude }}, {{ location.longitude }}]).addTo(map);
{% if submission.changes.latitude and submission.changes.longitude %}
const lat = {{ submission.changes.latitude }};
const lng = {{ submission.changes.longitude }};
map.setView([lat, lng], 13);
L.marker([lat, lng]).addTo(map);
{% else %}
map.setView([0, 0], 2);
{% endif %}
}, 100)"></div>
<div class="mt-4 space-y-2 text-sm text-gray-600 dark:text-gray-400">
{% if location.street_address %}<div>{{ location.street_address }}</div>{% endif %}
<div>
{% if location.city %}{{ location.city }}{% endif %}
{% if location.state %}, {{ location.state }}{% endif %}
{% if location.postal_code %} {{ location.postal_code }}{% endif %}
<!-- Address Display -->
<div class="mt-4 space-y-1">
{% if submission.changes.street_address %}
<div class="flex items-center text-gray-600 dark:text-gray-400">
<i class="w-5 mr-2 fas fa-map-marker-alt"></i>
{{ submission.changes.street_address }}
</div>
{% if location.country %}<div>{{ location.country }}</div>{% endif %}
{% endif %}
<div class="flex items-center text-gray-600 dark:text-gray-400">
<i class="w-5 mr-2 fas fa-city"></i>
{% if submission.changes.city %}{{ submission.changes.city }}{% endif %}
{% if submission.changes.state %}, {{ submission.changes.state }}{% endif %}
{% if submission.changes.postal_code %} {{ submission.changes.postal_code }}{% endif %}
</div>
{% if submission.changes.country %}
<div class="flex items-center text-gray-600 dark:text-gray-400">
<i class="w-5 mr-2 fas fa-globe"></i>
{{ submission.changes.country }}
</div>
{% endif %}
</div>
</div>

View File

@@ -19,15 +19,18 @@
}
</style>
<div class="p-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-300">Location</h3>
<div class="location-widget" id="locationWidget-{{ submission.id }}">
{# Search Form #}
<div class="relative mb-4">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Search Location
</label>
<input type="text"
id="locationSearch-{{ submission.id }}"
class="relative w-full px-4 py-2 border border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
class="relative w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
placeholder="Search for a location..."
autocomplete="off"
style="z-index: 10;">
@@ -39,67 +42,69 @@
{# Map Container #}
<div class="relative mb-4" style="z-index: 1;">
<div id="locationMap-{{ submission.id }}" class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
<div id="locationMap-{{ submission.id }}"
class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
</div>
{# Location Form Fields #}
<div class="relative grid grid-cols-1 gap-4 md:grid-cols-2" style="z-index: 10;">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Street Address
</label>
<input type="text"
name="street_address"
id="streetAddress-{{ submission.id }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="{{ form.street_address }}">
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.street_address }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
City
</label>
<input type="text"
name="city"
id="city-{{ submission.id }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="{{ form.city }}">
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.city }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
State/Region
</label>
<input type="text"
name="state"
id="state-{{ submission.id }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="{{ form.state }}">
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.state }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Country
</label>
<input type="text"
name="country"
id="country-{{ submission.id }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="{{ form.country }}">
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.country }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Postal Code
</label>
<input type="text"
name="postal_code"
id="postalCode-{{ submission.id }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="{{ form.postal_code }}">
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.postal_code }}">
</div>
</div>
{# Hidden Coordinate Fields #}
<div class="hidden">
<input type="hidden" name="latitude" id="latitude-{{ submission.id }}" value="{{ form.latitude }}">
<input type="hidden" name="longitude" id="longitude-{{ submission.id }}" value="{{ form.longitude }}">
<input type="hidden" name="latitude" id="latitude-{{ submission.id }}" value="{{ submission.changes.latitude }}">
<input type="hidden" name="longitude" id="longitude-{{ submission.id }}" value="{{ submission.changes.longitude }}">
</div>
</div>
</div>
@@ -111,7 +116,37 @@ document.addEventListener('DOMContentLoaded', function() {
const searchResults = document.getElementById('searchResults-{{ submission.id }}');
let searchTimeout;
// Initialize form fields with existing values
const fields = {
city: '{{ submission.changes.city|default:"" }}',
state: '{{ submission.changes.state|default:"" }}',
country: '{{ submission.changes.country|default:"" }}',
postal_code: '{{ submission.changes.postal_code|default:"" }}',
street_address: '{{ submission.changes.street_address|default:"" }}',
latitude: '{{ submission.changes.latitude|default:"" }}',
longitude: '{{ submission.changes.longitude|default:"" }}'
};
Object.entries(fields).forEach(([field, value]) => {
const element = document.getElementById(`${field}-{{ submission.id }}`);
if (element) {
element.value = value;
}
});
// Set initial search input value if location exists
if (fields.street_address || fields.city) {
const parts = [
fields.street_address,
fields.city,
fields.state,
fields.country
].filter(Boolean);
searchInput.value = parts.join(', ');
}
function normalizeCoordinate(value, maxDigits, decimalPlaces) {
if (!value) return null;
try {
const rounded = Number(value).toFixed(decimalPlaces);
const strValue = rounded.replace('.', '').replace('-', '');
@@ -167,22 +202,27 @@ document.addEventListener('DOMContentLoaded', function() {
}
// Create new map
maps[submissionId] = L.map(mapId).setView([0, 0], 2);
maps[submissionId] = L.map(mapId);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(maps[submissionId]);
// Initialize with existing coordinates if available
const initialLat = document.getElementById(`latitude-${submissionId}`).value;
const initialLng = document.getElementById(`longitude-${submissionId}`).value;
const initialLat = fields.latitude;
const initialLng = fields.longitude;
if (initialLat && initialLng) {
try {
const normalized = validateCoordinates(initialLat, initialLng);
maps[submissionId].setView([normalized.lat, normalized.lng], 13);
addMarker(normalized.lat, normalized.lng);
} catch (error) {
console.error('Invalid initial coordinates:', error);
maps[submissionId].setView([0, 0], 2);
}
} else {
maps[submissionId].setView([0, 0], 2);
}
// Handle map clicks
@@ -236,6 +276,15 @@ document.addEventListener('DOMContentLoaded', function() {
address.state || address.region || '';
document.getElementById(`country-${submissionId}`).value = address.country || '';
document.getElementById(`postalCode-${submissionId}`).value = address.postcode || '';
// Update search input
const locationString = [
document.getElementById(`streetAddress-${submissionId}`).value,
document.getElementById(`city-${submissionId}`).value,
document.getElementById(`state-${submissionId}`).value,
document.getElementById(`country-${submissionId}`).value
].filter(Boolean).join(', ');
searchInput.value = locationString;
} catch (error) {
console.error('Location update failed:', error);
alert(error.message || 'Failed to update location. Please try again.');
@@ -355,6 +404,11 @@ document.addEventListener('DOMContentLoaded', function() {
const mapContainer = document.getElementById(`locationMap-{{ submission.id }}`);
if (mapContainer) {
observer.observe(mapContainer.parentElement.parentElement, { attributes: true });
// Also initialize immediately if the container is already visible
if (window.getComputedStyle(mapContainer).display !== 'none') {
initMap();
}
}
});
</script>

View File

@@ -103,15 +103,15 @@
<!-- View Mode -->
<div x-show="!isEditing">
<!-- Location Map (View Mode) -->
{% if submission.content_type.model == 'park' and submission.changes.latitude and submission.changes.longitude %}
{% if submission.content_type.model == 'park' %}
<div class="mb-4">
{% include "moderation/partials/location_map.html" with location=submission.changes %}
{% include "moderation/partials/location_map.html" with submission=submission %}
</div>
{% endif %}
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
{% for field, value in submission.changes.items %}
{% if field != 'model_name' and field != 'latitude' and field != 'longitude' and field != 'street_address' and field != 'city' and field != 'state' and field != 'postal_code' and field != 'country' %}
{% if field != 'model_name' and field != 'latitude' and field != 'longitude' and field != 'street_address' and field != 'city' and field != 'state' and field != 'postal_code' and field != 'country' and field != 'location' %}
<div class="p-4 bg-gray-100 border rounded-lg dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<div class="text-sm font-medium text-gray-900 dark:text-gray-300">
{{ field|title }}:
@@ -176,8 +176,16 @@
hx-post="{% url 'moderation:edit_submission' submission.id %}"
hx-target="#submission-{{ submission.id }}"
class="grid grid-cols-1 gap-3 md:grid-cols-2">
<!-- Location Widget for Parks -->
{% if submission.content_type.model == 'park' %}
<div class="col-span-2">
{% include "moderation/partials/location_widget.html" with submission=submission %}
</div>
{% endif %}
{% for field, value in submission.changes.items %}
{% if field != 'model_name' and field != 'stats' and field != 'latitude' and field != 'longitude' and field != 'street_address' and field != 'city' and field != 'state' and field != 'postal_code' and field != 'country' %}
{% if field != 'model_name' and field != 'stats' and field != 'latitude' and field != 'longitude' and field != 'street_address' and field != 'city' and field != 'state' and field != 'postal_code' and field != 'country' and field != 'location' %}
<div class="p-4 bg-gray-100 border rounded-lg dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50"
{% if field == 'post_closing_status' %}x-show="status === 'CLOSING'"{% endif %}
{% if field == 'closing_date' %}x-show="['CLOSING', 'SBNO', 'CLOSED_PERM', 'DEMOLISHED', 'RELOCATED'].includes(status)"{% endif %}>
@@ -362,13 +370,6 @@
{% endif %}
{% endfor %}
<!-- Location Widget for Parks -->
{% if submission.content_type.model == 'park' %}
<div class="col-span-2">
{% include "moderation/partials/location_widget.html" with form=submission.changes %}
</div>
{% endif %}
<!-- Coaster Fields -->
<div x-show="showCoasterFields"
x-cloak