SEBSERV-192 also improved some error handling for exam import

This commit is contained in:
anhefti 2022-01-24 17:10:58 +01:00
parent fad3810ba8
commit e19e7aeb2a
10 changed files with 107 additions and 53 deletions

View file

@ -49,8 +49,6 @@ public class APIMessage implements Serializable {
PASSWORD_MISMATCH("1300", HttpStatus.BAD_REQUEST, "new password do not match confirmed password"),
MISSING_PASSWORD("1301", HttpStatus.BAD_REQUEST, "Missing Password"),
BINDING_ERROR("1500", HttpStatus.BAD_REQUEST, "External binding error"),
EXAM_CONSISTENCY_VALIDATION_SUPPORTER("1400", HttpStatus.OK, "No Exam Supporter defined for the Exam"),
EXAM_CONSISTENCY_VALIDATION_CONFIG("1401", HttpStatus.OK, "No SEB Exam Configuration defined for the Exam"),
EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION("1402", HttpStatus.OK,
@ -60,10 +58,20 @@ public class APIMessage implements Serializable {
EXAM_CONSISTENCY_VALIDATION_INVALID_ID_REFERENCE("1405", HttpStatus.OK,
"There seems to be an invalid exam - course identifier reference. The course cannot be found"),
EXAM_IMPORT_ERROR_AUTO_CONFIG("1500", HttpStatus.BAD_REQUEST,
"Failed to automatically create and link exam configuration from exam template"),
EXAM_IMPORT_ERROR_AUTO_CONFIG_LINKING("1500", HttpStatus.BAD_REQUEST,
"Failed to automatically link auto-generated exam configuration");
EXTERNAL_SERVICE_BINDING_ERROR("1500", HttpStatus.BAD_REQUEST, "External binding error"),
EXAM_IMPORT_ERROR_AUTO_SETUP("1600", HttpStatus.PARTIAL_CONTENT,
"Exam successfully imported but some additional initialization failed"),
EXAM_IMPORT_ERROR_AUTO_INDICATOR("1601", HttpStatus.PARTIAL_CONTENT,
"Failed to automatically create pre-defined indicators for the exam"),
EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES("1602", HttpStatus.PARTIAL_CONTENT,
"Failed to automatically create pre-defined attributes for the exam"),
EXAM_IMPORT_ERROR_AUTO_RESTRICTION("1603", HttpStatus.PARTIAL_CONTENT,
"Failed to automatically apply SEB restriction for the exam to the involved LMS"),
EXAM_IMPORT_ERROR_AUTO_CONFIG("1610", HttpStatus.PARTIAL_CONTENT,
"Failed to automatically create and link exam configuration from the exam template to the exam"),
EXAM_IMPORT_ERROR_AUTO_CONFIG_LINKING("1611", HttpStatus.PARTIAL_CONTENT,
"Failed to automatically link auto-generated exam configuration to the exam");
public final String messageCode;
public final HttpStatus httpStatus;

View file

@ -29,6 +29,7 @@ import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
@ -521,15 +522,20 @@ public class ExamForm implements TemplateComposer {
return processFormSave;
} catch (final Exception e) {
// try to geht the created exam id
Throwable error = e;
if (e instanceof FormPostException) {
error = ((FormPostException) e).getCause();
}
if (error instanceof RestCallError) {
final List<APIMessage> apiMessages = ((RestCallError) error).getAPIMessages();
if (apiMessages != null && !apiMessages.isEmpty()) {
final APIMessage apiMessage = apiMessages.get(0);
return handleExamImportSetupFailure(action, e);
}
}
private PageAction handleExamImportSetupFailure(final PageAction action, final Exception e) {
Throwable error = e;
if (e instanceof FormPostException) {
error = ((FormPostException) e).getCause();
}
if (error instanceof RestCallError) {
final List<APIMessage> apiMessages = ((RestCallError) error).getAPIMessages();
if (apiMessages != null && !apiMessages.isEmpty()) {
final APIMessage apiMessage = apiMessages.remove(0);
if (ErrorMessage.EXAM_IMPORT_ERROR_AUTO_SETUP.isOf(apiMessage)) {
final String examIdAttr = apiMessage.attributes
.stream()
.filter(attr -> attr.startsWith(API.PARAM_MODEL_ID))
@ -539,16 +545,21 @@ public class ExamForm implements TemplateComposer {
examIdAttr,
Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR);
if (API.PARAM_MODEL_ID.equals(split[0])) {
final String additionlMessages = apiMessages.stream()
.reduce(
"",
(acc, msg) -> acc + "<br/>&nbsp;&nbsp;&nbsp;" + msg.systemMessage,
(acc1, acc2) -> acc1 + acc2);
action.pageContext().publishPageMessage(
AUTO_GEN_CONFIG_ERROR_TITLE,
AUTO_GEN_CONFIG_ERROR_TEXT);
new LocTextKey(AUTO_GEN_CONFIG_ERROR_TEXT.name, additionlMessages));
return action.withEntityKey(new EntityKey(split[1], EntityType.EXAM));
}
}
}
}
throw e;
}
throw new RuntimeException("Error while handle exam import setup failure:", e);
}
private boolean testSEBRestrictionAPI(final Exam exam) {

View file

@ -136,6 +136,8 @@ public abstract class RestCall<T> {
responseEntity.getBody(),
RestCall.this.typeKey.typeRef));
} else if (responseEntity.getStatusCode() == HttpStatus.PARTIAL_CONTENT) {
return handleRestCallPartialResponse(responseEntity);
} else {
return handleRestCallError(responseEntity);
}
@ -182,6 +184,11 @@ public abstract class RestCall<T> {
}
}
private Result<T> handleRestCallPartialResponse(final ResponseEntity<String> responseEntity) {
// TODO Auto-generated method stub
return null;
}
public RestCallBuilder newBuilder() {
return new RestCallBuilder(
this.restService.getWebserviceAPIRestTemplate(),

View file

@ -27,7 +27,7 @@ public class ColorData {
public ColorData(final Display display) {
this.defaultColor = new Color(display, new RGB(220, 220, 220), 255);
this.color1 = new Color(display, new RGB(34, 177, 76), 255);
this.color1 = new Color(display, new RGB(255, 255, 255), 255);
this.color2 = new Color(display, new RGB(255, 194, 14), 255);
this.color3 = new Color(display, new RGB(237, 28, 36), 255);
this.darkColor = new Color(display, Constants.BLACK_RGB);
@ -42,8 +42,6 @@ public class ColorData {
switch (connectionData.clientConnection.status) {
case ACTIVE:
return (connectionData.missingPing) ? this.color2 : this.color1;
case DISABLED:
return this.color2;
default:
return this.defaultColor;
}

View file

@ -24,7 +24,6 @@ import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
@ -149,7 +148,8 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
}
}
return _exam;
});
}).onError(error -> log.error("Failed to create additional attributes defined by template for exam: ",
error));
}
@Override
@ -196,8 +196,7 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
error))
.getOrThrow(error -> new APIMessageException(
ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CONFIG,
error,
API.PARAM_MODEL_ID + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + exam.id));
error));
// map the exam configuration to the exam
this.examConfigurationMapDAO.createNew(new ExamConfigurationMap(
@ -212,8 +211,7 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
error))
.getOrThrow(error -> new APIMessageException(
ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CONFIG_LINKING,
error,
API.PARAM_MODEL_ID + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + exam.id));
error));
}
} else {
@ -223,7 +221,7 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
}
return exam;
});
}).onError(error -> log.error("Failed to create exam configuration defined by template for exam: ", error));
}
private Result<Exam> addIndicatorsFromTemplate(final Exam exam) {
@ -248,11 +246,10 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
examTemplate.indicatorTemplates
.forEach(it -> createIndicatorFromTemplate(it, exam));
}
return exam;
});
}).onError(error -> log.error("Failed to create indicators defined by template for exam: ", error));
}
private void createIndicatorFromTemplate(final IndicatorTemplate template, final Exam exam) {
@ -310,7 +307,7 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
.getOrThrow();
return exam;
});
}).onError(error -> log.error("Failed to apply default indicators for exam: ", error));
}
private String replaceVars(final String template, final Exam exam, final ExamTemplate examTemplate) {

View file

@ -150,11 +150,11 @@ public class JitsiProctoringService implements ExamProctoringService {
final ResponseEntity<String> result =
restTemplate.getForEntity(proctoringSettings.serverURL, String.class);
if (result.getStatusCode() != HttpStatus.OK) {
throw new APIMessageException(APIMessage.ErrorMessage.BINDING_ERROR);
throw new APIMessageException(APIMessage.ErrorMessage.EXTERNAL_SERVICE_BINDING_ERROR);
}
} catch (final Exception e) {
log.error("Failed to access proctoring service: {}", e.getMessage());
throw new APIMessageException(APIMessage.ErrorMessage.BINDING_ERROR, e.getMessage());
throw new APIMessageException(APIMessage.ErrorMessage.EXTERNAL_SERVICE_BINDING_ERROR, e.getMessage());
}
return true;

View file

@ -186,13 +186,13 @@ public class ZoomProctoringService implements ExamProctoringService {
if (result.getStatusCode() != HttpStatus.OK) {
throw new APIMessageException(
APIMessage.ErrorMessage.BINDING_ERROR,
APIMessage.ErrorMessage.EXTERNAL_SERVICE_BINDING_ERROR,
String.valueOf(result.getStatusCode()));
}
} catch (final Exception e) {
log.error("Failed to access Zoom service at: {}", proctoringSettings.serverURL, e);
throw new APIMessageException(APIMessage.ErrorMessage.BINDING_ERROR, e.getMessage());
throw new APIMessageException(APIMessage.ErrorMessage.EXTERNAL_SERVICE_BINDING_ERROR, e.getMessage());
}
return true;

View file

@ -8,6 +8,7 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
@ -35,6 +36,7 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
@ -411,11 +413,42 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
@Override
protected Result<Exam> notifyCreated(final Exam entity) {
return this.examTemplateService
final List<APIMessage> errors = new ArrayList<>();
this.examTemplateService
.addDefinedIndicators(entity)
.onErrorDo(error -> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_INDICATOR.of(error));
return entity;
})
.flatMap(this.examTemplateService::initAdditionalAttributes)
.onErrorDo(error -> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error));
return entity;
})
.flatMap(this.examTemplateService::initExamConfiguration)
.flatMap(this.examAdminService::applyAdditionalSEBRestrictions);
.onErrorDo(error -> {
if (error instanceof APIMessageException) {
errors.addAll(((APIMessageException) error).getAPIMessages());
} else {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CONFIG.of(error));
}
return entity;
})
.flatMap(this.examAdminService::applyAdditionalSEBRestrictions)
.onErrorDo(error -> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_RESTRICTION.of(error));
return entity;
});
if (!errors.isEmpty()) {
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()));
throw new APIMessageException(errors);
} else {
return Result.of(entity);
}
}
@Override

