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/shared-project/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 that are specific to the shared project.
8
*/
9
10
import { redux } from "@cocalc/frontend/app-framework";
11
import { Datastore, EnvVars } from "@cocalc/frontend/projects/actions";
12
import { CourseActions } from "../actions";
13
import { CourseStore } from "../store";
14
import { delay } from "awaiting";
15
16
export class SharedProjectActions {
17
private actions: CourseActions;
18
19
constructor(actions: CourseActions) {
20
this.actions = actions;
21
}
22
23
private get_store = (): CourseStore => {
24
const store = this.actions.get_store();
25
if (store == null) throw Error("no store");
26
return store;
27
};
28
29
// return the default title and description of the shared project.
30
private settings = (): {
31
title: string;
32
description: string;
33
image?: string;
34
} => {
35
const settings = this.get_store().get("settings");
36
return {
37
title: `Shared Project -- ${settings.get("title")}`,
38
description:
39
settings.get("description") +
40
"\n\n---\n\nThis project is shared with all students in the course.",
41
image: settings.get("custom_image"),
42
};
43
};
44
45
set_project_title = (): void => {
46
const store = this.get_store();
47
if (store == null) return;
48
const shared_id = store.get_shared_project_id();
49
if (!shared_id) return;
50
const { title } = this.settings();
51
redux.getActions("projects").set_project_title(shared_id, title);
52
};
53
54
set_project_description = (): void => {
55
const store = this.get_store();
56
if (store == null) return;
57
const shared_id = store.get_shared_project_id();
58
if (!shared_id) return;
59
60
const { description } = this.settings();
61
redux
62
.getActions("projects")
63
.set_project_description(shared_id, description);
64
};
65
66
// start the shared project running, stopping, etc. (if it exists)
67
action_shared_project = async (action: "start" | "stop"): Promise<void> => {
68
const store = this.get_store();
69
if (store == null) {
70
return;
71
}
72
const shared_project_id = store.get_shared_project_id();
73
if (!shared_project_id) {
74
return; // no shared project
75
}
76
const a = redux.getActions("projects");
77
if (a == null) return;
78
const f = a[action + "_project"].bind(a);
79
if (f == null) return;
80
await f(shared_project_id);
81
};
82
83
// configure the shared project so that it has everybody as collaborators
84
configure = async (): Promise<void> => {
85
const store = this.get_store();
86
const shared_project_id = store.get_shared_project_id();
87
if (!shared_project_id) {
88
return; // no shared project
89
}
90
const id = this.actions.set_activity({
91
desc: "Configuring shared project...",
92
});
93
try {
94
await this.set_project_title();
95
// add collabs -- all collaborators on course project and all students
96
const projects = redux.getStore("projects");
97
const shared_project_users = projects.get_users(shared_project_id);
98
if (shared_project_users == null) {
99
return;
100
}
101
const course_project_users = projects.get_users(
102
store.get("course_project_id"),
103
);
104
if (course_project_users == null) {
105
return;
106
}
107
const student_account_ids = {};
108
store.get_students().map((student, _) => {
109
if (!student.get("deleted")) {
110
const account_id = student.get("account_id");
111
if (account_id != null) {
112
student_account_ids[account_id] = true;
113
}
114
}
115
});
116
117
// Each of shared_project_users or course_project_users are
118
// immutable.js maps from account_id's to something, and students is a map from
119
// the student account_id's.
120
// Our goal is to ensure that:
121
// {shared_project_users} = {course_project_users} union {students}.
122
123
const actions = redux.getActions("projects");
124
if (!store.get_allow_collabs()) {
125
// Ensure the shared project users are all either course or students
126
for (const account_id in shared_project_users.toJS()) {
127
if (
128
!course_project_users.get(account_id) &&
129
!student_account_ids[account_id]
130
) {
131
await actions.remove_collaborator(shared_project_id, account_id);
132
}
133
}
134
}
135
// Ensure every course project user is on the shared project
136
for (const account_id in course_project_users.toJS()) {
137
if (!shared_project_users.get(account_id)) {
138
await actions.invite_collaborator(shared_project_id, account_id);
139
}
140
}
141
// Ensure every student is on the shared project
142
for (const account_id in student_account_ids) {
143
if (!shared_project_users.get(account_id)) {
144
await actions.invite_collaborator(shared_project_id, account_id);
145
}
146
}
147
148
// Set license key(s) on the shared project too, if there is one
149
// NOTE: we never remove it or any other licenses from the shared project,
150
// since instructor may want to augment license with another.
151
const site_license_id = store.getIn(["settings", "site_license_id"]);
152
if (site_license_id) {
153
try {
154
await actions.add_site_license_to_project(
155
shared_project_id,
156
site_license_id,
157
);
158
} catch (err) {
159
console.warn(`error adding site license to shared project -- ${err}`);
160
}
161
}
162
163
// Also set the compute image
164
await this.set_project_compute_image();
165
await this.set_datastore_and_envvars();
166
} catch (err) {
167
this.actions.set_error(`Error configuring shared project - ${err}`);
168
} finally {
169
this.actions.set_activity({ id });
170
}
171
};
172
173
set_project_compute_image = async (): Promise<void> => {
174
const store = this.get_store();
175
const shared_project_id = store.get_shared_project_id();
176
if (!shared_project_id) {
177
return; // no shared project
178
}
179
const dflt_img = await redux.getStore("customize").getDefaultComputeImage();
180
const img_id = store.get("settings").get("custom_image") ?? dflt_img;
181
const actions = redux.getProjectActions(shared_project_id);
182
await actions.set_compute_image(img_id);
183
};
184
185
set_datastore_and_envvars = async (): Promise<void> => {
186
const store = this.get_store();
187
const shared_project_id = store.get_shared_project_id();
188
if (!shared_project_id) {
189
return; // no shared project
190
}
191
const datastore: Datastore = store.get_datastore();
192
const envvars: EnvVars = store.get_envvars();
193
const actions = redux.getActions("projects");
194
await actions.set_project_course_info({
195
project_id: shared_project_id,
196
course_project_id: store.get("course_project_id"),
197
path: store.get("course_filename"),
198
pay: "", // pay
199
payInfo: null, // payInfo
200
account_id: null, // account_id
201
email_address: null, // email_address
202
datastore,
203
type: "shared", // type of project
204
student_project_functionality: null, // student_project_functionality (not used for shared projects)
205
envvars,
206
});
207
};
208
209
// set the shared project id in our syncdb
210
private set_project_id = (shared_project_id: string): void => {
211
this.actions.set({
212
table: "settings",
213
shared_project_id,
214
});
215
};
216
217
// create the globally shared project if it doesn't exist
218
create = async (): Promise<void> => {
219
const store = this.get_store();
220
if (store.get_shared_project_id()) {
221
return;
222
}
223
const id = this.actions.set_activity({
224
desc: "Creating shared project...",
225
});
226
let project_id: string;
227
try {
228
project_id = await redux
229
.getActions("projects")
230
.create_project(this.settings());
231
} catch (err) {
232
this.actions.set_error(`error creating shared project -- ${err}`);
233
return;
234
} finally {
235
this.actions.set_activity({ id });
236
}
237
this.set_project_id(project_id);
238
// wait for any changes to syncdb to update store, before
239
// calling configure (which relies on the store being updated).
240
await delay(10);
241
await this.configure();
242
};
243
244
// Delete the shared project, removing students too.
245
delete = async (): Promise<void> => {
246
const store = this.get_store();
247
const shared_id = store.get_shared_project_id();
248
if (!shared_id) {
249
return;
250
}
251
const project_actions = redux.getActions("projects");
252
// delete project
253
await project_actions.delete_project(shared_id);
254
255
// remove student collabs
256
const ids = store.get_student_ids({ deleted: false });
257
if (ids == undefined) {
258
return;
259
}
260
for (const student_id of ids) {
261
const student_account_id = store.unsafe_getIn([
262
"students",
263
student_id,
264
"account_id",
265
]);
266
if (student_account_id) {
267
await project_actions.remove_collaborator(
268
shared_id,
269
student_account_id,
270
);
271
}
272
}
273
// make the course itself forget about the shared project:
274
this.actions.set({
275
table: "settings",
276
shared_project_id: "",
277
});
278
};
279
}
280
281