feat: 城市手册后端完成 - 用户/区域/内容/服务/审核系统
This commit is contained in:
0
city-manual/backend/regions/__init__.py
Normal file
0
city-manual/backend/regions/__init__.py
Normal file
BIN
city-manual/backend/regions/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
city-manual/backend/regions/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
city-manual/backend/regions/__pycache__/admin.cpython-312.pyc
Normal file
BIN
city-manual/backend/regions/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
city-manual/backend/regions/__pycache__/apps.cpython-312.pyc
Normal file
BIN
city-manual/backend/regions/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
city-manual/backend/regions/__pycache__/models.cpython-312.pyc
Normal file
BIN
city-manual/backend/regions/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
city-manual/backend/regions/__pycache__/views.cpython-312.pyc
Normal file
BIN
city-manual/backend/regions/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
41
city-manual/backend/regions/admin.py
Normal file
41
city-manual/backend/regions/admin.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from django.contrib import admin
|
||||
from .models import Region, ModeratorApplication, ModeratorPermission, ModeratorSupport, PermissionRestriction
|
||||
|
||||
|
||||
@admin.register(Region)
|
||||
class RegionAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'level', 'parent', 'is_active', 'created_at']
|
||||
list_filter = ['level', 'is_active']
|
||||
search_fields = ['name']
|
||||
ordering = ['level', 'name']
|
||||
|
||||
|
||||
@admin.register(ModeratorApplication)
|
||||
class ModeratorApplicationAdmin(admin.ModelAdmin):
|
||||
list_display = ['applicant', 'region', 'status', 'support_count', 'required_support', 'deadline', 'created_at']
|
||||
list_filter = ['status', 'region']
|
||||
search_fields = ['applicant__username', 'region__name']
|
||||
ordering = ['-created_at']
|
||||
|
||||
|
||||
@admin.register(ModeratorPermission)
|
||||
class ModeratorPermissionAdmin(admin.ModelAdmin):
|
||||
list_display = ['moderator', 'region', 'rank', 'status', 'created_at']
|
||||
list_filter = ['rank', 'status']
|
||||
search_fields = ['moderator__username', 'region__name']
|
||||
ordering = ['-created_at']
|
||||
|
||||
|
||||
@admin.register(ModeratorSupport)
|
||||
class ModeratorSupportAdmin(admin.ModelAdmin):
|
||||
list_display = ['supporter', 'application', 'created_at']
|
||||
search_fields = ['supporter__username', 'application__region__name']
|
||||
ordering = ['-created_at']
|
||||
|
||||
|
||||
@admin.register(PermissionRestriction)
|
||||
class PermissionRestrictionAdmin(admin.ModelAdmin):
|
||||
list_display = ['restricted_moderator', 'restriction_type', 'operator', 'started_at', 'ended_at']
|
||||
list_filter = ['restriction_type']
|
||||
search_fields = ['restricted_moderator__username', 'operator__username']
|
||||
ordering = ['-started_at']
|
||||
6
city-manual/backend/regions/apps.py
Normal file
6
city-manual/backend/regions/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RegionsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'regions'
|
||||
93
city-manual/backend/regions/migrations/0001_initial.py
Normal file
93
city-manual/backend/regions/migrations/0001_initial.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# Generated by Django 4.2.11 on 2026-04-10 12:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ModeratorApplication',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('application_reason', models.TextField(blank=True, verbose_name='申请理由')),
|
||||
('support_count', models.PositiveIntegerField(default=0, verbose_name='支持人数')),
|
||||
('required_support', models.PositiveIntegerField(default=10, verbose_name='所需支持人数')),
|
||||
('deadline', models.DateTimeField(verbose_name='截止时间')),
|
||||
('status', models.CharField(choices=[('pending', '待审核'), ('approved', '已通过'), ('rejected', '已拒绝'), ('cancelled', '已取消')], default='pending', max_length=20, verbose_name='状态')),
|
||||
('reviewed_at', models.DateTimeField(blank=True, null=True, verbose_name='审核时间')),
|
||||
('review_comment', models.TextField(blank=True, verbose_name='审核意见')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='申请时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '版主申请',
|
||||
'verbose_name_plural': '版主申请',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ModeratorPermission',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('rank', models.CharField(choices=[('general', '将军'), ('colonel', '校官'), ('lieutenant', '尉官'), ('soldier', '士兵')], max_length=20, verbose_name='军衔')),
|
||||
('status', models.CharField(choices=[('active', '正常'), ('restricted', '限制'), ('revoked', '取消')], default='active', max_length=20, verbose_name='状态')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='授权时间')),
|
||||
('restricted_until', models.DateTimeField(blank=True, null=True, verbose_name='限制截止时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '版主权限',
|
||||
'verbose_name_plural': '版主权限',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ModeratorSupport',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='支持时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '支持记录',
|
||||
'verbose_name_plural': '支持记录',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PermissionRestriction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('restriction_type', models.CharField(choices=[('partial', '部分限制'), ('full', '完全限制')], max_length=20, verbose_name='限制类型')),
|
||||
('started_at', models.DateTimeField(auto_now_add=True, verbose_name='限制开始时间')),
|
||||
('ended_at', models.DateTimeField(blank=True, null=True, verbose_name='限制结束时间')),
|
||||
('reason', models.TextField(blank=True, verbose_name='限制原因')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '权限限制',
|
||||
'verbose_name_plural': '权限限制',
|
||||
'ordering': ['-started_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Region',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='名称')),
|
||||
('level', models.CharField(choices=[('province', '省级'), ('city', '市级'), ('county', '县级'), ('town', '镇级'), ('village', '村级')], max_length=20, verbose_name='级别')),
|
||||
('code', models.CharField(blank=True, max_length=20, verbose_name='行政区划代码')),
|
||||
('description', models.TextField(blank=True, verbose_name='描述')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='regions.region', verbose_name='上级区域')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '区域',
|
||||
'verbose_name_plural': '区域',
|
||||
'ordering': ['level', 'name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
71
city-manual/backend/regions/migrations/0002_initial.py
Normal file
71
city-manual/backend/regions/migrations/0002_initial.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# Generated by Django 4.2.11 on 2026-04-10 12:05
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('regions', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='permissionrestriction',
|
||||
name='operator',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='permission_restrictions_made', to=settings.AUTH_USER_MODEL, verbose_name='操作者'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='permissionrestriction',
|
||||
name='restricted_moderator',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='permission_restrictions', to=settings.AUTH_USER_MODEL, verbose_name='被限制版主'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moderatorsupport',
|
||||
name='application',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supports', to='regions.moderatorapplication', verbose_name='申请'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moderatorsupport',
|
||||
name='supporter',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moderator_supports', to=settings.AUTH_USER_MODEL, verbose_name='支持者'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moderatorpermission',
|
||||
name='moderator',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moderator_permissions', to=settings.AUTH_USER_MODEL, verbose_name='版主'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moderatorpermission',
|
||||
name='region',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moderator_permissions', to='regions.region', verbose_name='管辖区域'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moderatorapplication',
|
||||
name='applicant',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moderator_applications', to=settings.AUTH_USER_MODEL, verbose_name='申请者'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moderatorapplication',
|
||||
name='region',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moderator_applications', to='regions.region', verbose_name='申请区域'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moderatorapplication',
|
||||
name='reviewer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_applications', to=settings.AUTH_USER_MODEL, verbose_name='审核人'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='moderatorsupport',
|
||||
unique_together={('supporter', 'application')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='moderatorpermission',
|
||||
unique_together={('moderator', 'region')},
|
||||
),
|
||||
]
|
||||
0
city-manual/backend/regions/migrations/__init__.py
Normal file
0
city-manual/backend/regions/migrations/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
214
city-manual/backend/regions/models.py
Normal file
214
city-manual/backend/regions/models.py
Normal file
@@ -0,0 +1,214 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class Region(models.Model):
|
||||
"""版块/区域表 - 省市区乡镇村层级结构"""
|
||||
LEVEL_CHOICES = [
|
||||
('province', '省级'),
|
||||
('city', '市级'),
|
||||
('county', '县级'),
|
||||
('town', '镇级'),
|
||||
('village', '村级'),
|
||||
]
|
||||
|
||||
name = models.CharField('名称', max_length=100)
|
||||
level = models.CharField('级别', max_length=20, choices=LEVEL_CHOICES)
|
||||
parent = models.ForeignKey(
|
||||
'self',
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='children',
|
||||
verbose_name='上级区域'
|
||||
)
|
||||
code = models.CharField('行政区划代码', max_length=20, blank=True)
|
||||
description = models.TextField('描述', blank=True)
|
||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||
updated_at = models.DateTimeField('更新时间', auto_now=True)
|
||||
is_active = models.BooleanField('是否启用', default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '区域'
|
||||
verbose_name_plural = '区域'
|
||||
ordering = ['level', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.get_level_display()})"
|
||||
|
||||
def get_full_path(self):
|
||||
"""获取完整路径"""
|
||||
path = [self.name]
|
||||
current = self.parent
|
||||
while current:
|
||||
path.append(current.name)
|
||||
current = current.parent
|
||||
return ' > '.join(reversed(path))
|
||||
|
||||
|
||||
class ModeratorApplication(models.Model):
|
||||
"""版主申请表"""
|
||||
STATUS_CHOICES = [
|
||||
('pending', '待审核'),
|
||||
('approved', '已通过'),
|
||||
('rejected', '已拒绝'),
|
||||
('cancelled', '已取消'),
|
||||
]
|
||||
|
||||
applicant = models.ForeignKey(
|
||||
'users.User',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='moderator_applications',
|
||||
verbose_name='申请者'
|
||||
)
|
||||
region = models.ForeignKey(
|
||||
Region,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='moderator_applications',
|
||||
verbose_name='申请区域'
|
||||
)
|
||||
application_reason = models.TextField('申请理由', blank=True)
|
||||
support_count = models.PositiveIntegerField('支持人数', default=0)
|
||||
required_support = models.PositiveIntegerField('所需支持人数', default=10)
|
||||
deadline = models.DateTimeField('截止时间')
|
||||
status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='pending')
|
||||
reviewer = models.ForeignKey(
|
||||
'users.User',
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='reviewed_applications',
|
||||
verbose_name='审核人'
|
||||
)
|
||||
reviewed_at = models.DateTimeField('审核时间', null=True, blank=True)
|
||||
review_comment = models.TextField('审核意见', blank=True)
|
||||
created_at = models.DateTimeField('申请时间', auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '版主申请'
|
||||
verbose_name_plural = '版主申请'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.applicant.username} 申请 {self.region.name} 版主"
|
||||
|
||||
def is_expired(self):
|
||||
return timezone.now() > self.deadline
|
||||
|
||||
def auto_cancel_if_failed(self):
|
||||
"""如果截止且支持人数不足,自动取消"""
|
||||
if self.is_expired() and self.status == 'pending' and self.support_count < self.required_support:
|
||||
self.status = 'cancelled'
|
||||
self.save()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ModeratorSupport(models.Model):
|
||||
"""版主申请支持表"""
|
||||
supporter = models.ForeignKey(
|
||||
'users.User',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='moderator_supports',
|
||||
verbose_name='支持者'
|
||||
)
|
||||
application = models.ForeignKey(
|
||||
ModeratorApplication,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='supports',
|
||||
verbose_name='申请'
|
||||
)
|
||||
created_at = models.DateTimeField('支持时间', auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '支持记录'
|
||||
verbose_name_plural = '支持记录'
|
||||
unique_together = ['supporter', 'application']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.supporter.username} 支持 {self.application}"
|
||||
|
||||
|
||||
class ModeratorPermission(models.Model):
|
||||
"""版主权限表"""
|
||||
RANK_CHOICES = [
|
||||
('general', '将军'), # 省级
|
||||
('colonel', '校官'), # 市级
|
||||
('lieutenant', '尉官'), # 县级
|
||||
('soldier', '士兵'), # 镇村级
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('active', '正常'),
|
||||
('restricted', '限制'),
|
||||
('revoked', '取消'),
|
||||
]
|
||||
|
||||
moderator = models.ForeignKey(
|
||||
'users.User',
|
||||
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)
|
||||
status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='active')
|
||||
created_at = models.DateTimeField('授权时间', auto_now_add=True)
|
||||
restricted_until = models.DateTimeField('限制截止时间', null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '版主权限'
|
||||
verbose_name_plural = '版主权限'
|
||||
unique_together = ['moderator', 'region']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.moderator.username} - {self.region.name} ({self.get_rank_display()})"
|
||||
|
||||
def can_moderate(self, content_region):
|
||||
"""判断是否可以审核某个区域的内容"""
|
||||
if self.status != 'active':
|
||||
return False
|
||||
|
||||
# 检查是否在管辖范围内
|
||||
current = content_region
|
||||
while current:
|
||||
if current.id == self.region.id:
|
||||
return True
|
||||
current = current.parent
|
||||
return False
|
||||
|
||||
|
||||
class PermissionRestriction(models.Model):
|
||||
"""权限限制表"""
|
||||
operator = models.ForeignKey(
|
||||
'users.User',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='permission_restrictions_made',
|
||||
verbose_name='操作者'
|
||||
)
|
||||
restricted_moderator = models.ForeignKey(
|
||||
'users.User',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='permission_restrictions',
|
||||
verbose_name='被限制版主'
|
||||
)
|
||||
restriction_type = models.CharField('限制类型', max_length=20, choices=[
|
||||
('partial', '部分限制'),
|
||||
('full', '完全限制'),
|
||||
])
|
||||
started_at = models.DateTimeField('限制开始时间', auto_now_add=True)
|
||||
ended_at = models.DateTimeField('限制结束时间', null=True, blank=True)
|
||||
reason = models.TextField('限制原因', blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '权限限制'
|
||||
verbose_name_plural = '权限限制'
|
||||
ordering = ['-started_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.restricted_moderator.username} 被限制 ({self.restriction_type})"
|
||||
63
city-manual/backend/regions/serializers.py
Normal file
63
city-manual/backend/regions/serializers.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Region, ModeratorApplication, ModeratorPermission
|
||||
from users.serializers import UserSerializer
|
||||
|
||||
|
||||
class RegionSerializer(serializers.ModelSerializer):
|
||||
parent = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
children_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = [
|
||||
'id', 'name', 'level', 'parent', 'code', 'description',
|
||||
'is_active', 'created_at', 'children_count'
|
||||
]
|
||||
read_only_fields = ['created_at']
|
||||
|
||||
def get_children_count(self, obj):
|
||||
return obj.children.count()
|
||||
|
||||
|
||||
class RegionDetailSerializer(serializers.ModelSerializer):
|
||||
parent = RegionSerializer(read_only=True)
|
||||
children = RegionSerializer(many=True, read_only=True)
|
||||
articles_count = serializers.SerializerMethodField()
|
||||
services_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = [
|
||||
'id', 'name', 'level', 'parent', 'children', 'code', 'description',
|
||||
'is_active', 'created_at', 'articles_count', 'services_count'
|
||||
]
|
||||
|
||||
def get_articles_count(self, obj):
|
||||
return obj.articles.filter(publish_status='published').count()
|
||||
|
||||
def get_services_count(self, obj):
|
||||
return obj.featured_services.filter(publish_status='published').count()
|
||||
|
||||
|
||||
class ModeratorApplicationSerializer(serializers.ModelSerializer):
|
||||
applicant = UserSerializer(read_only=True)
|
||||
region = RegionSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ModeratorApplication
|
||||
fields = [
|
||||
'id', 'applicant', 'region', 'application_reason',
|
||||
'support_count', 'required_support', 'deadline',
|
||||
'status', 'created_at'
|
||||
]
|
||||
read_only_fields = ['applicant', 'support_count', 'status', 'created_at']
|
||||
|
||||
|
||||
class ModeratorPermissionSerializer(serializers.ModelSerializer):
|
||||
moderator = UserSerializer(read_only=True)
|
||||
region = RegionSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ModeratorPermission
|
||||
fields = ['id', 'moderator', 'region', 'rank', 'status', 'created_at']
|
||||
read_only_fields = ['created_at']
|
||||
3
city-manual/backend/regions/tests.py
Normal file
3
city-manual/backend/regions/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
107
city-manual/backend/regions/views.py
Normal file
107
city-manual/backend/regions/views.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from rest_framework import viewsets, permissions, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from django.utils import timezone
|
||||
from .models import Region, ModeratorApplication, ModeratorPermission, ModeratorSupport
|
||||
from .serializers import RegionSerializer, RegionDetailSerializer, ModeratorApplicationSerializer, ModeratorPermissionSerializer
|
||||
|
||||
|
||||
class RegionViewSet(viewsets.ModelViewSet):
|
||||
queryset = Region.objects.filter(is_active=True)
|
||||
serializer_class = RegionSerializer
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'retrieve':
|
||||
return RegionDetailSerializer
|
||||
return RegionSerializer
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def provinces(self, request):
|
||||
provinces = Region.objects.filter(level='province', is_active=True)
|
||||
serializer = self.get_serializer(provinces, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def children(self, request, pk=None):
|
||||
region = self.get_object()
|
||||
children = region.children.filter(is_active=True)
|
||||
serializer = self.get_serializer(children, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class ModeratorApplicationViewSet(viewsets.ModelViewSet):
|
||||
queryset = ModeratorApplication.objects.all()
|
||||
serializer_class = ModeratorApplicationSerializer
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
application = serializer.save(applicant=self.request.user)
|
||||
# 设置默认截止时间为 7 天后
|
||||
application.deadline = timezone.now() + timezone.timedelta(days=7)
|
||||
application.save()
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def support(self, request, pk=None):
|
||||
application = self.get_object()
|
||||
user = request.user
|
||||
|
||||
# 检查是否已经支持过
|
||||
if ModeratorSupport.objects.filter(supporter=user, application=application).exists():
|
||||
return Response({'detail': '已经支持过该申请'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 创建支持记录
|
||||
ModeratorSupport.objects.create(supporter=user, application=application)
|
||||
application.support_count += 1
|
||||
application.save()
|
||||
|
||||
return Response({'support_count': application.support_count})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def approve(self, request, pk=None):
|
||||
application = self.get_object()
|
||||
user = request.user
|
||||
|
||||
# 检查权限
|
||||
if not user.is_staff:
|
||||
return Response({'detail': '没有权限'}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
application.status = 'approved'
|
||||
application.reviewer = user
|
||||
application.reviewed_at = timezone.now()
|
||||
application.save()
|
||||
|
||||
# 创建版主权限
|
||||
ModeratorPermission.objects.create(
|
||||
moderator=application.applicant,
|
||||
region=application.region,
|
||||
rank=self._get_rank_by_level(application.region.level)
|
||||
)
|
||||
|
||||
return Response({'detail': '申请已批准'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def reject(self, request, pk=None):
|
||||
application = self.get_object()
|
||||
user = request.user
|
||||
|
||||
if not user.is_staff:
|
||||
return Response({'detail': '没有权限'}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
application.status = 'rejected'
|
||||
application.reviewer = user
|
||||
application.reviewed_at = timezone.now()
|
||||
application.review_comment = request.data.get('comment', '')
|
||||
application.save()
|
||||
|
||||
return Response({'detail': '申请已拒绝'})
|
||||
|
||||
def _get_rank_by_level(self, level):
|
||||
rank_map = {
|
||||
'province': 'general',
|
||||
'city': 'colonel',
|
||||
'county': 'lieutenant',
|
||||
'town': 'soldier',
|
||||
'village': 'soldier',
|
||||
}
|
||||
return rank_map.get(level, 'soldier')
|
||||
Reference in New Issue
Block a user