SEBSERV-553 fixed show quit link and improved logging

This commit is contained in:
anhefti 2024-06-25 09:59:13 +02:00
parent 485273d05e
commit 8b30771021
10 changed files with 197 additions and 99 deletions

View file

@ -178,7 +178,7 @@ public final class API {
public static final String LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID = "exam_template_id"; public static final String LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID = "exam_template_id";
public static final String LMS_FULL_INTEGRATION_EXAM_DATA = "exam_data"; public static final String LMS_FULL_INTEGRATION_EXAM_DATA = "exam_data";
public static final String LMS_FULL_INTEGRATION_QUIT_PASSWORD = "quit_password"; public static final String LMS_FULL_INTEGRATION_QUIT_PASSWORD = "quit_password";
public static final String LMS_FULL_INTEGRATION_QUIT_LINK = "quit_link"; public static final String LMS_FULL_INTEGRATION_QUIT_LINK = "show_quit_link";
public static final String LMS_FULL_INTEGRATION_USER_ID = "user_id"; public static final String LMS_FULL_INTEGRATION_USER_ID = "user_id";
public static final String LMS_FULL_INTEGRATION_USER_NAME = "user_username"; public static final String LMS_FULL_INTEGRATION_USER_NAME = "user_username";
public static final String LMS_FULL_INTEGRATION_USER_EMAIL = "user_email"; public static final String LMS_FULL_INTEGRATION_USER_EMAIL = "user_email";

View file

@ -89,4 +89,6 @@ public interface ConfigurationValueDAO extends EntityDAO<ConfigurationValue, Con
* @param pwd The hashed quit password * @param pwd The hashed quit password
* @return Result refer to void or to an error when happened*/ * @return Result refer to void or to an error when happened*/
Result<Void> saveQuitPassword(Long configurationId, String pwd); Result<Void> saveQuitPassword(Long configurationId, String pwd);
Result<ConfigurationValue> saveForce(ConfigurationValue configurationValue);
} }

View file

@ -268,41 +268,17 @@ public class ConfigurationValueDAOImpl implements ConfigurationValueDAO {
public Result<ConfigurationValue> save(final ConfigurationValue data) { public Result<ConfigurationValue> save(final ConfigurationValue data) {
return checkInstitutionalIntegrity(data) return checkInstitutionalIntegrity(data)
.map(this::checkFollowUpIntegrity) .map(this::checkFollowUpIntegrity)
.flatMap(this::attributeRecord) .map(this::saveData)
.map(attributeRecord -> { .flatMap(ConfigurationValueDAOImpl::toDomainModel)
.onError(TransactionHandler::rollback);
}
final Long id;
if (data.id == null) {
id = getByProperties(data)
.orElseGet(() -> {
log.debug("Missing SEB exam configuration attrribute value for: {}", data);
log.debug("Use self-healing strategy to recover from missing SEB exam "
+ "configuration attrribute value\n**** Create new AttributeValue for: {}",
data);
createNew(data); @Override
return getByProperties(data) public Result<ConfigurationValue> saveForce(final ConfigurationValue data) {
.orElseThrow(() -> new ResourceNotFoundException( return checkInstitutionalIntegrity(data)
EntityType.CONFIGURATION_VALUE, .map(this::saveData)
String.valueOf(data.attributeId)));
});
} else {
id = data.id;
}
final ConfigurationValueRecord newRecord = new ConfigurationValueRecord(
id,
null,
null,
null,
data.listIndex,
data.value);
this.configurationValueRecordMapper.updateByPrimaryKeySelective(newRecord);
return this.configurationValueRecordMapper.selectByPrimaryKey(id);
})
.flatMap(ConfigurationValueDAOImpl::toDomainModel) .flatMap(ConfigurationValueDAOImpl::toDomainModel)
.onError(TransactionHandler::rollback); .onError(TransactionHandler::rollback);
} }
@ -708,4 +684,38 @@ public class ConfigurationValueDAOImpl implements ConfigurationValueDAO {
.findFirst(); .findFirst();
} }
private ConfigurationValueRecord saveData(final ConfigurationValue data) {
final Long id;
if (data.id == null) {
id = getByProperties(data)
.orElseGet(() -> {
log.debug("Missing SEB exam configuration attrribute value for: {}", data);
log.debug("Use self-healing strategy to recover from missing SEB exam "
+ "configuration attrribute value\n**** Create new AttributeValue for: {}",
data);
createNew(data);
return getByProperties(data)
.orElseThrow(() -> new ResourceNotFoundException(
EntityType.CONFIGURATION_VALUE,
String.valueOf(data.attributeId)));
});
} else {
id = data.id;
}
final ConfigurationValueRecord newRecord = new ConfigurationValueRecord(
id,
null,
null,
null,
data.listIndex,
data.value);
this.configurationValueRecordMapper.updateByPrimaryKeySelective(newRecord);
return this.configurationValueRecordMapper.selectByPrimaryKey(id);
}
} }

