feat: add Teacher Admin role to four-tier permission system
Introduces a four-tier role system: Regular User → Student Admin → Teacher Admin → Super Admin. Teacher Admin can manage own contests, problemsets, and view classroom data. Student Admin (renamed from Admin) retains problem management only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,12 @@ class super_admin_required(BasePermissionDecorator):
|
||||
return user.is_authenticated and user.is_super_admin()
|
||||
|
||||
|
||||
class teacher_admin_required(BasePermissionDecorator):
|
||||
def check_permission(self, request):
|
||||
user = request.user
|
||||
return user.is_authenticated and user.is_teacher_or_above()
|
||||
|
||||
|
||||
class admin_role_required(BasePermissionDecorator):
|
||||
def check_permission(self, request):
|
||||
user = request.user
|
||||
|
||||
18
account/migrations/0006_alter_user_admin_type.py
Normal file
18
account/migrations/0006_alter_user_admin_type.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.4 on 2026-06-03 00:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('account', '0005_alter_user_is_disabled_alter_user_open_api_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='admin_type',
|
||||
field=models.TextField(choices=[('Regular User', 'Regular User'), ('Student Admin', 'Student Admin'), ('Teacher Admin', 'Teacher Admin'), ('Super Admin', 'Super Admin')], default='Regular User'),
|
||||
),
|
||||
]
|
||||
27
account/migrations/0007_rename_admin_to_student_admin.py
Normal file
27
account/migrations/0007_rename_admin_to_student_admin.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 6.0.4 on 2026-06-03 00:08
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def rename_admin_to_student_admin(apps, schema_editor):
|
||||
User = apps.get_model("account", "User")
|
||||
User.objects.filter(admin_type="Admin").update(admin_type="Student Admin")
|
||||
|
||||
|
||||
def rename_student_admin_to_admin(apps, schema_editor):
|
||||
User = apps.get_model("account", "User")
|
||||
User.objects.filter(admin_type="Student Admin").update(admin_type="Admin")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("account", "0006_alter_user_admin_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
rename_admin_to_student_admin,
|
||||
rename_student_admin_to_admin,
|
||||
),
|
||||
]
|
||||
@@ -7,7 +7,8 @@ from utils.models import JSONField
|
||||
|
||||
class AdminType(models.TextChoices):
|
||||
REGULAR_USER = "Regular User", "Regular User"
|
||||
ADMIN = "Admin", "Admin"
|
||||
STUDENT_ADMIN = "Student Admin", "Student Admin"
|
||||
TEACHER_ADMIN = "Teacher Admin", "Teacher Admin"
|
||||
SUPER_ADMIN = "Super Admin", "Super Admin"
|
||||
|
||||
|
||||
@@ -56,14 +57,24 @@ class User(AbstractBaseUser):
|
||||
def is_regular_user(self):
|
||||
return self.admin_type == AdminType.REGULAR_USER
|
||||
|
||||
def is_admin(self):
|
||||
return self.admin_type == AdminType.ADMIN
|
||||
def is_student_admin(self):
|
||||
return self.admin_type == AdminType.STUDENT_ADMIN
|
||||
|
||||
def is_teacher_admin(self):
|
||||
return self.admin_type == AdminType.TEACHER_ADMIN
|
||||
|
||||
def is_super_admin(self):
|
||||
return self.admin_type == AdminType.SUPER_ADMIN
|
||||
|
||||
def is_admin_role(self):
|
||||
return self.admin_type in [AdminType.ADMIN, AdminType.SUPER_ADMIN]
|
||||
return self.admin_type in [
|
||||
AdminType.STUDENT_ADMIN,
|
||||
AdminType.TEACHER_ADMIN,
|
||||
AdminType.SUPER_ADMIN,
|
||||
]
|
||||
|
||||
def is_teacher_or_above(self):
|
||||
return self.admin_type in [AdminType.TEACHER_ADMIN, AdminType.SUPER_ADMIN]
|
||||
|
||||
def can_mgmt_all_problem(self):
|
||||
return self.problem_permission == ProblemPermission.ALL
|
||||
|
||||
@@ -105,7 +105,9 @@ class UserAdminAPI(APIView):
|
||||
user.admin_type = data["admin_type"]
|
||||
user.is_disabled = data["is_disabled"]
|
||||
|
||||
if data["admin_type"] == AdminType.ADMIN:
|
||||
if data["admin_type"] == AdminType.STUDENT_ADMIN:
|
||||
user.problem_permission = data["problem_permission"] or ProblemPermission.OWN
|
||||
elif data["admin_type"] == AdminType.TEACHER_ADMIN:
|
||||
user.problem_permission = data["problem_permission"] or ProblemPermission.OWN
|
||||
elif data["admin_type"] == AdminType.SUPER_ADMIN:
|
||||
user.problem_permission = ProblemPermission.ALL
|
||||
|
||||
@@ -432,7 +432,7 @@ class UserRankAPI(AsyncAPIView):
|
||||
n = 0
|
||||
|
||||
profiles = UserProfile.objects.filter(
|
||||
user__admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN],
|
||||
user__admin_type__in=[AdminType.REGULAR_USER, AdminType.STUDENT_ADMIN],
|
||||
user__is_disabled=False,
|
||||
user__username__icontains=username,
|
||||
).select_related("user").filter(accepted_number__gte=0).order_by("-accepted_number", "submission_number")
|
||||
|
||||
@@ -22,7 +22,7 @@ class ClassRankAPI(APIView):
|
||||
User.objects.filter(
|
||||
class_name__isnull=False,
|
||||
is_disabled=False,
|
||||
admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN],
|
||||
admin_type__in=[AdminType.REGULAR_USER, AdminType.STUDENT_ADMIN],
|
||||
class_name__startswith=str(grade),
|
||||
)
|
||||
.values("class_name")
|
||||
@@ -35,7 +35,7 @@ class ClassRankAPI(APIView):
|
||||
users = User.objects.filter(
|
||||
class_name=class_name,
|
||||
is_disabled=False,
|
||||
admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN],
|
||||
admin_type__in=[AdminType.REGULAR_USER, AdminType.STUDENT_ADMIN],
|
||||
)
|
||||
user_ids = list(users.values_list("id", flat=True))
|
||||
|
||||
@@ -99,7 +99,7 @@ class UserClassRankAPI(APIView):
|
||||
class_users = User.objects.filter(
|
||||
class_name=user.class_name,
|
||||
is_disabled=False,
|
||||
admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN],
|
||||
admin_type__in=[AdminType.REGULAR_USER, AdminType.STUDENT_ADMIN],
|
||||
).select_related("userprofile")
|
||||
|
||||
user_ranks = []
|
||||
@@ -187,7 +187,7 @@ class ClassPKAPI(APIView):
|
||||
User.objects.filter(
|
||||
class_name__in=class_names,
|
||||
is_disabled=False,
|
||||
admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN],
|
||||
admin_type__in=[AdminType.REGULAR_USER, AdminType.STUDENT_ADMIN],
|
||||
).values_list("id", flat=True)
|
||||
)
|
||||
all_ac_list = sorted(
|
||||
@@ -206,7 +206,7 @@ class ClassPKAPI(APIView):
|
||||
users = User.objects.filter(
|
||||
class_name=class_name,
|
||||
is_disabled=False,
|
||||
admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN],
|
||||
admin_type__in=[AdminType.REGULAR_USER, AdminType.STUDENT_ADMIN],
|
||||
)
|
||||
user_ids = list(users.values_list("id", flat=True))
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import dateutil.parser
|
||||
from django.http import FileResponse
|
||||
from django.utils.timezone import now
|
||||
|
||||
from account.decorators import super_admin_required
|
||||
from account.decorators import ensure_created_by, super_admin_required, teacher_admin_required
|
||||
from account.models import User
|
||||
from problem.models import Problem
|
||||
from submission.models import JudgeStatus, Submission
|
||||
@@ -31,7 +31,7 @@ from ..serializers import (
|
||||
|
||||
class ContestAPI(APIView):
|
||||
@validate_serializer(CreateConetestSeriaizer)
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
data["start_time"] = dateutil.parser.parse(data["start_time"])
|
||||
@@ -50,13 +50,14 @@ class ContestAPI(APIView):
|
||||
return self.success(ContestAdminSerializer(contest).data)
|
||||
|
||||
@validate_serializer(EditConetestSeriaizer)
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def put(self, request):
|
||||
data = request.data
|
||||
try:
|
||||
contest = Contest.objects.get(id=data.pop("id"))
|
||||
except Contest.DoesNotExist:
|
||||
return self.error("Contest does not exist")
|
||||
ensure_created_by(contest, request.user)
|
||||
data["start_time"] = dateutil.parser.parse(data["start_time"])
|
||||
data["end_time"] = dateutil.parser.parse(data["end_time"])
|
||||
if data["end_time"] <= data["start_time"]:
|
||||
@@ -73,17 +74,20 @@ class ContestAPI(APIView):
|
||||
contest.save()
|
||||
return self.success(ContestAdminSerializer(contest).data)
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def get(self, request):
|
||||
contest_id = request.GET.get("id")
|
||||
if contest_id:
|
||||
try:
|
||||
contest = Contest.objects.get(id=contest_id)
|
||||
ensure_created_by(contest, request.user)
|
||||
return self.success(ContestAdminSerializer(contest).data)
|
||||
except Contest.DoesNotExist:
|
||||
return self.error("Contest does not exist")
|
||||
|
||||
contests = Contest.objects.all().order_by("-create_time")
|
||||
if not request.user.is_super_admin():
|
||||
contests = contests.filter(created_by=request.user)
|
||||
|
||||
keyword = request.GET.get("keyword")
|
||||
if keyword:
|
||||
@@ -159,7 +163,7 @@ class ContestAnnouncementAPI(APIView):
|
||||
|
||||
|
||||
class ACMContestHelper(APIView):
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def get(self, request):
|
||||
contest_id = request.GET.get("contest_id")
|
||||
if not contest_id:
|
||||
@@ -168,6 +172,7 @@ class ACMContestHelper(APIView):
|
||||
contest = Contest.objects.get(id=contest_id, visible=True)
|
||||
except Contest.DoesNotExist:
|
||||
return self.error("Contest does not exist")
|
||||
ensure_created_by(contest, request.user)
|
||||
|
||||
problems = Problem.objects.filter(contest=contest).values("id", "_id")
|
||||
problem_id_map = {str(p["id"]): p["_id"] for p in problems}
|
||||
@@ -191,7 +196,7 @@ class ACMContestHelper(APIView):
|
||||
results.sort(key=lambda x: -x["ac_info"]["ac_time"])
|
||||
return self.success(results)
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
@validate_serializer(ACMContesHelperSerializer)
|
||||
def put(self, request):
|
||||
data = request.data
|
||||
@@ -258,7 +263,7 @@ class DownloadContestSubmissions(APIView):
|
||||
|
||||
class ContestCloneAPI(APIView):
|
||||
@validate_serializer(ContestCloneSerializer)
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def post(self, request):
|
||||
try:
|
||||
original = Contest.objects.get(id=request.data["contest_id"])
|
||||
|
||||
@@ -124,7 +124,7 @@ class ContestRankAPI(APIView):
|
||||
return (
|
||||
ACMContestRank.objects.filter(
|
||||
contest=self.contest,
|
||||
user__admin_type=AdminType.REGULAR_USER,
|
||||
user__admin_type__in=[AdminType.REGULAR_USER, AdminType.STUDENT_ADMIN],
|
||||
user__is_disabled=False,
|
||||
)
|
||||
.select_related("user")
|
||||
|
||||
@@ -337,7 +337,7 @@ class ContestProblemAPI(ProblemBase):
|
||||
except Contest.DoesNotExist:
|
||||
return self.error("Contest does not exist")
|
||||
problems = Problem.objects.filter(contest=contest).order_by("-create_time")
|
||||
if user.is_admin():
|
||||
if not user.is_super_admin():
|
||||
problems = problems.filter(contest__created_by=user)
|
||||
keyword = request.GET.get("keyword")
|
||||
if keyword:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.db.models import Count, Q
|
||||
|
||||
from account.decorators import ensure_created_by, super_admin_required
|
||||
from account.decorators import ensure_created_by, teacher_admin_required
|
||||
from problem.models import Problem
|
||||
from problemset.models import (
|
||||
ProblemSet,
|
||||
@@ -29,10 +29,12 @@ from utils.api import APIView, validate_serializer
|
||||
class ProblemSetAdminAPI(APIView):
|
||||
"""题单管理API"""
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def get(self, request):
|
||||
"""获取题单列表(管理员)"""
|
||||
problem_sets = ProblemSet.objects.filter(visible=True).annotate(problems_count=Count("problemsetproblem", distinct=True)).order_by("-create_time")
|
||||
if not request.user.is_super_admin():
|
||||
problem_sets = problem_sets.filter(created_by=request.user)
|
||||
|
||||
# 过滤条件
|
||||
keyword = request.GET.get("keyword", "").strip()
|
||||
@@ -51,7 +53,7 @@ class ProblemSetAdminAPI(APIView):
|
||||
data = self.paginate_data(request, problem_sets, ProblemSetListSerializer)
|
||||
return self.success(data)
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
@validate_serializer(CreateProblemSetSerializer)
|
||||
def post(self, request):
|
||||
"""创建题单"""
|
||||
@@ -60,7 +62,7 @@ class ProblemSetAdminAPI(APIView):
|
||||
problem_set = ProblemSet.objects.create(**data)
|
||||
return self.success(ProblemSetSerializer(problem_set).data)
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
@validate_serializer(EditProblemSetSerializer)
|
||||
def put(self, request):
|
||||
"""编辑题单"""
|
||||
@@ -79,7 +81,7 @@ class ProblemSetAdminAPI(APIView):
|
||||
|
||||
return self.success(ProblemSetSerializer(problem_set).data)
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def delete(self, request):
|
||||
"""删除题单"""
|
||||
problem_set_id = request.GET.get("id")
|
||||
@@ -99,7 +101,7 @@ class ProblemSetAdminAPI(APIView):
|
||||
class ProblemSetDetailAdminAPI(APIView):
|
||||
"""题单详情管理API"""
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def get(self, request, problem_set_id):
|
||||
"""获取题单详情(管理员)"""
|
||||
try:
|
||||
@@ -115,7 +117,7 @@ class ProblemSetDetailAdminAPI(APIView):
|
||||
class ProblemSetProblemAdminAPI(APIView):
|
||||
"""题单题目管理API(管理员)"""
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def get(self, request, problem_set_id):
|
||||
"""获取题单中的题目列表(管理员)"""
|
||||
try:
|
||||
@@ -128,7 +130,7 @@ class ProblemSetProblemAdminAPI(APIView):
|
||||
serializer = ProblemSetProblemSerializer(problems, many=True, context={"request": request})
|
||||
return self.success(serializer.data)
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
@validate_serializer(AddProblemToSetSerializer)
|
||||
def post(self, request, problem_set_id):
|
||||
"""添加题目到题单(管理员)"""
|
||||
@@ -163,7 +165,7 @@ class ProblemSetProblemAdminAPI(APIView):
|
||||
|
||||
return self.success("题目已添加到题单")
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
@validate_serializer(EditProblemInSetSerializer)
|
||||
def put(self, request, problem_set_id, problem_set_problem_id):
|
||||
"""编辑题单中的题目(管理员)"""
|
||||
@@ -193,7 +195,7 @@ class ProblemSetProblemAdminAPI(APIView):
|
||||
|
||||
return self.success("题目已更新")
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def delete(self, request, problem_set_id, problem_set_problem_id):
|
||||
"""从题单中移除题目(管理员)"""
|
||||
try:
|
||||
@@ -213,7 +215,7 @@ class ProblemSetProblemAdminAPI(APIView):
|
||||
class ProblemSetBadgeAdminAPI(APIView):
|
||||
"""题单奖章管理API(管理员)"""
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def get(self, request, problem_set_id):
|
||||
"""获取题单的奖章列表(管理员)"""
|
||||
try:
|
||||
@@ -226,7 +228,7 @@ class ProblemSetBadgeAdminAPI(APIView):
|
||||
serializer = ProblemSetBadgeSerializer(badges, many=True)
|
||||
return self.success(serializer.data)
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
@validate_serializer(CreateProblemSetBadgeSerializer)
|
||||
def post(self, request, problem_set_id):
|
||||
"""创建题单奖章(管理员)"""
|
||||
@@ -242,7 +244,7 @@ class ProblemSetBadgeAdminAPI(APIView):
|
||||
|
||||
return self.success(ProblemSetBadgeSerializer(badge).data)
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
@validate_serializer(EditProblemSetBadgeSerializer)
|
||||
def put(self, request, problem_set_id, badge_id):
|
||||
"""编辑题单奖章(管理员)"""
|
||||
@@ -282,7 +284,7 @@ class ProblemSetBadgeAdminAPI(APIView):
|
||||
return self.success("奖章已更新,并重新计算了所有用户的徽章资格")
|
||||
return self.success("奖章已更新")
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def delete(self, request, problem_set_id, badge_id):
|
||||
"""删除题单奖章(管理员)"""
|
||||
try:
|
||||
@@ -302,7 +304,7 @@ class ProblemSetBadgeAdminAPI(APIView):
|
||||
class ProblemSetProgressAdminAPI(APIView):
|
||||
"""题单进度管理API(管理员)"""
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def get(self, request, problem_set_id):
|
||||
"""获取题单的所有用户进度(管理员)"""
|
||||
try:
|
||||
@@ -315,7 +317,7 @@ class ProblemSetProgressAdminAPI(APIView):
|
||||
serializer = ProblemSetProgressSerializer(progress_list, many=True)
|
||||
return self.success(serializer.data)
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def delete(self, request, problem_set_id, user_id):
|
||||
"""移除用户从题单(管理员)"""
|
||||
try:
|
||||
@@ -336,7 +338,7 @@ class ProblemSetProgressAdminAPI(APIView):
|
||||
class ProblemSetSyncAPI(APIView):
|
||||
"""题单同步管理API"""
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
def post(self, request, problem_set_id):
|
||||
"""手动同步题单的所有用户进度(管理员)"""
|
||||
try:
|
||||
@@ -354,7 +356,7 @@ class ProblemSetSyncAPI(APIView):
|
||||
class ProblemSetVisibleAPI(APIView):
|
||||
"""题单可见性管理API"""
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
@validate_serializer(ProblemSetVisibleSerializer)
|
||||
def put(self, request):
|
||||
"""切换题单可见性"""
|
||||
@@ -373,7 +375,7 @@ class ProblemSetVisibleAPI(APIView):
|
||||
class ProblemSetStatusAPI(APIView):
|
||||
"""题单状态管理API"""
|
||||
|
||||
@super_admin_required
|
||||
@teacher_admin_required
|
||||
@validate_serializer(ProblemSetUpdateStatusSerializer)
|
||||
def put(self, request):
|
||||
"""更新题单状态"""
|
||||
|
||||
@@ -15,7 +15,7 @@ from account.models import User, UserProfile, AdminType, ProblemPermission
|
||||
from problem.models import Problem, ProblemTag, ProblemRuleType
|
||||
from utils.constants import Difficulty
|
||||
|
||||
admin_type_map = {0: AdminType.REGULAR_USER, 1: AdminType.ADMIN, 2: AdminType.SUPER_ADMIN}
|
||||
admin_type_map = {0: AdminType.REGULAR_USER, 1: AdminType.STUDENT_ADMIN, 2: AdminType.SUPER_ADMIN}
|
||||
languages_map = {1: "C", 2: "C++", 3: "Java"}
|
||||
email_regex = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)")
|
||||
|
||||
@@ -95,7 +95,7 @@ def import_users():
|
||||
user.email = data["email"]
|
||||
admin_type = admin_type_map[data["admin_type"]]
|
||||
user.admin_type = admin_type
|
||||
if admin_type == AdminType.ADMIN:
|
||||
if admin_type == AdminType.STUDENT_ADMIN:
|
||||
user.problem_permission = ProblemPermission.OWN
|
||||
elif admin_type == AdminType.SUPER_ADMIN:
|
||||
user.problem_permission = ProblemPermission.ALL
|
||||
|
||||
Reference in New Issue
Block a user