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/export/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
import { replace_all, split } from "@cocalc/util/misc";
7
import { redux } from "@cocalc/frontend/app-framework";
8
import { webapp_client } from "@cocalc/frontend/webapp-client";
9
10
import { CourseActions } from "../actions";
11
import { CourseStore } from "../store";
12
13
export class ExportActions {
14
private course_actions: CourseActions;
15
16
constructor(course_actions: CourseActions) {
17
this.course_actions = course_actions;
18
}
19
20
private get_store = (): CourseStore => {
21
return this.course_actions.get_store();
22
};
23
24
private path = (ext: string, what: string): string => {
25
// make path more likely to be python-readable...
26
const path = this.get_store().get("course_filename");
27
const p: string = split(replace_all(path, "-", "_")).join("_");
28
const i: number = p.lastIndexOf(".");
29
return `course-exports/${p.slice(0, i)}/${what}.${ext}`;
30
};
31
32
private open_file = (path: string): void => {
33
const project_id = this.get_store().get("course_project_id");
34
redux.getProjectActions(project_id).open_file({
35
path,
36
foreground: true,
37
});
38
};
39
40
private write_file = async (path: string, content: string): Promise<void> => {
41
const actions = this.course_actions;
42
const id = actions.set_activity({ desc: `Writing ${path}` });
43
const project_id = this.get_store().get("course_project_id");
44
try {
45
await webapp_client.project_client.write_text_file({
46
project_id,
47
path,
48
content,
49
});
50
if (actions.is_closed()) return;
51
this.open_file(path);
52
} catch (err) {
53
if (actions.is_closed()) return;
54
actions.set_error(`Error writing '${path}' -- '${err}'`);
55
} finally {
56
if (actions.is_closed()) return;
57
actions.set_activity({ id });
58
}
59
};
60
61
// newlines and duplicated double-quotes
62
private sanitize_csv_entry = (s: string): string => {
63
return s.replace(/\n/g, "\\n").replace(/"/g, '""');
64
};
65
66
to_csv = async (): Promise<void> => {
67
const store = this.get_store();
68
const assignments = store.get_sorted_assignments();
69
// CSV definition: http://edoceo.com/utilitas/csv-file-format
70
// i.e. double quotes everywhere (not single!) and double quote in double quotes usually blows up
71
// We had these nice comments, but actually CSV has no official
72
// support for comments, and this breaks some parsers, e.g.,
73
// https://github.com/sagemathinc/cocalc/issues/7138
74
// const timestamp = webapp_client.server_time().toISOString();
75
// let content = `# Course '${store.getIn(["settings", "title"])}'\n`;
76
// content += `# exported ${timestamp}\n`;
77
let content = "Name,Id,Email,";
78
content +=
79
(() => {
80
const result: any[] = [];
81
for (const assignment of assignments) {
82
result.push(`\"grade: ${assignment.get("path")}\"`);
83
}
84
return result;
85
})().join(",") + ",";
86
content +=
87
(() => {
88
const result1: any[] = [];
89
for (const assignment of assignments) {
90
result1.push(`\"comments: ${assignment.get("path")}\"`);
91
}
92
return result1;
93
})().join(",") + "\n";
94
for (const student of store.get_sorted_students()) {
95
var left2;
96
const grades = (() => {
97
const result2: any[] = [];
98
for (const assignment of assignments) {
99
let grade = store.get_grade(
100
assignment.get("assignment_id"),
101
student.get("student_id"),
102
);
103
grade = grade != null ? grade : "";
104
grade = this.sanitize_csv_entry(grade);
105
result2.push(`\"${grade}\"`);
106
}
107
return result2;
108
})().join(",");
109
110
const comments = (() => {
111
const result3: any[] = [];
112
for (const assignment of assignments) {
113
let comment = store.get_comments(
114
assignment.get("assignment_id"),
115
student.get("student_id"),
116
);
117
comment = comment != null ? comment : "";
118
comment = this.sanitize_csv_entry(comment);
119
result3.push(`\"${comment}\"`);
120
}
121
return result3;
122
})().join(",");
123
const name = `\"${this.sanitize_csv_entry(
124
store.get_student_name(student.get("student_id")),
125
)}\"`;
126
const email = `\"${
127
(left2 = store.get_student_email(student.get("student_id"))) != null
128
? left2
129
: ""
130
}\"`;
131
const id = `\"${student.get("student_id")}\"`;
132
const line = [name, id, email, grades, comments].join(",");
133
content += line + "\n";
134
}
135
this.write_file(this.path("csv", "grades"), content);
136
};
137
138
private export_grades = (): object => {
139
const obj: any = {};
140
const store = this.get_store();
141
const assignments = store.get_sorted_assignments();
142
obj.course = store.getIn(["settings", "title"]);
143
obj.exported = webapp_client.server_time().toISOString();
144
obj.assignments = [] as string[];
145
for (const assignment of assignments) {
146
obj.assignments.push(assignment.get("path"));
147
}
148
const students: any[] = [];
149
for (const student of store.get_sorted_students()) {
150
const student_id = student.get("student_id");
151
const grades: string[] = [];
152
for (const assignment of assignments) {
153
const assignment_id = assignment.get("assignment_id");
154
const grade = store.get_grade(assignment_id, student_id);
155
grades.push(grade);
156
}
157
const comments: string[] = [];
158
for (const assignment of assignments) {
159
const assignment_id = assignment.get("assignment_id");
160
const comment = store.get_comments(assignment_id, student_id);
161
comments.push(comment);
162
}
163
const nbgrader: any[] = [];
164
for (const assignment of assignments) {
165
const x =
166
assignment.getIn(["nbgrader_scores", student_id])?.toJS() ?? {};
167
for (const path in x) {
168
for (const id in x[path]) {
169
const entry = x[path][id];
170
delete entry.manual;
171
}
172
}
173
nbgrader.push(x);
174
}
175
const name = store.get_student_name(student_id);
176
let email = store.get_student_email(student_id) ?? "None";
177
const id = student.get("student_id");
178
students.push({ name, id, email, grades, nbgrader, comments });
179
}
180
obj.students = students;
181
return obj;
182
};
183
184
to_json = async (): Promise<void> => {
185
const obj = this.export_grades();
186
this.write_file(
187
this.path("json", "grades"),
188
JSON.stringify(obj, undefined, 2),
189
);
190
};
191
192
to_py = async (): Promise<void> => {
193
const obj = this.export_grades();
194
let content = "";
195
for (const key in obj) {
196
content += `${key} = ${JSON.stringify(obj[key], undefined, 2)}\n`;
197
}
198
this.write_file(this.path("py", "grades"), content);
199
};
200
201
file_use_times = async (assignment_or_handout_id: string): Promise<void> => {
202
const id = this.course_actions.set_activity({
203
desc: "Exporting file use times...",
204
});
205
try {
206
const { assignment, handout } = this.course_actions.resolve({
207
assignment_id: assignment_or_handout_id,
208
handout_id: assignment_or_handout_id,
209
});
210
if (assignment != null) {
211
const target_json = this.path(
212
"json",
213
"file-use-times/assignment/" +
214
replace_all(assignment.get("path"), "/", "-"),
215
);
216
await this.course_actions.assignments.export_file_use_times(
217
assignment_or_handout_id,
218
target_json,
219
);
220
this.open_file(target_json);
221
} else if (handout != null) {
222
const target_json = this.path(
223
"json",
224
"file-use-times/handouts/" +
225
replace_all(handout.get("path"), "/", "-"),
226
);
227
await this.course_actions.handouts.export_file_use_times(
228
assignment_or_handout_id,
229
target_json,
230
);
231
this.open_file(target_json);
232
} else {
233
throw Error(
234
`Unknown handout or assignment "${assignment_or_handout_id}"`,
235
);
236
}
237
} catch (err) {
238
this.course_actions.set_error(`Error exporting file use times -- ${err}`);
239
} finally {
240
this.course_actions.set_activity({ id });
241
}
242
};
243
}
244
245