View file

@ -95,25 +95,25 @@ public interface ExamAdminService {
/** Updates needed additional attributes from assigned exam configuration for the exam /** Updates needed additional attributes from assigned exam configuration for the exam
* *
* @param examId The exam identifier */ * @param examId The exam identifier */
void updateAdditionalExamConfigAttributes(final Long examId); void updateAdditionalExamConfigAttributes(Long examId);
/** This indicates if proctoring is set and enabled for a certain exam. /** This indicates if proctoring is set and enabled for a certain exam.
* *
* @param examId the exam identifier * @param examId the exam identifier
* @return Result refer to proctoring is enabled flag or to an error when happened. */ * @return Result refer to proctoring is enabled flag or to an error when happened. */
Result<Boolean> isProctoringEnabled(final Long examId); Result<Boolean> isProctoringEnabled(Long examId);
/** This indicates if screen proctoring is set and enabled for a certain exam. /** This indicates if screen proctoring is set and enabled for a certain exam.
* *
* @param examId the exam identifier * @param examId the exam identifier
* @return Result refer to screen proctoring is enabled flag or to an error when happened. */ * @return Result refer to screen proctoring is enabled flag or to an error when happened. */
Result<Boolean> isScreenProctoringEnabled(final Long examId); Result<Boolean> isScreenProctoringEnabled(Long examId);
/** Get the exam proctoring service implementation for specified exam. /** Get the exam proctoring service implementation for specified exam.
* *
* @param examId the exam identifier * @param examId the exam identifier
* @return ExamProctoringService instance */ * @return ExamProctoringService instance */
Result<RemoteProctoringService> getExamProctoringService(final Long examId); Result<RemoteProctoringService> getExamProctoringService(Long examId);
/** This resets the proctoring settings for a given exam and stores the default settings. /** This resets the proctoring settings for a given exam and stores the default settings.
* *

View file

@ -54,6 +54,14 @@ public interface ExamConfigurationValueService {
*/ */
Result<Long> applyQuitPasswordToConfigs(Long examId, String quitPassword); Result<Long> applyQuitPasswordToConfigs(Long examId, String quitPassword);
/** Used to apply the quit pass given from the exam to all exam configuration for the exam.
*
* @param examId The exam identifier
* @param quitLink The quit link to set to all exam configuration of the given exam
* @return Result to the given exam id or to an error when happened
*/
Result<Long> applyQuitURLToConfigs(Long examId, String quitLink);
/** Get the quitLink SEB Setting from the Exam Configuration that is applied to the given exam. /** Get the quitLink SEB Setting from the Exam Configuration that is applied to the given exam.
* *
* @param examId Exam identifier * @param examId Exam identifier

View file

@ -152,52 +152,24 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
return examId; return examId;
} }
final Long configNodeId = this.examConfigurationMapDAO return saveSEBAttributeValueToConfig(examId, CONFIG_ATTR_NAME_QUIT_SECRET, quitSecret);
.getDefaultConfigurationNode(examId)
.getOr(null);
if (configNodeId == null) {
log.info("No Exam Configuration found for exam {} to apply quitPassword", examId);
return examId;
}
final Long attrId = getAttributeId(CONFIG_ATTR_NAME_QUIT_SECRET);
if (attrId == null) {
return examId;
}
final Configuration followupConfig = this.configurationDAO.getFollowupConfiguration(configNodeId)
.onError(error -> log.warn("Failed to get followup config for {} cause {}",
configNodeId,
error.getMessage()))
.getOr(null);
final ConfigurationValue configurationValue = new ConfigurationValue(
null,
followupConfig.institutionId,
followupConfig.id,
attrId,
0,
quitSecret
);
this.configurationValueDAO
.save(configurationValue)
.onError(err -> log.error(
"Failed to save quit password to config value: {}",
configurationValue,
err));
// TODO possible without save to history?
this.configurationDAO
.saveToHistory(configNodeId)
.onError(error -> log.warn("Failed to save to history for exam: {} cause: {}",
examId, error.getMessage()));
return examId;
}); });
} }
@Override
public Result<Long> applyQuitURLToConfigs(final Long examId, final String quitLink) {
return Result.tryCatch(() -> {
final String oldQuitLink = this.getQuitLink(examId);
if (Objects.equals(oldQuitLink, quitLink)) {
return examId;
}
return saveSEBAttributeValueToConfig(examId, CONFIG_ATTR_NAME_QUIT_LINK, quitLink);
});
}
@Override @Override
public String getQuitLink(final Long examId) { public String getQuitLink(final Long examId) {
try { try {
@ -236,4 +208,67 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
.getOr(null); .getOr(null);
} }
private Long saveSEBAttributeValueToConfig(
final Long examId,
final String attrName,
final String attrValue) {
final Long configNodeId = this.examConfigurationMapDAO
.getDefaultConfigurationNode(examId)
.getOr(null);
if (configNodeId == null) {
log.info("No Exam Configuration found for exam {} to apply SEB Setting: {}", examId, attrName);
return examId;
}
final Long attrId = getAttributeId(attrName);
if (attrId == null) {
return examId;
}
final Configuration lastStable = this.configurationDAO
.getConfigurationLastStableVersion(configNodeId)
.getOrThrow();
final Long followupId = configurationDAO
.getFollowupConfigurationId(configNodeId)
.getOrThrow();
// save to last sable version
this.configurationValueDAO
.saveForce(new ConfigurationValue(
null,
lastStable.institutionId,
lastStable.id,
attrId,
0,
attrValue
))
.onError(err -> log.error(
"Failed to save SEB Setting: {} to config: {}",
attrName,
lastStable,
err));
if (!Objects.equals(followupId, lastStable.id)) {
// save also to followup version
this.configurationValueDAO
.saveForce(new ConfigurationValue(
null,
lastStable.institutionId,
followupId,
attrId,
0,
attrValue
))
.onError(err -> log.error(
"Failed to save SEB Setting: {} to config: {}",
attrName,
lastStable,
err));
}
return examId;
}
} }

View file

@ -56,7 +56,7 @@ public interface FullLmsIntegrationService {
String quizId, String quizId,
String examTemplateId, String examTemplateId,
String quitPassword, String quitPassword,
String quitLink, boolean showQuitLink,
final String examData); final String examData);
Result<EntityKey> deleteExam( Result<EntityKey> deleteExam(
@ -117,7 +117,7 @@ public interface FullLmsIntegrationService {
@JsonProperty("template_id") @JsonProperty("template_id")
public final String template_id; public final String template_id;
@JsonProperty("show_quit_link") @JsonProperty("show_quit_link")
public final Boolean show_quit_link; public final String quit_link;
@JsonProperty("quit_password") @JsonProperty("quit_password")
public final String quit_password; public final String quit_password;
@ -127,7 +127,7 @@ public interface FullLmsIntegrationService {
final String quiz_id, final String quiz_id,
final Boolean exam_created, final Boolean exam_created,
final String template_id, final String template_id,
final Boolean show_quit_link, final String quit_link,
final String quit_password) { final String quit_password) {
this.id = id; this.id = id;
@ -135,7 +135,7 @@ public interface FullLmsIntegrationService {
this.quiz_id = quiz_id; this.quiz_id = quiz_id;
this.exam_created = exam_created; this.exam_created = exam_created;
this.template_id = template_id; this.template_id = template_id;
this.show_quit_link = show_quit_link; this.quit_link = quit_link;
this.quit_password = quit_password; this.quit_password = quit_password;
} }
} }

View file

@ -323,19 +323,28 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final String quizId, final String quizId,
final String examTemplateId, final String examTemplateId,
final String quitPassword, final String quitPassword,
final String quitLink, final boolean showQuitLink,
final String examData) { final String examData) {
return lmsSetupDAO return lmsSetupDAO
.getLmsSetupIdByConnectionId(lmsUUID) .getLmsSetupIdByConnectionId(lmsUUID)
.flatMap(lmsAPITemplateCacheService::getLmsAPITemplate) .flatMap(lmsAPITemplateCacheService::getLmsAPITemplate)
.map(template -> getQuizData(template, courseId, quizId, examData)) .map(template -> getQuizData(template, courseId, quizId, examData))
.map(createExam(examTemplateId, quitPassword)) .map(createExam(examTemplateId, showQuitLink, quitPassword))
.map(exam -> applyExamData(exam, false)) .map(exam -> applyExamData(exam, false))
.flatMap(sebRestrictionService::applySEBClientRestriction) .map(this::applySEBClientRestrictionIfRunning)
.map(this::applyConnectionConfiguration); .map(this::applyConnectionConfiguration);
} }
private Exam applySEBClientRestrictionIfRunning(final Exam exam) {
if (exam.status == Exam.ExamStatus.RUNNING) {
return sebRestrictionService
.applySEBClientRestriction(exam)
.getOrThrow();
}
return exam;
}
@Override @Override
public Result<EntityKey> deleteExam( public Result<EntityKey> deleteExam(
final String lmsUUID, final String lmsUUID,
@ -368,6 +377,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
.flatMap(this::findExam); .flatMap(this::findExam);
if (examResult.hasError()) { if (examResult.hasError()) {
log.error("Failed to find exam for SEB Connection Configuration download: ", examResult.getError());
throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("Exam not found")); throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("Exam not found"));
} }
@ -375,10 +385,15 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final String connectionConfigId = getConnectionConfigurationId(exam); final String connectionConfigId = getConnectionConfigurationId(exam);
if (StringUtils.isBlank(connectionConfigId)) { if (StringUtils.isBlank(connectionConfigId)) {
log.error("Failed to verify SEB Connection Configuration id for exam: {}", exam.name);
throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("No active Connection Configuration found")); throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("No active Connection Configuration found"));
} }
this.connectionConfigurationService.exportSEBClientConfiguration(out, connectionConfigId, exam.id); this.connectionConfigurationService.exportSEBClientConfiguration(
out,
connectionConfigId,
exam.id);
return Result.EMPTY; return Result.EMPTY;
} catch (final Exception e) { } catch (final Exception e) {
@ -469,6 +484,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
private Function<QuizData, Exam> createExam( private Function<QuizData, Exam> createExam(
final String examTemplateId, final String examTemplateId,
final boolean showQuitLink,
final String quitPassword) { final String quitPassword) {
return quizData -> { return quizData -> {
@ -503,6 +519,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
return examDAO return examDAO
.createNew(exam) .createNew(exam)
.flatMap(examImportService::applyExamImportInitialization) .flatMap(examImportService::applyExamImportInitialization)
.map( e -> this.applyQuitLinkToSEBConfig(e, showQuitLink))
.map(this::logExamCreated) .map(this::logExamCreated)
.getOrThrow(); .getOrThrow();
}; };
@ -572,7 +589,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final String templateId = deletion ? null : String.valueOf(exam.examTemplateId); final String templateId = deletion ? null : String.valueOf(exam.examTemplateId);
final String quitPassword = deletion ? null : examConfigurationValueService.getQuitPassword(exam.id); final String quitPassword = deletion ? null : examConfigurationValueService.getQuitPassword(exam.id);
final Boolean quitLink = deletion ? null : StringUtils.isNotBlank(examConfigurationValueService.getQuitLink(exam.id)); final String quitLink = deletion ? null : examConfigurationValueService.getQuitLink(exam.id);
final ExamData examData = new ExamData( final ExamData examData = new ExamData(
lmsUUID, lmsUUID,
@ -591,6 +608,35 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
return exam; return exam;
} }
private Exam applyQuitLinkToSEBConfig(final Exam exam, final boolean showQuitLink) {
try {
if (!showQuitLink) {
// check set no quit link to SEB config
examConfigurationValueService
.applyQuitURLToConfigs(exam.id, "")
.getOrThrow();
} else {
// check if in config quit link is set, if so nothing to do, if not generate one and apply
String quitLink = examConfigurationValueService.getQuitLink(exam.id);
if (StringUtils.isNotBlank(quitLink)) {
return exam;
}
quitLink = "http://quit_seb";
examConfigurationValueService
.applyQuitURLToConfigs(exam.id, quitLink)
.getOrThrow();
}
return exam;
} catch (final Exception e) {
log.error("Failed to apply quit link to SEB Exam Configuration: ", e);
return exam;
}
}
private Exam applyConnectionConfiguration(final Exam exam) { private Exam applyConnectionConfiguration(final Exam exam) {
return lmsAPITemplateCacheService return lmsAPITemplateCacheService
.getLmsAPITemplate(exam.lmsSetupId) .getLmsAPITemplate(exam.lmsSetupId)

View file

@ -209,12 +209,14 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
// data[addordelete]= int // data[addordelete]= int
// data[templateid]= int // data[templateid]= int
// data[showquitlink]= int // data[showquitlink]= int
// data[quitlink]=string
// data[quitsecret]= string // data[quitsecret]= string
data_mapping.put("quizid", examData.quiz_id); data_mapping.put("quizid", examData.quiz_id);
if (BooleanUtils.isTrue(examData.exam_created)) { if (BooleanUtils.isTrue(examData.exam_created)) {
data_mapping.put("addordelete", "1"); data_mapping.put("addordelete", "1");
data_mapping.put("templateid", examData.template_id); data_mapping.put("templateid", examData.template_id);
data_mapping.put("showquitlink", BooleanUtils.isTrue(examData.show_quit_link) ? "1" : "0"); data_mapping.put("showquitlink", StringUtils.isNotBlank(examData.quit_link) ? "1" : "0");
data_mapping.put("quitlink", examData.quit_link);
data_mapping.put("quitsecret", examData.quit_password); data_mapping.put("quitsecret", examData.quit_password);
} else { } else {
data_mapping.put("addordelete", "0"); data_mapping.put("addordelete", "0");

View file

@ -14,17 +14,15 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.io.PipedInputStream; import java.io.PipedInputStream;
import java.io.PipedOutputStream; import java.io.PipedOutputStream;
import java.util.Arrays;
import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -43,16 +41,13 @@ public class LmsIntegrationController {
private final FullLmsIntegrationService fullLmsIntegrationService; private final FullLmsIntegrationService fullLmsIntegrationService;
private final WebserviceInfo webserviceInfo; private final WebserviceInfo webserviceInfo;
private final ExamDAO examDAO;
public LmsIntegrationController( public LmsIntegrationController(
final FullLmsIntegrationService fullLmsIntegrationService, final FullLmsIntegrationService fullLmsIntegrationService,
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo) {
final ExamDAO examDAO) {
this.fullLmsIntegrationService = fullLmsIntegrationService; this.fullLmsIntegrationService = fullLmsIntegrationService;
this.webserviceInfo = webserviceInfo; this.webserviceInfo = webserviceInfo;
this.examDAO = examDAO;
} }
@RequestMapping( @RequestMapping(
@ -66,7 +61,7 @@ public class LmsIntegrationController {
@RequestParam(name = API.LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID) final String templateId, @RequestParam(name = API.LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID) final String templateId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_EXAM_DATA, required = false) final String examData, @RequestParam(name = API.LMS_FULL_INTEGRATION_EXAM_DATA, required = false) final String examData,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_PASSWORD, required = false) final String quitPassword, @RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_PASSWORD, required = false) final String quitPassword,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_LINK, required = false) final String quitLink, @RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_LINK, required = false) final int quitLink,
final HttpServletResponse response) { final HttpServletResponse response) {
final Exam exam = fullLmsIntegrationService.importExam( final Exam exam = fullLmsIntegrationService.importExam(
@ -75,7 +70,7 @@ public class LmsIntegrationController {
quizId, quizId,
templateId, templateId,
quitPassword, quitPassword,
quitLink, BooleanUtils.toBoolean(quitLink),
examData) examData)
.onError(e -> { .onError(e -> {
log.error( log.error(