steveyehtw commited on
Commit
2ad50f4
·
1 Parent(s): 45e9848

Keep new courses usable when persistence fails

Browse files
classroom_service.py CHANGED
@@ -108,14 +108,20 @@ def create_class(req:CourseCreate):
108
  check_password(req.password); code=clean(req.class_code,""); name=clean_label(req.course_name,code)
109
  if not code:raise HTTPException(400,"請輸入課程代碼")
110
  with lock,sqlite3.connect(DB) as db:
111
- if db.execute("SELECT 1 FROM classroom_courses WHERE class_code=?",(code,)).fetchone():raise HTTPException(409,"這個課程代碼已存在")
 
 
112
  rows=db.execute("SELECT class_code,course_name FROM classroom_courses ORDER BY course_name,class_code").fetchall()
113
- if not TOKEN:raise HTTPException(503,"請先在 Space Secrets 設定 HF_TOKEN")
114
- from huggingface_hub import CommitOperationAdd,HfApi
115
- payload=json.dumps([{"code":c,"name":n} for c,n in rows]+[{"code":code,"name":name}],ensure_ascii=False,indent=2)+"\n"
116
- HfApi(token=TOKEN).create_commit(repo_id=REPO,repo_type="space",operations=[CommitOperationAdd(path_in_repo="classroom_data/classes.json",path_or_fileobj=payload.encode())],commit_message=f"Add classroom course {code}")
117
- with lock,sqlite3.connect(DB) as db:db.execute("INSERT INTO classroom_courses VALUES(?,?,?)",(code,name,now()))
118
- return {"ok":True,"class_code":code,"course_name":name}
 
 
 
 
119
  @router.delete("/api/classroom/students")
120
  async def delete_student(req:StudentDelete):
121
  check_password(req.password); code=clean(req.class_code,"L6A"); student_id=clean(req.student_id,"student")
 
108
  check_password(req.password); code=clean(req.class_code,""); name=clean_label(req.course_name,code)
109
  if not code:raise HTTPException(400,"請輸入課程代碼")
110
  with lock,sqlite3.connect(DB) as db:
111
+ existing=db.execute("SELECT course_name FROM classroom_courses WHERE class_code=?",(code,)).fetchone()
112
+ if existing:name=existing[0]
113
+ else:db.execute("INSERT INTO classroom_courses VALUES(?,?,?)",(code,name,now()))
114
  rows=db.execute("SELECT class_code,course_name FROM classroom_courses ORDER BY course_name,class_code").fetchall()
115
+ warning=""
116
+ if TOKEN:
117
+ try:
118
+ from huggingface_hub import CommitOperationAdd,HfApi
119
+ payload=json.dumps([{"code":c,"name":n} for c,n in rows],ensure_ascii=False,indent=2)+"\n"
120
+ HfApi(token=TOKEN).create_commit(repo_id=REPO,repo_type="space",operations=[CommitOperationAdd(path_in_repo="classroom_data/classes.json",path_or_fileobj=payload.encode())],commit_message=f"Add classroom course {code}")
121
+ except Exception:
122
+ warning="課程已可使用,但尚未永久寫入 Space;請檢查 HF_TOKEN 的寫入權限。"
123
+ else:warning="課程已可使用,但尚未永久寫入 Space;請設定 HF_TOKEN。"
124
+ return {"ok":True,"class_code":code,"course_name":name,"already_exists":bool(existing),"warning":warning}
125
  @router.delete("/api/classroom/students")
126
  async def delete_student(req:StudentDelete):
127
  check_password(req.password); code=clean(req.class_code,"L6A"); student_id=clean(req.student_id,"student")
classroom_static/teacher.html CHANGED
@@ -3,4 +3,4 @@
3
  <section class="project-bar"><div class="project-context"><label for="teacherWeek">週次</label><select id="teacherWeek"></select><span id="submissionSummary" class="submission-summary">等待學生</span></div><div class="project-actions"><button class="copy-button" id="downloadButton">下載全班 ZIP</button><button class="btn compact" id="publishButton">結算並發布 Demo</button></div></section>
