Spaces:
Running
Running
Commit ·
12e244e
1
Parent(s): 768ace6
Make classroom panels resizable
Browse files- classroom_static/classroom.css +21 -12
- classroom_static/student.html +5 -3
- classroom_static/student.js +38 -4
- tests/test_admin.py +4 -2
classroom_static/classroom.css
CHANGED
|
@@ -141,27 +141,36 @@ button:disabled { cursor: not-allowed; opacity: .48; }
|
|
| 141 |
.project-context select,.project-context input { min-height:36px;max-width:280px;padding:0 var(--space-md);border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface);color:var(--ink);font-weight:700; }
|
| 142 |
.submission-summary { padding:var(--space-sm) var(--space-md);border-radius:999px;background:var(--success-soft);color:oklch(.37 .12 150);font-size:.8rem;font-weight:800; }
|
| 143 |
.student-layout {
|
|
|
|
|
|
|
| 144 |
min-height: 0;
|
| 145 |
flex: 1;
|
| 146 |
overflow: hidden;
|
| 147 |
display: grid;
|
| 148 |
-
grid-template-columns: minmax(
|
| 149 |
-
grid-template-areas: "reference editor console";
|
| 150 |
}
|
| 151 |
|
| 152 |
.panel { min-width: 0; min-height: 0; overflow: hidden; background: var(--surface); }
|
| 153 |
-
.reference-panel { grid-area: reference; display:grid;grid-template-rows:44px minmax(0,1fr);
|
| 154 |
-
.editor-panel { grid-area: editor; display:grid;grid-template-rows:
|
| 155 |
-
.console-panel { grid-area: console; display:grid;grid-template-rows:52px minmax(0,1fr);
|
| 156 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
.editor-toggle { min-height:32px;padding:0 var(--space-md);border:1px solid oklch(.42 .035 258);border-radius:var(--radius-sm);background:transparent;color:oklch(.82 .025 255);font-size:.78rem;font-weight:750; }
|
| 158 |
.editor-toggle:hover,.editor-toggle[aria-pressed="true"] { border-color:oklch(.66 .12 255);background:oklch(.3 .065 258);color:white; }
|
| 159 |
|
| 160 |
@media (min-width: 981px) {
|
| 161 |
-
.student-layout.reference-hidden { grid-template-columns:minmax(
|
| 162 |
-
.student-layout.console-hidden { grid-template-columns:minmax(
|
| 163 |
.student-layout.reference-hidden.console-hidden { grid-template-columns:minmax(0,1fr);grid-template-areas:"editor"; }
|
| 164 |
-
.student-layout.reference-hidden .reference-panel,.student-layout.console-hidden .console-panel { display:none; }
|
| 165 |
}
|
| 166 |
|
| 167 |
.panel-heading {
|
|
@@ -174,7 +183,7 @@ button:disabled { cursor: not-allowed; opacity: .48; }
|
|
| 174 |
border-bottom: 1px solid var(--line);
|
| 175 |
}
|
| 176 |
.panel-heading h1, .panel-heading h2 { margin: 0; font-size: 1rem; }
|
| 177 |
-
.panel-heading.dark { border-color:
|
| 178 |
.side-tabs { height:44px;display:flex;border-bottom:1px solid var(--line); }
|
| 179 |
.side-tab { flex:1;border:0;background:var(--surface);color:var(--muted);font-size:.8rem;font-weight:800; }
|
| 180 |
.side-tab.active { color:var(--navy);box-shadow:inset 0 -3px 0 var(--action); }
|
|
@@ -247,8 +256,7 @@ button:disabled { cursor: not-allowed; opacity: .48; }
|
|
| 247 |
.checkpoint-action { border-color: oklch(0.72 0.1 150); color: oklch(0.38 0.13 150); }
|
| 248 |
.checkpoint-action:hover { background: var(--success-soft); }
|
| 249 |
|
| 250 |
-
.
|
| 251 |
-
.file-name { font: 700 .875rem/1 "SFMono-Regular", Consolas, monospace; color: oklch(0.82 0.025 255); }
|
| 252 |
.editor-host { min-height:0; }
|
| 253 |
.CodeMirror { height: 100%; font: 500 1rem/1.7 "SFMono-Regular", Consolas, monospace; background: var(--editor); }
|
| 254 |
.CodeMirror-gutters { background: var(--editor); border-right: 1px solid var(--editor-line); }
|
|
@@ -343,6 +351,7 @@ dialog::backdrop { background: oklch(0.16 0.04 255 / .72); }
|
|
| 343 |
@media (max-width: 980px) {
|
| 344 |
.project-bar{align-items:stretch;flex-direction:column;padding:var(--space-md)}.project-actions{overflow-x:auto;flex-wrap:nowrap}.project-actions>*{flex:none}
|
| 345 |
.editor-toggle { display:none; }
|
|
|
|
| 346 |
.student-layout { display:block;min-height:0; }
|
| 347 |
.mobile-tabs { display:flex;flex:none;background:var(--surface);border-bottom:1px solid var(--line); }
|
| 348 |
.mobile-tab { flex: 1; min-height: 46px; border: 0; background: var(--surface); color: var(--muted); font-weight: 800; }
|
|
|
|
| 141 |
.project-context select,.project-context input { min-height:36px;max-width:280px;padding:0 var(--space-md);border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface);color:var(--ink);font-weight:700; }
|
| 142 |
.submission-summary { padding:var(--space-sm) var(--space-md);border-radius:999px;background:var(--success-soft);color:oklch(.37 .12 150);font-size:.8rem;font-weight:800; }
|
| 143 |
.student-layout {
|
| 144 |
+
--reference-width: 280px;
|
| 145 |
+
--console-width: 360px;
|
| 146 |
min-height: 0;
|
| 147 |
flex: 1;
|
| 148 |
overflow: hidden;
|
| 149 |
display: grid;
|
| 150 |
+
grid-template-columns: minmax(220px, var(--reference-width)) 9px minmax(320px, 1fr) 9px minmax(260px, var(--console-width));
|
| 151 |
+
grid-template-areas: "reference reference-resizer editor console-resizer console";
|
| 152 |
}
|
| 153 |
|
| 154 |
.panel { min-width: 0; min-height: 0; overflow: hidden; background: var(--surface); }
|
| 155 |
+
.reference-panel { grid-area: reference; display:grid;grid-template-rows:44px minmax(0,1fr); }
|
| 156 |
+
.editor-panel { grid-area: editor; display:grid;grid-template-rows:auto minmax(0,1fr);background:var(--editor); }
|
| 157 |
+
.console-panel { grid-area: console; display:grid;grid-template-rows:52px minmax(0,1fr); }
|
| 158 |
+
.panel-resizer { position:relative;min-width:9px;min-height:0;background:var(--shell);cursor:col-resize;touch-action:none; }
|
| 159 |
+
.panel-resizer::after { content:"";position:absolute;inset:0 auto 0 4px;width:1px;background:var(--line);transition:background-color 160ms ease,width 160ms ease; }
|
| 160 |
+
.panel-resizer:hover::after,.panel-resizer:focus-visible::after,.panel-resizer.dragging::after { width:2px;background:var(--action); }
|
| 161 |
+
.panel-resizer:focus-visible { outline:3px solid var(--help);outline-offset:-3px; }
|
| 162 |
+
.reference-resizer { grid-area:reference-resizer; }
|
| 163 |
+
.console-resizer { grid-area:console-resizer; }
|
| 164 |
+
body.panel-resizing { cursor:col-resize;user-select:none; }
|
| 165 |
+
.editor-toolbar { display:flex;align-items:center;justify-content:flex-end;gap:var(--space-sm);flex:0 1 auto;flex-wrap:wrap;min-width:0; }
|
| 166 |
.editor-toggle { min-height:32px;padding:0 var(--space-md);border:1px solid oklch(.42 .035 258);border-radius:var(--radius-sm);background:transparent;color:oklch(.82 .025 255);font-size:.78rem;font-weight:750; }
|
| 167 |
.editor-toggle:hover,.editor-toggle[aria-pressed="true"] { border-color:oklch(.66 .12 255);background:oklch(.3 .065 258);color:white; }
|
| 168 |
|
| 169 |
@media (min-width: 981px) {
|
| 170 |
+
.student-layout.reference-hidden { grid-template-columns:minmax(320px,1fr) 9px minmax(260px,var(--console-width));grid-template-areas:"editor console-resizer console"; }
|
| 171 |
+
.student-layout.console-hidden { grid-template-columns:minmax(220px,var(--reference-width)) 9px minmax(320px,1fr);grid-template-areas:"reference reference-resizer editor"; }
|
| 172 |
.student-layout.reference-hidden.console-hidden { grid-template-columns:minmax(0,1fr);grid-template-areas:"editor"; }
|
| 173 |
+
.student-layout.reference-hidden .reference-panel,.student-layout.reference-hidden .reference-resizer,.student-layout.console-hidden .console-panel,.student-layout.console-hidden .console-resizer { display:none; }
|
| 174 |
}
|
| 175 |
|
| 176 |
.panel-heading {
|
|
|
|
| 183 |
border-bottom: 1px solid var(--line);
|
| 184 |
}
|
| 185 |
.panel-heading h1, .panel-heading h2 { margin: 0; font-size: 1rem; }
|
| 186 |
+
.panel-heading.dark { min-width:0;padding-block:6px;flex-wrap:wrap;border-color:var(--editor-line);background:var(--editor-raised);color:var(--editor-ink); }
|
| 187 |
.side-tabs { height:44px;display:flex;border-bottom:1px solid var(--line); }
|
| 188 |
.side-tab { flex:1;border:0;background:var(--surface);color:var(--muted);font-size:.8rem;font-weight:800; }
|
| 189 |
.side-tab.active { color:var(--navy);box-shadow:inset 0 -3px 0 var(--action); }
|
|
|
|
| 256 |
.checkpoint-action { border-color: oklch(0.72 0.1 150); color: oklch(0.38 0.13 150); }
|
| 257 |
.checkpoint-action:hover { background: var(--success-soft); }
|
| 258 |
|
| 259 |
+
.file-name { min-width:0;flex:1 1 100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:700 .875rem/1 "SFMono-Regular",Consolas,monospace;color:oklch(.82 .025 255); }
|
|
|
|
| 260 |
.editor-host { min-height:0; }
|
| 261 |
.CodeMirror { height: 100%; font: 500 1rem/1.7 "SFMono-Regular", Consolas, monospace; background: var(--editor); }
|
| 262 |
.CodeMirror-gutters { background: var(--editor); border-right: 1px solid var(--editor-line); }
|
|
|
|
| 351 |
@media (max-width: 980px) {
|
| 352 |
.project-bar{align-items:stretch;flex-direction:column;padding:var(--space-md)}.project-actions{overflow-x:auto;flex-wrap:nowrap}.project-actions>*{flex:none}
|
| 353 |
.editor-toggle { display:none; }
|
| 354 |
+
.panel-resizer { display:none; }
|
| 355 |
.student-layout { display:block;min-height:0; }
|
| 356 |
.mobile-tabs { display:flex;flex:none;background:var(--surface);border-bottom:1px solid var(--line); }
|
| 357 |
.mobile-tab { flex: 1; min-height: 46px; border: 0; background: var(--surface); color: var(--muted); font-weight: 800; }
|
classroom_static/student.html
CHANGED
|
@@ -1,10 +1,12 @@
|
|
| 1 |
-
<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light"><title>Python 專案教室|學生工作區</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css"><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/material-darker.min.css"><link rel="stylesheet" href="/classroom-assets/classroom.css?v=2026fall-
|
| 2 |
<header class="app-header"><div class="brand"><div class="brand-mark">Py</div><div class="brand-copy"><strong>Python 專案教室</strong><span id="studentIdentity">尚未進入教室</span></div></div><div class="header-actions"><div class="connection-state"><i class="status-dot" id="connectionDot"></i><span id="connectionText">未連線</span></div><button class="btn help compact" id="helpButton">請老師協助</button><button class="btn secondary compact" id="identityButton">更換身分</button></div></header>
|
| 3 |
<section class="project-bar"><div class="project-context"><label for="weekSelect">Lesson</label><select id="weekSelect"></select><label for="projectName">專案</label><input id="projectName" maxlength="36"></div><div class="project-actions"><button class="copy-button" id="templateButton">匯入專案起始套版</button><button class="copy-button" id="progressButton">選擇舊進度</button><button class="copy-button" id="importButton">匯入 .py / ZIP</button><button class="copy-button" id="exportButton">匯出專案</button><button class="copy-button checkpoint-action" id="checkpointButton">暫存本週進度</button><button class="btn compact" id="submitButton">提交正式版本</button><input id="fileImport" type="file" accept=".py,.zip" hidden></div></section>
|
| 4 |
<nav class="mobile-tabs" role="tablist"><button class="mobile-tab" data-panel="referencePanel">專案/語法</button><button class="mobile-tab" data-panel="editorPanel" aria-selected="true">程式</button><button class="mobile-tab" data-panel="consolePanel">執行結果</button></nav>
|
| 5 |
-
<main class="student-layout"><aside class="panel reference-panel" id="referencePanel"><div class="side-tabs"><button class="side-tab active" data-side="filesView">專案檔案</button><button class="side-tab" data-side="syntaxView">語法查詢</button></div><section class="side-view" id="filesView"><div class="panel-heading"><h1>檔案夾</h1><button class="copy-button" id="newFileButton">+新增</button></div><div class="file-list" id="fileList"></div><p class="side-note">入口固定為 <code>main.py</code>,可用 <code>import</code> 引入其他 Python 檔。</p></section><section class="side-view" id="syntaxView" hidden><div class="panel-heading"><h1>Python 語法查詢</h1><span id="snippetCount">0 項</span></div><div class="search-wrap"><label class="field-label" for="syntaxSearch">輸入語法或用途</label><input class="text-field" id="syntaxSearch" type="search"><div class="chip-row" id="categoryFilters"></div></div><div class="snippet-list" id="snippetList"></div></section></aside>
|
|
|
|
| 6 |
<section class="panel editor-panel mobile-active" id="editorPanel"><div class="panel-heading dark"><span class="file-name" id="activeFileLabel">main.py</span><div class="editor-toolbar"><button class="editor-toggle" id="toggleReferenceButton" aria-pressed="false">隱藏側欄</button><button class="editor-toggle" id="toggleConsoleButton" aria-pressed="false">隱藏 Console</button><button class="btn stop compact" id="stopButton" disabled>停止</button><button class="btn compact" id="runButton" disabled>載入 Python…</button></div></div><div class="editor-host"><textarea id="codeEditor" aria-label="程式碼"></textarea></div></section>
|
|
|
|
| 7 |
<aside class="panel console-panel" id="consolePanel"><div class="panel-heading"><h2>互動式 Console</h2><button class="copy-button" id="clearOutputButton">清除</button></div><div class="console-body"><div class="console-stream"><pre class="console-output" id="consoleOutput">Python 執行環境載入中…</pre><form class="console-input-row" id="consoleInputForm" hidden><span aria-hidden="true">›</span><label class="sr-only" for="consoleInput">Python 程式要求輸入</label><input id="consoleInput" autocomplete="off"><button class="btn compact" type="submit">送出</button></form></div><p class="console-hint">執行 <code>main.py</code>;遇到 <code>input()</code> 時會在這裡詢問。</p><div class="console-status"><i class="status-dot" id="runStatusDot"></i><span id="runStatusText">準備環境</span><span>·</span><span id="saveStatus">尚未同步</span><span>·</span><span id="submitStatus">尚未暫存或提交</span></div></div></aside></main>
|
| 8 |
<dialog id="identityDialog"><form class="dialog-content form-stack" id="identityForm"><div><h1>進入程式教室</h1><p>選擇班級並輸入自己在名單上的姓名。</p></div><div><label class="field-label" for="classCode">課程</label><select class="text-field" id="classCode" required></select></div><div><label class="field-label" for="displayName">姓名</label><input class="text-field" id="displayName" autocomplete="name" required><p class="field-help">姓名必須與老師建立的名單一致。</p></div><p class="form-error" id="identityError" role="alert"></p><button class="btn" type="submit">進入教室</button></form></dialog>
|
| 9 |
<dialog id="progressDialog"><div class="dialog-content"><h2>選擇已儲存版本</h2><p>每次暫存與正式提交都會保留;載入只會建立工作副本,不會刪除其他版本。</p><div class="progress-list" id="progressList"></div><button class="btn secondary dialog-close">關閉</button></div></dialog><div class="toast" id="toast"></div>
|
| 10 |
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/python/python.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script><script src="/classroom-assets/student.js?v=2026fall-
|
|
|
|
| 1 |
+
<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light"><title>Python 專案教室|學生工作區</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css"><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/material-darker.min.css"><link rel="stylesheet" href="/classroom-assets/classroom.css?v=2026fall-9"></head><body>
|
| 2 |
<header class="app-header"><div class="brand"><div class="brand-mark">Py</div><div class="brand-copy"><strong>Python 專案教室</strong><span id="studentIdentity">尚未進入教室</span></div></div><div class="header-actions"><div class="connection-state"><i class="status-dot" id="connectionDot"></i><span id="connectionText">未連線</span></div><button class="btn help compact" id="helpButton">請老師協助</button><button class="btn secondary compact" id="identityButton">更換身分</button></div></header>
|
| 3 |
<section class="project-bar"><div class="project-context"><label for="weekSelect">Lesson</label><select id="weekSelect"></select><label for="projectName">專案</label><input id="projectName" maxlength="36"></div><div class="project-actions"><button class="copy-button" id="templateButton">匯入專案起始套版</button><button class="copy-button" id="progressButton">選擇舊進度</button><button class="copy-button" id="importButton">匯入 .py / ZIP</button><button class="copy-button" id="exportButton">匯出專案</button><button class="copy-button checkpoint-action" id="checkpointButton">暫存本週進度</button><button class="btn compact" id="submitButton">提交正式版本</button><input id="fileImport" type="file" accept=".py,.zip" hidden></div></section>
|
| 4 |
<nav class="mobile-tabs" role="tablist"><button class="mobile-tab" data-panel="referencePanel">專案/語法</button><button class="mobile-tab" data-panel="editorPanel" aria-selected="true">程式</button><button class="mobile-tab" data-panel="consolePanel">執行結果</button></nav>
|
| 5 |
+
<main class="student-layout" id="studentLayout"><aside class="panel reference-panel" id="referencePanel"><div class="side-tabs"><button class="side-tab active" data-side="filesView">專案檔案</button><button class="side-tab" data-side="syntaxView">語法查詢</button></div><section class="side-view" id="filesView"><div class="panel-heading"><h1>檔案夾</h1><button class="copy-button" id="newFileButton">+新增</button></div><div class="file-list" id="fileList"></div><p class="side-note">入口固定為 <code>main.py</code>,可用 <code>import</code> 引入其他 Python 檔。</p></section><section class="side-view" id="syntaxView" hidden><div class="panel-heading"><h1>Python 語法查詢</h1><span id="snippetCount">0 項</span></div><div class="search-wrap"><label class="field-label" for="syntaxSearch">輸入語法或用途</label><input class="text-field" id="syntaxSearch" type="search"><div class="chip-row" id="categoryFilters"></div></div><div class="snippet-list" id="snippetList"></div></section></aside>
|
| 6 |
+
<div class="panel-resizer reference-resizer" id="referenceResizer" role="separator" aria-label="調整專案側欄寬度" aria-orientation="vertical" tabindex="0" title="拖曳調整寬度;雙擊恢復預設"></div>
|
| 7 |
<section class="panel editor-panel mobile-active" id="editorPanel"><div class="panel-heading dark"><span class="file-name" id="activeFileLabel">main.py</span><div class="editor-toolbar"><button class="editor-toggle" id="toggleReferenceButton" aria-pressed="false">隱藏側欄</button><button class="editor-toggle" id="toggleConsoleButton" aria-pressed="false">隱藏 Console</button><button class="btn stop compact" id="stopButton" disabled>停止</button><button class="btn compact" id="runButton" disabled>載入 Python…</button></div></div><div class="editor-host"><textarea id="codeEditor" aria-label="程式碼"></textarea></div></section>
|
| 8 |
+
<div class="panel-resizer console-resizer" id="consoleResizer" role="separator" aria-label="調整 Console 寬度" aria-orientation="vertical" tabindex="0" title="拖曳調整寬度;雙擊恢復預設"></div>
|
| 9 |
<aside class="panel console-panel" id="consolePanel"><div class="panel-heading"><h2>互動式 Console</h2><button class="copy-button" id="clearOutputButton">清除</button></div><div class="console-body"><div class="console-stream"><pre class="console-output" id="consoleOutput">Python 執行環境載入中…</pre><form class="console-input-row" id="consoleInputForm" hidden><span aria-hidden="true">›</span><label class="sr-only" for="consoleInput">Python 程式要求輸入</label><input id="consoleInput" autocomplete="off"><button class="btn compact" type="submit">送出</button></form></div><p class="console-hint">執行 <code>main.py</code>;遇到 <code>input()</code> 時會在這裡詢問。</p><div class="console-status"><i class="status-dot" id="runStatusDot"></i><span id="runStatusText">準備環境</span><span>·</span><span id="saveStatus">尚未同步</span><span>·</span><span id="submitStatus">尚未暫存或提交</span></div></div></aside></main>
|
| 10 |
<dialog id="identityDialog"><form class="dialog-content form-stack" id="identityForm"><div><h1>進入程式教室</h1><p>選擇班級並輸入自己在名單上的姓名。</p></div><div><label class="field-label" for="classCode">課程</label><select class="text-field" id="classCode" required></select></div><div><label class="field-label" for="displayName">姓名</label><input class="text-field" id="displayName" autocomplete="name" required><p class="field-help">姓名必須與老師建立的名單一致。</p></div><p class="form-error" id="identityError" role="alert"></p><button class="btn" type="submit">進入教室</button></form></dialog>
|
| 11 |
<dialog id="progressDialog"><div class="dialog-content"><h2>選擇已儲存版本</h2><p>每次暫存與正式提交都會保留;載入只會建立工作副本,不會刪除其他版本。</p><div class="progress-list" id="progressList"></div><button class="btn secondary dialog-close">關閉</button></div></dialog><div class="toast" id="toast"></div>
|
| 12 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/python/python.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script><script src="/classroom-assets/student.js?v=2026fall-11"></script></body></html>
|
classroom_static/student.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
const SNIPPETS=[["基礎","輸入與輸出",'name = input("名字:")\nprint("你好", name)'],["判斷","if / elif / else",'if score >= 90:\n print("A")\nelse:\n print("繼續加油")'],["迴圈","for 與 range",'for number in range(1, 6):\n print(number)'],["迴圈","while",'while True:\n choice = input("選擇:")\n if choice == "q":\n break'],["資料","list",'items = []\nitems.append("線索")\nfor item in items:\n print(item)'],["資料","dict",'student = {"name": "Alex"}\nprint(student.get("team", "未分組"))'],["函式","def / return",'def add(a, b):\n return a + b'],["檔案","引入自己的模組",'from clues import find_clue'],["檔案","讀取 JSON",'import json\nwith open("data.json", encoding="utf-8") as file:\n data = json.load(file)'],["錯誤","try / except",'try:\n age = int(input("年齡:"))\nexcept ValueError:\n print("請輸入整數")']].map(([category,title,code])=>({category,title,code}));
|
| 2 |
-
const $=id=>document.getElementById(id),E={};["identityDialog","identityForm","identityError","classCode","displayName","studentIdentity","identityButton","connectionDot","connectionText","helpButton","weekSelect","projectName","templateButton","progressButton","importButton","exportButton","checkpointButton","submitButton","fileImport","fileList","newFileButton","activeFileLabel","syntaxSearch","categoryFilters","snippetList","snippetCount","runButton","stopButton","toggleReferenceButton","toggleConsoleButton","consoleInputForm","consoleInput","consoleOutput","clearOutputButton","runStatusDot","runStatusText","saveStatus","submitStatus","toast","progressDialog","progressList"].forEach(id=>E[id]=$(id));
|
| 3 |
const editor=CodeMirror.fromTextArea($("codeEditor"),{mode:"python",theme:"material-darker",lineNumbers:true,indentUnit:4,tabSize:4,extraKeys:{Tab:cm=>cm.replaceSelection(" ")}});
|
| 4 |
let studentId=localStorage.getItem("classroom_student_id")||"",allowLegacyDraft=!!studentId,catalog={courses:[]};
|
| 5 |
let course,files={"main.py":"# 在這裡寫 Python\n"},activeFile="main.py",socket,worker,workerReady=false,running=false,needsHelp=false,checkpointAt=null,submittedAt=null,output="",suppress=false,timer,reconnect,category="全部",inputBuffer,inputControl,inputBytes,stdoutDecoder=new TextDecoder(),stderrDecoder=new TextDecoder();
|
|
@@ -38,7 +38,41 @@ function connect(){
|
|
| 38 |
current.onclose=async e=>{if(socket!==current)return;E.connectionDot.className="status-dot error";E.connectionText.textContent="已離線";E.saveStatus.textContent="保存在此裝置";if(e.code===4004||e.code===4404){localStorage.removeItem("classroom_student_id");localStorage.removeItem("classroom_name");studentId="";await loadCatalog();E.identityError.textContent=e.code===4004?"老師已將你從名單移除。":"姓名與班級名單不符,請重新輸入。";E.identityDialog.showModal();return}reconnect=setTimeout(connect,2500)};
|
| 39 |
}
|
| 40 |
function renderState(){E.helpButton.classList.toggle("active",needsHelp);E.helpButton.textContent=needsHelp?"取消請求協助":"請老師協助";const time=value=>new Date(value).toLocaleTimeString("zh-TW",{hour:"2-digit",minute:"2-digit"}),labels=[];if(checkpointAt)labels.push(`本週暫存 ${time(checkpointAt)}`);if(submittedAt)labels.push(`正式提交 ${time(submittedAt)}`);E.submitStatus.textContent=labels.join(" · ")||"尚未暫存或提交"}
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
function renderLesson(){const item=lessonInfo(),shared=item?.workspace_id&&item.workspace_id!==item.id;E.templateButton.disabled=!item?.template;E.templateButton.textContent=item?.template?"匯入專案起始套版":shared?"沿用目前專案":"尚未提供套版"}
|
| 43 |
function initWorker(){worker?.terminate();workerReady=false;running=false;E.consoleInputForm.hidden=true;E.runButton.disabled=true;E.runButton.textContent="載入 Python…";if(!crossOriginIsolated||typeof SharedArrayBuffer==="undefined"){append("瀏覽器未啟用互動式 Python。請重新整理��並使用最新版 Chrome、Edge 或 Safari。\n");runState("error","環境不支援");return}inputBuffer=new SharedArrayBuffer(16392);inputControl=new Int32Array(inputBuffer,0,2);inputBytes=new Uint8Array(inputBuffer,8);worker=new Worker("/classroom-assets/py-worker.js?v=2026fall-4");worker.onmessage=e=>{const m=e.data;if(m.type==="ready"){workerReady=true;E.runButton.disabled=false;E.runButton.textContent="執行";runState("online","準備完成")}else if(m.type==="stream"){const decoder=m.channel==="stderr"?stderrDecoder:stdoutDecoder;append(decoder.decode(m.bytes,{stream:true}))}else if(m.type==="input_request"){E.consoleInputForm.hidden=false;E.consoleInput.value="";E.runStatusText.textContent="等待輸入";E.consoleInput.focus()}else if(m.type==="done"){E.consoleInputForm.hidden=true;runState("success","執行成功")}else if(m.type==="python_error"){E.consoleInputForm.hidden=true;if(m.text)append(`${m.text}\n`);runState("error","程式錯誤")}else if(m.type==="load_error"){append(`Python 載入失敗:${m.text}\n`);runState("error","載入失敗")}};worker.onerror=e=>{append(`Python 執行器錯誤:${e.message}\n`);runState("error","執行器錯誤")}}
|
| 44 |
function runState(s,t){running=s==="running";E.runStatusDot.className=`status-dot ${s}`;E.runStatusText.textContent=t;E.runButton.disabled=!workerReady||running;E.stopButton.disabled=!running;send({type:"status",status:s==="online"?"idle":s})}
|
|
@@ -54,5 +88,5 @@ E.exportButton.onclick=async()=>{files[activeFile]=editor.getValue();const z=new
|
|
| 54 |
E.checkpointButton.onclick=()=>{files[activeFile]=editor.getValue();if(!files["main.py"].trim())return toast("main.py 不能為空");saveLocal();if(!send({type:"checkpoint",files,project_name:E.projectName.value}))return toast("目前離線,請連線後再暫存");checkpointAt=Date.now();renderState();toast("已新增一個暫存版本,不會發布到 Demo")};
|
| 55 |
E.submitButton.onclick=()=>{files[activeFile]=editor.getValue();if(!files["main.py"].trim())return toast("main.py 不能為空");if(!confirm("新增一個正式提交版本?之前的暫存與提交都會保留。"))return;saveLocal();if(!send({type:"submit",files,project_name:E.projectName.value}))return toast("目前離線,請連線後再提交");submittedAt=Date.now();renderState();toast("已新增一個正式提交版本")};E.helpButton.onclick=()=>{needsHelp=!needsHelp;renderState();send({type:"help",needs_help:needsHelp})};
|
| 56 |
E.runButton.onclick=()=>{if(!workerReady||running)return;files[activeFile]=editor.getValue();output="";E.consoleOutput.textContent="";E.consoleInputForm.hidden=true;stdoutDecoder=new TextDecoder();stderrDecoder=new TextDecoder();Atomics.store(inputControl,0,0);runState("running","執行中");worker.postMessage({type:"run",files,entry:"main.py",inputBuffer})};E.consoleInputForm.onsubmit=e=>{e.preventDefault();if(!running||E.consoleInputForm.hidden)return;const encoded=new TextEncoder().encode(E.consoleInput.value);if(encoded.length>inputBytes.length)return toast("單次輸入內容太長");inputBytes.fill(0);inputBytes.set(encoded);Atomics.store(inputControl,1,encoded.length);append(`${E.consoleInput.value}\n`);E.consoleInputForm.hidden=true;E.runStatusText.textContent="執行中";Atomics.store(inputControl,0,1);Atomics.notify(inputControl,0,1)};E.stopButton.onclick=()=>{append("\n[已停止]\n");runState("idle","已停止");initWorker()};E.clearOutputButton.onclick=()=>{output="";E.consoleOutput.textContent="尚無執行結果。";send({type:"output",output:""})};E.syntaxSearch.oninput=renderSyntax;editor.on("change",()=>{if(!suppress){files[activeFile]=editor.getValue();sync()}});
|
| 57 |
-
E.toggleReferenceButton.onclick=()=>setLayout(!
|
| 58 |
-
(async()=>{[course]=await Promise.all([(await fetch("/api/classroom/course")).json(),loadCatalog()]);E.weekSelect.innerHTML=course.weeks.map(w=>`<option value="${w.id}">Lesson ${w.lesson??w.week}|${esc(w.title)}${w.workspace_id&&w.workspace_id!==w.id?"(同一專案)":""}</option>`).join("");const saved=localStorage.getItem("classroom_week");E.weekSelect.value=course.weeks.some(w=>w.id===saved)?saved:"week02";renderLesson();renderFiles();renderSyntax();setLayout(localStorage.getItem("classroom_hide_reference")==="true",localStorage.getItem("classroom_hide_console")==="true");initWorker();connect()})();
|
|
|
|
| 1 |
const SNIPPETS=[["基礎","輸入與輸出",'name = input("名字:")\nprint("你好", name)'],["判斷","if / elif / else",'if score >= 90:\n print("A")\nelse:\n print("繼續加油")'],["迴圈","for 與 range",'for number in range(1, 6):\n print(number)'],["迴圈","while",'while True:\n choice = input("選擇:")\n if choice == "q":\n break'],["資料","list",'items = []\nitems.append("線索")\nfor item in items:\n print(item)'],["資料","dict",'student = {"name": "Alex"}\nprint(student.get("team", "未分組"))'],["函式","def / return",'def add(a, b):\n return a + b'],["檔案","引入自己的模組",'from clues import find_clue'],["檔案","讀取 JSON",'import json\nwith open("data.json", encoding="utf-8") as file:\n data = json.load(file)'],["錯誤","try / except",'try:\n age = int(input("年齡:"))\nexcept ValueError:\n print("請輸入整數")']].map(([category,title,code])=>({category,title,code}));
|
| 2 |
+
const $=id=>document.getElementById(id),E={};["identityDialog","identityForm","identityError","classCode","displayName","studentIdentity","identityButton","connectionDot","connectionText","helpButton","weekSelect","projectName","templateButton","progressButton","importButton","exportButton","checkpointButton","submitButton","fileImport","fileList","newFileButton","activeFileLabel","syntaxSearch","categoryFilters","snippetList","snippetCount","runButton","stopButton","toggleReferenceButton","toggleConsoleButton","studentLayout","editorPanel","referencePanel","consolePanel","referenceResizer","consoleResizer","consoleInputForm","consoleInput","consoleOutput","clearOutputButton","runStatusDot","runStatusText","saveStatus","submitStatus","toast","progressDialog","progressList"].forEach(id=>E[id]=$(id));
|
| 3 |
const editor=CodeMirror.fromTextArea($("codeEditor"),{mode:"python",theme:"material-darker",lineNumbers:true,indentUnit:4,tabSize:4,extraKeys:{Tab:cm=>cm.replaceSelection(" ")}});
|
| 4 |
let studentId=localStorage.getItem("classroom_student_id")||"",allowLegacyDraft=!!studentId,catalog={courses:[]};
|
| 5 |
let course,files={"main.py":"# 在這裡寫 Python\n"},activeFile="main.py",socket,worker,workerReady=false,running=false,needsHelp=false,checkpointAt=null,submittedAt=null,output="",suppress=false,timer,reconnect,category="全部",inputBuffer,inputControl,inputBytes,stdoutDecoder=new TextDecoder(),stderrDecoder=new TextDecoder();
|
|
|
|
| 38 |
current.onclose=async e=>{if(socket!==current)return;E.connectionDot.className="status-dot error";E.connectionText.textContent="已離線";E.saveStatus.textContent="保存在此裝置";if(e.code===4004||e.code===4404){localStorage.removeItem("classroom_student_id");localStorage.removeItem("classroom_name");studentId="";await loadCatalog();E.identityError.textContent=e.code===4004?"老師已將你從名單移除。":"姓名與班級名單不符,請重新輸入。";E.identityDialog.showModal();return}reconnect=setTimeout(connect,2500)};
|
| 39 |
}
|
| 40 |
function renderState(){E.helpButton.classList.toggle("active",needsHelp);E.helpButton.textContent=needsHelp?"取消請求協助":"請老師協助";const time=value=>new Date(value).toLocaleTimeString("zh-TW",{hour:"2-digit",minute:"2-digit"}),labels=[];if(checkpointAt)labels.push(`本週暫存 ${time(checkpointAt)}`);if(submittedAt)labels.push(`正式提交 ${time(submittedAt)}`);E.submitStatus.textContent=labels.join(" · ")||"尚未暫存或提交"}
|
| 41 |
+
let editorRefreshFrame;
|
| 42 |
+
function refreshEditorLayout(){cancelAnimationFrame(editorRefreshFrame);editorRefreshFrame=requestAnimationFrame(()=>{editor.refresh();editorRefreshFrame=requestAnimationFrame(()=>editor.refresh())})}
|
| 43 |
+
const panelDefaults={reference:280,console:360},panelMinimums={reference:220,console:260},panelMaximums={reference:480,console:600};
|
| 44 |
+
function panelElement(side){return side==="reference"?E.referencePanel:E.consolePanel}
|
| 45 |
+
function resizerElement(side){return side==="reference"?E.referenceResizer:E.consoleResizer}
|
| 46 |
+
function panelLimits(side){
|
| 47 |
+
const other=side==="reference"?"console":"reference",otherHidden=E.studentLayout.classList.contains(`${other}-hidden`),otherWidth=otherHidden?0:panelElement(other).getBoundingClientRect().width;
|
| 48 |
+
const dividerCount=(E.studentLayout.classList.contains("reference-hidden")?0:1)+(E.studentLayout.classList.contains("console-hidden")?0:1);
|
| 49 |
+
const available=E.studentLayout.clientWidth-320-otherWidth-dividerCount*9;
|
| 50 |
+
return {min:panelMinimums[side],max:Math.max(panelMinimums[side],Math.min(panelMaximums[side],available))};
|
| 51 |
+
}
|
| 52 |
+
function setPanelWidth(side,value,persist=true){
|
| 53 |
+
const requested=parseFloat(value),limits=panelLimits(side),width=Math.round(Math.min(limits.max,Math.max(limits.min,Number.isFinite(requested)?requested:panelDefaults[side]))),resizer=resizerElement(side);
|
| 54 |
+
E.studentLayout.style.setProperty(`--${side}-width`,`${width}px`);
|
| 55 |
+
resizer.setAttribute("aria-valuemin",String(limits.min));resizer.setAttribute("aria-valuemax",String(limits.max));resizer.setAttribute("aria-valuenow",String(width));
|
| 56 |
+
if(persist)localStorage.setItem(`classroom_${side}_width`,String(width));
|
| 57 |
+
refreshEditorLayout();
|
| 58 |
+
}
|
| 59 |
+
function syncPanelWidths(persist=false){setPanelWidth("reference",getComputedStyle(E.studentLayout).getPropertyValue("--reference-width"),persist);setPanelWidth("console",getComputedStyle(E.studentLayout).getPropertyValue("--console-width"),persist)}
|
| 60 |
+
function setupResizer(side){
|
| 61 |
+
const resizer=resizerElement(side);let startX=0,startWidth=0;
|
| 62 |
+
const move=e=>{if(!resizer.hasPointerCapture(e.pointerId))return;const delta=e.clientX-startX;setPanelWidth(side,startWidth+(side==="reference"?delta:-delta))};
|
| 63 |
+
const finish=e=>{if(resizer.hasPointerCapture(e.pointerId))resizer.releasePointerCapture(e.pointerId);resizer.classList.remove("dragging");document.body.classList.remove("panel-resizing")};
|
| 64 |
+
resizer.addEventListener("pointerdown",e=>{if(matchMedia("(max-width: 980px)").matches)return;startX=e.clientX;startWidth=panelElement(side).getBoundingClientRect().width;resizer.setPointerCapture(e.pointerId);resizer.classList.add("dragging");document.body.classList.add("panel-resizing");e.preventDefault()});
|
| 65 |
+
resizer.addEventListener("pointermove",move);resizer.addEventListener("pointerup",finish);resizer.addEventListener("pointercancel",finish);
|
| 66 |
+
resizer.addEventListener("dblclick",()=>setPanelWidth(side,panelDefaults[side]));
|
| 67 |
+
resizer.addEventListener("keydown",e=>{if(!["ArrowLeft","ArrowRight","Home","End"].includes(e.key))return;e.preventDefault();const limits=panelLimits(side),current=panelElement(side).getBoundingClientRect().width;if(e.key==="Home")setPanelWidth(side,limits.min);else if(e.key==="End")setPanelWidth(side,limits.max);else{const dividerDirection=e.key==="ArrowRight"?1:-1;setPanelWidth(side,current+(side==="reference"?dividerDirection:-dividerDirection)*16)}});
|
| 68 |
+
}
|
| 69 |
+
function initResizablePanels(){
|
| 70 |
+
const savedReference=Number(localStorage.getItem("classroom_reference_width")),savedConsole=Number(localStorage.getItem("classroom_console_width"));
|
| 71 |
+
if(Number.isFinite(savedReference)&&savedReference>0)E.studentLayout.style.setProperty("--reference-width",`${savedReference}px`);
|
| 72 |
+
if(Number.isFinite(savedConsole)&&savedConsole>0)E.studentLayout.style.setProperty("--console-width",`${savedConsole}px`);
|
| 73 |
+
setupResizer("reference");setupResizer("console");new ResizeObserver(refreshEditorLayout).observe(E.editorPanel);window.addEventListener("resize",()=>syncPanelWidths(false));
|
| 74 |
+
}
|
| 75 |
+
function setLayout(referenceHidden,consoleHidden){E.studentLayout.classList.toggle("reference-hidden",referenceHidden);E.studentLayout.classList.toggle("console-hidden",consoleHidden);E.toggleReferenceButton.setAttribute("aria-pressed",String(referenceHidden));E.toggleConsoleButton.setAttribute("aria-pressed",String(consoleHidden));E.toggleReferenceButton.textContent=referenceHidden?"顯示側欄":"隱藏側欄";E.toggleConsoleButton.textContent=consoleHidden?"顯示 Console":"隱藏 Console";localStorage.setItem("classroom_hide_reference",String(referenceHidden));localStorage.setItem("classroom_hide_console",String(consoleHidden));requestAnimationFrame(()=>{syncPanelWidths(false);refreshEditorLayout()})}
|
| 76 |
function renderLesson(){const item=lessonInfo(),shared=item?.workspace_id&&item.workspace_id!==item.id;E.templateButton.disabled=!item?.template;E.templateButton.textContent=item?.template?"匯入專案起始套版":shared?"沿用目前專案":"尚未提供套版"}
|
| 77 |
function initWorker(){worker?.terminate();workerReady=false;running=false;E.consoleInputForm.hidden=true;E.runButton.disabled=true;E.runButton.textContent="載入 Python…";if(!crossOriginIsolated||typeof SharedArrayBuffer==="undefined"){append("瀏覽器未啟用互動式 Python。請重新整理��並使用最新版 Chrome、Edge 或 Safari。\n");runState("error","環境不支援");return}inputBuffer=new SharedArrayBuffer(16392);inputControl=new Int32Array(inputBuffer,0,2);inputBytes=new Uint8Array(inputBuffer,8);worker=new Worker("/classroom-assets/py-worker.js?v=2026fall-4");worker.onmessage=e=>{const m=e.data;if(m.type==="ready"){workerReady=true;E.runButton.disabled=false;E.runButton.textContent="執行";runState("online","準備完成")}else if(m.type==="stream"){const decoder=m.channel==="stderr"?stderrDecoder:stdoutDecoder;append(decoder.decode(m.bytes,{stream:true}))}else if(m.type==="input_request"){E.consoleInputForm.hidden=false;E.consoleInput.value="";E.runStatusText.textContent="等待輸入";E.consoleInput.focus()}else if(m.type==="done"){E.consoleInputForm.hidden=true;runState("success","執行成功")}else if(m.type==="python_error"){E.consoleInputForm.hidden=true;if(m.text)append(`${m.text}\n`);runState("error","程式錯誤")}else if(m.type==="load_error"){append(`Python 載入失敗:${m.text}\n`);runState("error","載入失敗")}};worker.onerror=e=>{append(`Python 執行器錯誤:${e.message}\n`);runState("error","執行器錯誤")}}
|
| 78 |
function runState(s,t){running=s==="running";E.runStatusDot.className=`status-dot ${s}`;E.runStatusText.textContent=t;E.runButton.disabled=!workerReady||running;E.stopButton.disabled=!running;send({type:"status",status:s==="online"?"idle":s})}
|
|
|
|
| 88 |
E.checkpointButton.onclick=()=>{files[activeFile]=editor.getValue();if(!files["main.py"].trim())return toast("main.py 不能為空");saveLocal();if(!send({type:"checkpoint",files,project_name:E.projectName.value}))return toast("目前離線,請連線後再暫存");checkpointAt=Date.now();renderState();toast("已新增一個暫存版本,不會發布到 Demo")};
|
| 89 |
E.submitButton.onclick=()=>{files[activeFile]=editor.getValue();if(!files["main.py"].trim())return toast("main.py 不能為空");if(!confirm("新增一個正式提交版本?之前的暫存與提交都會保留。"))return;saveLocal();if(!send({type:"submit",files,project_name:E.projectName.value}))return toast("目前離線,請連線後再提交");submittedAt=Date.now();renderState();toast("已新增一個正式提交版本")};E.helpButton.onclick=()=>{needsHelp=!needsHelp;renderState();send({type:"help",needs_help:needsHelp})};
|
| 90 |
E.runButton.onclick=()=>{if(!workerReady||running)return;files[activeFile]=editor.getValue();output="";E.consoleOutput.textContent="";E.consoleInputForm.hidden=true;stdoutDecoder=new TextDecoder();stderrDecoder=new TextDecoder();Atomics.store(inputControl,0,0);runState("running","執行中");worker.postMessage({type:"run",files,entry:"main.py",inputBuffer})};E.consoleInputForm.onsubmit=e=>{e.preventDefault();if(!running||E.consoleInputForm.hidden)return;const encoded=new TextEncoder().encode(E.consoleInput.value);if(encoded.length>inputBytes.length)return toast("單次輸入內容太長");inputBytes.fill(0);inputBytes.set(encoded);Atomics.store(inputControl,1,encoded.length);append(`${E.consoleInput.value}\n`);E.consoleInputForm.hidden=true;E.runStatusText.textContent="執行中";Atomics.store(inputControl,0,1);Atomics.notify(inputControl,0,1)};E.stopButton.onclick=()=>{append("\n[已停止]\n");runState("idle","已停止");initWorker()};E.clearOutputButton.onclick=()=>{output="";E.consoleOutput.textContent="尚無執行結果。";send({type:"output",output:""})};E.syntaxSearch.oninput=renderSyntax;editor.on("change",()=>{if(!suppress){files[activeFile]=editor.getValue();sync()}});
|
| 91 |
+
E.toggleReferenceButton.onclick=()=>setLayout(!E.studentLayout.classList.contains("reference-hidden"),E.studentLayout.classList.contains("console-hidden"));E.toggleConsoleButton.onclick=()=>setLayout(E.studentLayout.classList.contains("reference-hidden"),!E.studentLayout.classList.contains("console-hidden"));document.querySelectorAll(".side-tab").forEach(b=>b.onclick=()=>{document.querySelectorAll(".side-tab").forEach(x=>x.classList.toggle("active",x===b));document.querySelectorAll(".side-view").forEach(v=>v.hidden=v.id!==b.dataset.side)});document.querySelectorAll(".mobile-tab").forEach(b=>b.onclick=()=>{document.querySelectorAll(".mobile-tab").forEach(x=>x.setAttribute("aria-selected",String(x===b)));document.querySelectorAll(".student-layout>.panel").forEach(p=>p.classList.toggle("mobile-active",p.id===b.dataset.panel));if(b.dataset.panel==="editorPanel")refreshEditorLayout()});setInterval(()=>send({type:"ping"}),4000);
|
| 92 |
+
(async()=>{[course]=await Promise.all([(await fetch("/api/classroom/course")).json(),loadCatalog()]);E.weekSelect.innerHTML=course.weeks.map(w=>`<option value="${w.id}">Lesson ${w.lesson??w.week}|${esc(w.title)}${w.workspace_id&&w.workspace_id!==w.id?"(同一專案)":""}</option>`).join("");const saved=localStorage.getItem("classroom_week");E.weekSelect.value=course.weeks.some(w=>w.id===saved)?saved:"week02";renderLesson();renderFiles();renderSyntax();initResizablePanels();setLayout(localStorage.getItem("classroom_hide_reference")==="true",localStorage.getItem("classroom_hide_console")==="true");initWorker();connect()})();
|
tests/test_admin.py
CHANGED
|
@@ -25,9 +25,11 @@ class AdminServiceTests(unittest.TestCase):
|
|
| 25 |
}],
|
| 26 |
}, ensure_ascii=False), encoding="utf-8")
|
| 27 |
(self.data / "classes.json").write_text(json.dumps([{"code": "L6A", "name": "測試班", "students": []}], ensure_ascii=False), encoding="utf-8")
|
| 28 |
-
self.old = (classroom.DATA, classroom.TEMPLATES, classroom.PASSWORD, classroom.RUNTIME, classroom.DB, admin.ROOT, admin.PROJECTS, admin.commit_changes)
|
| 29 |
classroom.DATA = self.data
|
| 30 |
classroom.TEMPLATES = self.templates
|
|
|
|
|
|
|
| 31 |
classroom.PASSWORD = "teacher-test"
|
| 32 |
classroom.RUNTIME = self.root / "runtime"
|
| 33 |
classroom.DB = classroom.RUNTIME / "classroom.sqlite3"
|
|
@@ -43,7 +45,7 @@ class AdminServiceTests(unittest.TestCase):
|
|
| 43 |
admin.commit_changes = fake_commit
|
| 44 |
|
| 45 |
def tearDown(self):
|
| 46 |
-
classroom.DATA, classroom.TEMPLATES, classroom.PASSWORD, classroom.RUNTIME, classroom.DB, admin.ROOT, admin.PROJECTS, admin.commit_changes = self.old
|
| 47 |
self.temp_dir.cleanup()
|
| 48 |
|
| 49 |
def test_teacher_updates_week_and_template_files(self):
|
|
|
|
| 25 |
}],
|
| 26 |
}, ensure_ascii=False), encoding="utf-8")
|
| 27 |
(self.data / "classes.json").write_text(json.dumps([{"code": "L6A", "name": "測試班", "students": []}], ensure_ascii=False), encoding="utf-8")
|
| 28 |
+
self.old = (classroom.DATA, classroom.TEMPLATES, classroom.CHECKPOINTS, classroom.VERSIONS, classroom.PASSWORD, classroom.RUNTIME, classroom.DB, admin.ROOT, admin.PROJECTS, admin.commit_changes)
|
| 29 |
classroom.DATA = self.data
|
| 30 |
classroom.TEMPLATES = self.templates
|
| 31 |
+
classroom.CHECKPOINTS = self.data / "checkpoints"
|
| 32 |
+
classroom.VERSIONS = self.data / "versions"
|
| 33 |
classroom.PASSWORD = "teacher-test"
|
| 34 |
classroom.RUNTIME = self.root / "runtime"
|
| 35 |
classroom.DB = classroom.RUNTIME / "classroom.sqlite3"
|
|
|
|
| 45 |
admin.commit_changes = fake_commit
|
| 46 |
|
| 47 |
def tearDown(self):
|
| 48 |
+
classroom.DATA, classroom.TEMPLATES, classroom.CHECKPOINTS, classroom.VERSIONS, classroom.PASSWORD, classroom.RUNTIME, classroom.DB, admin.ROOT, admin.PROJECTS, admin.commit_changes = self.old
|
| 49 |
self.temp_dir.cleanup()
|
| 50 |
|
| 51 |
def test_teacher_updates_week_and_template_files(self):
|