SEBSERV-457 finished implementation

This commit is contained in:
anhefti 2023-12-14 16:24:08 +01:00
parent fc310597e6
commit cb9900a16d
13 changed files with 109 additions and 36 deletions

View file

@ -228,8 +228,7 @@ public class ExamForm implements TemplateComposer {
final EntityGrantCheck entityGrantCheck = currentUser.entityGrantCheck(exam);
final boolean modifyGrant = entityGrantCheck.m();
final boolean writeGrant = entityGrantCheck.w();
final boolean editable = modifyGrant &&
(exam.getStatus() == ExamStatus.UP_COMING || exam.getStatus() == ExamStatus.RUNNING);
final boolean editable = modifyGrant && (exam.getStatus() == ExamStatus.UP_COMING || exam.getStatus() == ExamStatus.RUNNING);
final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean(
exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED));
final boolean sebRestrictionAvailable = readonly && testSEBRestrictionAPI(exam);
@ -294,7 +293,8 @@ public class ExamForm implements TemplateComposer {
final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(formContext
.clearEntityKeys()
.removeAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA));
.removeAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA)
.removeAttribute(AttributeKeys.NEW_EXAM_NO_LMS));
// propagate content actions to action-pane
@ -302,7 +302,8 @@ public class ExamForm implements TemplateComposer {
.newAction(ActionDefinition.EXAM_MODIFY)
.withEntityKey(entityKey)
.publishIf(() -> modifyGrant && readonly && editable)
.publishIf(() -> modifyGrant && readonly &&
(editable || (exam.getStatus() == ExamStatus.FINISHED && exam.lmsSetupId == null)))
.newAction(ActionDefinition.EXAM_DELETE)
.withEntityKey(entityKey)
@ -316,7 +317,7 @@ public class ExamForm implements TemplateComposer {
.publishIf(() -> writeGrant && readonly && exam.status == ExamStatus.FINISHED)
.newAction(ActionDefinition.EXAM_SAVE)
.withExec(action -> (importFromQuizData)
.withExec(action -> importFromQuizData
? importExam(action, formHandle, sebRestrictionAvailable && exam.status == ExamStatus.RUNNING)
: formHandle.processFormSave(action))
.ignoreMoveAwayFromEdit()
@ -493,7 +494,7 @@ public class ExamForm implements TemplateComposer {
QuizData.QUIZ_ATTR_DESCRIPTION,
FORM_DESCRIPTION_TEXT_KEY,
exam.getDescription())
.asHTML(50)
.asHTMLOrArea(50, exam.lmsSetupId != null)
.readonly(true)
.withInputSpan(7)
.withEmptyCellSeparation(false))

View file

@ -94,6 +94,14 @@ public final class TextFieldBuilder extends FieldBuilder<String> {
return this;
}
public TextFieldBuilder asHTMLOrArea(final int minHeight, final boolean html) {
if (html) {
return this.asHTML(minHeight);
} else {
return this.asArea(minHeight);
}
}
public TextFieldBuilder asMarkupLabel() {
this.isMarkupLabel = true;
return this;
@ -187,4 +195,5 @@ public final class TextFieldBuilder extends FieldBuilder<String> {
+ HTML_TEXT_BLOCK_END;
}
}

View file

@ -222,7 +222,7 @@ public final class PageAction {
} catch (final FormPostException e) {
if (e.getCause() instanceof RestCallError) {
final RestCallError cause = (RestCallError) e.getCause();
if (cause.isUnexpectedError()) {
if (cause.isUnexpectedError() || log.isDebugEnabled()) {
log.error("Failed to execute action: {} | error: {} | cause: {}",
PageAction.this.getName(),
cause.getMessage(),

View file

@ -535,8 +535,9 @@ public class WidgetFactory {
final LocTextKey ariaLabel) {
final Text input = readonly
? new Text(content, SWT.LEFT | SWT.MULTI)
: new Text(content, SWT.LEFT | SWT.BORDER | SWT.MULTI);
? new Text(content, SWT.LEFT | SWT.MULTI | SWT.WRAP)
: new Text(content, SWT.LEFT | SWT.BORDER | SWT.MULTI | SWT.WRAP);
if (ariaLabel != null) {
WidgetFactory.setARIALabel(input, this.i18nSupport.getText(ariaLabel));
}

View file

@ -28,6 +28,7 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.mybatis.dynamic.sql.SqlBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -147,7 +148,11 @@ public class ConfigurationValueDAOImpl implements ConfigurationValueDAO {
attrId);
}
return records.get(0).getValue();
final String value = records.get(0).getValue();
if (value == null) {
return StringUtils.EMPTY;
}
return value;
});
}

View file

@ -270,7 +270,7 @@ public class ExamRecordDAO {
}
if (exam.status != null && !exam.status.name().equals(oldRecord.getStatus())) {
log.warn("Exam state change on save. Exam. {}, Old state: {}, new state: {}",
log.info("Exam state change on save. Exam. {}, Old state: {}, new state: {}",
exam.externalId,
oldRecord.getStatus(),
exam.status);

View file

@ -67,7 +67,7 @@ public interface ExamAdminService {
* @return Result refer to the created exam or to an error when happened */
Result<Exam> applyAdditionalSEBRestrictions(Exam exam);
/** Indicates whether a specific exam is been restricted with SEB restriction feature on the LMS or not.
/** Indicates whether a specific exam is being restricted with SEB restriction feature on the LMS or not.
*
* @param exam The exam instance
* @return Result refer to the restriction flag or to an error when happened */
@ -164,14 +164,23 @@ public interface ExamAdminService {
void notifyExamSaved(Exam exam);
static void newExamFieldValidation(final POSTMapper postParams) {
final Collection<APIMessage> validationErrors = new ArrayList<>();
noLMSFieldValidation(new Exam(postParams));
}
if (!postParams.contains(Domain.EXAM.ATTR_QUIZ_NAME)) {
static Exam noLMSFieldValidation(final Exam exam) {
// This only applies to exams that has no LMS
if (exam.lmsSetupId != null) {
return exam;
}
final Collection<APIMessage> validationErrors = new ArrayList<>();
if (StringUtils.isBlank(exam.name)) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_NAME,
"exam:quizName:notNull"));
} else {
final int length = postParams.getString(Domain.EXAM.ATTR_QUIZ_NAME).length();
final int length = exam.name.length();
if (length < 3 || length > 255) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_NAME,
@ -179,13 +188,13 @@ public interface ExamAdminService {
}
}
if (!postParams.contains(QuizData.QUIZ_ATTR_START_URL)) {
if (StringUtils.isBlank(exam.getStartURL())) {
validationErrors.add(APIMessage.fieldValidationError(
QuizData.QUIZ_ATTR_START_URL,
"exam:quiz_start_url:notNull"));
} else {
try {
new URL(postParams.getString(QuizData.QUIZ_ATTR_START_URL)).toURI();
new URL(exam.getStartURL()).toURI();
} catch (final Exception e) {
validationErrors.add(APIMessage.fieldValidationError(
QuizData.QUIZ_ATTR_START_URL,
@ -193,13 +202,13 @@ public interface ExamAdminService {
}
}
if (!postParams.contains(Domain.EXAM.ATTR_QUIZ_START_TIME)) {
if (exam.startTime == null) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_START_TIME,
"exam:quizStartTime:notNull"));
} else if (postParams.contains(Domain.EXAM.ATTR_QUIZ_END_TIME)) {
if (postParams.getDateTime(Domain.EXAM.ATTR_QUIZ_START_TIME)
.isAfter(postParams.getDateTime(Domain.EXAM.ATTR_QUIZ_END_TIME))) {
} else if (exam.endTime != null) {
if (exam.startTime
.isAfter(exam.endTime)) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_END_TIME,
"exam:quizEndTime:endBeforeStart"));
@ -209,6 +218,8 @@ public interface ExamAdminService {
if (!validationErrors.isEmpty()) {
throw new APIMessageException(validationErrors);
}
return exam;
}
/** Used to check threshold consistency for a given list of thresholds.
@ -326,4 +337,5 @@ public interface ExamAdminService {
}
}
}

View file

@ -166,6 +166,11 @@ public class ExamAdminServiceImpl implements ExamAdminService {
public Result<Exam> applyAdditionalSEBRestrictions(final Exam exam) {
return Result.tryCatch(() -> {
// this only applies to exams that are attached to an LMS
if (exam.lmsSetupId == null) {
return exam;
}
if (log.isDebugEnabled()) {
log.debug("Apply additional SEB restrictions for exam: {}",
exam.externalId);
@ -325,6 +330,11 @@ public class ExamAdminServiceImpl implements ExamAdminService {
private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) {
return Result.tryCatch(() -> {
if (exam.lmsSetupId == null) {
return exam;
}
final LmsAPITemplate lmsTemplate = this.lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId)
.getOrThrow();

View file

@ -84,9 +84,7 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
// check only if SEB_RESTRICTION feature is on
if (lmsSetup != null && lmsSetup.lmsType.features.contains(Features.SEB_RESTRICTION)) {
if (!exam.sebRestriction) {
return false;
}
return exam.sebRestriction;
}
return true;
@ -97,12 +95,11 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
public Result<SEBRestriction> getSEBRestrictionFromExam(final Exam exam) {
return Result.tryCatch(() -> {
// load the config keys from restriction and merge with new generated config keys
final Set<String> configKeys = new HashSet<>();
final Collection<String> generatedKeys = this.examConfigService
.generateConfigKeys(exam.institutionId, exam.id)
.getOrThrow();
configKeys.addAll(generatedKeys);
final Set<String> configKeys = new HashSet<>(generatedKeys);
if (!generatedKeys.isEmpty()) {
configKeys.addAll(this.lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId)
@ -210,6 +207,11 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
@EventListener(ExamStartedEvent.class)
public void notifyExamStarted(final ExamStartedEvent event) {
// This affects only exams with LMS binding...
if (event.exam.lmsSetupId == null) {
return;
}
if (log.isDebugEnabled()) {
log.debug("ExamStartedEvent received, process applySEBClientRestriction...");
}
@ -225,6 +227,11 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
@EventListener(ExamFinishedEvent.class)
public void notifyExamFinished(final ExamFinishedEvent event) {
// This affects only exams with LMS binding...
if (event.exam.lmsSetupId == null) {
return;
}
if (log.isDebugEnabled()) {
log.debug("ExamFinishedEvent received, process releaseSEBClientRestriction...");
}
@ -260,6 +267,11 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
@Override
public Result<Exam> applySEBClientRestriction(final Exam exam) {
return Result.tryCatch(() -> {
if (exam.lmsSetupId == null) {
log.info("No LMS for Exam: {}", exam.name);
return exam;
}
if (!this.lmsAPIService
.getLmsSetup(exam.lmsSetupId)
.getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) {

View file

@ -27,7 +27,7 @@ public class MockSEBRestrictionAPI implements SEBRestrictionAPI {
private static final Logger log = LoggerFactory.getLogger(MockSEBRestrictionAPI.class);
//private Map<Long, Boolean> restrictionDB = new ConcurrentHashMap<>();
private Map<Long, SEBRestriction> restrictionDB = new ConcurrentHashMap<>();
@Override
public LmsSetupTestResult testCourseRestrictionAPI() {
@ -37,7 +37,11 @@ public class MockSEBRestrictionAPI implements SEBRestrictionAPI {
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
log.info("Get SEB Client restriction for Exam: {}", exam);
return Result.ofError(new NoSEBRestrictionException());
if (!restrictionDB.containsKey(exam.id)) {
return Result.ofError(new NoSEBRestrictionException());
} else {
return Result.of(restrictionDB.get(exam.id));
}
}
@Override
@ -46,13 +50,24 @@ public class MockSEBRestrictionAPI implements SEBRestrictionAPI {
final SEBRestriction sebRestrictionData) {
log.info("Apply SEB Client restriction: {}", sebRestrictionData);
//return Result.ofError(new NoSEBRestrictionException());
restrictionDB.put(exam.id, sebRestrictionData);
return Result.of(sebRestrictionData);
}
@Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
log.info("Release SEB Client restriction for Exam: {}", exam);
if (restrictionDB.containsKey(exam.id)) {
SEBRestriction sebRestriction = restrictionDB.get(exam.id);
restrictionDB.put(
exam.id,
new SEBRestriction(
exam.id,
null,
null,
sebRestriction.additionalProperties,
sebRestriction.warningMessage));
}
return Result.of(exam);
}

View file

@ -106,7 +106,7 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
log.debug("Update-Lock successfully placed for all involved exams: {}", examsIds);
}
// check running exam integrity again after lock to ensure there where no SEB Client connection attempts in the meantime
// check running exam integrity again after lock to ensure there were no SEB Client connection attempts in the meantime
final Collection<Long> examIdsSecondCheck = checkRunningExamIntegrity(configurationNodeId)
.getOrThrow();
@ -129,7 +129,7 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
// generate the new Config Key and update the Config Key within the LMSSetup API for each exam (delete old Key and add new Key)
for (final Exam exam : exams) {
if (exam.getStatus() == ExamStatus.RUNNING) {
if (exam.getStatus() == ExamStatus.RUNNING && exam.lmsSetupId != null) {
this.examUpdateHandler
.getSEBRestrictionService()

View file

@ -172,7 +172,9 @@ public class RemoteProctoringRoomServiceImpl implements RemoteProctoringRoomServ
@EventListener
public void notifyExamFinished(final ExamFinishedEvent event) {
log.info("ExamFinishedEvent received, process disposeRoomsForExam...");
if (log.isDebugEnabled()) {
log.debug("ExamFinishedEvent received, process disposeRoomsForExam...");
}
disposeRoomsForExam(event.exam)
.onError(error -> log.error("Failed to dispose rooms for finished exam: {}", event.exam, error));
@ -183,12 +185,14 @@ public class RemoteProctoringRoomServiceImpl implements RemoteProctoringRoomServ
return Result.tryCatch(() -> {
log.info("Dispose and deleting proctoring rooms for exam: {}", exam.externalId);
final ProctoringServiceSettings proctoringSettings = this.examAdminService
.getProctoringServiceSettings(exam.id)
.getOrThrow();
if (proctoringSettings.enableProctoring) {
log.info("Dispose and deleting proctoring rooms for exam: {}", exam.externalId);
}
this.proctoringAdminService
.getExamProctoringService(proctoringSettings.serverType)
.flatMap(service -> service.disposeServiceRoomsForExam(exam.id, proctoringSettings))

View file

@ -539,7 +539,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
+ API.EXAM_ADMINISTRATION_SCREEN_PROCTORING_PATH_SEGMENT,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public ScreenProctoringSettings getScreenProctoringeSettings(
public ScreenProctoringSettings getScreenProctoringSettings(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
@ -655,6 +655,9 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
errors.add(0, ErrorMessage.EXAM_IMPORT_ERROR_AUTO_SETUP.of(
entity.getModelId(),
API.PARAM_MODEL_ID + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + entity.getModelId()));
log.warn("Exam successfully created but some initialization did go wrong: {}", errors);
throw new APIMessageException(errors);
} else {
return this.examDAO.byPK(entity.id);
@ -679,7 +682,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
@Override
protected Result<Exam> validForSave(final Exam entity) {
return super.validForSave(entity)
.map(this::checkExamSupporterRole);
.map(this::checkExamSupporterRole)
.map(ExamAdminService::noLMSFieldValidation);
}
@Override