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