good stuff

This commit is contained in:
pacnpal
2024-10-29 21:29:16 -04:00
parent 4e970400ef
commit 6880f36b99
42 changed files with 2835 additions and 262 deletions

0
moderation/__init__.py Normal file
View File

78
moderation/admin.py Normal file
View File

@@ -0,0 +1,78 @@
from django.contrib import admin
from django.contrib.admin import AdminSite
from django.utils.html import format_html
from django.urls import reverse
from .models import EditSubmission, PhotoSubmission
class ModerationAdminSite(AdminSite):
site_header = 'ThrillWiki Moderation'
site_title = 'ThrillWiki Moderation'
index_title = 'Moderation Dashboard'
def has_permission(self, request):
"""Only allow moderators and above to access this admin site"""
return request.user.is_authenticated and request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']
moderation_site = ModerationAdminSite(name='moderation')
class EditSubmissionAdmin(admin.ModelAdmin):
list_display = ['id', 'user_link', 'content_type', 'content_link', 'status', 'submitted_at', 'reviewed_by']
list_filter = ['status', 'content_type', 'submitted_at']
search_fields = ['user__username', 'reason', 'source', 'review_notes']
readonly_fields = ['user', 'content_type', 'object_id', 'changes', 'submitted_at']
def user_link(self, obj):
url = reverse('admin:accounts_user_change', args=[obj.user.id])
return format_html('<a href="{}">{}</a>', url, obj.user.username)
user_link.short_description = 'User'
def content_link(self, obj):
if hasattr(obj.content_object, 'get_absolute_url'):
url = obj.content_object.get_absolute_url()
return format_html('<a href="{}">{}</a>', url, str(obj.content_object))
return str(obj.content_object)
content_link.short_description = 'Content'
def save_model(self, request, obj, form, change):
if 'status' in form.changed_data:
if obj.status == 'APPROVED':
obj.approve(request.user, obj.review_notes)
elif obj.status == 'REJECTED':
obj.reject(request.user, obj.review_notes)
super().save_model(request, obj, form, change)
class PhotoSubmissionAdmin(admin.ModelAdmin):
list_display = ['id', 'user_link', 'content_type', 'content_link', 'photo_preview', 'status', 'submitted_at', 'reviewed_by']
list_filter = ['status', 'content_type', 'submitted_at']
search_fields = ['user__username', 'caption', 'review_notes']
readonly_fields = ['user', 'content_type', 'object_id', 'photo_preview', 'submitted_at']
def user_link(self, obj):
url = reverse('admin:accounts_user_change', args=[obj.user.id])
return format_html('<a href="{}">{}</a>', url, obj.user.username)
user_link.short_description = 'User'
def content_link(self, obj):
if hasattr(obj.content_object, 'get_absolute_url'):
url = obj.content_object.get_absolute_url()
return format_html('<a href="{}">{}</a>', url, str(obj.content_object))
return str(obj.content_object)
content_link.short_description = 'Content'
def photo_preview(self, obj):
if obj.photo:
return format_html('<img src="{}" style="max-height: 100px; max-width: 200px;" />', obj.photo.url)
return ''
photo_preview.short_description = 'Photo Preview'
def save_model(self, request, obj, form, change):
if 'status' in form.changed_data:
if obj.status == 'APPROVED':
obj.approve(request.user, obj.review_notes)
elif obj.status == 'REJECTED':
obj.reject(request.user, obj.review_notes)
super().save_model(request, obj, form, change)
# Register with moderation site only
moderation_site.register(EditSubmission, EditSubmissionAdmin)
moderation_site.register(PhotoSubmission, PhotoSubmissionAdmin)

6
moderation/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ModerationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'moderation'
verbose_name = 'Content Moderation'

View File

@@ -0,0 +1,16 @@
def moderation_access(request):
"""Add moderation access check to template context"""
context = {
'has_moderation_access': False,
'has_admin_access': False,
'has_superuser_access': False,
'user_role': None
}
if request.user.is_authenticated:
context['user_role'] = request.user.role
context['has_moderation_access'] = request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']
context['has_admin_access'] = request.user.role in ['ADMIN', 'SUPERUSER']
context['has_superuser_access'] = request.user.role == 'SUPERUSER'
return context

View File