View file

@ -75,11 +75,11 @@ sebserver.webservice.proctoring.resetBroadcastOnLeav=true
sebserver.webservice.proctoring.zoom.enableWaitingRoom=false
sebserver.webservice.proctoring.zoom.sendRejoinForCollectingRoom=true
# Default indicator example:
#sebserver.webservice.api.exam.indicator.name=Ping
#sebserver.webservice.api.exam.indicator.type=LAST_PING
#sebserver.webservice.api.exam.indicator.color=b4b4b4
#sebserver.webservice.api.exam.indicator.thresholds=[{'value':5000.0,'color':'22b14c'},{'value':10000.0,'color':'ff7e00'},{'value':15000.0,'color':'ed1c24'}]
# Default Ping indicator:
sebserver.webservice.api.exam.indicator.name=Ping
sebserver.webservice.api.exam.indicator.type=LAST_PING
sebserver.webservice.api.exam.indicator.color=ffffff
sebserver.webservice.api.exam.indicator.thresholds=[{"value":5000.0,"color":"22b14c"},{"value":10000.0,"color":"ff7e00"},{"value":15000.0,"color":"ed1c24"}]
# Default name and description template for auto-generated exam configuration
sebserver.webservice.configtemplate.examconfig.default.name=__startDate__ __examName__

