Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/course/assignments/assignment.tsx
1503 views
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 { Alert, Button, Card, Col, Input, Popconfirm, Row, Space } from "antd";
7
import { ReactElement, useState } from "react";
8
import { DebounceInput } from "react-debounce-input";
9
import { FormattedMessage, useIntl } from "react-intl";
10
import { AppRedux, useActions } from "@cocalc/frontend/app-framework";
11
import {
12
DateTimePicker,
13
Icon,
14
IconName,
15
Loading,
16
MarkdownInput,
17
Tip,
18
} from "@cocalc/frontend/components";
19
import { course, labels } from "@cocalc/frontend/i18n";
20
import { capitalize, trunc_middle } from "@cocalc/util/misc";
21
import { CourseActions } from "../actions";
22
import { BigTime, Progress } from "../common";
23
import { STEP_NAMES, STEPS_INTL } from "../common/consts";
24
import { NbgraderButton } from "../nbgrader/nbgrader-button";
25
import type {
26
AssignmentRecord,
27
CourseStore,
28
IsGradingMap,
29
NBgraderRunInfo,
30
} from "../store";
31
import * as styles from "../styles";
32
import { AssignmentCopyStep, AssignmentStatus } from "../types";
33
import {
34
step_direction,
35
step_ready,
36
step_verb,
37
STEPS,
38
useButtonSize,
39
} from "../util";
40
import { StudentListForAssignment } from "./assignment-student-list";
41
import { ConfigurePeerGrading } from "./configure-peer";
42
import { STUDENT_SUBDIR } from "./consts";
43
import { SkipCopy } from "./skip";
44
import { ComputeServerButton } from "../compute";
45
46
interface AssignmentProps {
47
active_feedback_edits: IsGradingMap;
48
assignment: AssignmentRecord;
49
background?: string;
50
expand_peer_config?: boolean;
51
frame_id?: string;
52
is_expanded?: boolean;
53
name: string;
54
nbgrader_run_info?: NBgraderRunInfo;
55
project_id: string;
56
redux: AppRedux;
57
students: object;
58
user_map: object;
59
}
60
61
function useCopyConfirmState() {
62
const [copy_confirm, set_copy_confirm] = useState<{
63
[state in AssignmentCopyStep]: boolean;
64
}>({
65
assignment: false,
66
collect: false,
67
peer_assignment: false,
68
peer_collect: false,
69
return_graded: false,
70
});
71
72
// modify flags, don't replace this entirely
73
function set(state: AssignmentCopyStep, value: boolean): void {
74
set_copy_confirm((prev) => ({ ...prev, [state]: value }));
75
}
76
77
return { copy_confirm, set };
78
}
79
80
export function Assignment({
81
active_feedback_edits,
82
assignment,
83
background,
84
expand_peer_config,
85
frame_id,
86
is_expanded,
87
name,
88
nbgrader_run_info,
89
project_id,
90
redux,
91
students,
92
user_map,
93
}: AssignmentProps) {
94
const intl = useIntl();
95
const size = useButtonSize();
96
97
const [
98
copy_assignment_confirm_overwrite,
99
set_copy_assignment_confirm_overwrite,
100
] = useState<boolean>(false);
101
const [
102
copy_assignment_confirm_overwrite_text,
103
set_copy_assignment_confirm_overwrite_text,
104
] = useState<string>("");
105
const [student_search, set_student_search] = useState<string>("");
106
const [copy_confirm, set_copy_confirm] = useState<boolean>(false);
107
108
const { copy_confirm: copy_confirm_state, set: set_copy_confirm_state } =
109
useCopyConfirmState();
110
const { copy_confirm: copy_confirm_all, set: set_copy_confirm_all } =
111
useCopyConfirmState();
112
113
const actions = useActions<CourseActions>({ name });
114
115
function get_store(): CourseStore {
116
return actions.get_store();
117
}
118
119
function is_peer_graded() {
120
return !!assignment.getIn(["peer_grade", "enabled"]);
121
}
122
123
function render_due() {
124
return (
125
<Space>
126
<div style={{ marginTop: "8px", color: "#666" }}>
127
<Tip
128
placement="top"
129
title="Set the due date"
130
tip="Set the due date for the assignment. This changes how the list of assignments is sorted. Note that you must explicitly click a button to collect student assignments when they are due -- they are not automatically collected on the due date. {intl.formatMessage(labels.you)} should also tell students when assignments are due (e.g., at the top of the assignment)."
131
>
132
Due
133
</Tip>
134
</div>
135
<DateTimePicker
136
placeholder={"Set Due Date"}
137
value={assignment.get("due_date")}
138
onChange={date_change}
139
/>
140
</Space>
141
);
142
}
143
144
function date_change(date): void {
145
actions.assignments.set_due_date(
146
assignment.get("assignment_id"),
147
date != null ? date.toISOString() : undefined,
148
);
149
}
150
151
function render_note() {
152
return (
153
<Row key="note" style={styles.note}>
154
<Col xs={4}>
155
<Tip
156
title="Notes about this assignment"
157
tip="Record notes about this assignment here. These notes are only visible to you, not to your students. Put any instructions to students about assignments in a file in the directory that contains the assignment."
158
>
159
Private Assignment Notes
160
<br />
161
<span style={{ color: "#666" }} />
162
</Tip>
163
</Col>
164
<Col xs={20}>
165
<MarkdownInput
166
persist_id={
167
assignment.get("path") + assignment.get("assignment_id") + "note"
168
}
169
attach_to={name}
170
rows={6}
171
placeholder="Private notes about this assignment (not visible to students)"
172
default_value={assignment.get("note")}
173
on_save={(value) =>
174
actions.assignments.set_assignment_note(
175
assignment.get("assignment_id"),
176
value,
177
)
178
}
179
/>
180
</Col>
181
</Row>
182
);
183
}
184
185
function render_export_file_use_times() {
186
return (
187
<Row key="file-use-times-export-used">
188
<Col xs={4}>
189
<Tip
190
title="Export when students used files"
191
tip="Export a JSON file containing extensive information about exactly when students have opened or edited files in this assignment. The JSON file will open in a new tab; the access_times (in milliseconds since the UNIX epoch) are when they opened the file and the edit_times are when they actually changed it through CoCalc's web-based editor."
192
>
193
Export file use times
194
<br />
195
<span style={{ color: "#666" }} />
196
</Tip>
197
</Col>
198
<Col xs={20}>
199
<Button
200
onClick={() =>
201
actions.export.file_use_times(assignment.get("assignment_id"))
202
}
203
>
204
Export file use times for this assignment
205
</Button>
206
</Col>
207
</Row>
208
);
209
}
210
211
function render_export_assignment() {
212
return (
213
<Row key="file-use-times-export-collected">
214
<Col xs={4}>
215
<Tip
216
title="Export collected student files"
217
tip="Export all student work to files in a single directory that are easy to grade or archive outside of CoCalc. Any Jupyter notebooks or Sage worksheets are first converted to PDF (if possible), and all files are renamed with the student as a filename prefix."
218
>
219
Export collected student files
220
<br />
221
<span style={{ color: "#666" }} />
222
</Tip>
223
</Col>
224
<Col xs={20}>
225
<Button
226
onClick={() =>
227
actions.assignments.export_collected(
228
assignment.get("assignment_id"),
229
)
230
}
231
>
232
Export collected student files to single directory, converting
233
Jupyter notebooks to pdf and html for easy offline grading.
234
</Button>
235
</Col>
236
</Row>
237
);
238
}
239
240
function render_no_content() {
241
if (assignment.get("deleted")) {
242
// no point
243
return null;
244
}
245
return (
246
<div style={{ margin: "15px auto", maxWidth: "800px", fontSize: "12pt" }}>
247
There are no files in this assignment yet. Please{" "}
248
<a onClick={open_assignment_path}>open the directory</a> for this
249
assignment, then create, upload, or copy any content you want into that
250
directory. {intl.formatMessage(labels.you)} will then be able to send it
251
to all of your students.
252
</div>
253
);
254
}
255
256
function render_more_header(num_files: number) {
257
let width;
258
const status: AssignmentStatus | undefined =
259
get_store().get_assignment_status(assignment.get("assignment_id"));
260
if (status == null) {
261
return <Loading key="loading_more" />;
262
}
263
const v: ReactElement<any>[] = [];
264
265
const bottom = {
266
borderBottom: "1px solid grey",
267
paddingBottom: "15px",
268
marginBottom: "15px",
269
};
270
v.push(
271
<Row key="header3" style={{ ...bottom, marginTop: "15px" }}>
272
<Col md={4}>{render_open_button()}</Col>
273
<Col md={20}>
274
<Row>
275
<Col md={8} style={{ fontSize: "14px" }} key="due">
276
{render_due()}
277
</Col>
278
<Col md={16} key="delete">
279
<Row>
280
<Col md={10}>{render_peer_button()}</Col>
281
<Col md={14}>
282
<ComputeServerButton
283
actions={actions}
284
unit={assignment as any}
285
/>
286
<span className="pull-right">{render_delete_button()}</span>
287
</Col>
288
</Row>
289
</Col>
290
</Row>
291
</Col>
292
</Row>,
293
);
294
295
if (expand_peer_config) {
296
v.push(
297
<Row key="header2-peer" style={bottom}>
298
<Col md={20} offset={4}>
299
{render_configure_peer()}
300
</Col>
301
</Row>,
302
);
303
}
304
305
const peer = is_peer_graded();
306
if (peer) {
307
width = 4;
308
} else {
309
width = 6;
310
}
311
312
if (num_files > 0) {
313
const buttons: ReactElement<any>[] = [];
314
const insert_grade_button = (key: string) => {
315
const b2 = render_skip_grading_button(status);
316
return buttons.push(
317
<Col md={width} key={key}>
318
{render_nbgrader_button(status)}
319
{b2}
320
</Col>,
321
);
322
};
323
324
for (const name of STEPS(peer)) {
325
const b = render_button(name, status);
326
// squeeze in the skip grading button (don't add it to STEPS!)
327
if (!peer && name === "return_graded") {
328
insert_grade_button("skip_grading");
329
}
330
if (b != null) {
331
buttons.push(
332
<Col md={width} key={name}>
333
{b}
334
</Col>,
335
);
336
if (peer && name === "peer_collect") {
337
insert_grade_button("skip_peer_collect");
338
}
339
}
340
}
341
342
v.push(
343
<Row key="header-control">
344
<Col md={4} key="search" style={{ paddingRight: "15px" }}>
345
<DebounceInput
346
debounceTimeout={500}
347
element={Input as any}
348
placeholder={"Filter students..."}
349
value={student_search}
350
onChange={(e) => set_student_search(e.target.value)}
351
/>
352
</Col>
353
<Col md={20} key="buttons">
354
<Row>{buttons}</Row>
355
</Col>
356
</Row>,
357
);
358
359
v.push(
360
<Row key="header2-copy">
361
<Col md={20} offset={4}>
362
{render_copy_confirms(status)}
363
</Col>
364
</Row>,
365
);
366
}
367
/* The whiteSpace:'normal' here is because we put this in an
368
antd Card title, which has line wrapping disabled. */
369
return <div style={{ whiteSpace: "normal" }}>{v}</div>;
370
}
371
372
function render_more() {
373
const num_files = assignment.get("listing")?.size ?? 0;
374
let body;
375
if (num_files == 0) {
376
body = render_no_content();
377
} else {
378
body = (
379
<>
380
<StudentListForAssignment
381
redux={redux}
382
frame_id={frame_id}
383
name={name}
384
assignment={assignment}
385
students={students}
386
user_map={user_map}
387
active_feedback_edits={active_feedback_edits}
388
nbgrader_run_info={nbgrader_run_info}
389
search={student_search}
390
/>
391
{render_note()}
392
<br />
393
<hr />
394
<br />
395
{render_export_file_use_times()}
396
<br />
397
{render_export_assignment()}
398
</>
399
);
400
}
401
return (
402
<Row key="more">
403
<Col sm={24}>
404
<Card title={render_more_header(num_files)}> {body}</Card>
405
</Col>
406
</Row>
407
);
408
}
409
410
function open_assignment_path(): void {
411
if (assignment.get("listing")?.size == 0) {
412
// there are no files yet, so we *close* the assignment
413
// details panel. This is just **a hack** so that the user
414
// has to re-open it after adding files, which will trigger
415
// updating the directory listing, hence show the rest
416
// of the assignment info. The alternative would be
417
// polling the directory or watching listings, which is
418
// a lot more work to properly implement.
419
actions.toggle_item_expansion(
420
"assignment",
421
assignment.get("assignment_id"),
422
);
423
}
424
return redux
425
.getProjectActions(project_id)
426
.open_directory(assignment.get("path"));
427
}
428
429
function render_open_button() {
430
return (
431
<Tip
432
key="open"
433
title={
434
<span>
435
<Icon name="folder-open" /> Open Folder
436
</span>
437
}
438
tip="Open the directory in the current project that contains the original files for this assignment. Edit files in this folder to create the content that your students will see when they receive an assignment."
439
>
440
<Button onClick={open_assignment_path}>
441
<Icon name="folder-open" /> {intl.formatMessage(labels.open)}...
442
</Button>
443
</Tip>
444
);
445
}
446
447
function show_copy_confirm(): void {
448
set_copy_confirm_state("assignment", true);
449
set_copy_confirm(true);
450
const assignment_id: string | undefined = assignment.get("assignment_id");
451
actions.assignments.update_listing(assignment_id);
452
}
453
454
function render_assignment_button(status) {
455
const last_assignment = assignment.get("last_assignment");
456
// Primary if it hasn't been assigned before or if it hasn't started assigning.
457
let type;
458
if (
459
!last_assignment ||
460
!(last_assignment.get("time") || last_assignment.get("start"))
461
) {
462
type = "primary";
463
} else {
464
type = "default";
465
}
466
if (status.assignment > 0 && status.not_assignment === 0) {
467
type = "dashed";
468
}
469
470
const label = intl.formatMessage(STEPS_INTL, {
471
step: STEP_NAMES.indexOf("Assign"),
472
});
473
const you = intl.formatMessage(labels.you);
474
const students = intl.formatMessage(course.students);
475
const tooltip = intl.formatMessage({
476
id: "course.assignments.assign.tooltip",
477
defaultMessage:
478
"Copy the files for this assignment from this project to all other student projects.",
479
description: "Students in an online course",
480
});
481
482
return [
483
<Button
484
key="assign"
485
type={type}
486
size={size}
487
onClick={show_copy_confirm}
488
disabled={copy_confirm}
489
>
490
<Tip
491
title={
492
<span>
493
{label}: <Icon name="user-secret" /> {you}{" "}
494
<Icon name="arrow-right" /> <Icon name="users" /> {students}{" "}
495
</span>
496
}
497
tip={tooltip}
498
>
499
<Icon name="share-square" /> {label}...
500
</Tip>
501
</Button>,
502
<Progress
503
key="progress"
504
done={status.assignment}
505
not_done={status.not_assignment}
506
step="assigned"
507
skipped={assignment.get("skip_assignment")}
508
/>,
509
];
510
}
511
512
function render_copy_confirms(status) {
513
const steps = STEPS(is_peer_graded());
514
const result: (ReactElement<any> | undefined)[] = [];
515
for (const step of steps) {
516
if (copy_confirm_state[step]) {
517
result.push(render_copy_confirm(step, status));
518
} else {
519
result.push(undefined);
520
}
521
}
522
return result;
523
}
524
525
function render_copy_confirm(step, status) {
526
return (
527
<span key={`copy_confirm_${step}`}>
528
{status[step] === 0
529
? render_copy_confirm_to_all(step, status)
530
: undefined}
531
{status[step] !== 0
532
? render_copy_confirm_to_all_or_new(step, status)
533
: undefined}
534
</span>
535
);
536
}
537
538
function render_copy_cancel(step) {
539
const cancel = () => {
540
set_copy_confirm_state(step, false);
541
set_copy_confirm_all(step, false);
542
set_copy_confirm(false);
543
set_copy_assignment_confirm_overwrite(false);
544
};
545
return (
546
<Button key="cancel" onClick={cancel} size={size}>
547
{intl.formatMessage(labels.close)}
548
</Button>
549
);
550
}
551
552
function render_copy_assignment_confirm_overwrite(step) {
553
if (!copy_assignment_confirm_overwrite) {
554
return;
555
}
556
const do_it = () => {
557
copy_assignment(step, false, true);
558
set_copy_assignment_confirm_overwrite(false);
559
set_copy_assignment_confirm_overwrite_text("");
560
};
561
return (
562
<div style={{ marginTop: "15px" }}>
563
Type in "OVERWRITE" if you are sure you want to overwrite any work they
564
may have.
565
<Input
566
autoFocus
567
onChange={(e) =>
568
set_copy_assignment_confirm_overwrite_text((e.target as any).value)
569
}
570
style={{ marginTop: "1ex" }}
571
/>
572
<Space style={{ textAlign: "center", marginTop: "15px" }}>
573
{render_copy_cancel(step)}
574
<Button
575
disabled={copy_assignment_confirm_overwrite_text !== "OVERWRITE"}
576
danger
577
onClick={do_it}
578
>
579
<Icon name="exclamation-triangle" /> Confirm replacing files
580
</Button>
581
</Space>
582
</div>
583
);
584
}
585
586
function copy_assignment(
587
step,
588
new_only: boolean,
589
overwrite: boolean = false,
590
) {
591
// assign assignment to all (non-deleted) students
592
const assignment_id: string | undefined = assignment.get("assignment_id");
593
if (assignment_id == null) throw Error("bug");
594
switch (step) {
595
case "assignment":
596
actions.assignments.copy_assignment_to_all_students(
597
assignment_id,
598
new_only,
599
overwrite,
600
);
601
break;
602
case "collect":
603
actions.assignments.copy_assignment_from_all_students(
604
assignment_id,
605
new_only,
606
);
607
break;
608
case "peer_assignment":
609
actions.assignments.peer_copy_to_all_students(assignment_id, new_only);
610
break;
611
case "peer_collect":
612
actions.assignments.peer_collect_from_all_students(
613
assignment_id,
614
new_only,
615
);
616
break;
617
case "return_graded":
618
actions.assignments.return_assignment_to_all_students(
619
assignment_id,
620
new_only,
621
);
622
break;
623
default:
624
console.log(`BUG -- unknown step: ${step}`);
625
}
626
set_copy_confirm_state(step, false);
627
set_copy_confirm_all(step, false);
628
set_copy_confirm(false);
629
}
630
631
function render_skip(step: AssignmentCopyStep) {
632
if (step === "return_graded") {
633
return;
634
}
635
return (
636
<div style={{ float: "right" }}>
637
<SkipCopy assignment={assignment} step={step} actions={actions} />
638
</div>
639
);
640
}
641
642
function render_has_student_subdir(step: AssignmentCopyStep) {
643
if (step != "assignment" || !assignment.get("has_student_subdir")) return;
644
return (
645
<Alert
646
style={{ marginBottom: "15px" }}
647
type="info"
648
message={`NOTE: Only the ${STUDENT_SUBDIR}/ subdirectory will be copied to the students.`}
649
/>
650
);
651
}
652
653
function render_parallel() {
654
const n = get_store().get_copy_parallel();
655
return (
656
<Tip
657
title={`Parallel limit: copy ${n} assignments at a time`}
658
tip="This is the max number of assignments to copy in parallel. Change this in course configuration."
659
>
660
<div style={{ marginTop: "10px", fontWeight: 400 }}>
661
Copy up to {n} assignments at once.
662
</div>
663
</Tip>
664
);
665
}
666
667
function render_copy_confirm_to_all(step: AssignmentCopyStep, status) {
668
const n = status[`not_${step}`];
669
const message = (
670
<div>
671
<div style={{ marginBottom: "15px" }}>
672
{capitalize(step_verb(step))} this homework {step_direction(step)} the{" "}
673
{n} student{n > 1 ? "s" : ""}
674
{step_ready(step, n)}?
675
</div>
676
{render_has_student_subdir(step)}
677
{render_skip(step)}
678
<Space wrap>
679
{render_copy_cancel(step)}
680
<Button
681
key="yes"
682
type="primary"
683
onClick={() => copy_assignment(step, false)}
684
>
685
Yes
686
</Button>
687
</Space>
688
{render_parallel()}
689
</div>
690
);
691
return (
692
<Alert
693
type="warning"
694
key={`${step}_confirm_to_all`}
695
style={{ marginTop: "15px" }}
696
message={message}
697
/>
698
);
699
}
700
701
function copy_confirm_all_caution(step: AssignmentCopyStep) {
702
switch (step) {
703
case "assignment":
704
return (
705
<span>
706
This will recopy all of the files to them. CAUTION: if you update a
707
file that a student has also worked on, their work will get copied
708
to a backup file ending in a tilde, or possibly only be available in
709
snapshots. Select "Replace student files!" in case you do <b>not</b>{" "}
710
want to create any backups and also <b>delete</b> all other files in
711
the assignment folder of their projects.{" "}
712
<a
713
target="_blank"
714
href="https://github.com/sagemathinc/cocalc/wiki/CourseCopy"
715
>
716
(more details)
717
</a>
718
.
719
</span>
720
);
721
case "collect":
722
return "This will recollect all of the homework from them. CAUTION: if you have graded/edited a file that a student has updated, your work will get copied to a backup file ending in a tilde, or possibly only be available in snapshots.";
723
case "return_graded":
724
return "This will rereturn all of the graded files to them.";
725
case "peer_assignment":
726
return "This will recopy all of the files to them. CAUTION: if there is a file a student has also worked on grading, their work will get copied to a backup file ending in a tilde, or possibly be only available in snapshots.";
727
case "peer_collect":
728
return "This will recollect all of the peer-graded homework from the students. CAUTION: if you have graded/edited a previously collected file that a student has updated, your work will get copied to a backup file ending in a tilde, or possibly only be available in snapshots.";
729
}
730
}
731
732
function render_copy_confirm_overwrite_all(step: AssignmentCopyStep) {
733
return (
734
<div key={"copy_confirm_overwrite_all"} style={{ marginTop: "15px" }}>
735
<div style={{ marginBottom: "15px" }}>
736
{copy_confirm_all_caution(step)}
737
</div>
738
<Space wrap>
739
{render_copy_cancel(step)}
740
<Button
741
key={"all"}
742
type={"dashed"}
743
disabled={copy_assignment_confirm_overwrite}
744
onClick={() => copy_assignment(step, false)}
745
>
746
Yes, do it (with backup)
747
</Button>
748
{step === "assignment" ? (
749
<Button
750
key={"all-overwrite"}
751
type={"dashed"}
752
onClick={() => set_copy_assignment_confirm_overwrite(true)}
753
disabled={copy_assignment_confirm_overwrite}
754
>
755
Replace student files!
756
</Button>
757
) : undefined}
758
</Space>
759
{render_copy_assignment_confirm_overwrite(step)}
760
</div>
761
);
762
}
763
764
function render_copy_confirm_to_all_or_new(step: AssignmentCopyStep, status) {
765
const n = status[`not_${step}`];
766
const m = n + status[step];
767
const message = (
768
<div>
769
<div style={{ marginBottom: "15px" }}>
770
{capitalize(step_verb(step))} this homework {step_direction(step)}
771
...
772
</div>
773
{render_has_student_subdir(step)}
774
{render_skip(step)}
775
<Space wrap>
776
{render_copy_cancel(step)}
777
<Button
778
key="all"
779
danger
780
onClick={() => {
781
set_copy_confirm_all(step, true);
782
set_copy_confirm(true);
783
}}
784
disabled={copy_confirm_all[step]}
785
>
786
{step === "assignment" ? "All" : "The"} {m} students
787
{step_ready(step, m)}...
788
</Button>
789
{n ? (
790
<Button
791
key="new"
792
type="primary"
793
onClick={() => copy_assignment(step, true)}
794
>
795
The {n} student{n > 1 ? "s" : ""} not already {step_verb(step)}
796
ed {step_direction(step)}
797
</Button>
798
) : undefined}
799
</Space>
800
{copy_confirm_all[step]
801
? render_copy_confirm_overwrite_all(step)
802
: undefined}
803
{render_parallel()}
804
</div>
805
);
806
return (
807
<Alert
808
type="warning"
809
key={`${step}_confirm_to_all_or_new`}
810
style={{ marginTop: "15px" }}
811
message={message}
812
/>
813
);
814
}
815
816
function render_collect_tip() {
817
return (
818
<span key="normal">
819
<FormattedMessage
820
id="course.assignments.collect.tooltip"
821
defaultMessage={`Collect an assignment from all of your students.
822
(There is currently no way to schedule collection at a specific time;
823
instead, collection happens when you click the button.)`}
824
/>
825
</span>
826
);
827
}
828
829
function render_button(state: AssignmentCopyStep, status) {
830
switch (state) {
831
case "collect":
832
return render_collect_button(status);
833
case "return_graded":
834
return render_return_graded_button(status);
835
case "peer_assignment":
836
return render_peer_assignment_button(status);
837
case "peer_collect":
838
return render_peer_collect_button(status);
839
case "assignment":
840
return render_assignment_button(status);
841
}
842
}
843
844
function render_collect_button(status) {
845
if (status.assignment === 0) {
846
// no button if nothing ever assigned
847
return;
848
}
849
let type;
850
if (status.collect > 0) {
851
// Have already collected something
852
if (status.not_collect === 0) {
853
type = "dashed";
854
} else {
855
type = "default";
856
}
857
} else {
858
type = "primary";
859
}
860
return [
861
<Button
862
key="collect"
863
onClick={() => {
864
set_copy_confirm_state("collect", true);
865
set_copy_confirm(true);
866
}}
867
disabled={copy_confirm}
868
type={type}
869
size={size}
870
>
871
<Tip
872
title={
873
<span>
874
Collect: <Icon name="users" />{" "}
875
{intl.formatMessage(course.students)} <Icon name="arrow-right" />{" "}
876
<Icon name="user-secret" /> You
877
</span>
878
}
879
tip={render_collect_tip()}
880
>
881
<Icon name="share-square" rotate={"180"} />{" "}
882
{intl.formatMessage(STEPS_INTL, {
883
step: STEP_NAMES.indexOf("Collect"),
884
})}
885
...
886
</Tip>
887
</Button>,
888
<Progress
889
key="progress"
890
done={status.collect}
891
not_done={status.not_collect}
892
step="collected"
893
skipped={assignment.get("skip_collect")}
894
/>,
895
];
896
}
897
898
function render_peer_assign_tip() {
899
return (
900
<span key="normal">
901
Send copies of collected homework out to all students for peer grading.
902
</span>
903
);
904
}
905
906
function render_peer_assignment_button(status) {
907
// Render the "Peer Assign..." button in the top row, for peer assigning to all
908
// students in the course.
909
if (status.peer_assignment == null) {
910
// not peer graded
911
return;
912
}
913
if (status.not_collect + status.not_assignment > 0) {
914
// collect everything before peer grading
915
return;
916
}
917
if (status.collect === 0) {
918
// nothing to peer assign
919
return;
920
}
921
let type;
922
if (status.peer_assignment > 0) {
923
// haven't peer-assigned anything yet
924
if (status.not_peer_assignment === 0) {
925
type = "dashed";
926
} else {
927
type = "default";
928
}
929
} else {
930
type = "primary";
931
}
932
const label = intl.formatMessage(STEPS_INTL, {
933
step: STEP_NAMES.indexOf("Peer Assign"),
934
});
935
return [
936
<Button
937
key="peer-assign"
938
onClick={() => {
939
set_copy_confirm_state("peer_assignment", true);
940
set_copy_confirm(true);
941
}}
942
disabled={copy_confirm}
943
type={type}
944
size={size}
945
>
946
<Tip
947
title={
948
<span>
949
{label}: <Icon name="users" /> {intl.formatMessage(labels.you)}{" "}
950
<Icon name="arrow-right" /> <Icon name="user-secret" />{" "}
951
{intl.formatMessage(course.students)}
952
</span>
953
}
954
tip={render_peer_assign_tip()}
955
>
956
<Icon name="share-square" /> {label}...
957
</Tip>
958
</Button>,
959
<Progress
960
key="progress"
961
done={status.peer_assignment}
962
not_done={status.not_peer_assignment}
963
step="peer assigned"
964
/>,
965
];
966
}
967
968
function render_peer_collect_tip() {
969
return (
970
<span key="normal">Collect the peer grading that your students did.</span>
971
);
972
}
973
974
function render_peer_collect_button(status) {
975
// Render the "Peer Collect..." button in the top row, for collecting peer grading from all
976
// students in the course.
977
if (status.peer_collect == null) {
978
return;
979
}
980
if (status.peer_assignment === 0) {
981
// haven't even peer assigned anything -- so nothing to collect
982
return;
983
}
984
if (status.not_peer_assignment > 0) {
985
// everybody must have received peer assignment, or collecting isn't allowed
986
return;
987
}
988
let type;
989
if (status.peer_collect > 0) {
990
// haven't peer-collected anything yet
991
if (status.not_peer_collect === 0) {
992
type = "dashed";
993
} else {
994
type = "default";
995
}
996
} else {
997
// warning, since we have already collected and this may overwrite
998
type = "primary";
999
}
1000
const label = intl.formatMessage(STEPS_INTL, {
1001
step: STEP_NAMES.indexOf("Peer Collect"),
1002
});
1003
return [
1004
<Button
1005
key="peer-collect"
1006
onClick={() => {
1007
set_copy_confirm_state("peer_collect", true);
1008
set_copy_confirm(true);
1009
}}
1010
disabled={copy_confirm}
1011
type={type}
1012
size={size}
1013
>
1014
<Tip
1015
title={
1016
<span>
1017
{label}: <Icon name="users" />{" "}
1018
{intl.formatMessage(course.students)} <Icon name="arrow-right" />{" "}
1019
<Icon name="user-secret" /> You
1020
</span>
1021
}
1022
tip={render_peer_collect_tip()}
1023
>
1024
<Icon name="share-square" rotate="180" /> {label}...
1025
</Tip>
1026
</Button>,
1027
<Progress
1028
key="progress"
1029
done={status.peer_collect}
1030
not_done={status.not_peer_collect}
1031
step="peer collected"
1032
/>,
1033
];
1034
}
1035
1036
function toggle_skip_grading() {
1037
actions.assignments.set_skip(
1038
assignment.get("assignment_id"),
1039
"grading",
1040
!assignment.get("skip_grading"),
1041
);
1042
}
1043
1044
function render_skip_grading_button(status) {
1045
if (status.collect === 0) {
1046
// No button if nothing collected.
1047
return;
1048
}
1049
const icon: IconName = assignment.get("skip_grading")
1050
? "check-square-o"
1051
: "square-o";
1052
return (
1053
<Button onClick={toggle_skip_grading} size={size}>
1054
<Icon name={icon} /> Skip entering grades
1055
</Button>
1056
);
1057
}
1058
1059
function render_nbgrader_button(status) {
1060
if (
1061
status.collect === 0 ||
1062
!assignment.get("nbgrader") ||
1063
assignment.get("skip_grading")
1064
) {
1065
// No button if nothing collected or not nbgrader support or
1066
// decided to skip grading
1067
return;
1068
}
1069
1070
return (
1071
<NbgraderButton
1072
assignment_id={assignment.get("assignment_id")}
1073
name={name}
1074
/>
1075
);
1076
}
1077
1078
function render_return_graded_button(status) {
1079
if (status.collect === 0) {
1080
// No button if nothing collected.
1081
return;
1082
}
1083
if (status.peer_collect != null && status.peer_collect === 0) {
1084
// Peer grading enabled, but we didn't collect anything yet
1085
return;
1086
}
1087
if (
1088
!assignment.get("skip_grading") &&
1089
status.not_return_graded === 0 &&
1090
status.return_graded === 0
1091
) {
1092
// Nothing unreturned and ungraded yet and also nothing returned yet
1093
return;
1094
}
1095
let type;
1096
if (status.return_graded > 0) {
1097
// Have already returned some
1098
if (status.not_return_graded === 0) {
1099
type = "dashed";
1100
} else {
1101
type = "default";
1102
}
1103
} else {
1104
type = "primary";
1105
}
1106
const label = intl.formatMessage(STEPS_INTL, {
1107
step: STEP_NAMES.indexOf("Return"),
1108
});
1109
return [
1110
<Button
1111
key="return"
1112
onClick={() => {
1113
set_copy_confirm_state("return_graded", true);
1114
set_copy_confirm(true);
1115
}}
1116
disabled={copy_confirm}
1117
type={type}
1118
size={size}
1119
>
1120
<Tip
1121
title={
1122
<span>
1123
{label}: <Icon name="user-secret" /> You{" "}
1124
<Icon name="arrow-right" /> <Icon name="users" />{" "}
1125
{intl.formatMessage(course.students)}{" "}
1126
</span>
1127
}
1128
tip="Copy the graded versions of files for this assignment from this project to all other student projects."
1129
>
1130
<Icon name="share-square" /> {label}...
1131
</Tip>
1132
</Button>,
1133
<Progress
1134
key="progress"
1135
done={status.return_graded}
1136
not_done={status.not_return_graded}
1137
step="returned"
1138
/>,
1139
];
1140
}
1141
1142
function delete_assignment() {
1143
actions.assignments.delete_assignment(assignment.get("assignment_id"));
1144
}
1145
1146
function undelete_assignment() {
1147
return actions.assignments.undelete_assignment(
1148
assignment.get("assignment_id"),
1149
);
1150
}
1151
1152
function render_delete_button() {
1153
if (assignment.get("deleted")) {
1154
return (
1155
<Tip
1156
key="delete"
1157
placement="left"
1158
title={intl.formatMessage({
1159
id: "course.assignment.undelete.title",
1160
defaultMessage: "Undelete assignment",
1161
})}
1162
tip={intl.formatMessage({
1163
id: "course.assignment.undelete.tooltip",
1164
defaultMessage:
1165
"Make the assignment visible again in the assignment list and in student grade lists.",
1166
})}
1167
>
1168
<Button onClick={undelete_assignment}>
1169
<Icon name="trash" /> {intl.formatMessage(labels.undelete)}
1170
</Button>
1171
</Tip>
1172
);
1173
} else {
1174
return (
1175
<Popconfirm
1176
title={
1177
<div style={{ maxWidth: "400px" }}>
1178
<FormattedMessage
1179
id="course.assignment.delete.confirm.info"
1180
defaultMessage={`<b>Are you sure you want to delete {name}"?</b>
1181
{br}
1182
This removes it from the assignment list and student grade lists,
1183
but does not delete any files off of disk.
1184
You can undelete an assignment later by showing it using the 'Show deleted assignments' button.`}
1185
values={{
1186
name: trunc_middle(assignment.get("path"), 24),
1187
br: <br />,
1188
}}
1189
/>
1190
</div>
1191
}
1192
onConfirm={delete_assignment}
1193
cancelText={intl.formatMessage(labels.cancel)}
1194
>
1195
<Button size={size}>
1196
<Icon name="trash" /> {intl.formatMessage(labels.delete)}...
1197
</Button>
1198
</Popconfirm>
1199
);
1200
}
1201
}
1202
1203
function render_configure_peer() {
1204
return <ConfigurePeerGrading actions={actions} assignment={assignment} />;
1205
}
1206
1207
function render_peer_button() {
1208
let icon;
1209
if (is_peer_graded()) {
1210
icon = "check-square-o";
1211
} else {
1212
icon = "square-o";
1213
}
1214
return (
1215
<Button
1216
disabled={expand_peer_config}
1217
onClick={() =>
1218
actions.toggle_item_expansion(
1219
"peer_config",
1220
assignment.get("assignment_id"),
1221
)
1222
}
1223
>
1224
<Icon name={icon} /> Peer Grading...
1225
</Button>
1226
);
1227
}
1228
1229
function render_summary_due_date() {
1230
const due_date = assignment.get("due_date");
1231
if (due_date) {
1232
return (
1233
<div style={{ marginTop: "12px" }}>
1234
Due <BigTime date={due_date} />
1235
</div>
1236
);
1237
}
1238
}
1239
1240
function render_assignment_name() {
1241
const num_items = assignment.get("listing")?.size ?? 0;
1242
return (
1243
<span>
1244
{trunc_middle(assignment.get("path"), 80)}
1245
{assignment.get("deleted") ? <b> (deleted)</b> : undefined}
1246
{num_items == 0 ? " - add content to this assignment..." : undefined}
1247
</span>
1248
);
1249
}
1250
1251
function render_assignment_title_link() {
1252
return (
1253
<a
1254
href=""
1255
onClick={(e) => {
1256
e.preventDefault();
1257
actions.toggle_item_expansion(
1258
"assignment",
1259
assignment.get("assignment_id"),
1260
);
1261
}}
1262
>
1263
<Icon
1264
style={{ marginRight: "10px" }}
1265
name={is_expanded ? "caret-down" : "caret-right"}
1266
/>
1267
{render_assignment_name()}
1268
</a>
1269
);
1270
}
1271
1272
function render_summary_line() {
1273
return (
1274
<Row key="summary" style={{ backgroundColor: background }}>
1275
<Col md={12}>
1276
<h5>{render_assignment_title_link()}</h5>
1277
</Col>
1278
<Col md={12}>{render_summary_due_date()}</Col>
1279
</Row>
1280
);
1281
}
1282
1283
return (
1284
<div>
1285
<Row style={is_expanded ? styles.selected_entry : styles.entry_style}>
1286
<Col xs={24}>
1287
{render_summary_line()}
1288
{is_expanded ? render_more() : undefined}
1289
</Col>
1290
</Row>
1291
</div>
1292
);
1293
}
1294
1295