fix(achievement): 指标的 AC 口径改用 is_accepted,与全项目一致
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,13 +13,17 @@ import logging
|
|||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from submission.models import JudgeStatus, Submission
|
from submission.models import JudgeStatus, Submission, is_accepted
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
METRIC_REGISTRY = {}
|
METRIC_REGISTRY = {}
|
||||||
META_METRICS = set()
|
META_METRICS = set()
|
||||||
|
|
||||||
|
# AC 口径与全项目一致:AST_CHECK_FAILED(代码结构检查未通过但测试点全过)也算通过。
|
||||||
|
# 见 submission/models.py 的 is_accepted()。ORM 过滤用这个常量,标量比较用 is_accepted()。
|
||||||
|
ACCEPTED_RESULTS = (JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED)
|
||||||
|
|
||||||
|
|
||||||
class BaseMetric:
|
class BaseMetric:
|
||||||
key = ""
|
key = ""
|
||||||
@@ -64,19 +68,20 @@ def build_ctx(user_id, sub):
|
|||||||
prior = _practice_submissions(user_id).filter(problem_id=sub.problem_id).exclude(id=sub.id)
|
prior = _practice_submissions(user_id).filter(problem_id=sub.problem_id).exclude(id=sub.id)
|
||||||
prior_stats = prior.aggregate(
|
prior_stats = prior.aggregate(
|
||||||
total=Count("id"),
|
total=Count("id"),
|
||||||
accepted=Count("id", filter=Q(result=JudgeStatus.ACCEPTED)),
|
accepted=Count("id", filter=Q(result__in=ACCEPTED_RESULTS)),
|
||||||
)
|
)
|
||||||
local_now = timezone.localtime(sub.create_time)
|
local_now = timezone.localtime(sub.create_time)
|
||||||
|
sub_is_accepted = is_accepted(sub.result)
|
||||||
return {
|
return {
|
||||||
"skip": False,
|
"skip": False,
|
||||||
"is_accepted": sub.result == JudgeStatus.ACCEPTED,
|
"is_accepted": sub_is_accepted,
|
||||||
# 该题此前的提交次数与 AC 次数
|
# 该题此前的提交次数与 AC 次数
|
||||||
"prior_count": prior_stats["total"],
|
"prior_count": prior_stats["total"],
|
||||||
"prior_accepted": prior_stats["accepted"],
|
"prior_accepted": prior_stats["accepted"],
|
||||||
# 首次 AC 这道题(此前从未 AC 过)
|
# 首次 AC 这道题(此前从未 AC 过)
|
||||||
"is_first_ac_of_problem": sub.result == JudgeStatus.ACCEPTED and prior_stats["accepted"] == 0,
|
"is_first_ac_of_problem": sub_is_accepted and prior_stats["accepted"] == 0,
|
||||||
# 一发入魂:此前无任何提交且本次 AC
|
# 一发入魂:此前无任何提交且本次 AC
|
||||||
"is_first_try_ac": sub.result == JudgeStatus.ACCEPTED and prior_stats["total"] == 0,
|
"is_first_try_ac": sub_is_accepted and prior_stats["total"] == 0,
|
||||||
"local_date": local_now.date().isoformat(),
|
"local_date": local_now.date().isoformat(),
|
||||||
"local_hour": local_now.hour,
|
"local_hour": local_now.hour,
|
||||||
}
|
}
|
||||||
@@ -89,7 +94,7 @@ class AcceptedCount(BaseMetric):
|
|||||||
metrics["accepted_count"] = metrics.get("accepted_count", 0) + 1
|
metrics["accepted_count"] = metrics.get("accepted_count", 0) + 1
|
||||||
|
|
||||||
def recompute(self, user):
|
def recompute(self, user):
|
||||||
return _practice_submissions(user.id).filter(result=JudgeStatus.ACCEPTED).values("problem_id").distinct().count()
|
return _practice_submissions(user.id).filter(result__in=ACCEPTED_RESULTS).values("problem_id").distinct().count()
|
||||||
|
|
||||||
|
|
||||||
@metric("submission_count", "提交总数", "提交次数(不含比赛)")
|
@metric("submission_count", "提交总数", "提交次数(不含比赛)")
|
||||||
@@ -134,7 +139,7 @@ class MaxAcStreakDays(BaseMetric):
|
|||||||
metrics["max_ac_streak_days"] = max(metrics.get("max_ac_streak_days", 0), current)
|
metrics["max_ac_streak_days"] = max(metrics.get("max_ac_streak_days", 0), current)
|
||||||
|
|
||||||
def recompute(self, user):
|
def recompute(self, user):
|
||||||
dates = sorted({timezone.localtime(t).date() for t in _practice_submissions(user.id).filter(result=JudgeStatus.ACCEPTED).values_list("create_time", flat=True)})
|
dates = sorted({timezone.localtime(t).date() for t in _practice_submissions(user.id).filter(result__in=ACCEPTED_RESULTS).values_list("create_time", flat=True)})
|
||||||
if not dates:
|
if not dates:
|
||||||
return None
|
return None
|
||||||
best = current = 1
|
best = current = 1
|
||||||
@@ -203,7 +208,7 @@ class FirstTryAcCount(BaseMetric):
|
|||||||
if s["problem_id"] in seen:
|
if s["problem_id"] in seen:
|
||||||
continue
|
continue
|
||||||
seen.add(s["problem_id"])
|
seen.add(s["problem_id"])
|
||||||
if s["result"] == JudgeStatus.ACCEPTED:
|
if is_accepted(s["result"]):
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@@ -241,7 +246,7 @@ class MaxWaBeforeAc(BaseMetric):
|
|||||||
pid = s["problem_id"]
|
pid = s["problem_id"]
|
||||||
if pid in attempts and attempts[pid] is None:
|
if pid in attempts and attempts[pid] is None:
|
||||||
continue
|
continue
|
||||||
if s["result"] == JudgeStatus.ACCEPTED:
|
if is_accepted(s["result"]):
|
||||||
best = max(best or 0, attempts.get(pid, 0))
|
best = max(best or 0, attempts.get(pid, 0))
|
||||||
attempts[pid] = None
|
attempts[pid] = None
|
||||||
else:
|
else:
|
||||||
@@ -262,7 +267,7 @@ class MaxAcInOneDay(BaseMetric):
|
|||||||
def recompute(self, user):
|
def recompute(self, user):
|
||||||
counts = {}
|
counts = {}
|
||||||
seen = set()
|
seen = set()
|
||||||
for s in _practice_submissions(user.id).filter(result=JudgeStatus.ACCEPTED).order_by("create_time").values("problem_id", "create_time"):
|
for s in _practice_submissions(user.id).filter(result__in=ACCEPTED_RESULTS).order_by("create_time").values("problem_id", "create_time"):
|
||||||
if s["problem_id"] in seen:
|
if s["problem_id"] in seen:
|
||||||
continue
|
continue
|
||||||
seen.add(s["problem_id"])
|
seen.add(s["problem_id"])
|
||||||
@@ -281,7 +286,7 @@ class MinAcCodeChars(BaseMetric):
|
|||||||
metrics["min_ac_code_chars"] = length if cur is None else min(cur, length)
|
metrics["min_ac_code_chars"] = length if cur is None else min(cur, length)
|
||||||
|
|
||||||
def recompute(self, user):
|
def recompute(self, user):
|
||||||
lengths = [len(c) for c in _practice_submissions(user.id).filter(result=JudgeStatus.ACCEPTED).values_list("code", flat=True)]
|
lengths = [len(c) for c in _practice_submissions(user.id).filter(result__in=ACCEPTED_RESULTS).values_list("code", flat=True)]
|
||||||
return min(lengths) if lengths else None
|
return min(lengths) if lengths else None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
- 后端 lint:`ruff check .` 与 `ruff format .`(E/F/I 规则,行宽 180,双引号)。每次提交前必须通过。
|
- 后端 lint:`ruff check .` 与 `ruff format .`(E/F/I 规则,行宽 180,双引号)。每次提交前必须通过。
|
||||||
- 前端格式化:`npm fmt`(Prettier)。
|
- 前端格式化:`npm fmt`(Prettier)。
|
||||||
- 所有基于提交的指标**只统计 `contest_id IS NULL` 的提交**,比赛题不计入成就。唯一例外是 `contest_joined`。
|
- 所有基于提交的指标**只统计 `contest_id IS NULL` 的提交**,比赛题不计入成就。唯一例外是 `contest_joined`。
|
||||||
|
- **判断提交是否算通过一律用 `submission.models.is_accepted()`**,即 `result in (ACCEPTED, AST_CHECK_FAILED)`,与项目其余所有 AC 统计口径一致。ORM 过滤用 `result__in=(JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED)`。(Task 2 执行期间由 reviewer 发现计划原文写成了 `result == ACCEPTED`,经人工裁决改正。)
|
||||||
- 成就为纯荣誉,**不发放任何可消费奖励**,不接入积分/道具/权限体系。
|
- 成就为纯荣誉,**不发放任何可消费奖励**,不接入积分/道具/权限体系。
|
||||||
- 判定任务内所有异常必须捕获并记日志,绝不允许影响判题结果。
|
- 判定任务内所有异常必须捕获并记日志,绝不允许影响判题结果。
|
||||||
- 指标未产生过有效值时,其 key **不存在于** `metrics` 字典中(而非置 0)。判定时遇到缺失 key 直接跳过该成就。
|
- 指标未产生过有效值时,其 key **不存在于** `metrics` 字典中(而非置 0)。判定时遇到缺失 key 直接跳过该成就。
|
||||||
|
|||||||
@@ -148,6 +148,14 @@ class MidnightSubmissions:
|
|||||||
|
|
||||||
这条对所有指标统一适用,累积型指标(缺失即视为未达标)行为不变,极小值型指标由此被正确保护。前端进度条同理:指标缺失时显示 `0 / N` 而不是拿缺失值参与计算。
|
这条对所有指标统一适用,累积型指标(缺失即视为未达标)行为不变,极小值型指标由此被正确保护。前端进度条同理:指标缺失时显示 `0 / N` 而不是拿缺失值参与计算。
|
||||||
|
|
||||||
|
### AC 的口径
|
||||||
|
|
||||||
|
判断一次提交是否算通过,一律使用 `submission/models.py` 的 `is_accepted()`,即 `result in (ACCEPTED, AST_CHECK_FAILED)`。
|
||||||
|
|
||||||
|
`AST_CHECK_FAILED`(状态码 10)表示测试用例全部通过、但违反了教师配置的代码结构规则。项目里每一处统计 AC 的地方都把它算作通过(个人主页的已通过题数、题目的通过人数、比赛排名等),成就必须跟随同一口径——否则学生个人主页显示「已通过 50 题」而成就进度条显示 47,会被当成 bug 来问。
|
||||||
|
|
||||||
|
ORM 过滤无法调用该函数,用 `result__in=(JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED)`。
|
||||||
|
|
||||||
### 比赛提交不计入成就
|
### 比赛提交不计入成就
|
||||||
|
|
||||||
**所有基于提交的指标只统计 `contest_id IS NULL` 的提交。** 比赛里做的题不算进「AC 100 题」这类成就。
|
**所有基于提交的指标只统计 `contest_id IS NULL` 的提交。** 比赛里做的题不算进「AC 100 题」这类成就。
|
||||||
|
|||||||
Reference in New Issue
Block a user