refactor: python-dateutil 换成标准库
- contest 后台创建/编辑:dateutil.parser.parse 换成 datetime.fromisoformat。serializer 里 start_time/end_time 声明的是 DateTimeField,而 validate_serializer 做的是 request.data = s.data, 即重新序列化一遍,所以视图拿到的必定是 DRF 规范化后的 ISO 8601, dateutil 的宽容解析能力在这里从来没用上 - AI 学情统计:relativedelta 换成 shift_months 辅助函数,复刻月末 收缩语义(1月31日 + 1个月 = 2月28/29日)。_parse_duration 的配置 由 delta 对象改为 rewind/advance 可调用,周档和月档统一 删除依赖:python-dateutil、six Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import calendar
|
||||
import hashlib
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Count, Min
|
||||
from django.db.models.functions import TruncDate
|
||||
@@ -47,6 +47,15 @@ GRADE_WEIGHTS = {"S": 4, "A": 3, "B": 2, "C": 1}
|
||||
AVERAGE_GRADE_THRESHOLDS = [(3.5, "S"), (2.5, "A"), (1.5, "B")]
|
||||
|
||||
|
||||
def shift_months(dt, months):
|
||||
"""按月平移。落到不存在的日期时收缩到当月最后一天(1月31日 + 1个月 = 2月28/29日)"""
|
||||
month_index = dt.month - 1 + months
|
||||
year = dt.year + month_index // 12
|
||||
month = month_index % 12 + 1
|
||||
day = min(dt.day, calendar.monthrange(year, month)[1])
|
||||
return dt.replace(year=year, month=month, day=day)
|
||||
|
||||
|
||||
def get_cache_key(prefix, *args):
|
||||
return hashlib.md5(f"{prefix}:{'_'.join(map(str, args))}".encode()).hexdigest()
|
||||
|
||||
@@ -429,12 +438,12 @@ class AIDurationDataAPI(APIView):
|
||||
class_user_ids = get_class_user_ids(user)
|
||||
use_class_scope = bool(user.class_name) and len(class_user_ids) > 1
|
||||
time_config = self._parse_duration(duration)
|
||||
start = datetime.fromisoformat(end_iso) - time_config["total_delta"]
|
||||
start = time_config["rewind"](datetime.fromisoformat(end_iso))
|
||||
|
||||
duration_data = []
|
||||
for i in range(time_config["show_count"]):
|
||||
start = start + time_config["delta"]
|
||||
period_end = start + time_config["delta"]
|
||||
start = time_config["advance"](start)
|
||||
period_end = time_config["advance"](start)
|
||||
|
||||
submission_count = Submission.objects.filter(user_id=user.id, create_time__gte=start, create_time__lte=period_end).count()
|
||||
|
||||
@@ -490,24 +499,26 @@ class AIDurationDataAPI(APIView):
|
||||
unit, count = duration.split(":")
|
||||
count = int(count)
|
||||
|
||||
# rewind 把结束时间倒推到区间起点,advance 前进一格。
|
||||
# 按月的档位不能用 timedelta(月长不固定),用 shift_months 处理
|
||||
configs = {
|
||||
("months", 2): {
|
||||
"show_count": 8,
|
||||
"show_unit": "weeks",
|
||||
"total_delta": timedelta(weeks=9),
|
||||
"delta": timedelta(weeks=1),
|
||||
"rewind": lambda dt: dt - timedelta(weeks=9),
|
||||
"advance": lambda dt: dt + timedelta(weeks=1),
|
||||
},
|
||||
("months", 6): {
|
||||
"show_count": 6,
|
||||
"show_unit": "months",
|
||||
"total_delta": relativedelta(months=7),
|
||||
"delta": relativedelta(months=1),
|
||||
"rewind": lambda dt: shift_months(dt, -7),
|
||||
"advance": lambda dt: shift_months(dt, 1),
|
||||
},
|
||||
("years", 1): {
|
||||
"show_count": 12,
|
||||
"show_unit": "months",
|
||||
"total_delta": relativedelta(months=13),
|
||||
"delta": relativedelta(months=1),
|
||||
"rewind": lambda dt: shift_months(dt, -13),
|
||||
"advance": lambda dt: shift_months(dt, 1),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -516,8 +527,8 @@ class AIDurationDataAPI(APIView):
|
||||
{
|
||||
"show_count": 4,
|
||||
"show_unit": "weeks",
|
||||
"total_delta": timedelta(weeks=5),
|
||||
"delta": timedelta(weeks=1),
|
||||
"rewind": lambda dt: dt - timedelta(weeks=5),
|
||||
"advance": lambda dt: dt + timedelta(weeks=1),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import copy
|
||||
import os
|
||||
import zipfile
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from ipaddress import ip_network
|
||||
|
||||
import dateutil.parser
|
||||
from django.http import FileResponse
|
||||
from django.utils.timezone import now
|
||||
|
||||
@@ -34,8 +33,9 @@ class ContestAPI(APIView):
|
||||
@teacher_admin_required
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
data["start_time"] = dateutil.parser.parse(data["start_time"])
|
||||
data["end_time"] = dateutil.parser.parse(data["end_time"])
|
||||
# DRF 的 DateTimeField 已经校验并规范化过,这里拿到的必定是 ISO 8601 字符串
|
||||
data["start_time"] = datetime.fromisoformat(data["start_time"])
|
||||
data["end_time"] = datetime.fromisoformat(data["end_time"])
|
||||
data["created_by"] = request.user
|
||||
if data["end_time"] <= data["start_time"]:
|
||||
return self.error("Start time must occur earlier than end time")
|
||||
@@ -58,8 +58,9 @@ class ContestAPI(APIView):
|
||||
except Contest.DoesNotExist:
|
||||
return self.error("Contest does not exist")
|
||||
ensure_created_by(contest, request.user)
|
||||
data["start_time"] = dateutil.parser.parse(data["start_time"])
|
||||
data["end_time"] = dateutil.parser.parse(data["end_time"])
|
||||
# DRF 的 DateTimeField 已经校验并规范化过,这里拿到的必定是 ISO 8601 字符串
|
||||
data["start_time"] = datetime.fromisoformat(data["start_time"])
|
||||
data["end_time"] = datetime.fromisoformat(data["end_time"])
|
||||
if data["end_time"] <= data["start_time"]:
|
||||
return self.error("Start time must occur earlier than end time")
|
||||
if not data["password"]:
|
||||
|
||||
@@ -427,10 +427,6 @@ pydantic-core==2.46.3 \
|
||||
--hash=sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127 \
|
||||
--hash=sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56
|
||||
# via pydantic
|
||||
python-dateutil==2.9.0.post0 \
|
||||
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
|
||||
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
|
||||
# via onlinejudge
|
||||
python-dotenv==1.2.2 \
|
||||
--hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \
|
||||
--hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3
|
||||
@@ -505,10 +501,6 @@ sentry-sdk==2.59.0 \
|
||||
--hash=sha256:abcf65ee9a9d9cdebf9ad369782408ecca9c1c792686ef06ba34f5ab233527fe \
|
||||
--hash=sha256:cd265808ef8bf3f3edf69b527c0a0b2b6b1322762679e55b8987db2e9584aec1
|
||||
# via onlinejudge
|
||||
six==1.17.0 \
|
||||
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
|
||||
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
|
||||
# via python-dateutil
|
||||
sniffio==1.3.1 \
|
||||
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
|
||||
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
|
||||
|
||||
@@ -19,7 +19,6 @@ dependencies = [
|
||||
"pillow>=12.2.0,<13",
|
||||
"psycopg>=3.3.4,<4",
|
||||
"psycopg-binary>=3.3.4,<4",
|
||||
"python-dateutil>=2.9.0.post0,<3",
|
||||
"sentry-sdk[django]>=2.0.0,<3",
|
||||
"tree-sitter>=0.25.2",
|
||||
"tree-sitter-c>=0.24.2",
|
||||
|
||||
23
uv.lock
generated
23
uv.lock
generated
@@ -397,7 +397,6 @@ dependencies = [
|
||||
{ name = "pillow" },
|
||||
{ name = "psycopg" },
|
||||
{ name = "psycopg-binary" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "sentry-sdk", extra = ["django"] },
|
||||
{ name = "tree-sitter" },
|
||||
{ name = "tree-sitter-c" },
|
||||
@@ -428,7 +427,6 @@ requires-dist = [
|
||||
{ name = "pillow", specifier = ">=12.2.0,<13" },
|
||||
{ name = "psycopg", specifier = ">=3.3.4,<4" },
|
||||
{ name = "psycopg-binary", specifier = ">=3.3.4,<4" },
|
||||
{ name = "python-dateutil", specifier = ">=2.9.0.post0,<3" },
|
||||
{ name = "sentry-sdk", extras = ["django"], specifier = ">=2.0.0,<3" },
|
||||
{ name = "tree-sitter", specifier = ">=0.25.2" },
|
||||
{ name = "tree-sitter-c", specifier = ">=0.24.2" },
|
||||
@@ -680,18 +678,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.2"
|
||||
@@ -799,15 +785,6 @@ django = [
|
||||
{ name = "django" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
|
||||
Reference in New Issue
Block a user