4
  <main class="teacher-layout"><section class="roster-panel"><div class="panel-heading"><h1>學生狀態</h1><span id="studentTotal">0 人</span></div><div class="filter-bar" id="teacherFilters"><button class="chip" data-filter="all" aria-pressed="true">全部</button><button class="chip" data-filter="submitted">已提交</button><button class="chip" data-filter="help">需要協助</button><button class="chip" data-filter="error">錯誤</button></div><div class="roster-header"><span>學生</span><span>狀態</span><span>檔案</span><span>提交</span></div><div class="roster-list" id="rosterList"></div></section>
5
  <section class="teacher-detail" id="teacherDetail"><div class="detail-empty" id="detailEmpty">選擇一位學生查看多檔案專案。</div><div class="detail-view" id="detailView" hidden><header class="detail-header"><div><h1 id="selectedStudentName"></h1><p class="detail-meta" id="selectedStudentMeta"></p></div><div class="detail-actions"><button class="copy-button" id="ackHelpButton" hidden>標記已查看</button><button class="copy-button" id="copyCodeButton">複製檔案</button><button class="copy-button danger-action" id="deleteStudentButton">刪除學生</button></div></header><div class="teacher-project"><nav class="teacher-file-tabs" id="teacherFiles"></nav><div class="detail-split"><section class="read-only-pane"><div class="pane-label"><span id="teacherActiveFile">main.py</span><span id="lineCount">0 行</span></div><pre class="code-readonly" id="studentCode"></pre></section><section class="read-only-pane"><div class="pane-label"><span>Console</span><span id="outputState"></span></div><pre class="output-readonly" id="studentOutput"></pre></section></div></div></div></section></main>
6
- <dialog id="loginDialog"><form class="dialog-content form-stack" id="loginForm"><div><h1>進入教師控制台</h1><p>選取現有課程,或建立新的課程代碼。</p></div><div><label class="field-label" for="classCode">課程</label><select class="text-field" id="classCode" required></select></div><div id="newClassFields" hidden><label class="field-label" for="newClassCode">新課程代碼</label><input class="text-field" id="newClassCode" placeholder="L6B"><label class="field-label secondary-label" for="newClassName">課程名稱</label><input class="text-field" id="newClassName" placeholder="進階專題 B 班"></div><div><label class="field-label" for="teacherPassword">教師密碼</label><input class="text-field" id="teacherPassword" type="password" required></div><p class="form-error" id="loginError"></p><button class="btn" type="submit">開始監看</button></form></dialog><dialog id="publishDialog"><div class="dialog-content"><h2>結算並發布</h2><p id="publishMessage"></p><div class="publish-actions"><button class="btn secondary" id="cancelPublish">取消</button><button class="btn" id="confirmPublish">確認發布</button></div></div></dialog><div class="toast" id="toast"></div><script src="/classroom-assets/teacher.js?v=2026fall-4"></script></body></html>
 
3
  <section class="project-bar"><div class="project-context"><label for="teacherWeek">週次</label><select id="teacherWeek"></select><span id="submissionSummary" class="submission-summary">等待學生</span></div><div class="project-actions"><button class="copy-button" id="downloadButton">下載全班 ZIP</button><button class="btn compact" id="publishButton">結算並發布 Demo</button></div></section>
4
  <main class="teacher-layout"><section class="roster-panel"><div class="panel-heading"><h1>學生狀態</h1><span id="studentTotal">0 人</span></div><div class="filter-bar" id="teacherFilters"><button class="chip" data-filter="all" aria-pressed="true">全部</button><button class="chip" data-filter="submitted">已提交</button><button class="chip" data-filter="help">需要協助</button><button class="chip" data-filter="error">錯誤</button></div><div class="roster-header"><span>學生</span><span>狀態</span><span>檔案</span><span>提交</span></div><div class="roster-list" id="rosterList"></div></section>
5
  <section class="teacher-detail" id="teacherDetail"><div class="detail-empty" id="detailEmpty">選擇一位學生查看多檔案專案。</div><div class="detail-view" id="detailView" hidden><header class="detail-header"><div><h1 id="selectedStudentName"></h1><p class="detail-meta" id="selectedStudentMeta"></p></div><div class="detail-actions"><button class="copy-button" id="ackHelpButton" hidden>標記已查看</button><button class="copy-button" id="copyCodeButton">複製檔案</button><button class="copy-button danger-action" id="deleteStudentButton">刪除學生</button></div></header><div class="teacher-project"><nav class="teacher-file-tabs" id="teacherFiles"></nav><div class="detail-split"><section class="read-only-pane"><div class="pane-label"><span id="teacherActiveFile">main.py</span><span id="lineCount">0 行</span></div><pre class="code-readonly" id="studentCode"></pre></section><section class="read-only-pane"><div class="pane-label"><span>Console</span><span id="outputState"></span></div><pre class="output-readonly" id="studentOutput"></pre></section></div></div></div></section></main>
