SEBSERV-400 fixed LMS messages

This commit is contained in:
anhefti 2023-03-21 10:02:51 +01:00
parent 21200bd9a2
commit 45464762bc
15 changed files with 93 additions and 45 deletions

View file

@ -48,6 +48,7 @@ public class APIMessage implements Serializable {
FIELD_VALIDATION("1200", HttpStatus.BAD_REQUEST, "Field validation error"),
PASSWORD_MISMATCH("1300", HttpStatus.BAD_REQUEST, "new password do not match confirmed password"),
MISSING_PASSWORD("1301", HttpStatus.BAD_REQUEST, "Missing Password"),
LMS_WARNINGS("1302", HttpStatus.CONFLICT, "Warnings from LMS"),
EXAM_CONSISTENCY_VALIDATION_SUPPORTER("1400", HttpStatus.OK, "No Exam Supporter defined for the Exam"),
EXAM_CONSISTENCY_VALIDATION_CONFIG("1401", HttpStatus.OK, "No SEB Exam Configuration defined for the Exam"),

View file

@ -31,6 +31,7 @@ public final class SEBRestriction implements Entity {
public static final String ATTR_BROWSER_KEYS = "browserExamKeys";
public static final String ATTR_CONFIG_KEYS = "configKeys";
public static final String ATTR_ADDITIONAL_PROPERTIES = "additionalProperties";
public static final String ATTR_WARNING = "warningMessage";
@JsonProperty(Domain.EXAM.ATTR_ID)
public final Long examId;
@ -40,18 +41,22 @@ public final class SEBRestriction implements Entity {
public final Collection<String> browserExamKeys;
@JsonProperty(ATTR_ADDITIONAL_PROPERTIES)
public final Map<String, String> additionalProperties;
@JsonProperty(ATTR_WARNING)
public final String warningMessage;
@JsonCreator
public SEBRestriction(
@JsonProperty(Domain.EXAM.ATTR_ID) final Long examId,
@JsonProperty(ATTR_CONFIG_KEYS) final Collection<String> configKeys,
@JsonProperty(ATTR_BROWSER_KEYS) final Collection<String> browserExamKeys,
@JsonProperty(ATTR_ADDITIONAL_PROPERTIES) final Map<String, String> additionalProperties) {
@JsonProperty(ATTR_ADDITIONAL_PROPERTIES) final Map<String, String> additionalProperties,
@JsonProperty(ATTR_WARNING) final String warningMessage) {
this.examId = examId;
this.configKeys = Utils.immutableCollectionOf(configKeys);
this.browserExamKeys = Utils.immutableCollectionOf(browserExamKeys);
this.additionalProperties = Utils.immutableMapOf(additionalProperties);
this.warningMessage = warningMessage;
}
@Override
@ -170,7 +175,8 @@ public final class SEBRestriction implements Entity {
examId,
edxData.configKeys,
edxData.browserExamKeys,
attrs);
attrs,
null);
}
public static SEBRestriction from(final Long examId, final MoodleSEBRestriction restriction) {
@ -178,6 +184,7 @@ public final class SEBRestriction implements Entity {
examId,
restriction.configKeys,
restriction.browserExamKeys,
null,
null);
}

View file

@ -160,7 +160,8 @@ public class ExamSEBRestrictionSettings {
Long.parseLong(entityKey.modelId),
null,
browserKeys,
additionalAttributes);
additionalAttributes,
null);
} catch (final Exception e) {
log.error("Unexpected error while trying to get settings from form: ", e);

View file

@ -435,12 +435,14 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
log.debug("Apply course restriction: {} for LMSSetup: {}", exam, lmsSetup());
}
return this.restrictionRequest.protectedRun(() -> this.sebRestrictionAPI
final Result<SEBRestriction> protectedRun = this.restrictionRequest.protectedRun(() -> this.sebRestrictionAPI
.applySEBClientRestriction(exam, sebRestrictionData)
.onError(error -> log.error(
"Failed to apply SEB restrictions: {}",
error.getMessage()))
.getOrThrow());
return protectedRun;
}
@Override
@ -455,12 +457,20 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
log.debug("Release course restriction: {} for LMSSetup: {}", exam.externalId, lmsSetup());
}
return this.releaseRestrictionRequest.protectedRun(() -> this.sebRestrictionAPI
final Result<Exam> protectedRun = this.releaseRestrictionRequest.protectedRun(() -> this.sebRestrictionAPI
.releaseSEBClientRestriction(exam)
.onError(error -> log.error(
"Failed to release SEB restrictions: {}",
error.getMessage()))
.getOrThrow());
if (protectedRun.hasError()) {
final Throwable cause = protectedRun.getError().getCause();
if (cause.getMessage().contains("LMS Warnings")) {
return Result.ofRuntimeError(cause.getMessage());
}
}
return protectedRun;
}
}