View file

@ -446,8 +446,8 @@ sebserver.exam.consistency.missing-seb-restriction= - There is currently no SEB
sebserver.exam.consistency.no-lms-connection= - Failed to connect to the LMS Setup of this exam yet.<br/>Please check the LMS connection within the LMS Setup.
sebserver.exam.consistency.invalid-lms-id= - The referencing course identifier seems to be invalid.<br/>Please check if the course for this exam still exists on the LMS and the course identifier has not changed.
sebserver.exam.confirm.remove-config=This exam is current running. The remove of the attached configuration will led to an invalid state<br/>where connecting SEB clients cannot download the configuration for the exam.<br/><br/>Are you sure to remove the configuration?
sebserver.exam.autogen.error.config.title=Exam Import with Template
sebserver.exam.autogen.error.config.text=There was an unexpected error while auto-create exam configuration.<br/>Please add an exam configuration manually.
sebserver.exam.autogen.error.config.title=Exam Import Setup Failure
sebserver.exam.autogen.error.config.text=There was an unexpected error while setting up the imported exam.<br/> Please note that the exam has successfully been imported and can be modified but one or more additional auto-setup(s) failed:<br/>{0}
sebserver.exam.action.list=Exam
sebserver.exam.action.list.view=View Exam
@ -1797,16 +1797,16 @@ sebserver.monitoring.exam.connection.action.instruction.quit.all.confirm=Are you
sebserver.monitoring.exam.connection.action.instruction.disable.selected.confirm=Are you sure to disable all selected SEB client connections?
sebserver.monitoring.exam.connection.action.instruction.disable.all.confirm=Are you sure to disable all active SEB client connections?
sebserver.monitoring.exam.connection.action.disable=Mark As Canceled
sebserver.monitoring.exam.connection.action.hide.requested=Hide Requested ({0})
sebserver.monitoring.exam.connection.action.show.requested=Show Requested ({0})
sebserver.monitoring.exam.connection.action.hide.active=Hide Active ({0})
sebserver.monitoring.exam.connection.action.show.active=Show Active ({0})
sebserver.monitoring.exam.connection.action.hide.closed=Hide Closed ({0})
sebserver.monitoring.exam.connection.action.show.closed=Show Closed ({0})
sebserver.monitoring.exam.connection.action.hide.disabled=Hide Canceled ({0})
sebserver.monitoring.exam.connection.action.show.disabled=Show Canceled ({0})
sebserver.monitoring.exam.connection.action.hide.undefined=Hide Undefined ({0})
sebserver.monitoring.exam.connection.action.show.undefined=Show Undefined ({0})
sebserver.monitoring.exam.connection.action.hide.requested=Hide Requested ( {0} )
sebserver.monitoring.exam.connection.action.show.requested=Show Requested ( {0} )
sebserver.monitoring.exam.connection.action.hide.active=Hide Active ( {0} )
sebserver.monitoring.exam.connection.action.show.active=Show Active ( {0} )
sebserver.monitoring.exam.connection.action.hide.closed=Hide Closed ( {0} )
sebserver.monitoring.exam.connection.action.show.closed=Show Closed ( {0} )
sebserver.monitoring.exam.connection.action.hide.disabled=Hide Canceled ( {0} )
sebserver.monitoring.exam.connection.action.show.disabled=Show Canceled ( {0} )
sebserver.monitoring.exam.connection.action.hide.undefined=Hide Undefined ( {0} )
sebserver.monitoring.exam.connection.action.show.undefined=Show Undefined ( {0} )
sebserver.monitoring.exam.connection.action.proctoring=Single Room Proctoring
sebserver.monitoring.exam.connection.action.proctoring.examroom=Exam Room Proctoring
sebserver.monitoring.exam.connection.action.openTownhall.confirm=You are about to open the town-hall room and force all SEB clients to join the town-hall room.<br/>Are you sure to open the town-hall?