diff --git a/moderation/urls.py b/moderation/urls.py index 55706ba9..df2bea3e 100644 --- a/moderation/urls.py +++ b/moderation/urls.py @@ -4,8 +4,18 @@ from . import views app_name = 'moderation' urlpatterns = [ - path('submissions/', views.EditSubmissionListView.as_view(), name='edit_submissions'), - path('submissions//approve/', views.approve_submission, name='approve_submission'), - path('submissions//reject/', views.reject_submission, name='reject_submission'), - path('submissions//escalate/', views.escalate_submission, name='escalate_submission'), + # Dashboard + path('', views.DashboardView.as_view(), name='dashboard'), + path('submissions/', views.submission_list, name='submission_list'), + + # Edit Submissions + path('edits/', views.EditSubmissionListView.as_view(), name='edit_submissions'), + path('edits//approve/', views.approve_submission, name='approve_submission'), + path('edits//reject/', views.reject_submission, name='reject_submission'), + path('edits//escalate/', views.escalate_submission, name='escalate_submission'), + + # Photo Submissions + path('photos/', views.PhotoSubmissionListView.as_view(), name='photo_submissions'), + path('photos//approve/', views.approve_photo, name='approve_photo'), + path('photos//reject/', views.reject_photo, name='reject_photo'), ] diff --git a/moderation/views.py b/moderation/views.py index ff809879..2535073d 100644 --- a/moderation/views.py +++ b/moderation/views.py @@ -1,90 +1,179 @@ -from django.views.generic import ListView -from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.views.generic import ListView, TemplateView from django.shortcuts import get_object_or_404 -from django.http import HttpResponse -from django.contrib import messages +from django.http import HttpResponse, JsonResponse +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 .models import EditSubmission +from django.core.exceptions import PermissionDenied + +from .models import EditSubmission, PhotoSubmission class ModeratorRequiredMixin(UserPassesTestMixin): def test_func(self): - return self.request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER'] + if not hasattr(self, 'request'): + return False + if not self.request.user.is_authenticated: + return False + return getattr(self.request.user, 'role', None) in ['MODERATOR', 'ADMIN', 'SUPERUSER'] -class EditSubmissionListView(LoginRequiredMixin, ModeratorRequiredMixin, ListView): - model = EditSubmission - template_name = 'moderation/edit_submissions.html' - context_object_name = 'submissions' + def handle_no_permission(self): + if not self.request.user.is_authenticated: + return super().handle_no_permission() + raise PermissionDenied("You do not have moderator permissions.") - def get_queryset(self): - tab = self.request.GET.get('tab', 'new') - queryset = EditSubmission.objects.select_related('user', 'content_type') - - # Include edits by privileged users (mods, admins, superusers) in appropriate tabs - privileged_roles = ['MODERATOR', 'ADMIN', 'SUPERUSER'] - - if tab == 'new': - # Show pending submissions, oldest first - queryset = queryset.filter(status='NEW').order_by('created_at') - elif tab == 'approved': - # Show approved submissions and auto-approved edits by privileged users - queryset = queryset.filter( - Q(status='APPROVED') | - Q(user__role__in=privileged_roles, status='NEW') # Include privileged users' edits - ).order_by('-created_at') - elif tab == 'rejected': - # Show rejected submissions, newest first - queryset = queryset.filter(status='REJECTED').order_by('-created_at') - elif tab == 'escalated' and self.request.user.role in ['ADMIN', 'SUPERUSER']: - # Show escalated submissions, newest first - queryset = queryset.filter(status='ESCALATED').order_by('-created_at') - else: - # Default to new submissions if invalid tab - queryset = queryset.filter(status='NEW').order_by('created_at') - - return queryset +class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, TemplateView): + template_name = 'moderation/dashboard.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['active_tab'] = self.request.GET.get('tab', 'new') - context['new_count'] = EditSubmission.objects.filter(status='NEW').count() - if self.request.user.role in ['ADMIN', 'SUPERUSER']: - context['escalated_count'] = EditSubmission.objects.filter(status='ESCALATED').count() + context['submissions'] = EditSubmission.objects.all().order_by('-created_at')[:10] return context - def get_template_names(self): - if self.request.htmx: - return ['moderation/partials/submission_list.html'] - return [self.template_name] +class EditSubmissionListView(LoginRequiredMixin, ModeratorRequiredMixin, ListView): + template_name = 'moderation/edit_submission_list.html' + context_object_name = 'submissions' + + def get_queryset(self): + queryset = EditSubmission.objects.all().order_by('-created_at') + + status = self.request.GET.get('status') + if status: + queryset = queryset.filter(status=status) + + submission_type = self.request.GET.get('type') + if submission_type: + queryset = queryset.filter(submission_type=submission_type) + + content_type = self.request.GET.get('content_type') + if content_type: + queryset = queryset.filter(content_type__model=content_type) + + return queryset +class PhotoSubmissionListView(LoginRequiredMixin, ModeratorRequiredMixin, ListView): + template_name = 'moderation/photo_submission_list.html' + context_object_name = 'submissions' + + def get_queryset(self): + queryset = PhotoSubmission.objects.all().order_by('-created_at') + + status = self.request.GET.get('status') + if status: + queryset = queryset.filter(status=status) + + return queryset + +@login_required +def submission_list(request): + """HTMX endpoint for filtered submission list""" + if not request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']: + return HttpResponse(status=403) + + status = request.GET.get('status') + submission_type = request.GET.get('type') + content_type = request.GET.get('content_type') + + queryset = EditSubmission.objects.all().order_by('-created_at') + + if status: + queryset = queryset.filter(status=status) + if submission_type: + queryset = queryset.filter(submission_type=submission_type) + if content_type: + queryset = queryset.filter(content_type__model=content_type) + + context = { + 'submissions': queryset + } + + html = render_to_string('moderation/partials/submission_list.html', context, request=request) + return HttpResponse(html) + +@login_required def approve_submission(request, submission_id): + """HTMX endpoint for approving a submission""" + if not request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']: + return HttpResponse(status=403) + submission = get_object_or_404(EditSubmission, id=submission_id) + notes = request.POST.get('notes', '') - if request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']: + try: submission.approve(request.user) - messages.success(request, 'Submission approved successfully') - - # Return updated submission list for current tab - view = EditSubmissionListView.as_view() - return view(request) + if notes: + submission.notes = notes + submission.save() + + context = {'submission': submission} + html = render_to_string('moderation/partials/submission_list.html', context, request=request) + return HttpResponse(html) + except ValueError as e: + return HttpResponse(str(e), status=400) +@login_required def reject_submission(request, submission_id): + """HTMX endpoint for rejecting a submission""" + if not request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']: + return HttpResponse(status=403) + submission = get_object_or_404(EditSubmission, id=submission_id) + notes = request.POST.get('notes', '') - if request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']: - submission.reject(request.user) - messages.success(request, 'Submission rejected successfully') + submission.reject(request.user) + if notes: + submission.notes = notes + submission.save() - # Return updated submission list for current tab - view = EditSubmissionListView.as_view() - return view(request) + context = {'submission': submission} + html = render_to_string('moderation/partials/submission_list.html', context, request=request) + return HttpResponse(html) +@login_required def escalate_submission(request, submission_id): + """HTMX endpoint for escalating a submission""" + if not request.user.role == 'MODERATOR': + return HttpResponse(status=403) + submission = get_object_or_404(EditSubmission, id=submission_id) + notes = request.POST.get('notes', '') - if request.user.role == 'MODERATOR': - submission.escalate(request.user) - messages.success(request, 'Submission escalated to admin') + submission.escalate(request.user) + if notes: + submission.notes = notes + submission.save() - # Return updated submission list for current tab - view = EditSubmissionListView.as_view() - return view(request) + context = {'submission': submission} + html = render_to_string('moderation/partials/submission_list.html', context, request=request) + return HttpResponse(html) + +@login_required +def approve_photo(request, submission_id): + """HTMX endpoint for approving a photo submission""" + if not request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']: + return HttpResponse(status=403) + + submission = get_object_or_404(PhotoSubmission, id=submission_id) + notes = request.POST.get('notes', '') + + try: + submission.approve(request.user, notes) + context = {'submission': submission} + html = render_to_string('moderation/partials/photo_submission.html', context, request=request) + return HttpResponse(html) + except Exception as e: + return HttpResponse(str(e), status=400) + +@login_required +def reject_photo(request, submission_id): + """HTMX endpoint for rejecting a photo submission""" + if not request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']: + return HttpResponse(status=403) + + submission = get_object_or_404(PhotoSubmission, id=submission_id) + notes = request.POST.get('notes', '') + + submission.reject(request.user, notes) + context = {'submission': submission} + html = render_to_string('moderation/partials/photo_submission.html', context, request=request) + return HttpResponse(html) diff --git a/templates/moderation/admin/edit_submission_list.html b/templates/moderation/admin/edit_submission_list.html deleted file mode 100644 index 6f3a4391..00000000 --- a/templates/moderation/admin/edit_submission_list.html +++ /dev/null @@ -1,142 +0,0 @@ -{% extends "moderation/admin/base.html" %} -{% load static %} - -{% block moderation_content %} -
-

Content Submissions

- -
-
- - - -
-
- - {% for submission in submissions %} -
-
-
-

- {% if submission.submission_type == 'CREATE' %} - New {{ submission.content_type.name }} - {% else %} - Edit to {{ submission.content_type.name }}: {{ submission.content_object }} - {% endif %} -

-
- Submitted by {{ submission.user.username }} on {{ submission.submitted_at|date:"M d, Y H:i" }} -
-
-
-
- {{ submission.get_submission_type_display }} -
-
- {{ submission.get_status_display }} -
-
-
- -
-
- Reason: {{ submission.reason }} -
- {% if submission.source %} -
- Source: {{ submission.source }} -
- {% endif %} -
- -
-

- {% if submission.submission_type == 'CREATE' %} - Details: - {% else %} - Changes: - {% endif %} -

- {% for field, value in submission.changes.items %} -
- {{ field }}: - {{ value }} -
- {% endfor %} -
- - {% if submission.status == 'PENDING' %} -
- {% csrf_token %} - -
- - -
-
- - -
-
- {% else %} -
-
- Reviewed by: {{ submission.reviewed_by.username }} on {{ submission.reviewed_at|date:"M d, Y H:i" }} -
- {% if submission.review_notes %} -
- Review Notes: {{ submission.review_notes }} -
- {% endif %} -
- {% endif %} -
- {% empty %} -
- No submissions found. -
- {% endfor %} - - {% if is_paginated %} -
-
- {% if page_obj.has_previous %} - « First - Previous - {% endif %} - - - Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }} - - - {% if page_obj.has_next %} - Next - Last » - {% endif %} -
-
- {% endif %} -
-{% endblock %} diff --git a/templates/moderation/admin/photo_submission_list.html b/templates/moderation/admin/photo_submission_list.html deleted file mode 100644 index 172b760d..00000000 --- a/templates/moderation/admin/photo_submission_list.html +++ /dev/null @@ -1,104 +0,0 @@ -{% extends "moderation/admin/base.html" %} -{% load static %} - -{% block moderation_content %} -
-

