improved LMS fail handling on exams

1. if LMS is not available the exams gets a state override and is not
running
2. if LMS lms is available but the course id is invalid, the exams gets
a state override and is not running
This commit is contained in:
anhefti 2021-03-03 13:28:33 +01:00
parent 527c005702
commit 2f8913f129
17 changed files with 150 additions and 91 deletions

View file

@ -52,7 +52,10 @@ public class APIMessage implements Serializable {
EXAM_CONSISTENCY_VALIDATION_CONFIG("1401", HttpStatus.OK, "No SEB Exam Configuration defined for the Exam"),
EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION("1402", HttpStatus.OK,
"SEB restriction API available but Exam not restricted on LMS side yet"),
EXAM_CONSISTENCY_VALIDATION_INDICATOR("1403", HttpStatus.OK, "No Indicator defined for the Exam");
EXAM_CONSISTENCY_VALIDATION_INDICATOR("1403", HttpStatus.OK, "No Indicator defined for the Exam"),
EXAM_CONSISTENCY_VALIDATION_LMS_CONNECTION("1404", HttpStatus.OK, "No Connection To LMS"),
EXAM_CONSISTENCY_VALIDATION_INVALID_ID_REFERENCE("1405", HttpStatus.OK,
"There seems to be an invalid exam - course identifier reference. The course cannot be found");
public final String messageCode;
public final HttpStatus httpStatus;

View file

@ -59,7 +59,9 @@ public final class Exam implements GrantEntity {
public enum ExamStatus {
UP_COMING,
RUNNING,
FINISHED
FINISHED,
CORRUPT_NO_LMS_CONNECTION,
CORRUPT_INVALID_ID
}
public enum ExamType {

View file

@ -82,6 +82,10 @@ public final class LmsSetupTestResult {
.anyMatch(error -> error.errorType == type);
}
public boolean hasAnyError() {
return !this.errors.isEmpty();
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();

View file

@ -35,11 +35,8 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
@ -62,7 +59,6 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckSEBRest
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.GetQuizData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.ImportAsExam;
@ -79,7 +75,7 @@ public class ExamForm implements TemplateComposer {
private static final Logger log = LoggerFactory.getLogger(ExamForm.class);
protected static final String ATTR_READ_GRANT = "ATTR_READ_GRANT";
protected static final String ATTR_MODIFY_GRANT = "ATTR_MODIFY_GRANT";
protected static final String ATTR_EDITABLE = "ATTR_EDITABLE";
protected static final String ATTR_EXAM_STATUS = "ATTR_EXAM_STATUS";
public static final LocTextKey EXAM_FORM_TITLE_KEY =
@ -118,6 +114,10 @@ public class ExamForm implements TemplateComposer {
new LocTextKey("sebserver.exam.consistency.missing-config");
private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_SEB_RESTRICTION =
new LocTextKey("sebserver.exam.consistency.missing-seb-restriction");
private final static LocTextKey CONSISTENCY_MESSAGE_VALIDATION_LMS_CONNECTION =
new LocTextKey("sebserver.exam.consistency.no-lms-connection");
private final static LocTextKey CONSISTENCY_MESSAGEINVALID_ID_REFERENCE =
new LocTextKey("sebserver.exam.consistency.invalid-lms-id");
private final Map<String, LocTextKey> consistencyMessageMapping;
private final PageService pageService;
@ -166,6 +166,12 @@ public class ExamForm implements TemplateComposer {
this.consistencyMessageMapping.put(
APIMessage.ErrorMessage.EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION.messageCode,
CONSISTENCY_MESSAGE_MISSING_SEB_RESTRICTION);
this.consistencyMessageMapping.put(
APIMessage.ErrorMessage.EXAM_CONSISTENCY_VALIDATION_LMS_CONNECTION.messageCode,
CONSISTENCY_MESSAGE_VALIDATION_LMS_CONNECTION);
this.consistencyMessageMapping.put(
APIMessage.ErrorMessage.EXAM_CONSISTENCY_VALIDATION_INVALID_ID_REFERENCE.messageCode,
CONSISTENCY_MESSAGEINVALID_ID_REFERENCE);
}
@Override
@ -218,9 +224,8 @@ public class ExamForm implements TemplateComposer {
final boolean modifyGrant = userGrantCheck.m();
final boolean writeGrant = userGrantCheck.w();
final ExamStatus examStatus = exam.getStatus();
final boolean editable = examStatus == ExamStatus.UP_COMING
|| examStatus == ExamStatus.RUNNING
&& currentUser.get().hasRole(UserRole.EXAM_ADMIN);
final boolean editable = modifyGrant && (examStatus == ExamStatus.UP_COMING ||
examStatus == ExamStatus.RUNNING);
final boolean sebRestrictionAvailable = testSEBRestrictionAPI(exam);
final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService
.getBuilder(CheckSEBRestriction.class)
@ -387,12 +392,7 @@ public class ExamForm implements TemplateComposer {
.newAction(ActionDefinition.EXAM_MODIFY_SEB_RESTRICTION_DETAILS)
.withEntityKey(entityKey)
.withExec(this.examSEBRestrictionSettings.settingsFunction(this.pageService))
.withAttribute(
ExamSEBRestrictionSettings.PAGE_CONTEXT_ATTR_LMS_TYPE,
this.restService.getBuilder(GetLmsSetup.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(exam.lmsSetupId))
.call()
.getOrThrow().lmsType.name())
.withAttribute(ExamSEBRestrictionSettings.PAGE_CONTEXT_ATTR_LMS_ID, String.valueOf(exam.lmsSetupId))
.withAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY, String.valueOf(!modifyGrant))
.noEventPropagation()
.publishIf(() -> sebRestrictionAvailable && readonly)
@ -433,7 +433,7 @@ public class ExamForm implements TemplateComposer {
formContext
.copyOf(content)
.withAttribute(ATTR_READ_GRANT, String.valueOf(userGrantCheck.r()))
.withAttribute(ATTR_MODIFY_GRANT, String.valueOf(modifyGrant))
.withAttribute(ATTR_EDITABLE, String.valueOf(editable))
.withAttribute(ATTR_EXAM_STATUS, examStatus.name()));
// Indicators
@ -441,7 +441,7 @@ public class ExamForm implements TemplateComposer {
formContext
.copyOf(content)
.withAttribute(ATTR_READ_GRANT, String.valueOf(userGrantCheck.r()))
.withAttribute(ATTR_MODIFY_GRANT, String.valueOf(modifyGrant))
.withAttribute(ATTR_EDITABLE, String.valueOf(editable))
.withAttribute(ATTR_EXAM_STATUS, examStatus.name()));
}
}
@ -467,11 +467,7 @@ public class ExamForm implements TemplateComposer {
}
private boolean testSEBRestrictionAPI(final Exam exam) {
final Result<LmsSetup> lmsSetupCall = this.restService.getBuilder(GetLmsSetup.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(exam.lmsSetupId))
.call();
if (!lmsSetupCall.hasError() && !lmsSetupCall.get().lmsType.features.contains(Features.SEB_RESTRICTION)) {
if (exam.status == ExamStatus.CORRUPT_NO_LMS_CONNECTION || exam.status == ExamStatus.CORRUPT_INVALID_ID) {
return false;
}

View file

@ -26,7 +26,6 @@ import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
@ -41,7 +40,6 @@ import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteExamConfigMapping;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMappingsPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.EntityTable;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@ -89,20 +87,15 @@ public class ExamFormConfigs implements TemplateComposer {
@Override
public void compose(final PageContext pageContext) {
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final Composite content = pageContext.getParent();
final EntityKey entityKey = pageContext.getEntityKey();
final boolean modifyGrant = BooleanUtils.toBoolean(
pageContext.getAttribute(ExamForm.ATTR_MODIFY_GRANT));
final boolean editable = BooleanUtils.toBoolean(
pageContext.getAttribute(ExamForm.ATTR_EDITABLE));
final boolean readGrant = BooleanUtils.toBoolean(
pageContext.getAttribute(ExamForm.ATTR_READ_GRANT));
final ExamStatus examStatus = ExamStatus.valueOf(
pageContext.getAttribute(ExamForm.ATTR_EXAM_STATUS));
final boolean isExamRunning = examStatus == ExamStatus.RUNNING;
final boolean editable = examStatus == ExamStatus.UP_COMING
|| examStatus == ExamStatus.RUNNING
&& currentUser.get().hasRole(UserRole.EXAM_ADMIN);
// List of SEB Configuration
this.widgetFactory.addFormSubContextHeader(
@ -134,7 +127,7 @@ public class ExamFormConfigs implements TemplateComposer {
this.resourceService::localizedExamConfigStatusName)
.widthProportion(1))
.withDefaultActionIf(
() -> true,
() -> readGrant,
this::viewExamConfigPageAction)
.withSelectionListener(this.pageService.getSelectionPublisher(
@ -162,12 +155,12 @@ public class ExamFormConfigs implements TemplateComposer {
.withParentEntityKey(entityKey)
.withExec(this.examToConfigBindingForm.bindFunction())
.noEventPropagation()
.publishIf(() -> modifyGrant && editable && !configurationTable.hasAnyContent())
.publishIf(() -> editable && !configurationTable.hasAnyContent())
.newAction(ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP)
.withParentEntityKey(entityKey)
.withEntityKey(configMapKey)
.publishIf(() -> modifyGrant && configurationTable.hasAnyContent(), false)
.publishIf(() -> readGrant && configurationTable.hasAnyContent(), false)
.newAction(ActionDefinition.EXAM_CONFIGURATION_DELETE_FROM_LIST)
.withEntityKey(entityKey)
@ -181,7 +174,7 @@ public class ExamFormConfigs implements TemplateComposer {
}
return null;
})
.publishIf(() -> modifyGrant && configurationTable.hasAnyContent() && editable, false)
.publishIf(() -> editable && configurationTable.hasAnyContent() && editable, false)
.newAction(ActionDefinition.EXAM_CONFIGURATION_GET_CONFIG_KEY)
.withSelect(

View file

@ -72,8 +72,8 @@ public class ExamFormIndicators implements TemplateComposer {
public void compose(final PageContext pageContext) {
final Composite content = pageContext.getParent();
final EntityKey entityKey = pageContext.getEntityKey();
final boolean modifyGrant = BooleanUtils.toBoolean(
pageContext.getAttribute(ExamForm.ATTR_MODIFY_GRANT));
final boolean editable = BooleanUtils.toBoolean(
pageContext.getAttribute(ExamForm.ATTR_EDITABLE));
// List of Indicators
this.widgetFactory.addFormSubContextHeader(
@ -111,7 +111,7 @@ public class ExamFormIndicators implements TemplateComposer {
.asMarkup()
.widthProportion(4))
.withDefaultActionIf(
() -> modifyGrant,
() -> editable,
() -> actionBuilder
.newAction(ActionDefinition.EXAM_INDICATOR_MODIFY_FROM_LIST)
.withParentEntityKey(entityKey)
@ -132,7 +132,7 @@ public class ExamFormIndicators implements TemplateComposer {
indicatorTable::getSelection,
PageAction::applySingleSelectionAsEntityKey,
INDICATOR_EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> modifyGrant && indicatorTable.hasAnyContent(), false)
.publishIf(() -> editable && indicatorTable.hasAnyContent(), false)
.newAction(ActionDefinition.EXAM_INDICATOR_DELETE_FROM_LIST)
.withEntityKey(entityKey)
@ -140,11 +140,11 @@ public class ExamFormIndicators implements TemplateComposer {
indicatorTable::getSelection,
this::deleteSelectedIndicator,
INDICATOR_EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> modifyGrant && indicatorTable.hasAnyContent(), false)
.publishIf(() -> editable && indicatorTable.hasAnyContent(), false)
.newAction(ActionDefinition.EXAM_INDICATOR_NEW)
.withParentEntityKey(entityKey)
.publishIf(() -> modifyGrant);
.publishIf(() -> editable);
}

View file

@ -254,7 +254,7 @@ public class ExamList implements TemplateComposer {
final Exam exam,
final PageService pageService) {
if (exam.getStatus() != ExamStatus.RUNNING) {
if (exam.getStatus() == ExamStatus.UP_COMING || exam.getStatus() == ExamStatus.FINISHED) {
return;
}

View file

@ -51,6 +51,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeactivateSE
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetCourseChapters;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetSEBRestrictionSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveSEBRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup;
@Lazy
@Component
@ -81,7 +82,7 @@ public class ExamSEBRestrictionSettings {
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_USER_BANNING_ENABLED =
new LocTextKey("sebserver.exam.form.sebrestriction.USER_BANNING_ENABLED");
static final String PAGE_CONTEXT_ATTR_LMS_TYPE = "ATTR_LMS_TYPE";
static final String PAGE_CONTEXT_ATTR_LMS_ID = "ATTR_LMS_ID";
Function<PageAction, PageAction> settingsFunction(final PageService pageService) {
@ -126,7 +127,7 @@ public class ExamSEBRestrictionSettings {
}
final EntityKey entityKey = pageContext.getEntityKey();
final LmsType lmsType = getLmsType(pageContext);
final LmsType lmsType = getLmsType(pageService, pageContext.getAttribute(PAGE_CONTEXT_ATTR_LMS_ID));
SEBRestriction bodyValue = null;
try {
final Form form = formHandle.getForm();
@ -194,7 +195,9 @@ public class ExamSEBRestrictionSettings {
final RestService restService = this.pageService.getRestService();
final ResourceService resourceService = this.pageService.getResourceService();
final EntityKey entityKey = this.pageContext.getEntityKey();
final LmsType lmsType = getLmsType(this.pageContext);
final LmsType lmsType = getLmsType(
this.pageService,
this.pageContext.getAttribute(PAGE_CONTEXT_ATTR_LMS_ID));
final boolean isReadonly = BooleanUtils.toBoolean(
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
@ -305,9 +308,16 @@ public class ExamSEBRestrictionSettings {
}
private LmsType getLmsType(final PageContext pageContext) {
private LmsType getLmsType(final PageService pageService, final String lmsSetupId) {
try {
return LmsType.valueOf(pageContext.getAttribute(PAGE_CONTEXT_ATTR_LMS_TYPE));
return pageService
.getRestService()
.getBuilder(GetLmsSetup.class)
.withURIVariable(API.PARAM_MODEL_ID, lmsSetupId)
.call()
.getOrThrow().lmsType;
} catch (final Exception e) {
return null;
}

View file

@ -102,7 +102,7 @@ public class ExamDAOImpl implements ExamDAO {
@Transactional(readOnly = true)
public Result<GrantEntity> examGrantEntityByPK(final Long id) {
return recordById(id)
.map(record -> toDomainModel(record, null).getOrThrow());
.map(record -> toDomainModel(record, null, null).getOrThrow());
}
@Override
@ -111,7 +111,7 @@ public class ExamDAOImpl implements ExamDAO {
return Result.tryCatch(() -> this.clientConnectionRecordMapper
.selectByPrimaryKey(connectionId))
.flatMap(ccRecord -> recordById(ccRecord.getExamId()))
.map(record -> toDomainModel(record, null).getOrThrow());
.map(record -> toDomainModel(record, null, null).getOrThrow());
}
@Override
@ -793,17 +793,48 @@ public class ExamDAOImpl implements ExamDAO {
final Map<String, QuizData> quizzes = this.lmsAPIService
.getLmsAPITemplate(lmsSetupId)
.map(template -> getQuizzesFromLMS(template, recordMapping.keySet(), cached))
.getOrThrow()
.onError(error -> log.error("Failed to get quizzes for exams: ", error))
.getOr(Collections.emptyList())
.stream()
.flatMap(Result::skipOnError)
.collect(Collectors.toMap(q -> q.id, Function.identity()));
if (records.size() != quizzes.size()) {
// Check if we have LMS connection to verify the source of the exam quiz mismatch
final LmsAPITemplate lmsSetup = this.lmsAPIService
.getLmsAPITemplate(lmsSetupId)
.getOrThrow();
if (log.isDebugEnabled()) {
log.debug("Quizzes size mismatch detected by getting exams quiz data from LMS: {}", lmsSetup);
}
if (lmsSetup.testCourseAccessAPI().hasAnyError()) {
// No course access on the LMS. This means we can't get any quizzes from this LMSSetup at the moment
// All exams are marked as corrupt because of LMS Setup failure
log.warn("Failed to get quizzes form LMS Setup. No access to LMS {}", lmsSetup);
return recordMapping.entrySet()
.stream()
.map(entry -> toDomainModel(
entry.getValue(),
null,
ExamStatus.CORRUPT_NO_LMS_CONNECTION)
.getOr(null))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
// collect Exam's
return recordMapping.entrySet()
.stream()
.map(entry -> toDomainModel(
entry.getValue(),
getQuizData(quizzes, entry.getKey(), entry.getValue()))
getQuizData(quizzes, entry.getKey(), entry.getValue()),
ExamStatus.CORRUPT_INVALID_ID)
.onError(error -> log.error(
"Failed to get quiz data from remote LMS for exam: ",
error))
@ -813,8 +844,11 @@ public class ExamDAOImpl implements ExamDAO {
});
}
private Collection<Result<QuizData>> getQuizzesFromLMS(final LmsAPITemplate template, final Set<String> ids,
private Collection<Result<QuizData>> getQuizzesFromLMS(
final LmsAPITemplate template,
final Set<String> ids,
final boolean cached) {
try {
return (cached)
? template.getQuizzesFromCache(ids)
@ -840,6 +874,7 @@ public class ExamDAOImpl implements ExamDAO {
// a case by using the short name of the quiz and search for the quiz within the course with this
// short name. If one quiz has been found that matches all criteria, we adapt the internal id
// mapping to this quiz.
// If recovering fails, this returns null and the calling side must handle the lack of quiz data
try {
final LmsSetup lmsSetup = this.lmsAPIService
.getLmsSetup(record.getLmsSetupId())
@ -914,7 +949,8 @@ public class ExamDAOImpl implements ExamDAO {
private Result<Exam> toDomainModel(
final ExamRecord record,
final QuizData quizData) {
final QuizData quizData,
final ExamStatus statusOverride) {
return Result.tryCatch(() -> {
@ -943,7 +979,7 @@ public class ExamDAOImpl implements ExamDAO {
ExamType.valueOf(record.getType()),
record.getOwner(),
supporter,
status,
(quizData != null) ? status : (statusOverride != null) ? statusOverride : status,
record.getBrowserKeys(),
BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : null),
record.getLastupdate());

View file

@ -191,7 +191,7 @@ public class MoodleCourseAccess extends CourseAccess {
if (restTemplateRequest.hasError()) {
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " +
this.moodleRestTemplateFactory.knownTokenAccessPaths;
log.error(message + " cause: ", restTemplateRequest.getError());
log.error(message + " cause: {}", restTemplateRequest.getError().getMessage());
return LmsSetupTestResult.ofTokenRequestError(message);
}
@ -213,14 +213,14 @@ public class MoodleCourseAccess extends CourseAccess {
protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) {
return () -> getRestTemplate()
.map(template -> getQuizzesForIds(template, ids))
.getOrThrow();
.getOr(Collections.emptyList());
}
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
return () -> getRestTemplate()
.map(template -> collectAllQuizzes(template, filterMap))
.getOrThrow();
.getOr(Collections.emptyList());
}
@Override
@ -380,17 +380,7 @@ public class MoodleCourseAccess extends CourseAccess {
return Collections.emptyList();
}
if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) {
log.warn(
"There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}",
this.lmsSetup,
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME,
courseQuizData.warnings.size(),
courseQuizData.warnings.iterator().next().toString());
if (log.isTraceEnabled()) {
log.trace("All warnings from Moodle: {}", courseQuizData.warnings.toString());
}
}
logMoodleWarnings(courseQuizData.warnings);
if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
log.error("No quizzes found for ids: {} on LMS; {}", quizIds, this.lmsSetup.name);
@ -461,17 +451,7 @@ public class MoodleCourseAccess extends CourseAccess {
Collections.emptyList();
}
if (courses.warnings != null && !courses.warnings.isEmpty()) {
log.warn(
"There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}",
this.lmsSetup,
MoodleCourseAccess.MOODLE_COURSE_BY_FIELD_API_FUNCTION_NAME,
courses.warnings.size(),
courses.warnings.iterator().next().toString());
if (log.isTraceEnabled()) {
log.trace("All warnings from Moodle: {}", courses.warnings.toString());
}
}
logMoodleWarnings(courses.warnings);
if (courses.courses == null || courses.courses.isEmpty()) {
log.error("No courses found for ids: {} on LMS: {}", ids, this.lmsSetup.name);
@ -643,6 +623,21 @@ public class MoodleCourseAccess extends CourseAccess {
return idNumber.equals(Constants.EMPTY_NOTE) ? null : idNumber;
}
private void logMoodleWarnings(final Collection<Warning> warnings) {
if (warnings != null && !warnings.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug(
"There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}",
this.lmsSetup,
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME,
warnings.size(),
warnings.iterator().next().toString());
} else if (log.isTraceEnabled()) {
log.trace("All warnings from Moodle: {}", warnings.toString());
}
}
}
private static final Pattern ACCESS_DENIED_PATTERN_1 =
Pattern.compile(Pattern.quote("No access rights"), Pattern.CASE_INSENSITIVE);
private static final Pattern ACCESS_DENIED_PATTERN_2 =

View file

@ -117,7 +117,7 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) {
log.debug("Get SEB Client restriction for Exam: {}", exam);
log.debug("Get SEB Client restriction for Exam: {}", exam.externalId);
}
return this.moodleCourseRestriction

View file

@ -128,14 +128,18 @@ class MoodleRestTemplateFactory {
.map(this::createRestTemplate)
.map(result -> {
if (result.hasError()) {
log.error("Failed to get access token: ", result.getError());
log.warn("Failed to get access token for LMS: {}({})",
this.lmsSetup.name,
this.lmsSetup.id);
}
return result;
})
.filter(Result::hasValue)
.findFirst()
.orElse(Result.ofRuntimeError(
"Failed to gain any access on paths: " + this.knownTokenAccessPaths));
"Failed to gain any access for LMS " +
this.lmsSetup.name + "(" + this.lmsSetup.id +
") on paths: " + this.knownTokenAccessPaths));
}
Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) {
@ -146,7 +150,9 @@ class MoodleRestTemplateFactory {
final CharSequence accessToken = template.getAccessToken();
if (accessToken == null) {
throw new RuntimeException("Failed to gain access token on path: " + accessTokenPath);
throw new RuntimeException("Failed to get access token for LMS " +
this.lmsSetup.name + "(" + this.lmsSetup.id +
") on path: " + accessTokenPath);
}
return template;

View file

@ -64,6 +64,7 @@ public interface ExamSessionService {
/** Use this to check the consistency of a running Exam.
* Current consistency checks are:
* - Check if there is at least one Exam supporter attached to the Exam
* - Check if we have access to LMS for the exam
* - Check if there is one default SEB Exam Configuration attached to the Exam
* - Check if SEB restriction API is available and the exam is running but not yet restricted on LMS side
* - Check if there is at least one Indicator defined for the monitoring of the Exam
@ -71,7 +72,7 @@ public interface ExamSessionService {
* @param examId the identifier of the Exam to check
* @return Result of one APIMessage per consistency check if the check failed. An empty Collection of everything is
* okay. */
Result<Collection<APIMessage>> checkRunningExamConsistency(Long examId);
Result<Collection<APIMessage>> checkExamConsistency(Long examId);
/** Use this to check if a specified Exam has currently active SEB Client connections.
*

View file

@ -133,7 +133,8 @@ public class ExamSessionCacheService {
case RUNNING: {
return true;
}
case UP_COMING: {
case UP_COMING:
case FINISHED: {
return this.examUpdateHandler.updateRunning(exam.id)
.map(e -> e.status == ExamStatus.RUNNING)
.getOr(false);

View file

@ -121,14 +121,22 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}
@Override
public Result<Collection<APIMessage>> checkRunningExamConsistency(final Long examId) {
public Result<Collection<APIMessage>> checkExamConsistency(final Long examId) {
return Result.tryCatch(() -> {
final Collection<APIMessage> result = new ArrayList<>();
if (isExamRunning(examId)) {
final Exam exam = getRunningExam(examId)
.getOrThrow();
final Exam exam = this.examDAO.byPK(examId)
.getOrThrow();
// check lms connection
if (exam.status == ExamStatus.CORRUPT_NO_LMS_CONNECTION) {
result.add(ErrorMessage.EXAM_CONSISTENCY_VALIDATION_LMS_CONNECTION.of(exam.getModelId()));
}
if (exam.status == ExamStatus.CORRUPT_INVALID_ID) {
result.add(ErrorMessage.EXAM_CONSISTENCY_VALIDATION_INVALID_ID_REFERENCE.of(exam.getModelId()));
}
if (exam.status == ExamStatus.RUNNING) {
// check exam supporter
if (exam.getSupporter().isEmpty()) {
result.add(ErrorMessage.EXAM_CONSISTENCY_VALIDATION_SUPPORTER.of(exam.getModelId()));

View file

@ -246,7 +246,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
checkReadPrivilege(institutionId);
return this.examSessionService
.checkRunningExamConsistency(modelId)
.checkExamConsistency(modelId)
.getOrThrow();
}

View file

@ -430,6 +430,8 @@ sebserver.exam.consistency.missing-supporter= - There are no Exam Supporter defi
sebserver.exam.consistency.missing-indicator= - There is no indicator defined for this exam. Use 'Add Indicator" on the right to add an indicator.
sebserver.exam.consistency.missing-config= - There is no configuration defined for this exam. Use 'Add Configuration' to attach one.
sebserver.exam.consistency.missing-seb-restriction= - There is currently no SEB restriction applied on the LMS side. Use 'Enable SEB Restriction' on the right to activate auto-restriction.<br/> Or if this is not possible consider doing it manually on the LMS.
sebserver.exam.consistency.no-lms-connection= - Failed to connect to the LMS Setup of this exam yet.<br/>Please check the LMS connection within the LMS Setup.
sebserver.exam.consistency.invalid-lms-id= - The referencing course identifier seems to be invalid.<br/>Please check if the course for this exam still exists on the LMS and the course identifier has not changed.
sebserver.exam.confirm.remove-config=This exam is current running. The remove of the attached configuration will led to an invalid state<br/>where connecting SEB clients cannot download the configuration for the exam.<br/><br/>Are you sure to remove the configuration?
sebserver.exam.action.list=Exam
@ -530,6 +532,8 @@ sebserver.exam.type.VDI.tooltip=Exam type specified for Virtual Desktop Infrastr
sebserver.exam.status.UP_COMING=Up Coming
sebserver.exam.status.RUNNING=Running
sebserver.exam.status.FINISHED=Finished
sebserver.exam.status.CORRUPT_NO_LMS_CONNECTION=Corrupt (No LMS Connection)
sebserver.exam.status.CORRUPT_INVALID_ID=Corrupt (Invalid Identifier)
sebserver.exam.configuration.list.actions=
sebserver.exam.configuration.list.title=Exam Configuration