Files
OnlineJudge/problemset/views/admin.py
yuetsh eed0e630e6 chore: 删掉 17 个确认无人调用的端点(净 -472 行)
阶段 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>
2026-08-08 06:59:00 -06:00

386 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.db.models import Count, Q
from account.decorators import ensure_created_by, teacher_admin_required
from problem.models import Problem
from problemset.models import (
ProblemSet,
ProblemSetBadge,
ProblemSetProblem,
ProblemSetProgress,
UserBadge,
)
from problemset.serializers import (
AddProblemToSetSerializer,
CreateProblemSetBadgeSerializer,
CreateProblemSetSerializer,
EditProblemInSetSerializer,
EditProblemSetBadgeSerializer,
EditProblemSetSerializer,
ProblemSetBadgeSerializer,
ProblemSetListSerializer,
ProblemSetProblemSerializer,
ProblemSetProgressSerializer,
ProblemSetSerializer,
ProblemSetUpdateStatusSerializer,
ProblemSetVisibleSerializer,
)
from utils.api import APIView, validate_serializer
class ProblemSetAdminAPI(APIView):
"""题单管理API"""
@teacher_admin_required
def get(self, request):
"""获取题单列表(管理员)"""
# 后台列表**不能**过滤 visible题单列表里有可见性开关一旦关掉
# 题单就从这个列表里消失,教师再也找不到它、也就无法在界面上打开回来。
# 前台列表views/oj.py该过滤后台不该。
problem_sets = ProblemSet.objects.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()
if keyword:
problem_sets = problem_sets.filter(Q(title__icontains=keyword) | Q(description__icontains=keyword))
difficulty = request.GET.get("difficulty")
if difficulty:
problem_sets = problem_sets.filter(difficulty=difficulty)
status = request.GET.get("status")
if status:
problem_sets = problem_sets.filter(status=status)
# 使用统一的分页方法
data = self.paginate_data(request, problem_sets, ProblemSetListSerializer)
return self.success(data)
@teacher_admin_required
@validate_serializer(CreateProblemSetSerializer)
def post(self, request):
"""创建题单"""
data = request.data
data["created_by"] = request.user
problem_set = ProblemSet.objects.create(**data)
return self.success(ProblemSetSerializer(problem_set).data)
@teacher_admin_required
@validate_serializer(EditProblemSetSerializer)
def put(self, request):
"""编辑题单"""
data = request.data
problem_set_id = data.pop("id")
try:
problem_set = ProblemSet.objects.get(id=problem_set_id)
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
for key, value in data.items():
setattr(problem_set, key, value)
problem_set.save()
return self.success(ProblemSetSerializer(problem_set).data)
@teacher_admin_required
def delete(self, request):
"""删除题单"""
problem_set_id = request.GET.get("id")
if not problem_set_id:
return self.error("题单ID是必需的")
try:
problem_set = ProblemSet.objects.get(id=problem_set_id)
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
problem_set.delete()
return self.success("题单已删除")
class ProblemSetDetailAdminAPI(APIView):
"""题单详情管理API"""
@teacher_admin_required
def get(self, request, problem_set_id):
"""获取题单详情(管理员)"""
try:
problem_set = ProblemSet.objects.get(id=problem_set_id)
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
serializer = ProblemSetSerializer(problem_set, context={"request": request})
return self.success(serializer.data)
class ProblemSetProblemAdminAPI(APIView):
"""题单题目管理API管理员"""
@teacher_admin_required
def get(self, request, problem_set_id):
"""获取题单中的题目列表(管理员)"""
try:
problem_set = ProblemSet.objects.get(id=problem_set_id)
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
problems = ProblemSetProblem.objects.filter(problemset=problem_set).order_by("order")
serializer = ProblemSetProblemSerializer(problems, many=True, context={"request": request})
return self.success(serializer.data)
@teacher_admin_required
@validate_serializer(AddProblemToSetSerializer)
def post(self, request, problem_set_id):
"""添加题目到题单(管理员)"""
try:
problem_set = ProblemSet.objects.get(id=problem_set_id)
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
data = request.data
try:
problem = Problem.objects.filter(
_id__iexact=data["problem_id"],
visible=True,
contest_id__isnull=True,
).get()
except Problem.DoesNotExist:
return self.error("题目不存在或不可见")
# 检查题目是否已经在题单中
if ProblemSetProblem.objects.filter(problemset=problem_set, problem=problem).exists():
return self.error("题目已在该题单中")
ProblemSetProblem.objects.create(
problemset=problem_set,
problem=problem,
order=data.get("order", 0),
is_required=data.get("is_required", True),
score=data.get("score", 0),
hint=data.get("hint", ""),
)
# 题目数就是进度的分母,加完必须重算:不重算的话已加入的学生进度百分比
# 全是虚高的,而且原本标成"已完成"的人会一直是已完成。
# 手动同步接口ProblemSetSyncAPI前端没有调用点所以只能在这里做。
ProblemSetProgress.sync_all_progress_for_problemset(problem_set)
return self.success("题目已添加到题单")
@teacher_admin_required
@validate_serializer(EditProblemInSetSerializer)
def put(self, request, problem_set_id, problem_set_problem_id):
"""编辑题单中的题目(管理员)"""
try:
problem_set = ProblemSet.objects.get(id=problem_set_id)
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
try:
problem_set_problem = ProblemSetProblem.objects.get(id=problem_set_problem_id, problemset=problem_set)
except ProblemSetProblem.DoesNotExist:
return self.error("题目不在该题单中")
data = request.data
# 更新题目属性
if "order" in data:
problem_set_problem.order = data["order"]
if "is_required" in data:
problem_set_problem.is_required = data["is_required"]
if "score" in data:
problem_set_problem.score = data["score"]
if "hint" in data:
problem_set_problem.hint = data["hint"]
problem_set_problem.save()
return self.success("题目已更新")
@teacher_admin_required
def delete(self, request, problem_set_id, problem_set_problem_id):
"""从题单中移除题目(管理员)"""
try:
problem_set = ProblemSet.objects.get(id=problem_set_id)
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
try:
problem_set_problem = ProblemSetProblem.objects.get(id=problem_set_problem_id, problemset=problem_set)
problem_set_problem.delete()
# 同上:分母变了,进度要重算
ProblemSetProgress.sync_all_progress_for_problemset(problem_set)
return self.success("题目已从题单中移除")
except ProblemSetProblem.DoesNotExist:
return self.error("题目不在该题单中")
class ProblemSetBadgeAdminAPI(APIView):
"""题单奖章管理API管理员"""
@teacher_admin_required
def get(self, request, problem_set_id):
"""获取题单的奖章列表(管理员)"""
try:
problem_set = ProblemSet.objects.get(id=problem_set_id)
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
badges = ProblemSetBadge.objects.filter(problemset=problem_set)
serializer = ProblemSetBadgeSerializer(badges, many=True)
return self.success(serializer.data)
@teacher_admin_required
@validate_serializer(CreateProblemSetBadgeSerializer)
def post(self, request, problem_set_id):
"""创建题单奖章(管理员)"""
try:
problem_set = ProblemSet.objects.get(id=problem_set_id)
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
data = request.data
data["problemset"] = problem_set
badge = ProblemSetBadge.objects.create(**data)
return self.success(ProblemSetBadgeSerializer(badge).data)
@teacher_admin_required
@validate_serializer(EditProblemSetBadgeSerializer)
def put(self, request, problem_set_id, badge_id):
"""编辑题单奖章(管理员)"""
try:
problem_set = ProblemSet.objects.get(id=problem_set_id)
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
try:
badge = ProblemSetBadge.objects.get(id=badge_id, problemset=problem_set)
except ProblemSetBadge.DoesNotExist:
return self.error("奖章不存在")
data = request.data
# 记录是否修改了条件相关的字段
condition_changed = False
# 更新奖章属性
if "name" in data:
badge.name = data["name"]
if "description" in data:
badge.description = data["description"]
if "icon" in data:
badge.icon = data["icon"]
if "condition_type" in data:
badge.condition_type = data["condition_type"]
condition_changed = True
if "condition_value" in data:
badge.condition_value = data["condition_value"]
condition_changed = True
badge.save() # post_save 信号自动触发 recalculate_user_badges
if condition_changed:
return self.success("奖章已更新,并重新计算了所有用户的徽章资格")
return self.success("奖章已更新")
@teacher_admin_required
def delete(self, request, problem_set_id, badge_id):
"""删除题单奖章(管理员)"""
try:
problem_set = ProblemSet.objects.get(id=problem_set_id)
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
try:
badge = ProblemSetBadge.objects.get(id=badge_id, problemset=problem_set)
badge.delete()
return self.success("奖章已删除")
except ProblemSetBadge.DoesNotExist:
return self.error("奖章不存在")
class ProblemSetProgressAdminAPI(APIView):
"""题单进度管理API管理员"""
@teacher_admin_required
def get(self, request, problem_set_id):
"""获取题单的所有用户进度(管理员)"""
try:
problem_set = ProblemSet.objects.get(id=problem_set_id)
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
progress_list = ProblemSetProgress.objects.filter(problemset=problem_set).order_by("-join_time")
serializer = ProblemSetProgressSerializer(progress_list, many=True)
return self.success(serializer.data)
@teacher_admin_required
def delete(self, request, problem_set_id, user_id):
"""移除用户从题单(管理员)"""
try:
problem_set = ProblemSet.objects.get(id=problem_set_id)
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
try:
progress = ProblemSetProgress.objects.get(problemset=problem_set, user_id=user_id)
progress.delete()
# 奖章是靠这个题单挣的,人移出去了奖章却留着 —— 学生个人页上会一直挂着
# 一个自己已经不在的题单的奖章,而且重新加入时会因唯一约束拿不到第二次。
UserBadge.objects.filter(user_id=user_id, badge__problemset=problem_set).delete()
return self.success("用户已从题单中移除")
except ProblemSetProgress.DoesNotExist:
return self.error("用户未加入该题单")
class ProblemSetVisibleAPI(APIView):
"""题单可见性管理API"""
@teacher_admin_required
@validate_serializer(ProblemSetVisibleSerializer)
def put(self, request):
"""切换题单可见性"""
data = request.data
try:
problem_set = ProblemSet.objects.get(id=data["id"])
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
problem_set.visible = not problem_set.visible
problem_set.save()
return self.success()
class ProblemSetStatusAPI(APIView):
"""题单状态管理API"""
@teacher_admin_required
@validate_serializer(ProblemSetUpdateStatusSerializer)
def put(self, request):
"""更新题单状态"""
data = request.data
try:
problem_set = ProblemSet.objects.get(id=data["id"])
ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist:
return self.error("题单不存在")
problem_set.status = data["status"]
problem_set.save()
return self.success()