diff --git a/moderation/templatetags/moderation_tags.py b/moderation/templatetags/moderation_tags.py index 61c6a9d7..4aad4182 100644 --- a/moderation/templatetags/moderation_tags.py +++ b/moderation/templatetags/moderation_tags.py @@ -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), []) diff --git a/moderation/views.py b/moderation/views.py index 42f47a1c..ab723692 100644 --- a/moderation/views.py +++ b/moderation/views.py @@ -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') - - 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) + queryset = get_filtered_queryset(request, status, submission_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,54 +215,73 @@ 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 - notes = request.POST.get('notes') - if not notes: - return HttpResponse("Notes are required when editing a submission", status=400) + 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: + edited_changes = dict(submission.changes) if submission.changes else {} + + # Update stats if present + if 'stats' in edited_changes: + 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 - try: - # Update the moderator_changes with the edited values - edited_changes = submission.changes.copy() - for field in submission.changes.keys(): - if field == 'stats': - edited_stats = {} - for key in submission.changes['stats'].keys(): - 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: - if new_value := request.POST.get(field): - # 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 - submission.notes = notes - submission.save() + edited_changes[field] = new_value + + # 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() + + # 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) - # 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 [] - } - 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) + except Exception as e: + return HttpResponse(str(e), status=400) @login_required 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) 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', '') - - 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 [] - } + queryset = get_filtered_queryset(request, status, submission_type) + context = get_context_data(request, queryset) + 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", "") - - 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 = { + queryset = get_filtered_queryset(request, status, submission_type) + + 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.""" diff --git a/templates/moderation/partials/location_map.html b/templates/moderation/partials/location_map.html index 5bc09e91..dcf35940 100644 --- a/templates/moderation/partials/location_map.html +++ b/templates/moderation/partials/location_map.html @@ -1,23 +1,48 @@ {% load moderation_tags %} -