Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.wise.portal.dao.teacherpresentation;

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;

public interface TeacherPresentationConfigDao<T extends TeacherPresentationConfig>
extends SimpleDao<T> {

/**
* @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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.wise.portal.dao.teacherpresentation.impl;

import java.util.List;

import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;

import org.springframework.stereotype.Repository;
import org.wise.portal.dao.impl.AbstractHibernateDao;
import org.wise.portal.dao.teacherpresentation.TeacherPresentationConfigDao;
import org.wise.portal.domain.group.Group;
import org.wise.portal.domain.run.Run;
import org.wise.vle.domain.teacherpresentation.TeacherPresentationConfig;

@Repository("teacherPresentationConfigDao")
public class HibernateTeacherPresentationConfigDao
extends AbstractHibernateDao<TeacherPresentationConfig>
implements TeacherPresentationConfigDao<TeacherPresentationConfig> {

@Override
protected Class<? extends TeacherPresentationConfig> getDataObjectClass() {
return TeacherPresentationConfig.class;
}

@Override
public TeacherPresentationConfig getConfig(Run run, Group period, String nodeId,
String componentId) {
CriteriaBuilder cb = getCriteriaBuilder();
CriteriaQuery<TeacherPresentationConfig> cq = cb.createQuery(TeacherPresentationConfig.class);
Root<TeacherPresentationConfig> 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<TeacherPresentationConfig> results = entityManager.createQuery(cq).getResultList();
return results.isEmpty() ? null : results.get(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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;

/**
* 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<Item> items, String studentNamesDisplay, String prompt) {
}

public record ConfigResponse(Long id, Long runId, Long periodId, String nodeId,
String componentId, String componentType, List<Item> items, String studentNamesDisplay,
String prompt, Long updatedByWorkgroupId, Long updatedAt) {
}

@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);
}
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<Integer> 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());
}
}

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<Workgroup> workgroups = workgroupService.getWorkgroupListByRunAndUser(run, user);
return workgroups.isEmpty() ? null : workgroups.get(0);
}

private ConfigResponse toResponse(TeacherPresentationConfig config) {
List<Item> items = new ArrayList<>();
for (Integer id : teacherPresentationConfigService.getStudentWorkIds(config)) {
items.add(new Item(id));
}
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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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;

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<Integer> 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<Integer> studentWorkIds, String studentNamesDisplay,
String prompt, Workgroup teacherWorkgroup);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
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.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 Set<String> NAMES_DISPLAY_OPTIONS = Set.of(
TeacherPresentationConfig.NAMES_SHOW, TeacherPresentationConfig.NAMES_HIDE,
TeacherPresentationConfig.NAMES_ANONYMIZE);

private final ObjectMapper objectMapper = new ObjectMapper();

@Autowired
private TeacherPresentationConfigDao<TeacherPresentationConfig> teacherPresentationConfigDao;

@Autowired
private StudentWorkDao<StudentWork> studentWorkDao;

@Override
@Transactional(readOnly = true)
public TeacherPresentationConfig getConfig(Run run, Group period, String nodeId,
String componentId) {
return teacherPresentationConfigDao.getConfig(run, period, nodeId, componentId);
}

@Override
public List<Integer> getStudentWorkIds(TeacherPresentationConfig config) {
try {
List<Map<String, Integer>> items = objectMapper.readValue(config.getItems(),
new TypeReference<List<Map<String, Integer>>>() {
});
List<Integer> ids = new ArrayList<>();
for (Map<String, Integer> 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<Integer> 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<Integer> 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;
}

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<Integer> validateStudentWorkIds(Run run, Group period, List<Integer> ids) {
Set<Integer> 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<Integer> studentWorkIds) {
List<Map<String, Integer>> 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);
}
}
}
Loading
Loading