SEBSERV-351 implementation (no tests yet)

This commit is contained in:
anhefti 2022-10-24 16:10:03 +02:00
parent f131276bdd
commit bb0c834676
17 changed files with 226 additions and 20 deletions

View file

@ -28,4 +28,10 @@ public interface ConfigurationAttributeDAO extends EntityDAO<ConfigurationAttrib
* @return Result refer to a collection of child ConfigurationAttribute or to an error if happened */
Result<Collection<ConfigurationAttribute>> allChildAttributes(final Long parentId);
/** Use this to geht an attribute identifier by attribute name
*
* @param configAttributeName the attribute name
* @return Result refer to the attribute identifier or to an error if happened */
Result<Long> getAttributeIdByName(String configAttributeName);
}

View file

@ -71,4 +71,11 @@ public interface ConfigurationValueDAO extends EntityDAO<ConfigurationValue, Con
Long configurationId,
Long attributeId);
/** Use this to get a specific SEB Settings attribute value.
*
* @param configId the configuration identifier for the value
* @param attrId the attribute identifier
* @return the String value of the SEB setting attribute */
Result<String> getConfigAttributeValue(Long configId, Long attrId);
}

View file

@ -93,6 +93,19 @@ public class ConfigurationAttributeDAOImpl implements ConfigurationAttributeDAO
});
}
@Override
@Transactional(readOnly = true)
public Result<Long> getAttributeIdByName(final String attributeName) {
return Result.tryCatch(() -> this.configurationAttributeRecordMapper
.selectByExample()
.where(
ConfigurationAttributeRecordDynamicSqlSupport.name,
SqlBuilder.isEqualTo(attributeName))
.build()
.execute()
.get(0).getId());
}
@Override
@Transactional(readOnly = true)
public Result<Collection<ConfigurationAttribute>> allMatching(

View file

@ -19,6 +19,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@ -122,6 +123,34 @@ public class ConfigurationValueDAOImpl implements ConfigurationValueDAO {
.collect(Collectors.toList()));
}
@Override
public Result<String> getConfigAttributeValue(final Long configId, final Long attrId) {
return Result.tryCatch(() -> {
final List<ConfigurationValueRecord> records = this.configurationValueRecordMapper.selectByExample()
.where(
ConfigurationValueRecordDynamicSqlSupport.configurationId,
isEqualTo(configId))
.and(
ConfigurationValueRecordDynamicSqlSupport.configurationAttributeId,
SqlBuilder.isEqualTo(attrId))
.build()
.execute();
if (records == null) {
throw new NoSuchElementException(
"No SEB setting attribute value found for configId: " + configId + " attrId: " + attrId);
}
if (records.size() > 1) {
log.warn("Found more then one attribute value for configId: {}, attrId:{} select first one.", configId,
attrId);
}
return records.get(0).getValue();
});
}
@Override
@Transactional(readOnly = true)
public Result<Collection<ConfigurationValue>> allOf(final Set<Long> pks) {

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.exam;
public interface ExamConfigurationValueService {
/** Get the actual SEB settings attribute value for the exam configuration mapped as default configuration
* to the given exam
*
* @param examId The exam identifier
* @param configAttributeName The name of the SEB settings attribute
* @return The current value of the above SEB settings attribute and given exam. */
String getMappedDefaultConfigAttributeValue(Long examId, String configAttributeName);
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
@Service
public class ExamConfigurationValueServiceImpl implements ExamConfigurationValueService {
private static final Logger log = LoggerFactory.getLogger(ExamConfigurationValueServiceImpl.class);
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final ConfigurationDAO configurationDAO;
private final ConfigurationAttributeDAO configurationAttributeDAO;
private final ConfigurationValueDAO configurationValueDAO;
public ExamConfigurationValueServiceImpl(
final ExamConfigurationMapDAO examConfigurationMapDAO,
final ConfigurationDAO configurationDAO,
final ConfigurationAttributeDAO configurationAttributeDAO,
final ConfigurationValueDAO configurationValueDAO) {
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.configurationDAO = configurationDAO;
this.configurationAttributeDAO = configurationAttributeDAO;
this.configurationValueDAO = configurationValueDAO;
}
@Override
public String getMappedDefaultConfigAttributeValue(final Long examId, final String configAttributeName) {
try {
final Long configId = this.examConfigurationMapDAO
.getDefaultConfigurationNode(examId)
.flatMap(nodeId -> this.configurationDAO.getConfigurationLastStableVersion(nodeId))
.map(config -> config.id)
.getOrThrow();
final Long attrId = this.configurationAttributeDAO
.getAttributeIdByName(configAttributeName)
.onError(error -> log.error("Failed to get attribute id with name: {} for exam: {}",
configAttributeName, examId, error))
.getOr(null);
return this.configurationValueDAO
.getConfigAttributeValue(configId, attrId)
.getOrThrow();
} catch (final Exception e) {
log.error("Unexpected error while trying to extract SEB settings attribute value:", e);
return null;
}
}
}

View file

@ -56,12 +56,12 @@ public interface SEBRestrictionAPI {
/** Applies SEB Client restrictions to the LMS with the given attributes.
*
* @param externalExamId The exam/course identifier from LMS side (Exam.externalId)
* @param exam The exam to apply the restriction for
* @param sebRestrictionData containing all data for SEB Client restriction to apply to the LMS
* @return Result refer to the given {@link SEBRestrictionData } if restriction was successful or to an error if
* not */
Result<SEBRestriction> applySEBClientRestriction(
String externalExamId,
Exam exam,
SEBRestriction sebRestrictionData);
/** Releases an already applied SEB Client restriction within the LMS for a given Exam.

View file

@ -409,7 +409,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final Exam exam,
final SEBRestriction sebRestrictionData) {
if (this.sebRestrictionAPI == null) {
@ -418,11 +418,11 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
}
if (log.isDebugEnabled()) {
log.debug("Apply course restriction: {} for LMSSetup: {}", externalExamId, lmsSetup());
log.debug("Apply course restriction: {} for LMSSetup: {}", exam, lmsSetup());
}
return this.restrictionRequest.protectedRun(() -> this.sebRestrictionAPI
.applySEBClientRestriction(externalExamId, sebRestrictionData)
.applySEBClientRestriction(exam, sebRestrictionData)
.onError(error -> log.error(
"Failed to apply SEB restrictions: {}",
error.getMessage()))

View file

@ -237,7 +237,7 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
return this.lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId)
.flatMap(lmsTemplate -> lmsTemplate.applySEBClientRestriction(
exam.externalId,
exam,
sebRestrictionData))
.map(data -> exam)
.getOrThrow();

View file

@ -56,8 +56,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier
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.impl.AbstractCachedCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.SEBServerData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.AssignmentData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.SEBServerData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.UserData;
public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements LmsAPITemplate {
@ -371,10 +371,10 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final Exam exam,
final SEBRestriction sebRestrictionData) {
return getRestTemplate()
.map(t -> this.setRestrictionForAssignmentId(t, externalExamId, sebRestrictionData));
.map(t -> this.setRestrictionForAssignmentId(t, exam.externalId, sebRestrictionData));
}
@Override

View file

@ -148,16 +148,16 @@ public class OpenEdxCourseRestriction implements SEBRestrictionAPI {
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final Exam exam,
final SEBRestriction sebRestrictionData) {
if (log.isDebugEnabled()) {
log.debug("PUT SEB Client restriction on course: {} : {}", externalExamId, sebRestrictionData);
log.debug("PUT SEB Client restriction on course: {} : {}", exam.externalId, sebRestrictionData);
}
return Result.tryCatch(() -> {
final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(externalExamId);
final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(exam.externalId);
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
@ -172,7 +172,7 @@ public class OpenEdxCourseRestriction implements SEBRestrictionAPI {
.getBody();
if (log.isDebugEnabled()) {
log.debug("Successfully PUT SEB Client restriction on course: {} : {}", externalExamId, body);
log.debug("Successfully PUT SEB Client restriction on course: {} : {}", exam.externalId, body);
}
return sebRestrictionData;

View file

@ -36,7 +36,7 @@ public class MockSEBRestrictionAPI implements SEBRestrictionAPI {
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final Exam exam,
final SEBRestriction sebRestrictionData) {
log.info("Apply SEB Client restriction: {}", sebRestrictionData);

View file

@ -126,11 +126,11 @@ public class MoodleCourseRestriction implements SEBRestrictionAPI {
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final Exam exam,
final SEBRestriction sebRestrictionData) {
return this.updateSEBRestriction(
externalExamId,
exam.externalId,
MoodleSEBRestriction.from(sebRestrictionData))
.map(result -> sebRestrictionData);
}

View file

@ -29,8 +29,10 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
}
@Override
public Result<SEBRestriction> applySEBClientRestriction(final String externalExamId,
public Result<SEBRestriction> applySEBClientRestriction(
final Exam exam,
final SEBRestriction sebRestrictionData) {
// TODO Auto-generated method stub
return null;
}

View file

@ -49,6 +49,7 @@ import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
@ -62,9 +63,16 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
private static final Logger log = LoggerFactory.getLogger(OlatLmsAPITemplate.class);
private static final String ADDITIONAL_ATTR_QUIT_LINK = "ADDITIONAL_ATTR_QUIT_LINK";
private static final String ADDITIONAL_ATTR_QUIT_SECRET = "ADDITIONAL_ATTR_QUIT_SECRET";
private static final String CONFIG_ATTR_NAME_QUIT_LINK = "quitURL";
private static final String CONFIG_ATTR_NAME_QUIT_SECRET = "hashedQuitPassword";
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final ClientCredentialService clientCredentialService;
private final APITemplateDataSupplier apiTemplateDataSupplier;
private final ExamConfigurationValueService examConfigurationValueService;
private final Long lmsSetupId;
private OlatLmsRestTemplate cachedRestTemplate;
@ -73,6 +81,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ClientCredentialService clientCredentialService,
final APITemplateDataSupplier apiTemplateDataSupplier,
final ExamConfigurationValueService examConfigurationValueService,
final CacheManager cacheManager) {
super(cacheManager);
@ -80,6 +89,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.clientCredentialService = clientCredentialService;
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
this.examConfigurationValueService = examConfigurationValueService;
this.lmsSetupId = apiTemplateDataSupplier.getLmsSetup().id;
}
@ -310,7 +320,15 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
private SEBRestriction getRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) {
final String url = String.format("/restapi/assessment_modes/%s/seb_restriction", id);
final RestrictionData r = this.apiGet(restTemplate, url, RestrictionData.class);
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<String, String>());
final HashMap<String, String> additionalAttributes = new HashMap<>();
if (StringUtils.isNotBlank(r.quitLink)) {
additionalAttributes.put(ADDITIONAL_ATTR_QUIT_LINK, r.quitLink);
}
if (StringUtils.isNotBlank(r.quitSecret)) {
additionalAttributes.put(ADDITIONAL_ATTR_QUIT_SECRET, r.quitSecret);
}
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, additionalAttributes);
}
private SEBRestriction setRestrictionForAssignmentId(
@ -322,6 +340,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
final RestrictionDataPost post = new RestrictionDataPost();
post.browserExamKeys = new ArrayList<>(restriction.browserExamKeys);
post.configKeys = new ArrayList<>(restriction.configKeys);
post.quitLink = restriction.getAdditionalProperties().getOrDefault(ADDITIONAL_ATTR_QUIT_LINK, null);
post.quitSecret = restriction.getAdditionalProperties().getOrDefault(ADDITIONAL_ATTR_QUIT_SECRET, null);
final RestrictionData r =
this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class);
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<String, String>());
@ -343,10 +363,12 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final Exam exam,
final SEBRestriction sebRestrictionData) {
populateWithQuitLinkAndSecret(exam, sebRestrictionData);
return getRestTemplate()
.map(t -> this.setRestrictionForAssignmentId(t, externalExamId, sebRestrictionData));
.map(t -> this.setRestrictionForAssignmentId(t, exam.externalId, sebRestrictionData));
}
@Override
@ -433,4 +455,28 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
});
}
private void populateWithQuitLinkAndSecret(final Exam exam, final SEBRestriction sebRestrictionData) {
try {
final String quitLink = this.examConfigurationValueService.getMappedDefaultConfigAttributeValue(
exam.id,
CONFIG_ATTR_NAME_QUIT_LINK);
final String quitSecret = this.examConfigurationValueService.getMappedDefaultConfigAttributeValue(
exam.id,
CONFIG_ATTR_NAME_QUIT_SECRET);
if (StringUtils.isNotEmpty(quitLink)) {
sebRestrictionData.additionalProperties.put(ADDITIONAL_ATTR_QUIT_LINK, quitLink);
}
if (StringUtils.isNotEmpty(quitSecret)) {
sebRestrictionData.additionalProperties.put(ADDITIONAL_ATTR_QUIT_SECRET, quitSecret);
}
} catch (final Exception e) {
log.error("Failed to populate SEB restriction with quit link and quit secret: ", e);
}
}
}

View file

@ -19,6 +19,7 @@ import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
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.exam.ExamConfigurationValueService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
@ -38,6 +39,7 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final ClientCredentialService clientCredentialService;
private final ExamConfigurationValueService examConfigurationValueService;
private final AsyncService asyncService;
private final Environment environment;
private final CacheManager cacheManager;
@ -45,12 +47,14 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
public OlatLmsAPITemplateFactory(
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ClientCredentialService clientCredentialService,
final ExamConfigurationValueService examConfigurationValueService,
final AsyncService asyncService,
final Environment environment,
final CacheManager cacheManager) {
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.clientCredentialService = clientCredentialService;
this.examConfigurationValueService = examConfigurationValueService;
this.asyncService = asyncService;
this.environment = environment;
this.cacheManager = cacheManager;
@ -68,6 +72,7 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
this.clientHttpRequestFactoryService,
this.clientCredentialService,
apiTemplateDataSupplier,
this.examConfigurationValueService,
this.cacheManager);
return new LmsAPITemplateAdapter(
this.asyncService,

View file

@ -61,12 +61,16 @@ public final class OlatLmsData {
* {
* "browserExamKeys": [ "1" ],
* "configKeys": null,
* "quitLink": "<the quit link from Exam Configuration>",
* "quitSecret": "<the quit password from Exam Configuration (base64 encoded)>",
* "key": 8028160
* }
*/
public long key;
public List<String> browserExamKeys;
public List<String> configKeys;
public String quitLink;
public String quitSecret;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@ -76,10 +80,14 @@ public final class OlatLmsData {
* {
* "configKeys": ["a", "b"],
* "browserExamKeys": ["1", "2"]
* "quitLink": "<the quit link from Exam Configuration>",
* "quitSecret": "<the quit password from Exam Configuration (base64 encoded)>",
* }
*/
public List<String> browserExamKeys;
public List<String> configKeys;
public String quitLink;
public String quitSecret;
}
}