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:
2026-06-02 18:13:33 -06:00
parent 125d57b123
commit f94d29cf93
12 changed files with 112 additions and 41 deletions

View File

@@ -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

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

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

View File

@@ -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

View File

@@ -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

View File

@@ -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")