View file

@ -27,6 +27,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.MoodleSEBRestriction;
@ -152,7 +153,8 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
exam.id,
configKeys,
browserExamKeys,
additionalAttributes);
additionalAttributes,
null);
});
}
@ -276,7 +278,13 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
.flatMap(lmsTemplate -> lmsTemplate.applySEBClientRestriction(
exam,
sebRestrictionData))
.map(data -> exam)
.map(data -> {
if (StringUtils.isNoneBlank(data.warningMessage)) {
throw new APIMessage.APIMessageException(
APIMessage.ErrorMessage.LMS_WARNINGS.of(data.warningMessage));
}
return exam;
})
.onError(error -> this.examDAO.setSEBRestriction(exam.id, false))
.getOrThrow();
})
@ -295,7 +303,9 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
log.debug(" *** SEB Restriction *** Release SEB Client restrictions from LMS for exam: {}",
exam);
}
template.releaseSEBClientRestriction(exam);
template
.releaseSEBClientRestriction(exam)
.getOrThrow();
}
return exam;

View file

@ -376,7 +376,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
final String url = String.format("/api/v2/assignments/%s", id);
final AssignmentData assignment = this.apiGet(restTemplate, url, AssignmentData.class);
final SEBServerData ts = assignment.integrations.safe_exam_browser_server;
return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap<String, String>());
return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap<String, String>(), null);
}
private SEBRestriction setRestrictionForAssignmentId(final RestTemplate restTemplate, final String id,
@ -389,7 +389,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
final AssignmentData r =
this.apiPatch(restTemplate, url, assignment, AssignmentData.class, AssignmentData.class);
final SEBServerData ts = assignment.integrations.safe_exam_browser_server;
return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap<String, String>());
return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap<String, String>(), null);
}
private SEBRestriction deleteRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) {
@ -401,7 +401,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
final AssignmentData r =
this.apiPatch(restTemplate, url, assignment, AssignmentData.class, AssignmentData.class);
final SEBServerData ts = assignment.integrations.safe_exam_browser_server;
return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap<String, String>());
return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap<String, String>(), null);
}
@Override

View file

@ -38,7 +38,7 @@ public class OpenEdxCourseRestriction implements SEBRestrictionAPI {
private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseRestriction.class);
private static final SEBRestriction NO_RESTRICTION_MARKER = new SEBRestriction(null, null, null, null);
private static final SEBRestriction NO_RESTRICTION_MARKER = new SEBRestriction(null, null, null, null, null);
private static final String OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO = "/seb-openedx/seb-info";
private static final String OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH =
"/seb-openedx/api/v1/course/%s/configuration/";

View file

