update
This commit is contained in:
0
moderation/__init__.py
Normal file
0
moderation/__init__.py
Normal file
32
moderation/admin.py
Normal file
32
moderation/admin.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Admin configuration for moderation app.
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from .models import ModerationQueue, ModerationAction, ModerationRule
|
||||
|
||||
|
||||
@admin.register(ModerationQueue)
|
||||
class ModerationQueueAdmin(admin.ModelAdmin):
|
||||
"""Moderation queue admin."""
|
||||
list_display = ('report', 'priority', 'assigned_to', 'created_at')
|
||||
list_filter = ('priority', 'created_at')
|
||||
search_fields = ('report__title',)
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
|
||||
@admin.register(ModerationAction)
|
||||
class ModerationActionAdmin(admin.ModelAdmin):
|
||||
"""Moderation action admin."""
|
||||
list_display = ('report', 'moderator', 'action_type', 'created_at')
|
||||
list_filter = ('action_type', 'created_at')
|
||||
search_fields = ('report__title', 'moderator__username', 'reason')
|
||||
readonly_fields = ('created_at',)
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
|
||||
@admin.register(ModerationRule)
|
||||
class ModerationRuleAdmin(admin.ModelAdmin):
|
||||
"""Moderation rule admin."""
|
||||
list_display = ('name', 'is_active', 'priority', 'updated_at')
|
||||
list_filter = ('is_active',)
|
||||
search_fields = ('name', 'description')
|
||||
6
moderation/apps.py
Normal file
6
moderation/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ModerationConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'moderation'
|
||||
77
moderation/migrations/0001_initial.py
Normal file
77
moderation/migrations/0001_initial.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-26 13:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('reports', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ModerationRule',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('priority', models.IntegerField(default=0, help_text='Rule priority (higher = evaluated first)')),
|
||||
('conditions', models.JSONField(default=dict, help_text='Conditions that trigger this rule')),
|
||||
('actions', models.JSONField(default=dict, help_text='Actions to take when rule matches')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Moderation Rule',
|
||||
'verbose_name_plural': 'Moderation Rules',
|
||||
'db_table': 'moderation_moderationrule',
|
||||
'ordering': ['-priority', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ModerationAction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action_type', models.CharField(choices=[('approve', 'Approve'), ('reject', 'Reject'), ('edit', 'Edit'), ('delete', 'Delete'), ('verify', 'Verify'), ('archive', 'Archive'), ('unarchive', 'Unarchive'), ('assign', 'Assign'), ('unassign', 'Unassign')], max_length=20)),
|
||||
('reason', models.CharField(blank=True, help_text='Reason for the action', max_length=200)),
|
||||
('notes', models.TextField(blank=True, help_text='Additional notes')),
|
||||
('previous_status', models.CharField(blank=True, max_length=20)),
|
||||
('new_status', models.CharField(blank=True, max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('moderator', models.ForeignKey(limit_choices_to={'role__in': ['moderator', 'admin']}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='moderation_actions', to=settings.AUTH_USER_MODEL)),
|
||||
('report', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moderation_actions', to='reports.scamreport')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Moderation Action',
|
||||
'verbose_name_plural': 'Moderation Actions',
|
||||
'db_table': 'moderation_moderationaction',
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['report', 'created_at'], name='moderation__report__971308_idx'), models.Index(fields=['moderator', 'created_at'], name='moderation__moderat_b59e8d_idx'), models.Index(fields=['action_type', 'created_at'], name='moderation__action__8d1226_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ModerationQueue',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('priority', models.CharField(choices=[('low', 'Low'), ('normal', 'Normal'), ('high', 'High'), ('urgent', 'Urgent')], default='normal', max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('assigned_to', models.ForeignKey(blank=True, limit_choices_to={'role__in': ['moderator', 'admin']}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_moderations', to=settings.AUTH_USER_MODEL)),
|
||||
('report', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='moderation_queue', to='reports.scamreport')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Moderation Queue',
|
||||
'verbose_name_plural': 'Moderation Queues',
|
||||
'db_table': 'moderation_moderationqueue',
|
||||
'ordering': ['-priority', 'created_at'],
|
||||
'indexes': [models.Index(fields=['priority', 'created_at'], name='moderation__priorit_02ba25_idx'), models.Index(fields=['assigned_to', 'created_at'], name='moderation__assigne_674975_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
18
moderation/migrations/0002_alter_moderationaction_reason.py
Normal file
18
moderation/migrations/0002_alter_moderationaction_reason.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-26 14:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('moderation', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='moderationaction',
|
||||
name='reason',
|
||||
field=models.TextField(blank=True, help_text='Reason for the action (visible to user for rejections)'),
|
||||
),
|
||||
]
|
||||
0
moderation/migrations/__init__.py
Normal file
0
moderation/migrations/__init__.py
Normal file
145
moderation/models.py
Normal file
145
moderation/models.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Moderation system models.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from reports.models import ScamReport
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ModerationQueue(models.Model):
|
||||
"""
|
||||
Queue for reports awaiting moderation.
|
||||
"""
|
||||
PRIORITY_CHOICES = [
|
||||
('low', 'Low'),
|
||||
('normal', 'Normal'),
|
||||
('high', 'High'),
|
||||
('urgent', 'Urgent'),
|
||||
]
|
||||
|
||||
report = models.OneToOneField(
|
||||
ScamReport,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='moderation_queue'
|
||||
)
|
||||
priority = models.CharField(
|
||||
max_length=20,
|
||||
choices=PRIORITY_CHOICES,
|
||||
default='normal'
|
||||
)
|
||||
assigned_to = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='assigned_moderations',
|
||||
limit_choices_to={'role__in': ['moderator', 'admin']}
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'moderation_moderationqueue'
|
||||
verbose_name = 'Moderation Queue'
|
||||
verbose_name_plural = 'Moderation Queues'
|
||||
ordering = ['-priority', 'created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['priority', 'created_at']),
|
||||
models.Index(fields=['assigned_to', 'created_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"Queue entry for Report #{self.report.id} - {self.get_priority_display()}"
|
||||
|
||||
|
||||
class ModerationAction(models.Model):
|
||||
"""
|
||||
Log of moderation actions taken.
|
||||
"""
|
||||
ACTION_TYPE_CHOICES = [
|
||||
('approve', 'Approve'),
|
||||
('reject', 'Reject'),
|
||||
('edit', 'Edit'),
|
||||
('delete', 'Delete'),
|
||||
('verify', 'Verify'),
|
||||
('archive', 'Archive'),
|
||||
('unarchive', 'Unarchive'),
|
||||
('assign', 'Assign'),
|
||||
('unassign', 'Unassign'),
|
||||
]
|
||||
|
||||
report = models.ForeignKey(
|
||||
ScamReport,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='moderation_actions'
|
||||
)
|
||||
moderator = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='moderation_actions',
|
||||
limit_choices_to={'role__in': ['moderator', 'admin']}
|
||||
)
|
||||
action_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=ACTION_TYPE_CHOICES
|
||||
)
|
||||
reason = models.TextField(
|
||||
blank=True,
|
||||
help_text='Reason for the action (visible to user for rejections)'
|
||||
)
|
||||
notes = models.TextField(
|
||||
blank=True,
|
||||
help_text='Additional notes'
|
||||
)
|
||||
previous_status = models.CharField(max_length=20, blank=True)
|
||||
new_status = models.CharField(max_length=20, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'moderation_moderationaction'
|
||||
verbose_name = 'Moderation Action'
|
||||
verbose_name_plural = 'Moderation Actions'
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['report', 'created_at']),
|
||||
models.Index(fields=['moderator', 'created_at']),
|
||||
models.Index(fields=['action_type', 'created_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_action_type_display()} on Report #{self.report.id} by {self.moderator}"
|
||||
|
||||
|
||||
class ModerationRule(models.Model):
|
||||
"""
|
||||
Automated moderation rules.
|
||||
"""
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.TextField(blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
priority = models.IntegerField(
|
||||
default=0,
|
||||
help_text='Rule priority (higher = evaluated first)'
|
||||
)
|
||||
conditions = models.JSONField(
|
||||
default=dict,
|
||||
help_text='Conditions that trigger this rule'
|
||||
)
|
||||
actions = models.JSONField(
|
||||
default=dict,
|
||||
help_text='Actions to take when rule matches'
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'moderation_moderationrule'
|
||||
verbose_name = 'Moderation Rule'
|
||||
verbose_name_plural = 'Moderation Rules'
|
||||
ordering = ['-priority', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({'Active' if self.is_active else 'Inactive'})"
|
||||
3
moderation/tests.py
Normal file
3
moderation/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
16
moderation/urls.py
Normal file
16
moderation/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
URL configuration for moderation app.
|
||||
"""
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'moderation'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.ModerationDashboardView.as_view(), name='dashboard'),
|
||||
path('queue/', views.ModerationQueueView.as_view(), name='queue'),
|
||||
path('report/<int:pk>/', views.ReportModerationView.as_view(), name='report_detail'),
|
||||
path('report/<int:pk>/approve/', views.ApproveReportView.as_view(), name='approve'),
|
||||
path('report/<int:pk>/reject/', views.RejectReportView.as_view(), name='reject'),
|
||||
]
|
||||
|
||||
129
moderation/views.py
Normal file
129
moderation/views.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Views for moderation app.
|
||||
"""
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views.generic import ListView, DetailView, UpdateView
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from reports.models import ScamReport
|
||||
from .models import ModerationQueue, ModerationAction
|
||||
|
||||
|
||||
class ModeratorRequiredMixin(UserPassesTestMixin):
|
||||
"""Mixin to require moderator role."""
|
||||
def test_func(self):
|
||||
return self.request.user.is_authenticated and self.request.user.is_moderator()
|
||||
|
||||
|
||||
class ModerationDashboardView(LoginRequiredMixin, ModeratorRequiredMixin, ListView):
|
||||
"""Moderation dashboard."""
|
||||
template_name = 'moderation/dashboard.html'
|
||||
context_object_name = 'reports'
|
||||
|
||||
def get_queryset(self):
|
||||
return ScamReport.objects.filter(
|
||||
status__in=['pending', 'under_review']
|
||||
).order_by('-created_at')[:10]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['pending_count'] = ScamReport.objects.filter(status='pending').count()
|
||||
context['under_review_count'] = ScamReport.objects.filter(status='under_review').count()
|
||||
context['verified_count'] = ScamReport.objects.filter(status='verified').count()
|
||||
return context
|
||||
|
||||
|
||||
class ModerationQueueView(LoginRequiredMixin, ModeratorRequiredMixin, ListView):
|
||||
"""Moderation queue."""
|
||||
model = ModerationQueue
|
||||
template_name = 'moderation/queue.html'
|
||||
context_object_name = 'queue_items'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
return ModerationQueue.objects.select_related(
|
||||
'report', 'assigned_to'
|
||||
).order_by('-priority', 'created_at')
|
||||
|
||||
|
||||
class ReportModerationView(LoginRequiredMixin, ModeratorRequiredMixin, DetailView):
|
||||
"""View report for moderation."""
|
||||
model = ScamReport
|
||||
template_name = 'moderation/report_detail.html'
|
||||
context_object_name = 'report'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['osint_results'] = self.object.osint_results.all()
|
||||
context['verifications'] = self.object.verifications.all()
|
||||
context['moderation_actions'] = self.object.moderation_actions.all()[:10]
|
||||
return context
|
||||
|
||||
|
||||
class ApproveReportView(LoginRequiredMixin, ModeratorRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
"""Approve a report."""
|
||||
model = ScamReport
|
||||
fields = []
|
||||
template_name = 'moderation/approve.html'
|
||||
success_message = "Report approved successfully!"
|
||||
|
||||
def form_valid(self, form):
|
||||
previous_status = form.instance.status
|
||||
form.instance.status = 'verified'
|
||||
form.instance.verified_at = timezone.now()
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Create moderation action
|
||||
ModerationAction.objects.create(
|
||||
report=form.instance,
|
||||
moderator=self.request.user,
|
||||
action_type='approve',
|
||||
previous_status=previous_status,
|
||||
new_status='verified'
|
||||
)
|
||||
|
||||
# Remove from queue
|
||||
ModerationQueue.objects.filter(report=form.instance).delete()
|
||||
|
||||
return response
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('moderation:queue')
|
||||
|
||||
|
||||
class RejectReportView(LoginRequiredMixin, ModeratorRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
"""Reject a report."""
|
||||
model = ScamReport
|
||||
fields = []
|
||||
template_name = 'moderation/reject.html'
|
||||
success_message = "Report rejected."
|
||||
|
||||
def form_valid(self, form):
|
||||
previous_status = form.instance.status
|
||||
form.instance.status = 'rejected'
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Get reason from form
|
||||
reason = self.request.POST.get('reason', '').strip()
|
||||
notes = self.request.POST.get('notes', '').strip()
|
||||
|
||||
# Create moderation action
|
||||
ModerationAction.objects.create(
|
||||
report=form.instance,
|
||||
moderator=self.request.user,
|
||||
action_type='reject',
|
||||
previous_status=previous_status,
|
||||
new_status='rejected',
|
||||
reason=reason,
|
||||
notes=notes
|
||||
)
|
||||
|
||||
# Remove from queue
|
||||
ModerationQueue.objects.filter(report=form.instance).delete()
|
||||
|
||||
return response
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('moderation:queue')
|
||||
Reference in New Issue
Block a user