mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:31:07 -05:00
Reorganize moderation dashboard structure:
- Move from /moderation/admin to /moderation to avoid conflicts - Improve template organization with partials - Add HTMX-powered navigation and filtering - Add smooth transitions and loading states - Improve photo submission handling - Add review notes functionality - Add confirmation dialogs
This commit is contained in:
@@ -4,8 +4,18 @@ from . import views
|
||||
app_name = 'moderation'
|
||||
|
||||
urlpatterns = [
|
||||
path('submissions/', views.EditSubmissionListView.as_view(), name='edit_submissions'),
|
||||
path('submissions/<int:submission_id>/approve/', views.approve_submission, name='approve_submission'),
|
||||
path('submissions/<int:submission_id>/reject/', views.reject_submission, name='reject_submission'),
|
||||
path('submissions/<int:submission_id>/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/<int:submission_id>/approve/', views.approve_submission, name='approve_submission'),
|
||||
path('edits/<int:submission_id>/reject/', views.reject_submission, name='reject_submission'),
|
||||
path('edits/<int:submission_id>/escalate/', views.escalate_submission, name='escalate_submission'),
|
||||
|
||||
# Photo Submissions
|
||||
path('photos/', views.PhotoSubmissionListView.as_view(), name='photo_submissions'),
|
||||
path('photos/<int:submission_id>/approve/', views.approve_photo, name='approve_photo'),
|
||||
path('photos/<int:submission_id>/reject/', views.reject_photo, name='reject_photo'),
|
||||
]
|
||||
|
||||
@@ -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):
|
||||
submission = get_object_or_404(EditSubmission, id=submission_id)
|
||||
"""HTMX endpoint for approving a submission"""
|
||||
if not request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']:
|
||||
return HttpResponse(status=403)
|
||||
|
||||
if request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']:
|
||||
submission = get_object_or_404(EditSubmission, id=submission_id)
|
||||
notes = request.POST.get('notes', '')
|
||||
|
||||
try:
|
||||
submission.approve(request.user)
|
||||
messages.success(request, 'Submission approved successfully')
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
{% extends "moderation/admin/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block moderation_content %}
|
||||
<div class="submission-list">
|
||||
<h1 class="mb-6 text-2xl font-bold">Content Submissions</h1>
|
||||
|
||||
<div class="mb-6 filters">
|
||||
<form method="get" class="flex flex-wrap gap-4">
|
||||
<select name="status" class="form-select">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="PENDING" {% if request.GET.status == 'PENDING' %}selected{% endif %}>Pending</option>
|
||||
<option value="APPROVED" {% if request.GET.status == 'APPROVED' %}selected{% endif %}>Approved</option>
|
||||
<option value="REJECTED" {% if request.GET.status == 'REJECTED' %}selected{% endif %}>Rejected</option>
|
||||
<option value="AUTO_APPROVED" {% if request.GET.status == 'AUTO_APPROVED' %}selected{% endif %}>Auto Approved</option>
|
||||
</select>
|
||||
<select name="type" class="form-select">
|
||||
<option value="">All Types</option>
|
||||
<option value="EDIT" {% if request.GET.type == 'EDIT' %}selected{% endif %}>Edits</option>
|
||||
<option value="CREATE" {% if request.GET.type == 'CREATE' %}selected{% endif %}>New Additions</option>
|
||||
</select>
|
||||
<button type="submit" class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">
|
||||
<i class="mr-2 fas fa-filter"></i>Filter
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% for submission in submissions %}
|
||||
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{% if submission.submission_type == 'CREATE' %}
|
||||
New {{ submission.content_type.name }}
|
||||
{% else %}
|
||||
Edit to {{ submission.content_type.name }}: {{ submission.content_object }}
|
||||
{% endif %}
|
||||
</h3>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Submitted by {{ submission.user.username }} on {{ submission.submitted_at|date:"M d, Y H:i" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="px-2 py-1 text-sm rounded-full {% if submission.submission_type == 'CREATE' %}bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200{% else %}bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200{% endif %}">
|
||||
{{ submission.get_submission_type_display }}
|
||||
</div>
|
||||
<div class="px-2 py-1 text-sm rounded-full {% if submission.status == 'PENDING' %}bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200
|
||||
{% elif submission.status == 'APPROVED' or submission.status == 'AUTO_APPROVED' %}bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200
|
||||
{% else %}bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200{% endif %}">
|
||||
{{ submission.get_status_display }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 rounded-lg bg-gray-50 dark:bg-gray-700/50">
|
||||
<div class="mb-2">
|
||||
<strong>Reason:</strong> {{ submission.reason }}
|
||||
</div>
|
||||
{% if submission.source %}
|
||||
<div>
|
||||
<strong>Source:</strong> {{ submission.source }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 rounded-lg bg-gray-50 dark:bg-gray-700/50">
|
||||
<h4 class="mb-2 font-medium">
|
||||
{% if submission.submission_type == 'CREATE' %}
|
||||
Details:
|
||||
{% else %}
|
||||
Changes:
|
||||
{% endif %}
|
||||
</h4>
|
||||
{% for field, value in submission.changes.items %}
|
||||
<div class="mb-2">
|
||||
<span class="font-medium">{{ field }}:</span>
|
||||
<span>{{ value }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if submission.status == 'PENDING' %}
|
||||
<form method="post" class="mt-4">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="submission_id" value="{{ submission.id }}">
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2 font-medium">Review Notes:</label>
|
||||
<textarea name="review_notes" rows="3"
|
||||
class="w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="{% if submission.submission_type == 'CREATE' %}Explain why you're approving/rejecting this new addition{% else %}Explain why you're approving/rejecting these changes{% endif %}"></textarea>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-4">
|
||||
<button type="submit" name="action" value="reject"
|
||||
class="px-4 py-2 text-white bg-red-600 rounded-lg hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600">
|
||||
<i class="mr-2 fas fa-times"></i>Reject
|
||||
</button>
|
||||
<button type="submit" name="action" value="approve"
|
||||
class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600">
|
||||
<i class="mr-2 fas fa-check"></i>Approve
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="p-4 rounded-lg bg-gray-50 dark:bg-gray-700/50">
|
||||
<div class="mb-2">
|
||||
<strong>Reviewed by:</strong> {{ submission.reviewed_by.username }} on {{ submission.reviewed_at|date:"M d, Y H:i" }}
|
||||
</div>
|
||||
{% if submission.review_notes %}
|
||||
<div>
|
||||
<strong>Review Notes:</strong> {{ submission.review_notes }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
||||
No submissions found.
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="flex justify-center mt-6">
|
||||
<div class="inline-flex rounded-md shadow-sm">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page=1{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">« First</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border-t border-b border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-3 py-2 text-gray-700 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border-t border-b border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Next</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Last »</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,104 +0,0 @@
|
||||
{% extends "moderation/admin/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block moderation_content %}
|
||||
<div class="submission-list">
|
||||
<h1 class="mb-6 text-2xl font-bold">Photo Submissions</h1>
|
||||
|
||||
<div class="mb-6 filters">
|
||||
<form method="get" class="flex space-x-4">
|
||||
<select name="status" class="form-select">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="PENDING" {% if request.GET.status == 'PENDING' %}selected{% endif %}>Pending</option>
|
||||
<option value="APPROVED" {% if request.GET.status == 'APPROVED' %}selected{% endif %}>Approved</option>
|
||||
<option value="REJECTED" {% if request.GET.status == 'REJECTED' %}selected{% endif %}>Rejected</option>
|
||||
<option value="AUTO_APPROVED" {% if request.GET.status == 'AUTO_APPROVED' %}selected{% endif %}>Auto Approved</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary">Filter</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% for submission in submissions %}
|
||||
<div class="submission-card">
|
||||
<div class="submission-header">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">
|
||||
Photo for {{ submission.content_type.name }}: {{ submission.content_object }}
|
||||
</h3>
|
||||
<div class="submission-meta">
|
||||
Submitted by {{ submission.user.username }} on {{ submission.submitted_at|date:"M d, Y H:i" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-badge status-{{ submission.status|lower }}">
|
||||
{{ submission.get_status_display }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="submission-details">
|
||||
<div class="mb-4 photo-preview">
|
||||
<img src="{{ submission.photo.url }}" alt="Submitted photo" class="max-w-lg rounded">
|
||||
</div>
|
||||
{% if submission.caption %}
|
||||
<div class="mb-2">
|
||||
<strong>Caption:</strong> {{ submission.caption }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if submission.date_taken %}
|
||||
<div class="mb-2">
|
||||
<strong>Date Taken:</strong> {{ submission.date_taken|date:"M d, Y" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if submission.status == 'PENDING' %}
|
||||
<form method="post" class="mt-4">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="submission_id" value="{{ submission.id }}">
|
||||
<div class="review-notes">
|
||||
<label class="block mb-2">Review Notes:</label>
|
||||
<textarea name="review_notes" rows="3" class="w-full"></textarea>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button type="submit" name="action" value="approve" class="btn-approve">
|
||||
Approve
|
||||
</button>
|
||||
<button type="submit" name="action" value="reject" class="btn-reject">
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="mt-4">
|
||||
<strong>Reviewed by:</strong> {{ submission.reviewed_by.username }}
|
||||
<br>
|
||||
<strong>Review Notes:</strong> {{ submission.review_notes }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="py-8 text-center text-gray-500">
|
||||
No submissions found.
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="mt-6 pagination">
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page=1">« first</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}">next</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}">last »</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -168,9 +168,9 @@
|
||||
<div class="moderation-nav-container">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{% url 'moderation:admin:index' %}"
|
||||
class="{% if request.resolver_match.url_name == 'index' %}active{% endif %}"
|
||||
hx-get="{% url 'moderation:admin:index' %}"
|
||||
<a href="{% url 'moderation:dashboard' %}"
|
||||
class="{% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
|
||||
hx-get="{% url 'moderation:dashboard' %}"
|
||||
hx-target="#submissions-content"
|
||||
hx-push-url="true">
|
||||
<i class="fas fa-tachometer-alt"></i>
|
||||
@@ -197,21 +197,24 @@
|
||||
Photo Submissions
|
||||
</a>
|
||||
</li>
|
||||
{% if user.role == 'ADMIN' or user.role == 'SUPERUSER' %}
|
||||
<li>
|
||||
<a href="{% url 'admin:index' %}"
|
||||
class="{% if request.resolver_match.url_name == 'admin' %}active{% endif %}">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
Admin Panel
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="moderation-content">
|
||||
{% block moderation_content %}{% endblock %}
|
||||
{% block moderation_content %}
|
||||
<div id="submissions-content">
|
||||
{% include "moderation/partials/submission_list.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div id="loading-indicator"
|
||||
class="fixed inset-0 flex items-center justify-center htmx-indicator bg-black/20 dark:bg-black/40">
|
||||
<div class="flex items-center p-6 space-x-4 bg-white rounded-lg shadow-xl dark:bg-gray-800">
|
||||
<div class="w-8 h-8 border-4 border-blue-500 rounded-full animate-spin border-t-transparent"></div>
|
||||
<span class="text-gray-700 dark:text-gray-300">Processing...</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
8
templates/moderation/edit_submission_list.html
Normal file
8
templates/moderation/edit_submission_list.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "moderation/dashboard.html" %}
|
||||
|
||||
{% block moderation_content %}
|
||||
<div id="submissions-content">
|
||||
{% include "moderation/partials/filters.html" %}
|
||||
{% include "moderation/partials/submission_list.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
85
templates/moderation/partials/photo_submission.html
Normal file
85
templates/moderation/partials/photo_submission.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<div class="p-6 submission-card" id="submission-{{ submission.id }}">
|
||||
<div class="mb-4 submission-header">
|
||||
<div>
|
||||
<h3 class="flex items-center gap-2 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
<span class="status-badge
|
||||
{% if submission.status == 'NEW' %}status-pending
|
||||
{% elif submission.status == 'APPROVED' %}status-approved
|
||||
{% elif submission.status == 'REJECTED' %}status-rejected
|
||||
{% elif submission.status == 'AUTO_APPROVED' %}status-approved{% endif %}">
|
||||
{{ submission.get_status_display }}
|
||||
</span>
|
||||
Photo for {{ submission.content_object }}
|
||||
</h3>
|
||||
<div class="mt-1 submission-meta">
|
||||
<span class="inline-flex items-center">
|
||||
<i class="mr-1 fas fa-user"></i>
|
||||
{{ submission.user.username }}
|
||||
</span>
|
||||
<span class="mx-2">•</span>
|
||||
<span class="inline-flex items-center">
|
||||
<i class="mr-1 fas fa-clock"></i>
|
||||
{{ submission.created_at|date:"M d, Y H:i" }}
|
||||
</span>
|
||||
{% if submission.date_taken %}
|
||||
<span class="mx-2">•</span>
|
||||
<span class="inline-flex items-center">
|
||||
<i class="mr-1 fas fa-calendar"></i>
|
||||
Taken: {{ submission.date_taken|date:"M d, Y" }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 overflow-hidden bg-gray-100 rounded-lg aspect-w-16 aspect-h-9 dark:bg-gray-800">
|
||||
<img src="{{ submission.photo.url }}"
|
||||
alt="{{ submission.caption|default:'Submitted photo' }}"
|
||||
class="object-contain w-full h-full">
|
||||
</div>
|
||||
|
||||
{% if submission.caption %}
|
||||
<div class="p-4 mt-2 rounded-lg bg-gray-50 dark:bg-gray-700/50">
|
||||
<div class="text-sm font-medium text-gray-700 dark:text-gray-300">Caption:</div>
|
||||
<div class="mt-1 text-gray-600 dark:text-gray-400">{{ submission.caption }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if submission.status == 'NEW' %}
|
||||
<div class="mt-4 review-notes" x-data="{ showNotes: false }">
|
||||
<textarea x-show="showNotes"
|
||||
name="notes"
|
||||
class="w-full p-3 border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Add review notes (optional)"
|
||||
rows="3"></textarea>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 mt-4 action-buttons">
|
||||
<button class="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
@click="showNotes = !showNotes">
|
||||
<i class="mr-2 fas fa-comment-alt"></i>
|
||||
Add Notes
|
||||
</button>
|
||||
|
||||
<button class="btn-approve"
|
||||
hx-post="{% url 'moderation:approve_photo' submission.id %}"
|
||||
hx-target="#submission-{{ submission.id }}"
|
||||
hx-include="closest .review-notes"
|
||||
hx-confirm="Are you sure you want to approve this photo?"
|
||||
hx-indicator="#loading-indicator">
|
||||
<i class="mr-2 fas fa-check"></i>
|
||||
Approve
|
||||
</button>
|
||||
|
||||
<button class="btn-reject"
|
||||
hx-post="{% url 'moderation:reject_photo' submission.id %}"
|
||||
hx-target="#submission-{{ submission.id }}"
|
||||
hx-include="closest .review-notes"
|
||||
hx-confirm="Are you sure you want to reject this photo?"
|
||||
hx-indicator="#loading-indicator">
|
||||
<i class="mr-2 fas fa-times"></i>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
16
templates/moderation/photo_submission_list.html
Normal file
16
templates/moderation/photo_submission_list.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "moderation/dashboard.html" %}
|
||||
|
||||
{% block moderation_content %}
|
||||
<div id="submissions-content" class="submission-list">
|
||||
{% for submission in submissions %}
|
||||
{% include "moderation/partials/photo_submission.html" %}
|
||||
{% empty %}
|
||||
<div class="p-8 text-center bg-white border rounded-lg shadow-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
|
||||
<div class="text-gray-500 dark:text-gray-400">
|
||||
<i class="mb-3 text-4xl fas fa-camera"></i>
|
||||
<p>No photo submissions found.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user