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/actions.ts
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
/*
7
Actions involving working with handouts.
8
*/
9
10
import type { CourseActions } from "../actions";
11
import type { CourseStore, HandoutRecord } from "../store";
12
import { webapp_client } from "../../webapp-client";
13
import { redux } from "../../app-framework";
14
import { uuid } from "@cocalc/util/misc";
15
import { map } from "awaiting";
16
import type { SyncDBRecordHandout } from "../types";
17
import { exec } from "../../frame-editors/generic/client";
18
import { export_student_file_use_times } from "../export/file-use-times";
19
import { COPY_TIMEOUT_MS } from "../consts";
20
21
export class HandoutsActions {
22
private course_actions: CourseActions;
23
24
constructor(course_actions: CourseActions) {
25
this.course_actions = course_actions;
26
}
27
28
private get_store = (): CourseStore => {
29
return this.course_actions.get_store();
30
};
31
32
// slight warning -- this is linear in the number of assignments (so do not overuse)
33
private getHandoutWithPath = (path: string): HandoutRecord | undefined => {
34
const store = this.get_store();
35
if (store == null) return;
36
return store
37
.get("handouts")
38
.valueSeq()
39
.filter((x) => x.get("path") == path)
40
.get(0);
41
};
42
43
addHandout = async (path: string | string[]): Promise<void> => {
44
// Add one or more handouts to the course, which is defined by giving a directory in the project.
45
// If the handout was previously deleted, this undeletes it.
46
if (typeof path != "string") {
47
// handle case of array of inputs
48
for (const p of path) {
49
await this.addHandout(p);
50
}
51
return;
52
}
53
const cur = this.getHandoutWithPath(path);
54
if (cur != null) {
55
// either undelete or nothing to do.
56
if (cur.get("deleted")) {
57
// undelete
58
this.undelete_handout(cur.get("handout_id"));
59
} else {
60
// nothing to do
61
}
62
return;
63
}
64
65
const target_path = path; // folder where we copy the handout to
66
try {
67
// Ensure the path actually exists in the instructor project.
68
await exec({
69
project_id: this.get_store().get("course_project_id"),
70
command: "mkdir",
71
args: ["-p", path],
72
err_on_exit: true,
73
});
74
} catch (err) {
75
this.course_actions.set_error(`error creating assignment: ${err}`);
76
return;
77
}
78
79
this.course_actions.set({
80
path,
81
target_path,
82
table: "handouts",
83
handout_id: uuid(),
84
});
85
};
86
87
delete_handout = (handout_id: string): void => {
88
this.course_actions.set({
89
deleted: true,
90
handout_id,
91
table: "handouts",
92
});
93
};
94
95
undelete_handout = (handout_id: string): void => {
96
this.course_actions.set({
97
deleted: false,
98
handout_id,
99
table: "handouts",
100
});
101
};
102
103
private set_handout_field = (handout, name, val): void => {
104
handout = this.get_store().get_handout(handout);
105
this.course_actions.set({
106
[name]: val,
107
table: "handouts",
108
handout_id: handout.get("handout_id"),
109
});
110
};
111
112
set_handout_note = (handout, note): void => {
113
this.set_handout_field(handout, "note", note);
114
};
115
116
private handout_finish_copy = (
117
handout_id: string,
118
student_id: string,
119
err: string,
120
): void => {
121
const { student, handout } = this.course_actions.resolve({
122
handout_id,
123
student_id,
124
});
125
if (student == null || handout == null) return;
126
const obj: SyncDBRecordHandout = {
127
table: "handouts",
128
handout_id: handout.get("handout_id"),
129
};
130
const h = this.course_actions.get_one(obj);
131
if (h == null) return;
132
const status_map: {
133
[student_id: string]: { time?: number; error?: string };
134
} = h.status ? h.status : {};
135
status_map[student_id] = { time: webapp_client.server_time() };
136
if (err) {
137
status_map[student_id].error = err;
138
}
139
obj.status = status_map;
140
this.course_actions.set(obj);
141
};
142
143
// returns false if an actual copy starts and true if not (since we
144
// already tried or closed the store).
145
private handout_start_copy = (
146
handout_id: string,
147
student_id: string,
148
): boolean => {
149
const obj: any = {
150
table: "handouts",
151
handout_id,
152
};
153
const x = this.course_actions.get_one(obj);
154
if (x == null) {
155
// no such handout.
156
return true;
157
}
158
const status_map = x.status != null ? x.status : {};
159
let student_status = status_map[student_id];
160
if (student_status == null) student_status = {};
161
if (
162
student_status.start != null &&
163
webapp_client.server_time() - student_status.start <= 15000
164
) {
165
return true; // never retry a copy until at least 15 seconds later.
166
}
167
student_status.start = webapp_client.server_time();
168
status_map[student_id] = student_status;
169
obj.status = status_map;
170
this.course_actions.set(obj);
171
return false;
172
};
173
174
// "Copy" of `stop_copying_assignment:`
175
stop_copying_handout = (handout_id: string, student_id: string): void => {
176
const obj: SyncDBRecordHandout = { table: "handouts", handout_id };
177
const h = this.course_actions.get_one(obj);
178
if (h == null) return;
179
const status = h.status;
180
if (status == null) return;
181
const student_status = status[student_id];
182
if (student_status == null) return;
183
if (student_status.start != null) {
184
delete student_status.start;
185
status[student_id] = student_status;
186
obj.status = status;
187
this.course_actions.set(obj);
188
}
189
};
190
191
// Copy the files for the given handout to the given student. If
192
// the student project doesn't exist yet, it will be created.
193
// You may also pass in an id for either the handout or student.
194
// "overwrite" (boolean, optional): if true, the copy operation will overwrite/delete remote files in student projects -- #1483
195
// If the store is initialized and the student and handout both exist,
196
// then calling this action will result in this getting set in the store:
197
//
198
// handout.status[student_id] = {time:?, error:err}
199
//
200
// where time >= now is the current time in milliseconds.
201
copy_handout_to_student = async (
202
handout_id: string,
203
student_id: string,
204
overwrite: boolean,
205
): Promise<void> => {
206
if (this.handout_start_copy(handout_id, student_id)) {
207
return;
208
}
209
const id = this.course_actions.set_activity({
210
desc: "Copying handout to a student",
211
});
212
const finish = (err?) => {
213
this.course_actions.clear_activity(id);
214
this.handout_finish_copy(handout_id, student_id, err);
215
if (err) {
216
this.course_actions.set_error(`copy handout to student: ${err}`);
217
}
218
};
219
const { store, student, handout } = this.course_actions.resolve({
220
student_id,
221
handout_id,
222
finish,
223
});
224
if (!student || !handout) return;
225
226
const student_name = store.get_student_name(student_id);
227
this.course_actions.set_activity({
228
id,
229
desc: `Copying handout to ${student_name}`,
230
});
231
let student_project_id: string | undefined = student.get("project_id");
232
const course_project_id = store.get("course_project_id");
233
const src_path = handout.get("path");
234
try {
235
if (student_project_id == null) {
236
this.course_actions.set_activity({
237
id,
238
desc: `${student_name}'s project doesn't exist, so creating it.`,
239
});
240
student_project_id =
241
await this.course_actions.student_projects.create_student_project(
242
student_id,
243
);
244
}
245
246
if (student_project_id == null) {
247
throw Error("bug -- student project should have been created");
248
}
249
250
this.course_actions.set_activity({
251
id,
252
desc: `Copying files to ${student_name}'s project`,
253
});
254
255
await webapp_client.project_client.copy_path_between_projects({
256
src_project_id: course_project_id,
257
src_path,
258
target_project_id: student_project_id,
259
target_path: handout.get("target_path"),
260
overwrite_newer: !!overwrite, // default is "false"
261
delete_missing: !!overwrite, // default is "false"
262
backup: !!!overwrite, // default is "true"
263
timeout: COPY_TIMEOUT_MS / 1000,
264
});
265
finish();
266
} catch (err) {
267
finish(err);
268
}
269
};
270
271
// Copy the given handout to all non-deleted students, doing several copies in parallel at once.
272
copy_handout_to_all_students = async (
273
handout_id: string,
274
new_only: boolean,
275
overwrite: boolean,
276
): Promise<void> => {
277
const desc: string =
278
"Copying handouts to all students " +
279
(new_only ? "who have not already received it" : "");
280
const short_desc = "copy handout to student";
281
282
const id = this.course_actions.set_activity({ desc });
283
const finish = (err?) => {
284
this.course_actions.clear_activity(id);
285
if (err) {
286
err = `${short_desc}: ${err}`;
287
this.course_actions.set_error(err);
288
}
289
};
290
const { store, handout } = this.course_actions.resolve({
291
handout_id,
292
finish,
293
});
294
if (!handout) return;
295
296
let errors = "";
297
const f = async (student_id: string): Promise<void> => {
298
if (new_only && store.handout_last_copied(handout_id, student_id)) {
299
return;
300
}
301
try {
302
await this.copy_handout_to_student(handout_id, student_id, overwrite);
303
} catch (err) {
304
errors += `\n ${err}`;
305
}
306
};
307
308
await map(
309
store.get_student_ids({ deleted: false }),
310
store.get_copy_parallel(),
311
f,
312
);
313
314
finish(errors);
315
};
316
317
open_handout = (handout_id: string, student_id: string): void => {
318
const { handout, student } = this.course_actions.resolve({
319
handout_id,
320
student_id,
321
});
322
if (student == null || handout == null) return;
323
const student_project_id = student.get("project_id");
324
if (student_project_id == null) {
325
this.course_actions.set_error(
326
"open_handout: student project not yet created",
327
);
328
return;
329
}
330
const path = handout.get("target_path");
331
const proj = student_project_id;
332
if (proj == null) {
333
this.course_actions.set_error("no such project");
334
return;
335
}
336
// Now open it
337
redux.getProjectActions(proj).open_directory(path);
338
};
339
340
export_file_use_times = async (
341
handout_id: string,
342
json_filename: string,
343
): Promise<void> => {
344
// Get the path of the handout
345
const { handout, store } = this.course_actions.resolve({
346
handout_id,
347
});
348
if (handout == null) {
349
throw Error("no such handout");
350
}
351
const path = handout.get("path");
352
await export_student_file_use_times(
353
store.get("course_project_id"),
354
path,
355
path,
356
store.get("students"),
357
json_filename,
358
store.get_student_name.bind(store),
359
);
360
};
361
}
362
363