Initial commit: React + Django 城市手册项目
- Django 4.2 + DRF + JWT + GraphQL - React 18 + MobX + styled-components - PostgreSQL 数据库 - Docker + Docker Compose + Nginx - 完整的功能模块(用户、版块、文章、服务、交互、版主管理) - 完整的文档(需求、部署、测试)
This commit is contained in:
1
backend/apps/moderation/__init__.py
Normal file
1
backend/apps/moderation/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Moderation app
|
||||
7
backend/apps/moderation/apps.py
Normal file
7
backend/apps/moderation/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ModerationConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.moderation'
|
||||
verbose_name = '版主管理'
|
||||
190
backend/apps/moderation/models.py
Normal file
190
backend/apps/moderation/models.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from apps.regions.models import Region
|
||||
|
||||
|
||||
class ModeratorApplication(models.Model):
|
||||
"""Model for moderator applications."""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('pending', '待审核'),
|
||||
('approved', '已通过'),
|
||||
('rejected', '已拒绝'),
|
||||
('cancelled', '已取消'),
|
||||
]
|
||||
|
||||
RANK_CHOICES = [
|
||||
('general', '将军'),
|
||||
('colonel', '校官'),
|
||||
('captain', '尉官'),
|
||||
('soldier', '士兵'),
|
||||
]
|
||||
|
||||
applicant = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='moderator_applications',
|
||||
verbose_name='申请者'
|
||||
)
|
||||
region = models.ForeignKey(
|
||||
Region,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='moderator_applications',
|
||||
verbose_name='申请的版块'
|
||||
)
|
||||
support_count = models.IntegerField(default=0, verbose_name='支持人数')
|
||||
deadline = models.DateTimeField(verbose_name='截止时间')
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name='状态')
|
||||
rank = models.CharField(max_length=20, choices=RANK_CHOICES, verbose_name='军衔级别')
|
||||
reviewed_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='reviewed_applications',
|
||||
verbose_name='审核人'
|
||||
)
|
||||
reviewed_at = models.DateTimeField(null=True, blank=True, verbose_name='审核时间')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='申请时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'moderator_applications'
|
||||
verbose_name = '版主申请'
|
||||
verbose_name_plural = '版主申请'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.applicant.username} - {self.region.name} ({self.get_status_display()})'
|
||||
|
||||
def is_expired(self):
|
||||
"""Check if the application has expired."""
|
||||
from django.utils import timezone
|
||||
return timezone.now() > self.deadline
|
||||
|
||||
def has_enough_support(self):
|
||||
"""Check if the application has enough support."""
|
||||
# TODO: Define minimum support count
|
||||
return self.support_count >= 10
|
||||
|
||||
|
||||
class ModeratorPermission(models.Model):
|
||||
"""Model for moderator permissions."""
|
||||
|
||||
PERMISSION_STATUS_CHOICES = [
|
||||
('active', '正常'),
|
||||
('restricted', '限制'),
|
||||
('revoked', '取消'),
|
||||
]
|
||||
|
||||
RANK_CHOICES = [
|
||||
('general', '将军'),
|
||||
('colonel', '校官'),
|
||||
('captain', '尉官'),
|
||||
('soldier', '士兵'),
|
||||
]
|
||||
|
||||
moderator = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='moderator_permissions',
|
||||
verbose_name='版主'
|
||||
)
|
||||
region = models.ForeignKey(
|
||||
Region,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='moderator_permissions',
|
||||
verbose_name='管辖版块'
|
||||
)
|
||||
rank = models.CharField(max_length=20, choices=RANK_CHOICES, verbose_name='军衔级别')
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=PERMISSION_STATUS_CHOICES,
|
||||
default='active',
|
||||
verbose_name='权限状态'
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
restricted_until = models.DateTimeField(null=True, blank=True, verbose_name='限制结束时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'moderator_permissions'
|
||||
verbose_name = '版主权限'
|
||||
verbose_name_plural = '版主权限'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.moderator.username} - {self.region.name} ({self.get_status_display()})'
|
||||
|
||||
def is_active(self):
|
||||
"""Check if the permission is currently active."""
|
||||
from django.utils import timezone
|
||||
if self.status != 'active':
|
||||
return False
|
||||
if self.restricted_until and timezone.now() < self.restricted_until:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class ModeratorSupport(models.Model):
|
||||
"""Model for moderator application supports."""
|
||||
|
||||
supporter = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='supported_applications',
|
||||
verbose_name='支持者'
|
||||
)
|
||||
application = models.ForeignKey(
|
||||
ModeratorApplication,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='supports',
|
||||
verbose_name='版主申请'
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='支持时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'moderator_supports'
|
||||
verbose_name = '版主支持'
|
||||
verbose_name_plural = '版主支持'
|
||||
unique_together = ['supporter', 'application']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.supporter.username} supports {self.application.region.name}'
|
||||
|
||||
|
||||
class PermissionRestriction(models.Model):
|
||||
"""Model for permission restrictions."""
|
||||
|
||||
RESTRICTION_TYPE_CHOICES = [
|
||||
('partial', '部分限制'),
|
||||
('full', '完全限制'),
|
||||
]
|
||||
|
||||
operator = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='created_restrictions',
|
||||
verbose_name='操作者'
|
||||
)
|
||||
target_moderator = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='received_restrictions',
|
||||
verbose_name='被限制版主'
|
||||
)
|
||||
restriction_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=RESTRICTION_TYPE_CHOICES,
|
||||
verbose_name='限制类型'
|
||||
)
|
||||
start_time = models.DateTimeField(verbose_name='限制开始时间')
|
||||
end_time = models.DateTimeField(verbose_name='限制结束时间')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'permission_restrictions'
|
||||
verbose_name = '权限限制'
|
||||
verbose_name_plural = '权限限制'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.operator.username} restricted {self.target_moderator.username} ({self.get_restriction_type_display()})'
|
||||
92
backend/apps/moderation/serializers.py
Normal file
92
backend/apps/moderation/serializers.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from rest_framework import serializers
|
||||
from .models import (
|
||||
ModeratorApplication,
|
||||
ModeratorPermission,
|
||||
ModeratorSupport,
|
||||
PermissionRestriction
|
||||
)
|
||||
|
||||
|
||||
class ModeratorApplicationSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for ModeratorApplication model."""
|
||||
|
||||
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
||||
rank_display = serializers.CharField(source='get_rank_display', read_only=True)
|
||||
applicant_username = serializers.CharField(source='applicant.username', read_only=True)
|
||||
region_name = serializers.CharField(source='region.name', read_only=True)
|
||||
region_path = serializers.SerializerMethodField()
|
||||
is_expired = serializers.BooleanField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ModeratorApplication
|
||||
fields = ('id', 'applicant', 'applicant_username', 'region', 'region_name', 'region_path',
|
||||
'support_count', 'deadline', 'status', 'status_display', 'rank',
|
||||
'is_expired', 'reviewed_by', 'reviewed_at', 'created_at')
|
||||
read_only_fields = ('id', 'created_at', 'reviewed_by', 'reviewed_at')
|
||||
|
||||
def get_region_path(self, obj):
|
||||
return obj.region.get_full_path()
|
||||
|
||||
|
||||
class ModeratorApplicationCreateSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for creating moderator applications."""
|
||||
|
||||
class Meta:
|
||||
model = ModeratorApplication
|
||||
fields = ('region', 'rank', 'deadline')
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['applicant'] = self.context['request'].user
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class ModeratorPermissionSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for ModeratorPermission model."""
|
||||
|
||||
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
||||
rank_display = serializers.CharField(source='get_rank_display', read_only=True)
|
||||
moderator_username = serializers.CharField(source='moderator.username', read_only=True)
|
||||
region_name = serializers.CharField(source='region.name', read_only=True)
|
||||
is_active = serializers.BooleanField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ModeratorPermission
|
||||
fields = ('id', 'moderator', 'moderator_username', 'region', 'region_name',
|
||||
'rank', 'rank_display', 'status', 'status_display', 'is_active',
|
||||
'restricted_until', 'created_at')
|
||||
read_only_fields = ('id', 'created_at')
|
||||
|
||||
|
||||
class ModeratorSupportSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for ModeratorSupport model."""
|
||||
|
||||
supporter_username = serializers.CharField(source='supporter.username', read_only=True)
|
||||
application_region_name = serializers.CharField(source='application.region.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ModeratorSupport
|
||||
fields = ('id', 'supporter', 'supporter_username', 'application',
|
||||
'application_region_name', 'created_at')
|
||||
read_only_fields = ('id', 'created_at')
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['supporter'] = self.context['request'].user
|
||||
# Increment support count
|
||||
application = validated_data['application']
|
||||
application.support_count += 1
|
||||
application.save()
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class PermissionRestrictionSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for PermissionRestriction model."""
|
||||
|
||||
restriction_type_display = serializers.CharField(source='get_restriction_type_display', read_only=True)
|
||||
operator_username = serializers.CharField(source='operator.username', read_only=True)
|
||||
target_moderator_username = serializers.CharField(source='target_moderator.username', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PermissionRestriction
|
||||
fields = ('id', 'operator', 'operator_username', 'target_moderator', 'target_moderator_username',
|
||||
'restriction_type', 'restriction_type_display', 'start_time', 'end_time', 'created_at')
|
||||
read_only_fields = ('id', 'created_at')
|
||||
16
backend/apps/moderation/urls.py
Normal file
16
backend/apps/moderation/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import (
|
||||
ModeratorApplicationViewSet,
|
||||
ModeratorPermissionViewSet,
|
||||
PermissionRestrictionViewSet
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'applications', ModeratorApplicationViewSet, basename='moderator_application')
|
||||
router.register(r'permissions', ModeratorPermissionViewSet, basename='moderator_permission')
|
||||
router.register(r'restrictions', PermissionRestrictionViewSet, basename='permission_restriction')
|
||||
|
||||
urlpatterns = [
|
||||
path('moderator/', include(router.urls)),
|
||||
]
|
||||
197
backend/apps/moderation/views.py
Normal file
197
backend/apps/moderation/views.py
Normal file
@@ -0,0 +1,197 @@
|
||||
from rest_framework import viewsets, permissions, status, filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django.db.models import Q
|
||||
from .models import (
|
||||
ModeratorApplication,
|
||||
ModeratorPermission,
|
||||
ModeratorSupport,
|
||||
PermissionRestriction
|
||||
)
|
||||
from .serializers import (
|
||||
ModeratorApplicationSerializer,
|
||||
ModeratorApplicationCreateSerializer,
|
||||
ModeratorPermissionSerializer,
|
||||
ModeratorSupportSerializer,
|
||||
PermissionRestrictionSerializer
|
||||
)
|
||||
|
||||
|
||||
class ModeratorApplicationViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for ModeratorApplication model."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [filters.SearchFilter, filters.OrderingFilter, filters.DjangoFilterBackend]
|
||||
search_fields = ['applicant__username', 'region__name']
|
||||
filterset_fields = ['status', 'rank', 'region']
|
||||
ordering_fields = ['created_at', 'deadline']
|
||||
ordering = ['-created_at']
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = ModeratorApplication.objects.select_related('applicant', 'region', 'reviewed_by')
|
||||
|
||||
# Admins see all
|
||||
if self.request.user.is_admin():
|
||||
return queryset
|
||||
|
||||
# Regular users see their own applications
|
||||
return queryset.filter(applicant=self.request.user)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'create':
|
||||
return ModeratorApplicationCreateSerializer
|
||||
return ModeratorApplicationSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def support(self, request, pk=None):
|
||||
"""Support a moderator application."""
|
||||
application = self.get_object()
|
||||
|
||||
# Check if application is still pending
|
||||
if application.status != 'pending':
|
||||
return Response(
|
||||
{'detail': 'Can only support pending applications'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Check if already supported
|
||||
if ModeratorSupport.objects.filter(
|
||||
supporter=request.user,
|
||||
application=application
|
||||
).exists():
|
||||
return Response(
|
||||
{'detail': 'Already supported this application'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
serializer = ModeratorSupportSerializer(data={'application': application.id})
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response({'message': 'Application supported', 'support_count': application.support_count})
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def approve(self, request, pk=None):
|
||||
"""Approve moderator application (admin only)."""
|
||||
if not request.user.is_admin():
|
||||
return Response(
|
||||
{'detail': 'Only admins can approve applications'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
application = self.get_object()
|
||||
|
||||
if application.status != 'pending':
|
||||
return Response(
|
||||
{'detail': 'Can only approve pending applications'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Check if has enough support
|
||||
if not application.has_enough_support():
|
||||
return Response(
|
||||
{'detail': 'Not enough support votes'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Create moderator permission
|
||||
from .models import ModeratorPermission
|
||||
ModeratorPermission.objects.create(
|
||||
moderator=application.applicant,
|
||||
region=application.region,
|
||||
rank=application.rank,
|
||||
status='active'
|
||||
)
|
||||
|
||||
# Update application status
|
||||
application.status = 'approved'
|
||||
application.reviewed_by = request.user
|
||||
application.reviewed_at = timezone.now()
|
||||
application.save()
|
||||
|
||||
return Response({'message': 'Application approved, moderator permissions granted'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def reject(self, request, pk=None):
|
||||
"""Reject moderator application (admin only)."""
|
||||
if not request.user.is_admin():
|
||||
return Response(
|
||||
{'detail': 'Only admins can reject applications'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
application = self.get_object()
|
||||
|
||||
if application.status != 'pending':
|
||||
return Response(
|
||||
{'detail': 'Can only reject pending applications'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
application.status = 'rejected'
|
||||
application.reviewed_by = request.user
|
||||
application.reviewed_at = timezone.now()
|
||||
application.save()
|
||||
|
||||
return Response({'message': 'Application rejected'})
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def my_applications(self, request):
|
||||
"""Get current user's applications."""
|
||||
applications = ModeratorApplication.objects.filter(
|
||||
applicant=request.user
|
||||
).select_related('region')
|
||||
serializer = self.get_serializer(applications, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class ModeratorPermissionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for ModeratorPermission model (read-only)."""
|
||||
|
||||
queryset = ModeratorPermission.objects.select_related('moderator', 'region')
|
||||
serializer_class = ModeratorPermissionSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
# Admins see all
|
||||
if self.request.user.is_admin():
|
||||
return self.queryset
|
||||
|
||||
# Moderators see their own permissions
|
||||
return self.queryset.filter(moderator=self.request.user)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def my_permissions(self, request):
|
||||
"""Get current user's moderator permissions."""
|
||||
permissions = ModeratorPermission.objects.filter(
|
||||
moderator=request.user,
|
||||
status='active'
|
||||
).select_related('region')
|
||||
serializer = self.get_serializer(permissions, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class PermissionRestrictionViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for PermissionRestriction model."""
|
||||
|
||||
queryset = PermissionRestriction.objects.select_related('operator', 'target_moderator')
|
||||
serializer_class = PermissionRestrictionSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
# Admins see all
|
||||
if self.request.user.is_admin():
|
||||
return self.queryset
|
||||
|
||||
# Moderators see restrictions on themselves
|
||||
return self.queryset.filter(target_moderator=self.request.user)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# Only admins can create restrictions
|
||||
if not self.request.user.is_admin():
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
raise PermissionDenied("Only admins can create restrictions")
|
||||
serializer.save(operator=self.request.user)
|
||||
Reference in New Issue
Block a user