Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/course/handouts/actions.ts
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
/*
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
const opts = {
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,
264
};
265
await webapp_client.project_client.copy_path_between_projects(opts);
266
267
await this.course_actions.compute.setComputeServerAssociations({
268
student_id,
269
src_path: opts.src_path,
270
target_project_id: opts.target_project_id,
271
target_path: opts.target_path,
272
unit_id: handout_id,
273
});
274
275
finish();
276
} catch (err) {
277
finish(err);
278
}
279
};
280
281
// Copy the given handout to all non-deleted students, doing several copies in parallel at once.
282
copy_handout_to_all_students = async (
283
handout_id: string,
284
new_only: boolean,
285
overwrite: boolean,
286
): Promise<void> => {
287
const desc: string =
288
"Copying handouts to all students " +
289
(new_only ? "who have not already received it" : "");
290
const short_desc = "copy handout to student";
291
292
const id = this.course_actions.set_activity({ desc });
293
const finish = (err?) => {
294
this.course_actions.clear_activity(id);
295
if (err) {
296
err = `${short_desc}: ${err}`;
297
this.course_actions.set_error(err);
298
}
299
};
300
const { store, handout } = this.course_actions.resolve({
301
handout_id,
302
finish,
303
});
304
if (!handout) return;
305
306
let errors = "";
307
const f = async (student_id: string): Promise<void> => {
308
if (new_only && store.handout_last_copied(handout_id, student_id)) {
309
return;
310
}
311
try {
312
await this.copy_handout_to_student(handout_id, student_id, overwrite);
313
} catch (err) {
314
errors += `\n ${err}`;
315
}
316
};
317
318
await map(
319
store.get_student_ids({ deleted: false }),
320
store.get_copy_parallel(),
321
f,
322
);
323
324
finish(errors);
325
};
326
327
open_handout = (handout_id: string, student_id: string): void => {
328
const { handout, student } = this.course_actions.resolve({
329
handout_id,
330
student_id,
331
});
332
if (student == null || handout == null) return;
333
const student_project_id = student.get("project_id");
334
if (student_project_id == null) {
335
this.course_actions.set_error(
336
"open_handout: student project not yet created",
337
);
338
return;
339
}
340
const path = handout.get("target_path");
341
const proj = student_project_id;
342
if (proj == null) {
343
this.course_actions.set_error("no such project");
344
return;
345
}
346
// Now open it
347
redux.getProjectActions(proj).open_directory(path);
348
};
349
350
export_file_use_times = async (
351
handout_id: string,
352
json_filename: string,
353
): Promise<void> => {
354
// Get the path of the handout
355
const { handout, store } = this.course_actions.resolve({
356
handout_id,
357
});
358
if (handout == null) {
359
throw Error("no such handout");
360
}
361
const path = handout.get("path");
362
await export_student_file_use_times(
363
store.get("course_project_id"),
364
path,
365
path,
366
store.get("students"),
367
json_filename,
368
store.get_student_name.bind(store),
369
);
370
};
371
}
372
373