SEBSERV-301 SEBSERV-372
This commit is contained in:
parent
b9962a2609
commit
2434fce036
26 changed files with 548 additions and 369 deletions
|
@ -63,6 +63,8 @@ public final class Exam implements GrantEntity {
|
|||
|
||||
public static final String ATTR_ADDITIONAL_ATTRIBUTES = "additionalAttributes";
|
||||
|
||||
/** This attribute name is used to store the number of quiz recover attempts done by exam update process */
|
||||
public static final String ADDITIONAL_ATTR_QUIZ_RECOVER_ATTEMPTS = "QUIZ_RECOVER_ATTEMPTS";
|
||||
/** This attribute name is used on exams to store the flag for indicating the signature key check */
|
||||
public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED = "SIGNATURE_KEY_CHECK_ENABLED";
|
||||
/** This attribute name is used to store the signature check grant threshold for numerical trust checks */
|
||||
|
|
|
@ -19,6 +19,7 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
|
|||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class MoodleSEBRestriction {
|
||||
|
||||
public static final String ATTR_ALT_BEK = "ALTERNATIVE_SEB_BEK";
|
||||
public static final String ATTR_BROWSER_KEYS = "BROWSER_KEYS";
|
||||
public static final String ATTR_CONFIG_KEYS = "CONFIG_KEYS";
|
||||
|
||||
|
|
|
@ -240,7 +240,9 @@ public class ExamForm implements TemplateComposer {
|
|||
.call()
|
||||
.onError(e -> log.error("Unexpected error while trying to verify seb restriction settings: ", e))
|
||||
.getOr(false);
|
||||
final boolean sebRestrictionMismatch = isRestricted != exam.sebRestriction;
|
||||
final boolean sebRestrictionMismatch = readonly &&
|
||||
sebRestrictionAvailable &&
|
||||
isRestricted != exam.sebRestriction;
|
||||
|
||||
// check exam consistency and inform the user if needed
|
||||
Collection<APIMessage> warnings = null;
|
||||
|
@ -669,14 +671,6 @@ public class ExamForm implements TemplateComposer {
|
|||
}
|
||||
}
|
||||
|
||||
private void showSEBRestrictionMismatchMessage(final Composite parent) {
|
||||
final Composite warningPanel = this.widgetFactory.createWarningPanel(parent);
|
||||
this.widgetFactory.labelLocalized(
|
||||
warningPanel,
|
||||
CustomVariant.MESSAGE,
|
||||
CONSISTENCY_MESSAGE_SEB_RESTRICTION_MISMATCH);
|
||||
}
|
||||
|
||||
private Result<Exam> getExistingExam(final PageContext pageContext) {
|
||||
final EntityKey entityKey = pageContext.getEntityKey();
|
||||
return this.restService.getBuilder(GetExam.class)
|
||||
|
|
|
@ -29,6 +29,7 @@ import ch.ethz.seb.sebserver.gbl.Constants;
|
|||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.MoodleSEBRestriction;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
||||
|
@ -73,6 +74,8 @@ public class ExamSEBRestrictionSettings {
|
|||
new LocTextKey("sebserver.exam.form.sebrestriction.configKeys");
|
||||
private final static LocTextKey SEB_RESTRICTION_FORM_BROWSER_KEYS =
|
||||
new LocTextKey("sebserver.exam.form.sebrestriction.browserExamKeys");
|
||||
private final static LocTextKey SEB_RESTRICTION_FORM_MOODLE_ALT_BEK_KEY =
|
||||
new LocTextKey("sebserver.exam.form.sebrestriction.ALT_BEK_KEY");
|
||||
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_WHITE_LIST_PATHS =
|
||||
new LocTextKey("sebserver.exam.form.sebrestriction.WHITELIST_PATHS");
|
||||
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_PERMISSIONS =
|
||||
|
@ -244,6 +247,16 @@ public class ExamSEBRestrictionSettings {
|
|||
.asArea(50)
|
||||
.readonly(true))
|
||||
|
||||
.addFieldIf(
|
||||
() -> lmsType == LmsType.MOODLE_PLUGIN,
|
||||
() -> FormBuilder.text(
|
||||
MoodleSEBRestriction.ATTR_ALT_BEK,
|
||||
SEB_RESTRICTION_FORM_MOODLE_ALT_BEK_KEY,
|
||||
sebRestriction
|
||||
.getAdditionalProperties()
|
||||
.get(MoodleSEBRestriction.ATTR_ALT_BEK))
|
||||
.readonly(true))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
SEBRestriction.ATTR_BROWSER_KEYS,
|
||||
SEB_RESTRICTION_FORM_BROWSER_KEYS,
|
||||
|
|
|
@ -44,12 +44,6 @@ public interface ExamAdminService {
|
|||
* @return The exam with the initial additional attributes */
|
||||
Result<Exam> initAdditionalAttributes(final Exam exam);
|
||||
|
||||
/** Saves additional attributes for the exam that are specific to a type of LMS
|
||||
*
|
||||
* @param exam The Exam to add the LMS specific attributes
|
||||
* @return Result refer to the created exam or to an error when happened */
|
||||
Result<Exam> saveLMSAttributes(Exam exam);
|
||||
|
||||
/** Saves the security key settings for an specific exam.
|
||||
*
|
||||
* @param institutionId The institution identifier
|
||||
|
|
|
@ -43,11 +43,11 @@ public interface ExamTemplateService {
|
|||
* @return Result refer to the Exam with added client groups or to an error if happened */
|
||||
Result<Exam> addDefinedClientGroups(Exam exam);
|
||||
|
||||
/** Initializes additional attributes for a specified Exam on creation.
|
||||
/** Initializes additional exam template attributes for a specified Exam on creation.
|
||||
*
|
||||
* @param exam The Exam to add the default indicator
|
||||
* @return Result refer to the created exam or to an error when happened */
|
||||
Result<Exam> initAdditionalAttributes(Exam exam);
|
||||
Result<Exam> initAdditionalTemplateAttributes(Exam exam);
|
||||
|
||||
/** Initializes a pre defined exam configuration. The configuration template to create a exam configuration
|
||||
* is defined by a given linked exam template. This is used to create the exam configuration and automatically
|
||||
|
|
|
@ -118,7 +118,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
KeyGenerators.string().generateKey().toString());
|
||||
|
||||
return exam;
|
||||
});
|
||||
}).flatMap(this::initAdditionalAttributesForMoodleExams);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -186,11 +186,6 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Exam> saveLMSAttributes(final Exam exam) {
|
||||
return initAdditionalAttributesForMoodleExams(exam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Boolean> isRestricted(final Exam exam) {
|
||||
if (exam == null) {
|
||||
|
@ -254,6 +249,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
.getLmsAPITemplate(exam.lmsSetupId)
|
||||
.getOrThrow();
|
||||
|
||||
// TODO check if this is still needed
|
||||
if (lmsTemplate.lmsSetup().lmsType == LmsType.MOODLE) {
|
||||
lmsTemplate.getQuiz(exam.externalId)
|
||||
.flatMap(quizData -> this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
|
@ -277,7 +273,8 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
EntityType.EXAM,
|
||||
exam.id,
|
||||
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK,
|
||||
moodleBEK).getOrThrow();
|
||||
moodleBEK)
|
||||
.getOrThrow();
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to create additional moodle SEB BEK attribute: ", e);
|
||||
}
|
||||
|
|
|
@ -57,7 +57,13 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
|
|||
.getDefaultConfigurationNode(examId)
|
||||
.flatMap(nodeId -> this.configurationDAO.getConfigurationLastStableVersion(nodeId))
|
||||
.map(config -> config.id)
|
||||
.getOrThrow();
|
||||
.onError(error -> log.warn("Failed to get default Exam Config for exam: {} cause: {}",
|
||||
examId, error.getMessage()))
|
||||
.getOr(null);
|
||||
|
||||
if (configId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Long attrId = this.configurationAttributeDAO
|
||||
.getAttributeIdByName(configAttributeName)
|
||||
|
@ -99,20 +105,22 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
|
|||
log.error("Failed to get SEB restriction with quit secret: ", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuitLink(final Long examId) {
|
||||
try {
|
||||
|
||||
return getMappedDefaultConfigAttributeValue(
|
||||
final String quitLink = getMappedDefaultConfigAttributeValue(
|
||||
examId,
|
||||
CONFIG_ATTR_NAME_QUIT_LINK);
|
||||
|
||||
return (quitLink != null) ? quitLink : StringUtils.EMPTY;
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to get SEB restriction with quit link: ", e);
|
||||
return null;
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO
|
|||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamTemplateDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateService;
|
||||
|
||||
@Lazy
|
||||
|
@ -61,7 +60,7 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
|
|||
private static final Logger log = LoggerFactory.getLogger(ExamTemplateServiceImpl.class);
|
||||
|
||||
private final AdditionalAttributesDAO additionalAttributesDAO;
|
||||
private final ExamAdminService examAdminService;
|
||||
|
||||
private final ExamTemplateDAO examTemplateDAO;
|
||||
private final ConfigurationNodeDAO configurationNodeDAO;
|
||||
private final ExamConfigurationMapDAO examConfigurationMapDAO;
|
||||
|
@ -78,7 +77,6 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
|
|||
|
||||
public ExamTemplateServiceImpl(
|
||||
final AdditionalAttributesDAO additionalAttributesDAO,
|
||||
final ExamAdminService examAdminService,
|
||||
final ExamTemplateDAO examTemplateDAO,
|
||||
final ConfigurationNodeDAO configurationNodeDAO,
|
||||
final ExamConfigurationMapDAO examConfigurationMapDAO,
|
||||
|
@ -97,7 +95,6 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
|
|||
this.configurationNodeDAO = configurationNodeDAO;
|
||||
this.examConfigurationMapDAO = examConfigurationMapDAO;
|
||||
this.additionalAttributesDAO = additionalAttributesDAO;
|
||||
this.examAdminService = examAdminService;
|
||||
this.indicatorDAO = indicatorDAO;
|
||||
this.clientGroupDAO = clientGroupDAO;
|
||||
this.jsonMapper = jsonMapper;
|
||||
|
@ -155,41 +152,40 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Result<Exam> initAdditionalAttributes(final Exam exam) {
|
||||
return this.examAdminService
|
||||
.saveLMSAttributes(exam)
|
||||
.map(_exam -> {
|
||||
public Result<Exam> initAdditionalTemplateAttributes(final Exam exam) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
if (exam.examTemplateId != null) {
|
||||
if (exam.examTemplateId != null) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Init exam: {} with additional attributes from exam template: {}",
|
||||
exam.externalId,
|
||||
exam.examTemplateId);
|
||||
}
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Init exam: {} with additional attributes from exam template: {}",
|
||||
exam.externalId,
|
||||
exam.examTemplateId);
|
||||
}
|
||||
|
||||
final ExamTemplate examTemplate = this.examTemplateDAO
|
||||
.byPK(exam.examTemplateId)
|
||||
.onError(error -> log.warn("No exam template found for id: {}",
|
||||
exam.examTemplateId,
|
||||
error.getMessage()))
|
||||
.getOr(null);
|
||||
final ExamTemplate examTemplate = this.examTemplateDAO
|
||||
.byPK(exam.examTemplateId)
|
||||
.onError(error -> log.warn("No exam template found for id: {}",
|
||||
exam.examTemplateId,
|
||||
error.getMessage()))
|
||||
.getOr(null);
|
||||
|
||||
if (examTemplate == null) {
|
||||
return exam;
|
||||
}
|
||||
if (examTemplate == null) {
|
||||
return exam;
|
||||
}
|
||||
|
||||
if (examTemplate.examAttributes != null && !examTemplate.examAttributes.isEmpty()) {
|
||||
this.additionalAttributesDAO.saveAdditionalAttributes(
|
||||
EntityType.EXAM,
|
||||
exam.getId(),
|
||||
examTemplate.examAttributes);
|
||||
}
|
||||
}
|
||||
return _exam;
|
||||
}).onError(error -> log.error(
|
||||
"Failed to create additional attributes defined by template for exam: ",
|
||||
error));
|
||||
if (examTemplate.examAttributes != null && !examTemplate.examAttributes.isEmpty()) {
|
||||
this.additionalAttributesDAO.saveAdditionalAttributes(
|
||||
EntityType.EXAM,
|
||||
exam.getId(),
|
||||
examTemplate.examAttributes);
|
||||
}
|
||||
}
|
||||
|
||||
return exam;
|
||||
}).onError(error -> log.error(
|
||||
"Failed to create additional attributes defined by template for exam: ",
|
||||
error));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
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.LmsSetupTestResult;
|
||||
|
@ -81,6 +82,12 @@ public interface CourseAccessAPI {
|
|||
* @return Result refer to the quiz data or to an error when happened */
|
||||
Result<QuizData> getQuiz(final String id);
|
||||
|
||||
/** Tries to recover dangling exam that has lost its quiz data with the id mapping.
|
||||
*
|
||||
* @param exam The dangling exam to try to recover
|
||||
* @return Result referring to the recovered QuizData or to an error when happened */
|
||||
Result<QuizData> tryRecoverQuizForExam(Exam exam);
|
||||
|
||||
/** Clears the underling caches if there are some for a particular implementation. */
|
||||
void clearCourseCache();
|
||||
|
||||
|
|
|
@ -19,8 +19,7 @@ public interface SEBRestrictionService {
|
|||
|
||||
String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key";
|
||||
/** This attribute name is used to store the per Moolde(plugin) exam generated alternative BEK */
|
||||
String ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK =
|
||||
SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX + "ALTERNATIVE_SEB_BEK";
|
||||
String ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK = "ALTERNATIVE_SEB_BEK";
|
||||
|
||||
/** Get the LmsAPIService that is used by the SEBRestrictionService */
|
||||
LmsAPIService getLmsAPIService();
|
||||
|
|
|
@ -281,6 +281,26 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
|
|||
.getOrThrow());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
|
||||
|
||||
if (this.courseAccessAPI == null) {
|
||||
return Result
|
||||
.ofError(new UnsupportedOperationException("Course API Not Supported For: " + getType().name()));
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Try to recover quiz for exam {} for LMSSetup: {}", exam, lmsSetup());
|
||||
}
|
||||
|
||||
return this.quizRequest.protectedRun(() -> this.courseAccessAPI
|
||||
.tryRecoverQuizForExam(exam)
|
||||
.onError(error -> log.error(
|
||||
"Failed to run protectedQuizRecoverRequest: {}",
|
||||
error.getMessage()))
|
||||
.getOrThrow());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCourseCache() {
|
||||
if (this.courseAccessAPI != null) {
|
||||
|
@ -372,8 +392,6 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
|
|||
log.debug("Get course restriction: {} for LMSSetup: {}", exam.externalId, lmsSetup());
|
||||
}
|
||||
|
||||
System.out.println("******************* getSEBClientRestriction");
|
||||
|
||||
return this.restrictionRequest.protectedRun(() -> this.sebRestrictionAPI
|
||||
.getSEBClientRestriction(exam)
|
||||
.onError(error -> log.error("Failed to get SEB restrictions: {}", error.getMessage()))
|
||||
|
|
|
@ -29,9 +29,11 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
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;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
|
||||
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.LmsSetup.LmsType;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
|
||||
|
@ -135,6 +137,16 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
|
|||
e);
|
||||
}
|
||||
|
||||
// special Moodle plugin case for ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK
|
||||
this.lmsAPIService.getLmsSetup(exam.lmsSetupId).map(lms -> {
|
||||
if (lms.lmsType == LmsType.MOODLE_PLUGIN) {
|
||||
additionalAttributes.put(
|
||||
MoodleSEBRestriction.ATTR_ALT_BEK,
|
||||
exam.getAdditionalAttribute(ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK));
|
||||
}
|
||||
return lms;
|
||||
});
|
||||
|
||||
return new SEBRestriction(
|
||||
exam.id,
|
||||
configKeys,
|
||||
|
|
|
@ -216,6 +216,11 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
|
|||
return quizRequest(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
|
||||
return Result.ofError(new UnsupportedOperationException("Recovering not supported"));
|
||||
}
|
||||
|
||||
private List<QuizData> collectAllQuizzes(final AnsPersonalRestTemplate restTemplate) {
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
final List<QuizData> quizDatas = getAssignments(restTemplate)
|
||||
|
|
|
@ -45,6 +45,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
|||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
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.LmsType;
|
||||
|
@ -204,6 +205,11 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess implements Co
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
|
||||
return Result.ofError(new UnsupportedOperationException("Recovering not supported"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
|
||||
if (ids.size() == 1) {
|
||||
|
|
|
@ -24,6 +24,7 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
|||
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
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.LmsType;
|
||||
|
@ -233,6 +234,11 @@ public class MockCourseAccessAPI implements CourseAccessAPI {
|
|||
.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
|
||||
return Result.ofError(new UnsupportedOperationException("Recovering not supported"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCourseCache() {
|
||||
// No cache here
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
@ -18,6 +18,7 @@ import java.util.Set;
|
|||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
|
@ -27,13 +28,16 @@ import org.springframework.web.util.UriComponentsBuilder;
|
|||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodleQuizRestriction;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseAccess;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseRestriction;
|
||||
|
||||
public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
|
||||
|
||||
|
@ -144,18 +148,27 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
|
|||
|
||||
System.out.println("***** callMoodleAPIFunction HttpEntity: " + functionReqEntity);
|
||||
|
||||
// TODO return json
|
||||
if (MoodlePluginCourseAccess.COURSES_API_FUNCTION_NAME.equals(functionName)) {
|
||||
return respondCourses(queryAttributes);
|
||||
} else if (MoodlePluginCourseAccess.QUIZZES_BY_COURSES_API_FUNCTION_NAME.equals(functionName)) {
|
||||
return respondQuizzes(queryAttributes);
|
||||
} else if (MoodlePluginCourseAccess.USERS_API_FUNCTION_NAME.equals(functionName)) {
|
||||
return respondUsers(queryAttributes);
|
||||
} else {
|
||||
} else if (MoodlePluginCourseRestriction.RESTRICTION_GET_FUNCTION_NAME.equals(functionName)) {
|
||||
|
||||
return respondGetRestriction(
|
||||
queryParams.getFirst(MoodlePluginCourseRestriction.ATTRIBUTE_QUIZ_ID),
|
||||
queryAttributes);
|
||||
} else if (MoodlePluginCourseRestriction.RESTRICTION_SET_FUNCTION_NAME.equals(functionName)) {
|
||||
return respondSetRestriction(
|
||||
queryParams.getFirst(MoodlePluginCourseRestriction.ATTRIBUTE_QUIZ_ID),
|
||||
queryAttributes);
|
||||
}
|
||||
|
||||
else {
|
||||
throw new RuntimeException("Unknown function: " + functionName);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final class MockCD {
|
||||
public final String id;
|
||||
public final String shortname;
|
||||
|
@ -167,8 +180,9 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
|
|||
public final Long enddate; // unix-time seconds UTC
|
||||
public final Long timecreated; // unix-time seconds UTC
|
||||
public final boolean visible;
|
||||
public final Collection<MockQ> quizzes;
|
||||
|
||||
public MockCD(final String num) {
|
||||
public MockCD(final String num, final Collection<MockQ> quizzes) {
|
||||
this.id = num;
|
||||
this.shortname = "c" + num;
|
||||
this.categoryid = "mock";
|
||||
|
@ -179,9 +193,11 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
|
|||
this.enddate = null;
|
||||
this.timecreated = Long.valueOf(num);
|
||||
this.visible = true;
|
||||
this.quizzes = quizzes;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final class MockQ {
|
||||
public final String id;
|
||||
public final String coursemodule;
|
||||
|
@ -209,12 +225,17 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
|
|||
System.out.println("************* from: " + from);
|
||||
final List<MockCD> courses;
|
||||
if (ids != null && !ids.isEmpty()) {
|
||||
courses = ids.stream().map(id -> new MockCD(id)).collect(Collectors.toList());
|
||||
courses = ids
|
||||
.stream()
|
||||
.map(id -> new MockCD(
|
||||
id,
|
||||
getQuizzesForCourse(Integer.parseInt(id))))
|
||||
.collect(Collectors.toList());
|
||||
} else if (from != null && Integer.valueOf(from) < 11) {
|
||||
courses = new ArrayList<>();
|
||||
final int num = (Integer.valueOf(from) > 0) ? 10 : 1;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
courses.add(new MockCD(String.valueOf(num + i)));
|
||||
courses.add(new MockCD(String.valueOf(num + i), getQuizzesForCourse(num + i)));
|
||||
}
|
||||
} else {
|
||||
courses = new ArrayList<>();
|
||||
|
@ -232,28 +253,56 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private String respondQuizzes(final MultiValueMap<String, String> queryAttributes) {
|
||||
try {
|
||||
final List<String> ids = queryAttributes.get(MoodlePluginCourseAccess.CRITERIA_COURSE_IDS);
|
||||
final List<MockQ> quizzes;
|
||||
if (ids != null && !ids.isEmpty()) {
|
||||
quizzes = ids.stream().map(id -> new MockQ(id, "10" + id)).collect(Collectors.toList());
|
||||
} else {
|
||||
quizzes = Collections.emptyList();
|
||||
}
|
||||
private final Map<String, MoodleQuizRestriction> restrcitions = new HashMap<>();
|
||||
|
||||
final Map<String, Object> response = new HashMap<>();
|
||||
response.put("quizzes", quizzes);
|
||||
final JSONMapper jsonMapper = new JSONMapper();
|
||||
final String result = jsonMapper.writeValueAsString(response);
|
||||
System.out.println("******** quizzes response: " + result);
|
||||
return result;
|
||||
private String respondSetRestriction(final String quizId, final MultiValueMap<String, String> queryAttributes) {
|
||||
final List<String> configKeys = queryAttributes.get(MoodlePluginCourseRestriction.ATTRIBUTE_CONFIG_KEYS);
|
||||
final List<String> beks = queryAttributes.get(MoodlePluginCourseRestriction.ATTRIBUTE_BROWSER_EXAM_KEYS);
|
||||
final String quitURL = queryAttributes.getFirst(MoodlePluginCourseRestriction.ATTRIBUTE_QUIT_URL);
|
||||
final String quitSecret = queryAttributes.getFirst(MoodlePluginCourseRestriction.ATTRIBUTE_QUIT_SECRET);
|
||||
|
||||
final MoodleQuizRestriction moodleQuizRestriction = new MoodleQuizRestriction(
|
||||
quizId,
|
||||
StringUtils.join(configKeys, Constants.LIST_SEPARATOR),
|
||||
StringUtils.join(beks, Constants.LIST_SEPARATOR),
|
||||
quitURL,
|
||||
quitSecret);
|
||||
this.restrcitions.put(quizId, moodleQuizRestriction);
|
||||
|
||||
final JSONMapper jsonMapper = new JSONMapper();
|
||||
try {
|
||||
return jsonMapper.writeValueAsString(moodleQuizRestriction);
|
||||
} catch (final JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String respondGetRestriction(final String quizId, final MultiValueMap<String, String> queryAttributes) {
|
||||
final MoodleQuizRestriction moodleQuizRestriction = this.restrcitions.get(quizId);
|
||||
if (moodleQuizRestriction != null) {
|
||||
final JSONMapper jsonMapper = new JSONMapper();
|
||||
try {
|
||||
return jsonMapper.writeValueAsString(moodleQuizRestriction);
|
||||
} catch (final JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private Collection<MockQ> getQuizzesForCourse(final int courseId) {
|
||||
final String id = String.valueOf(courseId);
|
||||
final Collection<MockQ> result = new ArrayList<>();
|
||||
result.add(new MockQ(id, "10" + id));
|
||||
if (courseId % 2 > 0) {
|
||||
result.add(new MockQ(id, "11" + id));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private String respondUsers(final MultiValueMap<String, String> queryAttributes) {
|
||||
// TODO
|
||||
return "";
|
||||
|
|
|
@ -47,7 +47,6 @@ public class MoodlePluginCheck {
|
|||
try {
|
||||
restTemplate.testAPIConnection(
|
||||
MoodlePluginCourseAccess.COURSES_API_FUNCTION_NAME,
|
||||
MoodlePluginCourseAccess.QUIZZES_BY_COURSES_API_FUNCTION_NAME,
|
||||
MoodlePluginCourseAccess.USERS_API_FUNCTION_NAME);
|
||||
} catch (final Exception e) {
|
||||
log.info("Moodle SEB Server Plugin not available: {}", e.getMessage());
|
||||
|
|
|
@ -25,7 +25,6 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
|
@ -249,22 +248,21 @@ public abstract class MoodleUtils {
|
|||
public final Long end_date; // unix-time seconds UTC
|
||||
public final Long time_created; // unix-time seconds UTC
|
||||
public final String category_id;
|
||||
|
||||
@JsonIgnore
|
||||
public final Collection<CourseQuiz> quizzes = new ArrayList<>();
|
||||
public final Collection<CourseQuiz> quizzes;
|
||||
|
||||
@JsonCreator
|
||||
public CourseData(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "shortname") final String short_name,
|
||||
@JsonProperty(value = "idnumber") final String idnumber,
|
||||
@JsonProperty(value = "fullname") final String full_name,
|
||||
@JsonProperty(value = "displayname") final String display_name,
|
||||
@JsonProperty(value = "summary") final String summary,
|
||||
@JsonProperty(value = "startdate") final Long start_date,
|
||||
@JsonProperty(value = "enddate") final Long end_date,
|
||||
@JsonProperty(value = "timecreated") final Long time_created,
|
||||
@JsonProperty(value = "categoryid") final String category_id) {
|
||||
@JsonProperty("id") final String id,
|
||||
@JsonProperty("shortname") final String short_name,
|
||||
@JsonProperty("idnumber") final String idnumber,
|
||||
@JsonProperty("fullname") final String full_name,
|
||||
@JsonProperty("displayname") final String display_name,
|
||||
@JsonProperty("summary") final String summary,
|
||||
@JsonProperty("startdate") final Long start_date,
|
||||
@JsonProperty("enddate") final Long end_date,
|
||||
@JsonProperty("timecreated") final Long time_created,
|
||||
@JsonProperty("categoryid") final String category_id,
|
||||
@JsonProperty("quizzes") final Collection<CourseQuiz> quizzes) {
|
||||
|
||||
this.id = id;
|
||||
this.short_name = short_name;
|
||||
|
@ -276,6 +274,7 @@ public abstract class MoodleUtils {
|
|||
this.end_date = end_date;
|
||||
this.time_created = time_created;
|
||||
this.category_id = category_id;
|
||||
this.quizzes = (quizzes == null) ? new ArrayList<>() : quizzes;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,8 +285,8 @@ public abstract class MoodleUtils {
|
|||
|
||||
@JsonCreator
|
||||
public Courses(
|
||||
@JsonProperty(value = "courses") final Collection<CourseData> courses,
|
||||
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
|
||||
@JsonProperty("courses") final Collection<CourseData> courses,
|
||||
@JsonProperty("warnings") final Collection<Warning> warnings) {
|
||||
this.courses = courses;
|
||||
this.warnings = warnings;
|
||||
}
|
||||
|
@ -300,8 +299,8 @@ public abstract class MoodleUtils {
|
|||
|
||||
@JsonCreator
|
||||
public CourseQuizData(
|
||||
@JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes,
|
||||
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
|
||||
@JsonProperty("quizzes") final Collection<CourseQuiz> quizzes,
|
||||
@JsonProperty("warnings") final Collection<Warning> warnings) {
|
||||
this.quizzes = quizzes;
|
||||
this.warnings = warnings;
|
||||
}
|
||||
|
@ -320,14 +319,14 @@ public abstract class MoodleUtils {
|
|||
|
||||
@JsonCreator
|
||||
public CourseQuiz(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "course") final String course,
|
||||
@JsonProperty(value = "coursemodule") final String course_module,
|
||||
@JsonProperty(value = "name") final String name,
|
||||
@JsonProperty(value = "intro") final String intro,
|
||||
@JsonProperty(value = "timeopen") final Long time_open,
|
||||
@JsonProperty(value = "timeclose") final Long time_close,
|
||||
@JsonProperty(value = "timelimit") final Long time_limit) {
|
||||
@JsonProperty("id") final String id,
|
||||
@JsonProperty("course") final String course,
|
||||
@JsonProperty("coursemodule") final String course_module,
|
||||
@JsonProperty("name") final String name,
|
||||
@JsonProperty("intro") final String intro,
|
||||
@JsonProperty("timeopen") final Long time_open,
|
||||
@JsonProperty("timeclose") final Long time_close,
|
||||
@JsonProperty("timelimit") final Long time_limit) {
|
||||
|
||||
this.id = id;
|
||||
this.course = course;
|
||||
|
@ -363,24 +362,24 @@ public abstract class MoodleUtils {
|
|||
|
||||
@JsonCreator
|
||||
public MoodleUserDetails(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "username") final String username,
|
||||
@JsonProperty(value = "firstname") final String firstname,
|
||||
@JsonProperty(value = "lastname") final String lastname,
|
||||
@JsonProperty(value = "fullname") final String fullname,
|
||||
@JsonProperty(value = "email") final String email,
|
||||
@JsonProperty(value = "department") final String department,
|
||||
@JsonProperty(value = "firstaccess") final Long firstaccess,
|
||||
@JsonProperty(value = "lastaccess") final Long lastaccess,
|
||||
@JsonProperty(value = "auth") final String auth,
|
||||
@JsonProperty(value = "suspended") final Boolean suspended,
|
||||
@JsonProperty(value = "confirmed") final Boolean confirmed,
|
||||
@JsonProperty(value = "lang") final String lang,
|
||||
@JsonProperty(value = "theme") final String theme,
|
||||
@JsonProperty(value = "timezone") final String timezone,
|
||||
@JsonProperty(value = "description") final String description,
|
||||
@JsonProperty(value = "mailformat") final Integer mailformat,
|
||||
@JsonProperty(value = "descriptionformat") final Integer descriptionformat) {
|
||||
@JsonProperty("id") final String id,
|
||||
@JsonProperty("username") final String username,
|
||||
@JsonProperty("firstname") final String firstname,
|
||||
@JsonProperty("lastname") final String lastname,
|
||||
@JsonProperty("fullname") final String fullname,
|
||||
@JsonProperty("email") final String email,
|
||||
@JsonProperty("department") final String department,
|
||||
@JsonProperty("firstaccess") final Long firstaccess,
|
||||
@JsonProperty("lastaccess") final Long lastaccess,
|
||||
@JsonProperty("auth") final String auth,
|
||||
@JsonProperty("suspended") final Boolean suspended,
|
||||
@JsonProperty("confirmed") final Boolean confirmed,
|
||||
@JsonProperty("lang") final String lang,
|
||||
@JsonProperty("theme") final String theme,
|
||||
@JsonProperty("timezone") final String timezone,
|
||||
@JsonProperty("description") final String description,
|
||||
@JsonProperty("mailformat") final Integer mailformat,
|
||||
@JsonProperty("descriptionformat") final Integer descriptionformat) {
|
||||
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
|
@ -403,52 +402,15 @@ public abstract class MoodleUtils {
|
|||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static final class MoodlePluginUserDetails {
|
||||
public final String id;
|
||||
public final String fullname;
|
||||
public final String username;
|
||||
public final String firstname;
|
||||
public final String lastname;
|
||||
public final String idnumber;
|
||||
public final String email;
|
||||
public final Map<String, String> customfields;
|
||||
|
||||
@JsonCreator
|
||||
public MoodlePluginUserDetails(
|
||||
final String id,
|
||||
final String username,
|
||||
final String firstname,
|
||||
final String lastname,
|
||||
final String idnumber,
|
||||
final String email,
|
||||
final Map<String, String> customfields) {
|
||||
|
||||
this.id = id;
|
||||
if (firstname != null && lastname != null) {
|
||||
this.fullname = firstname + Constants.SPACE + lastname;
|
||||
} else if (firstname != null) {
|
||||
this.fullname = firstname;
|
||||
} else {
|
||||
this.fullname = lastname;
|
||||
}
|
||||
this.username = username;
|
||||
this.firstname = firstname;
|
||||
this.lastname = lastname;
|
||||
this.idnumber = idnumber;
|
||||
this.email = email;
|
||||
this.customfields = Utils.immutableMapOf(customfields);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static final class CoursePage {
|
||||
public final Collection<CourseKey> courseKeys;
|
||||
public final Collection<Warning> warnings;
|
||||
|
||||
@JsonCreator
|
||||
public CoursePage(
|
||||
@JsonProperty(value = "courses") final Collection<CourseKey> courseKeys,
|
||||
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
|
||||
@JsonProperty("courses") final Collection<CourseKey> courseKeys,
|
||||
@JsonProperty("warnings") final Collection<Warning> warnings) {
|
||||
|
||||
this.courseKeys = courseKeys;
|
||||
this.warnings = warnings;
|
||||
|
@ -464,10 +426,10 @@ public abstract class MoodleUtils {
|
|||
|
||||
@JsonCreator
|
||||
public CourseKey(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "shortname") final String short_name,
|
||||
@JsonProperty(value = "categoryname") final String category_name,
|
||||
@JsonProperty(value = "sortorder") final String sort_order) {
|
||||
@JsonProperty("id") final String id,
|
||||
@JsonProperty("shortname") final String short_name,
|
||||
@JsonProperty("categoryname") final String category_name,
|
||||
@JsonProperty("sortorder") final String sort_order) {
|
||||
|
||||
this.id = id;
|
||||
this.short_name = short_name;
|
||||
|
@ -501,11 +463,11 @@ public abstract class MoodleUtils {
|
|||
|
||||
@JsonCreator
|
||||
public MoodleQuizRestriction(
|
||||
final String quiz_id,
|
||||
final String config_keys,
|
||||
final String browser_exam_keys,
|
||||
final String quit_link,
|
||||
final String quit_secret) {
|
||||
@JsonProperty("quiz_id") final String quiz_id,
|
||||
@JsonProperty("config_keys") final String config_keys,
|
||||
@JsonProperty("browser_exam_keys") final String browser_exam_keys,
|
||||
@JsonProperty("quit_link") final String quit_link,
|
||||
@JsonProperty("quit_secret") final String quit_secret) {
|
||||
|
||||
this.quiz_id = quiz_id;
|
||||
this.config_keys = config_keys;
|
||||
|
|
|
@ -38,6 +38,7 @@ import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
|||
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
|
||||
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
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.LmsType;
|
||||
|
@ -87,6 +88,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
static final String MOODLE_QUIZ_API_FUNCTION_NAME = "mod_quiz_get_quizzes_by_courses";
|
||||
static final String MOODLE_COURSE_API_COURSE_IDS = "courseids";
|
||||
static final String MOODLE_COURSE_API_IDS = "ids";
|
||||
static final String MOODLE_COURSE_API_COURSE_SHORTNAME = "shortname";
|
||||
static final String MOODLE_COURSE_SEARCH_API_FUNCTION_NAME = "core_course_search_courses";
|
||||
static final String MOODLE_COURSE_BY_FIELD_API_FUNCTION_NAME = "core_course_get_courses_by_field";
|
||||
static final String MOODLE_COURSE_API_FIELD_NAME = "field";
|
||||
|
@ -332,6 +334,71 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
|
||||
final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow();
|
||||
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
||||
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
|
||||
|
||||
// get the course name identifier for recovering
|
||||
final String shortname = MoodleUtils.getShortname(exam.externalId);
|
||||
|
||||
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
|
||||
attributes.add(MOODLE_COURSE_API_FIELD_NAME, MOODLE_COURSE_API_COURSE_SHORTNAME);
|
||||
attributes.add(MOODLE_COURSE_API_FIELD_VALUE, shortname);
|
||||
final String coursePageJSON = restTemplate.callMoodleAPIFunction(
|
||||
MOODLE_COURSE_BY_FIELD_API_FUNCTION_NAME,
|
||||
attributes);
|
||||
|
||||
final Map<String, CourseData> courses = this.jsonMapper.readValue(
|
||||
coursePageJSON,
|
||||
Courses.class).courses
|
||||
.stream()
|
||||
.collect(Collectors.toMap(c -> c.id, Function.identity()));
|
||||
|
||||
// then get all quizzes of courses and filter
|
||||
final LinkedMultiValueMap<String, String> cAttributes = new LinkedMultiValueMap<>();
|
||||
final List<String> courseIds = new ArrayList<>(courses.keySet());
|
||||
if (courseIds.size() == 1) {
|
||||
// NOTE: This is a workaround because the Moodle API do not support lists with only one element.
|
||||
courseIds.add("0");
|
||||
}
|
||||
cAttributes.put(
|
||||
MoodleCourseAccess.MOODLE_COURSE_API_COURSE_IDS,
|
||||
courseIds);
|
||||
|
||||
final String quizzesJSON = this.protectedMoodlePageCall
|
||||
.protectedRun(() -> restTemplate.callMoodleAPIFunction(
|
||||
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME,
|
||||
attributes))
|
||||
.getOrThrow();
|
||||
|
||||
this.jsonMapper.readValue(
|
||||
quizzesJSON,
|
||||
CourseQuizData.class).quizzes.stream().forEach(quiz -> {
|
||||
final CourseData data = courses.get(quiz.course);
|
||||
if (data != null) {
|
||||
data.quizzes.add(quiz);
|
||||
}
|
||||
});
|
||||
|
||||
return courses.values()
|
||||
.stream()
|
||||
.flatMap(c -> MoodleUtils.quizDataOf(
|
||||
lmsSetup,
|
||||
c,
|
||||
urlPrefix,
|
||||
this.prependShortCourseName).stream())
|
||||
.filter(q -> exam.name.contains(q.name))
|
||||
.findFirst()
|
||||
.get();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCourseCache() {
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
@ -43,6 +42,7 @@ import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
|||
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
|
||||
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
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.LmsType;
|
||||
|
@ -58,9 +58,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRe
|
|||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseData;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseQuizData;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.Courses;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodlePluginUserDetails;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodleUserDetails;
|
||||
|
||||
public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess implements CourseAccessAPI {
|
||||
|
||||
|
@ -68,10 +67,12 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
|
|||
|
||||
public static final String MOODLE_QUIZ_START_URL_PATH = "mod/quiz/view.php?id=";
|
||||
public static final String COURSES_API_FUNCTION_NAME = "quizaccess_sebserver_get_courses";
|
||||
public static final String QUIZZES_BY_COURSES_API_FUNCTION_NAME = "quizaccess_sebserver_get_quizzes_by_courses";
|
||||
public static final String USERS_API_FUNCTION_NAME = "quizaccess_sebserver_get_users";
|
||||
public static final String USERS_API_FUNCTION_NAME = "core_user_get_users_by_field";
|
||||
|
||||
public static final String ATTR_FIELD = "field";
|
||||
public static final String ATTR_VALUE = "value";
|
||||
public static final String ATTR_ID = "id";
|
||||
public static final String ATTR_SHORTNAME = "shortname";
|
||||
public static final String CRITERIA_COURSE_IDS = "ids";
|
||||
public static final String CRITERIA_FROM_DATE = "from_date";
|
||||
public static final String CRITERIA_TO_DATE = "to_date";
|
||||
|
@ -147,7 +148,6 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
|
|||
|
||||
restTemplate.testAPIConnection(
|
||||
COURSES_API_FUNCTION_NAME,
|
||||
QUIZZES_BY_COURSES_API_FUNCTION_NAME,
|
||||
USERS_API_FUNCTION_NAME);
|
||||
|
||||
} catch (final RuntimeException e) {
|
||||
|
@ -243,15 +243,50 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
|
|||
}
|
||||
|
||||
@Override
|
||||
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
|
||||
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup();
|
||||
final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow();
|
||||
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
||||
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
|
||||
|
||||
// get the course name identifier for recovering
|
||||
final String shortname = MoodleUtils.getShortname(exam.externalId);
|
||||
|
||||
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
|
||||
attributes.add(ATTR_FIELD, ATTR_SHORTNAME);
|
||||
attributes.add(ATTR_VALUE, shortname);
|
||||
final String courseJSON = restTemplate.callMoodleAPIFunction(
|
||||
COURSES_API_FUNCTION_NAME,
|
||||
attributes);
|
||||
|
||||
return this.jsonMapper.readValue(
|
||||
courseJSON,
|
||||
Courses.class).courses
|
||||
.stream()
|
||||
.flatMap(c -> MoodleUtils.quizDataOf(
|
||||
lmsSetup,
|
||||
c,
|
||||
urlPrefix,
|
||||
this.prependShortCourseName).stream())
|
||||
.filter(q -> exam.name.contains(q.name))
|
||||
.findFirst()
|
||||
.get();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final MoodleAPIRestTemplate template = getRestTemplate()
|
||||
.getOrThrow();
|
||||
|
||||
final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>();
|
||||
queryAttributes.add(ATTR_FIELD, "id");
|
||||
queryAttributes.add("values[0]", examineeUserId);
|
||||
queryAttributes.add(ATTR_FIELD, ATTR_ID);
|
||||
queryAttributes.add(ATTR_VALUE, examineeSessionId);
|
||||
|
||||
final String userDetailsJSON = template.callMoodleAPIFunction(
|
||||
USERS_API_FUNCTION_NAME,
|
||||
|
@ -266,21 +301,36 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
|
|||
throw new RuntimeException("No user details on Moodle API request (access-denied)");
|
||||
}
|
||||
|
||||
final MoodlePluginUserDetails[] userDetails = this.jsonMapper.<MoodlePluginUserDetails[]> readValue(
|
||||
final MoodleUserDetails[] userDetails = this.jsonMapper.<MoodleUserDetails[]> readValue(
|
||||
userDetailsJSON,
|
||||
new TypeReference<MoodlePluginUserDetails[]>() {
|
||||
new TypeReference<MoodleUserDetails[]>() {
|
||||
});
|
||||
|
||||
if (userDetails == null || userDetails.length <= 0) {
|
||||
throw new RuntimeException("No user details on Moodle API request");
|
||||
}
|
||||
|
||||
final Map<String, String> additionalAttributes = new HashMap<>();
|
||||
additionalAttributes.put("firstname", userDetails[0].firstname);
|
||||
additionalAttributes.put("lastname", userDetails[0].lastname);
|
||||
additionalAttributes.put("department", userDetails[0].department);
|
||||
additionalAttributes.put("firstaccess", String.valueOf(userDetails[0].firstaccess));
|
||||
additionalAttributes.put("lastaccess", String.valueOf(userDetails[0].lastaccess));
|
||||
additionalAttributes.put("auth", userDetails[0].auth);
|
||||
additionalAttributes.put("suspended", String.valueOf(userDetails[0].suspended));
|
||||
additionalAttributes.put("confirmed", String.valueOf(userDetails[0].confirmed));
|
||||
additionalAttributes.put("lang", userDetails[0].lang);
|
||||
additionalAttributes.put("theme", userDetails[0].theme);
|
||||
additionalAttributes.put("timezone", userDetails[0].timezone);
|
||||
additionalAttributes.put("description", userDetails[0].description);
|
||||
additionalAttributes.put("mailformat", String.valueOf(userDetails[0].mailformat));
|
||||
additionalAttributes.put("descriptionformat", String.valueOf(userDetails[0].descriptionformat));
|
||||
return new ExamineeAccountDetails(
|
||||
userDetails[0].id,
|
||||
userDetails[0].fullname,
|
||||
userDetails[0].username,
|
||||
userDetails[0].email,
|
||||
userDetails[0].customfields);
|
||||
additionalAttributes);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -314,63 +364,16 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
|
|||
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
|
||||
|
||||
// first get courses page from moodle
|
||||
final Map<String, CourseData> courseData = new HashMap<>();
|
||||
final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, quizFromTime, page, this.pageSize);
|
||||
|
||||
// no courses for page --> finish
|
||||
if (coursesPage == null || coursesPage.isEmpty()) {
|
||||
final Collection<CourseData> fetchCoursesPage =
|
||||
fetchCoursesPage(restTemplate, quizFromTime, page, this.pageSize);
|
||||
// finish if page is empty (no courses left
|
||||
if (fetchCoursesPage.isEmpty()) {
|
||||
asyncQuizFetchBuffer.finish();
|
||||
return;
|
||||
}
|
||||
|
||||
courseData.putAll(coursesPage
|
||||
.stream()
|
||||
.collect(Collectors.toMap(
|
||||
cd -> cd.id,
|
||||
Function.identity())));
|
||||
|
||||
// then get all quizzes of courses and filter
|
||||
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
|
||||
final List<String> courseIds = new ArrayList<>(courseData.keySet());
|
||||
attributes.put(CRITERIA_COURSE_IDS, courseIds);
|
||||
|
||||
final String quizzesJSON = this.protectedMoodlePageCall
|
||||
.protectedRun(() -> restTemplate.callMoodleAPIFunction(
|
||||
QUIZZES_BY_COURSES_API_FUNCTION_NAME,
|
||||
attributes))
|
||||
.getOrThrow();
|
||||
|
||||
final CourseQuizData courseQuizData = this.jsonMapper.readValue(
|
||||
quizzesJSON,
|
||||
CourseQuizData.class);
|
||||
|
||||
if (courseQuizData == null) {
|
||||
return; // SEBSERV-361
|
||||
}
|
||||
|
||||
if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) {
|
||||
MoodleUtils.logMoodleWarning(
|
||||
courseQuizData.warnings,
|
||||
lmsSetup.name,
|
||||
QUIZZES_BY_COURSES_API_FUNCTION_NAME);
|
||||
}
|
||||
|
||||
if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
|
||||
return; // no quizzes on this page
|
||||
}
|
||||
|
||||
courseQuizData.quizzes
|
||||
.stream()
|
||||
.filter(MoodleUtils.getQuizFilter())
|
||||
.forEach(quiz -> {
|
||||
final CourseData data = courseData.get(quiz.course);
|
||||
if (data != null) {
|
||||
data.quizzes.add(quiz);
|
||||
}
|
||||
});
|
||||
|
||||
courseData.values().stream()
|
||||
// fetch and buffer quizzes
|
||||
fetchCoursesPage.stream()
|
||||
.filter(c -> !c.quizzes.isEmpty())
|
||||
.forEach(c -> asyncQuizFetchBuffer.buffer.addAll(
|
||||
MoodleUtils.quizDataOf(lmsSetup, c, urlPrefix, this.prependShortCourseName)
|
||||
|
@ -378,18 +381,23 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
|
|||
.filter(quizFilter)
|
||||
.collect(Collectors.toList())));
|
||||
|
||||
// check thresholds
|
||||
if (asyncQuizFetchBuffer.buffer.size() > this.maxSize) {
|
||||
log.warn("Maximal moodle quiz fetch size of {} reached. Cancel fetch at this point.", this.maxSize);
|
||||
asyncQuizFetchBuffer.finish();
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<CourseData> getCoursesPage(
|
||||
private Collection<CourseData> fetchCoursesPage(
|
||||
final MoodleAPIRestTemplate restTemplate,
|
||||
final DateTime quizFromTime,
|
||||
final int page,
|
||||
final int size) throws JsonParseException, JsonMappingException, IOException {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Fetch course page: {}, size: {} quizFromTime: {}", page, size, quizFromTime);
|
||||
}
|
||||
|
||||
final String lmsName = getLmsSetupName();
|
||||
try {
|
||||
// get course ids per page
|
||||
|
@ -442,59 +450,31 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
|
|||
|
||||
private List<QuizData> getQuizzesForIds(
|
||||
final MoodleAPIRestTemplate restTemplate,
|
||||
final Set<String> quizIds) {
|
||||
final Set<String> internalIds) {
|
||||
|
||||
try {
|
||||
|
||||
final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup();
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Get quizzes for ids: {} and LMSSetup: {}", quizIds, lmsSetup);
|
||||
}
|
||||
|
||||
// get involved courses and map by course id
|
||||
final Map<String, CourseData> courseData = getCoursesForIds(
|
||||
restTemplate,
|
||||
quizIds.stream()
|
||||
.map(MoodleUtils::getCourseId)
|
||||
.collect(Collectors.toSet()))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(cd -> cd.id, Function.identity()));
|
||||
|
||||
// then get all quizzes of courses and filter
|
||||
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
|
||||
attributes.put(CRITERIA_COURSE_IDS, new ArrayList<>(courseData.keySet()));
|
||||
|
||||
final String quizzesJSON = restTemplate.callMoodleAPIFunction(
|
||||
QUIZZES_BY_COURSES_API_FUNCTION_NAME,
|
||||
attributes);
|
||||
|
||||
final CourseQuizData courseQuizData = this.jsonMapper.readValue(
|
||||
quizzesJSON,
|
||||
CourseQuizData.class);
|
||||
|
||||
if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) {
|
||||
MoodleUtils.logMoodleWarning(
|
||||
courseQuizData.warnings,
|
||||
lmsSetup.name,
|
||||
QUIZZES_BY_COURSES_API_FUNCTION_NAME);
|
||||
}
|
||||
|
||||
final Map<String, CourseData> finalCourseDataRef = courseData;
|
||||
courseQuizData.quizzes
|
||||
.stream()
|
||||
.forEach(quiz -> MoodleUtils.fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz));
|
||||
|
||||
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
||||
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
|
||||
final Set<String> moodleCourseIds = internalIds.stream()
|
||||
.map(MoodleUtils::getCourseId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return courseData.values()
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Get quizzes for internal ids: {}, Moodle courseI ids: {} and LMSSetup: {}",
|
||||
internalIds,
|
||||
moodleCourseIds,
|
||||
lmsSetup);
|
||||
}
|
||||
|
||||
return getCoursesForIds(restTemplate, moodleCourseIds)
|
||||
.stream()
|
||||
.filter(c -> !c.quizzes.isEmpty())
|
||||
.flatMap(cd -> MoodleUtils.quizDataOf(
|
||||
.filter(courseData -> !courseData.quizzes.isEmpty())
|
||||
.flatMap(courseData -> MoodleUtils.quizDataOf(
|
||||
lmsSetup,
|
||||
cd,
|
||||
courseData,
|
||||
urlPrefix,
|
||||
this.prependShortCourseName).stream())
|
||||
.collect(Collectors.toList());
|
||||
|
|
|
@ -105,7 +105,10 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
|
|||
final LinkedMultiValueMap<String, String> addQuery = new LinkedMultiValueMap<>();
|
||||
addQuery.add(ATTRIBUTE_QUIZ_ID, quizId);
|
||||
|
||||
final String srJSON = restTemplate.callMoodleAPIFunction(RESTRICTION_GET_FUNCTION_NAME, addQuery);
|
||||
final String srJSON = restTemplate.callMoodleAPIFunction(
|
||||
RESTRICTION_GET_FUNCTION_NAME,
|
||||
addQuery,
|
||||
new LinkedMultiValueMap<>());
|
||||
|
||||
try {
|
||||
|
||||
|
@ -139,9 +142,10 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
|
|||
final ArrayList<String> configKeys = new ArrayList<>(sebRestrictionData.configKeys);
|
||||
final String quitLink = this.examConfigurationValueService.getQuitLink(exam.id);
|
||||
final String quitSecret = this.examConfigurationValueService.getQuitSecret(exam.id);
|
||||
final String additionalBEK = sebRestrictionData.additionalProperties.get(
|
||||
final String additionalBEK = exam.getAdditionalAttribute(
|
||||
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK);
|
||||
if (additionalBEK != null) {
|
||||
|
||||
if (additionalBEK != null && !beks.contains(additionalBEK)) {
|
||||
beks.add(additionalBEK);
|
||||
}
|
||||
|
||||
|
@ -200,18 +204,12 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
|
|||
final List<String> configKeys = Arrays.asList(StringUtils.split(
|
||||
moodleRestriction.config_keys,
|
||||
Constants.LIST_SEPARATOR));
|
||||
final List<String> browserExamKeys = Arrays.asList(StringUtils.split(
|
||||
final List<String> browserExamKeys = new ArrayList<>(Arrays.asList(StringUtils.split(
|
||||
moodleRestriction.browser_exam_keys,
|
||||
Constants.LIST_SEPARATOR));
|
||||
Constants.LIST_SEPARATOR)));
|
||||
final Map<String, String> additionalProperties = new HashMap<>();
|
||||
additionalProperties.put(ATTRIBUTE_QUIT_URL, moodleRestriction.quit_link);
|
||||
|
||||
final String additionalBEK = exam.getAdditionalAttribute(
|
||||
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK);
|
||||
|
||||
if (additionalBEK != null) {
|
||||
browserExamKeys.remove(additionalBEK);
|
||||
}
|
||||
additionalProperties.put(ATTRIBUTE_QUIT_SECRET, moodleRestriction.quit_secret);
|
||||
|
||||
return new SEBRestriction(
|
||||
exam.id,
|
||||
|
|
|
@ -220,6 +220,11 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
|||
return quizRequest(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
|
||||
return Result.ofError(new UnsupportedOperationException("Recovering not supported"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
|
||||
return getRestTemplate().map(t -> this.getExamineeById(t, examineeUserId));
|
||||
|
|
|
@ -14,7 +14,6 @@ import java.util.Map;
|
|||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -24,21 +23,19 @@ import org.springframework.context.ApplicationEventPublisher;
|
|||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
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.Exam.ExamStatus;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
|
||||
|
@ -51,23 +48,28 @@ class ExamUpdateHandler {
|
|||
private static final Logger log = LoggerFactory.getLogger(ExamUpdateHandler.class);
|
||||
|
||||
private final ExamDAO examDAO;
|
||||
private final AdditionalAttributesDAO additionalAttributesDAO;
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
private final SEBRestrictionService sebRestrictionService;
|
||||
private final LmsAPIService lmsAPIService;
|
||||
private final String updatePrefix;
|
||||
private final Long examTimeSuffix;
|
||||
private final boolean tryRecoverExam;
|
||||
private final int recoverAttempts;
|
||||
|
||||
public ExamUpdateHandler(
|
||||
final ExamDAO examDAO,
|
||||
final AdditionalAttributesDAO additionalAttributesDAO,
|
||||
final ApplicationEventPublisher applicationEventPublisher,
|
||||
final SEBRestrictionService sebRestrictionService,
|
||||
final LmsAPIService lmsAPIService,
|
||||
final WebserviceInfo webserviceInfo,
|
||||
@Value("${sebserver.webservice.api.exam.time-suffix:3600000}") final Long examTimeSuffix,
|
||||
@Value("${sebserver.webservice.api.exam.tryrecover:false}") final boolean tryRecoverExam) {
|
||||
@Value("${sebserver.webservice.api.exam.tryrecover:true}") final boolean tryRecoverExam,
|
||||
@Value("${sebserver.webservice.api.exam.recoverattempts:3}") final int recoverAttempts) {
|
||||
|
||||
this.examDAO = examDAO;
|
||||
this.additionalAttributesDAO = additionalAttributesDAO;
|
||||
this.applicationEventPublisher = applicationEventPublisher;
|
||||
this.sebRestrictionService = sebRestrictionService;
|
||||
this.lmsAPIService = lmsAPIService;
|
||||
|
@ -75,6 +77,7 @@ class ExamUpdateHandler {
|
|||
+ "_" + webserviceInfo.getServerPort() + "_";
|
||||
this.examTimeSuffix = examTimeSuffix;
|
||||
this.tryRecoverExam = tryRecoverExam;
|
||||
this.recoverAttempts = recoverAttempts;
|
||||
}
|
||||
|
||||
public SEBRestrictionService getSEBRestrictionService() {
|
||||
|
@ -379,67 +382,124 @@ class ExamUpdateHandler {
|
|||
final String updateId) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final Exam exam = exams.get(quizId);
|
||||
final LmsAPITemplate lmsTemplate = this.lmsAPIService
|
||||
.getLmsAPITemplate(lmsSetupId)
|
||||
.getOrThrow();
|
||||
final int attempts = Integer.parseInt(this.additionalAttributesDAO.getAdditionalAttribute(
|
||||
EntityType.EXAM,
|
||||
exam.id,
|
||||
Exam.ADDITIONAL_ATTR_QUIZ_RECOVER_ATTEMPTS)
|
||||
.map(AdditionalAttributeRecord::getValue)
|
||||
.getOr("0"));
|
||||
|
||||
// If this is a Moodle quiz, try to recover from eventually restore of the quiz on the LMS side
|
||||
// NOTE: This is a workaround for Moodle quizzes that had have a recovery within the sandbox tool
|
||||
// Where potentially quiz identifiers get changed during such a recovery and the SEB Server
|
||||
// internal mapping is not working properly anymore. In this case we try to recover from such
|
||||
// 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
|
||||
if (lmsTemplate.getType() == LmsType.MOODLE) {
|
||||
|
||||
log.info("Try to recover quiz data for Moodle quiz with internal identifier: {}", quizId);
|
||||
|
||||
if (exam != null && exam.name != null
|
||||
&& !exam.name.startsWith(Constants.SQUARE_BRACE_OPEN.toString())) {
|
||||
|
||||
log.debug("Found formerName quiz name: {}", exam.name);
|
||||
|
||||
// get the course name identifier
|
||||
final String shortname = MoodleUtils.getShortname(quizId);
|
||||
if (StringUtils.isNotBlank(shortname)) {
|
||||
|
||||
log.debug("Using short-name: {} for recovering", shortname);
|
||||
|
||||
final QuizData recoveredQuizData = lmsTemplate
|
||||
.getQuizzes(new FilterMap())
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.filter(quiz -> {
|
||||
final String qShortName = MoodleUtils.getShortname(quiz.id);
|
||||
return qShortName != null && qShortName.equals(shortname);
|
||||
})
|
||||
.filter(quiz -> exam.name.equals(quiz.name))
|
||||
.findAny()
|
||||
.get();
|
||||
|
||||
if (recoveredQuizData != null) {
|
||||
|
||||
log.debug("Found quiz data for recovering: {}", recoveredQuizData);
|
||||
|
||||
// save exam with new external id and quit data
|
||||
this.examDAO
|
||||
.updateQuizData(exam.id, recoveredQuizData, updateId)
|
||||
.getOrThrow();
|
||||
|
||||
log.debug("Successfully recovered exam quiz data to new externalId {}",
|
||||
recoveredQuizData.id);
|
||||
}
|
||||
return recoveredQuizData;
|
||||
}
|
||||
if (attempts >= this.recoverAttempts) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Skip recovering quiz due to too many attempts: {}", exam.getModelId());
|
||||
throw new RuntimeException("Recover attempts reached");
|
||||
}
|
||||
}
|
||||
|
||||
if (exam.lmsAvailable == null || exam.isLmsAvailable()) {
|
||||
this.examDAO.markLMSAvailability(quizId, false, updateId);
|
||||
}
|
||||
throw new RuntimeException("Not Available");
|
||||
log.info(
|
||||
"Try to recover quiz data for Moodle quiz with internal identifier: {}",
|
||||
quizId);
|
||||
|
||||
return this.lmsAPIService
|
||||
.getLmsAPITemplate(lmsSetupId)
|
||||
.flatMap(template -> template.tryRecoverQuizForExam(exam))
|
||||
.onError(error -> {
|
||||
|
||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
EntityType.EXAM,
|
||||
exam.id,
|
||||
Exam.ADDITIONAL_ATTR_QUIZ_RECOVER_ATTEMPTS,
|
||||
String.valueOf(attempts + 1))
|
||||
.onError(error1 -> log.error("Failed to save new attempts: ", error1));
|
||||
|
||||
if (exam.lmsAvailable == null || exam.isLmsAvailable()) {
|
||||
this.examDAO.markLMSAvailability(quizId, false, updateId);
|
||||
}
|
||||
})
|
||||
.getOrThrowRuntime("Not Available");
|
||||
|
||||
// // If this is a Moodle quiz, try to recover from eventually restore of the quiz on the LMS side
|
||||
// // NOTE: This is a workaround for Moodle quizzes that had have a recovery within the sandbox tool
|
||||
// // Where potentially quiz identifiers get changed during such a recovery and the SEB Server
|
||||
// // internal mapping is not working properly anymore. In this case we try to recover from such
|
||||
// // 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
|
||||
// if (lmsTemplate.getType() == LmsType.MOODLE || lmsTemplate.getType() == LmsType.MOODLE_PLUGIN) {
|
||||
//
|
||||
// final int attempts = Integer.parseInt(this.additionalAttributesDAO.getAdditionalAttribute(
|
||||
// EntityType.EXAM,
|
||||
// exam.id,
|
||||
// Exam.ADDITIONAL_ATTR_QUIZ_RECOVER_ATTEMPTS)
|
||||
// .map(AdditionalAttributeRecord::getValue)
|
||||
// .getOr("0"));
|
||||
//
|
||||
// if (attempts >= this.recoverAttempts) {
|
||||
// if (log.isDebugEnabled()) {
|
||||
// log.debug("Skip recovering quiz due to too many attempts: {}", exam.getModelId());
|
||||
// throw new RuntimeException("Recover attempts reached");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// log.info(
|
||||
// "Try to recover quiz data for Moodle quiz with internal identifier: {}",
|
||||
// quizId);
|
||||
//
|
||||
// if (exam != null && exam.name != null
|
||||
// && !exam.name.startsWith(Constants.SQUARE_BRACE_OPEN.toString())) {
|
||||
//
|
||||
// log.debug("Found formerName quiz name: {}", exam.name);
|
||||
//
|
||||
// // get the course name identifier
|
||||
// final String shortname = MoodleUtils.getShortname(quizId);
|
||||
// if (StringUtils.isNotBlank(shortname)) {
|
||||
//
|
||||
// log.debug("Using short-name: {} for recovering", shortname);
|
||||
//
|
||||
// final QuizData recoveredQuizData = lmsTemplate
|
||||
// .getQuizzes(new FilterMap())
|
||||
// .getOrThrow()
|
||||
// .stream()
|
||||
// .filter(quiz -> {
|
||||
// final String qShortName = MoodleUtils.getShortname(quiz.id);
|
||||
// return qShortName != null && qShortName.equals(shortname);
|
||||
// })
|
||||
// .filter(quiz -> exam.name.equals(quiz.name))
|
||||
// .findAny()
|
||||
// .get();
|
||||
//
|
||||
// if (recoveredQuizData != null) {
|
||||
//
|
||||
// log.debug("Found quiz data for recovering: {}", recoveredQuizData);
|
||||
//
|
||||
// // save exam with new external id and quit data
|
||||
// this.examDAO
|
||||
// .updateQuizData(exam.id, recoveredQuizData, updateId)
|
||||
// .getOrThrow();
|
||||
//
|
||||
// log.debug("Successfully recovered exam quiz data to new externalId {}",
|
||||
// recoveredQuizData.id);
|
||||
// }
|
||||
// return recoveredQuizData;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
// EntityType.EXAM,
|
||||
// exam.id,
|
||||
// Exam.ADDITIONAL_ATTR_QUIZ_RECOVER_ATTEMPTS,
|
||||
// String.valueOf(attempts + 1))
|
||||
// .onError(error -> log.error("Failed to save new attempts: ", error));
|
||||
// }
|
||||
//
|
||||
// if (exam.lmsAvailable == null || exam.isLmsAvailable()) {
|
||||
// this.examDAO.markLMSAvailability(quizId, false, updateId);
|
||||
// }
|
||||
//
|
||||
// throw new RuntimeException("Not Available");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -528,7 +528,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CLIENT_GROUPS.of(error));
|
||||
return entity;
|
||||
})
|
||||
.flatMap(this.examTemplateService::initAdditionalAttributes)
|
||||
.flatMap(this.examTemplateService::initAdditionalTemplateAttributes)
|
||||
.onErrorDo(error -> {
|
||||
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error));
|
||||
return entity;
|
||||
|
@ -563,7 +563,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
return Result.tryCatch(() -> {
|
||||
this.examSessionService.flushCache(entity);
|
||||
return entity;
|
||||
}).flatMap(this.examAdminService::saveLMSAttributes);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -586,6 +586,7 @@ sebserver.exam.form.sebrestriction.PERMISSION_COMPONENTS=Permissions
|
|||
sebserver.exam.form.sebrestriction.PERMISSION_COMPONENTS.tooltip=Define the additional SEB restriction permissions
|
||||
sebserver.exam.form.sebrestriction.USER_BANNING_ENABLED=User Banning
|
||||
sebserver.exam.form.sebrestriction.USER_BANNING_ENABLED.tooltip=Indicates whether the user of a restricted access shall be banned on authentication failure or not
|
||||
sebserver.exam.form.sebrestriction.ALT_BEK_KEY=SEB Server Browser Exam Key
|
||||
|
||||
sebserver.exam.form.sebrestriction.whiteListPaths.ABOUT=About
|
||||
sebserver.exam.form.sebrestriction.whiteListPaths.ABOUT.tooltip=The "About" section of the Open edX course
|
||||
|
|
Loading…
Reference in a new issue