@@ -0,0 +1,183 @@
# Generated by Django 5.1.2 on 2024-10-30 00:41
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="EditSubmission",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("object_id", models.PositiveIntegerField()),
(
"changes",
models.JSONField(
help_text="JSON representation of the changes made"
),
),
("reason", models.TextField(help_text="Why this edit is needed")),
(
"source",
models.TextField(
blank=True,
help_text="Source of information for this edit (if applicable)",
),
),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("APPROVED", "Approved"),
("REJECTED", "Rejected"),
("AUTO_APPROVED", "Auto Approved"),
],
default="PENDING",
max_length=20,
),
),
("submitted_at", models.DateTimeField(auto_now_add=True)),
("reviewed_at", models.DateTimeField(blank=True, null=True)),
(
"review_notes",
models.TextField(
blank=True,
help_text="Notes from the moderator about this submission",
),
),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
(
"reviewed_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="reviewed_submissions",
to=settings.AUTH_USER_MODEL,
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="edit_submissions",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-submitted_at"],
"indexes": [
models.Index(
fields=["content_type", "object_id"],
name="moderation__content_922d2b_idx",
),
models.Index(
fields=["status"], name="moderation__status_e4eb2b_idx"
),
],
},
),
migrations.CreateModel(
name="PhotoSubmission",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("object_id", models.PositiveIntegerField()),
("photo", models.ImageField(upload_to="submissions/photos/")),
("caption", models.CharField(blank=True, max_length=255)),
("date_taken", models.DateField(blank=True, null=True)),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("APPROVED", "Approved"),
("REJECTED", "Rejected"),
("AUTO_APPROVED", "Auto Approved"),
],
default="PENDING",
max_length=20,
),
),
("submitted_at", models.DateTimeField(auto_now_add=True)),
("reviewed_at", models.DateTimeField(blank=True, null=True)),
(
"review_notes",
models.TextField(
blank=True,
help_text="Notes from the moderator about this photo submission",
),
),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
(
"reviewed_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="reviewed_photos",
to=settings.AUTH_USER_MODEL,
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="photo_submissions",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-submitted_at"],
"indexes": [
models.Index(
fields=["content_type", "object_id"],
name="moderation__content_7a7bc1_idx",
),
models.Index(
fields=["status"], name="moderation__status_7a1914_idx"
),
],
},
),
]

View File

@@ -0,0 +1,46 @@
# Generated by Django 5.1.2 on 2024-10-30 01:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("moderation", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="editsubmission",
name="submission_type",
field=models.CharField(
choices=[("EDIT", "Edit Existing"), ("CREATE", "Create New")],
default="EDIT",
max_length=10,
),
),
migrations.AlterField(
model_name="editsubmission",
name="changes",
field=models.JSONField(
help_text="JSON representation of the changes or new object data"
),
),
migrations.AlterField(
model_name="editsubmission",
name="object_id",
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name="editsubmission",
name="reason",
field=models.TextField(help_text="Why this edit/addition is needed"),
),
migrations.AlterField(
model_name="editsubmission",
name="source",
field=models.TextField(
blank=True, help_text="Source of information (if applicable)"
),
),
]

View File

205
moderation/mixins.py Normal file
View File

