Files
OnlineJudge/contest/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

179 lines
7.3 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 datetime import datetime, timedelta
from ipaddress import ip_network
from django.utils.timezone import now
from account.decorators import ensure_created_by, teacher_admin_required
from problem.models import Problem
from utils.api import APIView, validate_serializer
from ..models import ACMContestRank, Contest
from ..serializers import (
ACMContesHelperSerializer,
ContestAdminSerializer,
ContestCloneSerializer,
CreateConetestSeriaizer,
EditConetestSeriaizer,
)
class ContestAPI(APIView):
@validate_serializer(CreateConetestSeriaizer)
@teacher_admin_required
def post(self, request):
data = request.data
# DRF 的 DateTimeField 已经校验并规范化过,这里拿到的必定是 ISO 8601 字符串
data["start_time"] = datetime.fromisoformat(data["start_time"])
data["end_time"] = datetime.fromisoformat(data["end_time"])
data["created_by"] = request.user
if data["end_time"] <= data["start_time"]:
return self.error("Start time must occur earlier than end time")
if data.get("password") and data["password"] == "":
data["password"] = None
for ip_range in data["allowed_ip_ranges"]:
try:
ip_network(ip_range, strict=False)
except ValueError:
return self.error(f"{ip_range} is not a valid cidr network")
contest = Contest.objects.create(**data)
return self.success(ContestAdminSerializer(contest).data)
@validate_serializer(EditConetestSeriaizer)
@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)
# DRF 的 DateTimeField 已经校验并规范化过,这里拿到的必定是 ISO 8601 字符串
data["start_time"] = datetime.fromisoformat(data["start_time"])
data["end_time"] = datetime.fromisoformat(data["end_time"])
if data["end_time"] <= data["start_time"]:
return self.error("Start time must occur earlier than end time")
if not data["password"]:
data["password"] = None
for ip_range in data["allowed_ip_ranges"]:
try:
ip_network(ip_range, strict=False)
except ValueError:
return self.error(f"{ip_range} is not a valid cidr network")
for k, v in data.items():
setattr(contest, k, v)
contest.save()
return self.success(ContestAdminSerializer(contest).data)
@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:
contests = contests.filter(title__contains=keyword)
return self.success(self.paginate_data(request, contests, ContestAdminSerializer))
class ACMContestHelper(APIView):
@teacher_admin_required
def get(self, request):
contest_id = request.GET.get("contest_id")
if not contest_id:
return self.error("Parameter error, contest_id is required")
try:
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}
ranks = ACMContestRank.objects.filter(contest=contest, accepted_number__gt=0).values("id", "user__username", "user__userprofile__real_name", "submission_info")
results = []
for rank in ranks:
for problem_id, info in rank["submission_info"].items():
if info["is_ac"]:
results.append(
{
"id": rank["id"],
"username": rank["user__username"],
"real_name": rank["user__userprofile__real_name"],
"problem_id": problem_id,
"problem_display_id": problem_id_map.get(problem_id, problem_id),
"ac_info": info,
"checked": info.get("checked", False),
}
)
results.sort(key=lambda x: -x["ac_info"]["ac_time"])
return self.success(results)
@teacher_admin_required
@validate_serializer(ACMContesHelperSerializer)
def put(self, request):
data = request.data
# 原来只按 pk 取contest_id 明明在序列化器里、客户端一直在传,却完全没用上。
# 于是任何老师都能改别人比赛里的检查标记 —— teacher_admin_required 只保证
# "是老师"。同类的 get 是有 ensure_created_by 的,这里补齐。
try:
rank = ACMContestRank.objects.get(pk=data["rank_id"], contest_id=data["contest_id"])
except ACMContestRank.DoesNotExist:
return self.error("Rank id does not exist")
ensure_created_by(rank.contest, request.user)
problem_rank_status = rank.submission_info.get(data["problem_id"])
if not problem_rank_status:
return self.error("Problem id does not exist")
problem_rank_status["checked"] = data["checked"]
rank.save(update_fields=("submission_info",))
return self.success()
class ContestCloneAPI(APIView):
@validate_serializer(ContestCloneSerializer)
@teacher_admin_required
def post(self, request):
try:
original = Contest.objects.get(id=request.data["contest_id"])
except Contest.DoesNotExist:
return self.error("Contest does not exist")
duration = original.end_time - original.start_time
new_start = now() + timedelta(minutes=10)
new_end = new_start + duration
new_contest = Contest.objects.create(
title=original.title,
description=original.description,
tag=original.tag,
password=original.password,
visible=False,
allowed_ip_ranges=original.allowed_ip_ranges,
start_time=new_start,
end_time=new_end,
created_by=request.user,
)
for problem in Problem.objects.filter(contest=original):
tags = problem.tags.all()
problem.pk = None
problem.contest = new_contest
problem.submission_number = 0
problem.accepted_number = 0
problem.statistic_info = {}
problem.created_by = request.user
problem.save()
problem.tags.set(tags)
return self.success(ContestAdminSerializer(new_contest).data)