Skip to content

Commit 1ca4eb9

Browse files
New feature to upload multiple exercises
A new feature has been added to allow teachers to upload multiple exercises. To do so, they just have to use the new "Add multiple exercises" button available for each course and select a directory containing a folder for each new exercise. The folders must include the corresponding files that will later form the exercises' templates.
1 parent fcf5f3b commit 1ca4eb9

File tree

19 files changed

+308
-119
lines changed

19 files changed

+308
-119
lines changed

vscode4teaching-extension/package.json

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@
109109
"dark": "resources/dark/add.png"
110110
}
111111
},
112+
{
113+
"command": "vscode4teaching.addmultipleexercises",
114+
"title": "Add Multiple Exercises",
115+
"icon": {
116+
"light": "resources/light/add_multiple.png",
117+
"dark": "resources/dark/add_multiple.png"
118+
}
119+
},
112120
{
113121
"command": "vscode4teaching.editexercise",
114122
"title": "Edit Exercise",
@@ -209,30 +217,35 @@
209217
"group": "inline@2"
210218
},
211219
{
212-
"command": "vscode4teaching.editcourse",
220+
"command": "vscode4teaching.addmultipleexercises",
213221
"when": "view == vscode4teachingview && viewItem == courseteacher",
214222
"group": "inline@3"
215223
},
216224
{
217-
"command": "vscode4teaching.deletecourse",
225+
"command": "vscode4teaching.editcourse",
218226
"when": "view == vscode4teachingview && viewItem == courseteacher",
219227
"group": "inline@4"
220228
},
221229
{
222-
"command": "vscode4teaching.adduserstocourse",
230+
"command": "vscode4teaching.deletecourse",
223231
"when": "view == vscode4teachingview && viewItem == courseteacher",
224232
"group": "inline@5"
225233
},
226234
{
227-
"command": "vscode4teaching.removeusersfromcourse",
235+
"command": "vscode4teaching.adduserstocourse",
228236
"when": "view == vscode4teachingview && viewItem == courseteacher",
229237
"group": "inline@6"
230238
},
231239
{
232-
"command": "vscode4teaching.share",
240+
"command": "vscode4teaching.removeusersfromcourse",
233241
"when": "view == vscode4teachingview && viewItem == courseteacher",
234242
"group": "inline@7"
235243
},
244+
{
245+
"command": "vscode4teaching.share",
246+
"when": "view == vscode4teachingview && viewItem == courseteacher",
247+
"group": "inline@8"
248+
},
236249
{
237250
"command": "vscode4teaching.editexercise",
238251
"when": "view == vscode4teachingview && viewItem == exerciseteacher",
922 Bytes
Loading

vscode4teaching-extension/resources/dashboard/dashboard.css

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,3 @@ table th {
202202
transform: rotate(45deg);
203203
}
204204

205-
.workspace-link {
206-
margin-right: 4px;
207-
}
853 Bytes
Loading

vscode4teaching-extension/src/client/APIClient.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -211,14 +211,14 @@ class APIClientSingleton {
211211
return APIClient.createRequest(options, "Deleting course...");
212212
}
213213

214-
public addExercise(id: number, data: ExerciseEdit): AxiosPromise<Exercise> {
214+
public addExercises(courseId: number, exercisesData: ExerciseEdit[]): AxiosPromise<Exercise[]> {
215215
const options: AxiosBuildOptions = {
216-
url: "/api/courses/" + id + "/exercises",
216+
url: "/api/v2/courses/" + courseId + "/exercises",
217217
method: "POST",
218218
responseType: "json",
219-
data,
219+
data: exercisesData,
220220
};
221-
return APIClient.createRequest(options, "Adding exercise...");
221+
return APIClient.createRequest(options, "Adding new exercises...");
222222
}
223223

224224
public editExercise(id: number, data: ExerciseEdit): AxiosPromise<Exercise> {
@@ -231,7 +231,7 @@ class APIClientSingleton {
231231
return APIClient.createRequest(options, "Sending exercise info...");
232232
}
233233

234-
public uploadExerciseTemplate(id: number, data: Buffer): AxiosPromise<any> {
234+
public uploadExerciseTemplate(id: number, data: Buffer, showNotification?: boolean): AxiosPromise<any> {
235235
const dataForm = new FormData();
236236
dataForm.append("file", data, { filename: "template.zip" });
237237
const options: AxiosBuildOptions = {
@@ -240,7 +240,7 @@ class APIClientSingleton {
240240
responseType: "json",
241241
data: dataForm,
242242
};
243-
return APIClient.createRequest(options, "Uploading template...", true);
243+
return APIClient.createRequest(options, "Uploading template...", showNotification ?? true);
244244
}
245245

246246
public deleteExercise(id: number): AxiosPromise<void> {

vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { V4TBuildItems } from "./V4TItem/V4TBuiltItems";
1212
import { V4TItem } from "./V4TItem/V4TItem";
1313
import { V4TItemType } from "./V4TItem/V4TItemType";
1414
import { Validators } from "./Validators";
15+
import * as fs from "fs";
16+
import * as path from "path";
1517

1618
/**
1719
* Tree view that lists extension's basic options like:
@@ -195,7 +197,6 @@ export class CoursesProvider implements vscode.TreeDataProvider<V4TItem> {
195197
// Only axios requests throw error
196198
APIClient.handleAxiosError(error);
197199
}
198-
199200
}
200201

201202
/**
@@ -227,7 +228,7 @@ export class CoursesProvider implements vscode.TreeDataProvider<V4TItem> {
227228
if (item.item && instanceOfCourse(item.item)) {
228229
try {
229230
const selectedOption = await vscode.window.showWarningMessage("Are you sure you want to delete " + item.item.name + "?", { modal: true }, "Accept");
230-
if ((selectedOption === "Accept") && CurrentUser.isLoggedIn() && CurrentUser.getUserInfo().courses) {
231+
if (selectedOption === "Accept" && CurrentUser.isLoggedIn() && CurrentUser.getUserInfo().courses) {
231232
const response = await APIClient.deleteCourse(item.item.id);
232233
console.debug(response);
233234
await CurrentUser.updateUserInfo();
@@ -283,17 +284,17 @@ export class CoursesProvider implements vscode.TreeDataProvider<V4TItem> {
283284
try {
284285
this.loading = true;
285286
CoursesProvider.triggerTreeReload();
286-
const addExerciseData = await APIClient.addExercise(course.id, { name });
287+
const addExerciseData = await APIClient.addExercises(course.id, [{ name }]);
287288
console.debug(addExerciseData);
288289
try {
289290
// When exercise is createdupload template
290291
const zipContent = await FileZipUtil.getZipFromUris(fileUris);
291-
const response = await APIClient.uploadExerciseTemplate(addExerciseData.data.id, zipContent);
292+
const response = await APIClient.uploadExerciseTemplate(addExerciseData.data[0].id, zipContent);
292293
console.debug(response);
293294
} catch (uploadError) {
294295
try {
295296
// If upload fails delete the exercise and show error
296-
const response = await APIClient.deleteExercise(addExerciseData.data.id);
297+
const response = await APIClient.deleteExercise(addExerciseData.data[0].id);
297298
console.debug(response);
298299
APIClient.handleAxiosError(uploadError);
299300
} catch (deleteError) {
@@ -302,6 +303,7 @@ export class CoursesProvider implements vscode.TreeDataProvider<V4TItem> {
302303
} finally {
303304
this.loading = false;
304305
CoursesProvider.triggerTreeReload();
306+
vscode.window.showInformationMessage("Exercise added.");
305307
}
306308
} catch (error) {
307309
APIClient.handleAxiosError(error);
@@ -311,6 +313,65 @@ export class CoursesProvider implements vscode.TreeDataProvider<V4TItem> {
311313
}
312314
}
313315

316+
/**
317+
* Prepare and send multiple exercises' creation request
318+
* @param item course
319+
*/
320+
public async addMultipleExercises(item: V4TItem) {
321+
if (item.item && instanceOfCourse(item.item)) {
322+
const course = item.item;
323+
// Explain user how to organize their exercises' directory
324+
vscode.window.showInformationMessage("To upload multiple exercises, prepare a directory with a folder for each exercise, each folder including the exercise's corresponding template. When ready, click 'Accept'.", "Accept").then(async (ans) => {
325+
if (ans === "Accept") {
326+
// Ask user to select a directory
327+
// This directory has to contain exercises (1 folder = 1 new exercise)
328+
const parentDirectoryUri = await vscode.window.showOpenDialog({
329+
canSelectFiles: false,
330+
canSelectFolders: true,
331+
canSelectMany: false,
332+
openLabel: "Select directory",
333+
});
334+
if (parentDirectoryUri) {
335+
const fsUri = parentDirectoryUri[0].fsPath;
336+
// Get every folder from a selected directory
337+
const exercisesDirectories = fs.readdirSync(fsUri, { withFileTypes: true }).filter((d) => d.isDirectory());
338+
// Get the number of directories
339+
const availableFolderNumber = exercisesDirectories.length;
340+
// Prepare count of successfully uploaded exercises
341+
let uploadedExercises = 0;
342+
// Unsuccessful responses' control (true if there were any)
343+
let errorCaught = false;
344+
if (exercisesDirectories.length > 1) {
345+
// Exercises are uploaded in batches of 3 exercises
346+
while (exercisesDirectories.length > 0) {
347+
const exercisesDirChunk = exercisesDirectories.splice(0, 3);
348+
// Collect exercises' names from directories' names
349+
const exerciseData = await APIClient.addExercises(
350+
course.id,
351+
exercisesDirChunk.map((d) => ({ name: d.name }))
352+
);
353+
354+
// When added to DB, templates of each exercise are sent
355+
exerciseData.data.map(async (ex, index) => {
356+
APIClient.uploadExerciseTemplate(ex.id, await FileZipUtil.getZipFromUris([vscode.Uri.parse(fsUri + path.sep + exercisesDirChunk[index].name)]), false)
357+
.then((_) => uploadedExercises++)
358+
.catch((_) => (errorCaught = true));
359+
});
360+
}
361+
if (errorCaught || availableFolderNumber !== uploadedExercises) {
362+
vscode.window.showInformationMessage("All exercises were successfully uploaded.");
363+
} else {
364+
vscode.window.showErrorMessage("One or more exercises were not properly uploaded.");
365+
}
366+
} else {
367+
vscode.window.showErrorMessage("No exercises have been uploaded since there were not any to upload in the selected folder.");
368+
}
369+
}
370+
}
371+
});
372+
}
373+
}
374+
314375
/**
315376
* Show form for editing an exercise then call client.
316377
* @param item exercise
@@ -517,7 +578,7 @@ export class CoursesProvider implements vscode.TreeDataProvider<V4TItem> {
517578
* @param validator validator (check model/Validators.ts)
518579
* @param options available options for input box
519580
*/
520-
private async getInput(prompt: string, validator: ((value: string) => string | undefined | null | Thenable<string | undefined | null>), options?: { value?: string, password?: boolean }) {
581+
private async getInput(prompt: string, validator: (value: string) => string | undefined | null | Thenable<string | undefined | null>, options?: { value?: string; password?: boolean }) {
521582
let inputOptions: vscode.InputBoxOptions = { prompt };
522583
if (options) {
523584
if (options.value) {

vscode4teaching-extension/src/components/courses/V4TItem/V4TBuiltItems.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class V4TBuildItems {
1818
});
1919
public static readonly SIGNUP_TEACHER_ITEM = new V4TItem("Invite a teacher", V4TItemType.SignupTeacher, TreeItemCollapsibleState.None, undefined, undefined, {
2020
command: "vscode4teaching.signupteacher",
21-
title: "Sign up in VS Code 4 Teaching",
21+
title: "Invite a teacher to join VS Code 4 Teaching",
2222
});
2323
public static readonly LOGOUT_ITEM = new V4TItem("Logout", V4TItemType.Logout, TreeItemCollapsibleState.None, undefined, undefined, {
2424
command: "vscode4teaching.logout",
@@ -30,5 +30,4 @@ export class V4TBuildItems {
3030
});
3131
public static readonly NO_COURSES_ITEM = new V4TItem("No courses available", V4TItemType.NoCourses, TreeItemCollapsibleState.None);
3232
public static readonly NO_EXERCISES_ITEM = new V4TItem("No exercises available", V4TItemType.NoExercises, TreeItemCollapsibleState.None);
33-
3433
}

0 commit comments

Comments
 (0)