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 import template
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.contrib.contenttypes.models import ContentType 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 = template.Library()
@register.filter @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.""" """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 return None
app_label, model = model_path.split('.') app_label, model = model_path.split('.')
try: try:
content_type = ContentType.objects.get(app_label=app_label.lower(), model=model.lower()) content_type = ContentType.objects.get(app_label=app_label.lower(), model=model.lower())
model_class = content_type.model_class() model_class = content_type.model_class()
obj = model_class.objects.get(id=value) if not model_class:
return str(obj) return None
obj = model_class.objects.filter(id=value).first()
return str(obj) if obj else None
except Exception: except Exception:
return None return None
@register.filter @register.filter
def get_category_display(value): def get_category_display(value: Optional[str]) -> Optional[str]:
"""Get display value for ride category.""" """Get display value for ride category."""
if not value: if not value:
return None return None
@@ -33,24 +38,25 @@ def get_category_display(value):
'TR': 'Transport', 'TR': 'Transport',
'OT': 'Other' 'OT': 'Other'
} }
return categories.get(value, value) return categories.get(value)
@register.filter @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.""" """Get park area name from ID and park ID."""
if not value or not park_id: if not value or not park_id:
return None return None
try: try:
from parks.models import ParkArea from parks.models import ParkArea
area = ParkArea.objects.get(id=value, park_id=park_id) area = ParkArea.objects.filter(id=value, park_id=park_id).first()
return str(area) return str(area) if area else None
except Exception: except Exception:
return None return None
@register.filter @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.""" """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 []
return dictionary.get(str(key), []) 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.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.template.loader import render_to_string 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 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 accounts.models import User
from .models import EditSubmission, PhotoSubmission from .models import EditSubmission, PhotoSubmission
@@ -14,6 +17,7 @@ from parks.models import Park, ParkArea
from designers.models import Designer from designers.models import Designer
from companies.models import Manufacturer from companies.models import Manufacturer
from rides.models import RideModel from rides.models import RideModel
from location.models import Location
MODERATOR_ROLES = ['MODERATOR', 'ADMIN', 'SUPERUSER'] MODERATOR_ROLES = ['MODERATOR', 'ADMIN', 'SUPERUSER']
@@ -23,16 +27,53 @@ class ModeratorRequiredMixin(UserPassesTestMixin):
def test_func(self) -> bool: def test_func(self) -> bool:
"""Check if user has moderator permissions.""" """Check if user has moderator permissions."""
user = cast(User, self.request.user) user = cast(User, self.request.user)
return ( return user.is_authenticated and (user.role in MODERATOR_ROLES or user.is_superuser)
user.is_authenticated and
(getattr(user, 'role', None) in MODERATOR_ROLES or user.is_superuser)
)
def handle_no_permission(self) -> HttpResponse: def handle_no_permission(self) -> HttpResponse:
if not self.request.user.is_authenticated: if not self.request.user.is_authenticated:
return super().handle_no_permission() return super().handle_no_permission()
raise PermissionDenied("You do not have moderator permissions.") 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 @login_required
def search_parks(request: HttpRequest) -> HttpResponse: def search_parks(request: HttpRequest) -> HttpResponse:
"""HTMX endpoint for searching parks in moderation dashboard""" """HTMX endpoint for searching parks in moderation dashboard"""
@@ -43,19 +84,16 @@ def search_parks(request: HttpRequest) -> HttpResponse:
query = request.GET.get('q', '').strip() query = request.GET.get('q', '').strip()
submission_id = request.GET.get('submission_id') submission_id = request.GET.get('submission_id')
# If no query, show first 10 parks parks = Park.objects.all().order_by('name')
if not query: if query:
parks = Park.objects.all().order_by('name')[:10] parks = parks.filter(name__icontains=query)
else: parks = parks[:10]
parks = Park.objects.filter(name__icontains=query).order_by('name')[:10]
context = { return render(request, 'moderation/partials/park_search_results.html', {
'parks': parks, 'parks': parks,
'search_term': query, 'search_term': query,
'submission_id': submission_id 'submission_id': submission_id
} })
return render(request, 'moderation/partials/park_search_results.html', context)
@login_required @login_required
def search_manufacturers(request: HttpRequest) -> HttpResponse: def search_manufacturers(request: HttpRequest) -> HttpResponse:
@@ -67,19 +105,16 @@ def search_manufacturers(request: HttpRequest) -> HttpResponse:
query = request.GET.get('q', '').strip() query = request.GET.get('q', '').strip()
submission_id = request.GET.get('submission_id') submission_id = request.GET.get('submission_id')
# If no query, show first 10 manufacturers manufacturers = Manufacturer.objects.all().order_by('name')
if not query: if query:
manufacturers = Manufacturer.objects.all().order_by('name')[:10] manufacturers = manufacturers.filter(name__icontains=query)
else: manufacturers = manufacturers[:10]
manufacturers = Manufacturer.objects.filter(name__icontains=query).order_by('name')[:10]
context = { return render(request, 'moderation/partials/manufacturer_search_results.html', {
'manufacturers': manufacturers, 'manufacturers': manufacturers,
'search_term': query, 'search_term': query,
'submission_id': submission_id 'submission_id': submission_id
} })
return render(request, 'moderation/partials/manufacturer_search_results.html', context)
@login_required @login_required
def search_designers(request: HttpRequest) -> HttpResponse: def search_designers(request: HttpRequest) -> HttpResponse:
@@ -91,19 +126,16 @@ def search_designers(request: HttpRequest) -> HttpResponse:
query = request.GET.get('q', '').strip() query = request.GET.get('q', '').strip()
submission_id = request.GET.get('submission_id') submission_id = request.GET.get('submission_id')
# If no query, show first 10 designers designers = Designer.objects.all().order_by('name')
if not query: if query:
designers = Designer.objects.all().order_by('name')[:10] designers = designers.filter(name__icontains=query)
else: designers = designers[:10]
designers = Designer.objects.filter(name__icontains=query).order_by('name')[:10]
context = { return render(request, 'moderation/partials/designer_search_results.html', {
'designers': designers, 'designers': designers,
'search_term': query, 'search_term': query,
'submission_id': submission_id 'submission_id': submission_id
} })
return render(request, 'moderation/partials/designer_search_results.html', context)
@login_required @login_required
def search_ride_models(request: HttpRequest) -> HttpResponse: def search_ride_models(request: HttpRequest) -> HttpResponse:
@@ -117,50 +149,32 @@ def search_ride_models(request: HttpRequest) -> HttpResponse:
manufacturer_id = request.GET.get('manufacturer') manufacturer_id = request.GET.get('manufacturer')
queryset = RideModel.objects.all() queryset = RideModel.objects.all()
if manufacturer_id: if manufacturer_id:
queryset = queryset.filter(manufacturer_id=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 return render(request, 'moderation/partials/ride_model_search_results.html', {
if not query: 'ride_models': queryset,
ride_models = queryset.order_by('name')[:10]
else:
ride_models = queryset.filter(name__icontains=query).order_by('name')[:10]
context = {
'ride_models': ride_models,
'search_term': query, 'search_term': query,
'submission_id': submission_id 'submission_id': submission_id
} })
return render(request, 'moderation/partials/ride_model_search_results.html', context)
class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, ListView): class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, ListView):
template_name = 'moderation/dashboard.html' template_name = 'moderation/dashboard.html'
context_object_name = 'submissions' context_object_name = 'submissions'
paginate_by = 10 paginate_by = 10
def get_template_names(self): def get_template_names(self) -> List[str]:
if self.request.headers.get('HX-Request'): if self.request.headers.get('HX-Request'):
return ['moderation/partials/dashboard_content.html'] return ['moderation/partials/dashboard_content.html']
return [self.template_name] return [self.template_name]
def get_queryset(self): def get_queryset(self) -> QuerySet:
status = self.request.GET.get('status', 'PENDING') status = self.request.GET.get('status', 'PENDING')
submission_type = self.request.GET.get('submission_type', '') submission_type = self.request.GET.get('submission_type', '')
return get_filtered_queryset(self.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 := 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
@login_required @login_required
def submission_list(request: HttpRequest) -> HttpResponse: def submission_list(request: HttpRequest) -> HttpResponse:
@@ -172,42 +186,25 @@ def submission_list(request: HttpRequest) -> HttpResponse:
status = request.GET.get('status', 'PENDING') status = request.GET.get('status', 'PENDING')
submission_type = request.GET.get('submission_type', '') submission_type = request.GET.get('submission_type', '')
if submission_type == 'photo': queryset = get_filtered_queryset(request, status, submission_type)
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'): # Process location data for park submissions
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 = {}
for submission in queryset: for submission in queryset:
if submission.content_type.model == 'park' and submission.changes.get('park'): if (submission.content_type.model == 'park' and
park_id = submission.changes['park'] isinstance(submission.changes, dict)):
if park_id not in park_areas_by_park: # Extract location fields into a location object
park_areas_by_park[park_id] = [(a.id, str(a)) for a in ParkArea.objects.filter(park_id=park_id)] 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 = { context = get_context_data(request, queryset)
'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
}
# If it's an HTMX request, return just the content template_name = ('moderation/partials/dashboard_content.html'
if request.headers.get('HX-Request'): if request.headers.get('HX-Request')
return render(request, 'moderation/partials/dashboard_content.html', context) else 'moderation/dashboard.html')
# For direct URL access, return the full dashboard template return render(request, template_name, context)
return render(request, 'moderation/dashboard.html', context)
@login_required @login_required
def edit_submission(request: HttpRequest, submission_id: int) -> HttpResponse: 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) submission = get_object_or_404(EditSubmission, id=submission_id)
if request.method == 'POST': if request.method != 'POST':
# Handle the edit submission return HttpResponse("Invalid request method", status=405)
notes = request.POST.get('notes') notes = request.POST.get('notes')
if not notes: if not notes:
return HttpResponse("Notes are required when editing a submission", status=400) return HttpResponse("Notes are required when editing a submission", status=400)
try: try:
# Update the moderator_changes with the edited values edited_changes = dict(submission.changes) if submission.changes else {}
edited_changes = submission.changes.copy()
for field in submission.changes.keys(): # Update stats if present
if field == 'stats': if 'stats' in edited_changes:
edited_stats = {} edited_stats = {}
for key in submission.changes['stats'].keys(): for key in edited_changes['stats']:
if new_value := request.POST.get(f'stats.{key}'): if new_value := request.POST.get(f'stats.{key}'):
edited_stats[key] = new_value edited_stats[key] = new_value
edited_changes['stats'] = edited_stats 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): if new_value := request.POST.get(field):
# Handle special field types if field in ['latitude', 'longitude']:
if field in ['latitude', 'longitude', 'size_acres']: 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: try:
edited_changes[field] = float(new_value) edited_changes[field] = float(new_value)
except ValueError: except ValueError:
@@ -245,64 +263,48 @@ def edit_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
else: else:
edited_changes[field] = new_value 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.notes = notes
submission.save() submission.save()
# Return the updated submission # Process location data for display
context = { if submission.content_type.model == 'park':
'submission': submission, location_fields = ['latitude', 'longitude', 'street_address', 'city', 'state', 'postal_code', 'country']
'user': request.user, location_data = {field: json_changes.get(field) for field in location_fields}
'parks': [(p.id, str(p)) for p in Park.objects.all()], # Add location data back as a single object
'designers': [(d.id, str(d)) for d in Designer.objects.all()], json_changes['location'] = location_data
'manufacturers': [(m.id, str(m)) for m in Manufacturer.objects.all()], submission.changes = json_changes
'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'])], context = get_context_data(request, EditSubmission.objects.filter(id=submission_id))
'park_areas': [(a.id, str(a)) for a in ParkArea.objects.filter(park_id=edited_changes.get('park'))] if edited_changes.get('park') else []
}
return render(request, 'moderation/partials/submission_list.html', context) return render(request, 'moderation/partials/submission_list.html', context)
except Exception as e: except Exception as e:
return HttpResponse(str(e), status=400) return HttpResponse(str(e), status=400)
return HttpResponse("Invalid request method", status=405)
@login_required @login_required
def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse: def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
"""HTMX endpoint for approving a submission""" """HTMX endpoint for approving a submission"""
user = cast(User, request.user) user = cast(User, request.user)
submission = get_object_or_404(EditSubmission, id=submission_id) submission = get_object_or_404(EditSubmission, id=submission_id)
if submission.status == 'ESCALATED' and not (user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser): if not ((submission.status != 'ESCALATED' and user.role in MODERATOR_ROLES) or
return HttpResponse("Only admins can handle escalated submissions", status=403) user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser):
if not (user.role in MODERATOR_ROLES or user.is_superuser): return HttpResponse("Insufficient permissions", status=403)
return HttpResponse(status=403)
try: try:
submission.approve(user) submission.approve(user)
_update_submission_notes(submission, request.POST.get('notes')) _update_submission_notes(submission, request.POST.get('notes'))
# Get updated queryset with filters
status = request.GET.get('status', 'PENDING') status = request.GET.get('status', 'PENDING')
submission_type = request.GET.get('submission_type', '') submission_type = request.GET.get('submission_type', '')
queryset = get_filtered_queryset(request, status, submission_type)
if submission_type == 'photo': return render(request, 'moderation/partials/dashboard_content.html', {
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 = {
'submissions': queryset, 'submissions': queryset,
'user': request.user, 'user': request.user,
} })
return render(request, 'moderation/partials/dashboard_content.html', context)
except ValueError as e: except ValueError as e:
return HttpResponse(str(e), status=400) return HttpResponse(str(e), status=400)
@@ -312,42 +314,20 @@ def reject_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
user = cast(User, request.user) user = cast(User, request.user)
submission = get_object_or_404(EditSubmission, id=submission_id) submission = get_object_or_404(EditSubmission, id=submission_id)
if submission.status == 'ESCALATED' and not (user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser): if not ((submission.status != 'ESCALATED' and user.role in MODERATOR_ROLES) or
return HttpResponse("Only admins can handle escalated submissions", status=403) user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser):
if not (user.role in MODERATOR_ROLES or user.is_superuser): return HttpResponse("Insufficient permissions", status=403)
return HttpResponse(status=403)
submission.reject(user) submission.reject(user)
_update_submission_notes(submission, request.POST.get('notes')) _update_submission_notes(submission, request.POST.get('notes'))
# Get updated queryset with filters
status = request.GET.get('status', 'PENDING') status = request.GET.get('status', 'PENDING')
submission_type = request.GET.get('submission_type', '') 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) return render(request, 'moderation/partials/submission_list.html', context)
@login_required @login_required
def escalate_submission(request: HttpRequest, submission_id: int) -> HttpResponse: def escalate_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
"""HTMX endpoint for escalating a submission""" """HTMX endpoint for escalating a submission"""
@@ -362,28 +342,14 @@ def escalate_submission(request: HttpRequest, submission_id: int) -> HttpRespons
submission.escalate(user) submission.escalate(user)
_update_submission_notes(submission, request.POST.get("notes")) _update_submission_notes(submission, request.POST.get("notes"))
# Get updated queryset with filters
status = request.GET.get("status", "PENDING") status = request.GET.get("status", "PENDING")
submission_type = request.GET.get("submission_type", "") submission_type = request.GET.get("submission_type", "")
queryset = get_filtered_queryset(request, status, submission_type)
if submission_type == "photo": return render(request, "moderation/partials/dashboard_content.html", {
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 = {
"submissions": queryset, "submissions": queryset,
"user": request.user, "user": request.user,
} })
return render(request, "moderation/partials/dashboard_content.html", context)
@login_required @login_required
def approve_photo(request: HttpRequest, submission_id: int) -> HttpResponse: 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) return HttpResponse(status=403)
submission = get_object_or_404(PhotoSubmission, id=submission_id) submission = get_object_or_404(PhotoSubmission, id=submission_id)
try: try:
submission.approve(user, request.POST.get("notes", "")) submission.approve(user, request.POST.get("notes", ""))
return render( return render(request, "moderation/partials/photo_submission.html",
request, {"submission": submission})
"moderation/partials/photo_submission.html",
{"submission": submission},
)
except Exception as e: except Exception as e:
return HttpResponse(str(e), status=400) return HttpResponse(str(e), status=400)
@login_required @login_required
def reject_photo(request: HttpRequest, submission_id: int) -> HttpResponse: def reject_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
"""HTMX endpoint for rejecting a photo submission""" """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 = get_object_or_404(PhotoSubmission, id=submission_id)
submission.reject(user, request.POST.get("notes", "")) submission.reject(user, request.POST.get("notes", ""))
return render( return render(request, "moderation/partials/photo_submission.html",
request, "moderation/partials/photo_submission.html", {"submission": submission} {"submission": submission})
)
def _update_submission_notes(submission: EditSubmission, notes: Optional[str]) -> None: def _update_submission_notes(submission: EditSubmission, notes: Optional[str]) -> None:
"""Update submission notes if provided.""" """Update submission notes if provided."""

