Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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