CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/course/nbgrader/scores.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Component that shows all the scores for all problems and notebooks in a given assignment.
8
*/
9
10
import { Alert, Card } from "antd";
11
import { Icon } from "@cocalc/frontend/components";
12
import { useActions } from "@cocalc/frontend/app-framework";
13
import { useState } from "react";
14
import {
15
NotebookScores,
16
Score,
17
} from "@cocalc/frontend/jupyter/nbgrader/autograde";
18
import { get_nbgrader_score } from "../store";
19
import { CourseActions } from "../actions";
20
import { autograded_filename } from "../util";
21
22
interface Props {
23
nbgrader_scores: { [ipynb: string]: NotebookScores | string };
24
nbgrader_score_ids?: { [ipynb: string]: string[] };
25
assignment_id: string;
26
student_id: string;
27
name: string;
28
show_all?: boolean;
29
set_show_all?: () => void;
30
}
31
32
interface State {
33
filename?: string;
34
id?: string;
35
}
36
37
export function NbgraderScores({
38
nbgrader_scores,
39
nbgrader_score_ids,
40
assignment_id,
41
student_id,
42
name,
43
show_all,
44
set_show_all,
45
}: Props) {
46
const actions = useActions<CourseActions>({ name });
47
48
const [editingScore, setEditingScore] = useState<State>({});
49
50
function render_show_all() {
51
if (!show_all) return;
52
const v: JSX.Element[] = [];
53
for (const filename in nbgrader_scores) {
54
v.push(render_info_for_file(filename, nbgrader_scores[filename]));
55
}
56
return <div>{v}</div>;
57
}
58
59
function render_info_for_file(
60
filename: string,
61
scores: NotebookScores | string,
62
) {
63
return (
64
<div key={filename} style={{ marginBottom: "5px" }}>
65
{render_filename_links(filename)}
66
{render_scores_for_file(filename, scores)}
67
</div>
68
);
69
}
70
71
function open_filename(filename: string): void {
72
actions.assignments.open_file_in_collected_assignment(
73
assignment_id,
74
student_id,
75
filename,
76
);
77
}
78
79
function render_filename_links(filename: string) {
80
const filename2 = autograded_filename(filename);
81
return (
82
<div style={{ fontSize: "12px" }}>
83
<a
84
style={{ fontFamily: "monospace" }}
85
onClick={() => open_filename(filename)}
86
>
87
{filename}
88
</a>
89
<br />
90
<a
91
style={{ fontFamily: "monospace" }}
92
onClick={() => open_filename(filename2)}
93
>
94
{filename2}
95
</a>
96
</div>
97
);
98
}
99
100
function render_scores_for_file(
101
filename: string,
102
scores: NotebookScores | string,
103
) {
104
if (typeof scores == "string") {
105
return (
106
<Alert
107
type="error"
108
message={scores + "\n- try running nbgrader again."}
109
/>
110
);
111
}
112
const v: JSX.Element[] = [];
113
114
const ids: string[] = nbgrader_score_ids?.[filename] ?? [];
115
for (const id in scores) {
116
if (!ids.includes(id)) {
117
ids.push(id);
118
}
119
}
120
121
for (const id of ids) {
122
if (scores[id] != null) {
123
v.push(render_score(filename, id, scores[id]));
124
}
125
}
126
127
const style = { padding: "5px" };
128
return (
129
<table
130
style={{
131
border: "1px solid lightgray",
132
width: "100%",
133
borderRadius: "3px",
134
borderCollapse: "collapse",
135
}}
136
>
137
<thead>
138
<tr key={"header"} style={{ border: "1px solid grey" }}>
139
<th style={style}>Problem</th>
140
<th style={style}>Score</th>
141
</tr>
142
</thead>
143
<tbody>{v}</tbody>
144
</table>
145
);
146
}
147
148
function set_score(filename: string, id: string, value: string): void {
149
const score = parseFloat(value);
150
if (isNaN(score) || !isFinite(score)) {
151
return; // invalid scores gets thrown away
152
}
153
actions.assignments.set_specific_nbgrader_score(
154
assignment_id,
155
student_id,
156
filename,
157
id,
158
score,
159
true,
160
);
161
}
162
163
function render_assigned_score(filename: string, id: string, score: Score) {
164
if (!score.manual) {
165
return <>{score.score ?? "?"}</>;
166
}
167
168
const value = `${score.score != null ? score.score : ""}`;
169
const style = {
170
width: "48px",
171
color: "#666",
172
fontSize: "14px",
173
border: "1px solid lightgrey",
174
display: "inline-block",
175
padding: "1px",
176
};
177
if (editingScore.filename == filename && editingScore.id == id) {
178
return (
179
<input
180
spellCheck={false}
181
autoFocus
182
type="input"
183
defaultValue={value}
184
onBlur={(e) => stop_editing_score((e.target as any).value)}
185
style={style}
186
/>
187
);
188
} else {
189
return (
190
<span style={style} onClick={() => setEditingScore({ filename, id })}>
191
{value ? value : "-"}
192
</span>
193
);
194
}
195
}
196
197
function stop_editing_score(value: string): void {
198
if (editingScore.id != null && editingScore.filename != null) {
199
set_score(editingScore.filename, editingScore.id, value);
200
}
201
setEditingScore({
202
filename: undefined,
203
id: undefined,
204
});
205
}
206
207
function render_score(filename: string, id: string, score: Score) {
208
const backgroundColor = score.score == null ? "#fff1f0" : undefined;
209
const style = { padding: "5px", backgroundColor };
210
return (
211
<tr key={id}>
212
<td style={style}>{id}</td>
213
<td style={style}>
214
{render_assigned_score(filename, id, score)} / {score.points}
215
{render_needs_score(score)}
216
</td>
217
</tr>
218
);
219
}
220
221
function render_needs_score(score: Score) {
222
if (!score.manual || score.score != null) return;
223
return (
224
<div>
225
<Icon name="exclamation-triangle" /> Enter score above
226
</div>
227
);
228
}
229
230
function render_more_toggle(action_required: boolean) {
231
return (
232
<a onClick={() => set_show_all?.()}>
233
{action_required ? (
234
<>
235
<Icon name="exclamation-triangle" />{" "}
236
</>
237
) : undefined}
238
{show_all ? "" : "Edit..."}
239
</a>
240
);
241
}
242
243
function render_title(score, points, error) {
244
return (
245
<span>
246
<b>nbgrader:</b> {error ? "error" : `${score}/${points}`}
247
</span>
248
);
249
}
250
251
const { score, points, error, manual_needed } =
252
get_nbgrader_score(nbgrader_scores);
253
254
const action_required: boolean = !!(!show_all && (manual_needed || error));
255
256
const backgroundColor = action_required ? "#fff1f0" : undefined;
257
258
return (
259
<Card
260
size="small"
261
style={{ marginTop: "5px", backgroundColor }}
262
extra={render_more_toggle(action_required)}
263
title={render_title(score, points, error)}
264
styles={{ body: show_all ? {} : { padding: 0 } }}
265
>
266
{render_show_all()}
267
</Card>
268
);
269
}
270
271