From 6a9154ce69f3402772ce3c1b3484380c5550d1e5 Mon Sep 17 00:00:00 2001 From: pacnpal <183241239+pacnpal@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:37:36 +0000 Subject: [PATCH] Improve moderation dashboard UI and functionality - Add status tabs (Pending, Approved, Rejected, Escalated) - Implement HTMX for smooth tab switching and status changes - Add proper permissions for escalated submissions - Change Status filter to Submission Type (Text/Photo) - Move navigation into dashboard content - Fix tab menu visibility and transitions - Add contextual loading indicator - Update styling to match dark theme - Ensure consistent styling across components --- moderation/views.py | 191 ++++++++----- templates/moderation/dashboard.html | 177 ++---------- .../partials/dashboard_content.html | 160 +++++++---- .../moderation/partials/moderation_nav.html | 75 ++--- .../moderation/partials/submission_list.html | 270 +++++++++--------- 5 files changed, 440 insertions(+), 433 deletions(-) diff --git a/moderation/views.py b/moderation/views.py index 8fe35fd2..04243181 100644 --- a/moderation/views.py +++ b/moderation/views.py @@ -1,5 +1,5 @@ from django.views.generic import ListView, TemplateView -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, render from django.http import HttpResponse, JsonResponse, HttpRequest from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.decorators import login_required @@ -29,91 +29,102 @@ class ModeratorRequiredMixin(UserPassesTestMixin): return super().handle_no_permission() raise PermissionDenied("You do not have moderator permissions.") -class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, TemplateView): +class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, ListView): + template_name = 'moderation/dashboard.html' + context_object_name = 'submissions' + paginate_by = 10 + def get_template_names(self): if self.request.headers.get('HX-Request'): return ['moderation/partials/dashboard_content.html'] - return ['moderation/dashboard.html'] + return [self.template_name] - def get_context_data(self, **kwargs: Any) -> dict[str, Any]: - context = super().get_context_data(**kwargs) - context['submissions'] = EditSubmission.objects.all().order_by('-created_at')[:10] - return context - -class EditSubmissionListView(LoginRequiredMixin, ModeratorRequiredMixin, ListView): - context_object_name = 'submissions' - - def get_template_names(self): - if self.request.headers.get('HX-Request'): - return ['moderation/partials/edit_submission_content.html'] - return ['moderation/edit_submission_list.html'] - def get_queryset(self): - queryset = EditSubmission.objects.all().order_by('-created_at') + status = self.request.GET.get('status', 'NEW') + submission_type = self.request.GET.get('submission_type', '') - if status := self.request.GET.get('status'): - queryset = queryset.filter(status=status) + 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 submission_type := self.request.GET.get('type'): - queryset = queryset.filter(submission_type=submission_type) - - if content_type := self.request.GET.get('content_type'): - queryset = queryset.filter(content_type__model=content_type) - - return queryset - -class PhotoSubmissionListView(LoginRequiredMixin, ModeratorRequiredMixin, ListView): - context_object_name = 'submissions' - - def get_template_names(self): - if self.request.headers.get('HX-Request'): - return ['moderation/partials/photo_submission_content.html'] - return ['moderation/photo_submission_list.html'] - - def get_queryset(self): - queryset = PhotoSubmission.objects.all().order_by('-created_at') - - if status := self.request.GET.get('status'): - queryset = queryset.filter(status=status) + 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 def submission_list(request: HttpRequest) -> HttpResponse: - """HTMX endpoint for filtered submission list""" + """View for submission list with filters""" user = cast(User, request.user) if not (user.role in MODERATOR_ROLES or user.is_superuser): return HttpResponse(status=403) - queryset = EditSubmission.objects.all().order_by('-created_at') + status = request.GET.get('status', 'NEW') + submission_type = request.GET.get('submission_type', '') - if status := request.GET.get('status'): - queryset = queryset.filter(status=status) - if submission_type := request.GET.get('type'): - queryset = queryset.filter(submission_type=submission_type) - if content_type := request.GET.get('content_type'): - queryset = queryset.filter(content_type__model=content_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) - html = render_to_string( - 'moderation/partials/submission_list.html', - {'submissions': queryset}, - request=request - ) - return HttpResponse(html) + context = { + 'submissions': queryset, + 'user': request.user, + } + + # 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) + + # For direct URL access, return the full dashboard template + return render(request, 'moderation/dashboard.html', context) @login_required def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse: """HTMX endpoint for approving a submission""" user = cast(User, request.user) + submission = get_object_or_404(EditSubmission, id=submission_id) + + if submission.status == 'ESCALATED' and not (user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser): + return HttpResponse("Only admins can handle escalated submissions", status=403) if not (user.role in MODERATOR_ROLES or user.is_superuser): return HttpResponse(status=403) - - submission = get_object_or_404(EditSubmission, id=submission_id) try: submission.approve(user) _update_submission_notes(submission, request.POST.get('notes')) - return _render_submission_response('moderation/partials/submission_list.html', submission, request) + + # Get updated queryset with filters + status = request.GET.get('status', 'NEW') + 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 = { + 'submissions': queryset, + 'user': request.user, + } + + return render(request, 'moderation/partials/dashboard_content.html', context) except ValueError as e: return HttpResponse(str(e), status=400) @@ -121,27 +132,73 @@ def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse def reject_submission(request: HttpRequest, submission_id: int) -> HttpResponse: """HTMX endpoint for rejecting a submission""" user = cast(User, request.user) + submission = get_object_or_404(EditSubmission, id=submission_id) + + if submission.status == 'ESCALATED' and not (user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser): + return HttpResponse("Only admins can handle escalated submissions", status=403) if not (user.role in MODERATOR_ROLES or user.is_superuser): return HttpResponse(status=403) - - submission = get_object_or_404(EditSubmission, id=submission_id) + submission.reject(user) _update_submission_notes(submission, request.POST.get('notes')) - return _render_submission_response('moderation/partials/submission_list.html', submission, request) + # Get updated queryset with filters + status = request.GET.get('status', 'NEW') + 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 = { + 'submissions': queryset, + 'user': request.user, + } + + return render(request, 'moderation/partials/dashboard_content.html', context) @login_required def escalate_submission(request: HttpRequest, submission_id: int) -> HttpResponse: """HTMX endpoint for escalating a submission""" user = cast(User, request.user) - if user.role != 'MODERATOR' and not user.is_superuser: + if not (user.role in MODERATOR_ROLES or user.is_superuser): return HttpResponse(status=403) submission = get_object_or_404(EditSubmission, id=submission_id) + if submission.status == 'ESCALATED': + return HttpResponse("Submission is already escalated", status=400) + submission.escalate(user) _update_submission_notes(submission, request.POST.get('notes')) - return _render_submission_response('moderation/partials/submission_list.html', submission, request) + # Get updated queryset with filters + status = request.GET.get('status', 'NEW') + 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 = { + '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: @@ -154,7 +211,7 @@ def approve_photo(request: HttpRequest, submission_id: int) -> HttpResponse: try: submission.approve(user, request.POST.get('notes', '')) - return _render_submission_response('moderation/partials/photo_submission.html', submission, request) + return render(request, 'moderation/partials/photo_submission.html', {'submission': submission}) except Exception as e: return HttpResponse(str(e), status=400) @@ -168,16 +225,10 @@ 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_submission_response('moderation/partials/photo_submission.html', submission, request) + 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.""" if notes: submission.notes = notes submission.save() - -def _render_submission_response(template: str, submission: Any, request: HttpRequest) -> HttpResponse: - """Render submission template response.""" - context = {'submission': submission} - html = render_to_string(template, context, request=request) - return HttpResponse(html) diff --git a/templates/moderation/dashboard.html b/templates/moderation/dashboard.html index 66e3c081..89e8624f 100644 --- a/templates/moderation/dashboard.html +++ b/templates/moderation/dashboard.html @@ -5,108 +5,24 @@ {% block extra_css %}