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:
2026-08-05 23:05:50 -06:00
parent 989b33d827
commit 8557f8a5f5
5 changed files with 30 additions and 50 deletions

View File

@@ -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"]: