diff --git a/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignment.java b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignment.java --- a/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignment.java +++ b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignment.java @@ -82,4 +82,6 @@ public boolean hasBeenSubmitted() { return this.submissionDate != null; } + + public abstract void resetGrades(); } diff --git a/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentRepository.java b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentRepository.java --- a/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentRepository.java +++ b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentRepository.java @@ -5,6 +5,7 @@ import fr.univ_lille.gitlab.classrooms.users.ClassroomUser; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; import java.util.UUID; interface StudentAssignmentRepository extends JpaRepository { @@ -15,4 +16,6 @@ StudentAssignment findByAssignmentAndStudent(Assignment assignment, ClassroomUser student); List findByAssignmentClassroomAndStudent(Classroom classroom, ClassroomUser student); + + Optional findByAssignmentIdAndStudent(UUID assignmentId, ClassroomUser student); } diff --git a/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentService.java b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentService.java --- a/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentService.java +++ b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentService.java @@ -1,6 +1,9 @@ package fr.univ_lille.gitlab.classrooms.assignments; +import fr.univ_lille.gitlab.classrooms.users.ClassroomUser; + import java.util.Optional; +import java.util.UUID; public interface StudentAssignmentService { @@ -11,6 +14,8 @@ * @param gitlabProjectId * @return */ Optional getByGitlabProjectId(long gitlabProjectId); + + void resetGrades(ClassroomUser student, UUID studentAssignmentId); void save(StudentExerciseAssignment exerciseAssignment); } diff --git a/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentServiceImpl.java b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentServiceImpl.java --- a/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentServiceImpl.java +++ b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentServiceImpl.java @@ -1,21 +1,38 @@ package fr.univ_lille.gitlab.classrooms.assignments; +import fr.univ_lille.gitlab.classrooms.users.ClassroomUser; +import jakarta.transaction.Transactional; import org.springframework.stereotype.Service; import java.util.Optional; +import java.util.UUID; @Service class StudentAssignmentServiceImpl implements StudentAssignmentService { private final StudentExerciceAssignmentRepository studentExerciceAssignmentRepository; - StudentAssignmentServiceImpl(StudentExerciceAssignmentRepository studentExerciceAssignmentRepository) { + private final StudentAssignmentRepository studentAssignmentRepository; + + StudentAssignmentServiceImpl(StudentExerciceAssignmentRepository studentExerciceAssignmentRepository, StudentAssignmentRepository studentAssignmentRepository) { this.studentExerciceAssignmentRepository = studentExerciceAssignmentRepository; + this.studentAssignmentRepository = studentAssignmentRepository; } @Override public Optional getByGitlabProjectId(long gitlabProjectId) { return this.studentExerciceAssignmentRepository.findByGitlabProjectId(gitlabProjectId); + } + + @Override + @Transactional + public void resetGrades(ClassroomUser student, UUID assignmentId) { + var studentAssignment= this.studentAssignmentRepository.findByAssignmentIdAndStudent(assignmentId, student); + + studentAssignment.ifPresent(it -> { + it.resetGrades(); + this.studentAssignmentRepository.save(it); + }); } @Override diff --git a/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentViewController.java b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentViewController.java new file mode 100644 --- /dev/null +++ b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentViewController.java @@ -0,0 +1,47 @@ +package fr.univ_lille.gitlab.classrooms.assignments; + +import fr.univ_lille.gitlab.classrooms.users.ClassroomUser; +import fr.univ_lille.gitlab.classrooms.users.ClassroomUserService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.ModelAndView; + +import java.util.Optional; +import java.util.UUID; + +/** + * View Controller that manages all operations a Student or Teacher can do on a StudentAssignment. + */ +@Controller +@RequestMapping("/assignments/{assignmentId}/students/{studentId}") +class StudentAssignmentViewController { + + private final StudentAssignmentService studentAssignmentService; + + private final ClassroomUserService classroomUserService; + + StudentAssignmentViewController(StudentAssignmentService studentAssignmentService, ClassroomUserService classroomUserService) { + this.studentAssignmentService = studentAssignmentService; + this.classroomUserService = classroomUserService; + } + + @RolesAllowed({"STUDENT", "TEACHER"}) + @PostMapping("/reset") + ModelAndView resetGrades(@PathVariable UUID assignmentId, @PathVariable String studentId, @ModelAttribute("user") ClassroomUser connectedUser, HttpServletRequest request) { + var student = this.classroomUserService.getClassroomUser(studentId); + + if( ! connectedUser.isTeacher() && (! student.equals(connectedUser))){ + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Student can only reset its own grades"); + } + + this.studentAssignmentService.resetGrades(student, assignmentId); + + // redirect to the referer (ugly) + var view = Optional.ofNullable(request.getHeader("Referer")).orElse("/"); + return new ModelAndView("redirect:"+view); + } +} diff --git a/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentExerciseAssignment.java b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentExerciseAssignment.java --- a/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentExerciseAssignment.java +++ b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentExerciseAssignment.java @@ -56,4 +56,10 @@ return this.assignmentGrades.stream() .mapToLong(AssignmentGrade::getMaxScore) .sum(); } + + @Override + public void resetGrades() { + this.setSubmissionDate(null); + this.assignmentGrades.clear(); + } } diff --git a/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentQuizAssignment.java b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentQuizAssignment.java --- a/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentQuizAssignment.java +++ b/src/main/java/fr/univ_lille/gitlab/classrooms/assignments/StudentQuizAssignment.java @@ -5,4 +5,10 @@ @Entity public class StudentQuizAssignment extends StudentAssignment { + @Override + public void resetGrades() { + this.setSubmissionDate(null); + this.setScore(null); + this.setMaxScore(null); + } } diff --git a/src/main/java/fr/univ_lille/gitlab/classrooms/users/ClassroomUserServiceImpl.java b/src/main/java/fr/univ_lille/gitlab/classrooms/users/ClassroomUserServiceImpl.java --- a/src/main/java/fr/univ_lille/gitlab/classrooms/users/ClassroomUserServiceImpl.java +++ b/src/main/java/fr/univ_lille/gitlab/classrooms/users/ClassroomUserServiceImpl.java @@ -5,7 +5,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import java.net.MalformedURLException; -import java.net.URL; +import java.net.URI; import java.util.List; @Service @@ -35,7 +35,7 @@ classroomUser.setGitlabUserId(Long.parseLong(id.toString())); if(oauth2User.getAttributes().containsKey("avatar_url")){ try { - classroomUser.setAvatarUrl(new URL(oauth2User.getAttribute("avatar_url"))); + classroomUser.setAvatarUrl(URI.create(oauth2User.getAttribute("avatar_url")).toURL()); } catch (MalformedURLException ignore) { // ignore incorrect url } diff --git a/src/main/resources/templates/classrooms/student.html b/src/main/resources/templates/classrooms/student.html --- a/src/main/resources/templates/classrooms/student.html +++ b/src/main/resources/templates/classrooms/student.html @@ -65,6 +65,12 @@ th:text="${studentAssignment.score} + ' / ' + ${studentAssignment.maxScore}">8/10 +
+ +
diff --git a/src/main/resources/templates/exercise/all-submissions.html b/src/main/resources/templates/exercise/all-submissions.html --- a/src/main/resources/templates/exercise/all-submissions.html +++ b/src/main/resources/templates/exercise/all-submissions.html @@ -86,6 +86,15 @@ Open in Gitlab + +
+ +
+ diff --git a/src/main/resources/templates/quiz/all-submissions.html b/src/main/resources/templates/quiz/all-submissions.html --- a/src/main/resources/templates/quiz/all-submissions.html +++ b/src/main/resources/templates/quiz/all-submissions.html @@ -60,6 +60,15 @@
date
+ +
+ +
+ diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentServiceImplTest.java b/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentServiceImplTest.java --- a/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentServiceImplTest.java +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentServiceImplTest.java @@ -1,11 +1,17 @@ package fr.univ_lille.gitlab.classrooms.assignments; +import fr.univ_lille.gitlab.classrooms.users.ClassroomRole; +import fr.univ_lille.gitlab.classrooms.users.ClassroomUser; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -16,6 +22,10 @@ private StudentAssignmentServiceImpl studentAssignmentService; @Mock private StudentExerciceAssignmentRepository studentExerciceAssignmentRepository; + + + @Mock + private StudentAssignmentRepository studentAssignmentRepository; @Test void getByGitlabProjectId() { @@ -30,5 +40,34 @@ var exercise = new StudentExerciseAssignment(); this.studentAssignmentService.save(exercise); verify(this.studentExerciceAssignmentRepository).save(exercise); + } + + @Test + void resetGrades_doesNothing_ifNoStudentAssignment_exists(){ + var student = new ClassroomUser("luke", List.of(ClassroomRole.STUDENT)); + var assignmentId = UUID.randomUUID(); + + when(this.studentAssignmentRepository.findByAssignmentIdAndStudent(assignmentId, student)).thenReturn(Optional.empty()); + + this.studentAssignmentService.resetGrades(student, assignmentId); + + verify(studentAssignmentRepository).findByAssignmentIdAndStudent(assignmentId, student); + verifyNoMoreInteractions(studentAssignmentRepository); + } + + @Test + void resetGrades(){ + var student = new ClassroomUser("luke", List.of(ClassroomRole.STUDENT)); + var assignmentId = UUID.randomUUID(); + + var studentAssignment = mock(StudentAssignment.class); + + when(this.studentAssignmentRepository.findByAssignmentIdAndStudent(assignmentId, student)).thenReturn(Optional.of(studentAssignment)); + + this.studentAssignmentService.resetGrades(student, assignmentId); + + verify(studentAssignmentRepository).findByAssignmentIdAndStudent(assignmentId, student); + verify(studentAssignment).resetGrades(); + verify(studentAssignmentRepository).save(studentAssignment); } } diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentViewControllerMVCTest.java b/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentViewControllerMVCTest.java new file mode 100644 --- /dev/null +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentAssignmentViewControllerMVCTest.java @@ -0,0 +1,80 @@ +package fr.univ_lille.gitlab.classrooms.assignments; + +import fr.univ_lille.gitlab.classrooms.users.WithMockStudent; +import fr.univ_lille.gitlab.classrooms.users.WithMockTeacher; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.UUID; + +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; + +@SpringBootTest +@AutoConfigureMockMvc +@Sql("/sql/init-test-users.sql") +class StudentAssignmentViewControllerMVCTest { + + + @Autowired + private MockMvc mockMvc; + + @MockBean + private StudentAssignmentService studentAssignmentService; + + private final UUID quizAssignmentId = UUID.randomUUID(); + + @BeforeEach + void setUp() { + } + + @Test + @WithMockStudent(username = "leia.organa") + void testResetGrade_asWrongStudent_shouldBeForbidden() throws Exception { + mockMvc.perform(post("/assignments/{assignmentId}/students/{studentId}/reset", quizAssignmentId.toString(), "luke.skywalker") + .with(csrf())) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockStudent + void testResetGrade_asSelf() throws Exception { + mockMvc.perform(post("/assignments/{assignmentId}/students/{studentId}/reset", quizAssignmentId.toString(), "luke.skywalker") + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(view().name("redirect:/")); + + verify(studentAssignmentService).resetGrades(argThat(it -> it.getName().equals("luke.skywalker")), eq(quizAssignmentId)); + } + + @Test + @WithMockTeacher + void testResetGrade_asTeacher() throws Exception { + mockMvc.perform(post("/assignments/{assignmentId}/students/{studentId}/reset", quizAssignmentId.toString(), "luke.skywalker") + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(view().name("redirect:/")); + + verify(studentAssignmentService).resetGrades(argThat(it -> it.getName().equals("luke.skywalker")), eq(quizAssignmentId)); + } + + @Test + @WithMockTeacher + void testResetGrade_asTeacher_shouldRedirectToRefererPage() throws Exception { + mockMvc.perform(post("/assignments/{assignmentId}/students/{studentId}/reset", quizAssignmentId.toString(), "luke.skywalker") + .header("Referer", "http://localhost:8080/classrooms/death-star") + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(view().name("redirect:http://localhost:8080/classrooms/death-star")); + } + +} diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentExerciseAssignmentTest.java b/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentExerciseAssignmentTest.java --- a/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentExerciseAssignmentTest.java +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentExerciseAssignmentTest.java @@ -3,6 +3,8 @@ import fr.univ_lille.gitlab.classrooms.assignments.grading.junit.JUnitAssignmentGrade; import org.junit.jupiter.api.Test; +import java.time.ZonedDateTime; + import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; @@ -20,5 +22,28 @@ studentExerciseAssignment.getAssignmentGrades().add(junitGrade); studentExerciseAssignment.getAssignmentGrades().add(anotherJunitGrade); assertThat(studentExerciseAssignment.getAssignmentGrades()).hasSize(1); + } + + @Test + void resetGrade_shouldRemoveAllTheGrades(){ + var studentExerciseAssignment = new StudentExerciseAssignment(); + + var junitGrade = new JUnitAssignmentGrade(); + studentExerciseAssignment.getAssignmentGrades().add(junitGrade); + + studentExerciseAssignment.resetGrades(); + + assertThat(studentExerciseAssignment.getAssignmentGrades()).isEmpty(); + } + + @Test + void resetGrade_shouldSetTheAssignmentUnSubmitted(){ + var studentExerciseAssignment = new StudentExerciseAssignment(); + + studentExerciseAssignment.setSubmissionDate(ZonedDateTime.now()); + + studentExerciseAssignment.resetGrades(); + + assertThat(studentExerciseAssignment.hasBeenSubmitted()).isFalse(); } } diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentQuizAssignmentTest.java b/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentQuizAssignmentTest.java new file mode 100644 --- /dev/null +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/assignments/StudentQuizAssignmentTest.java @@ -0,0 +1,34 @@ +package fr.univ_lille.gitlab.classrooms.assignments; + +import org.junit.jupiter.api.Test; + +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +class StudentQuizAssignmentTest { + + @Test + void resetGrade_shouldResetTheScore(){ + var studentQuizAssignment = new StudentQuizAssignment(); + studentQuizAssignment.setScore(12L); + studentQuizAssignment.setMaxScore(42L); + + studentQuizAssignment.resetGrades(); + + assertThat(studentQuizAssignment.getScore()).isNull(); + assertThat(studentQuizAssignment.getMaxScore()).isNull(); + } + + @Test + void resetGrade_shouldSetTheAssignmentUnSubmitted(){ + var studentQuizAssignment = new StudentQuizAssignment(); + + studentQuizAssignment.setSubmissionDate(ZonedDateTime.now()); + + studentQuizAssignment.resetGrades(); + + assertThat(studentQuizAssignment.hasBeenSubmitted()).isFalse(); + } +} diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/users/WithMockStudent.java b/src/test/java/fr/univ_lille/gitlab/classrooms/users/WithMockStudent.java --- a/src/test/java/fr/univ_lille/gitlab/classrooms/users/WithMockStudent.java +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/users/WithMockStudent.java @@ -1,9 +1,14 @@ package fr.univ_lille.gitlab.classrooms.users; +import org.springframework.core.annotation.AliasFor; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) @WithMockClassroomUser(username = "luke.skywalker", roles = {ClassroomRole.STUDENT}) public @interface WithMockStudent { + + String username() default "luke.skywalker"; + } diff --git a/src/test/resources/sql/init-test-users.sql b/src/test/resources/sql/init-test-users.sql --- a/src/test/resources/sql/init-test-users.sql +++ b/src/test/resources/sql/init-test-users.sql @@ -1,3 +1,4 @@ DELETE FROM classroom_user; INSERT INTO classroom_user (name, roles, avatar_url, email) VALUES ('obiwan.kenobi', 'TEACHER', 'https://jedis.net/obiwan.png', 'obiwan.kenobi@jedis.net'); INSERT INTO classroom_user (name, roles, avatar_url, email) VALUES ('luke.skywalker', 'STUDENT', 'https://rebels.net/luke.png', 'luke.skywalker@rebels.net'); +INSERT INTO classroom_user (name, roles, avatar_url, email) VALUES ('leia.organa', 'STUDENT', 'https://rebels.net/leia.png', 'leia.organa@rebels.net');