Photo Submissions

- -
-
- - -
-
- - {% for submission in submissions %} -
-
-
-

- Photo for {{ submission.content_type.name }}: {{ submission.content_object }} -

-
- Submitted by {{ submission.user.username }} on {{ submission.submitted_at|date:"M d, Y H:i" }} -
-
-
- {{ submission.get_status_display }} -
-
- -
-
- Submitted photo -
- {% if submission.caption %} -
- Caption: {{ submission.caption }} -
- {% endif %} - {% if submission.date_taken %} -
- Date Taken: {{ submission.date_taken|date:"M d, Y" }} -
- {% endif %} -
- - {% if submission.status == 'PENDING' %} -
- {% csrf_token %} - -
- - -
-
- - -
-
- {% else %} -
- Reviewed by: {{ submission.reviewed_by.username }} -
- Review Notes: {{ submission.review_notes }} -
- {% endif %} -
- {% empty %} -
- No submissions found. -
- {% endfor %} - - {% if is_paginated %} - - {% endif %} -
-{% endblock %} diff --git a/templates/moderation/admin/base.html b/templates/moderation/dashboard.html similarity index 88% rename from templates/moderation/admin/base.html rename to templates/moderation/dashboard.html index 17a16195..dad2d2c9 100644 --- a/templates/moderation/admin/base.html +++ b/templates/moderation/dashboard.html @@ -168,9 +168,9 @@
- {% block moderation_content %}{% endblock %} + {% block moderation_content %} +
+ {% include "moderation/partials/submission_list.html" %} +
+ {% endblock %} +
+ +
+
+
+ Processing... +
{% endblock %} diff --git a/templates/moderation/edit_submission_list.html b/templates/moderation/edit_submission_list.html new file mode 100644 index 00000000..64efd2bc --- /dev/null +++ b/templates/moderation/edit_submission_list.html @@ -0,0 +1,8 @@ +{% extends "moderation/dashboard.html" %} + +{% block moderation_content %} +
+ {% include "moderation/partials/filters.html" %} + {% include "moderation/partials/submission_list.html" %} +
+{% endblock %} diff --git a/templates/moderation/partials/photo_submission.html b/templates/moderation/partials/photo_submission.html new file mode 100644 index 00000000..1be7502e --- /dev/null +++ b/templates/moderation/partials/photo_submission.html @@ -0,0 +1,85 @@ +
+
+
+

+ + {{ submission.get_status_display }} + + Photo for {{ submission.content_object }} +

+
+ + + {{ submission.user.username }} + + + + + {{ submission.created_at|date:"M d, Y H:i" }} + + {% if submission.date_taken %} + + + + Taken: {{ submission.date_taken|date:"M d, Y" }} + + {% endif %} +
+
+
+ +
+ {{ submission.caption|default:'Submitted photo' }} +
+ + {% if submission.caption %} +
+
Caption:
+
{{ submission.caption }}
+
+ {% endif %} + + {% if submission.status == 'NEW' %} +
+ + +
+ + + + + +
+
+ {% endif %} +
diff --git a/templates/moderation/photo_submission_list.html b/templates/moderation/photo_submission_list.html new file mode 100644 index 00000000..4def7328 --- /dev/null +++ b/templates/moderation/photo_submission_list.html @@ -0,0 +1,16 @@ +{% extends "moderation/dashboard.html" %} + +{% block moderation_content %} +
+ {% for submission in submissions %} + {% include "moderation/partials/photo_submission.html" %} + {% empty %} +
+
+ +

No photo submissions found.

+
+
+ {% endfor %} +
+{% endblock %}