Files
OnlineJudge/utils/shortcuts.py
yuetsh e42cfac3ce feat: 班级号位数不合法直接报错,规则收敛到一处
原先班级号位数不对是静默处理的:get_class_name 猜前几位,class_list
根本不校验,填错只表现为登录页班级下拉查不到人。现在两个入口都报错。

- get_class_name 先用 \d+ 抓全再判位数,不合法抛 APIError。不能直接用
  CLASS_NAME_RE 匹配,那样 ks251001 会"匹配成功"并悄悄取前 4 位,正是
  要避免的猜测
- CreateEditWebsiteConfigSerializer 加 validate_class_list
- 位数规则收敛到 utils/shortcuts 的 CLASS_NAME_MIN/MAX_DIGITS 和
  is_valid_class_name,两个调用方和错误文案都从常量拼

不以 ks+数字 开头的用户名(管理员、教师账号、kstest)仍返回 None 不报错,
只校验"看起来想当班级用户名"的。

批量导入的构造循环在 transaction.atomic() 之前,报错时一条都不入库。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 00:15:47 -06:00

91 lines
3.0 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.
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 build_query_string(kv_data, ignore_none=True):
# {"a": 1, "b": "test"} -> "?a=1&b=test"
query_string = ""
for k, v in kv_data.items():
if ignore_none is True and kv_data[k] is None:
continue
if query_string != "":
query_string += "&"
else:
query_string = "?"
query_string += k + "=" + str(v)
return query_string
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