This commit is contained in:
Iliyan Angelov
2025-11-26 22:32:20 +02:00
commit ed94dd22dd
150 changed files with 14058 additions and 0 deletions

0
moderation/__init__.py Normal file
View File

32
moderation/admin.py Normal file
View 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
View File

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

View 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')],
},
),
]

View 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)'),
),
]

View File

145
moderation/models.py Normal file
View 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
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 @@
"""
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
View 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')