阶段 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>
254 lines
9.5 KiB
Python
254 lines
9.5 KiB
Python
import asyncio
|
|
import os
|
|
|
|
from django.conf import settings
|
|
from django.contrib import auth
|
|
from django.db.models import Count, Q
|
|
from django.utils import timezone
|
|
from django.utils.decorators import method_decorator
|
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
|
|
|
from options.options import SysOptions
|
|
from problem.models import Problem
|
|
from submission.models import JudgeStatus, Submission
|
|
from utils.api import AsyncAPIView, validate_serializer
|
|
from utils.async_helpers import async_cache_get, async_cache_set
|
|
from utils.constants import CacheKey
|
|
from utils.shortcuts import datetime2str, rand_str
|
|
|
|
from ..decorators import login_required
|
|
from ..models import AdminType, User, UserProfile
|
|
from ..serializers import (
|
|
EditUserProfileSerializer,
|
|
ImageUploadForm,
|
|
RankInfoSerializer,
|
|
UserLoginSerializer,
|
|
UserProfileSerializer,
|
|
UserRegisterSerializer,
|
|
)
|
|
|
|
|
|
class UserProfileAPI(AsyncAPIView):
|
|
@method_decorator(ensure_csrf_cookie)
|
|
async def get(self, request, **kwargs):
|
|
user = request.user
|
|
if not user.is_authenticated:
|
|
return self.success()
|
|
show_real_name = False
|
|
username = request.GET.get("username")
|
|
try:
|
|
if username:
|
|
user = await User.objects.aget(username=username, is_disabled=False)
|
|
else:
|
|
user = request.user
|
|
show_real_name = True
|
|
except User.DoesNotExist:
|
|
return self.error("User does not exist")
|
|
profile = await UserProfile.objects.select_related("user").aget(user=user)
|
|
return self.success(UserProfileSerializer(profile, show_real_name=show_real_name).data)
|
|
|
|
@login_required
|
|
@validate_serializer(EditUserProfileSerializer)
|
|
async def put(self, request):
|
|
data = request.data
|
|
user_profile = await UserProfile.objects.select_related("user").aget(user=request.user)
|
|
for k, v in data.items():
|
|
setattr(user_profile, k, v)
|
|
await user_profile.asave()
|
|
return self.success(UserProfileSerializer(user_profile, show_real_name=True).data)
|
|
|
|
|
|
class Metrics(AsyncAPIView):
|
|
async def get(self, request):
|
|
userid = request.GET.get("userid")
|
|
qs = Submission.objects.filter(user_id=userid, contest_id__isnull=True)
|
|
count, latest, first = await asyncio.gather(
|
|
qs.acount(),
|
|
qs.order_by("-create_time").afirst(),
|
|
qs.order_by("create_time").afirst(),
|
|
)
|
|
if count == 0 or not latest or not first:
|
|
return self.error("暂无提交")
|
|
return self.success(
|
|
{
|
|
"now": datetime2str(timezone.now()),
|
|
"latest": datetime2str(latest.create_time),
|
|
"first": datetime2str(first.create_time),
|
|
}
|
|
)
|
|
|
|
|
|
class AvatarUploadAPI(AsyncAPIView):
|
|
request_parsers = ()
|
|
|
|
@login_required
|
|
async def post(self, request):
|
|
form = ImageUploadForm(request.POST, request.FILES)
|
|
if form.is_valid():
|
|
avatar = form.cleaned_data["image"]
|
|
else:
|
|
return self.error("Invalid file content")
|
|
if avatar.size > 2 * 1024 * 1024:
|
|
return self.error("Picture is too large")
|
|
suffix = os.path.splitext(avatar.name)[-1].lower()
|
|
if suffix not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]:
|
|
return self.error("Unsupported file format")
|
|
|
|
name = rand_str(10) + suffix
|
|
with open(os.path.join(settings.AVATAR_UPLOAD_DIR, name), "wb") as img:
|
|
for chunk in avatar:
|
|
img.write(chunk)
|
|
user_profile = await UserProfile.objects.aget(user=request.user)
|
|
|
|
user_profile.avatar = f"{settings.AVATAR_URI_PREFIX}/{name}"
|
|
await user_profile.asave()
|
|
return self.success("Succeeded")
|
|
|
|
|
|
class UserLoginAPI(AsyncAPIView):
|
|
@validate_serializer(UserLoginSerializer)
|
|
async def post(self, request):
|
|
data = request.data
|
|
user = await auth.aauthenticate(username=data["username"], password=data["password"])
|
|
if user:
|
|
if user.is_disabled:
|
|
return self.error("Your account has been disabled")
|
|
prev_login = user.last_login
|
|
await auth.alogin(request, user)
|
|
request.session["prev_login"] = datetime2str(prev_login) if prev_login else ""
|
|
return self.success("Succeeded")
|
|
else:
|
|
return self.error("Invalid username or password")
|
|
|
|
|
|
class UserLogoutAPI(AsyncAPIView):
|
|
async def get(self, request):
|
|
await auth.alogout(request)
|
|
return self.success()
|
|
|
|
|
|
class UserRegisterAPI(AsyncAPIView):
|
|
@validate_serializer(UserRegisterSerializer)
|
|
async def post(self, request):
|
|
if not await SysOptions.aget("allow_register"):
|
|
return self.error("Register function has been disabled by admin")
|
|
|
|
data = request.data
|
|
data["username"] = data["username"].lower()
|
|
data["email"] = data["email"].lower()
|
|
if await User.objects.filter(username=data["username"]).aexists():
|
|
return self.error("Username already exists")
|
|
if await User.objects.filter(email=data["email"]).aexists():
|
|
return self.error("Email already exists")
|
|
user = await User.objects.acreate(username=data["username"], email=data["email"])
|
|
user.set_password(data["password"])
|
|
await user.asave()
|
|
await UserProfile.objects.acreate(user=user)
|
|
return self.success("Succeeded")
|
|
|
|
|
|
class UserRankAPI(AsyncAPIView):
|
|
async def get(self, request):
|
|
username = request.GET.get("username", "")
|
|
try:
|
|
n = int(request.GET.get("n", "0"))
|
|
except ValueError:
|
|
n = 0
|
|
|
|
profiles = (
|
|
UserProfile.objects.filter(
|
|
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")
|
|
)
|
|
if n > 0:
|
|
profiles = profiles[:n]
|
|
return self.success(await self.async_paginate_data(request, profiles, RankInfoSerializer))
|
|
|
|
|
|
class UserActivityRankAPI(AsyncAPIView):
|
|
async def get(self, request):
|
|
start = request.GET.get("start")
|
|
if not start:
|
|
return self.error("start time is required")
|
|
cache_key = f"{CacheKey.user_activity_rank}:{start}"
|
|
cached = await async_cache_get(cache_key)
|
|
if cached is not None:
|
|
return self.success(cached)
|
|
|
|
hidden_names = User.objects.filter(Q(admin_type=AdminType.SUPER_ADMIN) | Q(is_disabled=True)).values_list("username", flat=True)
|
|
submissions = Submission.objects.filter(
|
|
contest_id__isnull=True,
|
|
create_time__gte=start,
|
|
result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED],
|
|
).exclude(username__in=hidden_names)
|
|
data = [row async for row in submissions.values("username").annotate(count=Count("problem_id", distinct=True)).order_by("-count")[:10]]
|
|
await async_cache_set(cache_key, data, 600)
|
|
return self.success(data)
|
|
|
|
|
|
class UserProblemRankAPI(AsyncAPIView):
|
|
async def get(self, request):
|
|
problem_id = request.GET.get("problem_id")
|
|
user = request.user
|
|
if not user.is_authenticated:
|
|
return self.error("User is not authenticated")
|
|
|
|
problem = await Problem.objects.aget(_id__iexact=problem_id, contest_id__isnull=True, visible=True)
|
|
submissions = Submission.objects.filter(problem=problem, result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED])
|
|
|
|
all_ac_count = await submissions.values("user_id").distinct().acount()
|
|
|
|
class_name = user.class_name or ""
|
|
class_ac_count = 0
|
|
|
|
if class_name:
|
|
users = User.objects.filter(class_name=user.class_name, is_disabled=False).values_list("id", flat=True)
|
|
user_ids = [user_id async for user_id in users]
|
|
submissions = submissions.filter(user_id__in=user_ids)
|
|
class_ac_count = await submissions.values("user_id").distinct().acount()
|
|
|
|
my_submissions = submissions.filter(user_id=user.id)
|
|
|
|
if not await my_submissions.aexists():
|
|
return self.success(
|
|
{
|
|
"class_name": class_name,
|
|
"rank": -1,
|
|
"class_ac_count": class_ac_count,
|
|
"all_ac_count": all_ac_count,
|
|
}
|
|
)
|
|
|
|
my_first_submission = await my_submissions.order_by("create_time").afirst()
|
|
rank = await submissions.filter(create_time__lte=my_first_submission.create_time).acount()
|
|
return self.success(
|
|
{
|
|
"class_name": class_name,
|
|
"rank": rank,
|
|
"class_ac_count": class_ac_count,
|
|
"all_ac_count": all_ac_count,
|
|
}
|
|
)
|
|
|
|
|
|
class ProfileProblemDisplayIDRefreshAPI(AsyncAPIView):
|
|
@login_required
|
|
async def get(self, request):
|
|
profile = await UserProfile.objects.aget(user=request.user)
|
|
acm_problems = profile.acm_problems_status.get("problems", {})
|
|
ids = list(acm_problems.keys())
|
|
if not ids:
|
|
return self.success()
|
|
display_ids = [did async for did in Problem.objects.filter(id__in=ids, visible=True).values_list("_id", flat=True)]
|
|
id_map = dict(zip(ids, display_ids))
|
|
for k, v in acm_problems.items():
|
|
v["_id"] = id_map[k]
|
|
await profile.asave(update_fields=["acm_problems_status"])
|
|
return self.success()
|
|
|