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,54 +215,73 @@ 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')
if not notes:
return HttpResponse("Notes are required when editing a submission", status=400)
try: notes = request.POST.get('notes')
# Update the moderator_changes with the edited values if not notes:
edited_changes = submission.changes.copy() return HttpResponse("Notes are required when editing a submission", status=400)
for field in submission.changes.keys():
if field == 'stats': try:
edited_stats = {} edited_changes = dict(submission.changes) if submission.changes else {}
for key in submission.changes['stats'].keys():
if new_value := request.POST.get(f'stats.{key}'): # Update stats if present
edited_stats[key] = new_value if 'stats' in edited_changes:
edited_changes['stats'] = edited_stats edited_stats = {}
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
# 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 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:
return HttpResponse(f"Invalid value for {field}", status=400)
else: else:
if new_value := request.POST.get(field): edited_changes[field] = new_value
# Handle special field types
if field in ['latitude', 'longitude', 'size_acres']:
try:
edited_changes[field] = float(new_value)
except ValueError:
return HttpResponse(f"Invalid value for {field}", status=400)
else:
edited_changes[field] = new_value
submission.moderator_changes = edited_changes # Convert to JSON-serializable format
submission.notes = notes json_changes = json.loads(json.dumps(edited_changes, cls=DjangoJSONEncoder))
submission.save() submission.moderator_changes = json_changes
submission.notes = notes
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'])],
'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)
except Exception as e: context = get_context_data(request, EditSubmission.objects.filter(id=submission_id))
return HttpResponse(str(e), status=400) return render(request, 'moderation/partials/submission_list.html', context)
return HttpResponse("Invalid request method", status=405) except Exception as e:
return HttpResponse(str(e), status=400)
@login_required @login_required
def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse: def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
@@ -273,36 +289,22 @@ def approve_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)
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,87 +19,92 @@
} }
</style> </style>
<div class="location-widget" id="locationWidget-{{ submission.id }}"> <div class="p-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
{# Search Form #} <h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-300">Location</h3>
<div class="relative mb-4">
<label class="block mb-1 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"
placeholder="Search for a location..."
autocomplete="off"
style="z-index: 10;">
<div id="searchResults-{{ submission.id }}"
style="position: absolute; top: 100%; left: 0; right: 0; z-index: 1000;"
class="hidden w-full mt-1 overflow-auto bg-white border rounded-md shadow-lg max-h-60 dark:bg-gray-700 dark:border-gray-600">
</div>
</div>
{# Map Container #} <div class="location-widget" id="locationWidget-{{ submission.id }}">
<div class="relative mb-4" style="z-index: 1;"> {# Search Form #}
<div id="locationMap-{{ submission.id }}" class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div> <div class="relative mb-4">
</div> <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 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;">
<div id="searchResults-{{ submission.id }}"
style="position: absolute; top: 100%; left: 0; right: 0; z-index: 1000;"
class="hidden w-full mt-1 overflow-auto bg-white border rounded-md shadow-lg max-h-60 dark:bg-gray-700 dark:border-gray-600">
</div>
</div>
{# Location Form Fields #} {# Map Container #}
<div class="relative grid grid-cols-1 gap-4 md:grid-cols-2" style="z-index: 10;"> <div class="relative mb-4" style="z-index: 1;">
<div> <div id="locationMap-{{ submission.id }}"
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300"> class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
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 }}">
</div> </div>
<div>
<label class="block mb-1 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 }}">
</div>
<div>
<label class="block mb-1 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 }}">
</div>
<div>
<label class="block mb-1 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 }}">
</div>
<div>
<label class="block mb-1 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 }}">
</div>
</div>
{# Hidden Coordinate Fields #} {# Location Form Fields #}
<div class="hidden"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<input type="hidden" name="latitude" id="latitude-{{ submission.id }}" value="{{ form.latitude }}"> <div>
<input type="hidden" name="longitude" id="longitude-{{ submission.id }}" value="{{ form.longitude }}"> <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 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-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 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-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 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-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 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-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 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="{{ submission.changes.latitude }}">
<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