@@ -0,0 +1,205 @@
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.contenttypes.models import ContentType
from django.http import JsonResponse, HttpResponseForbidden
from django.core.exceptions import PermissionDenied
from django.utils import timezone
import json
from .models import EditSubmission, PhotoSubmission
class EditSubmissionMixin(LoginRequiredMixin):
"""
Mixin for handling edit submissions with proper moderation.
"""
def handle_edit_submission(self, request, changes, reason='', source='', submission_type='EDIT'):
"""
Handle an edit submission based on user's role.
Args:
request: The HTTP request
changes: Dict of field changes {field_name: new_value}
reason: Why this edit is needed
source: Source of information (optional)
submission_type: 'EDIT' or 'CREATE'
Returns:
JsonResponse with status and message
"""
if not request.user.is_authenticated:
return JsonResponse({
'status': 'error',
'message': 'You must be logged in to make edits.'
}, status=403)
content_type = ContentType.objects.get_for_model(self.model)
# Create the submission
submission = EditSubmission(
user=request.user,
content_type=content_type,
submission_type=submission_type,
changes=changes,
reason=reason,
source=source
)
# For edits, set the object_id
if submission_type == 'EDIT':
obj = self.get_object()
submission.object_id = obj.id
# Auto-approve for moderators and above
if request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']:
obj = submission.auto_approve()
return JsonResponse({
'status': 'success',
'message': 'Changes saved successfully.',
'auto_approved': True,
'redirect_url': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None
})
# Submit for approval for regular users
submission.save()
return JsonResponse({
'status': 'success',
'message': 'Your changes have been submitted for approval.',
'auto_approved': False
})
def post(self, request, *args, **kwargs):
"""Handle POST requests for editing"""
try:
data = json.loads(request.body)
changes = data.get('changes', {})
reason = data.get('reason', '')
source = data.get('source', '')
submission_type = data.get('submission_type', 'EDIT')
if not changes:
return JsonResponse({
'status': 'error',
'message': 'No changes provided.'
}, status=400)
if not reason and request.user.role == 'USER':
return JsonResponse({
'status': 'error',
'message': 'Please provide a reason for your changes.'
}, status=400)
return self.handle_edit_submission(
request, changes, reason, source, submission_type
)
except json.JSONDecodeError:
return JsonResponse({
'status': 'error',
'message': 'Invalid JSON data.'
}, status=400)
except Exception as e:
return JsonResponse({
'status': 'error',
'message': str(e)
}, status=500)
class PhotoSubmissionMixin(LoginRequiredMixin):
"""
Mixin for handling photo submissions with proper moderation.
"""
def handle_photo_submission(self, request):
"""Handle a photo submission based on user's role"""
if not request.FILES.get('photo'):
return JsonResponse({
'status': 'error',
'message': 'No photo provided.'
}, status=400)
obj = self.get_object()
content_type = ContentType.objects.get_for_model(obj)
submission = PhotoSubmission(
user=request.user,
content_type=content_type,
object_id=obj.id,
photo=request.FILES['photo'],
caption=request.POST.get('caption', ''),
date_taken=request.POST.get('date_taken')
)
# Auto-approve for moderators and above
if request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']:
submission.auto_approve()
return JsonResponse({
'status': 'success',
'message': 'Photo uploaded successfully.',
'auto_approved': True
})
# Submit for approval for regular users
submission.save()
return JsonResponse({
'status': 'success',
'message': 'Your photo has been submitted for approval.',
'auto_approved': False
})
class ModeratorRequiredMixin(UserPassesTestMixin):
"""Require moderator or higher role for access"""
def test_func(self):
return (
self.request.user.is_authenticated and
self.request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']
)
def handle_no_permission(self):
if not self.request.user.is_authenticated:
return super().handle_no_permission()
return HttpResponseForbidden("You must be a moderator to access this page.")
class AdminRequiredMixin(UserPassesTestMixin):
"""Require admin or superuser role for access"""
def test_func(self):
return (
self.request.user.is_authenticated and
self.request.user.role in ['ADMIN', 'SUPERUSER']
)
def handle_no_permission(self):
if not self.request.user.is_authenticated:
return super().handle_no_permission()
return HttpResponseForbidden("You must be an admin to access this page.")
class InlineEditMixin:
"""Add inline editing context to views"""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.user.is_authenticated:
context['can_edit'] = True
context['can_auto_approve'] = self.request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']
if hasattr(self, 'get_object'):
obj = self.get_object()
context['pending_edits'] = EditSubmission.objects.filter(
content_type=ContentType.objects.get_for_model(obj),
object_id=obj.id,
status='PENDING'
).select_related('user').order_by('-submitted_at')
return context
class HistoryMixin:
"""Add edit history context to views"""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
obj = self.get_object()
# Get historical records
context['history'] = obj.history.all().select_related('history_user')
# Get related edit submissions
content_type = ContentType.objects.get_for_model(obj)
context['edit_submissions'] = EditSubmission.objects.filter(
content_type=content_type,
object_id=obj.id
).exclude(
status='PENDING'
).select_related('user', 'reviewed_by').order_by('-submitted_at')
return context

262
moderation/models.py Normal file
View File

