Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/course/handouts/handout.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 { useState } from "react";
8
import { FormattedMessage, useIntl } from "react-intl";
9
import { CSS, redux } from "@cocalc/frontend/app-framework";
10
import { Icon, MarkdownInput, Tip } from "@cocalc/frontend/components";
11
import { course, labels } from "@cocalc/frontend/i18n";
12
import { UserMap } from "@cocalc/frontend/todo-types";
13
import { capitalize, trunc_middle } from "@cocalc/util/misc";
14
import type { CourseActions } from "../actions";
15
import { CourseStore, HandoutRecord, StudentsMap } from "../store";
16
import * as styles from "../styles";
17
import { StudentListForHandout } from "./handout-student-list";
18
import { ComputeServerButton } from "../compute";
19
20
// Could be merged with steps system of assignments.
21
// Probably not a good idea mixing the two.
22
// Could also be coded into the components below but steps could be added in the future?
23
const STEPS = ["handout"] as const;
24
type STEP_TYPES = (typeof STEPS)[number];
25
26
function step_direction(step: STEP_TYPES): string {
27
switch (step) {
28
case "handout":
29
return "to";
30
default:
31
throw Error(`BUG! step_direction('${step}')`);
32
}
33
}
34
35
function step_verb(step: STEP_TYPES): string {
36
switch (step) {
37
case "handout":
38
return "distribute";
39
default:
40
throw Error(`BUG! step_verb('${step}')`);
41
}
42
}
43
44
function step_ready(step: STEP_TYPES): string | undefined {
45
switch (step) {
46
case "handout":
47
return "";
48
}
49
}
50
51
function past_tense(word: string): string {
52
if (word[word.length - 1] === "e") {
53
return word + "d";
54
} else {
55
return word + "ed";
56
}
57
}
58
59
interface HandoutProps {
60
frame_id?: string;
61
name: string;
62
handout: HandoutRecord;
63
backgroundColor?: string;
64
actions: CourseActions;
65
is_expanded: boolean;
66
students: StudentsMap;
67
user_map: UserMap;
68
project_id: string;
69
}
70
71
export function Handout({
72
frame_id,
73
name,
74
handout,
75
backgroundColor,
76
actions,
77
is_expanded,
78
students,
79
user_map,
80
project_id,
81
}: HandoutProps) {
82
const intl = useIntl();
83
const [copy_confirm, set_copy_confirm] = useState<boolean>(false);
84
const [copy_confirm_handout, set_copy_confirm_handout] =
85
useState<boolean>(false);
86
const [copy_confirm_all_handout, set_copy_confirm_all_handout] =
87
useState<boolean>(false);
88
const [copy_handout_confirm_overwrite, set_copy_handout_confirm_overwrite] =
89
useState<boolean>(false);
90
const [
91
copy_handout_confirm_overwrite_text,
92
set_copy_handout_confirm_overwrite_text,
93
] = useState<string>("");
94
95
function open_handout_path(e) {
96
e.preventDefault();
97
const actions = redux.getProjectActions(project_id);
98
if (actions != null) {
99
actions.open_directory(handout.get("path"));
100
}
101
}
102
103
function render_more_header() {
104
return (
105
<div style={{ display: "flex" }}>
106
<div
107
style={{
108
fontSize: "15pt",
109
marginBottom: "5px",
110
marginRight: "30px",
111
}}
112
>
113
{handout.get("path")}
114
</div>
115
<Button onClick={open_handout_path}>
116
<Icon name="folder-open" /> Open
117
</Button>
118
<div style={{ flex: 1 }} />
119
<ComputeServerButton unit={handout as any} actions={actions} />
120
<div style={{ flex: 1 }} />
121
{render_delete_button()}
122
</div>
123
);
124
}
125
126
function render_handout_notes() {
127
return (
128
<Row key="note" style={styles.note}>
129
<Col xs={4}>
130
<Tip
131
title={intl.formatMessage({
132
id: "course.handouts.handout_notes.tooltip.title",
133
defaultMessage: "Notes about this handout",
134
})}
135
tip={intl.formatMessage({
136
id: "course.handouts.handout_notes.tooltip.tooltip",
137
defaultMessage: `Record notes about this handout here.
138
These notes are only visible to you, not to your students.
139
Put any instructions to students about handouts in a file in the directory
140
that contains the handout.`,
141
})}
142
>
143
<FormattedMessage
144
id="course.handouts.handout_notes.title"
145
defaultMessage={"Handout Notes"}
146
/>
147
<br />
148
</Tip>
149
</Col>
150
<Col xs={20}>
151
<MarkdownInput
152
persist_id={
153
handout.get("path") + handout.get("handout_id") + "note"
154
}
155
attach_to={name}
156
rows={6}
157
placeholder={intl.formatMessage({
158
id: "course.handouts.handout_notes.placeholder",
159
defaultMessage:
160
"Notes about this handout (not visible to students)",
161
})}
162
default_value={handout.get("note")}
163
on_save={(value) =>
164
actions.handouts.set_handout_note(
165
handout.get("handout_id"),
166
value,
167
)
168
}
169
/>
170
</Col>
171
</Row>
172
);
173
}
174
175
function render_export_file_use_times() {
176
return (
177
<Row key="file-use-times-export-handout">
178
<Col xs={4}>
179
<Tip
180
title="Export when students used files"
181
tip="Export a JSON file containing extensive information about exactly when students have opened or edited files in this handout. 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."
182
>
183
Export file use times
184
<br />
185
</Tip>
186
</Col>
187
<Col xs={20}>
188
<Button
189
onClick={() =>
190
actions.export.file_use_times(handout.get("handout_id"))
191
}
192
>
193
Export file use times for this handout
194
</Button>
195
</Col>
196
</Row>
197
);
198
}
199
200
function render_copy_all(status) {
201
const steps = STEPS;
202
const result: (React.JSX.Element | undefined)[] = [];
203
for (const step of steps) {
204
if (copy_confirm_handout) {
205
result.push(render_copy_confirm(step, status));
206
} else {
207
result.push(undefined);
208
}
209
}
210
return result;
211
}
212
213
function render_copy_confirm(step: string, status) {
214
return (
215
<span key={`copy_confirm_${step}`}>
216
{status[step] === 0
217
? render_copy_confirm_to_all(step, status)
218
: undefined}
219
{status[step] !== 0
220
? render_copy_confirm_to_all_or_new(step, status)
221
: undefined}
222
</span>
223
);
224
}
225
226
function render_copy_cancel() {
227
const cancel = (): void => {
228
set_copy_confirm_handout(false);
229
set_copy_confirm_all_handout(false);
230
set_copy_confirm(false);
231
set_copy_handout_confirm_overwrite(false);
232
};
233
return (
234
<Button key="cancel" onClick={cancel}>
235
{intl.formatMessage(labels.cancel)}
236
</Button>
237
);
238
}
239
240
function render_copy_handout_confirm_overwrite(step: string) {
241
if (!copy_handout_confirm_overwrite) {
242
return;
243
}
244
const do_it = (): void => {
245
copy_handout(step, false, true);
246
set_copy_handout_confirm_overwrite(false);
247
set_copy_handout_confirm_overwrite_text("");
248
};
249
return (
250
<div style={{ marginTop: "15px" }}>
251
Type in "OVERWRITE" if you are certain to replace the handout files of
252
all students.
253
<Input
254
autoFocus
255
onChange={(e) =>
256
set_copy_handout_confirm_overwrite_text(e.target.value)
257
}
258
style={{ marginTop: "1ex" }}
259
/>
260
<Space style={{ textAlign: "center", marginTop: "15px" }}>
261
{render_copy_cancel()}
262
<Button
263
disabled={copy_handout_confirm_overwrite_text !== "OVERWRITE"}
264
danger
265
onClick={do_it}
266
>
267
<Icon name="exclamation-triangle" /> Confirm replacing files
268
</Button>
269
</Space>
270
</div>
271
);
272
}
273
274
function copy_handout(step, new_only, overwrite?): void {
275
// handout to all (non-deleted) students
276
switch (step) {
277
case "handout":
278
actions.handouts.copy_handout_to_all_students(
279
handout.get("handout_id"),
280
new_only,
281
overwrite,
282
);
283
break;
284
default:
285
console.log(`BUG -- unknown step: ${step}`);
286
}
287
set_copy_confirm_handout(false);
288
set_copy_confirm_all_handout(false);
289
set_copy_confirm(false);
290
}
291
292
function render_copy_confirm_to_all(step, status) {
293
const n = status[`not_${step}`];
294
return (
295
<Alert
296
type="warning"
297
key={`${step}_confirm_to_all`}
298
style={{ marginTop: "15px" }}
299
message={
300
<div>
301
<div style={{ marginBottom: "15px" }}>
302
{capitalize(step_verb(step))} this handout {step_direction(step)}{" "}
303
the {n} student{n > 1 ? "s" : ""}
304
{step_ready(step)}?
305
</div>
306
<Space>
307
{render_copy_cancel()}
308
<Button
309
key="yes"
310
type="primary"
311
onClick={() => copy_handout(step, false)}
312
>
313
Yes
314
</Button>
315
</Space>
316
</div>
317
}
318
/>
319
);
320
}
321
322
function copy_confirm_all_caution(step): string | undefined {
323
switch (step) {
324
case "handout":
325
return `\
326
This will recopy all of the files to them.
327
CAUTION: if you update a file that a student has also worked on, their work will get copied to a backup file ending in a tilde, or possibly only be available in snapshots.
328
Select "Replace student files!" in case you do not want to create any backups and also delete all other files in the handout directory of their projects.\
329
`;
330
}
331
}
332
333
function render_copy_confirm_overwrite_all(step) {
334
return (
335
<div key="copy_confirm_overwrite_all" style={{ marginTop: "15px" }}>
336
<div style={{ marginBottom: "15px" }}>
337
{copy_confirm_all_caution(step)}
338
</div>
339
<Space wrap>
340
{render_copy_cancel()}
341
<Button key="all" onClick={() => copy_handout(step, false)}>
342
Yes, do it
343
</Button>
344
<Button
345
key="all-overwrite"
346
danger
347
onClick={() => set_copy_handout_confirm_overwrite(true)}
348
>
349
Replace student files!
350
</Button>
351
</Space>
352
{render_copy_handout_confirm_overwrite(step)}
353
</div>
354
);
355
}
356
357
function render_copy_confirm_to_all_or_new(step, status) {
358
const n = status[`not_${step}`];
359
const m = n + status[step];
360
return (
361
<Alert
362
type="warning"
363
key={`${step}_confirm_to_all_or_new`}
364
style={{ marginTop: "15px" }}
365
message={
366
<div>
367
<div style={{ marginBottom: "15px" }}>
368
{capitalize(step_verb(step))} this handout {step_direction(step)}
369
...
370
</div>
371
<Space wrap>
372
{render_copy_cancel()}
373
<Button
374
key="all"
375
danger
376
onClick={() => {
377
set_copy_confirm_all_handout(true);
378
set_copy_confirm(true);
379
}}
380
disabled={copy_confirm_all_handout}
381
>
382
{step === "handout" ? "All" : "The"} {m} students
383
{step_ready(step)}
384
...
385
</Button>
386
{n ? (
387
<Button
388
key="new"
389
type="primary"
390
onClick={() => copy_handout(step, true)}
391
>
392
The {n} student{n > 1 ? "s" : ""} not already{" "}
393
{past_tense(step_verb(step))} {step_direction(step)}
394
</Button>
395
) : undefined}
396
</Space>
397
{copy_confirm_all_handout
398
? render_copy_confirm_overwrite_all(step)
399
: undefined}
400
</div>
401
}
402
/>
403
);
404
}
405
406
function render_handout_button(status) {
407
const handout_count = status.handout;
408
const { not_handout } = status;
409
let type;
410
if (handout_count === 0) {
411
type = "primary";
412
} else {
413
if (not_handout === 0) {
414
type = "dashed";
415
} else {
416
type = "default";
417
}
418
}
419
const tooltip = intl.formatMessage({
420
id: "course.handouts.handout_button.tooltip",
421
defaultMessage:
422
"Copy the files for this handout from this project to all other student projects.",
423
description: "student in an online course",
424
});
425
const label = intl.formatMessage(course.handout);
426
const you = intl.formatMessage(labels.you);
427
const students = intl.formatMessage(course.students);
428
429
return (
430
<Button
431
key="handout"
432
type={type}
433
onClick={() => {
434
set_copy_confirm_handout(true);
435
set_copy_confirm(true);
436
}}
437
disabled={copy_confirm}
438
style={outside_button_style}
439
>
440
<Tip
441
title={
442
<span>
443
{label}: <Icon name="user-secret" /> {you}{" "}
444
<Icon name="arrow-right" /> <Icon name="users" /> {students}{" "}
445
</span>
446
}
447
tip={tooltip}
448
>
449
<Icon name="share-square" /> {intl.formatMessage(course.distribute)}
450
...
451
</Tip>
452
</Button>
453
);
454
}
455
456
function delete_handout(): void {
457
actions.handouts.delete_handout(handout.get("handout_id"));
458
}
459
460
function undelete_handout(): void {
461
actions.handouts.undelete_handout(handout.get("handout_id"));
462
}
463
464
function render_delete_button() {
465
if (handout.get("deleted")) {
466
return (
467
<Tip
468
key="delete"
469
placement="left"
470
title="Undelete handout"
471
tip="Make the handout visible again in the handout list and in student grade lists."
472
>
473
<Button onClick={undelete_handout}>
474
<Icon name="trash" /> Undelete
475
</Button>
476
</Tip>
477
);
478
} else {
479
return (
480
<Popconfirm
481
key="delete"
482
onConfirm={delete_handout}
483
title={
484
<div style={{ maxWidth: "400px" }}>
485
<b>
486
Are you sure you want to delete "
487
{trunc_middle(handout.get("path"), 24)}"?
488
</b>
489
<br />
490
This removes it from the handout list and student grade lists, but
491
does not delete any files off of disk. You can always undelete an
492
handout later by showing it using the 'show deleted handouts'
493
button.
494
</div>
495
}
496
>
497
<Button>
498
<Icon name="trash" /> Delete...
499
</Button>
500
</Popconfirm>
501
);
502
}
503
}
504
505
function render_more() {
506
if (!is_expanded) return;
507
return (
508
<Row key="more">
509
<Col sm={24}>
510
<Card title={render_more_header()}>
511
<StudentListForHandout
512
frame_id={frame_id}
513
handout={handout}
514
students={students}
515
user_map={user_map}
516
actions={actions}
517
name={name}
518
/>
519
{render_handout_notes()}
520
<br />
521
<hr />
522
<br />
523
{render_export_file_use_times()}
524
</Card>
525
</Col>
526
</Row>
527
);
528
}
529
530
const outside_button_style: CSS = {
531
margin: "4px",
532
paddingTop: "6px",
533
paddingBottom: "4px",
534
};
535
536
function render_handout_name() {
537
return (
538
<h5>
539
<a
540
href=""
541
onClick={(e) => {
542
e.preventDefault();
543
return actions.toggle_item_expansion(
544
"handout",
545
handout.get("handout_id"),
546
);
547
}}
548
>
549
<Icon
550
style={{ marginRight: "10px", float: "left" }}
551
name={is_expanded ? "caret-down" : "caret-right"}
552
/>
553
<div>
554
{trunc_middle(handout.get("path"), 24)}
555
{handout.get("deleted") ? <b> (deleted)</b> : undefined}
556
</div>
557
</a>
558
</h5>
559
);
560
}
561
562
function get_store(): CourseStore {
563
const store = redux.getStore(name);
564
if (store == null) throw Error("store must be defined");
565
return store as unknown as CourseStore;
566
}
567
568
function render_handout_heading() {
569
let status = get_store().get_handout_status(handout.get("handout_id"));
570
if (status == null) {
571
status = {
572
handout: 0,
573
not_handout: 0,
574
};
575
}
576
return (
577
<Row key="summary" style={{ backgroundColor: backgroundColor }}>
578
<Col md={8} style={{ paddingRight: "0px" }}>
579
{render_handout_name()}
580
</Col>
581
<Col md={16}>
582
<Row style={{ marginLeft: "8px" }}>
583
{render_handout_button(status)}
584
<span
585
style={{ color: "#666", marginLeft: "5px", marginTop: "10px" }}
586
>
587
({status.handout}/{status.handout + status.not_handout}{" "}
588
transferred)
589
</span>
590
</Row>
591
<Row style={{ marginLeft: "8px" }}>{render_copy_all(status)}</Row>
592
</Col>
593
</Row>
594
);
595
}
596
597
return (
598
<div>
599
<Row style={is_expanded ? styles.selected_entry : styles.entry_style}>
600
<Col xs={24} style={{ paddingTop: "5px", paddingBottom: "5px" }}>
601
{render_handout_heading()}
602
{render_more()}
603
</Col>
604
</Row>
605
</div>
606
);
607
}
608
609