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