6
+ <dialog id="loginDialog"><form class="dialog-content form-stack" id="loginForm"><div><h1>進入教師控制台</h1><p>選取現有課程,或建立新的課程代碼。</p></div><div><label class="field-label" for="classCode">課程</label><select class="text-field" id="classCode" required></select></div><div id="newClassFields" hidden><label class="field-label" for="newClassCode">新課程代碼</label><input class="text-field" id="newClassCode" placeholder="L6B"><label class="field-label secondary-label" for="newClassName">課程名稱</label><input class="text-field" id="newClassName" placeholder="進階專題 B 班"></div><div><label class="field-label" for="teacherPassword">教師密碼</label><input class="text-field" id="teacherPassword" type="password" required></div><p class="form-error" id="loginError"></p><button class="btn" type="submit">開始監看</button></form></dialog><dialog id="publishDialog"><div class="dialog-content"><h2>結算並發布</h2><p id="publishMessage"></p><div class="publish-actions"><button class="btn secondary" id="cancelPublish">取消</button><button class="btn" id="confirmPublish">確認發布</button></div></div></dialog><div class="toast" id="toast"></div><script src="/classroom-assets/teacher.js?v=2026fall-6"></script></body></html>
classroom_static/teacher.js CHANGED
@@ -170,10 +170,11 @@ E.loginForm.onsubmit = async event => {
170
  if (!classCode) throw Error("請輸入新課程代碼");
171
  button.textContent = "建立課程中…";
172
  credentials = { classCode, password };
173
- await request("/api/classroom/classes", "POST", {
174
  class_code: classCode,
175
  course_name: E.newClassName.value.trim() || classCode
176
- });
 
177
  }
178
  credentials = { classCode, password };
179
  sessionStorage.setItem("teacher_class", classCode);
 
170
  if (!classCode) throw Error("請輸入新課程代碼");
171
  button.textContent = "建立課程中…";
172
  credentials = { classCode, password };
173
+ const created = await (await request("/api/classroom/classes", "POST", {
174
  class_code: classCode,
175
  course_name: E.newClassName.value.trim() || classCode
176
+ })).json();
177
+ if (created.warning) toast(created.warning);
178
  }
179
  credentials = { classCode, password };
180
  sessionStorage.setItem("teacher_class", classCode);
tests/test_classroom.py CHANGED
@@ -97,6 +97,24 @@ class ClassroomServiceTests(unittest.TestCase):
97
  self.assertEqual(classroom.snapshot("L6A", "week01"), [])
98
  self.assertEqual(classroom.snapshot("L6A", "week02"), [])
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  def test_published_final_can_be_loaded_after_runtime_restart(self):
101
  old_published = classroom.PUBLISHED
102
  try:
 
97
  self.assertEqual(classroom.snapshot("L6A", "week01"), [])
98
  self.assertEqual(classroom.snapshot("L6A", "week02"), [])
99
 
100
+ def test_course_is_available_even_when_repo_persistence_is_unavailable(self):
101
+ old_password, old_token = classroom.PASSWORD, classroom.TOKEN
102
+ try:
103
+ classroom.PASSWORD = "teacher-test"
104
+ classroom.TOKEN = ""
105
+ request = classroom.CourseCreate(
106
+ class_code="L6B", course_name="週六進階班", password="teacher-test"
107
+ )
108
+ created = classroom.create_class(request)
109
+ repeated = classroom.create_class(request)
110
+ finally:
111
+ classroom.PASSWORD, classroom.TOKEN = old_password, old_token
112
+ self.assertTrue(created["ok"])
113
+ self.assertIn("HF_TOKEN", created["warning"])
114
+ self.assertTrue(repeated["already_exists"])
115
+ course = next(item for item in classroom.catalog()["courses"] if item["class_code"] == "L6B")
116
+ self.assertEqual(course["course_name"], "週六進階班")
117
+
118
  def test_published_final_can_be_loaded_after_runtime_restart(self):
119
  old_published = classroom.PUBLISHED
120
  try: