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/common/student-assignment-info.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
import { Button, Col, Row, Space, Spin } from "antd";
7
import { ReactNode, useRef, useState } from "react";
8
import { FormattedMessage, useIntl } from "react-intl";
9
10
import { useActions } from "@cocalc/frontend/app-framework";
11
import { Gap, Icon, Markdown, Tip } from "@cocalc/frontend/components";
12
import ShowError from "@cocalc/frontend/components/error";
13
import { COPY_TIMEOUT_MS } from "@cocalc/frontend/course/consts";
14
import { MarkdownInput } from "@cocalc/frontend/editors/markdown-input";
15
import { labels } from "@cocalc/frontend/i18n";
16
import { NotebookScores } from "@cocalc/frontend/jupyter/nbgrader/autograde";
17
import { webapp_client } from "@cocalc/frontend/webapp-client";
18
import { to_json } from "@cocalc/util/misc";
19
import { BigTime } from ".";
20
import { CourseActions } from "../actions";
21
import { NbgraderScores } from "../nbgrader/scores";
22
import {
23
AssignmentRecord,
24
LastCopyInfo,
25
NBgraderRunInfo,
26
StudentRecord,
27
} from "../store";
28
import { AssignmentCopyType } from "../types";
29
import { useButtonSize } from "../util";
30
import { STEP_NAMES, Steps, STEPS_INTL, STEPS_INTL_ACTIVE } from "./consts";
31
32
interface StudentAssignmentInfoProps {
33
name: string;
34
title: ReactNode;
35
student: StudentRecord;
36
assignment: AssignmentRecord;
37
grade?: string;
38
comments?: string;
39
info: {
40
assignment_id: string;
41
student_id: string;
42
peer_assignment: boolean;
43
peer_collect: boolean;
44
last_assignment?: LastCopyInfo;
45
last_collect?: LastCopyInfo;
46
last_peer_assignment?: LastCopyInfo;
47
last_peer_collect?: LastCopyInfo;
48
last_return_graded?: LastCopyInfo;
49
};
50
nbgrader_scores?: { [ipynb: string]: NotebookScores | string };
51
nbgrader_score_ids?: { [ipynb: string]: string[] };
52
is_editing: boolean;
53
nbgrader_run_info?: NBgraderRunInfo;
54
}
55
56
interface RenderLastProps {
57
step: Steps;
58
type: AssignmentCopyType;
59
data?: any;
60
enable_copy?: boolean;
61
copy_tip?: string;
62
open_tip?: string;
63
omit_errors?: boolean;
64
}
65
66
const RECOPY_INIT: Record<Steps, false> = {
67
Assign: false,
68
Collect: false,
69
"Peer Assign": false,
70
Return: false,
71
"Peer Collect": false,
72
} as const;
73
74
function useRecopy(): [
75
typeof RECOPY_INIT,
76
(key: Steps, value: boolean) => void,
77
] {
78
const [recopy, set_recopy] = useState<typeof RECOPY_INIT>(RECOPY_INIT);
79
function set(key: Steps, value: boolean) {
80
set_recopy({ ...recopy, [key]: value });
81
}
82
return [recopy, set];
83
}
84
85
export function StudentAssignmentInfo({
86
name,
87
title,
88
student,
89
assignment,
90
grade = "",
91
comments = "",
92
info,
93
nbgrader_scores,
94
nbgrader_score_ids,
95
is_editing,
96
nbgrader_run_info,
97
}: StudentAssignmentInfoProps) {
98
const intl = useIntl();
99
const clicked_nbgrader = useRef<Date>();
100
const actions = useActions<CourseActions>({ name });
101
const size = useButtonSize();
102
const [recopy, set_recopy] = useRecopy();
103
104
function open(
105
type: AssignmentCopyType,
106
assignment_id: string,
107
student_id: string,
108
) {
109
return actions.assignments.open_assignment(type, assignment_id, student_id);
110
}
111
112
function copy(
113
type: AssignmentCopyType,
114
assignment_id: string,
115
student_id: string,
116
) {
117
return actions.assignments.copy_assignment(type, assignment_id, student_id);
118
}
119
120
function stop(
121
type: AssignmentCopyType,
122
assignment_id: string,
123
student_id: string,
124
) {
125
actions.assignments.stop_copying_assignment(
126
assignment_id,
127
student_id,
128
type,
129
);
130
}
131
132
function set_edited_feedback() {
133
actions.assignments.update_edited_feedback(
134
assignment.get("assignment_id"),
135
student.get("student_id"),
136
);
137
}
138
139
function stop_editing() {
140
actions.assignments.clear_edited_feedback(
141
assignment.get("assignment_id"),
142
student.get("student_id"),
143
);
144
}
145
146
function render_grade() {
147
if (is_editing) {
148
return (
149
<MarkdownInput
150
placeholder="Grade..."
151
value={grade || ""}
152
onBlur={(grade) => {
153
actions.assignments.set_grade(
154
assignment.get("assignment_id"),
155
student.get("student_id"),
156
grade,
157
);
158
}}
159
onShiftEnter={() => stop_editing()}
160
height="3em"
161
hideHelp
162
style={{ margin: "5px 0" }}
163
autoFocus
164
/>
165
);
166
} else {
167
const text = intl.formatMessage(
168
{
169
id: "course.student-assignment-info.grade.label",
170
defaultMessage: `{show, select, true {Grade: {grade}} other {Enter grade...}}`,
171
description: "Grade of an assignment in an online course",
172
},
173
{ grade, show: !!((grade ?? "").trim() || (comments ?? "").trim()) },
174
);
175
176
return (
177
<Button
178
key="edit"
179
onClick={() => set_edited_feedback()}
180
disabled={is_editing}
181
size={size}
182
>
183
{text}
184
</Button>
185
);
186
}
187
}
188
189
function render_comments() {
190
if (!is_editing) {
191
if (!comments?.trim()) return;
192
return (
193
<div style={{ width: "100%", paddingRight: "5px" }}>
194
<Markdown
195
value={comments}
196
style={{
197
width: "100%",
198
maxHeight: "4em",
199
overflowY: "auto",
200
padding: "5px",
201
border: "1px solid lightgray",
202
cursor: "pointer",
203
display: "inline-block",
204
}}
205
onClick={() => set_edited_feedback()}
206
/>
207
</div>
208
);
209
} else {
210
return (
211
<MarkdownInput
212
placeholder="Optional markdown comments..."
213
value={comments || ""}
214
onBlur={(comment) => {
215
actions.assignments.set_comment(
216
assignment.get("assignment_id"),
217
student.get("student_id"),
218
comment,
219
);
220
}}
221
onShiftEnter={() => stop_editing()}
222
height="7em"
223
hideHelp
224
/>
225
);
226
}
227
}
228
229
function render_nbgrader_scores() {
230
if (!nbgrader_scores) return;
231
return (
232
<div>
233
<NbgraderScores
234
show_all={is_editing}
235
set_show_all={() => set_edited_feedback()}
236
nbgrader_scores={nbgrader_scores}
237
nbgrader_score_ids={nbgrader_score_ids}
238
name={name}
239
student_id={student.get("student_id")}
240
assignment_id={assignment.get("assignment_id")}
241
/>
242
{render_run_nbgrader("Run nbgrader again")}
243
</div>
244
);
245
}
246
247
function render_run_nbgrader(label: JSX.Element | string) {
248
let running = false;
249
if (nbgrader_run_info != null) {
250
const t = nbgrader_run_info.get(
251
assignment.get("assignment_id") + "-" + student.get("student_id"),
252
);
253
if (t && webapp_client.server_time() - t <= 1000 * 60 * 10) {
254
// Time starting is set and it's also within the last few minutes.
255
// This "few minutes" is just in case -- we probably shouldn't need
256
// that at all ever, but it could make cocalc state usable in case of
257
// weird issues, I guess). User could also just close and re-open
258
// the course file, which resets this state completely.
259
running = true;
260
}
261
}
262
label = running ? (
263
<span>
264
{" "}
265
<Spin /> Running nbgrader
266
</span>
267
) : (
268
<span>{label}</span>
269
);
270
271
return (
272
<div style={{ marginTop: "5px" }}>
273
<Button
274
key="nbgrader"
275
disabled={running}
276
size={size}
277
onClick={() => {
278
if (
279
clicked_nbgrader.current != null &&
280
webapp_client.server_time() -
281
clicked_nbgrader.current.valueOf() <=
282
3000
283
) {
284
// User *just* clicked, and we want to avoid double click
285
// running nbgrader twice.
286
return;
287
}
288
289
clicked_nbgrader.current = new Date();
290
actions.assignments.run_nbgrader_for_one_student(
291
assignment.get("assignment_id"),
292
student.get("student_id"),
293
);
294
}}
295
>
296
<Icon name="graduation-cap" /> {label}
297
</Button>
298
</div>
299
);
300
}
301
302
function render_nbgrader() {
303
if (nbgrader_scores) {
304
return render_nbgrader_scores();
305
}
306
if (!assignment.get("nbgrader") || assignment.get("skip_grading")) return;
307
308
return render_run_nbgrader("Run nbgrader");
309
}
310
311
function render_save_button() {
312
if (!is_editing) return;
313
return (
314
<Button key="save" size={size} onClick={() => stop_editing()}>
315
Save
316
</Button>
317
);
318
}
319
320
function render_last_time(time: string | number | Date) {
321
return (
322
<div key="time" style={{ color: "#666" }}>
323
<BigTime date={time} />
324
</div>
325
);
326
}
327
328
function render_open_recopy_confirm(
329
step: Steps,
330
copy: Function,
331
copy_tip: string,
332
placement,
333
) {
334
if (recopy[step]) {
335
const v: JSX.Element[] = [];
336
v.push(
337
<Button
338
key="copy_cancel"
339
size={size}
340
onClick={() => set_recopy(step, false)}
341
>
342
{intl.formatMessage(labels.cancel)}
343
</Button>,
344
);
345
v.push(
346
<Button
347
key="recopy_confirm"
348
danger
349
size={size}
350
onClick={() => {
351
set_recopy(step, false);
352
copy();
353
}}
354
>
355
<Icon
356
name="share-square"
357
rotate={step.indexOf("ollect") !== -1 ? "180" : undefined}
358
/>{" "}
359
<FormattedMessage
360
id="course.student-assignment-info.recopy_confirm.label"
361
defaultMessage={`Yes, {activity} again`}
362
description={"Confirm an activity, like 'assign', 'collect', ..."}
363
values={{ activity: step_intl(step, false).toLowerCase() }}
364
/>
365
</Button>,
366
);
367
if (step.toLowerCase() === "assign") {
368
// inline-block because buttons above are float:left
369
v.push(
370
<div
371
key="what-happens"
372
style={{ margin: "5px", display: "inline-block" }}
373
>
374
<a
375
target="_blank"
376
href="https://doc.cocalc.com/teaching-tips_and_tricks.html#how-exactly-are-assignments-copied-to-students"
377
>
378
{intl.formatMessage({
379
id: "course.student-assignment-info.recopy.what_happens",
380
defaultMessage: "What happens when I assign again?",
381
description:
382
"Asking the question, what happens if all files are transferred to all students in an online course once again.",
383
})}
384
</a>
385
</div>,
386
);
387
}
388
return <Space wrap>{v}</Space>;
389
} else {
390
return (
391
<Button
392
key="copy"
393
type="dashed"
394
size={size}
395
onClick={() => set_recopy(step, true)}
396
>
397
<Tip title={step} placement={placement} tip={<span>{copy_tip}</span>}>
398
<Icon
399
name="share-square"
400
rotate={step.indexOf("ollect") !== -1 ? "180" : undefined}
401
/>{" "}
402
{step_intl(step, false)}...
403
</Tip>
404
</Button>
405
);
406
}
407
}
408
409
function render_open_recopy(
410
step: Steps,
411
open,
412
copy,
413
copy_tip: string,
414
open_tip: string,
415
) {
416
const placement = step === "Return" ? "left" : "right";
417
return (
418
<div key="open_recopy">
419
{render_open_recopy_confirm(step, copy, copy_tip, placement)}
420
<Gap />
421
<Button key="open" size={size} onClick={open}>
422
<Tip title="Open assignment" placement={placement} tip={open_tip}>
423
<Icon name="folder-open" /> {intl.formatMessage(labels.open)}
424
</Tip>
425
</Button>
426
</div>
427
);
428
}
429
430
function step_intl(step: Steps, active: boolean): string {
431
return intl.formatMessage(active ? STEPS_INTL_ACTIVE : STEPS_INTL, {
432
step: STEP_NAMES.indexOf(step),
433
});
434
}
435
436
function render_open_copying(step: Steps, open, stop) {
437
return (
438
<Space key="open_copying" wrap>
439
<Button key="copy" disabled={true} size={size}>
440
<Spin /> {step_intl(step, true)}
441
</Button>
442
<Button key="stop" danger onClick={stop} size={size}>
443
{intl.formatMessage(labels.cancel)} <Icon name="times" />
444
</Button>
445
<Button key="open" onClick={open} size={size}>
446
<Icon name="folder-open" /> {intl.formatMessage(labels.open)}
447
</Button>
448
</Space>
449
);
450
}
451
452
function render_copy(step: Steps, copy: () => void, copy_tip: string) {
453
let placement;
454
if (step === "Return") {
455
placement = "left";
456
}
457
return (
458
<Tip key="copy" title={step} tip={copy_tip} placement={placement}>
459
<Button onClick={copy} size={size}>
460
<Icon
461
name="share-square"
462
rotate={step.indexOf("ollect") !== -1 ? "180" : undefined}
463
/>{" "}
464
{step_intl(step, false)}
465
</Button>
466
</Tip>
467
);
468
}
469
470
function render_error(step: Steps, error) {
471
if (typeof error !== "string") {
472
error = to_json(error);
473
}
474
// We search for two different error messages, since different errors happen in
475
// KuCalc versus other places cocalc runs. It depends on what is doing the copy.
476
if (
477
error.indexOf("No such file or directory") !== -1 ||
478
error.indexOf("ENOENT") != -1
479
) {
480
error = `The student might have renamed or deleted the directory that contained their assignment. Open their project and see what happened. If they renamed it, you could rename it back, then collect the assignment again.\n${error}`;
481
} else {
482
error = `Try to ${step.toLowerCase()} again:\n` + error;
483
}
484
return (
485
<ShowError
486
key="error"
487
error={error}
488
style={{
489
marginTop: "5px",
490
maxHeight: "140px",
491
overflow: "auto",
492
display: "block",
493
}}
494
/>
495
);
496
}
497
498
function Status({
499
step,
500
type,
501
data = {},
502
enable_copy = false,
503
copy_tip = "",
504
open_tip = "",
505
omit_errors = false,
506
}: RenderLastProps): JSX.Element {
507
const do_open = () => open(type, info.assignment_id, info.student_id);
508
const do_copy = () => copy(type, info.assignment_id, info.student_id);
509
const do_stop = () => stop(type, info.assignment_id, info.student_id);
510
const v: JSX.Element[] = [];
511
if (enable_copy) {
512
if (webapp_client.server_time() - (data.start ?? 0) < COPY_TIMEOUT_MS) {
513
v.push(render_open_copying(step, do_open, do_stop));
514
} else if (data.time) {
515
v.push(
516
render_open_recopy(
517
step,
518
do_open,
519
do_copy,
520
copy_tip as string,
521
open_tip as string,
522
),
523
);
524
} else {
525
v.push(render_copy(step, do_copy, copy_tip as string));
526
}
527
}
528
if (data.time) {
529
v.push(render_last_time(data.time));
530
}
531
if (data.error && !omit_errors) {
532
v.push(render_error(step, data.error));
533
}
534
return <>{v}</>;
535
}
536
537
let show_grade_col, show_return_graded;
538
const peer_grade: boolean = !!assignment.getIn(["peer_grade", "enabled"]);
539
const skip_grading: boolean = !!assignment.get("skip_grading");
540
const skip_assignment: boolean = !!assignment.get("skip_assignment");
541
const skip_collect: boolean = !!assignment.get("skip_collect");
542
if (peer_grade) {
543
show_grade_col = !skip_grading && info.last_peer_collect;
544
show_return_graded = grade || (skip_grading && info.last_peer_collect);
545
} else {
546
show_grade_col = (!skip_grading && info.last_collect) || skip_collect;
547
show_return_graded =
548
grade ||
549
(skip_grading && info.last_collect) ||
550
(skip_grading && skip_collect);
551
}
552
553
const width = peer_grade ? 4 : 6;
554
555
function render_assignment_col() {
556
return (
557
<Col md={width} key="last_assignment">
558
<Status
559
step="Assign"
560
data={info.last_assignment}
561
type="assigned"
562
enable_copy={true}
563
copy_tip={intl.formatMessage({
564
id: "course.student-assignment-info.assignment_col.copy.tooltip",
565
defaultMessage: `Copy the assignment from your project to this student's project so they can do their homework.`,
566
description: "files of a student in an online course",
567
})}
568
open_tip={intl.formatMessage({
569
id: "course.student-assignment-info.assignment_col.open.tooltip",
570
defaultMessage: `Open the student's copy of this assignment directly in their project.
571
You will be able to see them type, chat with them, leave them hints, etc.`,
572
description: "files of a student in an online course",
573
})}
574
omit_errors={skip_assignment}
575
/>
576
</Col>
577
);
578
}
579
580
function render_collect_col() {
581
return (
582
<Col md={width} key="last_collect">
583
{skip_assignment ||
584
!(info.last_assignment != null
585
? info.last_assignment.error
586
: undefined) ? (
587
<Status
588
step="Collect"
589
data={info.last_collect}
590
type="collected"
591
enable_copy={info.last_assignment != null || skip_assignment}
592
copy_tip={intl.formatMessage({
593
id: "course.student-assignment-info.collect_col.copy.tooltip",
594
defaultMessage:
595
"Copy the assignment from your student's project back to your project so you can grade their work.",
596
description: "files of a student in an online course",
597
})}
598
open_tip={intl.formatMessage({
599
id: "course.student-assignment-info.collect_col.open.tooltip",
600
defaultMessage:
601
"Open the copy of your student's work in your own project, so that you can grade their work.",
602
description: "files of a student in an online course",
603
})}
604
omit_errors={skip_collect}
605
/>
606
) : undefined}
607
</Col>
608
);
609
}
610
611
function render_peer_assign_col() {
612
if (!peer_grade) return;
613
if (!info.peer_assignment) return;
614
if (info.last_collect?.error != null) return;
615
return (
616
<Col md={4} key="peer_assign">
617
<Status
618
step="Peer Assign"
619
data={info.last_peer_assignment}
620
type={"peer-assigned"}
621
enable_copy={info.last_collect != null}
622
copy_tip={intl.formatMessage({
623
id: "course.student-assignment-info.peer_assign_col.copy.tooltip",
624
defaultMessage:
625
"Copy collected assignments from your project to this student's project so they can grade them.",
626
description: "files of a student in an online course",
627
})}
628
open_tip={intl.formatMessage({
629
id: "course.student-assignment-info.peer_assign_col.open.tooltip",
630
defaultMessage:
631
"Open the student's copies of this assignment directly in their project, so you can see what they are peer grading.",
632
description: "files of a student in an online course",
633
})}
634
/>
635
</Col>
636
);
637
}
638
639
function render_peer_collect_col() {
640
if (!peer_grade) return;
641
if (!info.peer_collect) return;
642
return (
643
<Col md={4} key="peer_collect">
644
<Status
645
step="Peer Collect"
646
data={info.last_peer_collect}
647
type="peer-collected"
648
enable_copy={info.last_peer_assignment != null}
649
copy_tip={intl.formatMessage({
650
id: "course.student-assignment-info.peer_collect_col.copy.tooltip",
651
defaultMessage:
652
"Copy the peer-graded assignments from various student projects back to your project so you can assign their official grade.",
653
description: "files of a student in an online course",
654
})}
655
open_tip={intl.formatMessage({
656
id: "course.student-assignment-info.peer_collect_col.open.tooltip",
657
defaultMessage:
658
"Open your copy of your student's peer grading work in your own project, so that you can grade their work.",
659
660
description: "files of a student in an online course",
661
})}
662
/>
663
</Col>
664
);
665
}
666
667
function render_grade_col() {
668
// {render_enter_grade()}
669
return (
670
<Col md={width} key="grade">
671
{show_grade_col && (
672
<div>
673
{render_save_button()}
674
{render_grade()}
675
{render_comments()}
676
{render_nbgrader()}
677
</div>
678
)}
679
</Col>
680
);
681
}
682
683
function render_return_graded_col() {
684
return (
685
<Col md={width} key="return_graded">
686
{show_return_graded ? (
687
<Status
688
step="Return"
689
data={info.last_return_graded}
690
type="graded"
691
enable_copy={info.last_collect != null || skip_collect}
692
copy_tip={intl.formatMessage({
693
id: "course.student-assignment-info.graded_col.copy.tooltip",
694
defaultMessage: `Copy the graded assignment back to your student's project.`,
695
description: "files of a student in an online course",
696
})}
697
open_tip={intl.formatMessage({
698
id: "course.student-assignment-info.graded_col.open.tooltip",
699
defaultMessage: `Open the copy of your student's work that you returned to them.
700
This opens the returned assignment directly in their project.`,
701
description: "the files of a student in an online course",
702
})}
703
/>
704
) : undefined}
705
</Col>
706
);
707
}
708
709
return (
710
<div>
711
<Row
712
style={{
713
borderTop: "1px solid #aaa",
714
paddingTop: "5px",
715
paddingBottom: "5px",
716
}}
717
>
718
<Col md={4} key="title">
719
{title}
720
</Col>
721
<Col md={20} key="rest">
722
<Row>
723
{render_assignment_col()}
724
{render_collect_col()}
725
{render_peer_assign_col()}
726
{render_peer_collect_col()}
727
{render_grade_col()}
728
{render_return_graded_col()}
729
</Row>
730
</Col>
731
</Row>
732
</div>
733
);
734
}
735
736