View File

@@ -1,23 +1,48 @@
{% load moderation_tags %} {% 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"> <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">Location</h3> <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" <div class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"
id="viewMap-{{ submission.id }}" id="viewMap-{{ submission.id }}"
x-init="setTimeout(() => { 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', { L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors' attribution: '© OpenStreetMap contributors'
}).addTo(map); }).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> }, 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 %} <!-- Address Display -->
<div> <div class="mt-4 space-y-1">
{% if location.city %}{{ location.city }}{% endif %} {% if submission.changes.street_address %}
{% if location.state %}, {{ location.state }}{% endif %} <div class="flex items-center text-gray-600 dark:text-gray-400">
{% if location.postal_code %} {{ location.postal_code }}{% endif %} <i class="w-5 mr-2 fas fa-map-marker-alt"></i>
{{ submission.changes.street_address }}
</div> </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>
</div> </div>

View File

@@ -19,15 +19,18 @@
} }
</style> </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 }}"> <div class="location-widget" id="locationWidget-{{ submission.id }}">
{# Search Form #} {# Search Form #}
<div class="relative mb-4"> <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 Search Location
</label> </label>
<input type="text" <input type="text"
id="locationSearch-{{ submission.id }}" 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..." placeholder="Search for a location..."
autocomplete="off" autocomplete="off"
style="z-index: 10;"> style="z-index: 10;">
@@ -39,67 +42,69 @@
{# Map Container #} {# Map Container #}
<div class="relative mb-4" style="z-index: 1;"> <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> </div>
{# Location Form Fields #} {# 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> <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 Street Address
</label> </label>
<input type="text" <input type="text"
name="street_address" name="street_address"
id="streetAddress-{{ submission.id }}" 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" 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="{{ form.street_address }}"> value="{{ submission.changes.street_address }}">
</div> </div>
<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 City
</label> </label>
<input type="text" <input type="text"
name="city" name="city"
id="city-{{ submission.id }}" 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" 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="{{ form.city }}"> value="{{ submission.changes.city }}">
</div> </div>
<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 State/Region
</label> </label>
<input type="text" <input type="text"
name="state" name="state"
id="state-{{ submission.id }}" 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" 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="{{ form.state }}"> value="{{ submission.changes.state }}">
</div> </div>
<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 Country
</label> </label>
<input type="text" <input type="text"
name="country" name="country"
id="country-{{ submission.id }}" 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" 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="{{ form.country }}"> value="{{ submission.changes.country }}">
</div> </div>
<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 Postal Code
</label> </label>
<input type="text" <input type="text"
name="postal_code" name="postal_code"
id="postalCode-{{ submission.id }}" 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" 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="{{ form.postal_code }}"> value="{{ submission.changes.postal_code }}">
</div> </div>
</div> </div>
{# Hidden Coordinate Fields #} {# Hidden Coordinate Fields #}
<div class="hidden"> <div class="hidden">
<input type="hidden" name="latitude" id="latitude-{{ submission.id }}" value="{{ form.latitude }}"> <input type="hidden" name="latitude" id="latitude-{{ submission.id }}" value="{{ submission.changes.latitude }}">
<input type="hidden" name="longitude" id="longitude-{{ submission.id }}" value="{{ form.longitude }}"> <input type="hidden" name="longitude" id="longitude-{{ submission.id }}" value="{{ submission.changes.longitude }}">
</div>
</div> </div>
</div> </div>
@@ -111,7 +116,37 @@ document.addEventListener('DOMContentLoaded', function() {
const searchResults = document.getElementById('searchResults-{{ submission.id }}'); const searchResults = document.getElementById('searchResults-{{ submission.id }}');
let searchTimeout; 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) { function normalizeCoordinate(value, maxDigits, decimalPlaces) {
if (!value) return null;
try { try {
const rounded = Number(value).toFixed(decimalPlaces); const rounded = Number(value).toFixed(decimalPlaces);
const strValue = rounded.replace('.', '').replace('-', ''); const strValue = rounded.replace('.', '').replace('-', '');
@@ -167,22 +202,27 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// Create new map // 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', { L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors' attribution: '© OpenStreetMap contributors'
}).addTo(maps[submissionId]); }).addTo(maps[submissionId]);
// Initialize with existing coordinates if available // Initialize with existing coordinates if available
const initialLat = document.getElementById(`latitude-${submissionId}`).value; const initialLat = fields.latitude;
const initialLng = document.getElementById(`longitude-${submissionId}`).value; const initialLng = fields.longitude;
if (initialLat && initialLng) { if (initialLat && initialLng) {
try { try {
const normalized = validateCoordinates(initialLat, initialLng); const normalized = validateCoordinates(initialLat, initialLng);
maps[submissionId].setView([normalized.lat, normalized.lng], 13);
addMarker(normalized.lat, normalized.lng); addMarker(normalized.lat, normalized.lng);
} catch (error) { } catch (error) {
console.error('Invalid initial coordinates:', error); console.error('Invalid initial coordinates:', error);
maps[submissionId].setView([0, 0], 2);
} }
} else {
maps[submissionId].setView([0, 0], 2);
} }
// Handle map clicks // Handle map clicks
@@ -236,6 +276,15 @@ document.addEventListener('DOMContentLoaded', function() {
address.state || address.region || ''; address.state || address.region || '';
document.getElementById(`country-${submissionId}`).value = address.country || ''; document.getElementById(`country-${submissionId}`).value = address.country || '';
document.getElementById(`postalCode-${submissionId}`).value = address.postcode || ''; 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) { } catch (error) {
console.error('Location update failed:', error); console.error('Location update failed:', error);
alert(error.message || 'Failed to update location. Please try again.'); 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 }}`); const mapContainer = document.getElementById(`locationMap-{{ submission.id }}`);
if (mapContainer) { if (mapContainer) {
observer.observe(mapContainer.parentElement.parentElement, { attributes: true }); observer.observe(mapContainer.parentElement.parentElement, { attributes: true });
// Also initialize immediately if the container is already visible
if (window.getComputedStyle(mapContainer).display !== 'none') {
initMap();
}
} }
}); });
</script> </script>

View File

@@ -103,15 +103,15 @@
<!-- View Mode --> <!-- View Mode -->
<div x-show="!isEditing"> <div x-show="!isEditing">
<!-- Location Map (View Mode) --> <!-- 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"> <div class="mb-4">
{% include "moderation/partials/location_map.html" with location=submission.changes %} {% include "moderation/partials/location_map.html" with submission=submission %}
</div> </div>
{% endif %} {% endif %}
<div class="grid grid-cols-1 gap-3 md:grid-cols-2"> <div class="grid grid-cols-1 gap-3 md:grid-cols-2">
{% for field, value in submission.changes.items %} {% 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="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"> <div class="text-sm font-medium text-gray-900 dark:text-gray-300">
{{ field|title }}: {{ field|title }}:
@@ -176,8 +176,16 @@
hx-post="{% url 'moderation:edit_submission' submission.id %}" hx-post="{% url 'moderation:edit_submission' submission.id %}"
hx-target="#submission-{{ submission.id }}" hx-target="#submission-{{ submission.id }}"
class="grid grid-cols-1 gap-3 md:grid-cols-2"> 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 %} {% 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" <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 == 'post_closing_status' %}x-show="status === 'CLOSING'"{% endif %}
{% if field == 'closing_date' %}x-show="['CLOSING', 'SBNO', 'CLOSED_PERM', 'DEMOLISHED', 'RELOCATED'].includes(status)"{% endif %}> {% if field == 'closing_date' %}x-show="['CLOSING', 'SBNO', 'CLOSED_PERM', 'DEMOLISHED', 'RELOCATED'].includes(status)"{% endif %}>
@@ -362,13 +370,6 @@
{% endif %} {% endif %}
{% endfor %} {% 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 --> <!-- Coaster Fields -->
<div x-show="showCoasterFields" <div x-show="showCoasterFields"
x-cloak x-cloak