@@ -0,0 +1,262 @@
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
from django.utils import timezone
from django.apps import apps
class EditSubmission(models.Model):
STATUS_CHOICES = [
('PENDING', 'Pending'),
('APPROVED', 'Approved'),
('REJECTED', 'Rejected'),
('AUTO_APPROVED', 'Auto Approved'),
]
SUBMISSION_TYPE_CHOICES = [
('EDIT', 'Edit Existing'),
('CREATE', 'Create New'),
]
# Who submitted the edit
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='edit_submissions'
)
# What is being edited (Park or Ride)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField(null=True, blank=True) # Null for new objects
content_object = GenericForeignKey('content_type', 'object_id')
# Type of submission
submission_type = models.CharField(
max_length=10,
choices=SUBMISSION_TYPE_CHOICES,
default='EDIT'
)
# The actual changes/data
changes = models.JSONField(
help_text='JSON representation of the changes or new object data'
)
# Metadata
reason = models.TextField(
help_text='Why this edit/addition is needed'
)
source = models.TextField(
blank=True,
help_text='Source of information (if applicable)'
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='PENDING'
)
submitted_at = models.DateTimeField(auto_now_add=True)
# Review details
reviewed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='reviewed_submissions'
)
reviewed_at = models.DateTimeField(null=True, blank=True)
review_notes = models.TextField(
blank=True,
help_text='Notes from the moderator about this submission'
)
class Meta:
ordering = ['-submitted_at']
indexes = [
models.Index(fields=['content_type', 'object_id']),
models.Index(fields=['status']),
]
def __str__(self):
action = "creation" if self.submission_type == 'CREATE' else "edit"
target = self.content_object or self.content_type.model_class().__name__
return f"{action} by {self.user.username} on {target}"
def _resolve_foreign_keys(self, data):
"""Convert foreign key IDs to model instances"""
model_class = self.content_type.model_class()
resolved_data = data.copy()
for field_name, value in data.items():
field = model_class._meta.get_field(field_name)
if isinstance(field, models.ForeignKey) and value is not None:
related_model = field.related_model
resolved_data[field_name] = related_model.objects.get(id=value)
return resolved_data
def approve(self, moderator, notes=''):
"""Approve the submission and apply the changes"""
self.status = 'APPROVED'
self.reviewed_by = moderator
self.reviewed_at = timezone.now()
self.review_notes = notes
model_class = self.content_type.model_class()
resolved_data = self._resolve_foreign_keys(self.changes)
if self.submission_type == 'CREATE':
# Create new object
obj = model_class(**resolved_data)
obj.save()
# Update object_id after creation
self.object_id = obj.id
else:
# Apply changes to existing object
obj = self.content_object
for field, value in resolved_data.items():
setattr(obj, field, value)
obj.save()
self.save()
return obj
def reject(self, moderator, notes):
"""Reject the submission"""
self.status = 'REJECTED'
self.reviewed_by = moderator
self.reviewed_at = timezone.now()
self.review_notes = notes
self.save()
def auto_approve(self):
"""Auto-approve the submission (for moderators/admins)"""
self.status = 'AUTO_APPROVED'
self.reviewed_by = self.user
self.reviewed_at = timezone.now()
model_class = self.content_type.model_class()
resolved_data = self._resolve_foreign_keys(self.changes)
if self.submission_type == 'CREATE':
# Create new object
obj = model_class(**resolved_data)
obj.save()
# Update object_id after creation
self.object_id = obj.id
else:
# Apply changes to existing object
obj = self.content_object
for field, value in resolved_data.items():
setattr(obj, field, value)
obj.save()
self.save()
return obj
class PhotoSubmission(models.Model):
STATUS_CHOICES = [
('PENDING', 'Pending'),
('APPROVED', 'Approved'),
('REJECTED', 'Rejected'),
('AUTO_APPROVED', 'Auto Approved'),
]
# Who submitted the photo
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='photo_submissions'
)
# What the photo is for (Park or Ride)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
# The photo itself
photo = models.ImageField(upload_to='submissions/photos/')
caption = models.CharField(max_length=255, blank=True)
date_taken = models.DateField(null=True, blank=True)
# Metadata
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='PENDING'
)
submitted_at = models.DateTimeField(auto_now_add=True)
# Review details
reviewed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='reviewed_photos'
)
reviewed_at = models.DateTimeField(null=True, blank=True)
review_notes = models.TextField(
blank=True,
help_text='Notes from the moderator about this photo submission'
)
class Meta:
ordering = ['-submitted_at']
indexes = [
models.Index(fields=['content_type', 'object_id']),
models.Index(fields=['status']),
]
def __str__(self):
return f"Photo submission by {self.user.username} for {self.content_object}"
def approve(self, moderator, notes=''):
"""Approve the photo submission"""
from media.models import Photo
self.status = 'APPROVED'
self.reviewed_by = moderator
self.reviewed_at = timezone.now()
self.review_notes = notes
# Create the approved photo
Photo.objects.create(
user=self.user,
content_type=self.content_type,
object_id=self.object_id,
image=self.photo,
caption=self.caption,
date_taken=self.date_taken
)
self.save()
def reject(self, moderator, notes):
"""Reject the photo submission"""
self.status = 'REJECTED'
self.reviewed_by = moderator
self.reviewed_at = timezone.now()
self.review_notes = notes
self.save()
def auto_approve(self):
"""Auto-approve the photo submission (for moderators/admins)"""
from media.models import Photo
self.status = 'AUTO_APPROVED'
self.reviewed_by = self.user
self.reviewed_at = timezone.now()
# Create the approved photo
Photo.objects.create(
user=self.user,
content_type=self.content_type,
object_id=self.object_id,
image=self.photo,
caption=self.caption,
date_taken=self.date_taken
)
self.save()

