utils/tasks.py 整个文件(delete_files) utils/async_helpers.py async_cache_delete problem/utils.py build_problem_template utils/shortcuts.py build_query_string achievement/checker.py get_or_create_stat problem/serializers.py TemplateSerializer delete_files 的唯一调用者是上一个 commit 删掉的 DownloadContestSubmissions (`delete_files.send_with_options(args=(zip_path,), delay=300_000)`,生成 zip 后 延时清理)。django_dramatiq 按约定自动发现各 app 的 tasks.py,缺一个文件不报错, 已实跑确认 judge / achievement / flowchart 三处的 actor 都还在。 ## 扫出来但**不能删**的,记在这里免得下次又查一遍 **achievement/metrics.py 的 18 个指标类**:静态扫描报"零引用",因为类名确实 从不被引用 —— 它们靠 `@metric(key, name)` 装饰器在**定义时**注册进 METRIC_REGISTRY。删掉会静默丢掉成就维度,而且不报错。运行时确认注册表里 18 个一个不少。 **async_cache_get / async_cache_set**:各 6 处引用,只有 delete 那个是死的。 **parse_problem_template**:5 处引用,和被删的 build_problem_template 只是名字像。 **User 模型上的 open_api / open_api_appkey / two_factor_auth / session_keys**: 删字段要迁移,会改生产库结构 —— 而新后端正是靠"库结构与旧后端逐列一致"才能 回滚(阶段 5 演练验证过)。切换完成之前不动。 ## 验证 manage.py check 通过;起本地 Django + PostgreSQL 实跑 14 个端点全部 200 (前台 10 个 + 后台 4 个);ruff 全绿;行尾未被改动。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
import os
|
||
import random
|
||
import re
|
||
|
||
from django.utils.crypto import get_random_string
|
||
|
||
# 班级号的位数范围。学生用户名形如 ks<班级号><姓名>,班级号还要跟
|
||
# SysOptions.class_list 的条目、User.class_name 字段对得上。
|
||
# 改这里等于改全站规则,前端 ojnext/src/utils/constants.ts 里
|
||
# CLASS_NAME_DIGITS 是同一条规则的另一份,两边要一起改。
|
||
CLASS_NAME_MIN_DIGITS = 3
|
||
CLASS_NAME_MAX_DIGITS = 4
|
||
CLASS_NAME_RE = re.compile(rf"\d{{{CLASS_NAME_MIN_DIGITS},{CLASS_NAME_MAX_DIGITS}}}")
|
||
|
||
|
||
def is_valid_class_name(class_name):
|
||
"""班级号是否是合法位数的纯数字"""
|
||
return bool(CLASS_NAME_RE.fullmatch(class_name))
|
||
|
||
|
||
def rand_str(length=32, type="lower_hex"):
|
||
"""
|
||
生成指定长度的随机字符串或者数字, 可以用于密钥等安全场景
|
||
:param length: 字符串或者数字的长度
|
||
:param type: str 代表随机字符串,num 代表随机数字
|
||
:return: 字符串
|
||
"""
|
||
if type == "str":
|
||
return get_random_string(length, allowed_chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
|
||
elif type == "lower_str":
|
||
return get_random_string(length, allowed_chars="abcdefghijklmnopqrstuvwxyz0123456789")
|
||
elif type == "lower_hex":
|
||
return random.choice("123456789abcdef") + get_random_string(length - 1, allowed_chars="0123456789abcdef")
|
||
else:
|
||
return random.choice("123456789") + get_random_string(length - 1, allowed_chars="0123456789")
|
||
|
||
|
||
def strip_class_prefix(username, class_name):
|
||
"""
|
||
去掉用户名里的 ks<班级号> 前缀,得到学生本人那一段。
|
||
用户名形如 ks251张三,class_name 为 251 时返回 张三。
|
||
|
||
用 removeprefix 而不是按长度切片:前缀对不上时原样返回,
|
||
不会从中间截出乱码。
|
||
"""
|
||
if not class_name:
|
||
return username
|
||
return username.removeprefix(f"ks{class_name}")
|
||
|
||
|
||
def datetime2str(value, format="iso-8601"):
|
||
if format.lower() == "iso-8601":
|
||
value = value.isoformat()
|
||
if value.endswith("+00:00"):
|
||
value = value[:-6] + "Z"
|
||
return value
|
||
return value.strftime(format)
|
||
|
||
|
||
def natural_sort_key(s, _nsre=re.compile(r"(\d+)")):
|
||
return [int(text) if text.isdigit() else text.lower() for text in re.split(_nsre, s)]
|
||
|
||
|
||
def get_env(name, default=""):
|
||
return os.environ.get(name, default)
|
||
|
||
|
||
def DRAMATIQ_WORKER_ARGS(time_limit=3600_000, max_retries=0, max_age=7200_000):
|
||
return {"max_retries": max_retries, "time_limit": time_limit, "max_age": max_age}
|
||
|
||
|
||
def check_is_id(value):
|
||
try:
|
||
return int(value) > 0
|
||
except Exception:
|
||
return False
|