DRM / app.py
Codex
Polish admin tools and planner sidebar
d3b98cb
import hmac
import os
from datetime import date, datetime, timedelta
from pathlib import Path
from urllib.parse import urlparse
from zoneinfo import ZoneInfo
from flask import Flask, jsonify, redirect, render_template, request, session, url_for
from storage import DEFAULT_PERIOD_TIMES, ReminderStore, beijing_now, build_default_time_slots
BASE_DIR = Path(__file__).resolve().parent
STORE_PATH = BASE_DIR / "data" / "store.json"
TZ = ZoneInfo("Asia/Shanghai")
DEFAULT_PASSWORD = "123456"
WEEK_PATTERNS = {"all", "odd", "even"}
CLASS_PERIODS = [
{"label": "第01节课", "start": "08:15", "end": "09:00", "major": "第一大节"},
{"label": "第02节课", "start": "09:10", "end": "09:55", "major": "第一大节"},
{"label": "第03节课", "start": "10:15", "end": "11:00", "major": "第二大节"},
{"label": "第04节课", "start": "11:10", "end": "11:55", "major": "第二大节"},
{"label": "第05节课", "start": "13:50", "end": "14:35", "major": "第三大节"},
{"label": "第06节课", "start": "14:45", "end": "15:30", "major": "第三大节"},
{"label": "第07节课", "start": "15:40", "end": "16:25", "major": "第三大节"},
{"label": "第08节课", "start": "16:45", "end": "17:30", "major": "第四大节"},
{"label": "第09节课", "start": "17:40", "end": "18:25", "major": "第四大节"},
{"label": "第10节课", "start": "19:20", "end": "20:05", "major": "第五大节"},
{"label": "第11节课", "start": "20:15", "end": "21:00", "major": "第五大节"},
{"label": "第12节课", "start": "21:10", "end": "21:55", "major": "第五大节"},
]
MAJOR_BLOCKS = [
{"label": "第一大节", "start": "08:15", "end": "09:55"},
{"label": "第二大节", "start": "10:15", "end": "11:55"},
{"label": "第三大节", "start": "13:50", "end": "16:25"},
{"label": "第四大节", "start": "16:45", "end": "18:25"},
{"label": "第五大节", "start": "19:20", "end": "21:55"},
]
app = Flask(__name__)
app.config["JSON_AS_ASCII"] = False
app.secret_key = os.getenv("SECRET_KEY", "daily-reminder-master-secret")
store = ReminderStore(STORE_PATH)
MSG_LOGIN_REQUIRED = "请先登录"
MSG_WRONG_PASSWORD = "密码不正确"
MSG_ENTER_DUE = "请输入截止日期"
MSG_DUE_AFTER_NOW = "截止日期需要晚于当前北京时间"
MSG_CATEGORY_NAME = "分类名称至少 2 个字符"
MSG_TASK_NAME = "任务名称至少 2 个字符"
MSG_CATEGORY_NOT_FOUND = "分类不存在"
MSG_TASK_NOT_FOUND = "任务不存在"
MSG_COURSE_NAME = "课程名称至少 2 个字符"
MSG_COURSE_NOT_FOUND = "课程不存在"
MSG_INVALID_DATE = "日期格式不正确"
MSG_INVALID_TIME = "时间格式不正确"
MSG_INVALID_TIME_RANGE = "请设置有效的时间区间"
MSG_INVALID_WEEKDAY = "星期设置不正确"
MSG_INVALID_WEEK_RANGE = "开始周数不能晚于结束周数"
MSG_INVALID_WEEK_PATTERN = "单双周设置不正确"
MSG_INVALID_DURATION = "默认任务时长需在 30 到 240 分钟之间"
MSG_SCHEDULE_CONFLICT = "该时间段已有课程或任务,请换一个时间"
WEEKDAYS = [
"星期一",
"星期二",
"星期三",
"星期四",
"星期五",
"星期六",
"星期日",
]
WEEKDAY_SHORT = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
MAJOR_BLOCK_GROUPS = [
("第一大节", 0, 2),
("第二大节", 2, 4),
("第三大节", 4, 7),
("第四大节", 7, 9),
("第五大节", 9, 12),
]
def get_password() -> str:
return os.getenv("PASSWORD", DEFAULT_PASSWORD)
def is_authed() -> bool:
return bool(session.get("authenticated"))
def safe_next_path(raw_next: str | None) -> str:
if not raw_next:
return url_for("index")
parsed = urlparse(raw_next)
if parsed.scheme or parsed.netloc:
return url_for("index")
if not raw_next.startswith("/"):
return url_for("index")
return raw_next
def require_auth():
if not is_authed():
return jsonify({"ok": False, "error": MSG_LOGIN_REQUIRED}), 401
return None
def parse_due_at(raw_value: str) -> datetime:
if not raw_value:
raise ValueError(MSG_ENTER_DUE)
normalized = raw_value.strip()
if normalized.endswith("Z"):
normalized = normalized[:-1] + "+00:00"
due_at = datetime.fromisoformat(normalized)
if due_at.tzinfo is None:
due_at = due_at.replace(tzinfo=TZ)
due_at = due_at.astimezone(TZ).replace(minute=0, second=0, microsecond=0)
if due_at <= beijing_now():
raise ValueError(MSG_DUE_AFTER_NOW)
return due_at
def parse_iso_date(raw_value: str | None) -> date:
if not raw_value:
return beijing_now().date()
try:
return date.fromisoformat(str(raw_value))
except ValueError as exc:
raise ValueError(MSG_INVALID_DATE) from exc
def parse_time_to_minutes(raw_value: str) -> int:
try:
hour_text, minute_text = str(raw_value).split(":", 1)
hour = int(hour_text)
minute = int(minute_text)
except (TypeError, ValueError) as exc:
raise ValueError(MSG_INVALID_TIME) from exc
if not (0 <= hour <= 23 and 0 <= minute <= 59):
raise ValueError(MSG_INVALID_TIME)
return hour * 60 + minute
def minutes_to_hhmm(total_minutes: int) -> str:
hours = total_minutes // 60
minutes = total_minutes % 60
return f"{hours:02d}:{minutes:02d}"
def normalize_time_string(raw_value: str) -> str:
return minutes_to_hhmm(parse_time_to_minutes(raw_value))
def normalize_time_slots(raw_slots: list[dict]) -> list[dict[str, str]]:
if not isinstance(raw_slots, list) or not raw_slots:
raise ValueError(MSG_INVALID_TIME_RANGE)
normalized: list[dict[str, str]] = []
previous_end = None
for index, slot in enumerate(raw_slots, start=1):
label = str(slot.get("label", "")).strip() or f"第{index:02d}节课"
start = normalize_time_string(str(slot.get("start", "")).strip())
end = normalize_time_string(str(slot.get("end", "")).strip())
start_minutes = parse_time_to_minutes(start)
end_minutes = parse_time_to_minutes(end)
if start_minutes >= end_minutes:
raise ValueError(MSG_INVALID_TIME_RANGE)
if previous_end is not None and start_minutes < previous_end:
raise ValueError(MSG_INVALID_TIME_RANGE)
normalized.append(
{
"label": label,
"start": start,
"end": end,
}
)
previous_end = end_minutes
return normalized
def get_configured_time_slots(settings: dict | None = None) -> list[dict[str, str]]:
source = settings or store.get_schedule_settings()
raw_slots = source.get("time_slots") or [
{
"label": slot["label"],
"start": slot["start"],
"end": slot["end"],
}
for slot in CLASS_PERIODS
]
return normalize_time_slots(raw_slots)
def build_major_blocks_from_time_slots(time_slots: list[dict[str, str]]) -> list[dict[str, str]]:
major_blocks: list[dict[str, str]] = []
for label, start_index, end_index in MAJOR_BLOCK_GROUPS:
if len(time_slots) <= start_index:
continue
block_slots = time_slots[start_index:min(end_index, len(time_slots))]
if not block_slots:
continue
major_blocks.append(
{
"label": label,
"start": block_slots[0]["start"],
"end": block_slots[-1]["end"],
}
)
return major_blocks
def build_clean_default_time_slots() -> list[dict[str, str]]:
return [
{
"label": f"第{index:02d}节课",
"start": start,
"end": end,
}
for index, (start, end) in sorted(DEFAULT_PERIOD_TIMES.items())
]
def compute_task_progress(task: dict) -> float:
if task.get("completed"):
return 100.0
created_at = datetime.fromisoformat(task["created_at"])
due_at = datetime.fromisoformat(task["due_at"])
total_seconds = max((due_at - created_at).total_seconds(), 1)
elapsed_seconds = max((beijing_now() - created_at).total_seconds(), 0)
progress = max(0.0, min(100.0, elapsed_seconds / total_seconds * 100))
return round(progress, 1)
def current_date_payload() -> dict[str, str]:
now = beijing_now()
return {
"iso": now.isoformat(),
"weekday": WEEKDAYS[now.weekday()],
}
def serialize_task(task: dict) -> dict:
return {
**task,
"progress_percent": compute_task_progress(task),
}
def parse_course_payload(payload: dict) -> dict:
title = str(payload.get("title", "")).strip()
if len(title) < 2:
raise ValueError(MSG_COURSE_NAME)
try:
day_of_week = int(payload.get("day_of_week"))
start_week = int(payload.get("start_week"))
end_week = int(payload.get("end_week"))
except (TypeError, ValueError) as exc:
raise ValueError(MSG_INVALID_WEEKDAY) from exc
if day_of_week < 1 or day_of_week > 7:
raise ValueError(MSG_INVALID_WEEKDAY)
if start_week < 1 or end_week < 1 or start_week > end_week:
raise ValueError(MSG_INVALID_WEEK_RANGE)
week_pattern = str(payload.get("week_pattern", "all")).strip() or "all"
if week_pattern not in WEEK_PATTERNS:
raise ValueError(MSG_INVALID_WEEK_PATTERN)
start_time = normalize_time_string(str(payload.get("start_time", "")).strip())
end_time = normalize_time_string(str(payload.get("end_time", "")).strip())
if parse_time_to_minutes(start_time) >= parse_time_to_minutes(end_time):
raise ValueError(MSG_INVALID_TIME_RANGE)
return {
"title": title,
"day_of_week": day_of_week,
"start_time": start_time,
"end_time": end_time,
"start_week": start_week,
"end_week": end_week,
"week_pattern": week_pattern,
"location": str(payload.get("location", "")).strip(),
"color": str(payload.get("color", "")).strip() or None,
}
def parse_schedule_settings(payload: dict) -> dict:
semester_start = parse_iso_date(payload.get("semester_start")).isoformat()
day_start = normalize_time_string(str(payload.get("day_start", "")).strip())
day_end = normalize_time_string(str(payload.get("day_end", "")).strip())
if parse_time_to_minutes(day_start) >= parse_time_to_minutes(day_end):
raise ValueError(MSG_INVALID_TIME_RANGE)
try:
default_duration = int(payload.get("default_task_duration_minutes"))
except (TypeError, ValueError) as exc:
raise ValueError(MSG_INVALID_DURATION) from exc
if default_duration < 30 or default_duration > 240:
raise ValueError(MSG_INVALID_DURATION)
time_slots = normalize_time_slots(payload.get("time_slots") or get_configured_time_slots())
return {
"semester_start": semester_start,
"day_start": day_start,
"day_end": day_end,
"default_task_duration_minutes": default_duration,
"time_slots": time_slots,
}
def parse_task_schedule_payload(payload: dict, settings: dict) -> dict | None:
if payload.get("clear"):
return None
schedule_date = parse_iso_date(payload.get("date")).isoformat()
start_time = normalize_time_string(str(payload.get("start_time", "")).strip())
end_time = normalize_time_string(str(payload.get("end_time", "")).strip())
day_start = parse_time_to_minutes(settings["day_start"])
day_end = parse_time_to_minutes(settings["day_end"])
start_minutes = parse_time_to_minutes(start_time)
end_minutes = parse_time_to_minutes(end_time)
if not (day_start <= start_minutes < end_minutes <= day_end):
raise ValueError(MSG_INVALID_TIME_RANGE)
return {
"date": schedule_date,
"start_time": start_time,
"end_time": end_time,
}
def get_week_start(target_date: date) -> date:
return target_date - timedelta(days=target_date.isoweekday() - 1)
def get_academic_week(target_date: date, semester_start: date) -> int:
delta_days = (target_date - semester_start).days
return (delta_days // 7) + 1 if delta_days >= 0 else 0
def course_occurs_on(course: dict, current_week: int, selected_date: date) -> bool:
if current_week < 1:
return False
if course["day_of_week"] != selected_date.isoweekday():
return False
if current_week < course["start_week"] or current_week > course["end_week"]:
return False
if course["week_pattern"] == "odd" and current_week % 2 == 0:
return False
if course["week_pattern"] == "even" and current_week % 2 == 1:
return False
return True
def schedules_overlap(
left_start_minutes: int,
left_end_minutes: int,
right_start_minutes: int,
right_end_minutes: int,
) -> bool:
return left_start_minutes < right_end_minutes and right_start_minutes < left_end_minutes
def find_schedule_conflict(task_id: str, schedule: dict | None) -> str | None:
if not schedule:
return None
schedule_date = date.fromisoformat(schedule["date"])
start_minutes = parse_time_to_minutes(schedule["start_time"])
end_minutes = parse_time_to_minutes(schedule["end_time"])
settings = store.get_schedule_settings()
semester_start = date.fromisoformat(settings["semester_start"])
current_week = get_academic_week(schedule_date, semester_start)
for task in store.list_tasks():
if task["id"] == task_id:
continue
existing_schedule = task.get("schedule")
if not existing_schedule or existing_schedule.get("date") != schedule["date"]:
continue
existing_start = parse_time_to_minutes(existing_schedule["start_time"])
existing_end = parse_time_to_minutes(existing_schedule["end_time"])
if schedules_overlap(start_minutes, end_minutes, existing_start, existing_end):
return task["title"]
for course in store.list_courses():
if not course_occurs_on(course, current_week, schedule_date):
continue
course_start = parse_time_to_minutes(course["start_time"])
course_end = parse_time_to_minutes(course["end_time"])
if schedules_overlap(start_minutes, end_minutes, course_start, course_end):
return course["title"]
return None
def build_planner_payload(selected_date: date) -> dict:
settings = store.get_schedule_settings()
time_slots = get_configured_time_slots(settings)
major_blocks = build_major_blocks_from_time_slots(time_slots)
semester_start = date.fromisoformat(settings["semester_start"])
delta_days = (selected_date - semester_start).days
academic_week = (delta_days // 7) + 1 if delta_days >= 0 else 0
task_list = [serialize_task(task) for task in store.list_tasks()]
scheduled_items: list[dict] = []
for task in task_list:
schedule = task.get("schedule")
if schedule and schedule.get("date") == selected_date.isoformat():
scheduled_items.append(
{
"id": f"planner_{task['id']}",
"kind": "task",
"task_id": task["id"],
"title": task["title"],
"category_name": task["category_name"],
"completed": task.get("completed", False),
"due_at": task["due_at"],
"progress_percent": task["progress_percent"],
"start_time": schedule["start_time"],
"end_time": schedule["end_time"],
"locked": False,
}
)
for course in store.list_courses():
if course_occurs_on(course, academic_week, selected_date):
scheduled_items.append(
{
"id": f"planner_{course['id']}",
"kind": "course",
"course_id": course["id"],
"title": course["title"],
"location": course.get("location", ""),
"start_time": course["start_time"],
"end_time": course["end_time"],
"locked": True,
"week_pattern": course["week_pattern"],
"color": course["color"],
"start_week": course["start_week"],
"end_week": course["end_week"],
}
)
scheduled_items.sort(
key=lambda item: (
item["start_time"],
item["end_time"],
item["kind"],
)
)
return {
"selected_date": selected_date.isoformat(),
"weekday": WEEKDAYS[selected_date.weekday()],
"academic_week": academic_week,
"academic_label": f"第 {academic_week} 周" if academic_week > 0 else "开学前",
"settings": settings,
"time_slots": time_slots,
"major_blocks": major_blocks,
"tasks": task_list,
"scheduled_items": scheduled_items,
}
def build_week_planner_payload(selected_date: date) -> dict:
settings = store.get_schedule_settings()
time_slots = get_configured_time_slots(settings)
major_blocks = build_major_blocks_from_time_slots(time_slots)
semester_start = date.fromisoformat(settings["semester_start"])
week_start = get_week_start(selected_date)
week_end = week_start + timedelta(days=6)
academic_week = get_academic_week(selected_date, semester_start)
task_list = [serialize_task(task) for task in store.list_tasks()]
week_days = []
week_day_set = set()
today = beijing_now().date()
for offset in range(7):
current_day = week_start + timedelta(days=offset)
iso_day = current_day.isoformat()
week_day_set.add(iso_day)
week_days.append(
{
"iso": iso_day,
"label": WEEKDAYS[current_day.weekday()],
"short_label": WEEKDAY_SHORT[current_day.weekday()],
"month_day": current_day.strftime("%m/%d"),
"day_of_month": current_day.day,
"is_today": current_day == today,
}
)
scheduled_items: list[dict] = []
for task in task_list:
schedule = task.get("schedule")
if schedule and schedule.get("date") in week_day_set:
scheduled_items.append(
{
"id": f"planner_{task['id']}",
"kind": "task",
"task_id": task["id"],
"title": task["title"],
"category_name": task["category_name"],
"completed": task.get("completed", False),
"due_at": task["due_at"],
"progress_percent": task["progress_percent"],
"date": schedule["date"],
"start_time": schedule["start_time"],
"end_time": schedule["end_time"],
"locked": False,
}
)
courses = store.list_courses()
for offset in range(7):
current_day = week_start + timedelta(days=offset)
current_week = get_academic_week(current_day, semester_start)
for course in courses:
if course_occurs_on(course, current_week, current_day):
scheduled_items.append(
{
"id": f"planner_{course['id']}_{current_day.isoformat()}",
"kind": "course",
"course_id": course["id"],
"title": course["title"],
"location": course.get("location", ""),
"date": current_day.isoformat(),
"start_time": course["start_time"],
"end_time": course["end_time"],
"locked": True,
"week_pattern": course["week_pattern"],
"color": course["color"],
"start_week": course["start_week"],
"end_week": course["end_week"],
}
)
scheduled_items.sort(
key=lambda item: (
item.get("date", ""),
item["start_time"],
item["end_time"],
item["kind"],
)
)
return {
"selected_date": selected_date.isoformat(),
"weekday": WEEKDAYS[selected_date.weekday()],
"week_start": week_start.isoformat(),
"week_end": week_end.isoformat(),
"week_days": week_days,
"week_range_label": f"{week_start.strftime('%m/%d')} - {week_end.strftime('%m/%d')}",
"academic_week": academic_week,
"academic_label": f"第 {academic_week} 周" if academic_week > 0 else "开学前",
"settings": settings,
"time_slots": time_slots,
"major_blocks": major_blocks,
"tasks": task_list,
"scheduled_items": scheduled_items,
}
@app.context_processor
def inject_globals():
return {
"authenticated": is_authed(),
}
def build_admin_tabs(active_page: str) -> list[dict[str, str | bool]]:
tabs = [
("schedule", "时间表设置", "admin_schedule"),
("lists", "清单管理", "admin_lists"),
("courses", "课程管理", "admin_courses"),
]
return [
{
"key": key,
"label": label,
"href": url_for(endpoint),
"active": key == active_page,
}
for key, label, endpoint in tabs
]
def render_admin_page(active_page: str):
if not is_authed():
return redirect(url_for("index", login="required", next=request.path))
schedule_settings = store.get_schedule_settings()
return render_template(
"admin.html",
admin_page=active_page,
admin_tabs=build_admin_tabs(active_page),
categories=store.list_categories(),
courses=store.list_courses(),
schedule_settings=schedule_settings,
default_time_slots=build_clean_default_time_slots(),
)
@app.get("/")
def index():
login_required = request.args.get("login") == "required"
next_path = safe_next_path(request.args.get("next"))
planner_payload = build_week_planner_payload(beijing_now().date())
return render_template(
"index.html",
categories=store.list_categories(),
clock_meta=current_date_payload(),
planner_payload=planner_payload,
login_required=login_required,
next_path=next_path,
)
@app.get("/admin")
def admin():
return redirect(url_for("admin_schedule"))
@app.get("/admin/schedule")
def admin_schedule():
return render_admin_page("schedule")
@app.get("/admin/lists")
def admin_lists():
return render_admin_page("lists")
@app.get("/admin/courses")
def admin_courses():
return render_admin_page("courses")
@app.get("/api/planner")
def planner():
try:
selected_date = parse_iso_date(request.args.get("date"))
except ValueError as exc:
return jsonify({"ok": False, "error": str(exc)}), 400
return jsonify({"ok": True, "planner": build_week_planner_payload(selected_date)})
@app.post("/api/login")
def login():
payload = request.get_json(silent=True) or {}
password = str(payload.get("password", ""))
next_path = safe_next_path(payload.get("next"))
if hmac.compare_digest(password, get_password()):
session["authenticated"] = True
return jsonify({"ok": True, "next": next_path})
return jsonify({"ok": False, "error": MSG_WRONG_PASSWORD}), 401
@app.post("/api/logout")
def logout():
session.clear()
return jsonify({"ok": True})
@app.post("/api/categories")
def create_category():
auth_error = require_auth()
if auth_error:
return auth_error
payload = request.get_json(silent=True) or {}
name = str(payload.get("name", "")).strip()
if len(name) < 2:
return jsonify({"ok": False, "error": MSG_CATEGORY_NAME}), 400
category = store.create_category(name)
return jsonify({"ok": True, "category": category})
@app.patch("/api/categories/<category_id>")
def rename_category(category_id: str):
auth_error = require_auth()
if auth_error:
return auth_error
payload = request.get_json(silent=True) or {}
name = str(payload.get("name", "")).strip()
if len(name) < 2:
return jsonify({"ok": False, "error": MSG_CATEGORY_NAME}), 400
try:
category = store.rename_category(category_id, name)
except KeyError:
return jsonify({"ok": False, "error": MSG_CATEGORY_NOT_FOUND}), 404
return jsonify({"ok": True, "category": category})
@app.delete("/api/categories/<category_id>")
def delete_category(category_id: str):
auth_error = require_auth()
if auth_error:
return auth_error
try:
store.delete_category(category_id)
except KeyError:
return jsonify({"ok": False, "error": MSG_CATEGORY_NOT_FOUND}), 404
return jsonify({"ok": True})
@app.post("/api/categories/<category_id>/tasks")
def create_task(category_id: str):
auth_error = require_auth()
if auth_error:
return auth_error
payload = request.get_json(silent=True) or {}
title = str(payload.get("title", "")).strip()
if len(title) < 2:
return jsonify({"ok": False, "error": MSG_TASK_NAME}), 400
try:
due_at = parse_due_at(str(payload.get("due_at", "")))
except ValueError as exc:
return jsonify({"ok": False, "error": str(exc)}), 400
try:
task = store.add_task(category_id, title, due_at.isoformat())
except KeyError:
return jsonify({"ok": False, "error": MSG_CATEGORY_NOT_FOUND}), 404
return jsonify({"ok": True, "task": serialize_task(task)})
@app.patch("/api/tasks/<task_id>")
def update_task(task_id: str):
auth_error = require_auth()
if auth_error:
return auth_error
payload = request.get_json(silent=True) or {}
completed = bool(payload.get("completed"))
try:
task = store.toggle_task(task_id, completed)
except KeyError:
return jsonify({"ok": False, "error": MSG_TASK_NOT_FOUND}), 404
return jsonify({"ok": True, "task": serialize_task(task)})
@app.patch("/api/tasks/<task_id>/schedule")
def update_task_schedule(task_id: str):
auth_error = require_auth()
if auth_error:
return auth_error
payload = request.get_json(silent=True) or {}
settings = store.get_schedule_settings()
try:
schedule = parse_task_schedule_payload(payload, settings)
except ValueError as exc:
return jsonify({"ok": False, "error": str(exc)}), 400
conflict_title = find_schedule_conflict(task_id, schedule)
if conflict_title:
return jsonify(
{
"ok": False,
"error": f"该时间段与“{conflict_title}”冲突,请换一个时间",
}
), 400
try:
task = store.schedule_task(task_id, schedule)
except KeyError:
return jsonify({"ok": False, "error": MSG_TASK_NOT_FOUND}), 404
return jsonify({"ok": True, "task": serialize_task(task)})
@app.delete("/api/tasks/<task_id>")
def delete_task(task_id: str):
auth_error = require_auth()
if auth_error:
return auth_error
try:
store.delete_task(task_id)
except KeyError:
return jsonify({"ok": False, "error": MSG_TASK_NOT_FOUND}), 404
return jsonify({"ok": True})
@app.patch("/api/settings/schedule")
def update_schedule_settings():
auth_error = require_auth()
if auth_error:
return auth_error
payload = request.get_json(silent=True) or {}
try:
settings = parse_schedule_settings(payload)
except ValueError as exc:
return jsonify({"ok": False, "error": str(exc)}), 400
updated = store.update_schedule_settings(settings)
return jsonify({"ok": True, "settings": updated})
@app.post("/api/courses")
def create_course():
auth_error = require_auth()
if auth_error:
return auth_error
payload = request.get_json(silent=True) or {}
try:
course = store.create_course(parse_course_payload(payload))
except ValueError as exc:
return jsonify({"ok": False, "error": str(exc)}), 400
return jsonify({"ok": True, "course": course})
@app.patch("/api/courses/<course_id>")
def update_course(course_id: str):
auth_error = require_auth()
if auth_error:
return auth_error
payload = request.get_json(silent=True) or {}
try:
update_payload = parse_course_payload(payload)
except ValueError as exc:
return jsonify({"ok": False, "error": str(exc)}), 400
try:
course = store.update_course(course_id, update_payload)
except KeyError:
return jsonify({"ok": False, "error": MSG_COURSE_NOT_FOUND}), 404
return jsonify({"ok": True, "course": course})
@app.delete("/api/courses/<course_id>")
def delete_course(course_id: str):
auth_error = require_auth()
if auth_error:
return auth_error
try:
store.delete_course(course_id)
except KeyError:
return jsonify({"ok": False, "error": MSG_COURSE_NOT_FOUND}), 404
return jsonify({"ok": True})
if __name__ == "__main__":
port = int(os.getenv("PORT", "7860"))
app.run(host="0.0.0.0", port=port, debug=False)