diff --git a/src/main/java/org/wise/portal/dao/teacherreflection/TeacherReflectionDao.java b/src/main/java/org/wise/portal/dao/teacherreflection/TeacherReflectionDao.java new file mode 100644 index 000000000..aa888f1b8 --- /dev/null +++ b/src/main/java/org/wise/portal/dao/teacherreflection/TeacherReflectionDao.java @@ -0,0 +1,15 @@ +package org.wise.portal.dao.teacherreflection; + +import java.util.List; + +import org.wise.portal.dao.SimpleDao; +import org.wise.portal.domain.group.Group; +import org.wise.portal.domain.run.Run; +import org.wise.vle.domain.teacherreflection.TeacherReflection; + +public interface TeacherReflectionDao extends SimpleDao { + + List getQuestions(Run run, Group period, String nodeId, String componentId); + + T getQuestion(Run run, Group period, String nodeId, String componentId, String questionId); +} diff --git a/src/main/java/org/wise/portal/dao/teacherreflection/impl/HibernateTeacherReflectionDao.java b/src/main/java/org/wise/portal/dao/teacherreflection/impl/HibernateTeacherReflectionDao.java new file mode 100644 index 000000000..ae0b38c9b --- /dev/null +++ b/src/main/java/org/wise/portal/dao/teacherreflection/impl/HibernateTeacherReflectionDao.java @@ -0,0 +1,49 @@ +package org.wise.portal.dao.teacherreflection.impl; + +import java.util.List; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + +import org.springframework.stereotype.Repository; +import org.wise.portal.dao.impl.AbstractHibernateDao; +import org.wise.portal.dao.teacherreflection.TeacherReflectionDao; +import org.wise.portal.domain.group.Group; +import org.wise.portal.domain.run.Run; +import org.wise.vle.domain.teacherreflection.TeacherReflection; + +@Repository("teacherReflectionDao") +public class HibernateTeacherReflectionDao extends AbstractHibernateDao + implements TeacherReflectionDao { + + @Override + protected Class getDataObjectClass() { + return TeacherReflection.class; + } + + @Override + public List getQuestions(Run run, Group period, String nodeId, + String componentId) { + CriteriaBuilder cb = getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(TeacherReflection.class); + Root root = cq.from(TeacherReflection.class); + cq.select(root).where(cb.equal(root.get("run"), run), cb.equal(root.get("period"), period), + cb.equal(root.get("nodeId"), nodeId), cb.equal(root.get("componentId"), componentId)) + .orderBy(cb.asc(root.get("id"))); + return entityManager.createQuery(cq).getResultList(); + } + + @Override + public TeacherReflection getQuestion(Run run, Group period, String nodeId, String componentId, + String questionId) { + CriteriaBuilder cb = getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(TeacherReflection.class); + Root root = cq.from(TeacherReflection.class); + cq.select(root).where(cb.equal(root.get("run"), run), cb.equal(root.get("period"), period), + cb.equal(root.get("nodeId"), nodeId), cb.equal(root.get("componentId"), componentId), + cb.equal(root.get("questionId"), questionId)); + List results = entityManager.createQuery(cq).getResultList(); + return results.isEmpty() ? null : results.get(0); + } +} diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherReflectionAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherReflectionAPIController.java new file mode 100644 index 000000000..65f4ae070 --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherReflectionAPIController.java @@ -0,0 +1,127 @@ +package org.wise.portal.presentation.web.controllers.teacher; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; +import org.wise.portal.dao.ObjectNotFoundException; +import org.wise.portal.domain.group.Group; +import org.wise.portal.domain.run.Run; +import org.wise.portal.domain.user.User; +import org.wise.portal.domain.workgroup.Workgroup; +import org.wise.portal.service.run.RunService; +import org.wise.portal.service.teacherreflection.TeacherReflectionService; +import org.wise.portal.service.user.UserService; +import org.wise.portal.service.workgroup.WorkgroupService; +import org.wise.vle.domain.teacherreflection.TeacherReflection; + +/** + * REST API for TeacherReflection: viewing and answering teacher reflection questions + * for a component in a period of a run. + */ +@RestController +@RequestMapping("/api/teacher/reflection") +@PreAuthorize("hasRole('ROLE_TEACHER')") +public class TeacherReflectionAPIController { + + @Autowired + private TeacherReflectionService teacherReflectionService; + + @Autowired + private RunService runService; + + @Autowired + private UserService userService; + + @Autowired + private WorkgroupService workgroupService; + + public record AnswerRequest(Long runId, Long periodId, String nodeId, String componentId, + String componentType, String questionId, String questionText, String answerText, + Long answeredByWorkgroupId) { + } + + public record TeacherReflectionResponse(Long id, Long runId, Long periodId, String nodeId, + String componentId, String componentType, String questionId, String questionText, + String answerText, Long answeredByWorkgroupId, Long createdAt, Long updatedAt) { + } + + @GetMapping + public List getQuestions(Authentication auth, @RequestParam Long runId, + @RequestParam Long periodId, @RequestParam String nodeId, @RequestParam String componentId) + throws ObjectNotFoundException { + Run run = runService.retrieveById(runId); + User user = userService.retrieveUserByUsername(auth.getName()); + if (!runService.isAllowedToViewStudentWork(run, user)) { + throw new AccessDeniedException("Not permitted"); + } + Group period = getPeriod(run, periodId); + List reflections = teacherReflectionService.getQuestions(run, period, nodeId, + componentId); + List response = new ArrayList<>(); + for (TeacherReflection reflection : reflections) { + response.add(toResponse(reflection)); + } + return response; + } + + @PutMapping("/answer") + public TeacherReflectionResponse saveAnswer(Authentication auth, + @RequestBody AnswerRequest request) throws ObjectNotFoundException { + Run run = runService.retrieveById(request.runId()); + User user = userService.retrieveUserByUsername(auth.getName()); + if (!runService.isAllowedToGradeStudentWork(run, user)) { + throw new AccessDeniedException("Not permitted"); + } + Group period = getPeriod(run, request.periodId()); + if (request.answeredByWorkgroupId() == null) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "answeredByWorkgroupId cannot be null"); + } + Workgroup teacherWorkgroup = workgroupService.retrieveById(request.answeredByWorkgroupId()); + if (teacherWorkgroup != null && !teacherWorkgroup.isTeacherWorkgroup()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "Workgroup is not a teacher workgroup"); + } + if (teacherWorkgroup != null && !run.equals(teacherWorkgroup.getRun())) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "Workgroup does not belong to the run"); + } + try { + TeacherReflection reflection = teacherReflectionService.saveAnswer(run, period, + request.nodeId(), request.componentId(), request.componentType(), request.questionId(), + request.questionText(), request.answerText(), teacherWorkgroup); + return toResponse(reflection); + } catch (IllegalArgumentException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); + } + } + + private Group getPeriod(Run run, Long periodId) { + for (Group period : run.getPeriods()) { + if (period.getId().equals(periodId)) { + return period; + } + } + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Period does not belong to the run"); + } + + private TeacherReflectionResponse toResponse(TeacherReflection reflection) { + return new TeacherReflectionResponse(reflection.getId(), reflection.getRun().getId(), + reflection.getPeriod().getId(), reflection.getNodeId(), reflection.getComponentId(), + reflection.getComponentType(), reflection.getQuestionId(), reflection.getQuestionText(), + reflection.getAnswerText(), reflection.getAnsweredByWorkgroup().getId(), + reflection.getCreatedAt().getTime(), reflection.getUpdatedAt().getTime()); + } +} diff --git a/src/main/java/org/wise/portal/service/teacherreflection/TeacherReflectionService.java b/src/main/java/org/wise/portal/service/teacherreflection/TeacherReflectionService.java new file mode 100644 index 000000000..2b6500131 --- /dev/null +++ b/src/main/java/org/wise/portal/service/teacherreflection/TeacherReflectionService.java @@ -0,0 +1,18 @@ +package org.wise.portal.service.teacherreflection; + +import java.util.List; + +import org.wise.portal.domain.group.Group; +import org.wise.portal.domain.run.Run; +import org.wise.portal.domain.workgroup.Workgroup; +import org.wise.vle.domain.teacherreflection.TeacherReflection; + +public interface TeacherReflectionService { + + List getQuestions(Run run, Group period, String nodeId, + String componentId); + + TeacherReflection saveAnswer(Run run, Group period, String nodeId, + String componentId, String componentType, String questionId, String questionText, + String answerText, Workgroup teacherWorkgroup); +} diff --git a/src/main/java/org/wise/portal/service/teacherreflection/impl/TeacherReflectionServiceImpl.java b/src/main/java/org/wise/portal/service/teacherreflection/impl/TeacherReflectionServiceImpl.java new file mode 100644 index 000000000..9447088f7 --- /dev/null +++ b/src/main/java/org/wise/portal/service/teacherreflection/impl/TeacherReflectionServiceImpl.java @@ -0,0 +1,85 @@ +package org.wise.portal.service.teacherreflection.impl; + +import java.sql.Timestamp; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wise.portal.dao.teacherreflection.TeacherReflectionDao; +import org.wise.portal.domain.group.Group; +import org.wise.portal.domain.run.Run; +import org.wise.portal.domain.workgroup.Workgroup; +import org.wise.portal.service.teacherreflection.TeacherReflectionService; +import org.wise.vle.domain.teacherreflection.TeacherReflection; + +@Service +public class TeacherReflectionServiceImpl implements TeacherReflectionService { + + private static final int MAX_ID_LENGTH = 30; + private static final int MAX_QUESTION_TEXT_LENGTH = 5000; + private static final int MAX_ANSWER_TEXT_LENGTH = 20000; + + @Autowired + private TeacherReflectionDao teacherReflectionDao; + + @Override + @Transactional(readOnly = true) + public List getQuestions(Run run, Group period, String nodeId, + String componentId) { + return teacherReflectionDao.getQuestions(run, period, nodeId, componentId); + } + + @Override + @Transactional + public TeacherReflection saveAnswer(Run run, Group period, String nodeId, String componentId, + String componentType, String questionId, String questionText, String answerText, + Workgroup teacherWorkgroup) { + validateComponent(nodeId, componentId, componentType); + if (questionId == null || questionId.isBlank() || questionId.length() > MAX_ID_LENGTH) { + throw new IllegalArgumentException("Invalid questionId"); + } + if (questionText == null || questionText.length() > MAX_QUESTION_TEXT_LENGTH) { + throw new IllegalArgumentException("Invalid questionText"); + } + if (answerText == null || answerText.length() > MAX_ANSWER_TEXT_LENGTH) { + throw new IllegalArgumentException("Invalid answerText"); + } + if (teacherWorkgroup == null) { + throw new IllegalArgumentException("teacherWorkgroup cannot be null"); + } + + Timestamp now = new Timestamp(System.currentTimeMillis()); + TeacherReflection reflection = teacherReflectionDao.getQuestion(run, period, nodeId, + componentId, questionId); + if (reflection == null) { + reflection = new TeacherReflection(); + reflection.setRun(run); + reflection.setPeriod(period); + reflection.setNodeId(nodeId); + reflection.setComponentId(componentId); + reflection.setComponentType(componentType); + reflection.setQuestionId(questionId); + reflection.setCreatedAt(now); + } + reflection.setQuestionText(questionText); + reflection.setAnswerText(answerText); + reflection.setAnsweredByWorkgroup(teacherWorkgroup); + reflection.setUpdatedAt(now); + teacherReflectionDao.save(reflection); + return reflection; + } + + private void validateComponent(String nodeId, String componentId, String componentType) { + if (nodeId == null || nodeId.isBlank() || nodeId.length() > MAX_ID_LENGTH) { + throw new IllegalArgumentException("Invalid nodeId"); + } + if (componentId == null || componentId.isBlank() || componentId.length() > MAX_ID_LENGTH) { + throw new IllegalArgumentException("Invalid componentId"); + } + if (componentType == null || componentType.isBlank() + || componentType.length() > MAX_ID_LENGTH) { + throw new IllegalArgumentException("Invalid componentType"); + } + } +} diff --git a/src/main/java/org/wise/vle/domain/teacherreflection/TeacherReflection.java b/src/main/java/org/wise/vle/domain/teacherreflection/TeacherReflection.java new file mode 100644 index 000000000..b91a86771 --- /dev/null +++ b/src/main/java/org/wise/vle/domain/teacherreflection/TeacherReflection.java @@ -0,0 +1,81 @@ +package org.wise.vle.domain.teacherreflection; + +import java.sql.Timestamp; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.Getter; +import lombok.Setter; +import org.wise.portal.domain.group.Group; +import org.wise.portal.domain.group.impl.PersistentGroup; +import org.wise.portal.domain.run.Run; +import org.wise.portal.domain.run.impl.RunImpl; +import org.wise.portal.domain.workgroup.Workgroup; +import org.wise.portal.domain.workgroup.impl.WorkgroupImpl; + +/** + * A teacher's reflection and answer for a component in a period of a run. + * Stored in teacher_reflections. + */ +@Entity(name = "teacherReflection") +@Table(name = "teacher_reflections", uniqueConstraints = @UniqueConstraint(name = "teacherReflectionsUniqueKey", columnNames = { + "runId", "periodId", "nodeId", "componentId", + "questionId" }), indexes = @Index(columnList = "runId", name = "teacherReflectionsRunIdIndex")) +@Getter +@Setter +public class TeacherReflection { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(targetEntity = RunImpl.class, fetch = FetchType.LAZY) + @JoinColumn(name = "runId", nullable = false) + @JsonIgnore + private Run run; + + @ManyToOne(targetEntity = PersistentGroup.class, fetch = FetchType.LAZY) + @JoinColumn(name = "periodId", nullable = false) + @JsonIgnore + private Group period; + + @Column(name = "nodeId", length = 30, nullable = false) + private String nodeId; + + @Column(name = "componentId", length = 30, nullable = false) + private String componentId; + + @Column(name = "componentType", length = 30, nullable = false) + private String componentType; + + @Column(name = "questionId", length = 30, nullable = false) + private String questionId; + + @Column(name = "questionText", length = 65536, columnDefinition = "text", nullable = false) + private String questionText; + + @Column(name = "answerText", length = 65536, columnDefinition = "text", nullable = false) + private String answerText; + + @ManyToOne(targetEntity = WorkgroupImpl.class, fetch = FetchType.LAZY) + @JoinColumn(name = "answeredByWorkgroupId", nullable = false) + @JsonIgnore + private Workgroup answeredByWorkgroup; + + @Column(name = "createdAt", nullable = false) + private Timestamp createdAt; + + @Column(name = "updatedAt", nullable = false) + private Timestamp updatedAt; +} diff --git a/src/main/resources/db/migration/V2__create_teacher_reflections_table.sql b/src/main/resources/db/migration/V2__create_teacher_reflections_table.sql new file mode 100644 index 000000000..25220883d --- /dev/null +++ b/src/main/resources/db/migration/V2__create_teacher_reflections_table.sql @@ -0,0 +1,20 @@ +create table teacher_reflections ( + id bigint not null auto_increment, + runId bigint not null, + periodId bigint not null, + nodeId varchar(30) not null, + componentId varchar(30) not null, + componentType varchar(30) not null, + questionId varchar(30) not null, + questionText text not null, + answerText text not null, + answeredByWorkgroupId bigint not null, + createdAt datetime(3) not null, + updatedAt datetime(3) not null, + primary key (id), + unique key teacherReflectionsUniqueKey (runId, periodId, nodeId, componentId, questionId), + index teacherReflectionsRunIdIndex (runId), + constraint teacherReflectionsRunIdFK foreign key (runId) references runs (id), + constraint teacherReflectionsPeriodIdFK foreign key (periodId) references `groups` (id), + constraint teacherReflectionsWorkgroupIdFK foreign key (answeredByWorkgroupId) references workgroups (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/src/test/java/org/wise/portal/dao/teacherreflection/impl/HibernateTeacherReflectionDaoTest.java b/src/test/java/org/wise/portal/dao/teacherreflection/impl/HibernateTeacherReflectionDaoTest.java new file mode 100644 index 000000000..9c7733e3d --- /dev/null +++ b/src/test/java/org/wise/portal/dao/teacherreflection/impl/HibernateTeacherReflectionDaoTest.java @@ -0,0 +1,154 @@ +package org.wise.portal.dao.teacherreflection.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.wise.portal.domain.authentication.Schoollevel; +import org.wise.portal.domain.group.Group; +import org.wise.portal.domain.run.Run; +import org.wise.portal.domain.user.User; +import org.wise.portal.domain.workgroup.Workgroup; +import org.wise.portal.junit.AbstractTransactionalDbTests; +import org.wise.vle.domain.teacherreflection.TeacherReflection; + +@SpringBootTest +public class HibernateTeacherReflectionDaoTest extends AbstractTransactionalDbTests { + + private Run run; + private Run otherRun; + private Group period1; + private Group period2; + private Workgroup teacherWorkgroup; + + @Autowired + private HibernateTeacherReflectionDao teacherReflectionDao; + + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + Long projectId1 = getNextAvailableProjectId(); + Long projectId2 = getNextAvailableProjectId(); + Date startTime = Calendar.getInstance().getTime(); + + User teacher = createTeacherUser("Albus", "Dumbledore", "adumbledore", "Prof. Dumbledore", + "secret", "Hogwarts", "Scotland", "UK", "albus@hogwarts.edu", "Hogwarts School", + Schoollevel.COLLEGE, "google-teacher-1"); + + run = createProjectAndRun(projectId1, "Potions 101", teacher, startTime, "POT101"); + otherRun = createProjectAndRun(projectId2, "Defense Against the Dark Arts", teacher, startTime, + "DADA101"); + + period1 = createPeriod("Period 1"); + period2 = createPeriod("Period 2"); + + Set periods = new TreeSet<>(); + periods.add(period1); + periods.add(period2); + run.setPeriods(periods); + + teacherWorkgroup = addUserToRun(teacher, run, period1); + } + + private TeacherReflection createReflection(Run run, Group period, String nodeId, + String componentId, String componentType, String questionId, String questionText, + String answerText) { + TeacherReflection r = new TeacherReflection(); + r.setRun(run); + r.setPeriod(period); + r.setNodeId(nodeId); + r.setComponentId(componentId); + r.setComponentType(componentType); + r.setQuestionId(questionId); + r.setQuestionText(questionText); + r.setAnswerText(answerText); + r.setAnsweredByWorkgroup(teacherWorkgroup); + Timestamp now = new Timestamp(System.currentTimeMillis()); + r.setCreatedAt(now); + r.setUpdatedAt(now); + teacherReflectionDao.save(r); + return r; + } + + @Test + public void getQuestion_WhenNoneExists_ReturnsNull() { + TeacherReflection r = teacherReflectionDao.getQuestion(run, period1, "node1", + "comp1", "q1"); + assertNull(r); + } + + @Test + public void saveAndGetQuestion_WhenExists_ReturnsMatching() { + TeacherReflection saved = createReflection(run, period1, "node1", "comp1", "Discussion", + "q1", "What did you observe?", "Great engagement"); + + TeacherReflection retrieved = teacherReflectionDao.getQuestion(run, period1, + "node1", "comp1", "q1"); + assertNotNull(retrieved); + assertEquals(saved.getId(), retrieved.getId()); + assertEquals(run.getId(), retrieved.getRun().getId()); + assertEquals(period1.getId(), retrieved.getPeriod().getId()); + assertEquals("node1", retrieved.getNodeId()); + assertEquals("comp1", retrieved.getComponentId()); + assertEquals("Discussion", retrieved.getComponentType()); + assertEquals("q1", retrieved.getQuestionId()); + assertEquals("What did you observe?", retrieved.getQuestionText()); + assertEquals("Great engagement", retrieved.getAnswerText()); + assertEquals(teacherWorkgroup.getId(), retrieved.getAnsweredByWorkgroup().getId()); + } + + @Test + public void getQuestion_DifferentParameters_ReturnsNull() { + createReflection(run, period1, "node1", "comp1", "Discussion", "q1", "Question 1", "Answer 1"); + + assertNull(teacherReflectionDao.getQuestion(otherRun, period1, "node1", "comp1", "q1")); + assertNull(teacherReflectionDao.getQuestion(run, period2, "node1", "comp1", "q1")); + assertNull(teacherReflectionDao.getQuestion(run, period1, "node2", "comp1", "q1")); + assertNull(teacherReflectionDao.getQuestion(run, period1, "node1", "comp2", "q1")); + assertNull(teacherReflectionDao.getQuestion(run, period1, "node1", "comp1", "q2")); + } + + @Test + public void getQuestions_MultipleQuestions_ReturnsOnlyMatching() { + createReflection(run, period1, "node1", "comp1", "Discussion", "q1", "Q1 text", "Answer 1"); + createReflection(run, period1, "node1", "comp1", "Discussion", "q2", "Q2 text", "Answer 2"); + createReflection(run, period1, "node2", "comp2", "Discussion", "q1", "Other Q", "Other Answer"); + + List list = teacherReflectionDao.getQuestions(run, period1, + "node1", "comp1"); + assertEquals(2, list.size()); + assertTrue(list.stream().anyMatch(q -> "q1".equals(q.getQuestionId()))); + assertTrue(list.stream().anyMatch(q -> "q2".equals(q.getQuestionId()))); + } + + @Test + public void saveQuestion_UpdateExisting_UpdatesFields() { + TeacherReflection r = createReflection(run, period1, "node1", "comp1", "Discussion", + "q1", "Original Q", "Initial answer"); + Long id = r.getId(); + + r.setAnswerText("Updated answer"); + r.setUpdatedAt(new Timestamp(System.currentTimeMillis())); + teacherReflectionDao.save(r); + flush(); + + TeacherReflection updated = teacherReflectionDao.getQuestion(run, period1, + "node1", "comp1", "q1"); + assertNotNull(updated); + assertEquals(id, updated.getId()); + assertEquals("Updated answer", updated.getAnswerText()); + } +} diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/APIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/APIControllerTest.java index b12a2c804..e984fd9cd 100644 --- a/src/test/java/org/wise/portal/presentation/web/controllers/APIControllerTest.java +++ b/src/test/java/org/wise/portal/presentation/web/controllers/APIControllerTest.java @@ -232,8 +232,10 @@ protected WorkgroupImpl createWorkgroup(Long id, Run run, Group period, User stu protected WorkgroupImpl createTeacherWorkgroup(Run run, User teacher) { WorkgroupImpl workgroup = new WorkgroupImpl(); + workgroup.setId(4L); workgroup.setRun(run); workgroup.addMember(teacher); + workgroup.setTeacherWorkgroup(true); return workgroup; } diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherReflectionAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherReflectionAPIControllerTest.java new file mode 100644 index 000000000..60aeeeaad --- /dev/null +++ b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherReflectionAPIControllerTest.java @@ -0,0 +1,159 @@ +package org.wise.portal.presentation.web.controllers.teacher; + +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.List; + +import org.easymock.EasyMockExtension; +import org.easymock.Mock; +import org.easymock.TestSubject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.server.ResponseStatusException; +import org.wise.portal.dao.ObjectNotFoundException; +import org.wise.portal.presentation.web.controllers.APIControllerTest; +import org.wise.portal.presentation.web.controllers.teacher.TeacherReflectionAPIController.AnswerRequest; +import org.wise.portal.presentation.web.controllers.teacher.TeacherReflectionAPIController.TeacherReflectionResponse; +import org.wise.portal.service.teacherreflection.TeacherReflectionService; +import org.wise.vle.domain.teacherreflection.TeacherReflection; + +@ExtendWith(EasyMockExtension.class) +public class TeacherReflectionAPIControllerTest extends APIControllerTest { + + @TestSubject + private TeacherReflectionAPIController controller = new TeacherReflectionAPIController(); + + @Mock + private TeacherReflectionService teacherReflectionService; + + private void expectTeacherWithPermissions(boolean canView, boolean canGrade) + throws ObjectNotFoundException { + expect(runService.retrieveById(runId1)).andReturn(run1).anyTimes(); + expect(userService.retrieveUserByUsername(teacherAuth.getName())).andReturn(teacher1) + .anyTimes(); + expect(runService.isAllowedToViewStudentWork(run1, teacher1)).andReturn(canView).anyTimes(); + expect(runService.isAllowedToGradeStudentWork(run1, teacher1)).andReturn(canGrade).anyTimes(); + expect(workgroupService.getWorkgroupListByRunAndUser(run1, teacher1)) + .andReturn(Arrays.asList(teacher1Run1Workgroup)).anyTimes(); + } + + @Test + public void getQuestions_TeacherWithoutPermission_ThrowsAccessDenied() throws Exception { + expectTeacherWithPermissions(false, false); + replay(teacherReflectionService, runService, userService, workgroupService); + + assertThrows(AccessDeniedException.class, () -> controller.getQuestions(teacherAuth, runId1, + run1Period1Id, run1Node1Id, run1Component1Id)); + } + + @Test + public void getQuestions_PeriodNotInRun_ThrowsBadRequest() throws Exception { + expectTeacherWithPermissions(true, false); + replay(teacherReflectionService, runService, userService, workgroupService); + + assertThrows(ResponseStatusException.class, () -> controller.getQuestions(teacherAuth, runId1, + run2Period2Id, run1Node1Id, run1Component1Id)); + } + + @Test + public void getQuestions_Success_ReturnsList() throws Exception { + expectTeacherWithPermissions(true, false); + TeacherReflection r = new TeacherReflection(); + r.setId(10L); + r.setRun(run1); + r.setPeriod(run1Period1); + r.setNodeId(run1Node1Id); + r.setComponentId(run1Component1Id); + r.setComponentType("Discussion"); + r.setQuestionId("q1"); + r.setQuestionText("What did students say?"); + r.setAnswerText("Varied thoughts"); + r.setAnsweredByWorkgroup(teacher1Run1Workgroup); + Timestamp now = new Timestamp(System.currentTimeMillis()); + r.setCreatedAt(now); + r.setUpdatedAt(now); + + expect(teacherReflectionService.getQuestions(run1, run1Period1, run1Node1Id, run1Component1Id)) + .andReturn(Arrays.asList(r)); + replay(teacherReflectionService, runService, userService, workgroupService); + + List res = controller.getQuestions(teacherAuth, runId1, + run1Period1Id, run1Node1Id, run1Component1Id); + assertEquals(1, res.size()); + assertEquals("q1", res.get(0).questionId()); + assertEquals("What did students say?", res.get(0).questionText()); + assertEquals("Varied thoughts", res.get(0).answerText()); + } + + @Test + public void saveAnswer_TeacherWithoutPermission_ThrowsAccessDenied() throws Exception { + expectTeacherWithPermissions(true, false); + replay(teacherReflectionService, runService, userService, workgroupService); + + AnswerRequest req = new AnswerRequest(runId1, run1Period1Id, run1Node1Id, run1Component1Id, + "Discussion", "q1", "Question text", "Answer text", teacher1Run1Workgroup.getId()); + assertThrows(AccessDeniedException.class, () -> controller.saveAnswer(teacherAuth, req)); + } + + @Test + public void saveAnswer_NullAnsweredByWorkgroupId_ThrowsBadRequest() throws Exception { + expectTeacherWithPermissions(true, true); + replay(teacherReflectionService, runService, userService, workgroupService); + + AnswerRequest req = new AnswerRequest(runId1, run1Period1Id, run1Node1Id, run1Component1Id, + "Discussion", "q1", "Question text", "Answer text", null); + assertThrows(ResponseStatusException.class, () -> controller.saveAnswer(teacherAuth, req)); + } + + @Test + public void saveAnswer_WorkgroupNotInRun_ThrowsBadRequest() throws Exception { + expectTeacherWithPermissions(true, true); + expect(workgroupService.retrieveById(workgroup3Id)).andReturn(workgroup3); + replay(teacherReflectionService, runService, userService, workgroupService); + + AnswerRequest req = new AnswerRequest(runId1, run1Period1Id, run1Node1Id, run1Component1Id, + "Discussion", "q1", "Question text", "Answer text", workgroup3Id); + assertThrows(ResponseStatusException.class, () -> controller.saveAnswer(teacherAuth, req)); + } + + @Test + public void saveAnswer_Success_ReturnsResponse() throws Exception { + expectTeacherWithPermissions(true, true); + TeacherReflection r = new TeacherReflection(); + r.setId(10L); + r.setRun(run1); + r.setPeriod(run1Period1); + r.setNodeId(run1Node1Id); + r.setComponentId(run1Component1Id); + r.setComponentType("Discussion"); + r.setQuestionId("q1"); + r.setQuestionText("Question text"); + r.setAnswerText("Answer text"); + r.setAnsweredByWorkgroup(teacher1Run1Workgroup); + Timestamp now = new Timestamp(System.currentTimeMillis()); + r.setCreatedAt(now); + r.setUpdatedAt(now); + + expect(workgroupService.retrieveById(teacher1Run1Workgroup.getId())) + .andReturn(teacher1Run1Workgroup); + expect(teacherReflectionService.saveAnswer(run1, run1Period1, run1Node1Id, run1Component1Id, + "Discussion", "q1", "Question text", "Answer text", teacher1Run1Workgroup)).andReturn(r); + replay(teacherReflectionService, runService, userService, workgroupService); + + AnswerRequest req = new AnswerRequest(runId1, run1Period1Id, run1Node1Id, run1Component1Id, + "Discussion", "q1", "Question text", "Answer text", teacher1Run1Workgroup.getId()); + TeacherReflectionResponse res = controller.saveAnswer(teacherAuth, req); + assertNotNull(res); + assertEquals("q1", res.questionId()); + assertEquals("Question text", res.questionText()); + assertEquals("Answer text", res.answerText()); + assertEquals(teacher1Run1Workgroup.getId(), res.answeredByWorkgroupId()); + } +} diff --git a/src/test/java/org/wise/portal/service/teacherreflection/impl/TeacherReflectionServiceImplTest.java b/src/test/java/org/wise/portal/service/teacherreflection/impl/TeacherReflectionServiceImplTest.java new file mode 100644 index 000000000..38e33bbea --- /dev/null +++ b/src/test/java/org/wise/portal/service/teacherreflection/impl/TeacherReflectionServiceImplTest.java @@ -0,0 +1,118 @@ +package org.wise.portal.service.teacherreflection.impl; + +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.replay; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.List; + +import org.easymock.EasyMockExtension; +import org.easymock.Mock; +import org.easymock.TestSubject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.wise.portal.dao.teacherreflection.TeacherReflectionDao; +import org.wise.portal.domain.group.impl.PersistentGroup; +import org.wise.portal.domain.run.impl.RunImpl; +import org.wise.portal.domain.workgroup.impl.WorkgroupImpl; +import org.wise.vle.domain.teacherreflection.TeacherReflection; + +@ExtendWith(EasyMockExtension.class) +public class TeacherReflectionServiceImplTest { + + @TestSubject + private TeacherReflectionServiceImpl service = new TeacherReflectionServiceImpl(); + + @Mock + private TeacherReflectionDao teacherReflectionDao; + + private RunImpl run = new RunImpl(); + private PersistentGroup period = new PersistentGroup(); + private WorkgroupImpl teacherWorkgroup = new WorkgroupImpl(); + + @BeforeEach + public void setUp() { + run.setId(1L); + period.setId(10L); + teacherWorkgroup.setId(20L); + } + + @Test + public void getQuestions_DelegatesToDao() { + TeacherReflection r = new TeacherReflection(); + expect(teacherReflectionDao.getQuestions(run, period, "node1", "comp1")) + .andReturn(Arrays.asList(r)); + replay(teacherReflectionDao); + + List result = service.getQuestions(run, period, "node1", "comp1"); + assertEquals(1, result.size()); + assertEquals(r, result.get(0)); + } + + @Test + public void saveAnswer_InvalidQuestionId_ThrowsIllegalArgument() { + replay(teacherReflectionDao); + assertThrows(IllegalArgumentException.class, () -> service.saveAnswer(run, period, "node1", + "comp1", "Discussion", " ", "Why?", "Answer", teacherWorkgroup)); + } + + @Test + public void saveAnswer_QuestionTextTooLong_ThrowsIllegalArgument() { + replay(teacherReflectionDao); + assertThrows(IllegalArgumentException.class, () -> service.saveAnswer(run, period, "node1", + "comp1", "Discussion", "q1", "x".repeat(5001), "Answer", teacherWorkgroup)); + } + + @Test + public void saveAnswer_NullTeacherWorkgroup_ThrowsIllegalArgument() { + replay(teacherReflectionDao); + assertThrows(IllegalArgumentException.class, () -> service.saveAnswer(run, period, "node1", + "comp1", "Discussion", "q1", "Question text", "Answer text", null)); + } + + @Test + public void saveAnswer_NewQuestion_CreatesAndSaves() { + expect(teacherReflectionDao.getQuestion(run, period, "node1", "comp1", "q1")) + .andReturn(null); + teacherReflectionDao.save(anyObject(TeacherReflection.class)); + expectLastCall(); + replay(teacherReflectionDao); + + TeacherReflection r = service.saveAnswer(run, period, "node1", "comp1", "Discussion", + "q1", "Question text", "Answer text", teacherWorkgroup); + assertNotNull(r); + assertEquals("q1", r.getQuestionId()); + assertEquals("Question text", r.getQuestionText()); + assertEquals("Answer text", r.getAnswerText()); + assertEquals("Discussion", r.getComponentType()); + assertEquals(teacherWorkgroup, r.getAnsweredByWorkgroup()); + } + + @Test + public void saveAnswer_ExistingQuestion_UpdatesFields() { + TeacherReflection existing = new TeacherReflection(); + existing.setId(5L); + existing.setQuestionId("q1"); + existing.setQuestionText("Old question"); + existing.setAnswerText("Old answer"); + + expect(teacherReflectionDao.getQuestion(run, period, "node1", "comp1", "q1")) + .andReturn(existing); + teacherReflectionDao.save(existing); + expectLastCall(); + replay(teacherReflectionDao); + + TeacherReflection r = service.saveAnswer(run, period, "node1", "comp1", "Discussion", + "q1", "New question", "New answer", teacherWorkgroup); + assertEquals(5L, r.getId()); + assertEquals("New question", r.getQuestionText()); + assertEquals("New answer", r.getAnswerText()); + assertEquals(teacherWorkgroup, r.getAnsweredByWorkgroup()); + } +}