3
moderation/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

16
moderation/urls.py Normal file
View File

@@ -0,0 +1,16 @@
from django.urls import path, include
from .admin import moderation_site
from .views import EditSubmissionListView, PhotoSubmissionListView
app_name = 'moderation'
urlpatterns = [
# Custom moderation views
path('submissions/', include([
path('edits/', EditSubmissionListView.as_view(), name='edit_submissions'),
path('photos/', PhotoSubmissionListView.as_view(), name='photo_submissions'),
])),
# Admin site URLs
path('admin/', moderation_site.urls),
]

100
moderation/views.py Normal file
View File

@@ -0,0 +1,100 @@
from django.views.generic import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone
from .models import EditSubmission, PhotoSubmission
from .mixins import ModeratorRequiredMixin
class EditSubmissionListView(ModeratorRequiredMixin, ListView):
model = EditSubmission
template_name = 'moderation/admin/edit_submission_list.html'
context_object_name = 'submissions'
paginate_by = 20
def get_queryset(self):
queryset = super().get_queryset().select_related(
'user', 'reviewed_by', 'content_type'
).order_by('-submitted_at')
# Filter by status
status = self.request.GET.get('status')
if status:
queryset = queryset.filter(status=status)
# Filter by submission type
submission_type = self.request.GET.get('type')
if submission_type:
queryset = queryset.filter(submission_type=submission_type)
return queryset
def post(self, request, *args, **kwargs):
submission_id = request.POST.get('submission_id')
action = request.POST.get('action')
review_notes = request.POST.get('review_notes', '')
submission = get_object_or_404(EditSubmission, id=submission_id)
if action == 'approve':
obj = submission.approve(request.user, review_notes)
message = 'New addition approved successfully.' if submission.submission_type == 'CREATE' else 'Changes approved successfully.'
return JsonResponse({
'status': 'success',
'message': message,
'redirect_url': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None
})
elif action == 'reject':
submission.reject(request.user, review_notes)
message = 'New addition rejected.' if submission.submission_type == 'CREATE' else 'Changes rejected.'
return JsonResponse({
'status': 'success',
'message': message
})
return JsonResponse({
'status': 'error',
'message': 'Invalid action.'
}, status=400)
class PhotoSubmissionListView(ModeratorRequiredMixin, ListView):
model = PhotoSubmission
template_name = 'moderation/admin/photo_submission_list.html'
context_object_name = 'submissions'
paginate_by = 20
def get_queryset(self):
queryset = super().get_queryset().select_related(
'user', 'reviewed_by', 'content_type'
).order_by('-submitted_at')
status = self.request.GET.get('status')
if status:
queryset = queryset.filter(status=status)
return queryset
def post(self, request, *args, **kwargs):
submission_id = request.POST.get('submission_id')
action = request.POST.get('action')
review_notes = request.POST.get('review_notes', '')
submission = get_object_or_404(PhotoSubmission, id=submission_id)
if action == 'approve':
submission.approve(request.user, review_notes)
return JsonResponse({
'status': 'success',
'message': 'Photo approved successfully.'
})
elif action == 'reject':
submission.reject(request.user, review_notes)
return JsonResponse({
'status': 'success',
'message': 'Photo rejected successfully.'
})
return JsonResponse({
'status': 'error',
'message': 'Invalid action.'
}, status=400)