From 5c184cee6d846f017c5b750c8611606ec643783c Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Thu, 8 Oct 2026 11:09:48 -0700 Subject: [PATCH 1/5] feat: add TeacherPresentation config and reflection answers API --- .../TeacherPresentationConfigDao.java | 25 ++ ...HibernateTeacherPresentationConfigDao.java | 74 ++++++ ...eacherPresentationConfigAPIController.java | 172 ++++++++++++++ .../TeacherPresentationConfigService.java | 46 ++++ .../TeacherPresentationConfigServiceImpl.java | 218 ++++++++++++++++++ .../TeacherPresentationConfig.java | 89 +++++++ .../TeacherPresentationReflectionAnswer.java | 64 +++++ ...V2__create_teacher_presentation_tables.sql | 35 +++ ...erPresentationConfigAPIControllerTest.java | 165 +++++++++++++ ...cherPresentationConfigServiceImplTest.java | 166 +++++++++++++ 10 files changed, 1054 insertions(+) create mode 100644 src/main/java/org/wise/portal/dao/teacherpresentation/TeacherPresentationConfigDao.java create mode 100644 src/main/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDao.java create mode 100644 src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIController.java create mode 100644 src/main/java/org/wise/portal/service/teacherpresentation/TeacherPresentationConfigService.java create mode 100644 src/main/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImpl.java create mode 100644 src/main/java/org/wise/vle/domain/teacherpresentation/TeacherPresentationConfig.java create mode 100644 src/main/java/org/wise/vle/domain/teacherpresentation/TeacherPresentationReflectionAnswer.java create mode 100644 src/main/resources/db/migration/V2__create_teacher_presentation_tables.sql create mode 100644 src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIControllerTest.java create mode 100644 src/test/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImplTest.java diff --git a/src/main/java/org/wise/portal/dao/teacherpresentation/TeacherPresentationConfigDao.java b/src/main/java/org/wise/portal/dao/teacherpresentation/TeacherPresentationConfigDao.java new file mode 100644 index 000000000..57a4ef54a --- /dev/null +++ b/src/main/java/org/wise/portal/dao/teacherpresentation/TeacherPresentationConfigDao.java @@ -0,0 +1,25 @@ +package org.wise.portal.dao.teacherpresentation; + +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.teacherpresentation.TeacherPresentationConfig; +import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; + +public interface TeacherPresentationConfigDao + extends SimpleDao { + + /** + * @return the config for the given run, period, node and component, or null if none exists + */ + TeacherPresentationConfig getConfig(Run run, Group period, String nodeId, String componentId); + + List getAnswers(TeacherPresentationConfig config); + + TeacherPresentationReflectionAnswer getAnswer(TeacherPresentationConfig config, + String questionId); + + void saveAnswer(TeacherPresentationReflectionAnswer answer); +} diff --git a/src/main/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDao.java b/src/main/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDao.java new file mode 100644 index 000000000..5bf015b53 --- /dev/null +++ b/src/main/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDao.java @@ -0,0 +1,74 @@ +package org.wise.portal.dao.teacherpresentation.impl; + +import java.util.List; + +import jakarta.persistence.TypedQuery; +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.teacherpresentation.TeacherPresentationConfigDao; +import org.wise.portal.domain.group.Group; +import org.wise.portal.domain.run.Run; +import org.wise.vle.domain.teacherpresentation.TeacherPresentationConfig; +import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; + +@Repository("teacherPresentationConfigDao") +public class HibernateTeacherPresentationConfigDao + extends AbstractHibernateDao + implements TeacherPresentationConfigDao { + + @Override + protected Class getDataObjectClass() { + return TeacherPresentationConfig.class; + } + + @Override + public TeacherPresentationConfig getConfig(Run run, Group period, String nodeId, + String componentId) { + CriteriaBuilder cb = getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(TeacherPresentationConfig.class); + Root root = cq.from(TeacherPresentationConfig.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)); + List results = entityManager.createQuery(cq).getResultList(); + return results.isEmpty() ? null : results.get(0); + } + + @Override + public List getAnswers(TeacherPresentationConfig config) { + CriteriaBuilder cb = getCriteriaBuilder(); + CriteriaQuery cq = cb + .createQuery(TeacherPresentationReflectionAnswer.class); + Root root = cq + .from(TeacherPresentationReflectionAnswer.class); + cq.select(root).where(cb.equal(root.get("teacherPresentationConfig"), config)); + return entityManager.createQuery(cq).getResultList(); + } + + @Override + public TeacherPresentationReflectionAnswer getAnswer(TeacherPresentationConfig config, + String questionId) { + CriteriaBuilder cb = getCriteriaBuilder(); + CriteriaQuery cq = cb + .createQuery(TeacherPresentationReflectionAnswer.class); + Root root = cq + .from(TeacherPresentationReflectionAnswer.class); + cq.select(root).where(cb.equal(root.get("teacherPresentationConfig"), config), + cb.equal(root.get("questionId"), questionId)); + TypedQuery query = entityManager.createQuery(cq); + List results = query.getResultList(); + return results.isEmpty() ? null : results.get(0); + } + + @Override + public void saveAnswer(TeacherPresentationReflectionAnswer answer) { + if (answer.getId() == null) { + entityManager.persist(answer); + } else { + entityManager.merge(answer); + } + } +} diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIController.java new file mode 100644 index 000000000..30aba3822 --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIController.java @@ -0,0 +1,172 @@ +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.teacherpresentation.TeacherPresentationConfigService; +import org.wise.portal.service.user.UserService; +import org.wise.portal.service.workgroup.WorkgroupService; +import org.wise.vle.domain.teacherpresentation.TeacherPresentationConfig; +import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; + +/** + * REST API for TeacherPresentation: the teacher's selected student work, student name display + * setting, prompt and reflection answers for a component in a period. + */ +@RestController +@RequestMapping("/api/teacher/presentation-config") +@PreAuthorize("hasRole('ROLE_TEACHER')") +public class TeacherPresentationConfigAPIController { + + @Autowired + private TeacherPresentationConfigService teacherPresentationConfigService; + + @Autowired + private RunService runService; + + @Autowired + private UserService userService; + + @Autowired + private WorkgroupService workgroupService; + + public record Item(Integer studentWorkId) { + } + + public record ConfigRequest(Long runId, Long periodId, String nodeId, String componentId, + String componentType, List items, String studentNamesDisplay, String prompt) { + } + + public record AnswerRequest(Long runId, Long periodId, String nodeId, String componentId, + String componentType, String questionId, String questionText, String answerText) { + } + + public record AnswerResponse(String questionId, String questionText, String answerText, + Long answeredByWorkgroupId, Long updatedAt) { + } + + public record ConfigResponse(Long id, Long runId, Long periodId, String nodeId, + String componentId, String componentType, List items, String studentNamesDisplay, + String prompt, Long updatedByWorkgroupId, Long updatedAt, List answers) { + } + + @GetMapping + ConfigResponse getConfig(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); + TeacherPresentationConfig config = teacherPresentationConfigService.getConfig(run, period, + nodeId, componentId); + if (config == null) { + return new ConfigResponse(null, runId, periodId, nodeId, componentId, null, new ArrayList<>(), + TeacherPresentationConfig.NAMES_HIDE, null, null, null, new ArrayList<>()); + } + return toResponse(config); + } + + @PutMapping + ConfigResponse saveConfig(Authentication auth, @RequestBody ConfigRequest 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()); + List studentWorkIds = new ArrayList<>(); + if (request.items() != null) { + for (Item item : request.items()) { + studentWorkIds.add(item == null ? null : item.studentWorkId()); + } + } + try { + TeacherPresentationConfig config = teacherPresentationConfigService.saveConfig(run, period, + request.nodeId(), request.componentId(), request.componentType(), studentWorkIds, + request.studentNamesDisplay(), request.prompt(), getTeacherWorkgroup(run, user)); + return toResponse(config); + } catch (IllegalArgumentException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); + } + } + + @PutMapping("/answers") + AnswerResponse 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()); + try { + TeacherPresentationReflectionAnswer answer = teacherPresentationConfigService.saveAnswer(run, + period, request.nodeId(), request.componentId(), request.componentType(), + request.questionId(), request.questionText(), request.answerText(), + getTeacherWorkgroup(run, user)); + return toAnswerResponse(answer); + } 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 Workgroup getTeacherWorkgroup(Run run, User user) { + List workgroups = workgroupService.getWorkgroupListByRunAndUser(run, user); + return workgroups.isEmpty() ? null : workgroups.get(0); + } + + private ConfigResponse toResponse(TeacherPresentationConfig config) { + List items = new ArrayList<>(); + for (Integer id : teacherPresentationConfigService.getStudentWorkIds(config)) { + items.add(new Item(id)); + } + List answers = new ArrayList<>(); + for (TeacherPresentationReflectionAnswer answer : teacherPresentationConfigService + .getAnswers(config)) { + answers.add(toAnswerResponse(answer)); + } + Workgroup updatedBy = config.getUpdatedByWorkgroup(); + return new ConfigResponse(config.getId(), config.getRun().getId(), config.getPeriod().getId(), + config.getNodeId(), config.getComponentId(), config.getComponentType(), items, + config.getStudentNamesDisplay(), config.getPrompt(), + updatedBy == null ? null : updatedBy.getId(), config.getUpdatedAt().getTime(), answers); + } + + private AnswerResponse toAnswerResponse(TeacherPresentationReflectionAnswer answer) { + Workgroup answeredBy = answer.getAnsweredByWorkgroup(); + return new AnswerResponse(answer.getQuestionId(), answer.getQuestionText(), + answer.getAnswerText(), answeredBy == null ? null : answeredBy.getId(), + answer.getUpdatedAt().getTime()); + } +} diff --git a/src/main/java/org/wise/portal/service/teacherpresentation/TeacherPresentationConfigService.java b/src/main/java/org/wise/portal/service/teacherpresentation/TeacherPresentationConfigService.java new file mode 100644 index 000000000..6eef6a227 --- /dev/null +++ b/src/main/java/org/wise/portal/service/teacherpresentation/TeacherPresentationConfigService.java @@ -0,0 +1,46 @@ +package org.wise.portal.service.teacherpresentation; + +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.teacherpresentation.TeacherPresentationConfig; +import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; + +public interface TeacherPresentationConfigService { + + int MAX_ITEMS = 500; + int MAX_PROMPT_LENGTH = 1000; + + /** + * @return the config, or null if the teacher has not saved one yet + */ + TeacherPresentationConfig getConfig(Run run, Group period, String nodeId, String componentId); + + List getAnswers(TeacherPresentationConfig config); + + List getStudentWorkIds(TeacherPresentationConfig config); + + /** + * Creates or updates the config for the run, period, node and component. + * + * @param prompt the teacher's prompt. null or blank resets to the component default. + * @param studentNamesDisplay one of show, hide or anonymize. null keeps the current value (or + * hide for a new config). + * @throws IllegalArgumentException if the input is invalid + */ + TeacherPresentationConfig saveConfig(Run run, Group period, String nodeId, String componentId, + String componentType, List studentWorkIds, String studentNamesDisplay, + String prompt, Workgroup teacherWorkgroup); + + /** + * Creates or updates the answer to a reflection question. Creates the config if it does not + * exist yet. + * + * @throws IllegalArgumentException if the input is invalid + */ + TeacherPresentationReflectionAnswer 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/teacherpresentation/impl/TeacherPresentationConfigServiceImpl.java b/src/main/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImpl.java new file mode 100644 index 000000000..1c9bcf165 --- /dev/null +++ b/src/main/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImpl.java @@ -0,0 +1,218 @@ +package org.wise.portal.service.teacherpresentation.impl; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wise.portal.dao.ObjectNotFoundException; +import org.wise.portal.dao.teacherpresentation.TeacherPresentationConfigDao; +import org.wise.portal.dao.work.StudentWorkDao; +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.teacherpresentation.TeacherPresentationConfigService; +import org.wise.vle.domain.teacherpresentation.TeacherPresentationConfig; +import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; +import org.wise.vle.domain.work.StudentWork; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Service +public class TeacherPresentationConfigServiceImpl implements TeacherPresentationConfigService { + + 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; + private static final Set NAMES_DISPLAY_OPTIONS = Set.of( + TeacherPresentationConfig.NAMES_SHOW, TeacherPresentationConfig.NAMES_HIDE, + TeacherPresentationConfig.NAMES_ANONYMIZE); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired + private TeacherPresentationConfigDao teacherPresentationConfigDao; + + @Autowired + private StudentWorkDao studentWorkDao; + + @Override + @Transactional(readOnly = true) + public TeacherPresentationConfig getConfig(Run run, Group period, String nodeId, + String componentId) { + return teacherPresentationConfigDao.getConfig(run, period, nodeId, componentId); + } + + @Override + @Transactional(readOnly = true) + public List getAnswers(TeacherPresentationConfig config) { + return teacherPresentationConfigDao.getAnswers(config); + } + + @Override + public List getStudentWorkIds(TeacherPresentationConfig config) { + try { + List> items = objectMapper.readValue(config.getItems(), + new TypeReference>>() { + }); + List ids = new ArrayList<>(); + for (Map item : items) { + ids.add(item.get("studentWorkId")); + } + return ids; + } catch (JsonProcessingException e) { + return new ArrayList<>(); + } + } + + @Override + @Transactional + public TeacherPresentationConfig saveConfig(Run run, Group period, String nodeId, + String componentId, String componentType, List studentWorkIds, + String studentNamesDisplay, String prompt, Workgroup teacherWorkgroup) { + validateComponent(nodeId, componentId, componentType); + if (studentNamesDisplay != null && !NAMES_DISPLAY_OPTIONS.contains(studentNamesDisplay)) { + throw new IllegalArgumentException("Invalid studentNamesDisplay: " + studentNamesDisplay); + } + String normalizedPrompt = normalizePrompt(prompt); + Set uniqueIds = validateStudentWorkIds(run, period, studentWorkIds); + TeacherPresentationConfig config = getOrCreateConfig(run, period, nodeId, componentId, + componentType); + config.setItems(serializeItems(uniqueIds)); + if (studentNamesDisplay != null) { + config.setStudentNamesDisplay(studentNamesDisplay); + } + config.setPrompt(normalizedPrompt); + config.setUpdatedByWorkgroup(teacherWorkgroup); + config.setUpdatedAt(new Timestamp(System.currentTimeMillis())); + teacherPresentationConfigDao.save(config); + return config; + } + + @Override + @Transactional + public TeacherPresentationReflectionAnswer 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("answerText is too long"); + } + TeacherPresentationConfig config = getOrCreateConfig(run, period, nodeId, componentId, + componentType); + if (config.getId() == null) { + teacherPresentationConfigDao.save(config); + } + Timestamp now = new Timestamp(System.currentTimeMillis()); + TeacherPresentationReflectionAnswer answer = teacherPresentationConfigDao.getAnswer(config, + questionId); + if (answer == null) { + answer = new TeacherPresentationReflectionAnswer(); + answer.setTeacherPresentationConfig(config); + answer.setQuestionId(questionId); + answer.setCreatedAt(now); + } + answer.setQuestionText(questionText); + answer.setAnswerText(answerText); + answer.setAnsweredByWorkgroup(teacherWorkgroup); + answer.setUpdatedAt(now); + teacherPresentationConfigDao.saveAnswer(answer); + return answer; + } + + private TeacherPresentationConfig getOrCreateConfig(Run run, Group period, String nodeId, + String componentId, String componentType) { + TeacherPresentationConfig config = teacherPresentationConfigDao.getConfig(run, period, nodeId, + componentId); + if (config == null) { + Timestamp now = new Timestamp(System.currentTimeMillis()); + config = new TeacherPresentationConfig(); + config.setRun(run); + config.setPeriod(period); + config.setNodeId(nodeId); + config.setComponentId(componentId); + config.setComponentType(componentType); + config.setCreatedAt(now); + config.setUpdatedAt(now); + } + return config; + } + + 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"); + } + } + + private String normalizePrompt(String prompt) { + if (prompt == null || prompt.isBlank()) { + return null; + } + if (prompt.length() > MAX_PROMPT_LENGTH) { + throw new IllegalArgumentException("prompt is too long"); + } + return prompt; + } + + /** + * Each student work must belong to the run and period. It is not required to belong to the + * component, because discussion posts can come from connected components. + */ + private Set validateStudentWorkIds(Run run, Group period, List ids) { + Set uniqueIds = new LinkedHashSet<>(); + if (ids != null) { + uniqueIds.addAll(ids); + } + if (uniqueIds.size() > MAX_ITEMS) { + throw new IllegalArgumentException("Too many items. The maximum is " + MAX_ITEMS); + } + for (Integer id : uniqueIds) { + if (id == null) { + throw new IllegalArgumentException("Invalid studentWorkId"); + } + StudentWork work; + try { + work = studentWorkDao.getById(id); + } catch (ObjectNotFoundException e) { + throw new IllegalArgumentException("Unknown studentWorkId: " + id); + } + if (work == null || !work.getRun().getId().equals(run.getId()) + || !work.getPeriod().getId().equals(period.getId())) { + throw new IllegalArgumentException("Unknown studentWorkId: " + id); + } + } + return uniqueIds; + } + + private String serializeItems(Set studentWorkIds) { + List> items = new ArrayList<>(); + for (Integer id : studentWorkIds) { + items.add(Map.of("studentWorkId", id)); + } + try { + return objectMapper.writeValueAsString(items); + } catch (JsonProcessingException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/src/main/java/org/wise/vle/domain/teacherpresentation/TeacherPresentationConfig.java b/src/main/java/org/wise/vle/domain/teacherpresentation/TeacherPresentationConfig.java new file mode 100644 index 000000000..34e07aa6e --- /dev/null +++ b/src/main/java/org/wise/vle/domain/teacherpresentation/TeacherPresentationConfig.java @@ -0,0 +1,89 @@ +package org.wise.vle.domain.teacherpresentation; + +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 TeacherPresentation configuration for one component in one period of a run. It + * stores which student work items (e.g. discussion posts and comments) the teacher chose to + * present, how student names are displayed, and an optional override of the default prompt. + */ +@Entity(name = "teacherPresentationConfig") +@Table(name = "teacher_presentation_config", + uniqueConstraints = @UniqueConstraint(name = "teacherPresentationConfigUniqueKey", + columnNames = { "runId", "periodId", "nodeId", "componentId" }), + indexes = @Index(columnList = "runId", name = "teacherPresentationConfigRunIdIndex")) +@Getter +@Setter +public class TeacherPresentationConfig { + + public static final String NAMES_SHOW = "show"; + public static final String NAMES_HIDE = "hide"; + public static final String NAMES_ANONYMIZE = "anonymize"; + + @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; + + /** JSON array of the form [{"studentWorkId": 123}, ...] */ + @Column(name = "items", length = 65536, columnDefinition = "text", nullable = false) + private String items = "[]"; + + @Column(name = "studentNamesDisplay", length = 30, nullable = false) + private String studentNamesDisplay = NAMES_HIDE; + + /** The teacher's edited prompt. null means use the component's default prompt. */ + @Column(name = "prompt", length = 65536, columnDefinition = "text") + private String prompt; + + @ManyToOne(targetEntity = WorkgroupImpl.class, fetch = FetchType.LAZY) + @JoinColumn(name = "updatedByWorkgroupId") + @JsonIgnore + private Workgroup updatedByWorkgroup; + + @Column(name = "createdAt", nullable = false) + private Timestamp createdAt; + + @Column(name = "updatedAt", nullable = false) + private Timestamp updatedAt; +} diff --git a/src/main/java/org/wise/vle/domain/teacherpresentation/TeacherPresentationReflectionAnswer.java b/src/main/java/org/wise/vle/domain/teacherpresentation/TeacherPresentationReflectionAnswer.java new file mode 100644 index 000000000..e44e54403 --- /dev/null +++ b/src/main/java/org/wise/vle/domain/teacherpresentation/TeacherPresentationReflectionAnswer.java @@ -0,0 +1,64 @@ +package org.wise.vle.domain.teacherpresentation; + +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.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.Getter; +import lombok.Setter; +import org.wise.portal.domain.workgroup.Workgroup; +import org.wise.portal.domain.workgroup.impl.WorkgroupImpl; + +/** + * A teacher's answer to one reflection question of a TeacherPresentation. Only the latest answer + * per question is kept. The question text is copied at answer time so research data survives later + * edits to the question. + */ +@Entity(name = "teacherPresentationReflectionAnswer") +@Table(name = "teacher_presentation_reflection_answers", + uniqueConstraints = @UniqueConstraint( + name = "teacherPresentationReflectionAnswersUniqueKey", + columnNames = { "teacherPresentationConfigId", "questionId" })) +@Getter +@Setter +public class TeacherPresentationReflectionAnswer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "teacherPresentationConfigId", nullable = false) + @JsonIgnore + private TeacherPresentationConfig teacherPresentationConfig; + + @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") + private String answerText; + + @ManyToOne(targetEntity = WorkgroupImpl.class, fetch = FetchType.LAZY) + @JoinColumn(name = "answeredByWorkgroupId") + @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_presentation_tables.sql b/src/main/resources/db/migration/V2__create_teacher_presentation_tables.sql new file mode 100644 index 000000000..157005495 --- /dev/null +++ b/src/main/resources/db/migration/V2__create_teacher_presentation_tables.sql @@ -0,0 +1,35 @@ +create table teacher_presentation_config ( + 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, + items text not null, + studentNamesDisplay varchar(30) not null default 'hide', + prompt text, + updatedByWorkgroupId bigint, + createdAt datetime(3) not null, + updatedAt datetime(3) not null, + primary key (id), + unique key teacherPresentationConfigUniqueKey (runId, periodId, nodeId, componentId), + index teacherPresentationConfigRunIdIndex (runId), + constraint teacherPresentationConfigRunIdFK foreign key (runId) references runs (id), + constraint teacherPresentationConfigPeriodIdFK foreign key (periodId) references `groups` (id), + constraint teacherPresentationConfigUpdatedByWorkgroupIdFK foreign key (updatedByWorkgroupId) references workgroups (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +create table teacher_presentation_reflection_answers ( + id bigint not null auto_increment, + teacherPresentationConfigId bigint not null, + questionId varchar(30) not null, + questionText text not null, + answerText text, + answeredByWorkgroupId bigint, + createdAt datetime(3) not null, + updatedAt datetime(3) not null, + primary key (id), + unique key teacherPresentationReflectionAnswersUniqueKey (teacherPresentationConfigId, questionId), + constraint teacherPresentationReflectionAnswersConfigIdFK foreign key (teacherPresentationConfigId) references teacher_presentation_config (id) on delete cascade, + constraint teacherPresentationReflectionAnswersWorkgroupIdFK foreign key (answeredByWorkgroupId) references workgroups (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIControllerTest.java new file mode 100644 index 000000000..9c5ba41ae --- /dev/null +++ b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIControllerTest.java @@ -0,0 +1,165 @@ +package org.wise.portal.presentation.web.controllers.teacher; + +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.isNull; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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.http.HttpStatus; +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.TeacherPresentationConfigAPIController.AnswerRequest; +import org.wise.portal.presentation.web.controllers.teacher.TeacherPresentationConfigAPIController.AnswerResponse; +import org.wise.portal.presentation.web.controllers.teacher.TeacherPresentationConfigAPIController.ConfigRequest; +import org.wise.portal.presentation.web.controllers.teacher.TeacherPresentationConfigAPIController.ConfigResponse; +import org.wise.portal.presentation.web.controllers.teacher.TeacherPresentationConfigAPIController.Item; +import org.wise.portal.service.teacherpresentation.TeacherPresentationConfigService; +import org.wise.vle.domain.teacherpresentation.TeacherPresentationConfig; +import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; + +@ExtendWith(EasyMockExtension.class) +public class TeacherPresentationConfigAPIControllerTest extends APIControllerTest { + + @TestSubject + private TeacherPresentationConfigAPIController controller = new TeacherPresentationConfigAPIController(); + + @Mock + private TeacherPresentationConfigService teacherPresentationConfigService; + + 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(); + } + + private ConfigRequest request(Long periodId, List items, String namesDisplay, + String prompt) { + return new ConfigRequest(runId1, periodId, run1Node1Id, run1Component1Id, "Discussion", items, + namesDisplay, prompt); + } + + @Test + public void getConfig_NoConfigSaved_ReturnDefaults() throws Exception { + expectTeacherWithPermissions(true, true); + expect(teacherPresentationConfigService.getConfig(run1, run1Period1, run1Node1Id, + run1Component1Id)).andReturn(null); + replay(runService, userService, workgroupService, teacherPresentationConfigService); + ConfigResponse response = controller.getConfig(teacherAuth, runId1, run1Period1Id, + run1Node1Id, run1Component1Id); + assertTrue(response.items().isEmpty()); + assertEquals("hide", response.studentNamesDisplay()); + assertNull(response.prompt()); + assertTrue(response.answers().isEmpty()); + verify(teacherPresentationConfigService); + } + + @Test + public void getConfig_NoViewPermission_ThrowAccessDenied() throws Exception { + expectTeacherWithPermissions(false, false); + replay(runService, userService, workgroupService, teacherPresentationConfigService); + assertThrows(AccessDeniedException.class, () -> controller.getConfig(teacherAuth, runId1, + run1Period1Id, run1Node1Id, run1Component1Id)); + } + + @Test + public void getConfig_PeriodNotInRun_ThrowBadRequest() throws Exception { + expectTeacherWithPermissions(true, true); + replay(runService, userService, workgroupService, teacherPresentationConfigService); + ResponseStatusException e = assertThrows(ResponseStatusException.class, + () -> controller.getConfig(teacherAuth, runId1, 99999L, run1Node1Id, run1Component1Id)); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); + } + + @Test + public void saveConfig_NoGradePermission_ThrowAccessDenied() throws Exception { + expectTeacherWithPermissions(true, false); + replay(runService, userService, workgroupService, teacherPresentationConfigService); + assertThrows(AccessDeniedException.class, () -> controller.saveConfig(teacherAuth, + request(run1Period1Id, Arrays.asList(new Item(1)), "hide", null))); + } + + @Test + public void saveConfig_ServiceRejectsInput_ThrowBadRequest() throws Exception { + expectTeacherWithPermissions(true, true); + expect(teacherPresentationConfigService.saveConfig(eq(run1), eq(run1Period1), + eq(run1Node1Id), eq(run1Component1Id), eq("Discussion"), anyObject(), eq("bogus"), + isNull(), eq(teacher1Run1Workgroup))).andThrow(new IllegalArgumentException("Invalid")); + replay(runService, userService, workgroupService, teacherPresentationConfigService); + ResponseStatusException e = assertThrows(ResponseStatusException.class, () -> controller + .saveConfig(teacherAuth, request(run1Period1Id, Arrays.asList(new Item(1)), "bogus", null))); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); + } + + @Test + public void saveConfig_ValidRequest_SaveWithTeacherWorkgroupAndReturnConfig() throws Exception { + expectTeacherWithPermissions(true, true); + TeacherPresentationConfig saved = new TeacherPresentationConfig(); + saved.setId(5L); + saved.setRun(run1); + saved.setPeriod(run1Period1); + saved.setNodeId(run1Node1Id); + saved.setComponentId(run1Component1Id); + saved.setComponentType("Discussion"); + saved.setUpdatedAt(new Timestamp(1000)); + saved.setPrompt("My prompt"); + saved.setStudentNamesDisplay("anonymize"); + saved.setUpdatedByWorkgroup(teacher1Run1Workgroup); + expect(teacherPresentationConfigService.saveConfig(eq(run1), eq(run1Period1), + eq(run1Node1Id), eq(run1Component1Id), eq("Discussion"), eq(Arrays.asList(7, 8)), + eq("anonymize"), eq("My prompt"), eq(teacher1Run1Workgroup))).andReturn(saved); + expect(teacherPresentationConfigService.getStudentWorkIds(saved)) + .andReturn(Arrays.asList(7, 8)); + expect(teacherPresentationConfigService.getAnswers(saved)) + .andReturn(Arrays.asList()); + replay(runService, userService, workgroupService, teacherPresentationConfigService); + ConfigResponse response = controller.saveConfig(teacherAuth, + request(run1Period1Id, Arrays.asList(new Item(7), new Item(8)), "anonymize", "My prompt")); + assertEquals(5L, response.id()); + assertEquals(2, response.items().size()); + assertEquals("My prompt", response.prompt()); + assertEquals(teacher1Run1Workgroup.getId(), response.updatedByWorkgroupId()); + verify(teacherPresentationConfigService); + } + + @Test + public void saveAnswer_ValidRequest_ReturnAnswer() throws Exception { + expectTeacherWithPermissions(true, true); + TeacherPresentationReflectionAnswer answer = new TeacherPresentationReflectionAnswer(); + answer.setQuestionId("q1"); + answer.setQuestionText("Why?"); + answer.setAnswerText("Because"); + answer.setAnsweredByWorkgroup(teacher1Run1Workgroup); + answer.setUpdatedAt(new Timestamp(2000)); + expect(teacherPresentationConfigService.saveAnswer(run1, run1Period1, run1Node1Id, + run1Component1Id, "Discussion", "q1", "Why?", "Because", teacher1Run1Workgroup)) + .andReturn(answer); + replay(runService, userService, workgroupService, teacherPresentationConfigService); + AnswerResponse response = controller.saveAnswer(teacherAuth, new AnswerRequest(runId1, + run1Period1Id, run1Node1Id, run1Component1Id, "Discussion", "q1", "Why?", "Because")); + assertEquals("q1", response.questionId()); + assertEquals("Because", response.answerText()); + verify(teacherPresentationConfigService); + } +} diff --git a/src/test/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImplTest.java b/src/test/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImplTest.java new file mode 100644 index 000000000..9bfbe86c3 --- /dev/null +++ b/src/test/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImplTest.java @@ -0,0 +1,166 @@ +package org.wise.portal.service.teacherpresentation.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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +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.ObjectNotFoundException; +import org.wise.portal.dao.teacherpresentation.TeacherPresentationConfigDao; +import org.wise.portal.dao.work.StudentWorkDao; +import org.wise.portal.domain.group.impl.PersistentGroup; +import org.wise.portal.domain.run.impl.RunImpl; +import org.wise.vle.domain.teacherpresentation.TeacherPresentationConfig; +import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; +import org.wise.vle.domain.work.StudentWork; + +@ExtendWith(EasyMockExtension.class) +public class TeacherPresentationConfigServiceImplTest { + + @TestSubject + private TeacherPresentationConfigServiceImpl service = new TeacherPresentationConfigServiceImpl(); + + @Mock + private TeacherPresentationConfigDao teacherPresentationConfigDao; + + @Mock + private StudentWorkDao studentWorkDao; + + private RunImpl run = new RunImpl(); + private RunImpl otherRun = new RunImpl(); + private PersistentGroup period = new PersistentGroup(); + private PersistentGroup otherPeriod = new PersistentGroup(); + + @BeforeEach + public void setUp() { + run.setId(1L); + otherRun.setId(2L); + period.setId(10L); + otherPeriod.setId(11L); + } + + private StudentWork work(RunImpl workRun, PersistentGroup workPeriod) { + StudentWork work = new StudentWork(); + work.setRun(workRun); + work.setPeriod(workPeriod); + return work; + } + + private TeacherPresentationConfig save(List ids, String namesDisplay, String prompt) { + return service.saveConfig(run, period, "node1", "comp1", "Discussion", ids, namesDisplay, + prompt, null); + } + + @Test + public void saveConfig_InvalidNamesDisplay_ThrowIllegalArgument() { + replay(teacherPresentationConfigDao, studentWorkDao); + assertThrows(IllegalArgumentException.class, () -> save(new ArrayList<>(), "bogus", null)); + } + + @Test + public void saveConfig_MoreThan100Items_ThrowIllegalArgument() { + List ids = new ArrayList<>(); + for (int i = 0; i < 101; i++) { + ids.add(i); + } + replay(teacherPresentationConfigDao, studentWorkDao); + assertThrows(IllegalArgumentException.class, () -> save(ids, "hide", null)); + } + + @Test + public void saveConfig_PromptTooLong_ThrowIllegalArgument() { + replay(teacherPresentationConfigDao, studentWorkDao); + assertThrows(IllegalArgumentException.class, + () -> save(new ArrayList<>(), "hide", "x".repeat(1001))); + } + + @Test + public void saveConfig_StudentWorkFromOtherPeriod_ThrowIllegalArgument() throws Exception { + expect(studentWorkDao.getById(5)).andReturn(work(run, otherPeriod)); + replay(teacherPresentationConfigDao, studentWorkDao); + assertThrows(IllegalArgumentException.class, () -> save(Arrays.asList(5), "hide", null)); + } + + @Test + public void saveConfig_StudentWorkFromOtherRun_ThrowIllegalArgument() throws Exception { + expect(studentWorkDao.getById(5)).andReturn(work(otherRun, period)); + replay(teacherPresentationConfigDao, studentWorkDao); + assertThrows(IllegalArgumentException.class, () -> save(Arrays.asList(5), "hide", null)); + } + + @Test + public void saveConfig_UnknownStudentWork_ThrowIllegalArgument() throws Exception { + expect(studentWorkDao.getById(5)).andThrow(new ObjectNotFoundException(5L, StudentWork.class)); + replay(teacherPresentationConfigDao, studentWorkDao); + assertThrows(IllegalArgumentException.class, () -> save(Arrays.asList(5), "hide", null)); + } + + @Test + public void saveConfig_NewConfig_CreateWithDefaultsAndDeduplicateItems() throws Exception { + expect(studentWorkDao.getById(5)).andReturn(work(run, period)); + expect(teacherPresentationConfigDao.getConfig(run, period, "node1", "comp1")).andReturn(null); + teacherPresentationConfigDao.save(anyObject(TeacherPresentationConfig.class)); + expectLastCall(); + replay(teacherPresentationConfigDao, studentWorkDao); + TeacherPresentationConfig config = save(Arrays.asList(5, 5), null, " "); + assertEquals("hide", config.getStudentNamesDisplay()); + assertNull(config.getPrompt()); + assertEquals(Arrays.asList(5), service.getStudentWorkIds(config)); + assertEquals("Discussion", config.getComponentType()); + } + + @Test + public void saveConfig_ExistingConfig_UpdatePromptAndNames() throws Exception { + TeacherPresentationConfig existing = new TeacherPresentationConfig(); + existing.setStudentNamesDisplay("show"); + expect(teacherPresentationConfigDao.getConfig(run, period, "node1", "comp1")) + .andReturn(existing); + teacherPresentationConfigDao.save(existing); + expectLastCall(); + replay(teacherPresentationConfigDao, studentWorkDao); + TeacherPresentationConfig config = save(new ArrayList<>(), "anonymize", "My prompt"); + assertEquals("anonymize", config.getStudentNamesDisplay()); + assertEquals("My prompt", config.getPrompt()); + assertEquals(0, service.getStudentWorkIds(config).size()); + } + + @Test + public void saveAnswer_ExistingAnswer_UpdateInsteadOfCreatingHistory() { + TeacherPresentationConfig config = new TeacherPresentationConfig(); + config.setId(3L); + TeacherPresentationReflectionAnswer existing = new TeacherPresentationReflectionAnswer(); + existing.setId(9L); + existing.setQuestionId("q1"); + expect(teacherPresentationConfigDao.getConfig(run, period, "node1", "comp1")) + .andReturn(config); + expect(teacherPresentationConfigDao.getAnswer(config, "q1")).andReturn(existing); + teacherPresentationConfigDao.saveAnswer(existing); + expectLastCall(); + replay(teacherPresentationConfigDao, studentWorkDao); + TeacherPresentationReflectionAnswer answer = service.saveAnswer(run, period, "node1", "comp1", + "Discussion", "q1", "Why?", "New answer", null); + assertEquals(9L, answer.getId()); + assertEquals("New answer", answer.getAnswerText()); + assertEquals("Why?", answer.getQuestionText()); + } + + @Test + public void saveAnswer_InvalidQuestionId_ThrowIllegalArgument() { + replay(teacherPresentationConfigDao, studentWorkDao); + assertThrows(IllegalArgumentException.class, () -> service.saveAnswer(run, period, "node1", + "comp1", "Discussion", " ", "Why?", "a", null)); + } +} From e719ee3accd0c8e9b54b5e4679c0f7f6bb7fb1f3 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Thu, 8 Oct 2026 17:07:58 -0700 Subject: [PATCH 2/5] test: update maximum item limit to 500 in TeacherPresentationConfigServiceImplTest --- .../impl/TeacherPresentationConfigServiceImplTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImplTest.java b/src/test/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImplTest.java index 9bfbe86c3..7996aef45 100644 --- a/src/test/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImplTest.java +++ b/src/test/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImplTest.java @@ -71,9 +71,9 @@ public void saveConfig_InvalidNamesDisplay_ThrowIllegalArgument() { } @Test - public void saveConfig_MoreThan100Items_ThrowIllegalArgument() { + public void saveConfig_MoreThan500Items_ThrowIllegalArgument() { List ids = new ArrayList<>(); - for (int i = 0; i < 101; i++) { + for (int i = 0; i < 501; i++) { ids.add(i); } replay(teacherPresentationConfigDao, studentWorkDao); @@ -144,8 +144,7 @@ public void saveAnswer_ExistingAnswer_UpdateInsteadOfCreatingHistory() { TeacherPresentationReflectionAnswer existing = new TeacherPresentationReflectionAnswer(); existing.setId(9L); existing.setQuestionId("q1"); - expect(teacherPresentationConfigDao.getConfig(run, period, "node1", "comp1")) - .andReturn(config); + expect(teacherPresentationConfigDao.getConfig(run, period, "node1", "comp1")).andReturn(config); expect(teacherPresentationConfigDao.getAnswer(config, "q1")).andReturn(existing); teacherPresentationConfigDao.saveAnswer(existing); expectLastCall(); From 5d571c2f51db58084831320699c294a15d80c70e Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Fri, 9 Oct 2026 09:32:48 -0700 Subject: [PATCH 3/5] test: add unit tests for HibernateTeacherPresentationConfigDao --- ...rnateTeacherPresentationConfigDaoTest.java | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 src/test/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDaoTest.java diff --git a/src/test/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDaoTest.java b/src/test/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDaoTest.java new file mode 100644 index 000000000..4cbf3a21c --- /dev/null +++ b/src/test/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDaoTest.java @@ -0,0 +1,221 @@ +package org.wise.portal.dao.teacherpresentation.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.teacherpresentation.TeacherPresentationConfig; +import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; + +@SpringBootTest +public class HibernateTeacherPresentationConfigDaoTest extends AbstractTransactionalDbTests { + + private Run run; + private Run otherRun; + private Group period1; + private Group period2; + private Workgroup teacherWorkgroup; + + @Autowired + private HibernateTeacherPresentationConfigDao teacherPresentationConfigDao; + + @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 TeacherPresentationConfig createConfig(Run run, Group period, String nodeId, + String componentId, String componentType) { + TeacherPresentationConfig config = new TeacherPresentationConfig(); + config.setRun(run); + config.setPeriod(period); + config.setNodeId(nodeId); + config.setComponentId(componentId); + config.setComponentType(componentType); + config.setItems("[{\"studentWorkId\":101}]"); + config.setStudentNamesDisplay(TeacherPresentationConfig.NAMES_HIDE); + config.setPrompt("Sample discussion prompt"); + config.setUpdatedByWorkgroup(teacherWorkgroup); + Timestamp now = new Timestamp(System.currentTimeMillis()); + config.setCreatedAt(now); + config.setUpdatedAt(now); + teacherPresentationConfigDao.save(config); + return config; + } + + private TeacherPresentationReflectionAnswer createAnswer(TeacherPresentationConfig config, + String questionId, String questionText, String answerText) { + TeacherPresentationReflectionAnswer answer = new TeacherPresentationReflectionAnswer(); + answer.setTeacherPresentationConfig(config); + answer.setQuestionId(questionId); + answer.setQuestionText(questionText); + answer.setAnswerText(answerText); + answer.setAnsweredByWorkgroup(teacherWorkgroup); + Timestamp now = new Timestamp(System.currentTimeMillis()); + answer.setCreatedAt(now); + answer.setUpdatedAt(now); + teacherPresentationConfigDao.saveAnswer(answer); + return answer; + } + + @Test + public void getConfig_WhenNoneExists_ReturnsNull() { + TeacherPresentationConfig config = teacherPresentationConfigDao.getConfig(run, period1, + "node1", "component1"); + assertNull(config); + } + + @Test + public void saveAndGetConfig_WhenExists_ReturnsMatchingConfig() { + TeacherPresentationConfig saved = createConfig(run, period1, "node1", "component1", + "Discussion"); + + TeacherPresentationConfig retrieved = teacherPresentationConfigDao.getConfig(run, period1, + "node1", "component1"); + assertNotNull(retrieved); + assertEquals(saved.getId(), retrieved.getId()); + assertEquals(run.getId(), retrieved.getRun().getId()); + assertEquals(period1.getId(), retrieved.getPeriod().getId()); + assertEquals("node1", retrieved.getNodeId()); + assertEquals("component1", retrieved.getComponentId()); + assertEquals("Discussion", retrieved.getComponentType()); + assertEquals("[{\"studentWorkId\":101}]", retrieved.getItems()); + assertEquals(TeacherPresentationConfig.NAMES_HIDE, retrieved.getStudentNamesDisplay()); + assertEquals("Sample discussion prompt", retrieved.getPrompt()); + assertEquals(teacherWorkgroup.getId(), retrieved.getUpdatedByWorkgroup().getId()); + } + + @Test + public void getConfig_WithDifferentParameters_ReturnsNull() { + createConfig(run, period1, "node1", "component1", "Discussion"); + + assertNull(teacherPresentationConfigDao.getConfig(otherRun, period1, "node1", "component1")); + assertNull(teacherPresentationConfigDao.getConfig(run, period2, "node1", "component1")); + assertNull(teacherPresentationConfigDao.getConfig(run, period1, "otherNode", "component1")); + assertNull(teacherPresentationConfigDao.getConfig(run, period1, "node1", "otherComponent")); + } + + @Test + public void saveAnswer_NewAnswer_PersistsAndCanBeRetrievedByQuestionId() { + TeacherPresentationConfig config = createConfig(run, period1, "node1", "component1", + "Discussion"); + + TeacherPresentationReflectionAnswer answer = createAnswer(config, "q1", + "What patterns did you notice?", "Students struggled with step 2."); + assertNotNull(answer.getId()); + + TeacherPresentationReflectionAnswer retrieved = teacherPresentationConfigDao.getAnswer(config, + "q1"); + assertNotNull(retrieved); + assertEquals(answer.getId(), retrieved.getId()); + assertEquals("q1", retrieved.getQuestionId()); + assertEquals("What patterns did you notice?", retrieved.getQuestionText()); + assertEquals("Students struggled with step 2.", retrieved.getAnswerText()); + assertEquals(teacherWorkgroup.getId(), retrieved.getAnsweredByWorkgroup().getId()); + } + + @Test + public void saveAnswer_ExistingAnswer_UpdatesAnswer() { + TeacherPresentationConfig config = createConfig(run, period1, "node1", "component1", + "Discussion"); + TeacherPresentationReflectionAnswer answer = createAnswer(config, "q1", + "Initial question", "Initial answer"); + Long answerId = answer.getId(); + + answer.setAnswerText("Updated answer"); + answer.setUpdatedAt(new Timestamp(System.currentTimeMillis())); + teacherPresentationConfigDao.saveAnswer(answer); + flush(); + + TeacherPresentationReflectionAnswer updated = teacherPresentationConfigDao.getAnswer(config, + "q1"); + assertNotNull(updated); + assertEquals(answerId, updated.getId()); + assertEquals("Updated answer", updated.getAnswerText()); + } + + @Test + public void getAnswers_MultipleAnswersForConfig_ReturnsOnlyAnswersForThatConfig() { + TeacherPresentationConfig config1 = createConfig(run, period1, "node1", "component1", + "Discussion"); + TeacherPresentationConfig config2 = createConfig(run, period1, "node2", "component2", + "Discussion"); + + createAnswer(config1, "q1", "Q1 text", "Answer 1"); + createAnswer(config1, "q2", "Q2 text", "Answer 2"); + createAnswer(config2, "q1", "Q1 for config2", "Answer other"); + + List answersConfig1 = teacherPresentationConfigDao + .getAnswers(config1); + assertEquals(2, answersConfig1.size()); + assertTrue(answersConfig1.stream().anyMatch(a -> "q1".equals(a.getQuestionId()))); + assertTrue(answersConfig1.stream().anyMatch(a -> "q2".equals(a.getQuestionId()))); + + List answersConfig2 = teacherPresentationConfigDao + .getAnswers(config2); + assertEquals(1, answersConfig2.size()); + assertEquals("q1", answersConfig2.get(0).getQuestionId()); + assertEquals("Answer other", answersConfig2.get(0).getAnswerText()); + } + + @Test + public void getAnswer_NonExistentQuestionId_ReturnsNull() { + TeacherPresentationConfig config = createConfig(run, period1, "node1", "component1", + "Discussion"); + createAnswer(config, "q1", "Question 1", "Answer 1"); + + assertNull(teacherPresentationConfigDao.getAnswer(config, "nonExistentQuestion")); + } + + @Test + public void delete_Config_RemovesConfig() { + TeacherPresentationConfig config = createConfig(run, period1, "node1", "component1", + "Discussion"); + flush(); + + teacherPresentationConfigDao.delete(config); + flush(); + + assertNull(teacherPresentationConfigDao.getConfig(run, period1, "node1", "component1")); + } +} From 97663419f1d01b6b034c438fa60446c505d1c954 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Fri, 9 Oct 2026 09:43:25 -0700 Subject: [PATCH 4/5] test: add unit tests for TeacherPresentationConfigAPIController error handling and configuration methods --- ...erPresentationConfigAPIControllerTest.java | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIControllerTest.java index 9c5ba41ae..4b5ddf5b5 100644 --- a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIControllerTest.java +++ b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIControllerTest.java @@ -12,6 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.sql.Timestamp; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -162,4 +163,150 @@ public void saveAnswer_ValidRequest_ReturnAnswer() throws Exception { assertEquals("Because", response.answerText()); verify(teacherPresentationConfigService); } + + @Test + public void saveAnswer_ServiceRejectsInput_ThrowBadRequest() throws Exception { + expectTeacherWithPermissions(true, true); + expect(teacherPresentationConfigService.saveAnswer(eq(run1), eq(run1Period1), + eq(run1Node1Id), eq(run1Component1Id), eq("Discussion"), eq(""), anyObject(), + anyObject(), eq(teacher1Run1Workgroup))) + .andThrow(new IllegalArgumentException("Invalid questionId")); + replay(runService, userService, workgroupService, teacherPresentationConfigService); + + ResponseStatusException e = assertThrows(ResponseStatusException.class, () -> controller + .saveAnswer(teacherAuth, new AnswerRequest(runId1, run1Period1Id, run1Node1Id, + run1Component1Id, "Discussion", "", "Why?", "Because"))); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); + } + + @Test + public void saveAnswer_NoGradePermission_ThrowAccessDenied() throws Exception { + expectTeacherWithPermissions(true, false); + replay(runService, userService, workgroupService, teacherPresentationConfigService); + + assertThrows(AccessDeniedException.class, () -> controller.saveAnswer(teacherAuth, + new AnswerRequest(runId1, run1Period1Id, run1Node1Id, run1Component1Id, + "Discussion", "q1", "Why?", "Because"))); + } + + @Test + public void saveAnswer_PeriodNotInRun_ThrowBadRequest() throws Exception { + expectTeacherWithPermissions(true, true); + replay(runService, userService, workgroupService, teacherPresentationConfigService); + + ResponseStatusException e = assertThrows(ResponseStatusException.class, () -> controller + .saveAnswer(teacherAuth, new AnswerRequest(runId1, 99999L, run1Node1Id, + run1Component1Id, "Discussion", "q1", "Why?", "Because"))); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); + } + + @Test + public void getConfig_ExistingConfigWithAnswers_ReturnPopulatedResponse() throws Exception { + expectTeacherWithPermissions(true, true); + + TeacherPresentationConfig config = new TeacherPresentationConfig(); + config.setId(10L); + config.setRun(run1); + config.setPeriod(run1Period1); + config.setNodeId(run1Node1Id); + config.setComponentId(run1Component1Id); + config.setComponentType("Discussion"); + config.setPrompt("Prompt override"); + config.setStudentNamesDisplay("show"); + config.setUpdatedAt(new Timestamp(1000)); + config.setUpdatedByWorkgroup(teacher1Run1Workgroup); + + TeacherPresentationReflectionAnswer answer = new TeacherPresentationReflectionAnswer(); + answer.setQuestionId("q1"); + answer.setQuestionText("Reflection question"); + answer.setAnswerText("Reflection answer"); + answer.setUpdatedAt(new Timestamp(2000)); + answer.setAnsweredByWorkgroup(teacher1Run1Workgroup); + + expect(teacherPresentationConfigService.getConfig(run1, run1Period1, run1Node1Id, + run1Component1Id)).andReturn(config); + expect(teacherPresentationConfigService.getStudentWorkIds(config)) + .andReturn(Arrays.asList(101, 102)); + expect(teacherPresentationConfigService.getAnswers(config)) + .andReturn(Arrays.asList(answer)); + + replay(runService, userService, workgroupService, teacherPresentationConfigService); + + ConfigResponse response = controller.getConfig(teacherAuth, runId1, run1Period1Id, + run1Node1Id, run1Component1Id); + + assertEquals(10L, response.id()); + assertEquals(2, response.items().size()); + assertEquals(101, response.items().get(0).studentWorkId()); + assertEquals("Prompt override", response.prompt()); + assertEquals("show", response.studentNamesDisplay()); + assertEquals(1, response.answers().size()); + assertEquals("q1", response.answers().get(0).questionId()); + assertEquals("Reflection answer", response.answers().get(0).answerText()); + verify(teacherPresentationConfigService); + } + + @Test + public void saveConfig_NullItems_SaveWithEmptyList() throws Exception { + expectTeacherWithPermissions(true, true); + TeacherPresentationConfig saved = new TeacherPresentationConfig(); + saved.setId(6L); + saved.setRun(run1); + saved.setPeriod(run1Period1); + saved.setNodeId(run1Node1Id); + saved.setComponentId(run1Component1Id); + saved.setComponentType("Discussion"); + saved.setUpdatedAt(new Timestamp(1000)); + saved.setStudentNamesDisplay("hide"); + expect(teacherPresentationConfigService.saveConfig(eq(run1), eq(run1Period1), + eq(run1Node1Id), eq(run1Component1Id), eq("Discussion"), eq(new ArrayList<>()), + eq("hide"), isNull(), eq(teacher1Run1Workgroup))).andReturn(saved); + expect(teacherPresentationConfigService.getStudentWorkIds(saved)) + .andReturn(new ArrayList<>()); + expect(teacherPresentationConfigService.getAnswers(saved)) + .andReturn(new ArrayList<>()); + replay(runService, userService, workgroupService, teacherPresentationConfigService); + ConfigResponse response = controller.saveConfig(teacherAuth, + request(run1Period1Id, null, "hide", null)); + assertEquals(6L, response.id()); + assertTrue(response.items().isEmpty()); + verify(teacherPresentationConfigService); + } + + @Test + public void saveConfig_TeacherWithNoWorkgroup_SaveWithNullWorkgroup() throws Exception { + expect(runService.retrieveById(runId1)).andReturn(run1).anyTimes(); + expect(userService.retrieveUserByUsername(teacherAuth.getName())).andReturn(teacher1) + .anyTimes(); + expect(runService.isAllowedToViewStudentWork(run1, teacher1)).andReturn(true).anyTimes(); + expect(runService.isAllowedToGradeStudentWork(run1, teacher1)).andReturn(true).anyTimes(); + expect(workgroupService.getWorkgroupListByRunAndUser(run1, teacher1)) + .andReturn(new ArrayList<>()).anyTimes(); + + TeacherPresentationConfig saved = new TeacherPresentationConfig(); + saved.setId(7L); + saved.setRun(run1); + saved.setPeriod(run1Period1); + saved.setNodeId(run1Node1Id); + saved.setComponentId(run1Component1Id); + saved.setComponentType("Discussion"); + saved.setUpdatedAt(new Timestamp(1000)); + saved.setStudentNamesDisplay("hide"); + saved.setUpdatedByWorkgroup(null); + + expect(teacherPresentationConfigService.saveConfig(eq(run1), eq(run1Period1), + eq(run1Node1Id), eq(run1Component1Id), eq("Discussion"), anyObject(), + eq("hide"), isNull(), isNull())).andReturn(saved); + expect(teacherPresentationConfigService.getStudentWorkIds(saved)) + .andReturn(new ArrayList<>()); + expect(teacherPresentationConfigService.getAnswers(saved)) + .andReturn(new ArrayList<>()); + + replay(runService, userService, workgroupService, teacherPresentationConfigService); + ConfigResponse response = controller.saveConfig(teacherAuth, + request(run1Period1Id, new ArrayList<>(), "hide", null)); + assertEquals(7L, response.id()); + assertNull(response.updatedByWorkgroupId()); + verify(teacherPresentationConfigService); + } } From cdbe40e6671746f428b02bb5161838d7bc6a94bf Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Fri, 9 Oct 2026 13:34:30 -0700 Subject: [PATCH 5/5] refactor(teacher-presentation): strip reflection answers from presentation feature --- .../TeacherPresentationConfigDao.java | 10 --- ...HibernateTeacherPresentationConfigDao.java | 37 -------- ...eacherPresentationConfigAPIController.java | 47 +--------- .../TeacherPresentationConfigService.java | 13 --- .../TeacherPresentationConfigServiceImpl.java | 46 ---------- .../TeacherPresentationReflectionAnswer.java | 64 ------------- ...V2__create_teacher_presentation_tables.sql | 15 ---- ...rnateTeacherPresentationConfigDaoTest.java | 90 ------------------- ...erPresentationConfigAPIControllerTest.java | 80 +---------------- ...cherPresentationConfigServiceImplTest.java | 27 ------ 10 files changed, 4 insertions(+), 425 deletions(-) delete mode 100644 src/main/java/org/wise/vle/domain/teacherpresentation/TeacherPresentationReflectionAnswer.java diff --git a/src/main/java/org/wise/portal/dao/teacherpresentation/TeacherPresentationConfigDao.java b/src/main/java/org/wise/portal/dao/teacherpresentation/TeacherPresentationConfigDao.java index 57a4ef54a..863671815 100644 --- a/src/main/java/org/wise/portal/dao/teacherpresentation/TeacherPresentationConfigDao.java +++ b/src/main/java/org/wise/portal/dao/teacherpresentation/TeacherPresentationConfigDao.java @@ -1,12 +1,9 @@ package org.wise.portal.dao.teacherpresentation; -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.teacherpresentation.TeacherPresentationConfig; -import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; public interface TeacherPresentationConfigDao extends SimpleDao { @@ -15,11 +12,4 @@ public interface TeacherPresentationConfigDao getAnswers(TeacherPresentationConfig config); - - TeacherPresentationReflectionAnswer getAnswer(TeacherPresentationConfig config, - String questionId); - - void saveAnswer(TeacherPresentationReflectionAnswer answer); } diff --git a/src/main/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDao.java b/src/main/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDao.java index 5bf015b53..234126d62 100644 --- a/src/main/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDao.java +++ b/src/main/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDao.java @@ -2,7 +2,6 @@ import java.util.List; -import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; @@ -13,7 +12,6 @@ import org.wise.portal.domain.group.Group; import org.wise.portal.domain.run.Run; import org.wise.vle.domain.teacherpresentation.TeacherPresentationConfig; -import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; @Repository("teacherPresentationConfigDao") public class HibernateTeacherPresentationConfigDao @@ -36,39 +34,4 @@ public TeacherPresentationConfig getConfig(Run run, Group period, String nodeId, List results = entityManager.createQuery(cq).getResultList(); return results.isEmpty() ? null : results.get(0); } - - @Override - public List getAnswers(TeacherPresentationConfig config) { - CriteriaBuilder cb = getCriteriaBuilder(); - CriteriaQuery cq = cb - .createQuery(TeacherPresentationReflectionAnswer.class); - Root root = cq - .from(TeacherPresentationReflectionAnswer.class); - cq.select(root).where(cb.equal(root.get("teacherPresentationConfig"), config)); - return entityManager.createQuery(cq).getResultList(); - } - - @Override - public TeacherPresentationReflectionAnswer getAnswer(TeacherPresentationConfig config, - String questionId) { - CriteriaBuilder cb = getCriteriaBuilder(); - CriteriaQuery cq = cb - .createQuery(TeacherPresentationReflectionAnswer.class); - Root root = cq - .from(TeacherPresentationReflectionAnswer.class); - cq.select(root).where(cb.equal(root.get("teacherPresentationConfig"), config), - cb.equal(root.get("questionId"), questionId)); - TypedQuery query = entityManager.createQuery(cq); - List results = query.getResultList(); - return results.isEmpty() ? null : results.get(0); - } - - @Override - public void saveAnswer(TeacherPresentationReflectionAnswer answer) { - if (answer.getId() == null) { - entityManager.persist(answer); - } else { - entityManager.merge(answer); - } - } } diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIController.java index 30aba3822..8b47b9d00 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIController.java @@ -25,7 +25,6 @@ import org.wise.portal.service.user.UserService; import org.wise.portal.service.workgroup.WorkgroupService; import org.wise.vle.domain.teacherpresentation.TeacherPresentationConfig; -import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; /** * REST API for TeacherPresentation: the teacher's selected student work, student name display @@ -55,17 +54,9 @@ public record ConfigRequest(Long runId, Long periodId, String nodeId, String com String componentType, List items, String studentNamesDisplay, String prompt) { } - public record AnswerRequest(Long runId, Long periodId, String nodeId, String componentId, - String componentType, String questionId, String questionText, String answerText) { - } - - public record AnswerResponse(String questionId, String questionText, String answerText, - Long answeredByWorkgroupId, Long updatedAt) { - } - public record ConfigResponse(Long id, Long runId, Long periodId, String nodeId, String componentId, String componentType, List items, String studentNamesDisplay, - String prompt, Long updatedByWorkgroupId, Long updatedAt, List answers) { + String prompt, Long updatedByWorkgroupId, Long updatedAt) { } @GetMapping @@ -82,7 +73,7 @@ ConfigResponse getConfig(Authentication auth, @RequestParam Long runId, nodeId, componentId); if (config == null) { return new ConfigResponse(null, runId, periodId, nodeId, componentId, null, new ArrayList<>(), - TeacherPresentationConfig.NAMES_HIDE, null, null, null, new ArrayList<>()); + TeacherPresentationConfig.NAMES_HIDE, null, null, null); } return toResponse(config); } @@ -112,26 +103,6 @@ ConfigResponse saveConfig(Authentication auth, @RequestBody ConfigRequest reques } } - @PutMapping("/answers") - AnswerResponse 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()); - try { - TeacherPresentationReflectionAnswer answer = teacherPresentationConfigService.saveAnswer(run, - period, request.nodeId(), request.componentId(), request.componentType(), - request.questionId(), request.questionText(), request.answerText(), - getTeacherWorkgroup(run, user)); - return toAnswerResponse(answer); - } 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)) { @@ -151,22 +122,10 @@ private ConfigResponse toResponse(TeacherPresentationConfig config) { for (Integer id : teacherPresentationConfigService.getStudentWorkIds(config)) { items.add(new Item(id)); } - List answers = new ArrayList<>(); - for (TeacherPresentationReflectionAnswer answer : teacherPresentationConfigService - .getAnswers(config)) { - answers.add(toAnswerResponse(answer)); - } Workgroup updatedBy = config.getUpdatedByWorkgroup(); return new ConfigResponse(config.getId(), config.getRun().getId(), config.getPeriod().getId(), config.getNodeId(), config.getComponentId(), config.getComponentType(), items, config.getStudentNamesDisplay(), config.getPrompt(), - updatedBy == null ? null : updatedBy.getId(), config.getUpdatedAt().getTime(), answers); - } - - private AnswerResponse toAnswerResponse(TeacherPresentationReflectionAnswer answer) { - Workgroup answeredBy = answer.getAnsweredByWorkgroup(); - return new AnswerResponse(answer.getQuestionId(), answer.getQuestionText(), - answer.getAnswerText(), answeredBy == null ? null : answeredBy.getId(), - answer.getUpdatedAt().getTime()); + updatedBy == null ? null : updatedBy.getId(), config.getUpdatedAt().getTime()); } } diff --git a/src/main/java/org/wise/portal/service/teacherpresentation/TeacherPresentationConfigService.java b/src/main/java/org/wise/portal/service/teacherpresentation/TeacherPresentationConfigService.java index 6eef6a227..d77adb36b 100644 --- a/src/main/java/org/wise/portal/service/teacherpresentation/TeacherPresentationConfigService.java +++ b/src/main/java/org/wise/portal/service/teacherpresentation/TeacherPresentationConfigService.java @@ -6,7 +6,6 @@ import org.wise.portal.domain.run.Run; import org.wise.portal.domain.workgroup.Workgroup; import org.wise.vle.domain.teacherpresentation.TeacherPresentationConfig; -import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; public interface TeacherPresentationConfigService { @@ -18,8 +17,6 @@ public interface TeacherPresentationConfigService { */ TeacherPresentationConfig getConfig(Run run, Group period, String nodeId, String componentId); - List getAnswers(TeacherPresentationConfig config); - List getStudentWorkIds(TeacherPresentationConfig config); /** @@ -33,14 +30,4 @@ public interface TeacherPresentationConfigService { TeacherPresentationConfig saveConfig(Run run, Group period, String nodeId, String componentId, String componentType, List studentWorkIds, String studentNamesDisplay, String prompt, Workgroup teacherWorkgroup); - - /** - * Creates or updates the answer to a reflection question. Creates the config if it does not - * exist yet. - * - * @throws IllegalArgumentException if the input is invalid - */ - TeacherPresentationReflectionAnswer 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/teacherpresentation/impl/TeacherPresentationConfigServiceImpl.java b/src/main/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImpl.java index 1c9bcf165..c1c976cc0 100644 --- a/src/main/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImpl.java +++ b/src/main/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImpl.java @@ -18,7 +18,6 @@ import org.wise.portal.domain.workgroup.Workgroup; import org.wise.portal.service.teacherpresentation.TeacherPresentationConfigService; import org.wise.vle.domain.teacherpresentation.TeacherPresentationConfig; -import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; import org.wise.vle.domain.work.StudentWork; import com.fasterxml.jackson.core.JsonProcessingException; @@ -29,8 +28,6 @@ public class TeacherPresentationConfigServiceImpl implements TeacherPresentationConfigService { 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; private static final Set NAMES_DISPLAY_OPTIONS = Set.of( TeacherPresentationConfig.NAMES_SHOW, TeacherPresentationConfig.NAMES_HIDE, TeacherPresentationConfig.NAMES_ANONYMIZE); @@ -50,12 +47,6 @@ public TeacherPresentationConfig getConfig(Run run, Group period, String nodeId, return teacherPresentationConfigDao.getConfig(run, period, nodeId, componentId); } - @Override - @Transactional(readOnly = true) - public List getAnswers(TeacherPresentationConfig config) { - return teacherPresentationConfigDao.getAnswers(config); - } - @Override public List getStudentWorkIds(TeacherPresentationConfig config) { try { @@ -96,43 +87,6 @@ public TeacherPresentationConfig saveConfig(Run run, Group period, String nodeId return config; } - @Override - @Transactional - public TeacherPresentationReflectionAnswer 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("answerText is too long"); - } - TeacherPresentationConfig config = getOrCreateConfig(run, period, nodeId, componentId, - componentType); - if (config.getId() == null) { - teacherPresentationConfigDao.save(config); - } - Timestamp now = new Timestamp(System.currentTimeMillis()); - TeacherPresentationReflectionAnswer answer = teacherPresentationConfigDao.getAnswer(config, - questionId); - if (answer == null) { - answer = new TeacherPresentationReflectionAnswer(); - answer.setTeacherPresentationConfig(config); - answer.setQuestionId(questionId); - answer.setCreatedAt(now); - } - answer.setQuestionText(questionText); - answer.setAnswerText(answerText); - answer.setAnsweredByWorkgroup(teacherWorkgroup); - answer.setUpdatedAt(now); - teacherPresentationConfigDao.saveAnswer(answer); - return answer; - } - private TeacherPresentationConfig getOrCreateConfig(Run run, Group period, String nodeId, String componentId, String componentType) { TeacherPresentationConfig config = teacherPresentationConfigDao.getConfig(run, period, nodeId, diff --git a/src/main/java/org/wise/vle/domain/teacherpresentation/TeacherPresentationReflectionAnswer.java b/src/main/java/org/wise/vle/domain/teacherpresentation/TeacherPresentationReflectionAnswer.java deleted file mode 100644 index e44e54403..000000000 --- a/src/main/java/org/wise/vle/domain/teacherpresentation/TeacherPresentationReflectionAnswer.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.wise.vle.domain.teacherpresentation; - -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.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; -import lombok.Getter; -import lombok.Setter; -import org.wise.portal.domain.workgroup.Workgroup; -import org.wise.portal.domain.workgroup.impl.WorkgroupImpl; - -/** - * A teacher's answer to one reflection question of a TeacherPresentation. Only the latest answer - * per question is kept. The question text is copied at answer time so research data survives later - * edits to the question. - */ -@Entity(name = "teacherPresentationReflectionAnswer") -@Table(name = "teacher_presentation_reflection_answers", - uniqueConstraints = @UniqueConstraint( - name = "teacherPresentationReflectionAnswersUniqueKey", - columnNames = { "teacherPresentationConfigId", "questionId" })) -@Getter -@Setter -public class TeacherPresentationReflectionAnswer { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "teacherPresentationConfigId", nullable = false) - @JsonIgnore - private TeacherPresentationConfig teacherPresentationConfig; - - @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") - private String answerText; - - @ManyToOne(targetEntity = WorkgroupImpl.class, fetch = FetchType.LAZY) - @JoinColumn(name = "answeredByWorkgroupId") - @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_presentation_tables.sql b/src/main/resources/db/migration/V2__create_teacher_presentation_tables.sql index 157005495..27b743728 100644 --- a/src/main/resources/db/migration/V2__create_teacher_presentation_tables.sql +++ b/src/main/resources/db/migration/V2__create_teacher_presentation_tables.sql @@ -18,18 +18,3 @@ create table teacher_presentation_config ( constraint teacherPresentationConfigPeriodIdFK foreign key (periodId) references `groups` (id), constraint teacherPresentationConfigUpdatedByWorkgroupIdFK foreign key (updatedByWorkgroupId) references workgroups (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -create table teacher_presentation_reflection_answers ( - id bigint not null auto_increment, - teacherPresentationConfigId bigint not null, - questionId varchar(30) not null, - questionText text not null, - answerText text, - answeredByWorkgroupId bigint, - createdAt datetime(3) not null, - updatedAt datetime(3) not null, - primary key (id), - unique key teacherPresentationReflectionAnswersUniqueKey (teacherPresentationConfigId, questionId), - constraint teacherPresentationReflectionAnswersConfigIdFK foreign key (teacherPresentationConfigId) references teacher_presentation_config (id) on delete cascade, - constraint teacherPresentationReflectionAnswersWorkgroupIdFK foreign key (answeredByWorkgroupId) references workgroups (id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/src/test/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDaoTest.java b/src/test/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDaoTest.java index 4cbf3a21c..98e24fcdd 100644 --- a/src/test/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDaoTest.java +++ b/src/test/java/org/wise/portal/dao/teacherpresentation/impl/HibernateTeacherPresentationConfigDaoTest.java @@ -3,12 +3,10 @@ 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; @@ -23,7 +21,6 @@ import org.wise.portal.domain.workgroup.Workgroup; import org.wise.portal.junit.AbstractTransactionalDbTests; import org.wise.vle.domain.teacherpresentation.TeacherPresentationConfig; -import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; @SpringBootTest public class HibernateTeacherPresentationConfigDaoTest extends AbstractTransactionalDbTests { @@ -83,21 +80,6 @@ private TeacherPresentationConfig createConfig(Run run, Group period, String nod return config; } - private TeacherPresentationReflectionAnswer createAnswer(TeacherPresentationConfig config, - String questionId, String questionText, String answerText) { - TeacherPresentationReflectionAnswer answer = new TeacherPresentationReflectionAnswer(); - answer.setTeacherPresentationConfig(config); - answer.setQuestionId(questionId); - answer.setQuestionText(questionText); - answer.setAnswerText(answerText); - answer.setAnsweredByWorkgroup(teacherWorkgroup); - Timestamp now = new Timestamp(System.currentTimeMillis()); - answer.setCreatedAt(now); - answer.setUpdatedAt(now); - teacherPresentationConfigDao.saveAnswer(answer); - return answer; - } - @Test public void getConfig_WhenNoneExists_ReturnsNull() { TeacherPresentationConfig config = teacherPresentationConfigDao.getConfig(run, period1, @@ -135,78 +117,6 @@ public void getConfig_WithDifferentParameters_ReturnsNull() { assertNull(teacherPresentationConfigDao.getConfig(run, period1, "node1", "otherComponent")); } - @Test - public void saveAnswer_NewAnswer_PersistsAndCanBeRetrievedByQuestionId() { - TeacherPresentationConfig config = createConfig(run, period1, "node1", "component1", - "Discussion"); - - TeacherPresentationReflectionAnswer answer = createAnswer(config, "q1", - "What patterns did you notice?", "Students struggled with step 2."); - assertNotNull(answer.getId()); - - TeacherPresentationReflectionAnswer retrieved = teacherPresentationConfigDao.getAnswer(config, - "q1"); - assertNotNull(retrieved); - assertEquals(answer.getId(), retrieved.getId()); - assertEquals("q1", retrieved.getQuestionId()); - assertEquals("What patterns did you notice?", retrieved.getQuestionText()); - assertEquals("Students struggled with step 2.", retrieved.getAnswerText()); - assertEquals(teacherWorkgroup.getId(), retrieved.getAnsweredByWorkgroup().getId()); - } - - @Test - public void saveAnswer_ExistingAnswer_UpdatesAnswer() { - TeacherPresentationConfig config = createConfig(run, period1, "node1", "component1", - "Discussion"); - TeacherPresentationReflectionAnswer answer = createAnswer(config, "q1", - "Initial question", "Initial answer"); - Long answerId = answer.getId(); - - answer.setAnswerText("Updated answer"); - answer.setUpdatedAt(new Timestamp(System.currentTimeMillis())); - teacherPresentationConfigDao.saveAnswer(answer); - flush(); - - TeacherPresentationReflectionAnswer updated = teacherPresentationConfigDao.getAnswer(config, - "q1"); - assertNotNull(updated); - assertEquals(answerId, updated.getId()); - assertEquals("Updated answer", updated.getAnswerText()); - } - - @Test - public void getAnswers_MultipleAnswersForConfig_ReturnsOnlyAnswersForThatConfig() { - TeacherPresentationConfig config1 = createConfig(run, period1, "node1", "component1", - "Discussion"); - TeacherPresentationConfig config2 = createConfig(run, period1, "node2", "component2", - "Discussion"); - - createAnswer(config1, "q1", "Q1 text", "Answer 1"); - createAnswer(config1, "q2", "Q2 text", "Answer 2"); - createAnswer(config2, "q1", "Q1 for config2", "Answer other"); - - List answersConfig1 = teacherPresentationConfigDao - .getAnswers(config1); - assertEquals(2, answersConfig1.size()); - assertTrue(answersConfig1.stream().anyMatch(a -> "q1".equals(a.getQuestionId()))); - assertTrue(answersConfig1.stream().anyMatch(a -> "q2".equals(a.getQuestionId()))); - - List answersConfig2 = teacherPresentationConfigDao - .getAnswers(config2); - assertEquals(1, answersConfig2.size()); - assertEquals("q1", answersConfig2.get(0).getQuestionId()); - assertEquals("Answer other", answersConfig2.get(0).getAnswerText()); - } - - @Test - public void getAnswer_NonExistentQuestionId_ReturnsNull() { - TeacherPresentationConfig config = createConfig(run, period1, "node1", "component1", - "Discussion"); - createAnswer(config, "q1", "Question 1", "Answer 1"); - - assertNull(teacherPresentationConfigDao.getAnswer(config, "nonExistentQuestion")); - } - @Test public void delete_Config_RemovesConfig() { TeacherPresentationConfig config = createConfig(run, period1, "node1", "component1", diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIControllerTest.java index 4b5ddf5b5..90897b64d 100644 --- a/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIControllerTest.java +++ b/src/test/java/org/wise/portal/presentation/web/controllers/teacher/TeacherPresentationConfigAPIControllerTest.java @@ -26,14 +26,11 @@ 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.TeacherPresentationConfigAPIController.AnswerRequest; -import org.wise.portal.presentation.web.controllers.teacher.TeacherPresentationConfigAPIController.AnswerResponse; import org.wise.portal.presentation.web.controllers.teacher.TeacherPresentationConfigAPIController.ConfigRequest; import org.wise.portal.presentation.web.controllers.teacher.TeacherPresentationConfigAPIController.ConfigResponse; import org.wise.portal.presentation.web.controllers.teacher.TeacherPresentationConfigAPIController.Item; import org.wise.portal.service.teacherpresentation.TeacherPresentationConfigService; import org.wise.vle.domain.teacherpresentation.TeacherPresentationConfig; -import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; @ExtendWith(EasyMockExtension.class) public class TeacherPresentationConfigAPIControllerTest extends APIControllerTest { @@ -72,7 +69,6 @@ public void getConfig_NoConfigSaved_ReturnDefaults() throws Exception { assertTrue(response.items().isEmpty()); assertEquals("hide", response.studentNamesDisplay()); assertNull(response.prompt()); - assertTrue(response.answers().isEmpty()); verify(teacherPresentationConfigService); } @@ -132,8 +128,6 @@ public void saveConfig_ValidRequest_SaveWithTeacherWorkgroupAndReturnConfig() th eq("anonymize"), eq("My prompt"), eq(teacher1Run1Workgroup))).andReturn(saved); expect(teacherPresentationConfigService.getStudentWorkIds(saved)) .andReturn(Arrays.asList(7, 8)); - expect(teacherPresentationConfigService.getAnswers(saved)) - .andReturn(Arrays.asList()); replay(runService, userService, workgroupService, teacherPresentationConfigService); ConfigResponse response = controller.saveConfig(teacherAuth, request(run1Period1Id, Arrays.asList(new Item(7), new Item(8)), "anonymize", "My prompt")); @@ -145,63 +139,7 @@ public void saveConfig_ValidRequest_SaveWithTeacherWorkgroupAndReturnConfig() th } @Test - public void saveAnswer_ValidRequest_ReturnAnswer() throws Exception { - expectTeacherWithPermissions(true, true); - TeacherPresentationReflectionAnswer answer = new TeacherPresentationReflectionAnswer(); - answer.setQuestionId("q1"); - answer.setQuestionText("Why?"); - answer.setAnswerText("Because"); - answer.setAnsweredByWorkgroup(teacher1Run1Workgroup); - answer.setUpdatedAt(new Timestamp(2000)); - expect(teacherPresentationConfigService.saveAnswer(run1, run1Period1, run1Node1Id, - run1Component1Id, "Discussion", "q1", "Why?", "Because", teacher1Run1Workgroup)) - .andReturn(answer); - replay(runService, userService, workgroupService, teacherPresentationConfigService); - AnswerResponse response = controller.saveAnswer(teacherAuth, new AnswerRequest(runId1, - run1Period1Id, run1Node1Id, run1Component1Id, "Discussion", "q1", "Why?", "Because")); - assertEquals("q1", response.questionId()); - assertEquals("Because", response.answerText()); - verify(teacherPresentationConfigService); - } - - @Test - public void saveAnswer_ServiceRejectsInput_ThrowBadRequest() throws Exception { - expectTeacherWithPermissions(true, true); - expect(teacherPresentationConfigService.saveAnswer(eq(run1), eq(run1Period1), - eq(run1Node1Id), eq(run1Component1Id), eq("Discussion"), eq(""), anyObject(), - anyObject(), eq(teacher1Run1Workgroup))) - .andThrow(new IllegalArgumentException("Invalid questionId")); - replay(runService, userService, workgroupService, teacherPresentationConfigService); - - ResponseStatusException e = assertThrows(ResponseStatusException.class, () -> controller - .saveAnswer(teacherAuth, new AnswerRequest(runId1, run1Period1Id, run1Node1Id, - run1Component1Id, "Discussion", "", "Why?", "Because"))); - assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); - } - - @Test - public void saveAnswer_NoGradePermission_ThrowAccessDenied() throws Exception { - expectTeacherWithPermissions(true, false); - replay(runService, userService, workgroupService, teacherPresentationConfigService); - - assertThrows(AccessDeniedException.class, () -> controller.saveAnswer(teacherAuth, - new AnswerRequest(runId1, run1Period1Id, run1Node1Id, run1Component1Id, - "Discussion", "q1", "Why?", "Because"))); - } - - @Test - public void saveAnswer_PeriodNotInRun_ThrowBadRequest() throws Exception { - expectTeacherWithPermissions(true, true); - replay(runService, userService, workgroupService, teacherPresentationConfigService); - - ResponseStatusException e = assertThrows(ResponseStatusException.class, () -> controller - .saveAnswer(teacherAuth, new AnswerRequest(runId1, 99999L, run1Node1Id, - run1Component1Id, "Discussion", "q1", "Why?", "Because"))); - assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); - } - - @Test - public void getConfig_ExistingConfigWithAnswers_ReturnPopulatedResponse() throws Exception { + public void getConfig_ExistingConfig_ReturnPopulatedResponse() throws Exception { expectTeacherWithPermissions(true, true); TeacherPresentationConfig config = new TeacherPresentationConfig(); @@ -216,19 +154,10 @@ public void getConfig_ExistingConfigWithAnswers_ReturnPopulatedResponse() throws config.setUpdatedAt(new Timestamp(1000)); config.setUpdatedByWorkgroup(teacher1Run1Workgroup); - TeacherPresentationReflectionAnswer answer = new TeacherPresentationReflectionAnswer(); - answer.setQuestionId("q1"); - answer.setQuestionText("Reflection question"); - answer.setAnswerText("Reflection answer"); - answer.setUpdatedAt(new Timestamp(2000)); - answer.setAnsweredByWorkgroup(teacher1Run1Workgroup); - expect(teacherPresentationConfigService.getConfig(run1, run1Period1, run1Node1Id, run1Component1Id)).andReturn(config); expect(teacherPresentationConfigService.getStudentWorkIds(config)) .andReturn(Arrays.asList(101, 102)); - expect(teacherPresentationConfigService.getAnswers(config)) - .andReturn(Arrays.asList(answer)); replay(runService, userService, workgroupService, teacherPresentationConfigService); @@ -240,9 +169,6 @@ public void getConfig_ExistingConfigWithAnswers_ReturnPopulatedResponse() throws assertEquals(101, response.items().get(0).studentWorkId()); assertEquals("Prompt override", response.prompt()); assertEquals("show", response.studentNamesDisplay()); - assertEquals(1, response.answers().size()); - assertEquals("q1", response.answers().get(0).questionId()); - assertEquals("Reflection answer", response.answers().get(0).answerText()); verify(teacherPresentationConfigService); } @@ -263,8 +189,6 @@ public void saveConfig_NullItems_SaveWithEmptyList() throws Exception { eq("hide"), isNull(), eq(teacher1Run1Workgroup))).andReturn(saved); expect(teacherPresentationConfigService.getStudentWorkIds(saved)) .andReturn(new ArrayList<>()); - expect(teacherPresentationConfigService.getAnswers(saved)) - .andReturn(new ArrayList<>()); replay(runService, userService, workgroupService, teacherPresentationConfigService); ConfigResponse response = controller.saveConfig(teacherAuth, request(run1Period1Id, null, "hide", null)); @@ -299,8 +223,6 @@ public void saveConfig_TeacherWithNoWorkgroup_SaveWithNullWorkgroup() throws Exc eq("hide"), isNull(), isNull())).andReturn(saved); expect(teacherPresentationConfigService.getStudentWorkIds(saved)) .andReturn(new ArrayList<>()); - expect(teacherPresentationConfigService.getAnswers(saved)) - .andReturn(new ArrayList<>()); replay(runService, userService, workgroupService, teacherPresentationConfigService); ConfigResponse response = controller.saveConfig(teacherAuth, diff --git a/src/test/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImplTest.java b/src/test/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImplTest.java index 7996aef45..75ba3332b 100644 --- a/src/test/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImplTest.java +++ b/src/test/java/org/wise/portal/service/teacherpresentation/impl/TeacherPresentationConfigServiceImplTest.java @@ -24,7 +24,6 @@ import org.wise.portal.domain.group.impl.PersistentGroup; import org.wise.portal.domain.run.impl.RunImpl; import org.wise.vle.domain.teacherpresentation.TeacherPresentationConfig; -import org.wise.vle.domain.teacherpresentation.TeacherPresentationReflectionAnswer; import org.wise.vle.domain.work.StudentWork; @ExtendWith(EasyMockExtension.class) @@ -136,30 +135,4 @@ public void saveConfig_ExistingConfig_UpdatePromptAndNames() throws Exception { assertEquals("My prompt", config.getPrompt()); assertEquals(0, service.getStudentWorkIds(config).size()); } - - @Test - public void saveAnswer_ExistingAnswer_UpdateInsteadOfCreatingHistory() { - TeacherPresentationConfig config = new TeacherPresentationConfig(); - config.setId(3L); - TeacherPresentationReflectionAnswer existing = new TeacherPresentationReflectionAnswer(); - existing.setId(9L); - existing.setQuestionId("q1"); - expect(teacherPresentationConfigDao.getConfig(run, period, "node1", "comp1")).andReturn(config); - expect(teacherPresentationConfigDao.getAnswer(config, "q1")).andReturn(existing); - teacherPresentationConfigDao.saveAnswer(existing); - expectLastCall(); - replay(teacherPresentationConfigDao, studentWorkDao); - TeacherPresentationReflectionAnswer answer = service.saveAnswer(run, period, "node1", "comp1", - "Discussion", "q1", "Why?", "New answer", null); - assertEquals(9L, answer.getId()); - assertEquals("New answer", answer.getAnswerText()); - assertEquals("Why?", answer.getQuestionText()); - } - - @Test - public void saveAnswer_InvalidQuestionId_ThrowIllegalArgument() { - replay(teacherPresentationConfigDao, studentWorkDao); - assertThrows(IllegalArgumentException.class, () -> service.saveAnswer(run, period, "node1", - "comp1", "Discussion", " ", "Why?", "a", null)); - } }