SEBSERV-301 SEBSERV-372

This commit is contained in:
anhefti 2022-12-22 17:10:41 +01:00
parent b9962a2609
commit 2434fce036
26 changed files with 548 additions and 369 deletions

View file

@ -63,6 +63,8 @@ public final class Exam implements GrantEntity {
public static final String ATTR_ADDITIONAL_ATTRIBUTES = "additionalAttributes"; 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 */ /** 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"; 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 */ /** This attribute name is used to store the signature check grant threshold for numerical trust checks */

View file

@ -19,6 +19,7 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public class MoodleSEBRestriction { 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_BROWSER_KEYS = "BROWSER_KEYS";
public static final String ATTR_CONFIG_KEYS = "CONFIG_KEYS"; public static final String ATTR_CONFIG_KEYS = "CONFIG_KEYS";

View file

@ -240,7 +240,9 @@ public class ExamForm implements TemplateComposer {
.call() .call()
.onError(e -> log.error("Unexpected error while trying to verify seb restriction settings: ", e)) .onError(e -> log.error("Unexpected error while trying to verify seb restriction settings: ", e))
.getOr(false); .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 // check exam consistency and inform the user if needed
Collection<APIMessage> warnings = null; 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) { private Result<Exam> getExistingExam(final PageContext pageContext) {
final EntityKey entityKey = pageContext.getEntityKey(); final EntityKey entityKey = pageContext.getEntityKey();
return this.restService.getBuilder(GetExam.class) return this.restService.getBuilder(GetExam.class)

View file

@ -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.api.API;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; 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.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.OpenEdxSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
@ -73,6 +74,8 @@ public class ExamSEBRestrictionSettings {
new LocTextKey("sebserver.exam.form.sebrestriction.configKeys"); new LocTextKey("sebserver.exam.form.sebrestriction.configKeys");
private final static LocTextKey SEB_RESTRICTION_FORM_BROWSER_KEYS = private final static LocTextKey SEB_RESTRICTION_FORM_BROWSER_KEYS =
new LocTextKey("sebserver.exam.form.sebrestriction.browserExamKeys"); 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 = private final static LocTextKey SEB_RESTRICTION_FORM_EDX_WHITE_LIST_PATHS =
new LocTextKey("sebserver.exam.form.sebrestriction.WHITELIST_PATHS"); new LocTextKey("sebserver.exam.form.sebrestriction.WHITELIST_PATHS");
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_PERMISSIONS = private final static LocTextKey SEB_RESTRICTION_FORM_EDX_PERMISSIONS =
@ -244,6 +247,16 @@ public class ExamSEBRestrictionSettings {
.asArea(50) .asArea(50)
.readonly(true)) .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( .addField(FormBuilder.text(
SEBRestriction.ATTR_BROWSER_KEYS, SEBRestriction.ATTR_BROWSER_KEYS,
SEB_RESTRICTION_FORM_BROWSER_KEYS, SEB_RESTRICTION_FORM_BROWSER_KEYS,

View file

@ -44,12 +44,6 @@ public interface ExamAdminService {
* @return The exam with the initial additional attributes */ * @return The exam with the initial additional attributes */
Result<Exam> initAdditionalAttributes(final Exam exam); 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. /** Saves the security key settings for an specific exam.
* *
* @param institutionId The institution identifier * @param institutionId The institution identifier

View file

@ -43,11 +43,11 @@ public interface ExamTemplateService {
* @return Result refer to the Exam with added client groups or to an error if happened */ * @return Result refer to the Exam with added client groups or to an error if happened */
Result<Exam> addDefinedClientGroups(Exam exam); 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 * @param exam The Exam to add the default indicator
* @return Result refer to the created exam or to an error when happened */ * @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 /** 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 * is defined by a given linked exam template. This is used to create the exam configuration and automatically

View file

@ -118,7 +118,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
KeyGenerators.string().generateKey().toString()); KeyGenerators.string().generateKey().toString());
return exam; return exam;
}); }).flatMap(this::initAdditionalAttributesForMoodleExams);
} }
@Override @Override
@ -186,11 +186,6 @@ public class ExamAdminServiceImpl implements ExamAdminService {
}); });
} }
@Override
public Result<Exam> saveLMSAttributes(final Exam exam) {
return initAdditionalAttributesForMoodleExams(exam);
}
@Override @Override
public Result<Boolean> isRestricted(final Exam exam) { public Result<Boolean> isRestricted(final Exam exam) {
if (exam == null) { if (exam == null) {
@ -254,6 +249,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
.getLmsAPITemplate(exam.lmsSetupId) .getLmsAPITemplate(exam.lmsSetupId)
.getOrThrow(); .getOrThrow();
// TODO check if this is still needed
if (lmsTemplate.lmsSetup().lmsType == LmsType.MOODLE) { if (lmsTemplate.lmsSetup().lmsType == LmsType.MOODLE) {
lmsTemplate.getQuiz(exam.externalId) lmsTemplate.getQuiz(exam.externalId)
.flatMap(quizData -> this.additionalAttributesDAO.saveAdditionalAttribute( .flatMap(quizData -> this.additionalAttributesDAO.saveAdditionalAttribute(
@ -277,7 +273,8 @@ public class ExamAdminServiceImpl implements ExamAdminService {
EntityType.EXAM, EntityType.EXAM,
exam.id, exam.id,
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK, SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK,
moodleBEK).getOrThrow(); moodleBEK)
.getOrThrow();
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to create additional moodle SEB BEK attribute: ", e); log.error("Failed to create additional moodle SEB BEK attribute: ", e);
} }

View file

@ -57,7 +57,13 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
.getDefaultConfigurationNode(examId) .getDefaultConfigurationNode(examId)
.flatMap(nodeId -> this.configurationDAO.getConfigurationLastStableVersion(nodeId)) .flatMap(nodeId -> this.configurationDAO.getConfigurationLastStableVersion(nodeId))
.map(config -> config.id) .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 final Long attrId = this.configurationAttributeDAO
.getAttributeIdByName(configAttributeName) .getAttributeIdByName(configAttributeName)
@ -99,20 +105,22 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
log.error("Failed to get SEB restriction with quit secret: ", e); log.error("Failed to get SEB restriction with quit secret: ", e);
} }
return null; return StringUtils.EMPTY;
} }
@Override @Override
public String getQuitLink(final Long examId) { public String getQuitLink(final Long examId) {
try { try {
return getMappedDefaultConfigAttributeValue( final String quitLink = getMappedDefaultConfigAttributeValue(
examId, examId,
CONFIG_ATTR_NAME_QUIT_LINK); CONFIG_ATTR_NAME_QUIT_LINK);
return (quitLink != null) ? quitLink : StringUtils.EMPTY;
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to get SEB restriction with quit link: ", e); log.error("Failed to get SEB restriction with quit link: ", e);
return null; return StringUtils.EMPTY;
} }
} }

View file

@ -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.ExamTemplateDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; 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.dao.IndicatorDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateService; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateService;
@Lazy @Lazy
@ -61,7 +60,7 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
private static final Logger log = LoggerFactory.getLogger(ExamTemplateServiceImpl.class); private static final Logger log = LoggerFactory.getLogger(ExamTemplateServiceImpl.class);
private final AdditionalAttributesDAO additionalAttributesDAO; private final AdditionalAttributesDAO additionalAttributesDAO;
private final ExamAdminService examAdminService;
private final ExamTemplateDAO examTemplateDAO; private final ExamTemplateDAO examTemplateDAO;
private final ConfigurationNodeDAO configurationNodeDAO; private final ConfigurationNodeDAO configurationNodeDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO; private final ExamConfigurationMapDAO examConfigurationMapDAO;
@ -78,7 +77,6 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
public ExamTemplateServiceImpl( public ExamTemplateServiceImpl(
final AdditionalAttributesDAO additionalAttributesDAO, final AdditionalAttributesDAO additionalAttributesDAO,
final ExamAdminService examAdminService,
final ExamTemplateDAO examTemplateDAO, final ExamTemplateDAO examTemplateDAO,
final ConfigurationNodeDAO configurationNodeDAO, final ConfigurationNodeDAO configurationNodeDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO, final ExamConfigurationMapDAO examConfigurationMapDAO,
@ -97,7 +95,6 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
this.configurationNodeDAO = configurationNodeDAO; this.configurationNodeDAO = configurationNodeDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO; this.examConfigurationMapDAO = examConfigurationMapDAO;
this.additionalAttributesDAO = additionalAttributesDAO; this.additionalAttributesDAO = additionalAttributesDAO;
this.examAdminService = examAdminService;
this.indicatorDAO = indicatorDAO; this.indicatorDAO = indicatorDAO;
this.clientGroupDAO = clientGroupDAO; this.clientGroupDAO = clientGroupDAO;
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
@ -155,41 +152,40 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
} }
@Override @Override
public Result<Exam> initAdditionalAttributes(final Exam exam) { public Result<Exam> initAdditionalTemplateAttributes(final Exam exam) {
return this.examAdminService return Result.tryCatch(() -> {
.saveLMSAttributes(exam)
.map(_exam -> {
if (exam.examTemplateId != null) { if (exam.examTemplateId != null) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Init exam: {} with additional attributes from exam template: {}", log.debug("Init exam: {} with additional attributes from exam template: {}",
exam.externalId, exam.externalId,
exam.examTemplateId); exam.examTemplateId);
} }
final ExamTemplate examTemplate = this.examTemplateDAO final ExamTemplate examTemplate = this.examTemplateDAO
.byPK(exam.examTemplateId) .byPK(exam.examTemplateId)
.onError(error -> log.warn("No exam template found for id: {}", .onError(error -> log.warn("No exam template found for id: {}",
exam.examTemplateId, exam.examTemplateId,
error.getMessage())) error.getMessage()))
.getOr(null); .getOr(null);
if (examTemplate == null) { if (examTemplate == null) {
return exam; return exam;
} }
if (examTemplate.examAttributes != null && !examTemplate.examAttributes.isEmpty()) { if (examTemplate.examAttributes != null && !examTemplate.examAttributes.isEmpty()) {
this.additionalAttributesDAO.saveAdditionalAttributes( this.additionalAttributesDAO.saveAdditionalAttributes(
EntityType.EXAM, EntityType.EXAM,
exam.getId(), exam.getId(),
examTemplate.examAttributes); examTemplate.examAttributes);
} }
} }
return _exam;
}).onError(error -> log.error( return exam;
"Failed to create additional attributes defined by template for exam: ", }).onError(error -> log.error(
error)); "Failed to create additional attributes defined by template for exam: ",
error));
} }
@Override @Override

View file

@ -17,6 +17,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; 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.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; 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 */ * @return Result refer to the quiz data or to an error when happened */
Result<QuizData> getQuiz(final String id); 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. */ /** Clears the underling caches if there are some for a particular implementation. */
void clearCourseCache(); void clearCourseCache();

View file

@ -19,8 +19,7 @@ public interface SEBRestrictionService {
String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key"; String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key";
/** This attribute name is used to store the per Moolde(plugin) exam generated alternative BEK */ /** This attribute name is used to store the per Moolde(plugin) exam generated alternative BEK */
String ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK = String ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK = "ALTERNATIVE_SEB_BEK";
SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX + "ALTERNATIVE_SEB_BEK";
/** Get the LmsAPIService that is used by the SEBRestrictionService */ /** Get the LmsAPIService that is used by the SEBRestrictionService */
LmsAPIService getLmsAPIService(); LmsAPIService getLmsAPIService();

View file

@ -281,6 +281,26 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
.getOrThrow()); .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 @Override
public void clearCourseCache() { public void clearCourseCache() {
if (this.courseAccessAPI != null) { if (this.courseAccessAPI != null) {
@ -372,8 +392,6 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
log.debug("Get course restriction: {} for LMSSetup: {}", exam.externalId, lmsSetup()); log.debug("Get course restriction: {} for LMSSetup: {}", exam.externalId, lmsSetup());
} }
System.out.println("******************* getSEBClientRestriction");
return this.restrictionRequest.protectedRun(() -> this.sebRestrictionAPI return this.restrictionRequest.protectedRun(() -> this.sebRestrictionAPI
.getSEBClientRestriction(exam) .getSEBClientRestriction(exam)
.onError(error -> log.error("Failed to get SEB restrictions: {}", error.getMessage())) .onError(error -> log.error("Failed to get SEB restrictions: {}", error.getMessage()))

View file

@ -29,9 +29,11 @@ import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.EntityType; 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;
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.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; 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.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.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
@ -135,6 +137,16 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
e); 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( return new SEBRestriction(
exam.id, exam.id,
configKeys, configKeys,

View file

@ -216,6 +216,11 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
return quizRequest(id); 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) { private List<QuizData> collectAllQuizzes(final AnsPersonalRestTemplate restTemplate) {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final List<QuizData> quizDatas = getAssignments(restTemplate) final List<QuizData> quizDatas = getAssignments(restTemplate)

View file

@ -45,6 +45,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; 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.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.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; 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 @Override
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) { public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
if (ids.size() == 1) { if (ids.size() == 1) {

View file

@ -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.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; 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.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.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
@ -233,6 +234,11 @@ public class MockCourseAccessAPI implements CourseAccessAPI {
.get()); .get());
} }
@Override
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
return Result.ofError(new UnsupportedOperationException("Recovering not supported"));
}
@Override @Override
public void clearCourseCache() { public void clearCourseCache() {
// No cache here // No cache here

View file

@ -9,7 +9,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -18,6 +18,7 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -27,13 +28,16 @@ import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.core.JsonProcessingException; 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.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; 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.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils; 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.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.MoodlePluginCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseRestriction;
public class MockupRestTemplateFactory implements MoodleRestTemplateFactory { public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
@ -144,18 +148,27 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
System.out.println("***** callMoodleAPIFunction HttpEntity: " + functionReqEntity); System.out.println("***** callMoodleAPIFunction HttpEntity: " + functionReqEntity);
// TODO return json
if (MoodlePluginCourseAccess.COURSES_API_FUNCTION_NAME.equals(functionName)) { if (MoodlePluginCourseAccess.COURSES_API_FUNCTION_NAME.equals(functionName)) {
return respondCourses(queryAttributes); 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)) { } else if (MoodlePluginCourseAccess.USERS_API_FUNCTION_NAME.equals(functionName)) {
return respondUsers(queryAttributes); 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); throw new RuntimeException("Unknown function: " + functionName);
} }
} }
@SuppressWarnings("unused")
private static final class MockCD { private static final class MockCD {
public final String id; public final String id;
public final String shortname; public final String shortname;
@ -167,8 +180,9 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
public final Long enddate; // unix-time seconds UTC public final Long enddate; // unix-time seconds UTC
public final Long timecreated; // unix-time seconds UTC public final Long timecreated; // unix-time seconds UTC
public final boolean visible; 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.id = num;
this.shortname = "c" + num; this.shortname = "c" + num;
this.categoryid = "mock"; this.categoryid = "mock";
@ -179,9 +193,11 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
this.enddate = null; this.enddate = null;
this.timecreated = Long.valueOf(num); this.timecreated = Long.valueOf(num);
this.visible = true; this.visible = true;
this.quizzes = quizzes;
} }
} }
@SuppressWarnings("unused")
private static final class MockQ { private static final class MockQ {
public final String id; public final String id;
public final String coursemodule; public final String coursemodule;
@ -209,12 +225,17 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
System.out.println("************* from: " + from); System.out.println("************* from: " + from);
final List<MockCD> courses; final List<MockCD> courses;
if (ids != null && !ids.isEmpty()) { 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) { } else if (from != null && Integer.valueOf(from) < 11) {
courses = new ArrayList<>(); courses = new ArrayList<>();
final int num = (Integer.valueOf(from) > 0) ? 10 : 1; final int num = (Integer.valueOf(from) > 0) ? 10 : 1;
for (int i = 0; i < 10; i++) { 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 { } else {
courses = new ArrayList<>(); courses = new ArrayList<>();
@ -232,28 +253,56 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
} }
} }
private String respondQuizzes(final MultiValueMap<String, String> queryAttributes) { private final Map<String, MoodleQuizRestriction> restrcitions = new HashMap<>();
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();
}
final Map<String, Object> response = new HashMap<>(); private String respondSetRestriction(final String quizId, final MultiValueMap<String, String> queryAttributes) {
response.put("quizzes", quizzes); final List<String> configKeys = queryAttributes.get(MoodlePluginCourseRestriction.ATTRIBUTE_CONFIG_KEYS);
final JSONMapper jsonMapper = new JSONMapper(); final List<String> beks = queryAttributes.get(MoodlePluginCourseRestriction.ATTRIBUTE_BROWSER_EXAM_KEYS);
final String result = jsonMapper.writeValueAsString(response); final String quitURL = queryAttributes.getFirst(MoodlePluginCourseRestriction.ATTRIBUTE_QUIT_URL);
System.out.println("******** quizzes response: " + result); final String quitSecret = queryAttributes.getFirst(MoodlePluginCourseRestriction.ATTRIBUTE_QUIT_SECRET);
return result;
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) { } catch (final JsonProcessingException e) {
e.printStackTrace(); e.printStackTrace();
return ""; 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) { private String respondUsers(final MultiValueMap<String, String> queryAttributes) {
// TODO // TODO
return ""; return "";

View file

@ -47,7 +47,6 @@ public class MoodlePluginCheck {
try { try {
restTemplate.testAPIConnection( restTemplate.testAPIConnection(
MoodlePluginCourseAccess.COURSES_API_FUNCTION_NAME, MoodlePluginCourseAccess.COURSES_API_FUNCTION_NAME,
MoodlePluginCourseAccess.QUIZZES_BY_COURSES_API_FUNCTION_NAME,
MoodlePluginCourseAccess.USERS_API_FUNCTION_NAME); MoodlePluginCourseAccess.USERS_API_FUNCTION_NAME);
} catch (final Exception e) { } catch (final Exception e) {
log.info("Moodle SEB Server Plugin not available: {}", e.getMessage()); log.info("Moodle SEB Server Plugin not available: {}", e.getMessage());

View file

@ -25,7 +25,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; 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 end_date; // unix-time seconds UTC
public final Long time_created; // unix-time seconds UTC public final Long time_created; // unix-time seconds UTC
public final String category_id; public final String category_id;
public final Collection<CourseQuiz> quizzes;
@JsonIgnore
public final Collection<CourseQuiz> quizzes = new ArrayList<>();
@JsonCreator @JsonCreator
public CourseData( public CourseData(
@JsonProperty(value = "id") final String id, @JsonProperty("id") final String id,
@JsonProperty(value = "shortname") final String short_name, @JsonProperty("shortname") final String short_name,
@JsonProperty(value = "idnumber") final String idnumber, @JsonProperty("idnumber") final String idnumber,
@JsonProperty(value = "fullname") final String full_name, @JsonProperty("fullname") final String full_name,
@JsonProperty(value = "displayname") final String display_name, @JsonProperty("displayname") final String display_name,
@JsonProperty(value = "summary") final String summary, @JsonProperty("summary") final String summary,
@JsonProperty(value = "startdate") final Long start_date, @JsonProperty("startdate") final Long start_date,
@JsonProperty(value = "enddate") final Long end_date, @JsonProperty("enddate") final Long end_date,
@JsonProperty(value = "timecreated") final Long time_created, @JsonProperty("timecreated") final Long time_created,
@JsonProperty(value = "categoryid") final String category_id) { @JsonProperty("categoryid") final String category_id,
@JsonProperty("quizzes") final Collection<CourseQuiz> quizzes) {
this.id = id; this.id = id;
this.short_name = short_name; this.short_name = short_name;
@ -276,6 +274,7 @@ public abstract class MoodleUtils {
this.end_date = end_date; this.end_date = end_date;
this.time_created = time_created; this.time_created = time_created;
this.category_id = category_id; this.category_id = category_id;
this.quizzes = (quizzes == null) ? new ArrayList<>() : quizzes;
} }
} }
@ -286,8 +285,8 @@ public abstract class MoodleUtils {
@JsonCreator @JsonCreator
public Courses( public Courses(
@JsonProperty(value = "courses") final Collection<CourseData> courses, @JsonProperty("courses") final Collection<CourseData> courses,
@JsonProperty(value = "warnings") final Collection<Warning> warnings) { @JsonProperty("warnings") final Collection<Warning> warnings) {
this.courses = courses; this.courses = courses;
this.warnings = warnings; this.warnings = warnings;
} }
@ -300,8 +299,8 @@ public abstract class MoodleUtils {
@JsonCreator @JsonCreator
public CourseQuizData( public CourseQuizData(
@JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes, @JsonProperty("quizzes") final Collection<CourseQuiz> quizzes,
@JsonProperty(value = "warnings") final Collection<Warning> warnings) { @JsonProperty("warnings") final Collection<Warning> warnings) {
this.quizzes = quizzes; this.quizzes = quizzes;
this.warnings = warnings; this.warnings = warnings;
} }
@ -320,14 +319,14 @@ public abstract class MoodleUtils {
@JsonCreator @JsonCreator
public CourseQuiz( public CourseQuiz(
@JsonProperty(value = "id") final String id, @JsonProperty("id") final String id,
@JsonProperty(value = "course") final String course, @JsonProperty("course") final String course,
@JsonProperty(value = "coursemodule") final String course_module, @JsonProperty("coursemodule") final String course_module,
@JsonProperty(value = "name") final String name, @JsonProperty("name") final String name,
@JsonProperty(value = "intro") final String intro, @JsonProperty("intro") final String intro,
@JsonProperty(value = "timeopen") final Long time_open, @JsonProperty("timeopen") final Long time_open,
@JsonProperty(value = "timeclose") final Long time_close, @JsonProperty("timeclose") final Long time_close,
@JsonProperty(value = "timelimit") final Long time_limit) { @JsonProperty("timelimit") final Long time_limit) {
this.id = id; this.id = id;
this.course = course; this.course = course;
@ -363,24 +362,24 @@ public abstract class MoodleUtils {
@JsonCreator @JsonCreator
public MoodleUserDetails( public MoodleUserDetails(
@JsonProperty(value = "id") final String id, @JsonProperty("id") final String id,
@JsonProperty(value = "username") final String username, @JsonProperty("username") final String username,
@JsonProperty(value = "firstname") final String firstname, @JsonProperty("firstname") final String firstname,
@JsonProperty(value = "lastname") final String lastname, @JsonProperty("lastname") final String lastname,
@JsonProperty(value = "fullname") final String fullname, @JsonProperty("fullname") final String fullname,
@JsonProperty(value = "email") final String email, @JsonProperty("email") final String email,
@JsonProperty(value = "department") final String department, @JsonProperty("department") final String department,
@JsonProperty(value = "firstaccess") final Long firstaccess, @JsonProperty("firstaccess") final Long firstaccess,
@JsonProperty(value = "lastaccess") final Long lastaccess, @JsonProperty("lastaccess") final Long lastaccess,
@JsonProperty(value = "auth") final String auth, @JsonProperty("auth") final String auth,
@JsonProperty(value = "suspended") final Boolean suspended, @JsonProperty("suspended") final Boolean suspended,
@JsonProperty(value = "confirmed") final Boolean confirmed, @JsonProperty("confirmed") final Boolean confirmed,
@JsonProperty(value = "lang") final String lang, @JsonProperty("lang") final String lang,
@JsonProperty(value = "theme") final String theme, @JsonProperty("theme") final String theme,
@JsonProperty(value = "timezone") final String timezone, @JsonProperty("timezone") final String timezone,
@JsonProperty(value = "description") final String description, @JsonProperty("description") final String description,
@JsonProperty(value = "mailformat") final Integer mailformat, @JsonProperty("mailformat") final Integer mailformat,
@JsonProperty(value = "descriptionformat") final Integer descriptionformat) { @JsonProperty("descriptionformat") final Integer descriptionformat) {
this.id = id; this.id = id;
this.username = username; 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) @JsonIgnoreProperties(ignoreUnknown = true)
public static final class CoursePage { public static final class CoursePage {
public final Collection<CourseKey> courseKeys; public final Collection<CourseKey> courseKeys;
public final Collection<Warning> warnings; public final Collection<Warning> warnings;
@JsonCreator
public CoursePage( public CoursePage(
@JsonProperty(value = "courses") final Collection<CourseKey> courseKeys, @JsonProperty("courses") final Collection<CourseKey> courseKeys,
@JsonProperty(value = "warnings") final Collection<Warning> warnings) { @JsonProperty("warnings") final Collection<Warning> warnings) {
this.courseKeys = courseKeys; this.courseKeys = courseKeys;
this.warnings = warnings; this.warnings = warnings;
@ -464,10 +426,10 @@ public abstract class MoodleUtils {
@JsonCreator @JsonCreator
public CourseKey( public CourseKey(
@JsonProperty(value = "id") final String id, @JsonProperty("id") final String id,
@JsonProperty(value = "shortname") final String short_name, @JsonProperty("shortname") final String short_name,
@JsonProperty(value = "categoryname") final String category_name, @JsonProperty("categoryname") final String category_name,
@JsonProperty(value = "sortorder") final String sort_order) { @JsonProperty("sortorder") final String sort_order) {
this.id = id; this.id = id;
this.short_name = short_name; this.short_name = short_name;
@ -501,11 +463,11 @@ public abstract class MoodleUtils {
@JsonCreator @JsonCreator
public MoodleQuizRestriction( public MoodleQuizRestriction(
final String quiz_id, @JsonProperty("quiz_id") final String quiz_id,
final String config_keys, @JsonProperty("config_keys") final String config_keys,
final String browser_exam_keys, @JsonProperty("browser_exam_keys") final String browser_exam_keys,
final String quit_link, @JsonProperty("quit_link") final String quit_link,
final String quit_secret) { @JsonProperty("quit_secret") final String quit_secret) {
this.quiz_id = quiz_id; this.quiz_id = quiz_id;
this.config_keys = config_keys; this.config_keys = config_keys;

View file

@ -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.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; 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.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.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; 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_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_COURSE_IDS = "courseids";
static final String MOODLE_COURSE_API_IDS = "ids"; 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_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_BY_FIELD_API_FUNCTION_NAME = "core_course_get_courses_by_field";
static final String MOODLE_COURSE_API_FIELD_NAME = "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 @Override
public void clearCourseCache() { public void clearCourseCache() {

View file

@ -19,7 +19,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; 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.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; 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.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.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; 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.MoodleRestTemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils; 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.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.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 { 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 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 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 = "core_user_get_users_by_field";
public static final String USERS_API_FUNCTION_NAME = "quizaccess_sebserver_get_users";
public static final String ATTR_FIELD = "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_COURSE_IDS = "ids";
public static final String CRITERIA_FROM_DATE = "from_date"; public static final String CRITERIA_FROM_DATE = "from_date";
public static final String CRITERIA_TO_DATE = "to_date"; public static final String CRITERIA_TO_DATE = "to_date";
@ -147,7 +148,6 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
restTemplate.testAPIConnection( restTemplate.testAPIConnection(
COURSES_API_FUNCTION_NAME, COURSES_API_FUNCTION_NAME,
QUIZZES_BY_COURSES_API_FUNCTION_NAME,
USERS_API_FUNCTION_NAME); USERS_API_FUNCTION_NAME);
} catch (final RuntimeException e) { } catch (final RuntimeException e) {
@ -243,15 +243,50 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
} }
@Override @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(() -> { return Result.tryCatch(() -> {
final MoodleAPIRestTemplate template = getRestTemplate() final MoodleAPIRestTemplate template = getRestTemplate()
.getOrThrow(); .getOrThrow();
final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>(); final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>();
queryAttributes.add(ATTR_FIELD, "id"); queryAttributes.add(ATTR_FIELD, ATTR_ID);
queryAttributes.add("values[0]", examineeUserId); queryAttributes.add(ATTR_VALUE, examineeSessionId);
final String userDetailsJSON = template.callMoodleAPIFunction( final String userDetailsJSON = template.callMoodleAPIFunction(
USERS_API_FUNCTION_NAME, 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)"); 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, userDetailsJSON,
new TypeReference<MoodlePluginUserDetails[]>() { new TypeReference<MoodleUserDetails[]>() {
}); });
if (userDetails == null || userDetails.length <= 0) { if (userDetails == null || userDetails.length <= 0) {
throw new RuntimeException("No user details on Moodle API request"); 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( return new ExamineeAccountDetails(
userDetails[0].id, userDetails[0].id,
userDetails[0].fullname, userDetails[0].fullname,
userDetails[0].username, userDetails[0].username,
userDetails[0].email, 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 + MOODLE_QUIZ_START_URL_PATH
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; : lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
// first get courses page from moodle final Collection<CourseData> fetchCoursesPage =
final Map<String, CourseData> courseData = new HashMap<>(); fetchCoursesPage(restTemplate, quizFromTime, page, this.pageSize);
final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, quizFromTime, page, this.pageSize); // finish if page is empty (no courses left
if (fetchCoursesPage.isEmpty()) {
// no courses for page --> finish
if (coursesPage == null || coursesPage.isEmpty()) {
asyncQuizFetchBuffer.finish(); asyncQuizFetchBuffer.finish();
return; return;
} }
courseData.putAll(coursesPage // fetch and buffer quizzes
.stream() fetchCoursesPage.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()
.filter(c -> !c.quizzes.isEmpty()) .filter(c -> !c.quizzes.isEmpty())
.forEach(c -> asyncQuizFetchBuffer.buffer.addAll( .forEach(c -> asyncQuizFetchBuffer.buffer.addAll(
MoodleUtils.quizDataOf(lmsSetup, c, urlPrefix, this.prependShortCourseName) MoodleUtils.quizDataOf(lmsSetup, c, urlPrefix, this.prependShortCourseName)
@ -378,18 +381,23 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
.filter(quizFilter) .filter(quizFilter)
.collect(Collectors.toList()))); .collect(Collectors.toList())));
// check thresholds
if (asyncQuizFetchBuffer.buffer.size() > this.maxSize) { if (asyncQuizFetchBuffer.buffer.size() > this.maxSize) {
log.warn("Maximal moodle quiz fetch size of {} reached. Cancel fetch at this point.", this.maxSize); log.warn("Maximal moodle quiz fetch size of {} reached. Cancel fetch at this point.", this.maxSize);
asyncQuizFetchBuffer.finish(); asyncQuizFetchBuffer.finish();
} }
} }
private Collection<CourseData> getCoursesPage( private Collection<CourseData> fetchCoursesPage(
final MoodleAPIRestTemplate restTemplate, final MoodleAPIRestTemplate restTemplate,
final DateTime quizFromTime, final DateTime quizFromTime,
final int page, final int page,
final int size) throws JsonParseException, JsonMappingException, IOException { final int size) throws JsonParseException, JsonMappingException, IOException {
if (log.isDebugEnabled()) {
log.debug("Fetch course page: {}, size: {} quizFromTime: {}", page, size, quizFromTime);
}
final String lmsName = getLmsSetupName(); final String lmsName = getLmsSetupName();
try { try {
// get course ids per page // get course ids per page
@ -442,59 +450,31 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
private List<QuizData> getQuizzesForIds( private List<QuizData> getQuizzesForIds(
final MoodleAPIRestTemplate restTemplate, final MoodleAPIRestTemplate restTemplate,
final Set<String> quizIds) { final Set<String> internalIds) {
try { try {
final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup(); 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)) final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + 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() .stream()
.filter(c -> !c.quizzes.isEmpty()) .filter(courseData -> !courseData.quizzes.isEmpty())
.flatMap(cd -> MoodleUtils.quizDataOf( .flatMap(courseData -> MoodleUtils.quizDataOf(
lmsSetup, lmsSetup,
cd, courseData,
urlPrefix, urlPrefix,
this.prependShortCourseName).stream()) this.prependShortCourseName).stream())
.collect(Collectors.toList()); .collect(Collectors.toList());

View file

@ -105,7 +105,10 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
final LinkedMultiValueMap<String, String> addQuery = new LinkedMultiValueMap<>(); final LinkedMultiValueMap<String, String> addQuery = new LinkedMultiValueMap<>();
addQuery.add(ATTRIBUTE_QUIZ_ID, quizId); 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 { try {
@ -139,9 +142,10 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
final ArrayList<String> configKeys = new ArrayList<>(sebRestrictionData.configKeys); final ArrayList<String> configKeys = new ArrayList<>(sebRestrictionData.configKeys);
final String quitLink = this.examConfigurationValueService.getQuitLink(exam.id); final String quitLink = this.examConfigurationValueService.getQuitLink(exam.id);
final String quitSecret = this.examConfigurationValueService.getQuitSecret(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); SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK);
if (additionalBEK != null) {
if (additionalBEK != null && !beks.contains(additionalBEK)) {
beks.add(additionalBEK); beks.add(additionalBEK);
} }
@ -200,18 +204,12 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
final List<String> configKeys = Arrays.asList(StringUtils.split( final List<String> configKeys = Arrays.asList(StringUtils.split(
moodleRestriction.config_keys, moodleRestriction.config_keys,
Constants.LIST_SEPARATOR)); 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, moodleRestriction.browser_exam_keys,
Constants.LIST_SEPARATOR)); Constants.LIST_SEPARATOR)));
final Map<String, String> additionalProperties = new HashMap<>(); final Map<String, String> additionalProperties = new HashMap<>();
additionalProperties.put(ATTRIBUTE_QUIT_URL, moodleRestriction.quit_link); additionalProperties.put(ATTRIBUTE_QUIT_URL, moodleRestriction.quit_link);
additionalProperties.put(ATTRIBUTE_QUIT_SECRET, moodleRestriction.quit_secret);
final String additionalBEK = exam.getAdditionalAttribute(
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK);
if (additionalBEK != null) {
browserExamKeys.remove(additionalBEK);
}
return new SEBRestriction( return new SEBRestriction(
exam.id, exam.id,

View file

@ -220,6 +220,11 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
return quizRequest(id); return quizRequest(id);
} }
@Override
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
return Result.ofError(new UnsupportedOperationException("Recovering not supported"));
}
@Override @Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) { public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
return getRestTemplate().map(t -> this.getExamineeById(t, examineeUserId)); return getRestTemplate().map(t -> this.getExamineeById(t, examineeUserId));

View file

@ -14,7 +14,6 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.DateTimeZone; import org.joda.time.DateTimeZone;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -24,21 +23,19 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; 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;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; 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.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.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo; 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.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.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.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.ExamFinishedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent; 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 static final Logger log = LoggerFactory.getLogger(ExamUpdateHandler.class);
private final ExamDAO examDAO; private final ExamDAO examDAO;
private final AdditionalAttributesDAO additionalAttributesDAO;
private final ApplicationEventPublisher applicationEventPublisher; private final ApplicationEventPublisher applicationEventPublisher;
private final SEBRestrictionService sebRestrictionService; private final SEBRestrictionService sebRestrictionService;
private final LmsAPIService lmsAPIService; private final LmsAPIService lmsAPIService;
private final String updatePrefix; private final String updatePrefix;
private final Long examTimeSuffix; private final Long examTimeSuffix;
private final boolean tryRecoverExam; private final boolean tryRecoverExam;
private final int recoverAttempts;
public ExamUpdateHandler( public ExamUpdateHandler(
final ExamDAO examDAO, final ExamDAO examDAO,
final AdditionalAttributesDAO additionalAttributesDAO,
final ApplicationEventPublisher applicationEventPublisher, final ApplicationEventPublisher applicationEventPublisher,
final SEBRestrictionService sebRestrictionService, final SEBRestrictionService sebRestrictionService,
final LmsAPIService lmsAPIService, final LmsAPIService lmsAPIService,
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
@Value("${sebserver.webservice.api.exam.time-suffix:3600000}") final Long examTimeSuffix, @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.examDAO = examDAO;
this.additionalAttributesDAO = additionalAttributesDAO;
this.applicationEventPublisher = applicationEventPublisher; this.applicationEventPublisher = applicationEventPublisher;
this.sebRestrictionService = sebRestrictionService; this.sebRestrictionService = sebRestrictionService;
this.lmsAPIService = lmsAPIService; this.lmsAPIService = lmsAPIService;
@ -75,6 +77,7 @@ class ExamUpdateHandler {
+ "_" + webserviceInfo.getServerPort() + "_"; + "_" + webserviceInfo.getServerPort() + "_";
this.examTimeSuffix = examTimeSuffix; this.examTimeSuffix = examTimeSuffix;
this.tryRecoverExam = tryRecoverExam; this.tryRecoverExam = tryRecoverExam;
this.recoverAttempts = recoverAttempts;
} }
public SEBRestrictionService getSEBRestrictionService() { public SEBRestrictionService getSEBRestrictionService() {
@ -379,67 +382,124 @@ class ExamUpdateHandler {
final String updateId) { final String updateId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final Exam exam = exams.get(quizId); final Exam exam = exams.get(quizId);
final LmsAPITemplate lmsTemplate = this.lmsAPIService final int attempts = Integer.parseInt(this.additionalAttributesDAO.getAdditionalAttribute(
.getLmsAPITemplate(lmsSetupId) EntityType.EXAM,
.getOrThrow(); 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 if (attempts >= this.recoverAttempts) {
// NOTE: This is a workaround for Moodle quizzes that had have a recovery within the sandbox tool if (log.isDebugEnabled()) {
// Where potentially quiz identifiers get changed during such a recovery and the SEB Server log.debug("Skip recovering quiz due to too many attempts: {}", exam.getModelId());
// internal mapping is not working properly anymore. In this case we try to recover from such throw new RuntimeException("Recover attempts reached");
// 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 (exam.lmsAvailable == null || exam.isLmsAvailable()) { log.info(
this.examDAO.markLMSAvailability(quizId, false, updateId); "Try to recover quiz data for Moodle quiz with internal identifier: {}",
} quizId);
throw new RuntimeException("Not Available");
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");
}); });
} }

View file

@ -528,7 +528,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CLIENT_GROUPS.of(error)); errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CLIENT_GROUPS.of(error));
return entity; return entity;
}) })
.flatMap(this.examTemplateService::initAdditionalAttributes) .flatMap(this.examTemplateService::initAdditionalTemplateAttributes)
.onErrorDo(error -> { .onErrorDo(error -> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error)); errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error));
return entity; return entity;
@ -563,7 +563,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
this.examSessionService.flushCache(entity); this.examSessionService.flushCache(entity);
return entity; return entity;
}).flatMap(this.examAdminService::saveLMSAttributes); });
} }
@Override @Override

View file

@ -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.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=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.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=About
sebserver.exam.form.sebrestriction.whiteListPaths.ABOUT.tooltip=The "About" section of the Open edX course sebserver.exam.form.sebrestriction.whiteListPaths.ABOUT.tooltip=The "About" section of the Open edX course