From 07b113b29cc8c21bd9006df699304c4efa933a43 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Wed, 20 Oct 2021 12:11:17 +0200 Subject: [PATCH 01/35] Don't remove session on http error --- vscode4teaching-extension/src/client/APIClient.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vscode4teaching-extension/src/client/APIClient.ts b/vscode4teaching-extension/src/client/APIClient.ts index d56168ae..b3d6fc3e 100644 --- a/vscode4teaching-extension/src/client/APIClient.ts +++ b/vscode4teaching-extension/src/client/APIClient.ts @@ -99,12 +99,13 @@ class APIClientSingleton { console.error(error); if (axios.isCancel(error)) { vscode.window.showErrorMessage("Request timeout."); - APIClientSession.invalidateSession(); + // APIClientSession.invalidateSession(); } else if (error.response) { if (error.response.status === 401 && !APIClient.error401thrown) { vscode.window.showWarningMessage("It seems that we couldn't log in, please log in."); APIClient.error401thrown = true; - APIClientSession.invalidateSession(); + APIClient.getXSRFToken(); + // APIClientSession.invalidateSession(); CoursesProvider.triggerTreeReload(); } else if (error.response.status === 403 && !APIClient.error403thrown) { vscode.window.showWarningMessage("Something went wrong, please try again."); @@ -118,7 +119,7 @@ class APIClientSingleton { vscode.window.showErrorMessage("Error " + error.response.status + ". " + msg); APIClient.error401thrown = false; APIClient.error403thrown = false; - APIClientSession.invalidateSession(); + // APIClientSession.invalidateSession(); } } else if (error.request) { vscode.window.showErrorMessage("Can't connect to the server. " + error.message); From 2b89dc3c2655b07eb9b02c270f674647ca6fadb2 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Wed, 20 Oct 2021 13:10:05 +0200 Subject: [PATCH 02/35] Only invalidate session on 401 --- vscode4teaching-extension/src/client/APIClient.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vscode4teaching-extension/src/client/APIClient.ts b/vscode4teaching-extension/src/client/APIClient.ts index b3d6fc3e..965e3507 100644 --- a/vscode4teaching-extension/src/client/APIClient.ts +++ b/vscode4teaching-extension/src/client/APIClient.ts @@ -101,11 +101,12 @@ class APIClientSingleton { vscode.window.showErrorMessage("Request timeout."); // APIClientSession.invalidateSession(); } else if (error.response) { + console.log(error.response); + console.log(error.request); if (error.response.status === 401 && !APIClient.error401thrown) { vscode.window.showWarningMessage("It seems that we couldn't log in, please log in."); APIClient.error401thrown = true; - APIClient.getXSRFToken(); - // APIClientSession.invalidateSession(); + APIClientSession.invalidateSession(); CoursesProvider.triggerTreeReload(); } else if (error.response.status === 403 && !APIClient.error403thrown) { vscode.window.showWarningMessage("Something went wrong, please try again."); From ee692020bbf09ecc2b52fb5f5eb7a35f70a63af3 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Wed, 20 Oct 2021 14:10:54 +0200 Subject: [PATCH 03/35] Don't try to initialize liveshare if not logged in. Don't show open dashboard message after clicking open or diff --- vscode4teaching-extension/src/extension.ts | 41 +++++++++++----------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index 466699d7..5ff456a4 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -45,7 +45,6 @@ export let commentInterval: NodeJS.Timeout; export let wsLiveshare: WebSocketV4TConnection | undefined; export let liveshareService: LiveShareService | undefined; -// TODO: Comments not working export function activate(context: vscode.ExtensionContext) { vscode.window.registerTreeDataProvider("vscode4teachingview", coursesProvider); const sessionInitialized = APIClient.initializeSessionFromFile(); @@ -273,11 +272,6 @@ export function activate(context: vscode.ExtensionContext) { deleteCourseDisposable, refreshView, refreshCourse, addExercise, editExercise, deleteExercise, addUsersToCourse, removeUsersFromCourse, getStudentFiles, diff, createComment, share, signup, signupTeacher, getWithCode, finishExercise, showDashboard, showLiveshareBoard); - initializeLiveShare().then(() => { - console.log("LiveShare initialized"); - console.log(liveshareService); - console.log(wsLiveshare); - }); } export function deactivate() { @@ -304,7 +298,7 @@ export function disableFeatures() { global.clearInterval(commentInterval); } -export async function initializeExtension(cwds: ReadonlyArray) { +export async function initializeExtension(cwds: ReadonlyArray, restartDashboard?: boolean) { disableFeatures(); @@ -323,6 +317,11 @@ export async function initializeExtension(cwds: ReadonlyArray { + console.log("LiveShare initialized"); + console.log(liveshareService); + console.log(wsLiveshare); + }); try { const courses = CurrentUser.getUserInfo().courses; if (courses && !showLiveshareBoardItem) { @@ -373,18 +372,20 @@ export async function initializeExtension(cwds: ReadonlyArray { - console.debug(value); - if (value === openDashboard) { - console.debug("Opening dashboard"); - return vscode.commands.executeCommand("vscode4teaching.showdashboard"); - } - }).then(() => console.debug("Message dismissed")); + if (!restartDashboard) { + const message = ` + The exercise has been downloaded! You can see the template files and your students' files in the Explorer view (Ctrl + Shift + E). + You can also open the Dashboard to monitor their progress (you can also open it from the status bar's 'Dashboard' button. + `; + const openDashboard = "Open dashboard"; + vscode.window.showInformationMessage(message, openDashboard).then((value: string | undefined) => { + console.debug(value); + if (value === openDashboard) { + console.debug("Opening dashboard"); + return vscode.commands.executeCommand("vscode4teaching.showdashboard"); + } + }).then(() => console.debug("Message dismissed")); + } } } // Set template location if exists @@ -536,7 +537,7 @@ async function getMultipleStudentExerciseFiles(courseName: string, exercise: Exe ...subdirectoriesURIs); currentCwds = vscode.workspace.workspaceFolders; if (currentCwds && !newWorkspaces) { - await initializeExtension(currentCwds); + await initializeExtension(currentCwds, true); } } } From 9ff98e298d3612861327aea4f0092e69018670b6 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Thu, 21 Oct 2021 10:00:45 +0200 Subject: [PATCH 04/35] Don't invalidate session on non axios error. Improve tests for client api --- .../src/client/APIClient.ts | 1 - .../test/unitSuite/Client.test.ts | 25 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/vscode4teaching-extension/src/client/APIClient.ts b/vscode4teaching-extension/src/client/APIClient.ts index 965e3507..b6ae8eca 100644 --- a/vscode4teaching-extension/src/client/APIClient.ts +++ b/vscode4teaching-extension/src/client/APIClient.ts @@ -127,7 +127,6 @@ class APIClientSingleton { APIClientSession.invalidateSession(); } else { vscode.window.showErrorMessage(error.message); - APIClientSession.invalidateSession(); } } diff --git a/vscode4teaching-extension/test/unitSuite/Client.test.ts b/vscode4teaching-extension/test/unitSuite/Client.test.ts index 91b95780..7db5cb10 100644 --- a/vscode4teaching-extension/test/unitSuite/Client.test.ts +++ b/vscode4teaching-extension/test/unitSuite/Client.test.ts @@ -33,10 +33,16 @@ function createSessionFile(newXsrfToken: string, newJwtToken: string) { fs.writeFileSync(APIClientSession.sessionPath, newJwtToken + "\n" + newXsrfToken); } -function expectSessionInvalidated() { - // Session is invalidated - expect(mockedCurrentUser.resetUserInfo).toHaveBeenCalledTimes(1); - expect(fs.existsSync(APIClientSession.sessionPath)).toBeFalsy(); +function expectSessionInvalidated(isInvalidated: boolean) { + if (isInvalidated) { + // Session is invalidated + expect(mockedCurrentUser.resetUserInfo).toHaveBeenCalledTimes(1); + expect(fs.existsSync(APIClientSession.sessionPath)).toBeFalsy(); + } else { + // Session is not invalidated + expect(mockedCurrentUser.resetUserInfo).toHaveBeenCalledTimes(0); + expect(fs.existsSync(APIClientSession.sessionPath)).toBeTruthy(); + } } describe("Client", () => { @@ -153,7 +159,7 @@ describe("Client", () => { it("should invalidate session", () => { APIClient.invalidateSession(); - expectSessionInvalidated(); + expectSessionInvalidated(true); }); it("should handle error 401", () => { @@ -170,7 +176,7 @@ describe("Client", () => { // Refresh tree view expect(mockedCoursesTreeProvider.triggerTreeReload).toHaveBeenCalledTimes(1); expect(global.console.error).toHaveBeenCalledTimes(1); - expectSessionInvalidated(); + expectSessionInvalidated(true); }); it("should handle error 403", () => { @@ -215,6 +221,7 @@ describe("Client", () => { expect(mockedAxios).toHaveBeenCalledTimes(1); expect(mockedAxios).toHaveBeenNthCalledWith(1, expectedAxiosConfigXSRFRequest); expect(global.console.error).toHaveBeenCalledTimes(1); + expectSessionInvalidated(false); }); it("should handle rest of http errors", () => { @@ -233,7 +240,7 @@ describe("Client", () => { // Error message showed expect(mockedVscode.window.showErrorMessage).toHaveBeenCalledTimes(1); expect(mockedVscode.window.showErrorMessage).toHaveBeenCalledWith("Error 400. " + JSON.stringify(dataError)); - expectSessionInvalidated(); + expectSessionInvalidated(false); expect(global.console.error).toHaveBeenCalledTimes(1); }); @@ -249,7 +256,7 @@ describe("Client", () => { // Error message showed expect(mockedVscode.window.showErrorMessage).toHaveBeenCalledTimes(1); expect(mockedVscode.window.showErrorMessage).toHaveBeenCalledWith("Can't connect to the server. " + error.message); - expectSessionInvalidated(); + expectSessionInvalidated(true); expect(global.console.error).toHaveBeenCalledTimes(1); }); @@ -262,7 +269,7 @@ describe("Client", () => { // Error message showed expect(mockedVscode.window.showErrorMessage).toHaveBeenCalledTimes(1); expect(mockedVscode.window.showErrorMessage).toHaveBeenCalledWith(error.message); - expectSessionInvalidated(); + expectSessionInvalidated(false); expect(global.console.error).toHaveBeenCalledTimes(1); }); From 772944d700c8036477fe4a3faa3d168241d77ba2 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Thu, 21 Oct 2021 10:52:44 +0200 Subject: [PATCH 05/35] Control more exceptions to not show a 500 error on some of them --- .../controllers/ExceptionController.java | 35 +++++++--------- .../ExerciseFilesControllerTests.java | 40 +++++++++++++++++++ .../integrationtests/IntegrationTests.java | 34 ++++++++++++++++ .../ExerciseFilesServiceImplTests.java | 31 ++++++++++++++ 4 files changed, 119 insertions(+), 21 deletions(-) diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExceptionController.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExceptionController.java index 7706bdc7..3365ed8f 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExceptionController.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExceptionController.java @@ -10,6 +10,10 @@ import com.vscode4teaching.vscode4teachingserver.controllers.exceptioncontrol.ValidationErrorResponse; import com.vscode4teaching.vscode4teachingserver.controllers.exceptioncontrol.ValidationErrorResponse.ErrorDetail; import com.vscode4teaching.vscode4teachingserver.services.exceptions.CantRemoveCreatorException; +import com.vscode4teaching.vscode4teachingserver.services.exceptions.EmptyJSONObjectException; +import com.vscode4teaching.vscode4teachingserver.services.exceptions.EmptyURIException; +import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseFinishedException; +import com.vscode4teaching.vscode4teachingserver.services.exceptions.MissingPropertyException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.NoTemplateException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotCreatorException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotFoundException; @@ -93,39 +97,28 @@ public ResponseEntity handleUsernameNotFoundException(UsernameNotFoundEx return new ResponseEntity<>("This user does not exist: " + e.getMessage(), HttpStatus.UNAUTHORIZED); } - @ExceptionHandler(NotInCourseException.class) + @ExceptionHandler(MalformedJwtException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) - public ResponseEntity handleNotInCourseException(NotInCourseException e) { + public ResponseEntity handleMalformedJwtException(MalformedJwtException e) { return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED); } + @ExceptionHandler(value = { NotInCourseException.class, CantRemoveCreatorException.class, NotCreatorException.class }) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ResponseEntity handleNotInCourseException(NotInCourseException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } + @ExceptionHandler(NoTemplateException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ResponseEntity handleNoTemplateException(NoTemplateException e) { return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); } - @ExceptionHandler(MultipartException.class) + @ExceptionHandler(value = { ExerciseFinishedException.class, MultipartException.class, + EmptyJSONObjectException.class, EmptyURIException.class, MissingPropertyException.class}) @ResponseStatus(HttpStatus.BAD_REQUEST) public ResponseEntity handleMultipartException(MultipartException e) { return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); } - - @ExceptionHandler(MalformedJwtException.class) - @ResponseStatus(HttpStatus.UNAUTHORIZED) - public ResponseEntity handleMalformedJwtException(MalformedJwtException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED); - } - - @ExceptionHandler(CantRemoveCreatorException.class) - @ResponseStatus(HttpStatus.UNAUTHORIZED) - public ResponseEntity handleCantRemoveCreatorException(CantRemoveCreatorException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED); - } - - @ExceptionHandler(NotCreatorException.class) - @ResponseStatus(HttpStatus.UNAUTHORIZED) - public ResponseEntity handleNotCreatorException(NotCreatorException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED); - } } \ No newline at end of file diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java index 51bb8669..40edd965 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java @@ -35,6 +35,7 @@ import com.vscode4teaching.vscode4teachingserver.model.ExerciseFile; import com.vscode4teaching.vscode4teachingserver.model.views.FileViews; import com.vscode4teaching.vscode4teachingserver.services.ExerciseFilesService; +import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseFinishedException; import org.apache.tomcat.util.http.fileupload.FileUtils; import org.junit.jupiter.api.AfterEach; @@ -192,6 +193,45 @@ public void uploadFile() throws Exception { verify(filesService, times(1)).saveExerciseFiles(anyLong(), any(MultipartFile.class), anyString()); } + @Test + public void uploadFileFinishedException() throws Exception { + Exercise exercise = new Exercise("Exercise 1"); + exercise.setId(1l); + byte[] mock = null; + MockMultipartFile mockMultiFile1 = new MockMultipartFile("file", "exs.zip", "application/zip", mock); + Files.createDirectories(Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/ex3")); + Path path1 = Paths.get("src/test/java/com/vscode4teaching/vscode4teachingserver/files/ex1.html"); + Path path1Copy = Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/ex1.html"); + Files.copy(path1, path1Copy, StandardCopyOption.REPLACE_EXISTING); + Path path2 = Paths.get("src/test/java/com/vscode4teaching/vscode4teachingserver/files/ex2.html"); + Path path2Copy = Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/ex2.html"); + Files.copy(path2, path2Copy, StandardCopyOption.REPLACE_EXISTING); + Path path3 = Paths.get("src/test/java/com/vscode4teaching/vscode4teachingserver/files/ex3/ex3.html"); + Path path3Copy = Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/ex3/ex3.html"); + Files.copy(path3, path3Copy, StandardCopyOption.REPLACE_EXISTING); + + File mockFile1 = new File("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/", "ex1.html"); + File mockFile2 = new File("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/", "ex2.html"); + File mockFile3 = new File("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/", "ex3/ex3.html"); + Map> returnMap = new HashMap<>(); + returnMap.put(exercise, Arrays.asList(mockFile1, mockFile2, mockFile3)); + when(filesService.saveExerciseFiles(anyLong(), any(MultipartFile.class), anyString())).thenThrow(ExerciseFinishedException.class); + + MvcResult result = mockMvc.perform(multipart("/api/exercises/1/files").file(mockMultiFile1).with(csrf()) + .header("Authorization", "Bearer " + jwtToken.getJwtToken())).andExpect(status().isBadRequest()).andReturn(); + + List expectedResponse = new ArrayList<>(); + expectedResponse.add(new UploadFileResponse("ex1.html", "text/html", 23l)); + expectedResponse.add(new UploadFileResponse("ex2.html", "text/html", 23l)); + expectedResponse.add(new UploadFileResponse("ex3" + File.separator + "ex3.html", "text/html", 23l)); + + assertThat(result.getResponse().getContentAsString()) + .isEqualToIgnoringWhitespace(objectMapper.writeValueAsString(expectedResponse)); + + logger.info(result.getResponse().getContentAsString()); + verify(filesService, times(1)).saveExerciseFiles(anyLong(), any(MultipartFile.class), anyString()); + } + @Test public void uploadFile_noBody() throws Exception { mockMvc.perform(multipart("/api/exercises/1/files").with(csrf()).header("Authorization", diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/integrationtests/IntegrationTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/integrationtests/IntegrationTests.java index c3afc486..d7838fce 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/integrationtests/IntegrationTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/integrationtests/IntegrationTests.java @@ -52,4 +52,38 @@ public void login_into_createCourse() throws Exception { Course actualResponse = objectMapper.readValue(mvcResult.getResponse().getContentAsString(), Course.class); assertThat(actualResponse.getName()).isEqualTo(course.getName()); } + + // Login, try to create course without body (fails with bad request) and token + // doesn't get invalidated on error + @Test + public void expectError() throws Exception { + JWTRequest jwtRequest = new JWTRequest(); + jwtRequest.setUsername("johndoe"); + jwtRequest.setPassword("teacherpassword"); + + MvcResult loginResult = mockMvc.perform(post("/api/login").contentType("application/json").with(csrf()) + .content(objectMapper.writeValueAsString(jwtRequest))).andExpect(status().isOk()).andReturn(); + JWTResponse jwtToken = objectMapper.readValue(loginResult.getResponse().getContentAsString(), + JWTResponse.class); + + MvcResult mvcErrorResult = mockMvc.perform(post("/api/courses").contentType("application/json").with(csrf()) + .header("Authorization", "Bearer " + jwtToken.getJwtToken())).andExpect(status().isBadRequest()) + .andReturn(); + + CourseDTO course = new CourseDTO(); + course.setName("Spring Boot Course"); + + MvcResult mvcCorrectResult = mockMvc + .perform(post("/api/courses").contentType("application/json").with(csrf()) + .content(objectMapper.writeValueAsString(course)) + .header("Authorization", "Bearer " + jwtToken.getJwtToken())) + .andExpect(status().isCreated()).andReturn(); + + String actualErrorResponse = mvcErrorResult.getResponse().getContentAsString(); + assertThat(actualErrorResponse).isEqualTo(""); + Course actualCorrectResponse = objectMapper.readValue(mvcCorrectResult.getResponse().getContentAsString(), + Course.class); + assertThat(actualCorrectResponse.getName()).isEqualTo(course.getName()); + } + } \ No newline at end of file diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java index 297cb8b4..2dec7f9c 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java @@ -29,6 +29,7 @@ import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.ExerciseUserInfoRepository; import com.vscode4teaching.vscode4teachingserver.model.repositories.UserRepository; +import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseFinishedException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseNotFoundException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.NoTemplateException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotInCourseException; @@ -241,6 +242,36 @@ public void saveExerciseFiles() throws Exception { assertThat(savedFiles.get(2).getAbsolutePath()).isEqualToIgnoringCase(exercise.getUserFiles().get(2).getPath()); } + @Test + public void saveExerciseFilesFinishedError() throws Exception { + User student = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); + student.setId(3l); + Role studentRole = new Role("ROLE_STUDENT"); + studentRole.setId(2l); + student.addRole(studentRole); + Course course = new Course("Spring Boot Course"); + course.setId(4l); + course.addUserInCourse(student); + Exercise exercise = new Exercise(); + exercise.setName("Exercise 1"); + exercise.setId(1l); + course.addExercise(exercise); + exercise.setCourse(course); + ExerciseUserInfo eui = new ExerciseUserInfo(exercise, student); + eui.setStatus(1); + when(exerciseUserInfoRepository.findByExercise_IdAndUser_Username(anyLong(), anyString())) + .thenReturn(Optional.of(eui)); + // Get files + File file = Paths.get("src/test/java/com/vscode4teaching/vscode4teachingserver/files", "exs.zip").toFile(); + MultipartFile mockFile = new MockMultipartFile("file", file.getName(), "application/zip", + new FileInputStream(file)); + + ExerciseFinishedException e = assertThrows(ExerciseFinishedException.class, () -> filesService.saveExerciseFiles(1l, mockFile, "johndoe")); + + assertThat(e.getMessage()).isEqualToIgnoringWhitespace("Exercise is marked as finished: 1"); + verify(exerciseUserInfoRepository, times(1)).findByExercise_IdAndUser_Username(anyLong(), anyString()); + } + @Test public void saveExerciseTemplate() throws Exception { User teacher = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); From 34e0245211d95af2905a2d6a47373a01f69d177d Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Thu, 21 Oct 2021 11:25:08 +0200 Subject: [PATCH 06/35] Add error handling to finish exercise requestr --- vscode4teaching-extension/src/extension.ts | 32 +++++++++++++--------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index 5ff456a4..e96211f5 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -223,19 +223,25 @@ export function activate(context: vscode.ExtensionContext) { const warnMessage = "Finish exercise? Exercise will be marked as finished and you will not be able to upload any more updates"; const selectedOption = await vscode.window.showWarningMessage(warnMessage, { modal: true }, "Accept"); if (selectedOption === "Accept" && finishItem) { - const response = await APIClient.updateExerciseUserInfo(finishItem.getExerciseId(), 1); - console.debug(response); - if (response.data.status === 1 && finishItem) { - finishItem.dispose(); - if (changeEvent) { - changeEvent.dispose(); - } - if (createEvent) { - createEvent.dispose(); - } - if (deleteEvent) { - deleteEvent.dispose(); + try { + const response = await APIClient.updateExerciseUserInfo(finishItem.getExerciseId(), 1); + console.debug(response); + if (response.data.status === 1 && finishItem) { + finishItem.dispose(); + if (changeEvent) { + changeEvent.dispose(); + } + if (createEvent) { + createEvent.dispose(); + } + if (deleteEvent) { + deleteEvent.dispose(); + } + } else { + vscode.window.showErrorMessage("An unexpected error has occurred. The exercise has not been marked as finished. Please try again."); } + } catch (error) { + APIClient.handleAxiosError(error); } } }); @@ -357,7 +363,7 @@ export async function initializeExtension(cwds: ReadonlyArray console.debug("Message dismissed")); } From 099aa89e9d6bc77f01fc9329e225879841bf8b7d Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Thu, 21 Oct 2021 11:36:22 +0200 Subject: [PATCH 07/35] Remove duplicated test --- .../ExerciseFilesControllerTests.java | 39 +------------------ 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java index 40edd965..cd9322c8 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java @@ -54,6 +54,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.util.NestedServletException; @SpringBootTest @TestPropertySource(locations = "classpath:test.properties") @@ -193,44 +194,6 @@ public void uploadFile() throws Exception { verify(filesService, times(1)).saveExerciseFiles(anyLong(), any(MultipartFile.class), anyString()); } - @Test - public void uploadFileFinishedException() throws Exception { - Exercise exercise = new Exercise("Exercise 1"); - exercise.setId(1l); - byte[] mock = null; - MockMultipartFile mockMultiFile1 = new MockMultipartFile("file", "exs.zip", "application/zip", mock); - Files.createDirectories(Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/ex3")); - Path path1 = Paths.get("src/test/java/com/vscode4teaching/vscode4teachingserver/files/ex1.html"); - Path path1Copy = Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/ex1.html"); - Files.copy(path1, path1Copy, StandardCopyOption.REPLACE_EXISTING); - Path path2 = Paths.get("src/test/java/com/vscode4teaching/vscode4teachingserver/files/ex2.html"); - Path path2Copy = Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/ex2.html"); - Files.copy(path2, path2Copy, StandardCopyOption.REPLACE_EXISTING); - Path path3 = Paths.get("src/test/java/com/vscode4teaching/vscode4teachingserver/files/ex3/ex3.html"); - Path path3Copy = Paths.get("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/ex3/ex3.html"); - Files.copy(path3, path3Copy, StandardCopyOption.REPLACE_EXISTING); - - File mockFile1 = new File("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/", "ex1.html"); - File mockFile2 = new File("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/", "ex2.html"); - File mockFile3 = new File("v4t-course-test/spring_boot_course_2/exercise_1_1/johndoe/", "ex3/ex3.html"); - Map> returnMap = new HashMap<>(); - returnMap.put(exercise, Arrays.asList(mockFile1, mockFile2, mockFile3)); - when(filesService.saveExerciseFiles(anyLong(), any(MultipartFile.class), anyString())).thenThrow(ExerciseFinishedException.class); - - MvcResult result = mockMvc.perform(multipart("/api/exercises/1/files").file(mockMultiFile1).with(csrf()) - .header("Authorization", "Bearer " + jwtToken.getJwtToken())).andExpect(status().isBadRequest()).andReturn(); - - List expectedResponse = new ArrayList<>(); - expectedResponse.add(new UploadFileResponse("ex1.html", "text/html", 23l)); - expectedResponse.add(new UploadFileResponse("ex2.html", "text/html", 23l)); - expectedResponse.add(new UploadFileResponse("ex3" + File.separator + "ex3.html", "text/html", 23l)); - - assertThat(result.getResponse().getContentAsString()) - .isEqualToIgnoringWhitespace(objectMapper.writeValueAsString(expectedResponse)); - - logger.info(result.getResponse().getContentAsString()); - verify(filesService, times(1)).saveExerciseFiles(anyLong(), any(MultipartFile.class), anyString()); - } @Test public void uploadFile_noBody() throws Exception { From d828674129678628aeed13d74de7e90c73ae28ec Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Mon, 25 Oct 2021 12:01:16 +0200 Subject: [PATCH 08/35] Possible fix to zip duplicating files --- .../ExerciseFilesServiceImpl.java | 66 +++++++++---------- .../ExerciseFilesControllerTests.java | 2 - 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java index 24c4d756..413e20fe 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java @@ -104,6 +104,9 @@ private Map> saveFiles(Long exerciseId, MultipartFile file, .orElseThrow(() -> new NotInCourseException("User not in course: " + requestUsername)); ExceptionUtil.throwExceptionIfNotInCourse(course, requestUsername, isTemplate); String lastFolderPath = isTemplate ? "template" : requestUsername; + // For example, for root path "v4t_courses", a course "Course 1" with id 34, an exercise "Exercise 1" with id 77 + // and a user "john.doe" the final directory path would be + // v4t_courses/course_1_34/exercise_1_77/john.doe Path targetDirectory = Paths.get(rootPath + File.separator + course.getName().toLowerCase().replace(" ", "_") + "_" + course.getId() + File.separator + exercise.getName().toLowerCase().replace(" ", "_") + "_" + exercise.getId() + File.separator + lastFolderPath).toAbsolutePath().normalize(); @@ -114,47 +117,38 @@ private Map> saveFiles(Long exerciseId, MultipartFile file, ZipInputStream zis = new ZipInputStream(file.getInputStream()); ZipEntry zipEntry = zis.getNextEntry(); List files = new ArrayList<>(); - - Set pathSet = new HashSet<>(); while (zipEntry != null) { File destFile = newFile(targetDirectory.toFile(), zipEntry); - String parsed = ""; - if (File.separatorChar == '/') { - parsed = destFile.getAbsolutePath().replace("\\", "/"); - - } else if (File.separatorChar == '\\') { - parsed = destFile.getAbsolutePath().replace("/", "\\"); - } - if (!pathSet.contains(parsed)) { - pathSet.add(parsed); - if (zipEntry.isDirectory()) { - Files.createDirectories(destFile.toPath()); - } else { - if (!destFile.getParentFile().exists()) { - Files.createDirectories(destFile.getParentFile().toPath()); - } - files.add(destFile); - try (FileOutputStream fos = new FileOutputStream(destFile)) { - int len; - while ((len = zis.read(buffer)) > 0) { - fos.write(buffer, 0, len); - } - } - Optional previousFileOpt = fileRepository.findByPath(destFile.getCanonicalPath()); - if (!previousFileOpt.isPresent()) { - ExerciseFile exFile = new ExerciseFile(destFile.getCanonicalPath()); - if (isTemplate) { - ExerciseFile savedFile = fileRepository.save(exFile); - exercise.addFileToTemplate(savedFile); - } else { - exFile.setOwner(user); - ExerciseFile savedFile = fileRepository.save(exFile); - exercise.addUserFile(savedFile); - } + if (zipEntry.isDirectory()) { + if (!destFile.isDirectory() && !destFile.mkdirs()) { + throw new IOException("Failed to create directory " + destFile); + } + } else { + // fix for Windows-created archives + File parent = destFile.getParentFile(); + if (!parent.isDirectory() && !parent.mkdirs()) { + throw new IOException("Failed to create directory " + parent); + } + files.add(destFile); + FileOutputStream fos = new FileOutputStream(destFile); + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + fos.close(); + Optional previousFileOpt = fileRepository.findByPath(destFile.getCanonicalPath()); + if (!previousFileOpt.isPresent()) { + ExerciseFile exFile = new ExerciseFile(destFile.getCanonicalPath()); + if (isTemplate) { + ExerciseFile savedFile = fileRepository.save(exFile); + exercise.addFileToTemplate(savedFile); + } else { + exFile.setOwner(user); + ExerciseFile savedFile = fileRepository.save(exFile); + exercise.addUserFile(savedFile); } } } - zipEntry = zis.getNextEntry(); } zis.closeEntry(); diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java index cd9322c8..5dc492e1 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseFilesControllerTests.java @@ -35,7 +35,6 @@ import com.vscode4teaching.vscode4teachingserver.model.ExerciseFile; import com.vscode4teaching.vscode4teachingserver.model.views.FileViews; import com.vscode4teaching.vscode4teachingserver.services.ExerciseFilesService; -import com.vscode4teaching.vscode4teachingserver.services.exceptions.ExerciseFinishedException; import org.apache.tomcat.util.http.fileupload.FileUtils; import org.junit.jupiter.api.AfterEach; @@ -54,7 +53,6 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.util.NestedServletException; @SpringBootTest @TestPropertySource(locations = "classpath:test.properties") From b3862330a259cf3a295e11dc18acd5c8f116ae9b Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:29:39 +0200 Subject: [PATCH 09/35] Trim sharing code when importing course --- .../src/components/courses/CoursesTreeProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts b/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts index 190a9d05..eb599c05 100644 --- a/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts +++ b/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts @@ -389,7 +389,7 @@ export class CoursesProvider implements vscode.TreeDataProvider { const code = await this.getInput("Introduce sharing code", Validators.validateSharingCode); if (code) { try { - const response = await APIClient.getCourseWithCode(code); + const response = await APIClient.getCourseWithCode(code.trim()); console.debug(response); const course: Course = response.data; CurrentUser.addNewCourse(course); From 053ceaaf13f2ff6d5fe51c8616c355e50dcab5a2 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:32:15 +0200 Subject: [PATCH 10/35] Fix events not being disposed when finishing exercise --- vscode4teaching-extension/src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index e96211f5..11b0d42c 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -226,7 +226,7 @@ export function activate(context: vscode.ExtensionContext) { try { const response = await APIClient.updateExerciseUserInfo(finishItem.getExerciseId(), 1); console.debug(response); - if (response.data.status === 1 && finishItem) { + if ((response.data.status === 1) && finishItem) { finishItem.dispose(); if (changeEvent) { changeEvent.dispose(); From 5cc90ddec271bb3a8375f0ebb65fe0c8fd1d8f0c Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:54:14 +0200 Subject: [PATCH 11/35] Change order of template and file in diff --- vscode4teaching-extension/src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index 11b0d42c..c811dd94 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -150,7 +150,7 @@ export function activate(context: vscode.ExtensionContext) { const templateFile = path.resolve(templates[parentDir], relativePath); if (fs.existsSync(templateFile)) { const templateFileUri = vscode.Uri.file(templateFile); - vscode.commands.executeCommand("vscode.diff", file, templateFileUri); + vscode.commands.executeCommand("vscode.diff", templateFileUri, file); } else { vscode.window.showErrorMessage("File doesn't exist in the template."); } From fa1281fd89251b8b111c1c28752c374ab5b64fe9 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Thu, 28 Oct 2021 10:41:21 +0200 Subject: [PATCH 12/35] Open explorer when content is downloaded --- vscode4teaching-extension/src/extension.ts | 36 ++++++++++++---------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index c811dd94..45593561 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -361,11 +361,13 @@ export async function initializeExtension(cwds: ReadonlyArray { const message = ` - The exercise has been downloaded! You can start editing its files in the Explorer view (Ctrl + Shift + E). + The exercise has been downloaded! You can start editing its files in the Explorer view. You can mark the exercise as finished using the 'Finish' button in the status bar below. `; - vscode.window.showInformationMessage(message).then(() => console.debug("Message dismissed")); + return vscode.window.showInformationMessage(message); + }).then(() => console.debug("Message dismissed")); } } catch (error) { APIClient.handleAxiosError(error); @@ -378,20 +380,22 @@ export async function initializeExtension(cwds: ReadonlyArray { - console.debug(value); - if (value === openDashboard) { - console.debug("Opening dashboard"); - return vscode.commands.executeCommand("vscode4teaching.showdashboard"); - } - }).then(() => console.debug("Message dismissed")); - } + vscode.commands.executeCommand("workbench.view.explorer").then(() => { + if (!restartDashboard) { + const message = ` + The exercise has been downloaded! You can see the template files and your students' files in the Explorer view. + You can also open the Dashboard to monitor their progress (you can also open it from the status bar's 'Dashboard' button. + `; + const openDashboard = "Open dashboard"; + vscode.window.showInformationMessage(message, openDashboard).then((value: string | undefined) => { + console.debug(value); + if (value === openDashboard) { + console.debug("Opening dashboard"); + return vscode.commands.executeCommand("vscode4teaching.showdashboard"); + } + }).then(() => console.debug("Message dismissed")); + } + }); } } // Set template location if exists From ce1a933c7b208a929a1a11a37c0b4b85ffb35f7d Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Thu, 28 Oct 2021 10:44:03 +0200 Subject: [PATCH 13/35] Prepare for next version --- .travis.yml | 4 ++-- vscode4teaching-extension/package.json | 2 +- vscode4teaching-server/pom.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 478ebd45..d576bb7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,9 +15,9 @@ jobs: - "./mvnw clean package -B -q" after_script: - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - - docker build -t vscode4teaching/vscode4teaching:2.0.1 . + - docker build -t vscode4teaching/vscode4teaching:2.0.2 . - docker build -t vscode4teaching/vscode4teaching:latest . - - docker push vscode4teaching/vscode4teaching:2.0.1 + - docker push vscode4teaching/vscode4teaching:2.0.2 - docker push vscode4teaching/vscode4teaching:latest - language: node_js os: diff --git a/vscode4teaching-extension/package.json b/vscode4teaching-extension/package.json index 1da085b5..61bfbb8e 100644 --- a/vscode4teaching-extension/package.json +++ b/vscode4teaching-extension/package.json @@ -12,7 +12,7 @@ }, "displayName": "VS Code 4 Teaching", "description": "Bring the programming exercises directly to the student’s editor.", - "version": "2.0.1", + "version": "2.0.2", "engines": { "vscode": "^1.45.1" }, diff --git a/vscode4teaching-server/pom.xml b/vscode4teaching-server/pom.xml index 1083ad29..d16dd617 100644 --- a/vscode4teaching-server/pom.xml +++ b/vscode4teaching-server/pom.xml @@ -12,7 +12,7 @@ com.vscode4teaching vscode4teaching-server - 2.0.1 + 2.0.2 VSCode 4 Teaching Server side of VSCode 4 Teaching extension. From f824d15d7aea4ae0471697b6db72af9f0a3b8639 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Thu, 28 Oct 2021 10:50:19 +0200 Subject: [PATCH 14/35] Fix some bugs and race conditions --- vscode4teaching-extension/src/extension.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index 45593561..52e7c19a 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -222,7 +222,7 @@ export function activate(context: vscode.ExtensionContext) { const finishExercise = vscode.commands.registerCommand("vscode4teaching.finishexercise", async () => { const warnMessage = "Finish exercise? Exercise will be marked as finished and you will not be able to upload any more updates"; const selectedOption = await vscode.window.showWarningMessage(warnMessage, { modal: true }, "Accept"); - if (selectedOption === "Accept" && finishItem) { + if ((selectedOption === "Accept") && finishItem) { try { const response = await APIClient.updateExerciseUserInfo(finishItem.getExerciseId(), 1); console.debug(response); @@ -342,7 +342,7 @@ export async function initializeExtension(cwds: ReadonlyArray Date: Thu, 28 Oct 2021 13:03:44 +0200 Subject: [PATCH 15/35] Control duplicate exceptions when saving files --- .../servicesimpl/ExerciseFilesServiceImpl.java | 11 ++++++++--- .../servicetests/ExerciseFilesServiceImplTests.java | 2 -- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java index 413e20fe..5acc7b98 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java @@ -29,6 +29,9 @@ import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotFoundException; import com.vscode4teaching.vscode4teachingserver.services.exceptions.NotInCourseException; +import org.hibernate.exception.ConstraintViolationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -40,6 +43,7 @@ public class ExerciseFilesServiceImpl implements ExerciseFilesService { private final ExerciseFileRepository fileRepository; private final ExerciseUserInfoRepository exerciseUserInfoRepository; private final UserRepository userRepository; + private final Logger logger = LoggerFactory.getLogger(ExerciseFilesServiceImpl.class); @Value("${v4t.filedirectory}") private String rootPath; @@ -136,9 +140,8 @@ private Map> saveFiles(Long exerciseId, MultipartFile file, fos.write(buffer, 0, len); } fos.close(); - Optional previousFileOpt = fileRepository.findByPath(destFile.getCanonicalPath()); - if (!previousFileOpt.isPresent()) { - ExerciseFile exFile = new ExerciseFile(destFile.getCanonicalPath()); + ExerciseFile exFile = new ExerciseFile(destFile.getCanonicalPath()); + try { if (isTemplate) { ExerciseFile savedFile = fileRepository.save(exFile); exercise.addFileToTemplate(savedFile); @@ -147,6 +150,8 @@ private Map> saveFiles(Long exerciseId, MultipartFile file, ExerciseFile savedFile = fileRepository.save(exFile); exercise.addUserFile(savedFile); } + } catch (ConstraintViolationException ex) { + logger.error(ex.getMessage()); } } zipEntry = zis.getNextEntry(); diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java index 2dec7f9c..914a01be 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java @@ -199,7 +199,6 @@ public void saveExerciseFiles() throws Exception { when(exerciseRepository.save(any(Exercise.class))).then(returnsFirstArg()); when(exerciseUserInfoRepository.findByExercise_IdAndUser_Username(anyLong(), anyString())) .thenReturn(Optional.of(eui)); - when(fileRepository.findByPath(any(String.class))).thenReturn(Optional.empty()); // Get files File file = Paths.get("src/test/java/com/vscode4teaching/vscode4teachingserver/files", "exs.zip").toFile(); MultipartFile mockFile = new MockMultipartFile("file", file.getName(), "application/zip", @@ -212,7 +211,6 @@ public void saveExerciseFiles() throws Exception { verify(userRepository, times(1)).findByUsername(anyString()); verify(fileRepository, times(3)).save(any(ExerciseFile.class)); verify(exerciseRepository, times(1)).save(any(Exercise.class)); - verify(fileRepository, times(3)).findByPath(any(String.class)); verify(exerciseUserInfoRepository, times(1)).findByExercise_IdAndUser_Username(anyLong(), anyString()); assertThat(Files.exists(Paths.get("null/"))).isTrue(); assertThat(Files.exists(Paths.get("null/spring_boot_course_4/exercise_1_1/"))).isTrue(); From 16a8d3cab73bdff41f095e384bc03a63e20e5e7b Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Fri, 29 Oct 2021 10:15:15 +0200 Subject: [PATCH 16/35] Show warning if dashboard tries to open a file that doesnt exist --- .../components/dashboard/DashboardWebview.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts index 6a867de2..70041b44 100644 --- a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts +++ b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts @@ -110,9 +110,14 @@ export class DashboardWebview { if (workspaces) { const wsF = vscode.workspace.workspaceFolders?.find((e) => e.name === message.username); if (wsF) { - const doc1 = await vscode.workspace.openTextDocument(await this.findLastModifiedFile(wsF, message.lastMod)); - // let doc1 = await vscode.workspace.openTextDocument(await this.findMainFile(wsF)); - await vscode.window.showTextDocument(doc1); + const lastFile = await this.findLastModifiedFile(wsF, message.lastMod); + if (!lastFile) { + vscode.window.showWarningMessage("Last modified file no longer exists (it might have been deleted by the student)"); + } else { + const doc1 = await vscode.workspace.openTextDocument(lastFile); + // let doc1 = await vscode.workspace.openTextDocument(await this.findMainFile(wsF)); + await vscode.window.showTextDocument(doc1); + } } } }); @@ -125,7 +130,12 @@ export class DashboardWebview { if (workspaces) { const wsF = vscode.workspace.workspaceFolders?.find((e) => e.name === message.username); if (wsF) { - await vscode.commands.executeCommand("vscode4teaching.diff", await this.findLastModifiedFile(wsF, message.lastMod)); + const lastFile = await this.findLastModifiedFile(wsF, message.lastMod); + if (!lastFile) { + vscode.window.showWarningMessage("Last modified file no longer exists (it might have been deleted by the student)"); + } else { + await vscode.commands.executeCommand("vscode4teaching.diff", lastFile); + } } } }); From 290ef3a8c744e4f21e555ed25c5d03b2262b5146 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Fri, 29 Oct 2021 10:39:06 +0200 Subject: [PATCH 17/35] Disable buttons while downloading files --- .../src/components/courses/CoursesTreeProvider.ts | 6 ++++++ vscode4teaching-extension/src/extension.ts | 14 ++++++++++++-- .../test/unitSuite/Commands.test.ts | 2 +- .../test/unitSuite/__mocks__/vscode.js | 4 ++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts b/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts index eb599c05..30274a71 100644 --- a/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts +++ b/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts @@ -399,6 +399,12 @@ export class CoursesProvider implements vscode.TreeDataProvider { } } } + + public changeLoading(loading: boolean) { + this.loading = loading; + CoursesProvider.triggerTreeReload(); + } + /** * Create exercise buttons from exercises. * @param element course diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index 52e7c19a..cd961ffe 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -95,11 +95,21 @@ export function activate(context: vscode.ExtensionContext) { }); const getFilesDisposable = vscode.commands.registerCommand("vscode4teaching.getexercisefiles", async (courseName: string, exercise: Exercise) => { - await getSingleStudentExerciseFiles(courseName, exercise); + coursesProvider.changeLoading(true); + try { + await getSingleStudentExerciseFiles(courseName, exercise); + } finally { + coursesProvider.changeLoading(false); + } }); const getStudentFiles = vscode.commands.registerCommand("vscode4teaching.getstudentfiles", async (courseName: string, exercise: Exercise) => { - await getMultipleStudentExerciseFiles(courseName, exercise); + coursesProvider.changeLoading(true); + try { + await getMultipleStudentExerciseFiles(courseName, exercise); + } finally { + coursesProvider.changeLoading(false); + } }); const addCourseDisposable = vscode.commands.registerCommand("vscode4teaching.addcourse", () => { diff --git a/vscode4teaching-extension/test/unitSuite/Commands.test.ts b/vscode4teaching-extension/test/unitSuite/Commands.test.ts index 343bc727..30e242d3 100644 --- a/vscode4teaching-extension/test/unitSuite/Commands.test.ts +++ b/vscode4teaching-extension/test/unitSuite/Commands.test.ts @@ -433,6 +433,6 @@ describe("Command implementations", () => { expect(mockedPath.resolve).toHaveBeenNthCalledWith(2, "template", "file.txt"); expect(mockedFs.existsSync).toHaveBeenCalledTimes(1); expect(mockedVscode.commands.executeCommand).toHaveBeenCalledTimes(1); - expect(mockedVscode.commands.executeCommand).toHaveBeenNthCalledWith(1, "vscode.diff", file, mockedVscode.Uri.file("template/file.txt")); + expect(mockedVscode.commands.executeCommand).toHaveBeenNthCalledWith(1, "vscode.diff", mockedVscode.Uri.file("template/file.txt"), file); }); }); diff --git a/vscode4teaching-extension/test/unitSuite/__mocks__/vscode.js b/vscode4teaching-extension/test/unitSuite/__mocks__/vscode.js index 9cff34e8..1aa01c20 100644 --- a/vscode4teaching-extension/test/unitSuite/__mocks__/vscode.js +++ b/vscode4teaching-extension/test/unitSuite/__mocks__/vscode.js @@ -142,8 +142,8 @@ const Range = jest.fn().mockImplementation((startLine, startCharacter, endLine, }); const commands = { - registerCommand: jest.fn(), - executeCommand: jest.fn() + registerCommand: jest.fn(() => Promise.resolve({ data: {} })), + executeCommand: jest.fn(() => Promise.resolve({ data: {} })) }; const TreeItemCollapsibleState = { From 1ba1a7d97be1a9874cb2c7f62bf16ed1738e56d9 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Fri, 29 Oct 2021 13:41:57 +0200 Subject: [PATCH 18/35] Add progress notification for longer requests and add spinner in status bar message for request that require less time --- .../src/client/APIClient.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/vscode4teaching-extension/src/client/APIClient.ts b/vscode4teaching-extension/src/client/APIClient.ts index b6ae8eca..e4863faa 100644 --- a/vscode4teaching-extension/src/client/APIClient.ts +++ b/vscode4teaching-extension/src/client/APIClient.ts @@ -160,7 +160,7 @@ class APIClientSingleton { method: "GET", responseType: "arraybuffer", }; - return APIClient.createRequest(options, "Downloading exercise files..."); + return APIClient.createRequest(options, "Downloading exercise files...", true); } public addCourse(data: CourseEdit): AxiosPromise { @@ -230,7 +230,7 @@ class APIClientSingleton { responseType: "json", data: dataForm, }; - return APIClient.createRequest(options, "Uploading template..."); + return APIClient.createRequest(options, "Uploading template...", true); } public deleteExercise(id: number): AxiosPromise { @@ -298,7 +298,7 @@ class APIClientSingleton { responseType: "json", data: dataForm, }; - return APIClient.createRequest(options, "Uploading files..."); + return APIClient.createRequest(options, "Uploading files...", true); } public getAllStudentFiles(exerciseId: number): AxiosPromise { @@ -307,7 +307,7 @@ class APIClientSingleton { method: "GET", responseType: "arraybuffer", }; - return APIClient.createRequest(options, "Downloading student files..."); + return APIClient.createRequest(options, "Downloading student files...", true); } public getTemplate(exerciseId: number): AxiosPromise { @@ -316,7 +316,7 @@ class APIClientSingleton { method: "GET", responseType: "arraybuffer", }; - return APIClient.createRequest(options, "Downloading exercise template..."); + return APIClient.createRequest(options, "Downloading exercise template...", true); } public getFilesInfo(username: string, exerciseId: number): AxiosPromise { @@ -445,10 +445,18 @@ class APIClientSingleton { * @param options Options from to build axios request * @param statusMessage message to add to the vscode status bar */ - private createRequest(options: AxiosBuildOptions, statusMessage: string): AxiosPromise { + private createRequest(options: AxiosBuildOptions, statusMessage: string, notification: boolean = false): AxiosPromise { const axiosOptions = APIClientSession.buildOptions(options); const thenable = axios(axiosOptions.axiosOptions); - vscode.window.setStatusBarMessage(statusMessage, thenable); + if (notification) { + vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + cancellable: false, + title: statusMessage, + }, (progress, token) => thenable); + } else { + vscode.window.setStatusBarMessage("$(sync~spin)" + statusMessage, thenable); + } return thenable.then((result) => { if (axiosOptions.timeout) { clearTimeout(axiosOptions.timeout); From e362a2cac3ee87c9fa3f95d4a8d772c5f09863c2 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Fri, 29 Oct 2021 14:54:20 +0200 Subject: [PATCH 19/35] Improve error messages --- .../src/components/dashboard/DashboardWebview.ts | 12 ++++++++++-- vscode4teaching-extension/src/utils/FileZipUtil.ts | 6 ++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts index 70041b44..63eeb70e 100644 --- a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts +++ b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts @@ -108,7 +108,7 @@ export class DashboardWebview { await vscode.commands.executeCommand("vscode4teaching.getstudentfiles", course.name, exercise).then(async () => { const workspaces = vscode.workspace.workspaceFolders; if (workspaces) { - const wsF = vscode.workspace.workspaceFolders?.find((e) => e.name === message.username); + const wsF = workspaces.find((e) => e.name === message.username); if (wsF) { const lastFile = await this.findLastModifiedFile(wsF, message.lastMod); if (!lastFile) { @@ -118,7 +118,11 @@ export class DashboardWebview { // let doc1 = await vscode.workspace.openTextDocument(await this.findMainFile(wsF)); await vscode.window.showTextDocument(doc1); } + } else { + vscode.window.showErrorMessage("Student's files not found."); } + } else { + vscode.window.showErrorMessage("The exercise's workspace is not open."); } }); break; @@ -128,7 +132,7 @@ export class DashboardWebview { await vscode.commands.executeCommand("vscode4teaching.getstudentfiles", course.name, exercise).then(async () => { const workspaces = vscode.workspace.workspaceFolders; if (workspaces) { - const wsF = vscode.workspace.workspaceFolders?.find((e) => e.name === message.username); + const wsF = workspaces.find((e) => e.name === message.username); if (wsF) { const lastFile = await this.findLastModifiedFile(wsF, message.lastMod); if (!lastFile) { @@ -136,7 +140,11 @@ export class DashboardWebview { } else { await vscode.commands.executeCommand("vscode4teaching.diff", lastFile); } + } else { + vscode.window.showErrorMessage("Student's files not found."); } + } else { + vscode.window.showErrorMessage("The exercise's workspace is not open."); } }); break; diff --git a/vscode4teaching-extension/src/utils/FileZipUtil.ts b/vscode4teaching-extension/src/utils/FileZipUtil.ts index dbc2f152..7dd63c70 100644 --- a/vscode4teaching-extension/src/utils/FileZipUtil.ts +++ b/vscode4teaching-extension/src/utils/FileZipUtil.ts @@ -129,7 +129,8 @@ export class FileZipUtil { const v4tpath = v4tpathArray[i]; fs.writeFileSync(v4tpath, fileData); } catch (error) { - vscode.window.showErrorMessage(error); + console.error(error); + vscode.window.showErrorMessage(error as string); } } // The purpose of this file is to indicate this is an exercise directory to V4T to enable file uploads, etc @@ -142,7 +143,8 @@ export class FileZipUtil { fs.writeFileSync(path.resolve(zipInfo.dir, "v4texercise.v4t"), JSON.stringify(fileContent), { encoding: "utf8" }); return zipInfo.dir; } catch (error) { - vscode.window.showErrorMessage(error); + console.error(error); + vscode.window.showErrorMessage(error as string); } } From db6aaeea8a8f5e9a19888179d1b1f6b2f489c8a5 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Mon, 8 Nov 2021 11:49:57 +0100 Subject: [PATCH 20/35] Check if file is in db before saving --- .../ExerciseFilesServiceImpl.java | 16 ++- .../ExerciseFilesServiceImplTests.java | 127 +++++++++++++----- 2 files changed, 106 insertions(+), 37 deletions(-) diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java index 5acc7b98..320517a1 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseFilesServiceImpl.java @@ -142,13 +142,15 @@ private Map> saveFiles(Long exerciseId, MultipartFile file, fos.close(); ExerciseFile exFile = new ExerciseFile(destFile.getCanonicalPath()); try { - if (isTemplate) { - ExerciseFile savedFile = fileRepository.save(exFile); - exercise.addFileToTemplate(savedFile); - } else { - exFile.setOwner(user); - ExerciseFile savedFile = fileRepository.save(exFile); - exercise.addUserFile(savedFile); + if (!fileRepository.findByPath(destFile.getCanonicalPath()).isPresent()) { + if (isTemplate) { + ExerciseFile savedFile = fileRepository.save(exFile); + exercise.addFileToTemplate(savedFile); + } else { + exFile.setOwner(user); + ExerciseFile savedFile = fileRepository.save(exFile); + exercise.addUserFile(savedFile); + } } } catch (ConstraintViolationException ex) { logger.error(ex.getMessage()); diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java index 914a01be..1e315caa 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseFilesServiceImplTests.java @@ -15,9 +15,11 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import com.vscode4teaching.vscode4teachingserver.model.Course; import com.vscode4teaching.vscode4teachingserver.model.Exercise; @@ -37,6 +39,7 @@ import org.apache.tomcat.util.http.fileupload.FileUtils; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -69,6 +72,13 @@ public class ExerciseFilesServiceImplTests { private static final Logger logger = LoggerFactory.getLogger(ExerciseFilesServiceImplTests.class); + private Set pathsSaved = new HashSet<>(); + + @BeforeEach + public void startup() { + this.pathsSaved = new HashSet<>(); + } + @AfterEach public void cleanup() { try { @@ -177,41 +187,21 @@ public void getExerciseFiles_noTemplate() throws Exception { verify(exerciseRepository, times(1)).findById(anyLong()); } - @Test - public void saveExerciseFiles() throws Exception { - User student = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); - student.setId(3l); - Role studentRole = new Role("ROLE_STUDENT"); - studentRole.setId(2l); - student.addRole(studentRole); - Course course = new Course("Spring Boot Course"); - course.setId(4l); - course.addUserInCourse(student); - Exercise exercise = new Exercise(); - exercise.setName("Exercise 1"); - exercise.setId(1l); - course.addExercise(exercise); - exercise.setCourse(course); - ExerciseUserInfo eui = new ExerciseUserInfo(exercise, student); - when(exerciseRepository.findById(anyLong())).thenReturn(Optional.of(exercise)); - when(userRepository.findByUsername(anyString())).thenReturn(Optional.of(student)); - when(fileRepository.save(any(ExerciseFile.class))).then(returnsFirstArg()); - when(exerciseRepository.save(any(Exercise.class))).then(returnsFirstArg()); - when(exerciseUserInfoRepository.findByExercise_IdAndUser_Username(anyLong(), anyString())) - .thenReturn(Optional.of(eui)); + private List runSaveExerciseFiles() throws Exception { // Get files File file = Paths.get("src/test/java/com/vscode4teaching/vscode4teachingserver/files", "exs.zip").toFile(); MultipartFile mockFile = new MockMultipartFile("file", file.getName(), "application/zip", new FileInputStream(file)); Map> filesMap = filesService.saveExerciseFiles(1l, mockFile, "johndoe"); - List savedFiles = filesMap.values().stream().findFirst().get(); + return filesMap.values().stream().findFirst().get(); + } - verify(exerciseRepository, times(1)).findById(anyLong()); - verify(userRepository, times(1)).findByUsername(anyString()); - verify(fileRepository, times(3)).save(any(ExerciseFile.class)); - verify(exerciseRepository, times(1)).save(any(Exercise.class)); - verify(exerciseUserInfoRepository, times(1)).findByExercise_IdAndUser_Username(anyLong(), anyString()); + private boolean checkPathHasBeenSaved(String path) { + return this.pathsSaved.add(path); + } + + private void fileAsserts() throws IOException { assertThat(Files.exists(Paths.get("null/"))).isTrue(); assertThat(Files.exists(Paths.get("null/spring_boot_course_4/exercise_1_1/"))).isTrue(); assertThat(Files.exists(Paths.get("null/spring_boot_course_4/exercise_1_1/johndoe"))).isTrue(); @@ -224,6 +214,11 @@ public void saveExerciseFiles() throws Exception { .contains("Exercise 2"); assertThat(Files.readAllLines(Paths.get("null/spring_boot_course_4/exercise_1_1/johndoe/ex3/ex3.html"))) .contains("Exercise 3"); + } + + private void exerciseUserInfoAsserts(ExerciseUserInfo eui) { + Exercise exercise = eui.getExercise(); + User student = eui.getUser(); assertThat(exercise.getUserFiles()).hasSize(3); assertThat(exercise.getUserFiles().get(0).getOwner()).isEqualTo(student); assertThat(exercise.getUserFiles().get(1).getOwner()).isEqualTo(student); @@ -234,12 +229,83 @@ public void saveExerciseFiles() throws Exception { Paths.get("null/spring_boot_course_4/exercise_1_1/johndoe/ex2.html").toAbsolutePath().toString()); assertThat(exercise.getUserFiles().get(2).getPath()).isEqualToIgnoringCase( Paths.get("null/spring_boot_course_4/exercise_1_1/johndoe/ex3/ex3.html").toAbsolutePath().toString()); + } + + private ExerciseUserInfo setupSaveExerciseFiles() { + User student = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); + student.setId(3l); + Role studentRole = new Role("ROLE_STUDENT"); + studentRole.setId(2l); + student.addRole(studentRole); + Course course = new Course("Spring Boot Course"); + course.setId(4l); + course.addUserInCourse(student); + Exercise exercise = new Exercise(); + exercise.setName("Exercise 1"); + exercise.setId(1l); + course.addExercise(exercise); + exercise.setCourse(course); + ExerciseUserInfo eui = new ExerciseUserInfo(exercise, student); + when(exerciseRepository.findById(anyLong())).thenReturn(Optional.of(exercise)); + when(userRepository.findByUsername(anyString())).thenReturn(Optional.of(student)); + when(fileRepository.findByPath(anyString())).thenAnswer(I -> { + String path = (String) I.getArguments()[0]; + if (this.checkPathHasBeenSaved(path)) { + return Optional.empty(); + } else { + return Optional.of(new ExerciseFile(path)); + } + }); + when(fileRepository.save(any(ExerciseFile.class))).then(returnsFirstArg()); + when(exerciseRepository.save(any(Exercise.class))).then(returnsFirstArg()); + when(exerciseUserInfoRepository.findByExercise_IdAndUser_Username(anyLong(), anyString())) + .thenReturn(Optional.of(eui)); + return eui; + } + + @Test + public void saveExerciseFiles() throws Exception { + ExerciseUserInfo eui = this.setupSaveExerciseFiles(); + Exercise exercise = eui.getExercise(); + + List savedFiles = this.runSaveExerciseFiles(); + + verify(exerciseRepository, times(1)).findById(anyLong()); + verify(userRepository, times(1)).findByUsername(anyString()); + verify(fileRepository, times(3)).findByPath(anyString()); + verify(fileRepository, times(3)).save(any(ExerciseFile.class)); + verify(exerciseRepository, times(1)).save(any(Exercise.class)); + verify(exerciseUserInfoRepository, times(1)).findByExercise_IdAndUser_Username(anyLong(), anyString()); + this.fileAsserts(); + this.exerciseUserInfoAsserts(eui); assertThat(savedFiles.size()).isEqualTo(3); assertThat(savedFiles.get(0).getAbsolutePath()).isEqualToIgnoringCase(exercise.getUserFiles().get(0).getPath()); assertThat(savedFiles.get(1).getAbsolutePath()).isEqualToIgnoringCase(exercise.getUserFiles().get(1).getPath()); assertThat(savedFiles.get(2).getAbsolutePath()).isEqualToIgnoringCase(exercise.getUserFiles().get(2).getPath()); } + @Test + public void saveExerciseFilesIgnoreDuplicates() throws Exception { + ExerciseUserInfo eui = this.setupSaveExerciseFiles(); + Exercise exercise = eui.getExercise(); + + // Run twice to send duplicates + this.runSaveExerciseFiles(); + List savedFiles = this.runSaveExerciseFiles(); + + verify(exerciseRepository, times(2)).findById(anyLong()); + verify(userRepository, times(2)).findByUsername(anyString()); + verify(fileRepository, times(6)).findByPath(anyString()); + verify(fileRepository, times(3)).save(any(ExerciseFile.class)); + verify(exerciseRepository, times(2)).save(any(Exercise.class)); + verify(exerciseUserInfoRepository, times(2)).findByExercise_IdAndUser_Username(anyLong(), anyString()); + this.fileAsserts(); + this.exerciseUserInfoAsserts(eui); + assertThat(savedFiles.get(0).getAbsolutePath()).isEqualToIgnoringCase(exercise.getUserFiles().get(0).getPath()); + assertThat(savedFiles.get(1).getAbsolutePath()).isEqualToIgnoringCase(exercise.getUserFiles().get(1).getPath()); + assertThat(savedFiles.get(2).getAbsolutePath()).isEqualToIgnoringCase(exercise.getUserFiles().get(2).getPath()); + } + @Test public void saveExerciseFilesFinishedError() throws Exception { User student = new User("johndoejr@gmail.com", "johndoe", "pass", "John", "Doe"); @@ -264,8 +330,9 @@ public void saveExerciseFilesFinishedError() throws Exception { MultipartFile mockFile = new MockMultipartFile("file", file.getName(), "application/zip", new FileInputStream(file)); - ExerciseFinishedException e = assertThrows(ExerciseFinishedException.class, () -> filesService.saveExerciseFiles(1l, mockFile, "johndoe")); - + ExerciseFinishedException e = assertThrows(ExerciseFinishedException.class, + () -> filesService.saveExerciseFiles(1l, mockFile, "johndoe")); + assertThat(e.getMessage()).isEqualToIgnoringWhitespace("Exercise is marked as finished: 1"); verify(exerciseUserInfoRepository, times(1)).findByExercise_IdAndUser_Username(anyLong(), anyString()); } From c4ceef0a5a3b9649affc1df0d3f5250e20f8c8d6 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Mon, 8 Nov 2021 12:23:05 +0100 Subject: [PATCH 21/35] Fix duplicate entry when saving superuser --- .../vscode4teachingserver/DatabaseSuperuserInitializer.java | 6 ++++-- .../model/repositories/UserRepository.java | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseSuperuserInitializer.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseSuperuserInitializer.java index 6c4f1a19..a74ac689 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseSuperuserInitializer.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/DatabaseSuperuserInitializer.java @@ -57,7 +57,9 @@ private User saveUser(User user) { @Override public void run(String... args) throws Exception { - User superuser = new User(email, username, passwordEncoder.encode(password), name, lastname); - saveUser(superuser); + if (!userRepository.findByEmail(email).isPresent()) { + User superuser = new User(email, username, passwordEncoder.encode(password), name, lastname); + saveUser(superuser); + } } } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/UserRepository.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/UserRepository.java index 70475024..170d3764 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/UserRepository.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/repositories/UserRepository.java @@ -8,4 +8,5 @@ public interface UserRepository extends JpaRepository { Optional findByUsername(String username); + Optional findByEmail(String email); } \ No newline at end of file From 9e419e79c16adc946b9836ace0a9a90abdce61cb Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Tue, 9 Nov 2021 10:16:38 +0100 Subject: [PATCH 22/35] Fix regression in dashboard welcome message showing when it shouldn't --- vscode4teaching-extension/src/extension.ts | 38 +++++++++++++--------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index cd961ffe..0c13ca85 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -314,7 +314,7 @@ export function disableFeatures() { global.clearInterval(commentInterval); } -export async function initializeExtension(cwds: ReadonlyArray, restartDashboard?: boolean) { +export async function initializeExtension(cwds: ReadonlyArray, hideWelcomeMessage?: boolean) { disableFeatures(); @@ -372,12 +372,14 @@ export async function initializeExtension(cwds: ReadonlyArray { - const message = ` - The exercise has been downloaded! You can start editing its files in the Explorer view. - You can mark the exercise as finished using the 'Finish' button in the status bar below. - `; - return vscode.window.showInformationMessage(message); - }).then(() => console.debug("Message dismissed")); + if (!hideWelcomeMessage) { + const message = ` + The exercise has been downloaded! You can start editing its files in the Explorer view. + You can mark the exercise as finished using the 'Finish' button in the status bar below. + `; + vscode.window.showInformationMessage(message).then(() => console.debug("Message dismissed")); + } + }); } } catch (error) { APIClient.handleAxiosError(error); @@ -391,7 +393,7 @@ export async function initializeExtension(cwds: ReadonlyArray { - if (!restartDashboard) { + if (!hideWelcomeMessage) { const message = ` The exercise has been downloaded! You can see the template files and your students' files in the Explorer view. You can also open the Dashboard to monitor their progress (you can also open it from the status bar's 'Dashboard' button. @@ -513,10 +515,12 @@ async function getSingleStudentExerciseFiles(courseName: string, exercise: Exerc const newWorkspace = vscode.workspace.updateWorkspaceFolders(0, vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, { uri, name: exercise.name }); - currentCwds = vscode.workspace.workspaceFolders; - if (currentCwds && !newWorkspace) { - await initializeExtension(currentCwds); - } + vscode.workspace.onDidChangeWorkspaceFolders(() => { + currentCwds = vscode.workspace.workspaceFolders; + if (currentCwds && !newWorkspace) { + initializeExtension(currentCwds, true); + } + }); } } } @@ -555,10 +559,12 @@ async function getMultipleStudentExerciseFiles(courseName: string, exercise: Exe const newWorkspaces = vscode.workspace.updateWorkspaceFolders(0, vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, ...subdirectoriesURIs); - currentCwds = vscode.workspace.workspaceFolders; - if (currentCwds && !newWorkspaces) { - await initializeExtension(currentCwds, true); - } + vscode.workspace.onDidChangeWorkspaceFolders(() => { + currentCwds = vscode.workspace.workspaceFolders; + if (currentCwds && !newWorkspaces) { + initializeExtension(currentCwds, true); + } + }); } } } From 6ad66dcbee86ec1f77d1925f774fa58231547f30 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Tue, 9 Nov 2021 11:00:59 +0100 Subject: [PATCH 23/35] Fix extensions reloading when opening the same workspace if a new student was added --- vscode4teaching-extension/src/extension.ts | 34 +++++++++++++++------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index 0c13ca85..eefa0815 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -512,15 +512,15 @@ async function getSingleStudentExerciseFiles(courseName: string, exercise: Exerc const username = CurrentUser.getUserInfo().username; const fileInfoPath = path.resolve(FileZipUtil.INTERNAL_FILES_DIR, username, ".fileInfo", exercise.name); await getFilesInfo(exercise, fileInfoPath, [username]); - const newWorkspace = vscode.workspace.updateWorkspaceFolders(0, - vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, - { uri, name: exercise.name }); vscode.workspace.onDidChangeWorkspaceFolders(() => { currentCwds = vscode.workspace.workspaceFolders; - if (currentCwds && !newWorkspace) { + if (currentCwds) { initializeExtension(currentCwds, true); } }); + vscode.workspace.updateWorkspaceFolders(0, + vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, + { uri, name: exercise.name }); } } } @@ -543,8 +543,22 @@ async function getMultipleStudentExerciseFiles(courseName: string, exercise: Exe const newWorkspaceURIs = await getStudentExerciseFiles(courseName, exercise); if (newWorkspaceURIs && newWorkspaceURIs[1]) { const wsURI: string = newWorkspaceURIs[1]; - const directories = fs.readdirSync(wsURI, { withFileTypes: true }) + let directories = fs.readdirSync(wsURI, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()); + /* + Move "template" directory to beginning of directory array + As in the documentation for vscode.workspace.onDidChangeWorkspaceFolders: + + If the first workspace folder is added, removed or changed, the currently executing extensions + (including the one that called this method) will be terminated and restarted so that the (deprecated) + rootPath property is updated to point to the first workspace folder. + + The folder that never changes is the "template" one, so we move it to the beginning of the array to avoid + reloading all extensions if the same workspace is opened and there are new students added. + */ + const template = directories.filter((dirent) => dirent.name === "template")[0]; + directories = directories.filter((dirent) => dirent.name !== "template"); + directories.unshift(template); // Get file info for id references if (coursesProvider && CurrentUser.isLoggedIn()) { const usernames = directories.filter((dirent) => !dirent.name.includes("template")).map((dirent) => dirent.name); @@ -555,16 +569,16 @@ async function getMultipleStudentExerciseFiles(courseName: string, exercise: Exe uri: vscode.Uri.file(path.resolve(wsURI, dirent.name)), }; }); - // open all student files and template - const newWorkspaces = vscode.workspace.updateWorkspaceFolders(0, - vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, - ...subdirectoriesURIs); vscode.workspace.onDidChangeWorkspaceFolders(() => { currentCwds = vscode.workspace.workspaceFolders; - if (currentCwds && !newWorkspaces) { + if (currentCwds) { initializeExtension(currentCwds, true); } }); + // open all student files and template + vscode.workspace.updateWorkspaceFolders(0, + vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, + ...subdirectoriesURIs); } } } From f99dc6cd495125446cadf581a81c047ee0db039c Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Tue, 9 Nov 2021 14:47:26 +0100 Subject: [PATCH 24/35] Minor fixes to tests --- .../src/client/APIClient.ts | 2 +- .../components/dashboard/DashboardWebview.ts | 4 +- .../test/unitSuite/Client.test.ts | 12 +-- .../test/unitSuite/ClientAPICalls.test.ts | 74 +++++++++++-------- .../test/unitSuite/__mocks__/vscode.js | 7 ++ 5 files changed, 59 insertions(+), 40 deletions(-) diff --git a/vscode4teaching-extension/src/client/APIClient.ts b/vscode4teaching-extension/src/client/APIClient.ts index e4863faa..0c0a6939 100644 --- a/vscode4teaching-extension/src/client/APIClient.ts +++ b/vscode4teaching-extension/src/client/APIClient.ts @@ -455,7 +455,7 @@ class APIClientSingleton { title: statusMessage, }, (progress, token) => thenable); } else { - vscode.window.setStatusBarMessage("$(sync~spin)" + statusMessage, thenable); + vscode.window.setStatusBarMessage("$(sync~spin) " + statusMessage, thenable); } return thenable.then((result) => { if (axiosOptions.timeout) { diff --git a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts index 63eeb70e..86498d34 100644 --- a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts +++ b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts @@ -71,7 +71,9 @@ export class DashboardWebview { // This happens when the user closes the panel or when the panel is closed programatically this.panel.onDidDispose(() => { global.clearInterval(this.lastUpdatedInterval); - // global.clearInterval(this._reloadInterval); + // if (this._reloadInterval !== undefined) { + // global.clearInterval(this._reloadInterval); + // } this.ws.close(); this.dispose(); }); diff --git a/vscode4teaching-extension/test/unitSuite/Client.test.ts b/vscode4teaching-extension/test/unitSuite/Client.test.ts index 7db5cb10..2845ea7b 100644 --- a/vscode4teaching-extension/test/unitSuite/Client.test.ts +++ b/vscode4teaching-extension/test/unitSuite/Client.test.ts @@ -136,9 +136,9 @@ describe("Client", () => { expect(mockedVscode.window.showErrorMessage).toHaveBeenCalledTimes(0); // Set status bar when fetching XSRF expect(mockedVscode.window.setStatusBarMessage).toHaveBeenCalledTimes(2); - expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(1, "Fetching server info...", expect.anything()); + expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(1, "$(sync~spin) Fetching server info...", expect.anything()); // Set status bar when calling login - expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(2, "Logging in to VS Code 4 Teaching...", expect.anything()); + expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(2, "$(sync~spin) Logging in to VS Code 4 Teaching...", expect.anything()); // Make a request for XSRF Token expect(mockedAxios).toHaveBeenCalledTimes(2); expect(mockedAxios).toHaveBeenNthCalledWith(1, expectedAxiosConfigXSRFRequest); @@ -217,7 +217,7 @@ describe("Client", () => { expect(mockedVscode.window.showWarningMessage).toHaveBeenCalledWith("Something went wrong, please try again."); // Fetch XSRF Token expect(mockedVscode.window.setStatusBarMessage).toHaveBeenCalledTimes(1); - expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(1, "Fetching server info...", expect.anything()); + expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(1, "$(sync~spin) Fetching server info...", expect.anything()); expect(mockedAxios).toHaveBeenCalledTimes(1); expect(mockedAxios).toHaveBeenNthCalledWith(1, expectedAxiosConfigXSRFRequest); expect(global.console.error).toHaveBeenCalledTimes(1); @@ -344,9 +344,9 @@ describe("Client", () => { expect(mockedVscode.window.showErrorMessage).toHaveBeenCalledTimes(0); // Set status bar when fetching XSRF expect(mockedVscode.window.setStatusBarMessage).toHaveBeenCalledTimes(2); - expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(1, "Fetching server info...", expect.anything()); + expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(1, "$(sync~spin) Fetching server info...", expect.anything()); // Set status bar when calling signup - expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(2, "Signing up to VS Code 4 Teaching...", expect.anything()); + expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(2, "$(sync~spin) Signing up to VS Code 4 Teaching...", expect.anything()); // Make a request for XSRF Token expect(mockedAxios).toHaveBeenCalledTimes(2); expect(mockedAxios).toHaveBeenNthCalledWith(1, expectedAxiosConfigXSRFRequest); @@ -410,7 +410,7 @@ describe("Client", () => { expect(mockedVscode.window.showWarningMessage).toHaveBeenCalledTimes(0); expect(mockedVscode.window.showErrorMessage).toHaveBeenCalledTimes(0); // Set status bar when calling signup - expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(1, "Signing teacher up to VS Code 4 Teaching...", expect.anything()); + expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(1, "$(sync~spin) Signing teacher up to VS Code 4 Teaching...", expect.anything()); // Make a request for signing up expect(mockedAxios).toHaveBeenCalledTimes(1); expect(mockedAxios).toHaveBeenNthCalledWith(1, expectedAxiosConfigSignupRequest); diff --git a/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts b/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts index a3683f23..3419388e 100644 --- a/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts +++ b/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts @@ -28,17 +28,26 @@ const baseUrl = "https://edukafora.codeurjc.es"; // This tests don't bother with the response of the calls, only the request parameters describe("client API calls", () => { - function expectCorrectRequest(options: AxiosRequestConfig, message: string, thenable: AxiosPromise) { + function expectCorrectRequest(options: AxiosRequestConfig, message: string, notification: boolean, thenable: AxiosPromise) { expect(mockedAxios).toHaveBeenCalledTimes(1); if (options.data instanceof FormData) { // if formdata content-type is generated randomly const config = (mockedAxios.mock.calls[0][0] as AxiosRequestConfig); options.headers = config.headers; options.data = config.data; + } + if (notification) { + expect(mockedVscode.window.withProgress).toHaveBeenCalledTimes(1); + expect(mockedVscode.window.withProgress).toHaveBeenNthCalledWith(1, { + location: mockedVscode.ProgressLocation.Notification, + cancellable: false, + title: message, + }, expect.any(Function)); + } else { + expect(mockedVscode.window.setStatusBarMessage).toHaveBeenCalledTimes(1); + expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(1, "$(sync~spin) " + message, thenable); } expect(mockedAxios).toHaveBeenNthCalledWith(1, options); - expect(mockedVscode.window.setStatusBarMessage).toHaveBeenCalledTimes(1); - expect(mockedVscode.window.setStatusBarMessage).toHaveBeenNthCalledWith(1, message, thenable); } const xsrfToken = "test"; @@ -52,6 +61,7 @@ describe("client API calls", () => { afterEach(() => { mockedAxios.mockClear(); mockedVscode.window.setStatusBarMessage.mockClear(); + mockedVscode.window.withProgress.mockClear(); APIClientSession.xsrfToken = undefined; APIClientSession.jwtToken = undefined; }); @@ -78,7 +88,7 @@ describe("client API calls", () => { const thenable = APIClient.getServerUserInfo(); - expectCorrectRequest(expectedOptions, "Fetching user data...", thenable); + expectCorrectRequest(expectedOptions, "Fetching user data...", false, thenable); }); it("should request get exercises correctly", () => { @@ -100,7 +110,7 @@ describe("client API calls", () => { const thenable = APIClient.getExercises(courseId); - expectCorrectRequest(expectedOptions, "Fetching exercises...", thenable); + expectCorrectRequest(expectedOptions, "Fetching exercises...", false, thenable); }); it("should request get exercise files correctly", () => { @@ -122,7 +132,7 @@ describe("client API calls", () => { const thenable = APIClient.getExerciseFiles(exerciseId); - expectCorrectRequest(expectedOptions, "Downloading exercise files...", thenable); + expectCorrectRequest(expectedOptions, "Downloading exercise files...", true, thenable); }); it("should request add course correctly", () => { @@ -147,7 +157,7 @@ describe("client API calls", () => { const thenable = APIClient.addCourse(course); - expectCorrectRequest(expectedOptions, "Creating course...", thenable); + expectCorrectRequest(expectedOptions, "Creating course...", false, thenable); }); it("should request edit course correctly", () => { @@ -173,7 +183,7 @@ describe("client API calls", () => { const thenable = APIClient.editCourse(oldCourseId, course); - expectCorrectRequest(expectedOptions, "Editing course...", thenable); + expectCorrectRequest(expectedOptions, "Editing course...", false, thenable); }); it("should request delete course correctly", () => { @@ -196,7 +206,7 @@ describe("client API calls", () => { const thenable = APIClient.deleteCourse(oldCourseId); - expectCorrectRequest(expectedOptions, "Deleting course...", thenable); + expectCorrectRequest(expectedOptions, "Deleting course...", false, thenable); }); it("should request add exercise correctly", () => { @@ -222,7 +232,7 @@ describe("client API calls", () => { const thenable = APIClient.addExercise(courseId, exercise); - expectCorrectRequest(expectedOptions, "Adding exercise...", thenable); + expectCorrectRequest(expectedOptions, "Adding exercise...", false, thenable); }); it("should request edit exercise correctly", () => { @@ -248,7 +258,7 @@ describe("client API calls", () => { const thenable = APIClient.editExercise(exerciseId, exercise); - expectCorrectRequest(expectedOptions, "Sending exercise info...", thenable); + expectCorrectRequest(expectedOptions, "Sending exercise info...", false, thenable); }); it("should request upload exercise template correctly", () => { @@ -273,7 +283,7 @@ describe("client API calls", () => { const thenable = APIClient.uploadExerciseTemplate(exerciseId, data); - expectCorrectRequest(expectedOptions, "Uploading template...", thenable); + expectCorrectRequest(expectedOptions, "Uploading template...", true, thenable); }); it("should request delete exercise template correctly", () => { @@ -295,7 +305,7 @@ describe("client API calls", () => { const thenable = APIClient.deleteExercise(exerciseId); - expectCorrectRequest(expectedOptions, "Deleting exercise...", thenable); + expectCorrectRequest(expectedOptions, "Deleting exercise...", false, thenable); }); it("should request get all users correctly", () => { @@ -316,7 +326,7 @@ describe("client API calls", () => { const thenable = APIClient.getAllUsers(); - expectCorrectRequest(expectedOptions, "Fetching user data...", thenable); + expectCorrectRequest(expectedOptions, "Fetching user data...", false, thenable); }); it("should request get users in course correctly", () => { @@ -338,7 +348,7 @@ describe("client API calls", () => { const thenable = APIClient.getUsersInCourse(courseId); - expectCorrectRequest(expectedOptions, "Fetching user data...", thenable); + expectCorrectRequest(expectedOptions, "Fetching user data...", false, thenable); }); it("should request add users to course correctly", () => { @@ -363,7 +373,7 @@ describe("client API calls", () => { const thenable = APIClient.addUsersToCourse(courseId, data); - expectCorrectRequest(expectedOptions, "Adding users to course...", thenable); + expectCorrectRequest(expectedOptions, "Adding users to course...", false, thenable); }); it("should request remove users from course correctly", () => { @@ -388,7 +398,7 @@ describe("client API calls", () => { const thenable = APIClient.removeUsersFromCourse(courseId, data); - expectCorrectRequest(expectedOptions, "Removing users from course...", thenable); + expectCorrectRequest(expectedOptions, "Removing users from course...", false, thenable); }); it("should request get creator correctly", () => { @@ -410,7 +420,7 @@ describe("client API calls", () => { const thenable = APIClient.getCreator(courseId); - expectCorrectRequest(expectedOptions, "Getting course info...", thenable); + expectCorrectRequest(expectedOptions, "Getting course info...", false, thenable); }); it("should request upload files correctly", () => { @@ -435,7 +445,7 @@ describe("client API calls", () => { const thenable = APIClient.uploadFiles(exerciseId, data); - expectCorrectRequest(expectedOptions, "Uploading files...", thenable); + expectCorrectRequest(expectedOptions, "Uploading files...", true, thenable); }); it("should request get all student files correctly", () => { @@ -457,7 +467,7 @@ describe("client API calls", () => { const thenable = APIClient.getAllStudentFiles(exerciseId); - expectCorrectRequest(expectedOptions, "Downloading student files...", thenable); + expectCorrectRequest(expectedOptions, "Downloading student files...", true, thenable); }); it("should request get template correctly", () => { @@ -479,7 +489,7 @@ describe("client API calls", () => { const thenable = APIClient.getTemplate(exerciseId); - expectCorrectRequest(expectedOptions, "Downloading exercise template...", thenable); + expectCorrectRequest(expectedOptions, "Downloading exercise template...", true, thenable); }); it("should request get files info correctly", () => { @@ -502,7 +512,7 @@ describe("client API calls", () => { const thenable = APIClient.getFilesInfo(username, exerciseId); - expectCorrectRequest(expectedOptions, "Fetching file information...", thenable); + expectCorrectRequest(expectedOptions, "Fetching file information...", false, thenable); }); it("should request save comment correctly", () => { @@ -528,7 +538,7 @@ describe("client API calls", () => { const thenable = APIClient.saveComment(fileId, serverThread); - expectCorrectRequest(expectedOptions, "Saving comments...", thenable); + expectCorrectRequest(expectedOptions, "Saving comments...", false, thenable); }); it("should request get comment correctly", () => { @@ -550,7 +560,7 @@ describe("client API calls", () => { const thenable = APIClient.getComments(fileId); - expectCorrectRequest(expectedOptions, "Fetching comments...", thenable); + expectCorrectRequest(expectedOptions, "Fetching comments...", false, thenable); }); it("should request get all comment correctly", () => { @@ -573,7 +583,7 @@ describe("client API calls", () => { const thenable = APIClient.getAllComments(username, exerciseId); - expectCorrectRequest(expectedOptions, "Fetching comments...", thenable); + expectCorrectRequest(expectedOptions, "Fetching comments...", false, thenable); }); it("should request get sharing code for course correctly", () => { @@ -599,7 +609,7 @@ describe("client API calls", () => { const thenable = APIClient.getSharingCode(course); - expectCorrectRequest(expectedOptions, "Fetching sharing code...", thenable); + expectCorrectRequest(expectedOptions, "Fetching sharing code...", false, thenable); }); it("should request get sharing code for exercise correctly", () => { @@ -624,7 +634,7 @@ describe("client API calls", () => { const thenable = APIClient.getSharingCode(exercise); - expectCorrectRequest(expectedOptions, "Fetching sharing code...", thenable); + expectCorrectRequest(expectedOptions, "Fetching sharing code...", false, thenable); }); it("should request update comment thread line correctly", () => { @@ -650,7 +660,7 @@ describe("client API calls", () => { const thenable = APIClient.updateCommentThreadLine(fileId, lineInfo.line, lineInfo.lineText); - expectCorrectRequest(expectedOptions, "Saving comments...", thenable); + expectCorrectRequest(expectedOptions, "Saving comments...", false, thenable); }); it("should request get sharing code for exercise correctly", () => { @@ -672,7 +682,7 @@ describe("client API calls", () => { const thenable = APIClient.getCourseWithCode(code); - expectCorrectRequest(expectedOptions, "Fetching course data...", thenable); + expectCorrectRequest(expectedOptions, "Fetching course data...", false, thenable); }); it("should request get exercise user info for exercise correctly", () => { @@ -694,7 +704,7 @@ describe("client API calls", () => { const thenable = APIClient.getExerciseUserInfo(exerciseId); - expectCorrectRequest(expectedOptions, "Fetching exercise info for current user...", thenable); + expectCorrectRequest(expectedOptions, "Fetching exercise info for current user...", false, thenable); }); it("should request update exercise user info for exercise correctly", () => { @@ -719,7 +729,7 @@ describe("client API calls", () => { const thenable = APIClient.updateExerciseUserInfo(exerciseId, status); - expectCorrectRequest(expectedOptions, "Updating exercise user info...", thenable); + expectCorrectRequest(expectedOptions, "Updating exercise user info...", false, thenable); }); it("should request update exercise user info for exercise correctly", () => { @@ -741,6 +751,6 @@ describe("client API calls", () => { const thenable = APIClient.getAllStudentsExerciseUserInfo(exerciseId); - expectCorrectRequest(expectedOptions, "Fetching students' exercise user info...", thenable); + expectCorrectRequest(expectedOptions, "Fetching students' exercise user info...", false, thenable); }); }); diff --git a/vscode4teaching-extension/test/unitSuite/__mocks__/vscode.js b/vscode4teaching-extension/test/unitSuite/__mocks__/vscode.js index 1aa01c20..8f8aba7b 100644 --- a/vscode4teaching-extension/test/unitSuite/__mocks__/vscode.js +++ b/vscode4teaching-extension/test/unitSuite/__mocks__/vscode.js @@ -56,6 +56,7 @@ const window = { showQuickPick: jest.fn((array, options) => { return Promise.resolve([...array]); }), + withProgress: jest.fn(), }; const WorkspaceConfiguration = { @@ -86,6 +87,7 @@ const workspace = { onWillSaveTextDocument: jest.fn(), updateWorkspaceFolders: jest.fn(), getWorkspaceFolder: jest.fn(), + onDidChangeWorkspaceFolders: jest.fn(), }; const Uri = jest.fn().mockImplementation((x) => { @@ -210,6 +212,10 @@ const MarkdownString = jest.fn().mockImplementation((text) => { const RelativePattern = jest.fn(); +const ProgressLocation = { + Notification: 0, +} + const vscode = { WorkspaceFolder, ExtensionContext, @@ -237,6 +243,7 @@ const vscode = { EndOfLine, MarkdownString, RelativePattern, + ProgressLocation }; module.exports = vscode; \ No newline at end of file From 5790a46b031d0b9d0c9d8e416416c91598802467 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Wed, 10 Nov 2021 12:37:50 +0100 Subject: [PATCH 25/35] Wait 500 ms after a change in a file to upload Previously, each change to a file triggered the zipping and uploading. If multiple files were created/modified in quickly, it would send a request for each one (i.e.: compiling with mvn package). Now, it waits for more files before triggering the upload. --- vscode4teaching-extension/src/extension.ts | 41 ++++++++++++++++------ 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index eefa0815..76810331 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -42,6 +42,7 @@ export let changeEvent: vscode.Disposable; export let createEvent: vscode.Disposable; export let deleteEvent: vscode.Disposable; export let commentInterval: NodeJS.Timeout; +export let uploadTimeout: NodeJS.Timeout | undefined; export let wsLiveshare: WebSocketV4TConnection | undefined; export let liveshareService: LiveShareService | undefined; @@ -437,21 +438,39 @@ function setStudentEvents(jszipFile: JSZip, cwd: vscode.WorkspaceFolder, zipUri: const pattern = new vscode.RelativePattern(cwd, "**/*"); const fsw = vscode.workspace.createFileSystemWatcher(pattern); changeEvent = fsw.onDidChange((e: vscode.Uri) => { - FileZipUtil.updateFile(jszipFile, e.fsPath, cwd.uri.fsPath, ignoredFiles, exerciseId).then(() => { - console.debug("File edited: " + e.fsPath); - }); - EUIUpdateService.updateExercise(e, exerciseId); + if (uploadTimeout) { + global.clearTimeout(uploadTimeout); + } + uploadTimeout = global.setTimeout(() => { + uploadTimeout = undefined; + FileZipUtil.updateFile(jszipFile, e.fsPath, cwd.uri.fsPath, ignoredFiles, exerciseId).then(() => { + console.debug("File edited: " + e.fsPath); + }); + EUIUpdateService.updateExercise(e, exerciseId); + }, 500); }); createEvent = fsw.onDidCreate((e: vscode.Uri) => { - FileZipUtil.updateFile(jszipFile, e.fsPath, cwd.uri.fsPath, ignoredFiles, exerciseId).then(() => { - console.debug("File added: " + e.fsPath); - }); - EUIUpdateService.updateExercise(e, exerciseId); + if (uploadTimeout) { + global.clearTimeout(uploadTimeout); + } + uploadTimeout = global.setTimeout(() => { + uploadTimeout = undefined; + FileZipUtil.updateFile(jszipFile, e.fsPath, cwd.uri.fsPath, ignoredFiles, exerciseId).then(() => { + console.debug("File added: " + e.fsPath); + }); + EUIUpdateService.updateExercise(e, exerciseId); + }, 500); }); deleteEvent = fsw.onDidDelete((e: vscode.Uri) => { - FileZipUtil.deleteFile(jszipFile, e.fsPath, cwd.uri.fsPath, ignoredFiles, exerciseId).then(() => { - console.debug("File deleted: " + e.fsPath); - }); + if (uploadTimeout) { + global.clearTimeout(uploadTimeout); + } + uploadTimeout = global.setTimeout(() => { + uploadTimeout = undefined; + FileZipUtil.deleteFile(jszipFile, e.fsPath, cwd.uri.fsPath, ignoredFiles, exerciseId).then(() => { + console.debug("File deleted: " + e.fsPath); + }); + }, 500); }); vscode.workspace.onWillSaveTextDocument((e: vscode.TextDocumentWillSaveEvent) => { From 82cd8498f396fa51d9bd7bb36b1252e10c55d524 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Wed, 10 Nov 2021 13:49:06 +0100 Subject: [PATCH 26/35] Disable open and diff buttons when downloading --- .../resources/dashboard/dashboard.js | 19 +++++++++++++++++-- .../components/dashboard/DashboardWebview.ts | 6 ++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/vscode4teaching-extension/resources/dashboard/dashboard.js b/vscode4teaching-extension/resources/dashboard/dashboard.js index f551c65b..9e8cb49d 100644 --- a/vscode4teaching-extension/resources/dashboard/dashboard.js +++ b/vscode4teaching-extension/resources/dashboard/dashboard.js @@ -18,13 +18,25 @@ window.addEventListener('message', event => { const message = event.data; - for (const key in message) { - document.getElementById(key).textContent = message[key] + switch (message.type) { + case 'updateDate': + for (const key in message.update) { + document.getElementById(key).textContent = message.update[key] + } + break; + case 'openDone': + document.querySelectorAll("button." + message.username).forEach((e) => { + e.disabled = false; + }); + break; } }) document.querySelectorAll(".workspace-link").forEach((row) => { row.addEventListener("click", () => { + Array.from(row.parentElement.children).forEach((e) => { + e.disabled = true; + }); const username = Array.from(row.parentElement.parentElement.children).find(e => e.classList.contains('username')).innerHTML; vscode.postMessage({ type: "goToWorkspace", @@ -36,6 +48,9 @@ document.querySelectorAll(".workspace-link-diff").forEach((row) => { row.addEventListener("click", () => { + Array.from(row.parentElement.children).forEach((e) => { + e.disabled = true; + }); const username = Array.from(row.parentElement.parentElement.children).find(e => e.classList.contains('username')).innerHTML; vscode.postMessage({ type: "diff", diff --git a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts index 86498d34..7df188e2 100644 --- a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts +++ b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts @@ -126,6 +126,7 @@ export class DashboardWebview { } else { vscode.window.showErrorMessage("The exercise's workspace is not open."); } + this.panel.webview.postMessage({ type: "openDone", username: message.username}); }); break; } @@ -148,6 +149,7 @@ export class DashboardWebview { } else { vscode.window.showErrorMessage("The exercise's workspace is not open."); } + this.panel.webview.postMessage({ type: "openDone", username: message.username }); }); break; } @@ -229,7 +231,7 @@ export class DashboardWebview { for (const eui of this._euis) { message["user-lastmod-" + eui.user.id] = this.getElapsedTime(eui.updateDateTime); } - this.panel.webview.postMessage(message); + this.panel.webview.postMessage({type: "updateDate", update: message}); } private async findLastModifiedFile(folder: vscode.WorkspaceFolder, fileRoute: string) { @@ -318,7 +320,7 @@ export class DashboardWebview { } } rows = rows + ``; - const buttons = ``; + const buttons = ``; rows += eui.lastModifiedFile ? buttons : `Not found`; rows = rows + `\n`; rows = rows + `${this.getElapsedTime(eui.updateDateTime)}\n`; From 2ebe57a6fca6eea659529397501b85fca8dc3a69 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Wed, 10 Nov 2021 14:15:27 +0100 Subject: [PATCH 27/35] Disable all download buttons on dashboard while downloading content --- vscode4teaching-extension/resources/dashboard/dashboard.js | 4 ++-- .../src/components/dashboard/DashboardWebview.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vscode4teaching-extension/resources/dashboard/dashboard.js b/vscode4teaching-extension/resources/dashboard/dashboard.js index 9e8cb49d..f3b66ebb 100644 --- a/vscode4teaching-extension/resources/dashboard/dashboard.js +++ b/vscode4teaching-extension/resources/dashboard/dashboard.js @@ -25,7 +25,7 @@ } break; case 'openDone': - document.querySelectorAll("button." + message.username).forEach((e) => { + document.querySelectorAll(".button-col > button").forEach((e) => { e.disabled = false; }); break; @@ -34,7 +34,7 @@ document.querySelectorAll(".workspace-link").forEach((row) => { row.addEventListener("click", () => { - Array.from(row.parentElement.children).forEach((e) => { + document.querySelectorAll(".button-col > button").forEach((e) => { e.disabled = true; }); const username = Array.from(row.parentElement.parentElement.children).find(e => e.classList.contains('username')).innerHTML; diff --git a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts index 7df188e2..83549298 100644 --- a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts +++ b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts @@ -319,7 +319,7 @@ export class DashboardWebview { break; } } - rows = rows + ``; + rows = rows + ``; const buttons = ``; rows += eui.lastModifiedFile ? buttons : `Not found`; rows = rows + `\n`; From 6d0221b86e7d59976d5157f8385c5e7e01656710 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Mon, 15 Nov 2021 12:44:19 +0100 Subject: [PATCH 28/35] Bump needed vscode version Patch selected is the one that fixes the issues with the let's encrypt expired root cert --- vscode4teaching-extension/package.json | 2 +- vscode4teaching-extension/src/extension.ts | 45 +++++++++++----------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/vscode4teaching-extension/package.json b/vscode4teaching-extension/package.json index 61bfbb8e..5708636e 100644 --- a/vscode4teaching-extension/package.json +++ b/vscode4teaching-extension/package.json @@ -14,7 +14,7 @@ "description": "Bring the programming exercises directly to the student’s editor.", "version": "2.0.2", "engines": { - "vscode": "^1.45.1" + "vscode": "^1.61.0" }, "categories": [ "Other" diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index 76810331..25621eac 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -372,15 +372,6 @@ export async function initializeExtension(cwds: ReadonlyArray { - if (!hideWelcomeMessage) { - const message = ` - The exercise has been downloaded! You can start editing its files in the Explorer view. - You can mark the exercise as finished using the 'Finish' button in the status bar below. - `; - vscode.window.showInformationMessage(message).then(() => console.debug("Message dismissed")); - } - }); } } catch (error) { APIClient.handleAxiosError(error); @@ -393,24 +384,32 @@ export async function initializeExtension(cwds: ReadonlyArray { - if (!hideWelcomeMessage) { - const message = ` + } + } + vscode.commands.executeCommand("workbench.view.explorer").then(() => { + if (!hideWelcomeMessage) { + if (currentUserIsTeacher) { + const message = ` + The exercise has been downloaded! You can start editing its files in the Explorer view. + You can mark the exercise as finished using the 'Finish' button in the status bar below. + `; + vscode.window.showInformationMessage(message).then(() => console.debug("Message dismissed")); + } else { + const message = ` The exercise has been downloaded! You can see the template files and your students' files in the Explorer view. You can also open the Dashboard to monitor their progress (you can also open it from the status bar's 'Dashboard' button. `; - const openDashboard = "Open dashboard"; - vscode.window.showInformationMessage(message, openDashboard).then((value: string | undefined) => { - console.debug(value); - if (value === openDashboard) { - console.debug("Opening dashboard"); - return vscode.commands.executeCommand("vscode4teaching.showdashboard"); - } - }).then(() => console.debug("Message dismissed")); - } - }); + const openDashboard = "Open dashboard"; + vscode.window.showInformationMessage(message, openDashboard).then((value: string | undefined) => { + console.debug(value); + if (value === openDashboard) { + console.debug("Opening dashboard"); + return vscode.commands.executeCommand("vscode4teaching.showdashboard"); + } + }).then(() => console.debug("Message dismissed")); + } } - } + }); // Set template location if exists if (currentUserIsTeacher && v4tjson.template) { // Template should be the same in the workspace From c6be8f81ee2eabe94e6babf2f60993f885ea2f85 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Mon, 15 Nov 2021 14:54:40 +0100 Subject: [PATCH 29/35] Add warning about configuration needed to be able to make api calls Refer to this issue for more info: https://github.com/microsoft/vscode/issues/136787 Settings changed are the following: https://github.com/microsoft/vscode/issues/136787#issuecomment-968771447 --- vscode4teaching-extension/src/extension.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index 25621eac..481d23ea 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -289,6 +289,23 @@ export function activate(context: vscode.ExtensionContext) { deleteCourseDisposable, refreshView, refreshCourse, addExercise, editExercise, deleteExercise, addUsersToCourse, removeUsersFromCourse, getStudentFiles, diff, createComment, share, signup, signupTeacher, getWithCode, finishExercise, showDashboard, showLiveshareBoard); + // Temp fix for this issue https://github.com/microsoft/vscode/issues/136787 + // TODO: Remove this when the issue is fixed + const isWin = process.platform === "win32"; + if (isWin) { + if (vscode.workspace.getConfiguration("http").get("systemCertificates")) { + vscode.window.showWarningMessage("There may be issues connecting to the server unless you change your configuration settings.\nClicking the button will automatically make all configuration changes needed.", "Change configuration and restart").then((selected) => { + if (selected) { + vscode.workspace.getConfiguration("http").update("systemCertificates", false, true).then(() => { + vscode.commands.executeCommand("workbench.action.reloadWindow"); + }, (error) => { + console.error(error); + vscode.window.showErrorMessage("There was an error updating your configuration: " + error); + }); + } + }); + } + } } export function deactivate() { From 8747bb5de2f27cd36feebab8858af3e42a2f406c Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Tue, 16 Nov 2021 10:19:47 +0100 Subject: [PATCH 30/35] Add notification for dashboard get --- vscode4teaching-extension/src/client/APIClient.ts | 2 +- vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vscode4teaching-extension/src/client/APIClient.ts b/vscode4teaching-extension/src/client/APIClient.ts index 0c0a6939..2ccf3b94 100644 --- a/vscode4teaching-extension/src/client/APIClient.ts +++ b/vscode4teaching-extension/src/client/APIClient.ts @@ -417,7 +417,7 @@ class APIClientSingleton { method: "GET", responseType: "json", }; - return APIClient.createRequest(options, "Fetching students' exercise user info..."); + return APIClient.createRequest(options, "Fetching students' exercise user info...", true); } private signUp(credentials: UserSignup): AxiosPromise { diff --git a/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts b/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts index 3419388e..a38ef1d7 100644 --- a/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts +++ b/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts @@ -751,6 +751,6 @@ describe("client API calls", () => { const thenable = APIClient.getAllStudentsExerciseUserInfo(exerciseId); - expectCorrectRequest(expectedOptions, "Fetching students' exercise user info...", false, thenable); + expectCorrectRequest(expectedOptions, "Fetching students' exercise user info...", true, thenable); }); }); From 5df0c8ae08763c8dd31d5b0ae4911862dd5da356 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Tue, 16 Nov 2021 10:20:18 +0100 Subject: [PATCH 31/35] Add message notification to initialization test --- vscode4teaching-extension/src/extension.ts | 12 +++--------- .../test/unitSuite/EntryPoint.test.ts | 6 ++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index 481d23ea..714d3ea7 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -405,17 +405,11 @@ export async function initializeExtension(cwds: ReadonlyArray { if (!hideWelcomeMessage) { - if (currentUserIsTeacher) { - const message = ` - The exercise has been downloaded! You can start editing its files in the Explorer view. - You can mark the exercise as finished using the 'Finish' button in the status bar below. - `; + if (!currentUserIsTeacher) { + const message = `The exercise has been downloaded! You can start editing its files in the Explorer view. You can mark the exercise as finished using the 'Finish' button in the status bar below.`; vscode.window.showInformationMessage(message).then(() => console.debug("Message dismissed")); } else { - const message = ` - The exercise has been downloaded! You can see the template files and your students' files in the Explorer view. - You can also open the Dashboard to monitor their progress (you can also open it from the status bar's 'Dashboard' button. - `; + const message = `The exercise has been downloaded! You can see the template files and your students' files in the Explorer view. You can also open the Dashboard to monitor their progress (you can also open it from the status bar's 'Dashboard' button.`; const openDashboard = "Open dashboard"; vscode.window.showInformationMessage(message, openDashboard).then((value: string | undefined) => { console.debug(value); diff --git a/vscode4teaching-extension/test/unitSuite/EntryPoint.test.ts b/vscode4teaching-extension/test/unitSuite/EntryPoint.test.ts index f9a8bd97..28da9997 100644 --- a/vscode4teaching-extension/test/unitSuite/EntryPoint.test.ts +++ b/vscode4teaching-extension/test/unitSuite/EntryPoint.test.ts @@ -193,6 +193,12 @@ describe("Extension entry point", () => { expect(fswFunctionMocks.onDidDelete).toHaveBeenCalledTimes(1); expect(mockedVscode.workspace.onWillSaveTextDocument).toHaveBeenCalledTimes(1); expect(mockedVscode.workspace.onDidSaveTextDocument).toHaveBeenCalledTimes(1); + expect(mockedVscode.commands.executeCommand).toHaveBeenCalledTimes(2); + expect(mockedVscode.commands.executeCommand).toHaveBeenNthCalledWith(1, "setContext", "vscode4teaching.isTeacher", false); + expect(mockedVscode.commands.executeCommand).toHaveBeenNthCalledWith(2, "workbench.view.explorer"); + expect(mockedVscode.window.showInformationMessage).toHaveBeenCalledTimes(1); + const message = `The exercise has been downloaded! You can start editing its files in the Explorer view. You can mark the exercise as finished using the 'Finish' button in the status bar below.`; + expect(mockedVscode.window.showInformationMessage).toHaveBeenNthCalledWith(1, message); expect(extension.finishItem).toBeTruthy(); }); From 01de66ab0b3f8de53f619b897c9984d8881cfafa Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Tue, 16 Nov 2021 12:00:35 +0100 Subject: [PATCH 32/35] Hide buttons when uploading template to exercise to avoid accessing an exercise before it is uploaded --- .../src/components/courses/CoursesTreeProvider.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts b/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts index 30274a71..8db7279d 100644 --- a/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts +++ b/vscode4teaching-extension/src/components/courses/CoursesTreeProvider.ts @@ -1,6 +1,5 @@ import * as vscode from "vscode"; import { APIClient } from "../../client/APIClient"; -import { APIClientSession } from "../../client/APIClientSession"; import { CurrentUser } from "../../client/CurrentUser"; import { Course, instanceOfCourse } from "../../model/serverModel/course/Course"; import { instanceOfExercise } from "../../model/serverModel/exercise/Exercise"; @@ -253,6 +252,8 @@ export class CoursesProvider implements vscode.TreeDataProvider { // Create zip file from files and send them const course: Course = item.item; try { + this.loading = true; + CoursesProvider.triggerTreeReload(); const addExerciseData = await APIClient.addExercise(course.id, { name }); console.debug(addExerciseData); try { @@ -260,7 +261,6 @@ export class CoursesProvider implements vscode.TreeDataProvider { const zipContent = await FileZipUtil.getZipFromUris(fileUris); const response = await APIClient.uploadExerciseTemplate(addExerciseData.data.id, zipContent); console.debug(response); - CoursesProvider.triggerTreeReload(item); } catch (uploadError) { try { // If upload fails delete the exercise and show error @@ -270,6 +270,9 @@ export class CoursesProvider implements vscode.TreeDataProvider { } catch (deleteError) { APIClient.handleAxiosError(deleteError); } + } finally { + this.loading = false; + CoursesProvider.triggerTreeReload(); } } catch (error) { APIClient.handleAxiosError(error); From 6cf019de335b7758d259366b9ee6891c958c52db Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Tue, 16 Nov 2021 14:38:59 +0100 Subject: [PATCH 33/35] Save all modified files instead of the last --- .../controllers/ExerciseController.java | 2 +- .../controllers/dtos/ExerciseUserInfoDTO.java | 13 +++++++----- .../model/ExerciseUserInfo.java | 21 +++++++++++++------ .../services/ExerciseInfoService.java | 2 +- .../servicesimpl/ExerciseInfoServiceImpl.java | 4 ++-- .../ExerciseControllerTests.java | 8 +++++-- .../ExerciseInfoServiceImplTests.java | 21 ++++++++++++------- 7 files changed, 47 insertions(+), 24 deletions(-) diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExerciseController.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExerciseController.java index 94649fd0..c2fd27f1 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExerciseController.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/ExerciseController.java @@ -106,7 +106,7 @@ public ResponseEntity getExerciseUserInfo(@PathVariable Long e public ResponseEntity updateExerciseUserInfo(@PathVariable Long exerciseId, @RequestBody ExerciseUserInfoDTO exerciseUserInfoDTO, HttpServletRequest request) throws NotFoundException { return ResponseEntity.ok(exerciseInfoService.updateExerciseUserInfo(exerciseId, - jwtTokenUtil.getUsernameFromToken(request), exerciseUserInfoDTO.getStatus(), exerciseUserInfoDTO.getLastModifiedFile())); + jwtTokenUtil.getUsernameFromToken(request), exerciseUserInfoDTO.getStatus(), exerciseUserInfoDTO.getModifiedFiles())); } @GetMapping("/exercises/{exerciseId}/info/teacher") diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/ExerciseUserInfoDTO.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/ExerciseUserInfoDTO.java index bd1d85d0..b88ad85a 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/ExerciseUserInfoDTO.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/controllers/dtos/ExerciseUserInfoDTO.java @@ -1,8 +1,10 @@ package com.vscode4teaching.vscode4teachingserver.controllers.dtos; +import java.util.List; + public class ExerciseUserInfoDTO { private int status; - private String lastModifiedFile; + private List modifiedFiles; public boolean isFinished() { return status == 1; @@ -20,11 +22,12 @@ public void setStatus(int status) { this.status = status; } - public String getLastModifiedFile() { - return lastModifiedFile; + public List getModifiedFiles() { + return modifiedFiles; } - public void setLastModifiedFile(String lastModifiedFile) { - this.lastModifiedFile = lastModifiedFile; + public void setModifiedFiles(List modifiedFiles) { + this.modifiedFiles = modifiedFiles; } + } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseUserInfo.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseUserInfo.java index 30315cc3..d895adae 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseUserInfo.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/model/ExerciseUserInfo.java @@ -2,7 +2,11 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -44,8 +48,9 @@ public class ExerciseUserInfo { @JsonView(ExerciseUserInfoViews.GeneralView.class) private LocalDateTime updateDateTime; + @ElementCollection @JsonView(ExerciseUserInfoViews.GeneralView.class) - private String lastModifiedFile; + private Set modifiedFiles = new HashSet<>(); public ExerciseUserInfo() { @@ -83,13 +88,17 @@ public void setStatus(int status) { this.updateDateTime = LocalDateTime.now(ZoneOffset.UTC); } - public String getLastModifiedFile() { - return lastModifiedFile; + public Set getModifiedFiles() { + return modifiedFiles; } - public void setLastModifiedFile(String lastModifiedFile) { - if (lastModifiedFile != null) { - this.lastModifiedFile = lastModifiedFile; + public void setModifiedFiles(Set modifiedFiles) { + this.modifiedFiles = modifiedFiles; + } + + public void addModifiedFiles(Collection modifiedFiles) { + if (modifiedFiles != null) { + this.modifiedFiles.addAll(modifiedFiles); } } } \ No newline at end of file diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/ExerciseInfoService.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/ExerciseInfoService.java index 3f7ae84e..0d86f35f 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/ExerciseInfoService.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/services/ExerciseInfoService.java @@ -19,7 +19,7 @@ public interface ExerciseInfoService { public ExerciseUserInfo getExerciseUserInfo(@Min(0) Long exerciseId, @NotEmpty String username) throws NotFoundException; - public ExerciseUserInfo updateExerciseUserInfo(@Min(0) Long exerciseId, @NotEmpty String username, int status, String lastModifiedFile) + public ExerciseUserInfo updateExerciseUserInfo(@Min(0) Long exerciseId, @NotEmpty String username, int status, List modifiedFiles) throws NotFoundException; public List getAllStudentExerciseUserInfo(@Min(0) Long exerciseId, String requestUsername) diff --git a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseInfoServiceImpl.java b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseInfoServiceImpl.java index b06907ab..4857ed4d 100644 --- a/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseInfoServiceImpl.java +++ b/vscode4teaching-server/src/main/java/com/vscode4teaching/vscode4teachingserver/servicesimpl/ExerciseInfoServiceImpl.java @@ -41,11 +41,11 @@ public ExerciseUserInfo getExerciseUserInfo(@Min(0) Long exerciseId, @NotEmpty S } @Override - public ExerciseUserInfo updateExerciseUserInfo(@Min(0) Long exerciseId, @NotEmpty String username, int status, String lastModifiedFile) + public ExerciseUserInfo updateExerciseUserInfo(@Min(0) Long exerciseId, @NotEmpty String username, int status, List modifiedFiles) throws NotFoundException { ExerciseUserInfo eui = this.getAndCheckExerciseUserInfo(exerciseId, username); eui.setStatus(status); - eui.setLastModifiedFile(lastModifiedFile); + eui.addModifiedFiles(modifiedFiles); eui = exerciseUserInfoRepository.save(eui); this.websocketHandler.refreshExerciseDashboards(eui.getExercise().getCourse().getTeachers()); return eui; diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseControllerTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseControllerTests.java index 3640fb0f..56301927 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseControllerTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/controllertests/ExerciseControllerTests.java @@ -2,6 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.times; @@ -260,10 +262,12 @@ public void updateExerciseUserInfo_valid() throws Exception { user.setId(4l); ExerciseUserInfoDTO euiDTO = new ExerciseUserInfoDTO(); euiDTO.setStatus(1); - euiDTO.setLastModifiedFile("/sample"); + ArrayList euiModifiedFiles = new ArrayList<>(); + euiModifiedFiles.add("/sample"); + euiDTO.setModifiedFiles(euiModifiedFiles); ExerciseUserInfo updatedEui = new ExerciseUserInfo(ex, user); updatedEui.setStatus(1); - when(exerciseInfoService.updateExerciseUserInfo(1l, "johndoe", 1, "/sample")).thenReturn(updatedEui); + when(exerciseInfoService.updateExerciseUserInfo(anyLong(), anyString(), anyInt(), anyList())).thenReturn(updatedEui); MvcResult mvcResult = mockMvc .perform(put("/api/exercises/1/info").contentType("application/json").with(csrf()) diff --git a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseInfoServiceImplTests.java b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseInfoServiceImplTests.java index 98623b53..d157e409 100644 --- a/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseInfoServiceImplTests.java +++ b/vscode4teaching-server/src/test/java/com/vscode4teaching/vscode4teachingserver/servicetests/ExerciseInfoServiceImplTests.java @@ -90,8 +90,11 @@ public void updateExerciseUserInfo_valid() throws NotFoundException { User user = new User("johndoe@john.com", username, "johndoeuser", "John", "Doe", studentRole); ExerciseUserInfo eui = new ExerciseUserInfo(exercise, user); eui.setStatus(0); - eui.setLastModifiedFile("/old/file.txt"); - String newFilePath = "modified/file.txt"; + Set euiOldModifiedFiles = new HashSet<>(); + euiOldModifiedFiles.add("/old/file.txt"); + eui.setModifiedFiles(euiOldModifiedFiles); + ArrayList euiNewModifiedFiles = new ArrayList<>(); + euiNewModifiedFiles.add("/modified/file.txt"); Optional euiOpt = Optional.of(eui); course.addUserInCourse(user); User creator = new User("creator@john.com", "creator", "creatorteacher", "creator", "Doe Sr", studentRole, new Role("ROLE_TEACHER")); @@ -103,12 +106,14 @@ public void updateExerciseUserInfo_valid() throws NotFoundException { teacherSet.add(creator); when(exerciseUserInfoRepository.findByExercise_IdAndUser_Username(exerciseId, username)).thenReturn(euiOpt); when(exerciseUserInfoRepository.save(any(ExerciseUserInfo.class))).then(returnsFirstArg()); - ExerciseUserInfo savedEui = exerciseInfoService.updateExerciseUserInfo(exerciseId, username, 1, newFilePath); + ExerciseUserInfo savedEui = exerciseInfoService.updateExerciseUserInfo(exerciseId, username, 1, euiNewModifiedFiles); assertThat(savedEui.getExercise()).isEqualTo(exercise); assertThat(savedEui.getUser()).isEqualTo(user); assertThat(savedEui.getStatus() == 1).isTrue(); - assertThat(savedEui.getLastModifiedFile()).isEqualTo(newFilePath); + assertThat(savedEui.getModifiedFiles()).size().isEqualTo(2); + assertThat(savedEui.getModifiedFiles()).contains("/modified/file.txt"); + assertThat(savedEui.getModifiedFiles()).contains("/old/file.txt"); verify(exerciseUserInfoRepository, times(1)).findByExercise_IdAndUser_Username(exerciseId, username); verify(exerciseUserInfoRepository, times(1)).save(any(ExerciseUserInfo.class)); verify(websocketHandler, times(1)).refreshExerciseDashboards(teacherSet); @@ -125,8 +130,9 @@ public void updateExerciseUserInfo_valid_no_file() throws NotFoundException { User user = new User("johndoe@john.com", username, "johndoeuser", "John", "Doe", studentRole); ExerciseUserInfo eui = new ExerciseUserInfo(exercise, user); eui.setStatus(0); - String oldFilePath = "/old/file.txt"; - eui.setLastModifiedFile(oldFilePath); + Set euiOldModifiedFiles = new HashSet<>(); + euiOldModifiedFiles.add("/old/file.txt"); + eui.setModifiedFiles(euiOldModifiedFiles); Optional euiOpt = Optional.of(eui); when(exerciseUserInfoRepository.findByExercise_IdAndUser_Username(exerciseId, username)).thenReturn(euiOpt); when(exerciseUserInfoRepository.save(any(ExerciseUserInfo.class))).then(returnsFirstArg()); @@ -135,7 +141,8 @@ public void updateExerciseUserInfo_valid_no_file() throws NotFoundException { assertThat(savedEui.getExercise()).isEqualTo(exercise); assertThat(savedEui.getUser()).isEqualTo(user); assertThat(savedEui.getStatus() == 0).isTrue(); - assertThat(savedEui.getLastModifiedFile()).isEqualTo(oldFilePath); + assertThat(savedEui.getModifiedFiles()).size().isEqualTo(1); + assertThat(savedEui.getModifiedFiles()).contains("/old/file.txt"); verify(exerciseUserInfoRepository, times(1)).findByExercise_IdAndUser_Username(exerciseId, username); verify(exerciseUserInfoRepository, times(1)).save(any(ExerciseUserInfo.class)); } From 9be32107712bba8dc8c583af0f1ab0c875a37dcb Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Tue, 23 Nov 2021 11:48:00 +0100 Subject: [PATCH 34/35] Add tree view of modified files when opening in dashboard --- vscode4teaching-extension/package-lock.json | 94 ++++---- vscode4teaching-extension/package.json | 2 + .../resources/dashboard/dashboard.js | 2 - .../src/client/APIClient.ts | 4 +- .../components/dashboard/DashboardWebview.ts | 207 ++++++++++++++---- .../src/components/dashboard/OpenQuickPick.ts | 28 +++ vscode4teaching-extension/src/extension.ts | 6 +- .../serverModel/exercise/ExerciseUserInfo.ts | 2 +- .../src/services/EUIUpdateService.ts | 25 +-- 9 files changed, 257 insertions(+), 113 deletions(-) create mode 100644 vscode4teaching-extension/src/components/dashboard/OpenQuickPick.ts diff --git a/vscode4teaching-extension/package-lock.json b/vscode4teaching-extension/package-lock.json index f0822903..c6bfc324 100644 --- a/vscode4teaching-extension/package-lock.json +++ b/vscode4teaching-extension/package-lock.json @@ -1,18 +1,19 @@ { "name": "vscode4teaching", - "version": "2.0.1", + "version": "2.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode4teaching", - "version": "2.0.1", + "version": "2.0.2", "license": "SEE LICENSE IN LICENSE", "dependencies": { "axios": "^0.21.1", "form-data": "^3.0.0", "ignore": "^5.1.6", "jszip": "^3.4.0", + "lodash.escaperegexp": "^4.1.2", "mkdirp": "^1.0.4", "vsls": "^1.0.3015", "ws": "^7.4.6" @@ -24,6 +25,7 @@ "@types/glob": "^7.1.1", "@types/jest": "^25.2.3", "@types/jest-cli": "^24.3.0", + "@types/lodash.escaperegexp": "^4.1.6", "@types/mkdirp": "^1.0.0", "@types/node": "^10.15.1", "@types/rimraf": "^3.0.0", @@ -42,7 +44,7 @@ "vscode-test": "^1.3.0" }, "engines": { - "vscode": "^1.45.1" + "vscode": "^1.61.0" } }, "node_modules/@babel/code-frame": { @@ -2443,6 +2445,21 @@ "node": ">= 8.3" } }, + "node_modules/@types/lodash": { + "version": "4.14.177", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz", + "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==", + "dev": true + }, + "node_modules/@types/lodash.escaperegexp": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/lodash.escaperegexp/-/lodash.escaperegexp-4.1.6.tgz", + "integrity": "sha512-uENiqxLlqh6RzeE1cC6Z2gHqakToN9vKlTVCFkSVjAfeMeh2fY0916tHwJHeeKs28qB/hGYvKuampGYH5QDVCw==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -7914,6 +7931,11 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -9594,15 +9616,6 @@ "source-map": "^0.6.0" } }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", @@ -10144,15 +10157,6 @@ "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev" } }, - "node_modules/tslint/node_modules/diff": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", - "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/tslint/node_modules/mkdirp": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", @@ -10166,12 +10170,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/tslint/node_modules/mkdirp/node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, "node_modules/tsutils": { "version": "2.29.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", @@ -12778,6 +12776,21 @@ "jest-cli": "*" } }, + "@types/lodash": { + "version": "4.14.177", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz", + "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==", + "dev": true + }, + "@types/lodash.escaperegexp": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/lodash.escaperegexp/-/lodash.escaperegexp-4.1.6.tgz", + "integrity": "sha512-uENiqxLlqh6RzeE1cC6Z2gHqakToN9vKlTVCFkSVjAfeMeh2fY0916tHwJHeeKs28qB/hGYvKuampGYH5QDVCw==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -17131,6 +17144,11 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -18487,14 +18505,6 @@ "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } } }, "source-map-url": { @@ -18917,12 +18927,6 @@ "tsutils": "^2.29.0" }, "dependencies": { - "diff": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", - "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", - "dev": true - }, "mkdirp": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", @@ -18930,14 +18934,6 @@ "dev": true, "requires": { "minimist": "^1.2.5" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - } } } } diff --git a/vscode4teaching-extension/package.json b/vscode4teaching-extension/package.json index 5708636e..958da80a 100644 --- a/vscode4teaching-extension/package.json +++ b/vscode4teaching-extension/package.json @@ -310,6 +310,7 @@ "@types/glob": "^7.1.1", "@types/jest": "^25.2.3", "@types/jest-cli": "^24.3.0", + "@types/lodash.escaperegexp": "^4.1.6", "@types/mkdirp": "^1.0.0", "@types/node": "^10.15.1", "@types/rimraf": "^3.0.0", @@ -332,6 +333,7 @@ "form-data": "^3.0.0", "ignore": "^5.1.6", "jszip": "^3.4.0", + "lodash.escaperegexp": "^4.1.2", "mkdirp": "^1.0.4", "vsls": "^1.0.3015", "ws": "^7.4.6" diff --git a/vscode4teaching-extension/resources/dashboard/dashboard.js b/vscode4teaching-extension/resources/dashboard/dashboard.js index f3b66ebb..4172f5f4 100644 --- a/vscode4teaching-extension/resources/dashboard/dashboard.js +++ b/vscode4teaching-extension/resources/dashboard/dashboard.js @@ -41,7 +41,6 @@ vscode.postMessage({ type: "goToWorkspace", username: username, - lastMod:row.attributes.getNamedItem("data-lastMod").value, }); }); }); @@ -55,7 +54,6 @@ vscode.postMessage({ type: "diff", username: username, - lastMod:row.attributes.getNamedItem("data-lastMod-diff").value, }); }); }); diff --git a/vscode4teaching-extension/src/client/APIClient.ts b/vscode4teaching-extension/src/client/APIClient.ts index 2ccf3b94..8e5945d9 100644 --- a/vscode4teaching-extension/src/client/APIClient.ts +++ b/vscode4teaching-extension/src/client/APIClient.ts @@ -398,14 +398,14 @@ class APIClientSingleton { return APIClient.createRequest(options, "Fetching exercise info for current user..."); } - public updateExerciseUserInfo(exerciseId: number, status: number, lastModifiedFile?: string): AxiosPromise { + public updateExerciseUserInfo(exerciseId: number, status: number, modifiedFiles?: string[]): AxiosPromise { const options: AxiosBuildOptions = { url: "/api/exercises/" + exerciseId + "/info", method: "PUT", responseType: "json", data: { status, - lastModifiedFile, + modifiedFiles, }, }; return APIClient.createRequest(options, "Updating exercise user info..."); diff --git a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts index 83549298..a1b1831a 100644 --- a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts +++ b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts @@ -6,6 +6,7 @@ import { WebSocketV4TConnection } from "../../client/WebSocketV4TConnection"; import { Course } from "../../model/serverModel/course/Course"; import { Exercise } from "../../model/serverModel/exercise/Exercise"; import { ExerciseUserInfo } from "../../model/serverModel/exercise/ExerciseUserInfo"; +import { OpenQuickPick } from "./OpenQuickPick"; export class DashboardWebview { public static currentPanel: DashboardWebview | undefined; @@ -107,49 +108,28 @@ export class DashboardWebview { // break; // } case "goToWorkspace": { - await vscode.commands.executeCommand("vscode4teaching.getstudentfiles", course.name, exercise).then(async () => { - const workspaces = vscode.workspace.workspaceFolders; - if (workspaces) { - const wsF = workspaces.find((e) => e.name === message.username); - if (wsF) { - const lastFile = await this.findLastModifiedFile(wsF, message.lastMod); - if (!lastFile) { - vscode.window.showWarningMessage("Last modified file no longer exists (it might have been deleted by the student)"); - } else { - const doc1 = await vscode.workspace.openTextDocument(lastFile); - // let doc1 = await vscode.workspace.openTextDocument(await this.findMainFile(wsF)); - await vscode.window.showTextDocument(doc1); - } - } else { - vscode.window.showErrorMessage("Student's files not found."); - } - } else { - vscode.window.showErrorMessage("The exercise's workspace is not open."); + this.showQuickPick(message.username, course, exercise).then(async (filePath) => { + if (filePath !== undefined) { + const doc1 = await vscode.workspace.openTextDocument(filePath); + await vscode.window.showTextDocument(doc1); } - this.panel.webview.postMessage({ type: "openDone", username: message.username}); + this.panel.webview.postMessage({ type: "openDone", username: message.username }); + }).catch((err) => { + console.error(err); + vscode.window.showErrorMessage(err); }); break; } case "diff": { - await vscode.commands.executeCommand("vscode4teaching.getstudentfiles", course.name, exercise).then(async () => { - const workspaces = vscode.workspace.workspaceFolders; - if (workspaces) { - const wsF = workspaces.find((e) => e.name === message.username); - if (wsF) { - const lastFile = await this.findLastModifiedFile(wsF, message.lastMod); - if (!lastFile) { - vscode.window.showWarningMessage("Last modified file no longer exists (it might have been deleted by the student)"); - } else { - await vscode.commands.executeCommand("vscode4teaching.diff", lastFile); - } - } else { - vscode.window.showErrorMessage("Student's files not found."); - } - } else { - vscode.window.showErrorMessage("The exercise's workspace is not open."); + this.showQuickPick(message.username, course, exercise).then(async (filePath) => { + if (filePath !== undefined) { + await vscode.commands.executeCommand("vscode4teaching.diff", filePath); } this.panel.webview.postMessage({ type: "openDone", username: message.username }); + }).catch((err) => { + console.error(err); + vscode.window.showErrorMessage(err); }); break; } @@ -231,10 +211,10 @@ export class DashboardWebview { for (const eui of this._euis) { message["user-lastmod-" + eui.user.id] = this.getElapsedTime(eui.updateDateTime); } - this.panel.webview.postMessage({type: "updateDate", update: message}); + this.panel.webview.postMessage({ type: "updateDate", update: message }); } - private async findLastModifiedFile(folder: vscode.WorkspaceFolder, fileRoute: string) { + private async findLastModifiedFile(folder: vscode.WorkspaceFolder, fileRoute: string): Promise<{ uri: vscode.Uri, relativePath: string }> { if (fileRoute === "null") { return this.findMainFile(folder); } const fileRegex = /^\/[^\/]+\/[^\/]+\/[^\/]+\/(.+)$/; @@ -242,24 +222,26 @@ export class DashboardWebview { if (regexResults && regexResults.length > 1) { const match: vscode.Uri[] = await vscode.workspace.findFiles(new vscode.RelativePattern(folder, regexResults[1])); if (match.length === 1) { - return match[0]; + return { uri: match[0], relativePath: regexResults[1] }; } } return this.findMainFile(folder); } - private async findMainFile(folder: vscode.WorkspaceFolder) { + private async findMainFile(folder: vscode.WorkspaceFolder): Promise<{ uri: vscode.Uri, relativePath: string }> { const patterns = ["readme.*", "readme", "Main.*", "main.*", "index.html", "*"]; let matches: vscode.Uri[] = []; let i = 0; + let pattern = ""; while (matches.length <= 0 && i < patterns.length) { - const file = new vscode.RelativePattern(folder, patterns[i++]); + pattern = patterns[i++]; + const file = new vscode.RelativePattern(folder, pattern); matches = (await vscode.workspace.findFiles(file)); } if (matches.length <= 0) { matches = (await vscode.workspace.findFiles(new vscode.RelativePattern(folder, "*"))); } - return matches[0]; + return { uri: matches[0], relativePath: pattern }; } private reloadData() { @@ -320,8 +302,8 @@ export class DashboardWebview { } } rows = rows + ``; - const buttons = ``; - rows += eui.lastModifiedFile ? buttons : `Not found`; + const buttons = ``; + rows += buttons; rows = rows + `\n`; rows = rows + `${this.getElapsedTime(eui.updateDateTime)}\n`; rows = rows + "\n"; @@ -422,4 +404,143 @@ export class DashboardWebview { return `${Math.floor(elapsedTime)} ${unit}`; } + + /** + * Show quick pick with all modified files in EUIs for each user + * @param username string username + * @param course Course course + * @param exercise Exercise exercise + * @returns Thenable the selected file + */ + private async showQuickPick(username: string, course: Course, exercise: Exercise): Promise { + // Download most recent files + await vscode.commands.executeCommand("vscode4teaching.getstudentfiles", course.name, exercise); + return vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + cancellable: false, + title: "Getting modified files...", + }, (progress, token) => this.buildQuickPickItems(username)) + .then(async (result: OpenQuickPick[]) => { + if (result) { + const selection = await this.showQuickPickRecursive(result); + if (selection) { + return selection; + } + } + }); + } + + private async buildQuickPickItems(username: string): Promise { + // Find all modified files URIs (paths) + const workspaces = vscode.workspace.workspaceFolders; + if (workspaces) { + const wsF = workspaces.find((e) => e.name === username); + if (wsF) { + const euis = this._euis.filter((eui) => eui.user.username === username); + const uris: vscode.Uri[] = []; + const relativePaths: string[] = []; + if (euis.length > 0) { + const eui = euis[0]; + if (eui.modifiedFiles && eui.modifiedFiles.length > 0) { + for (const fileName of eui.modifiedFiles) { + const lastFile = await this.findLastModifiedFile(wsF, fileName); + if (lastFile.uri) { + relativePaths.push(lastFile.relativePath); + uris.push(lastFile.uri); + } + } + } else { + vscode.window.showWarningMessage(`No modified files for ${username}`); + } + } + if (uris.length > 0) { + // Create directory tree object from relative paths with relative path and children + // Result is array of objects with keys "name" with the name of the file and "children" if the file is a directory listing its children + const result: OpenQuickPick[] = []; + const level: { [key: string]: any } = { result }; + relativePaths.forEach((relPath) => { + relPath.split("/").reduce((r, name, i, a) => { + if (!r[name]) { + r[name] = { result: [] }; + r.result.push(new OpenQuickPick(name, r[name].result)); + } + return r[name]; + }, level); + }); + // Add full relative path to each leaf node + relativePaths.forEach((relPath, index) => { + const parts = relPath.split("/"); + let currentNode = result; + for (const part of parts) { + const node = currentNode.find((e) => e.name === part); + if (node) { + if (node.children && node.children.length > 0) { + currentNode = node.children; + } else { + node.fullPath = uris[index]; + break; + } + } + } + }); + // Combine paths if there is only 1 directory as a child + result.forEach((e) => this.shortenPaths(e)); + // Add parent directories/files to all children + result.forEach((e) => this.addParentsToChildren(result, e.children)); + return result; + } else { + throw new Error("No files found for student " + username); + } + } else { + throw new Error("No files found for student " + username); + } + } else { + throw new Error("The exercise's workspace is not open."); + } + } + + // Add parent object as property to children in the tree with key "children" as array of objects + private addParentsToChildren(parents: OpenQuickPick[], children: OpenQuickPick[]) { + children.forEach((child) => { + child.parents = parents; + if (child.children) { + this.addParentsToChildren(children, child.children); + } + }); + } + + // Combines parent and child as a single path if there is only one child and it is a directory + private shortenPaths(item: OpenQuickPick) { + if ((item.children.length === 1) && (item.children[0].children.length > 0)) { + const child = item.children[0]; + item.name = item.name + "/" + child.name; + item.children = child.children; + this.shortenPaths(item); + } else { + for (const child of item.children) { + this.shortenPaths(child); + } + } + } + + // Recursive show quick pick with files given the file or directory name and an array of children + private async showQuickPickRecursive(recursiveFiles: OpenQuickPick[]): Promise { + if (!recursiveFiles[0].isGoBackButton && recursiveFiles[0].parents && recursiveFiles[0].parents.length > 0) { + recursiveFiles.unshift(new OpenQuickPick("..", [], recursiveFiles[0].parents, true)); + } + const selection = await vscode.window.showQuickPick(recursiveFiles, { + placeHolder: "Choose a file to open", + }); + if (selection) { + if (selection.parents && selection.label === "..") { + return this.showQuickPickRecursive(selection.parents); + } + if (selection.children && selection.children.length > 0) { + return this.showQuickPickRecursive(selection.children); + } else { + console.log(selection); + return selection.fullPath; + } + } + } } diff --git a/vscode4teaching-extension/src/components/dashboard/OpenQuickPick.ts b/vscode4teaching-extension/src/components/dashboard/OpenQuickPick.ts new file mode 100644 index 00000000..18485235 --- /dev/null +++ b/vscode4teaching-extension/src/components/dashboard/OpenQuickPick.ts @@ -0,0 +1,28 @@ +import * as vscode from "vscode"; + +export class OpenQuickPick implements vscode.QuickPickItem { + + constructor( + public name: string, + public children: OpenQuickPick[], + public parents?: OpenQuickPick[], + public isGoBackButton: boolean = false, + public fullPath?: vscode.Uri, + public description?: string, + public detail?: string, + public picked?: boolean, + public alwaysShow?: boolean, + ) { + + } + + get label(): string { + let prefix = ""; + if (this.children.length > 0) { + prefix = "$(folder) "; + } else if (!this.isGoBackButton) { + prefix = "$(file) "; + } + return prefix + this.name; + } +} diff --git a/vscode4teaching-extension/src/extension.ts b/vscode4teaching-extension/src/extension.ts index 714d3ea7..9b375fa2 100644 --- a/vscode4teaching-extension/src/extension.ts +++ b/vscode4teaching-extension/src/extension.ts @@ -448,6 +448,7 @@ function setStudentEvents(jszipFile: JSZip, cwd: vscode.WorkspaceFolder, zipUri: const pattern = new vscode.RelativePattern(cwd, "**/*"); const fsw = vscode.workspace.createFileSystemWatcher(pattern); changeEvent = fsw.onDidChange((e: vscode.Uri) => { + EUIUpdateService.addModifiedPath(e); if (uploadTimeout) { global.clearTimeout(uploadTimeout); } @@ -455,11 +456,12 @@ function setStudentEvents(jszipFile: JSZip, cwd: vscode.WorkspaceFolder, zipUri: uploadTimeout = undefined; FileZipUtil.updateFile(jszipFile, e.fsPath, cwd.uri.fsPath, ignoredFiles, exerciseId).then(() => { console.debug("File edited: " + e.fsPath); + EUIUpdateService.updateExercise(exerciseId); }); - EUIUpdateService.updateExercise(e, exerciseId); }, 500); }); createEvent = fsw.onDidCreate((e: vscode.Uri) => { + EUIUpdateService.addModifiedPath(e); if (uploadTimeout) { global.clearTimeout(uploadTimeout); } @@ -467,8 +469,8 @@ function setStudentEvents(jszipFile: JSZip, cwd: vscode.WorkspaceFolder, zipUri: uploadTimeout = undefined; FileZipUtil.updateFile(jszipFile, e.fsPath, cwd.uri.fsPath, ignoredFiles, exerciseId).then(() => { console.debug("File added: " + e.fsPath); + EUIUpdateService.updateExercise(exerciseId); }); - EUIUpdateService.updateExercise(e, exerciseId); }, 500); }); deleteEvent = fsw.onDidDelete((e: vscode.Uri) => { diff --git a/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseUserInfo.ts b/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseUserInfo.ts index 98cc5928..ac88deaa 100644 --- a/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseUserInfo.ts +++ b/vscode4teaching-extension/src/model/serverModel/exercise/ExerciseUserInfo.ts @@ -6,5 +6,5 @@ export interface ExerciseUserInfo { user: User; status: number; updateDateTime: string; - lastModifiedFile?: string; + modifiedFiles?: string[]; } diff --git a/vscode4teaching-extension/src/services/EUIUpdateService.ts b/vscode4teaching-extension/src/services/EUIUpdateService.ts index 826ee366..6bac111e 100644 --- a/vscode4teaching-extension/src/services/EUIUpdateService.ts +++ b/vscode4teaching-extension/src/services/EUIUpdateService.ts @@ -1,32 +1,29 @@ +import escapeRegExp from "lodash.escaperegexp"; import * as vscode from "vscode"; import { APIClient } from "../client/APIClient"; -import { ExerciseUserInfo } from "../model/serverModel/exercise/ExerciseUserInfo"; import { FileZipUtil } from "../utils/FileZipUtil"; export class EUIUpdateService { - public static getExerciseInfoFromUri(uri: vscode.Uri) { + public static addModifiedPath(uri: vscode.Uri) { const matches = (this.URI_REGEX.exec(uri.path)); if (!matches) { return null; } matches.shift(); - return { - uri: uri.path, - path: matches[0], - username: matches[1], - courseName: matches[2], - exerciseName: matches[3], - fileName: matches[matches.length - 1], - }; + this.modifiedPaths.add(matches[0]); } - public static async updateExercise(uri: vscode.Uri, exerciseId: number) { - const info = this.getExerciseInfoFromUri(uri); + public static async updateExercise(exerciseId: number) { const response = await APIClient.getExerciseUserInfo(exerciseId); console.debug(response); const originalStatus = response.data.status; - const responseEui = await APIClient.updateExerciseUserInfo(exerciseId, originalStatus, info?.path || ""); + const responseEui = await APIClient.updateExerciseUserInfo(exerciseId, originalStatus, Array.from(this.modifiedPaths)); + this.modifiedPaths.clear(); console.debug(responseEui); } - private static readonly URI_REGEX: RegExp = new RegExp("/" + FileZipUtil.downloadDir + "(\/([^\/]+)\/([^\/]+)\/([^\/]+)\/(.+))$"); + // Regex to extract the path for the file. + // Group is the path with username, course and exercise included. + private static readonly URI_REGEX: RegExp = new RegExp(escapeRegExp(vscode.Uri.file(FileZipUtil.downloadDir).path) + "(\/[^\/]+\/[^\/]+\/[^\/]+\/.+)$", "i"); + private static modifiedPaths: Set = new Set(); + } From f038ebe3303321651e9d138adcb5a9dda7e29386 Mon Sep 17 00:00:00 2001 From: ivchicano <46921761+ivchicano@users.noreply.github.com> Date: Tue, 23 Nov 2021 12:21:35 +0100 Subject: [PATCH 35/35] Change tree view for a list of files when opening in dashboard --- .../components/dashboard/DashboardWebview.ts | 29 +------------------ .../test/unitSuite/ClientAPICalls.test.ts | 2 +- .../test/unitSuite/Commands.test.ts | 2 +- .../test/unitSuite/DashboardWebview.test.ts | 11 ++----- .../test/unitSuite/EntryPoint.test.ts | 2 +- 5 files changed, 7 insertions(+), 39 deletions(-) diff --git a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts index a1b1831a..241491a9 100644 --- a/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts +++ b/vscode4teaching-extension/src/components/dashboard/DashboardWebview.ts @@ -457,36 +457,9 @@ export class DashboardWebview { // Create directory tree object from relative paths with relative path and children // Result is array of objects with keys "name" with the name of the file and "children" if the file is a directory listing its children const result: OpenQuickPick[] = []; - const level: { [key: string]: any } = { result }; - relativePaths.forEach((relPath) => { - relPath.split("/").reduce((r, name, i, a) => { - if (!r[name]) { - r[name] = { result: [] }; - r.result.push(new OpenQuickPick(name, r[name].result)); - } - return r[name]; - }, level); - }); - // Add full relative path to each leaf node relativePaths.forEach((relPath, index) => { - const parts = relPath.split("/"); - let currentNode = result; - for (const part of parts) { - const node = currentNode.find((e) => e.name === part); - if (node) { - if (node.children && node.children.length > 0) { - currentNode = node.children; - } else { - node.fullPath = uris[index]; - break; - } - } - } + result.push(new OpenQuickPick(relPath, [], undefined, false, uris[index])); }); - // Combine paths if there is only 1 directory as a child - result.forEach((e) => this.shortenPaths(e)); - // Add parent directories/files to all children - result.forEach((e) => this.addParentsToChildren(result, e.children)); return result; } else { throw new Error("No files found for student " + username); diff --git a/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts b/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts index a38ef1d7..02e25ee3 100644 --- a/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts +++ b/vscode4teaching-extension/test/unitSuite/ClientAPICalls.test.ts @@ -35,7 +35,7 @@ describe("client API calls", () => { const config = (mockedAxios.mock.calls[0][0] as AxiosRequestConfig); options.headers = config.headers; options.data = config.data; - } + } if (notification) { expect(mockedVscode.window.withProgress).toHaveBeenCalledTimes(1); expect(mockedVscode.window.withProgress).toHaveBeenNthCalledWith(1, { diff --git a/vscode4teaching-extension/test/unitSuite/Commands.test.ts b/vscode4teaching-extension/test/unitSuite/Commands.test.ts index 30e242d3..7032a70b 100644 --- a/vscode4teaching-extension/test/unitSuite/Commands.test.ts +++ b/vscode4teaching-extension/test/unitSuite/Commands.test.ts @@ -194,7 +194,7 @@ describe("Command implementations", () => { user, exercise, updateDateTime: new Date().toISOString(), - lastModifiedFile: "", + modifiedFiles: [], }; const response: AxiosResponse = { data: eui, diff --git a/vscode4teaching-extension/test/unitSuite/DashboardWebview.test.ts b/vscode4teaching-extension/test/unitSuite/DashboardWebview.test.ts index fd19138e..03d9af98 100644 --- a/vscode4teaching-extension/test/unitSuite/DashboardWebview.test.ts +++ b/vscode4teaching-extension/test/unitSuite/DashboardWebview.test.ts @@ -60,7 +60,7 @@ describe("Dashboard webview", () => { user: student1, status: 0, updateDateTime: new Date(new Date(now.setDate(now.getDate() - 1)).toISOString()).toISOString(), - lastModifiedFile: "/index.html", + modifiedFiles: ["/index.html"], }); now = new Date(new Date().toLocaleString("en-US", { timeZone: "UTC" })); euis.push({ @@ -68,7 +68,7 @@ describe("Dashboard webview", () => { user: student2, status: 1, updateDateTime: new Date(new Date(now.setMinutes(now.getMinutes() - 13)).toISOString()).toISOString(), - lastModifiedFile: "/readme.md", + modifiedFiles: ["/readme.md"], }); now = new Date(new Date().toLocaleString("en-US", { timeZone: "UTC" })); euis.push({ @@ -76,7 +76,7 @@ describe("Dashboard webview", () => { user: student3, status: 2, updateDateTime: new Date(new Date(now.setSeconds(now.getSeconds() - 35)).toISOString()).toISOString(), - lastModifiedFile: undefined, + modifiedFiles: undefined, }); DashboardWebview.show(euis, course, exercise); if (DashboardWebview.currentPanel) { @@ -131,10 +131,8 @@ describe("Dashboard webview", () => { expect(tableData[2].attribs.class).toBe("not-started-cell"); expect(tableData[3].childNodes[0].name).toBe("button"); expect(tableData[3].childNodes[0].firstChild.data).toBe("Open"); - expect(tableData[3].childNodes[0].attribs["data-lastmod"]).toBe("/index.html"); expect(tableData[3].childNodes[1].name).toBe("button"); expect(tableData[3].childNodes[1].firstChild.data).toBe("Diff"); - expect(tableData[3].childNodes[1].attribs["data-lastmod-diff"]).toBe("/index.html"); // expect(tableData[4].firstChild.data === "1 d" || tableData[4].firstChild.data === "24 h").toBe(true); expect(tableData[5].firstChild.data).toBe("Student 2"); expect(tableData[6].firstChild.data).toBe("student2"); @@ -142,16 +140,13 @@ describe("Dashboard webview", () => { expect(tableData[7].attribs.class).toBe("finished-cell"); expect(tableData[8].childNodes[0].name).toBe("button"); expect(tableData[8].childNodes[0].firstChild.data).toBe("Open"); - expect(tableData[8].childNodes[0].attribs["data-lastmod"]).toBe("/readme.md"); expect(tableData[8].childNodes[1].name).toBe("button"); expect(tableData[8].childNodes[1].firstChild.data).toBe("Diff"); - expect(tableData[8].childNodes[1].attribs["data-lastmod-diff"]).toBe("/readme.md"); // expect(tableData[9].firstChild.data).toBe("13 min"); expect(tableData[10].firstChild.data).toBe("Student 3"); expect(tableData[11].firstChild.data).toBe("student3"); expect(tableData[12].firstChild.data).toBe("On progress"); expect(tableData[12].attribs.class).toBe("onprogress-cell"); - expect(tableData[13].firstChild.data).toBe("Not found"); // expect(tableData[14].firstChild.data).toBe("35 s"); } else { fail("Current panel wasn't created"); diff --git a/vscode4teaching-extension/test/unitSuite/EntryPoint.test.ts b/vscode4teaching-extension/test/unitSuite/EntryPoint.test.ts index 28da9997..943ea057 100644 --- a/vscode4teaching-extension/test/unitSuite/EntryPoint.test.ts +++ b/vscode4teaching-extension/test/unitSuite/EntryPoint.test.ts @@ -132,7 +132,7 @@ describe("Extension entry point", () => { user, status: 0, updateDateTime: new Date().toISOString(), - lastModifiedFile: "", + modifiedFiles: [], }; const euiResponse: AxiosResponse = { data: eui,