Spaces:
Running
Running
Commit ·
2ad50f4
1
Parent(s): 45e9848
Keep new courses usable when persistence fails
Browse files- classroom_service.py +13 -7
- classroom_static/teacher.html +1 -1
- classroom_static/teacher.js +3 -2
- tests/test_classroom.py +18 -0
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 |
-
|
|
|
|
|
|
|
| 112 |
rows=db.execute("SELECT class_code,course_name FROM classroom_courses ORDER BY course_name,class_code").fetchall()
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-
|
|
|
|
| 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:
|