DRM / storage.py
Codex
Refactor admin pages and home cards
cbd13dc
import json
import threading
import uuid
from copy import deepcopy
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo
TZ = ZoneInfo("Asia/Shanghai")
DEFAULT_SCHEDULE_SETTINGS = {
"semester_start": "2026-03-09",
"day_start": "08:15",
"day_end": "23:00",
"default_task_duration_minutes": 45,
}
COURSE_COLOR_PALETTE = [
"#64d7f5",
"#7ee082",
"#ffbf69",
"#ff8a80",
"#b197fc",
"#83c5be",
]
DEFAULT_COURSE_SEED_VERSION = "2026-spring-v1"
DEFAULT_PERIOD_TIMES = {
1: ("08:15", "09:00"),
2: ("09:10", "09:55"),
3: ("10:15", "11:00"),
4: ("11:10", "11:55"),
5: ("13:50", "14:35"),
6: ("14:45", "15:30"),
7: ("15:40", "16:25"),
8: ("16:45", "17:30"),
9: ("17:40", "18:25"),
10: ("19:20", "20:05"),
11: ("20:15", "21:00"),
12: ("21:10", "21:55"),
}
def build_default_time_slots() -> list[dict[str, str]]:
slots: list[dict[str, str]] = []
for index, (start, end) in DEFAULT_PERIOD_TIMES.items():
slots.append(
{
"label": f"第{index:02d}节课",
"start": start,
"end": end,
}
)
return slots
DEFAULT_SCHEDULE_SETTINGS["time_slots"] = build_default_time_slots()
DEFAULT_IMPORTED_COURSES = [
{
"title": "形势与政策-2_20",
"day_of_week": 7,
"start_period": 10,
"end_period": 11,
"start_week": 9,
"end_week": 15,
"week_pattern": "odd",
"location": "江安综合楼C座C407",
"color": "#E0B100",
},
{
"title": "数字逻辑:应用与设计_09",
"day_of_week": 1,
"start_period": 5,
"end_period": 7,
"start_week": 1,
"end_week": 16,
"week_pattern": "all",
"location": "江安一教A座A412",
"color": "#FF5AA5",
},
{
"title": "中国近现代史纲要_59",
"day_of_week": 1,
"start_period": 10,
"end_period": 12,
"start_week": 1,
"end_week": 16,
"week_pattern": "all",
"location": "江安综合楼C座C403",
"color": "#66BB6A",
},
{
"title": "人工智能导论_666",
"day_of_week": 2,
"start_period": 1,
"end_period": 2,
"start_week": 1,
"end_week": 16,
"week_pattern": "all",
"location": "江安一教B座B201",
"color": "#C77400",
},
{
"title": "微积分(I)-2_33",
"day_of_week": 2,
"start_period": 3,
"end_period": 4,
"start_week": 1,
"end_week": 16,
"week_pattern": "all",
"location": "江安一教B座B101",
"color": "#DD8E88",
},
{
"title": "体育-2游泳_12",
"day_of_week": 2,
"start_period": 5,
"end_period": 6,
"start_week": 1,
"end_week": 12,
"week_pattern": "all",
"location": "江安未来游泳馆",
"color": "#717171",
},
{
"title": "城市经济学_03",
"day_of_week": 2,
"start_period": 8,
"end_period": 9,
"start_week": 1,
"end_week": 16,
"week_pattern": "all",
"location": "江安一教A座A308",
"color": "#F6AD9A",
},
{
"title": "新中国史_02",
"day_of_week": 2,
"start_period": 10,
"end_period": 12,
"start_week": 1,
"end_week": 11,
"week_pattern": "all",
"location": "江安综合楼C座C407",
"color": "#FF9A6A",
},
{
"title": "通用英语 I-2_49",
"day_of_week": 3,
"start_period": 1,
"end_period": 2,
"start_week": 1,
"end_week": 16,
"week_pattern": "all",
"location": "江安二基楼B座B409",
"color": "#55C08D",
},
{
"title": "线性代数(理工)_35",
"day_of_week": 3,
"start_period": 3,
"end_period": 4,
"start_week": 1,
"end_week": 16,
"week_pattern": "all",
"location": "江安综合楼C座C303",
"color": "#9A6EAB",
},
{
"title": "微积分(I)-2_33",
"day_of_week": 4,
"start_period": 1,
"end_period": 3,
"start_week": 1,
"end_week": 16,
"week_pattern": "all",
"location": "江安一教B座B101",
"color": "#DD8E88",
},
{
"title": "面向对象程序设计(Java篇)_03",
"day_of_week": 4,
"start_period": 5,
"end_period": 8,
"start_week": 1,
"end_week": 13,
"week_pattern": "all",
"location": "江安综合楼B座B205",
"color": "#C99B89",
},
{
"title": "深度学习_01",
"day_of_week": 4,
"start_period": 10,
"end_period": 12,
"start_week": 6,
"end_week": 16,
"week_pattern": "all",
"location": "江安一教A座A207",
"color": "#FA8D92",
},
{
"title": "大学物理(理工)III-1_09",
"day_of_week": 5,
"start_period": 1,
"end_period": 2,
"start_week": 1,
"end_week": 16,
"week_pattern": "all",
"location": "江安一教B座B401",
"color": "#8A860C",
},
{
"title": "线性代数(理工)_35",
"day_of_week": 5,
"start_period": 3,
"end_period": 4,
"start_week": 1,
"end_week": 16,
"week_pattern": "all",
"location": "江安一教B座B301",
"color": "#9A6EAB",
},
{
"title": "线性代数习题课_35",
"day_of_week": 6,
"start_period": 7,
"end_period": 8,
"start_week": 2,
"end_week": 16,
"week_pattern": "all",
"location": "江安一教B座B104",
"color": "#F0A794",
},
]
def beijing_now() -> datetime:
return datetime.now(TZ)
def iso_now() -> str:
return beijing_now().isoformat()
def make_id(prefix: str) -> str:
return f"{prefix}_{uuid.uuid4().hex[:10]}"
def build_default_courses() -> list[dict[str, Any]]:
created_at = iso_now()
courses: list[dict[str, Any]] = []
for index, item in enumerate(DEFAULT_IMPORTED_COURSES):
start_time = DEFAULT_PERIOD_TIMES[item["start_period"]][0]
end_time = DEFAULT_PERIOD_TIMES[item["end_period"]][1]
courses.append(
{
"id": make_id("course"),
"title": item["title"],
"day_of_week": item["day_of_week"],
"start_time": start_time,
"end_time": end_time,
"start_week": item["start_week"],
"end_week": item["end_week"],
"week_pattern": item["week_pattern"],
"location": item["location"],
"color": item.get("color") or COURSE_COLOR_PALETTE[index % len(COURSE_COLOR_PALETTE)],
"created_at": created_at,
}
)
return courses
class ReminderStore:
def __init__(self, path: Path):
self.path = path
self.lock = threading.Lock()
self.path.parent.mkdir(parents=True, exist_ok=True)
if not self.path.exists():
self._write(self._seed_data())
else:
self._ensure_schema()
def _seed_data(self) -> dict[str, Any]:
now = beijing_now().replace(minute=0, second=0, microsecond=0)
def task(title: str, hours_from_now: int) -> dict[str, Any]:
created = now - timedelta(hours=max(hours_from_now - 4, 1))
due = now + timedelta(hours=hours_from_now)
return {
"id": make_id("task"),
"title": title,
"created_at": created.isoformat(),
"due_at": due.isoformat(),
"completed": False,
"completed_at": None,
"schedule": None,
}
return {
"categories": [
{
"id": make_id("cat"),
"name": "今日节奏",
"created_at": iso_now(),
"tasks": [
task("整理今天的重点任务", 10),
task("晚间复盘 15 分钟", 14),
],
},
{
"id": make_id("cat"),
"name": "学习推进",
"created_at": iso_now(),
"tasks": [
task("完成一节课程并记笔记", 26),
],
},
{
"id": make_id("cat"),
"name": "生活安排",
"created_at": iso_now(),
"tasks": [
task("补充下周需要采购的清单", 40),
],
},
],
"courses": build_default_courses(),
"course_seed_version": DEFAULT_COURSE_SEED_VERSION,
"schedule_settings": deepcopy(DEFAULT_SCHEDULE_SETTINGS),
}
def _normalize_data(self, data: dict[str, Any]) -> tuple[dict[str, Any], bool]:
changed = False
categories = data.setdefault("categories", [])
courses = data.setdefault("courses", [])
settings = data.setdefault("schedule_settings", {})
course_seed_version = data.get("course_seed_version")
for key, value in DEFAULT_SCHEDULE_SETTINGS.items():
if key not in settings:
settings[key] = deepcopy(value)
changed = True
time_slots = settings.get("time_slots")
if not isinstance(time_slots, list) or not time_slots:
settings["time_slots"] = build_default_time_slots()
changed = True
else:
normalized_time_slots = []
for index, slot in enumerate(time_slots, start=1):
normalized_time_slots.append(
{
"label": str(slot.get("label", "")).strip() or f"第{index:02d}节课",
"start": str(slot.get("start", DEFAULT_PERIOD_TIMES.get(index, ("08:15", "09:00"))[0])),
"end": str(slot.get("end", DEFAULT_PERIOD_TIMES.get(index, ("08:15", "09:00"))[1])),
}
)
if normalized_time_slots != time_slots:
settings["time_slots"] = normalized_time_slots
changed = True
if course_seed_version != DEFAULT_COURSE_SEED_VERSION:
if not courses:
data["courses"] = build_default_courses()
courses = data["courses"]
data["course_seed_version"] = DEFAULT_COURSE_SEED_VERSION
changed = True
for category in categories:
if "created_at" not in category:
category["created_at"] = iso_now()
changed = True
tasks = category.setdefault("tasks", [])
for task in tasks:
if "completed" not in task:
task["completed"] = False
changed = True
if "completed_at" not in task:
task["completed_at"] = None
changed = True
if "schedule" not in task:
task["schedule"] = None
changed = True
for index, course in enumerate(courses):
if "created_at" not in course:
course["created_at"] = iso_now()
changed = True
if "week_pattern" not in course:
course["week_pattern"] = "all"
changed = True
if "location" not in course:
course["location"] = ""
changed = True
if "color" not in course:
course["color"] = COURSE_COLOR_PALETTE[index % len(COURSE_COLOR_PALETTE)]
changed = True
return data, changed
def _ensure_schema(self) -> None:
with self.lock:
data = self._read()
normalized, changed = self._normalize_data(data)
if changed:
self._write(normalized)
def _read(self) -> dict[str, Any]:
with self.path.open("r", encoding="utf-8") as file:
return json.load(file)
def _write(self, data: dict[str, Any]) -> None:
temp_path = self.path.with_suffix(".tmp")
with temp_path.open("w", encoding="utf-8") as file:
json.dump(data, file, ensure_ascii=False, indent=2)
temp_path.replace(self.path)
def snapshot(self) -> dict[str, Any]:
with self.lock:
data = self._read()
normalized, changed = self._normalize_data(data)
if changed:
self._write(normalized)
return deepcopy(normalized)
def list_categories(self) -> list[dict[str, Any]]:
data = self.snapshot()
categories = data.get("categories", [])
for category in categories:
category["tasks"] = sorted(
category.get("tasks", []),
key=lambda item: (
item.get("completed", False),
item.get("due_at", ""),
item.get("created_at", ""),
),
)
return categories
def list_tasks(self) -> list[dict[str, Any]]:
data = self.snapshot()
flattened: list[dict[str, Any]] = []
for category in data.get("categories", []):
for task in category.get("tasks", []):
task_copy = deepcopy(task)
task_copy["category_id"] = category["id"]
task_copy["category_name"] = category["name"]
flattened.append(task_copy)
return sorted(
flattened,
key=lambda task: (
task.get("completed", False),
task.get("due_at", ""),
task.get("created_at", ""),
),
)
def list_courses(self) -> list[dict[str, Any]]:
data = self.snapshot()
return sorted(
data.get("courses", []),
key=lambda course: (
course.get("day_of_week", 1),
course.get("start_time", ""),
course.get("start_week", 1),
),
)
def get_schedule_settings(self) -> dict[str, Any]:
data = self.snapshot()
return deepcopy(data.get("schedule_settings", DEFAULT_SCHEDULE_SETTINGS))
def update_schedule_settings(self, payload: dict[str, Any]) -> dict[str, Any]:
with self.lock:
data = self._read()
settings = data.setdefault("schedule_settings", deepcopy(DEFAULT_SCHEDULE_SETTINGS))
for key, value in payload.items():
settings[key] = value
self._write(data)
return deepcopy(settings)
def create_category(self, name: str) -> dict[str, Any]:
with self.lock:
data = self._read()
category = {
"id": make_id("cat"),
"name": name,
"created_at": iso_now(),
"tasks": [],
}
data.setdefault("categories", []).append(category)
self._write(data)
return deepcopy(category)
def rename_category(self, category_id: str, name: str) -> dict[str, Any]:
with self.lock:
data = self._read()
for category in data.get("categories", []):
if category["id"] == category_id:
category["name"] = name
self._write(data)
return deepcopy(category)
raise KeyError("Category not found")
def delete_category(self, category_id: str) -> None:
with self.lock:
data = self._read()
original_count = len(data.get("categories", []))
data["categories"] = [
category
for category in data.get("categories", [])
if category["id"] != category_id
]
if len(data["categories"]) == original_count:
raise KeyError("Category not found")
self._write(data)
def add_task(self, category_id: str, title: str, due_at: str) -> dict[str, Any]:
with self.lock:
data = self._read()
for category in data.get("categories", []):
if category["id"] == category_id:
item = {
"id": make_id("task"),
"title": title,
"created_at": iso_now(),
"due_at": due_at,
"completed": False,
"completed_at": None,
"schedule": None,
}
category.setdefault("tasks", []).append(item)
self._write(data)
return deepcopy(item)
raise KeyError("Category not found")
def toggle_task(self, task_id: str, completed: bool) -> dict[str, Any]:
with self.lock:
data = self._read()
for category in data.get("categories", []):
for task in category.get("tasks", []):
if task["id"] == task_id:
task["completed"] = completed
task["completed_at"] = iso_now() if completed else None
self._write(data)
return deepcopy(task)
raise KeyError("Task not found")
def delete_task(self, task_id: str) -> None:
with self.lock:
data = self._read()
for category in data.get("categories", []):
original_count = len(category.get("tasks", []))
category["tasks"] = [
task for task in category.get("tasks", []) if task["id"] != task_id
]
if len(category["tasks"]) != original_count:
self._write(data)
return
raise KeyError("Task not found")
def schedule_task(self, task_id: str, schedule: dict[str, Any] | None) -> dict[str, Any]:
with self.lock:
data = self._read()
for category in data.get("categories", []):
for task in category.get("tasks", []):
if task["id"] == task_id:
task["schedule"] = deepcopy(schedule) if schedule else None
self._write(data)
task_copy = deepcopy(task)
task_copy["category_id"] = category["id"]
task_copy["category_name"] = category["name"]
return task_copy
raise KeyError("Task not found")
def create_course(self, payload: dict[str, Any]) -> dict[str, Any]:
with self.lock:
data = self._read()
courses = data.setdefault("courses", [])
course = {
"id": make_id("course"),
"title": payload["title"],
"day_of_week": payload["day_of_week"],
"start_time": payload["start_time"],
"end_time": payload["end_time"],
"start_week": payload["start_week"],
"end_week": payload["end_week"],
"week_pattern": payload.get("week_pattern", "all"),
"location": payload.get("location", ""),
"color": payload.get(
"color",
COURSE_COLOR_PALETTE[len(courses) % len(COURSE_COLOR_PALETTE)],
),
"created_at": iso_now(),
}
courses.append(course)
self._write(data)
return deepcopy(course)
def update_course(self, course_id: str, payload: dict[str, Any]) -> dict[str, Any]:
with self.lock:
data = self._read()
for course in data.get("courses", []):
if course["id"] == course_id:
course.update(payload)
self._write(data)
return deepcopy(course)
raise KeyError("Course not found")
def delete_course(self, course_id: str) -> None:
with self.lock:
data = self._read()
original_count = len(data.get("courses", []))
data["courses"] = [
course
for course in data.get("courses", [])
if course["id"] != course_id
]
if len(data["courses"]) == original_count:
raise KeyError("Course not found")
self._write(data)