@ -67,6 +67,10 @@ public interface MoodleAPIRestTemplate {
this.message = message;
}
public String getMessage() {
return this.message;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();

View file

@ -12,12 +12,14 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.LinkedMultiValueMap;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
@ -46,7 +48,6 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
public static final String ATTRIBUTE_QUIT_URL = "quitlink";
public static final String ATTRIBUTE_QUIT_SECRET = "quitsecret";
private static final String NO_RESTRICTION_SET_WARNING = "error/SEB Server is not enabled";
private static final String DELETED_RESTRICTION_WARNING = "You have deleted restriction";
private final JSONMapper jsonMapper;
@ -178,8 +179,9 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
addQuery,
queryAttributes);
if (!srJSON.contains(DELETED_RESTRICTION_WARNING)) {
log.warn("Release SEB restriction seems to failed. Moodle response: {}", srJSON);
final SEBRestriction restrictionFromJson = restrictionFromJson(exam, srJSON);
if (StringUtils.isNotBlank(restrictionFromJson.warningMessage)) {
throw new RuntimeException("LMS Warnings: " + restrictionFromJson.warningMessage);
}
return exam;
@ -195,21 +197,8 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
exam.id,
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyMap());
}
// check no restriction set
if (srJSON.contains(NO_RESTRICTION_SET_WARNING)) {
if (log.isDebugEnabled()) {
log.debug("No restriction set for exam: {}", exam);
}
return new SEBRestriction(
exam.id,
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyMap());
Collections.emptyMap(),
"Missing or blank response from Moodle");
}
// fist try to get from multiple data
@ -217,7 +206,19 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
srJSON,
MoodleUtils.MoodleQuizRestrictions.class);
return toSEBRestriction(exam, moodleRestrictions);
String warningMessages = null;
if (moodleRestrictions.warnings != null && !moodleRestrictions.warnings.isEmpty()) {
warningMessages = StringUtils.join(
moodleRestrictions.warnings
.stream()
.map(MoodleAPIRestTemplate.Warning::getMessage)
.collect(Collectors.toList()),
Constants.LIST_SEPARATOR);
log.warn("Warnings from Moodle: {}", moodleRestrictions.warnings);
}
return toSEBRestriction(exam, moodleRestrictions, warningMessages);
} catch (final Exception e) {
try {
// then try to get from single
@ -225,7 +226,7 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
srJSON,
MoodleUtils.MoodleQuizRestriction.class);
return toSEBRestriction(exam, moodleRestriction);
return toSEBRestriction(exam, moodleRestriction, null);
} catch (final Exception ee) {
throw new RuntimeException("Unexpected error while get SEB restriction: ", ee);
}
@ -234,22 +235,33 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
private SEBRestriction toSEBRestriction(
final Exam exam,
final MoodleQuizRestrictions moodleRestrictions) {
final MoodleQuizRestrictions moodleRestrictions,
final String warnings) {
if (moodleRestrictions.warnings != null && !moodleRestrictions.warnings.isEmpty()) {
log.warn("Moodle restriction call warnings: {}", moodleRestrictions.warnings);
}
if (moodleRestrictions.data == null || moodleRestrictions.data.isEmpty()) {
if (StringUtils.isNotBlank(warnings)) {
return new SEBRestriction(
exam.id,
null,
null,
null,
warnings);
} else {
throw new IllegalArgumentException("Expecting MoodleQuizRestriction not available. Exam: " + exam);
}
}
return toSEBRestriction(exam, moodleRestrictions.data.iterator().next());
return toSEBRestriction(exam, moodleRestrictions.data.iterator().next(), warnings);
}
private SEBRestriction toSEBRestriction(
final Exam exam,
final MoodleQuizRestriction moodleRestriction) {
final MoodleQuizRestriction moodleRestriction,
final String warnings) {
final Map<String, String> additionalProperties = new HashMap<>();
additionalProperties.put(ATTRIBUTE_QUIT_URL, moodleRestriction.quitlink);
@ -259,7 +271,8 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
exam.id,
moodleRestriction.configkeys,
moodleRestriction.browserkeys,
additionalProperties);
additionalProperties,
warnings);
}
private Result<MoodleAPIRestTemplate> getRestTemplate() {

View file

@ -346,7 +346,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
additionalAttributes.put(ADDITIONAL_ATTR_QUIT_SECRET, r.quitSecret);
}
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, additionalAttributes);
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, additionalAttributes, null);
}
private SEBRestriction setRestrictionForAssignmentId(
@ -364,7 +364,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
}
final RestrictionData r =
this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class);
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<String, String>());
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<String, String>(),
null);
}
private SEBRestriction deleteRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) {
@ -372,7 +373,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
final RestrictionData r = this.apiDelete(restTemplate, url, RestrictionData.class);
// OLAT returns RestrictionData with null values upon deletion.
// We return it here for consistency, even though SEB server does not need it
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<String, String>());
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<String, String>(),
null);
}
@Override

View file

@ -224,7 +224,7 @@ public class ModelObjectJSONGenerator {
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));
domainObject = new SEBRestriction(
1L, Arrays.asList("key1", "key2"), Arrays.asList("key1", "key2"), attrs);
1L, Arrays.asList("key1", "key2"), Arrays.asList("key1", "key2"), attrs, null);
System.out.println(domainObject.getClass().getSimpleName() + ":");
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));

View file

@ -34,7 +34,7 @@ public class OpenEdxSebRestrictionTest {
final JSONMapper mapper = new JSONMapper();
final OpenEdxSEBRestriction data =
OpenEdxSEBRestriction.from(new SEBRestriction(null, null, null, null));
OpenEdxSEBRestriction.from(new SEBRestriction(null, null, null, null, null));
final String json = mapper.writeValueAsString(data);
assertEquals(
"{\"CONFIG_KEYS\":[],\"BROWSER_KEYS\":[],\"WHITELIST_PATHS\":[],\"BLACKLIST_CHAPTERS\":[],\"PERMISSION_COMPONENTS\":[\"AlwaysAllowStaff\"],\"USER_BANNING_ENABLED\":false}",

View file

@ -3514,7 +3514,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
restriction.examId,
restriction.configKeys,
examKeys,
Collections.emptyMap()))
Collections.emptyMap(), null))
.call()
.getOrThrow();
assertNotNull(exam);

View file

@ -103,7 +103,7 @@ public class OlatLmsAPITemplateTest extends AdministrationAPIIntegrationTester {
2L,
Arrays.asList("configKey1", "configKey2"),
Arrays.asList("browserKey1", "browserKey2"),
null);
null, null);
olatLmsAPITemplate.applySEBClientRestriction(exam, sebRestriction);

View file

@ -72,7 +72,7 @@ public class MoodlePluginCourseRestrictionTest {
exam.id,
Lists.list("configKey1"),
Lists.list("BEK1", "BEK2"),
Collections.emptyMap());
Collections.emptyMap(), null);
assertFalse(candidate.hasSEBClientRestriction(exam));