阶段 0 盘点时判为 CUT 的 17 条,当时只在代码里标了 DEPRECATED(2026-05-26),
没删。这次删掉:15 个视图类整体移除,2 条只删 URL。
## 为什么有两条只能删 URL
ProblemSetProblemAPI 和 ProblemSetProgressAPI **各有一条在用的路径**:
ProblemSetProblemAPI /problemset/<id>/problems 在用
/problemset/<id>/problems/<pid> 废弃
ProblemSetProgressAPI /problemset/progress 在用
/problemset/<id>/progress 废弃
删类会直接搞挂"看题单题目"和"加入题单"。清单里记的是**路径**不是视图类,
这个区别不看一眼就动手会出事。
## 核实过程
不敢只信清单 —— 今天已经因为"读过文档还是漏了盲点"栽过一次(前端 /api2 前缀
漏改 5 处)。所以:
1. 用 ast(不是正则)建「视图类 → 它的所有 URL」映射,判定哪些类的全部 URL
都废弃。正则版把 B 组算成 0 个,而我明知至少有 2 个 —— 那种"看起来干净"的
结果最危险。
2. DEPRECATED 检测最初往前多看一行,把上一条的行内注释算到了下一条头上,
于是 /hitokoto、/profile、/reset_password 被误判成废弃。这三个明显在用,
才发现范围错了。
3. 按端点字符串精确搜整个 ojnext(覆盖 http.get、原生 fetch、独立 axios、
模板插值),16/16 确认无调用。**并且用 5 个明确在用的端点反测了检测器本身** ——
未经验证的检测器报"全部无调用"是没有意义的。
## 验证
起本地 Django + PostgreSQL 实跑 33 条断言,全过:
- 保留的端点还能用(13 条,含那两个"类保留"的路径,加入题单确认真的成功)
- 删掉的端点确实 404(17 条)
- reverse() 也解析不到了(3 条)
`ruff check .` 全绿(顺手清掉 44 个因此变成未使用的 import)。
## 一个差点造成的意外
脚本无条件重写所有 urls.py,把 class_pk/ 和 message/ 下三个 **CRLF** 文件
静默转成了 LF —— 内容没变但整文件显示为改动。已原样恢复,最终 diff 是
纯删除,零意外改动。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
189 lines
6.9 KiB
Python
189 lines
6.9 KiB
Python
import re
|
|
|
|
from django.contrib.auth.hashers import make_password
|
|
from django.db import IntegrityError, transaction
|
|
from django.db.models import F, Q
|
|
from django.utils.crypto import get_random_string
|
|
|
|
from submission.models import Submission
|
|
from utils.api import APIError, APIView, validate_serializer
|
|
from utils.shortcuts import CLASS_NAME_MAX_DIGITS, CLASS_NAME_MIN_DIGITS, is_valid_class_name, rand_str
|
|
|
|
from ..decorators import super_admin_required
|
|
from ..models import AdminType, ProblemPermission, User, UserProfile
|
|
from ..serializers import (
|
|
EditUserSerializer,
|
|
ImportUserSerializer,
|
|
UserAdminSerializer,
|
|
)
|
|
|
|
|
|
# ks251XXX 或者 ks2510XX 返回 251 或者 2510。
|
|
# 不以 ks+数字 开头的(管理员、教师账号)返回 None。
|
|
# 位数不对就直接报错,不猜——猜错会把 class_name 存歪,
|
|
# 而剥前缀显示姓名、班级下拉、统计页都依赖它准确。
|
|
# 这里先用 \d+ 抓全再判位数,不能直接用 CLASS_NAME_RE 匹配:
|
|
# 那样 ks251001 会"匹配成功"并悄悄取前 4 位,正是要避免的猜测。
|
|
def get_class_name(username):
|
|
result = re.match(r"ks(\d+)", username)
|
|
if not result:
|
|
return None
|
|
class_name = result.group(1)
|
|
if not is_valid_class_name(class_name):
|
|
raise APIError(f"用户名 {username} 的班级号 {class_name} 是 {len(class_name)} 位,必须是 {CLASS_NAME_MIN_DIGITS}~{CLASS_NAME_MAX_DIGITS} 位数字")
|
|
return class_name
|
|
|
|
|
|
class UserAdminAPI(APIView):
|
|
@validate_serializer(ImportUserSerializer)
|
|
@super_admin_required
|
|
def post(self, request):
|
|
"""
|
|
Import User
|
|
"""
|
|
data = request.data["users"]
|
|
|
|
user_list = []
|
|
for user_data in data:
|
|
if len(user_data) != 4 or len(user_data[0]) > 32:
|
|
return self.error(f"Error occurred while processing data '{user_data}'")
|
|
user_list.append(
|
|
User(
|
|
username=user_data[0],
|
|
password=make_password(user_data[1]),
|
|
email=user_data[2],
|
|
raw_password=user_data[1],
|
|
class_name=get_class_name(user_data[0]),
|
|
)
|
|
)
|
|
|
|
try:
|
|
with transaction.atomic():
|
|
ret = User.objects.bulk_create(user_list)
|
|
UserProfile.objects.bulk_create([UserProfile(user=ret[i], real_name=data[i][3]) for i in range(len(ret))])
|
|
return self.success()
|
|
except IntegrityError as e:
|
|
# Extract detail from exception message
|
|
# duplicate key value violates unique constraint "user_username_key"
|
|
# DETAIL: Key (username)=(root11) already exists.
|
|
return self.error(str(e).split("\n")[1])
|
|
|
|
@validate_serializer(EditUserSerializer)
|
|
@super_admin_required
|
|
def put(self, request):
|
|
"""
|
|
Edit user api
|
|
"""
|
|
data = request.data
|
|
try:
|
|
user = User.objects.get(id=data["id"])
|
|
except User.DoesNotExist:
|
|
return self.error("User does not exist")
|
|
if User.objects.filter(username=data["username"].lower()).exclude(id=user.id).exists():
|
|
return self.error("Username already exists")
|
|
if User.objects.filter(email=data["email"].lower()).exclude(id=user.id).exists():
|
|
return self.error("Email already exists")
|
|
|
|
pre_username = user.username
|
|
user.username = data["username"].lower()
|
|
user.class_name = get_class_name(data["username"])
|
|
user.email = data["email"].lower()
|
|
user.admin_type = data["admin_type"]
|
|
user.is_disabled = data["is_disabled"]
|
|
|
|
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
|
|
else:
|
|
user.problem_permission = ProblemPermission.NONE
|
|
|
|
if data["password"]:
|
|
user.set_password(data["password"])
|
|
|
|
if data["open_api"]:
|
|
# Avoid reset user appkey after saving changes
|
|
if not user.open_api:
|
|
user.open_api_appkey = rand_str()
|
|
else:
|
|
user.open_api_appkey = None
|
|
user.open_api = data["open_api"]
|
|
|
|
user.save()
|
|
if pre_username != user.username:
|
|
Submission.objects.filter(username=pre_username).update(username=user.username)
|
|
|
|
UserProfile.objects.filter(user=user).update(real_name=data["real_name"])
|
|
return self.success(UserAdminSerializer(user).data)
|
|
|
|
@super_admin_required
|
|
def get(self, request):
|
|
"""
|
|
User list api / Get user by id
|
|
"""
|
|
user_id = request.GET.get("id")
|
|
if user_id:
|
|
try:
|
|
user = User.objects.get(id=user_id)
|
|
except User.DoesNotExist:
|
|
return self.error("User does not exist")
|
|
return self.success(UserAdminSerializer(user).data)
|
|
|
|
# 获取排序参数
|
|
order_by = request.GET.get("order_by", "")
|
|
|
|
# 根据排序参数设置排序规则
|
|
if order_by == "-last_login":
|
|
# 最近登录,将 None 值放在最后
|
|
user = User.objects.all().order_by(F("last_login").desc(nulls_last=True))
|
|
else:
|
|
# 默认按创建时间倒序
|
|
user = User.objects.all().order_by("-create_time")
|
|
|
|
type = request.GET.get("type", "")
|
|
|
|
if type:
|
|
user = user.filter(admin_type=type)
|
|
|
|
keyword = request.GET.get("keyword", None)
|
|
if keyword:
|
|
user = user.filter(Q(username__icontains=keyword) | Q(userprofile__real_name__icontains=keyword) | Q(email__icontains=keyword))
|
|
return self.success(self.paginate_data(request, user, UserAdminSerializer))
|
|
|
|
@super_admin_required
|
|
def delete(self, request):
|
|
id = request.GET.get("id")
|
|
if not id:
|
|
return self.error("Invalid Parameter, id is required")
|
|
ids = id.split(",")
|
|
if str(request.user.id) in ids:
|
|
return self.error("Current user can not be deleted")
|
|
User.objects.filter(id__in=ids).delete()
|
|
return self.success()
|
|
|
|
|
|
class ResetUserPasswordAPI(APIView):
|
|
@super_admin_required
|
|
def post(self, request):
|
|
"""
|
|
重置用户密码为随机6位数字(不包括0)
|
|
"""
|
|
data = request.data
|
|
user_id = data["id"]
|
|
|
|
try:
|
|
user = User.objects.get(id=user_id)
|
|
except User.DoesNotExist:
|
|
return self.error("User does not exist")
|
|
|
|
# 生成6位随机数字密码(不包括0)
|
|
new_password = get_random_string(6, allowed_chars="123456789")
|
|
|
|
# 设置新密码
|
|
user.set_password(new_password)
|
|
user.save()
|
|
|
|
return self.success(new_password)
|