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>
This commit is contained in:
2026-08-08 06:59:00 -06:00
parent 4ebcecfab4
commit eed0e630e6
18 changed files with 11 additions and 483 deletions

View File

@@ -1,30 +1,19 @@
import copy
import os
import zipfile
from datetime import datetime, timedelta
from ipaddress import ip_network
from django.http import FileResponse
from django.utils.timezone import now
from account.decorators import ensure_created_by, super_admin_required, teacher_admin_required
from account.models import User
from account.decorators import ensure_created_by, teacher_admin_required
from problem.models import Problem
from submission.models import JudgeStatus, Submission
from utils.api import APIView, validate_serializer
from utils.shortcuts import rand_str
from utils.tasks import delete_files
from ..models import ACMContestRank, Contest, ContestAnnouncement
from ..models import ACMContestRank, Contest
from ..serializers import (
ACMContesHelperSerializer,
ContestAdminSerializer,
ContestAnnouncementSerializer,
ContestCloneSerializer,
CreateConetestSeriaizer,
CreateContestAnnouncementSerializer,
EditConetestSeriaizer,
EditContestAnnouncementSerializer,
)
@@ -96,73 +85,6 @@ class ContestAPI(APIView):
return self.success(self.paginate_data(request, contests, ContestAdminSerializer))
# DEPRECATED: 前端未调用 (2026-05-26)
class ContestAnnouncementAPI(APIView):
@validate_serializer(CreateContestAnnouncementSerializer)
@super_admin_required
def post(self, request):
"""
Create one contest_announcement.
"""
data = request.data
try:
contest = Contest.objects.get(id=data.pop("contest_id"))
data["contest"] = contest
data["created_by"] = request.user
except Contest.DoesNotExist:
return self.error("Contest does not exist")
announcement = ContestAnnouncement.objects.create(**data)
return self.success(ContestAnnouncementSerializer(announcement).data)
@validate_serializer(EditContestAnnouncementSerializer)
@super_admin_required
def put(self, request):
"""
update contest_announcement
"""
data = request.data
try:
contest_announcement = ContestAnnouncement.objects.get(id=data.pop("id"))
except ContestAnnouncement.DoesNotExist:
return self.error("Contest announcement does not exist")
for k, v in data.items():
setattr(contest_announcement, k, v)
contest_announcement.save()
return self.success()
@super_admin_required
def delete(self, request):
"""
Delete one contest_announcement.
"""
contest_announcement_id = request.GET.get("id")
if contest_announcement_id:
ContestAnnouncement.objects.filter(id=contest_announcement_id).delete()
return self.success()
@super_admin_required
def get(self, request):
"""
Get one contest_announcement or contest_announcement list.
"""
contest_announcement_id = request.GET.get("id")
if contest_announcement_id:
try:
contest_announcement = ContestAnnouncement.objects.get(id=contest_announcement_id)
return self.success(ContestAnnouncementSerializer(contest_announcement).data)
except ContestAnnouncement.DoesNotExist:
return self.error("Contest announcement does not exist")
contest_id = request.GET.get("contest_id")
if not contest_id:
return self.error("Parameter error")
contest_announcements = ContestAnnouncement.objects.filter(contest_id=contest_id)
keyword = request.GET.get("keyword")
if keyword:
contest_announcements = contest_announcements.filter(title__contains=keyword)
return self.success(ContestAnnouncementSerializer(contest_announcements, many=True).data)
class ACMContestHelper(APIView):
@teacher_admin_required
def get(self, request):
@@ -217,55 +139,6 @@ class ACMContestHelper(APIView):
return self.success()
# DEPRECATED: 前端未调用 (2026-05-26)
class DownloadContestSubmissions(APIView):
def _dump_submissions(self, contest, exclude_admin=True):
problem_ids = contest.problem_set.all().values_list("id", "_id")
id2display_id = {k[0]: k[1] for k in problem_ids}
ac_map = {k[0]: False for k in problem_ids}
submissions = Submission.objects.filter(contest=contest, result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).order_by("-create_time")
user_ids = submissions.values_list("user_id", flat=True)
users = User.objects.filter(id__in=user_ids)
path = f"/tmp/{rand_str()}.zip"
with zipfile.ZipFile(path, "w") as zip_file:
for user in users:
if user.is_admin_role() and exclude_admin:
continue
user_ac_map = copy.deepcopy(ac_map)
user_submissions = submissions.filter(user_id=user.id)
for submission in user_submissions:
problem_id = submission.problem_id
if user_ac_map[problem_id]:
continue
file_name = f"{user.username}_{id2display_id[submission.problem_id]}.txt"
compression = zipfile.ZIP_DEFLATED
zip_file.writestr(
zinfo_or_arcname=f"{file_name}",
data=submission.code,
compress_type=compression,
)
user_ac_map[problem_id] = True
return path
@super_admin_required
def get(self, request):
contest_id = request.GET.get("contest_id")
if not contest_id:
return self.error("Parameter error")
try:
contest = Contest.objects.get(id=contest_id)
except Contest.DoesNotExist:
return self.error("Contest does not exist")
exclude_admin = request.GET.get("exclude_admin") == "1"
zip_path = self._dump_submissions(contest, exclude_admin)
delete_files.send_with_options(args=(zip_path,), delay=300_000)
resp = FileResponse(open(zip_path, "rb"))
resp["Content-Type"] = "application/zip"
resp["Content-Disposition"] = f"attachment;filename={os.path.basename(zip_path)}"
return resp
class ContestCloneAPI(APIView):
@validate_serializer(ContestCloneSerializer)
@teacher_admin_required

View File

@@ -16,23 +16,8 @@ from utils.api import AsyncAPIView, validate_serializer
from utils.constants import CONTEST_PASSWORD_SESSION_KEY, ContestStatus
from utils.shortcuts import check_is_id, datetime2str
from ..models import ACMContestRank, Contest, ContestAnnouncement
from ..serializers import ACMContestRankSerializer, ContestAnnouncementSerializer, ContestPasswordVerifySerializer, ContestSerializer
# DEPRECATED: 前端未调用 (2026-05-26)
class ContestAnnouncementListAPI(AsyncAPIView):
@check_contest_permission(check_type="announcements")
async def get(self, request):
contest_id = request.GET.get("contest_id")
if not contest_id:
return self.error("Invalid parameter, contest_id is required")
qs = ContestAnnouncement.objects.select_related("created_by").filter(contest_id=contest_id, visible=True)
max_id = request.GET.get("max_id")
if max_id:
qs = qs.filter(id__gt=max_id)
data = await self.async_serialize_data(ContestAnnouncementSerializer, [item async for item in qs], many=True)
return self.success(data)
from ..models import ACMContestRank, Contest
from ..serializers import ACMContestRankSerializer, ContestPasswordVerifySerializer, ContestSerializer
class ContestAPI(AsyncAPIView):