SEBSERV-457 implementation

This commit is contained in:
anhefti 2023-12-14 08:19:33 +01:00
parent 8584ff5312
commit 0544d7a799
38 changed files with 725 additions and 299 deletions

View file

@ -47,6 +47,7 @@ public final class API {
public static final String PARAM_VIEW_ID = "viewId";
public static final String PARAM_INSTRUCTION_TYPE = "instructionType";
public static final String PARAM_INSTRUCTION_ATTRIBUTES = "instructionAttributes";
public static final String PARAM_ADDITIONAL_ATTRIBUTES = "additionalAttributes";
public static final String DEFAULT_CONFIG_TEMPLATE_ID = String.valueOf(ConfigurationNode.DEFAULT_TEMPLATE_ID);

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.gbl.api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@ -19,6 +21,8 @@ import com.fasterxml.jackson.datatype.joda.JodaModule;
@Component
public class JSONMapper extends ObjectMapper {
private static final Logger log = LoggerFactory.getLogger(JSONMapper.class);
private static final long serialVersionUID = 2883304481547670626L;
public JSONMapper() {
@ -33,4 +37,15 @@ public class JSONMapper extends ObjectMapper {
super.setSerializationInclusion(Include.NON_NULL);
}
public String writeValueAsStringOr(final Object entity, final String or) {
if (entity == null) {
return or;
}
try {
return super.writeValueAsString(entity);
} catch (final Exception e) {
log.error("Failed to serialize value: {}", entity, e);
return or;
}
}
}

View file

@ -17,6 +17,7 @@ import java.util.Map;
import javax.validation.constraints.NotNull;
import ch.ethz.seb.sebserver.gbl.api.API;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
@ -63,8 +64,6 @@ public final class Exam implements GrantEntity {
public static final String FILTER_CACHED_QUIZZES = "cached-quizzes";
public static final String FILTER_ATTR_HIDE_MISSING = "show-missing";
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 */
@ -100,7 +99,6 @@ public final class Exam implements GrantEntity {
public final Long institutionId;
@JsonProperty(EXAM.ATTR_LMS_SETUP_ID)
@NotNull
public final Long lmsSetupId;
@JsonProperty(EXAM.ATTR_EXTERNAL_ID)
@ -150,7 +148,7 @@ public final class Exam implements GrantEntity {
@JsonProperty(EXAM.ATTR_LAST_MODIFIED)
public final Long lastModified;
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES)
@JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES)
public final Map<String, String> additionalAttributes;
@JsonIgnore
@ -178,7 +176,7 @@ public final class Exam implements GrantEntity {
@JsonProperty(EXAM.ATTR_LASTUPDATE) final String lastUpdate,
@JsonProperty(EXAM.ATTR_EXAM_TEMPLATE_ID) final Long examTemplateId,
@JsonProperty(EXAM.ATTR_LAST_MODIFIED) final Long lastModified,
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
@JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
this.id = id;
this.institutionId = institutionId;
@ -209,6 +207,41 @@ public final class Exam implements GrantEntity {
this.allowedSEBVersions = initAllowedSEBVersions();
}
public Exam(final POSTMapper postMap) {
this.id = null;
this.institutionId = postMap.getLong(EXAM.ATTR_INSTITUTION_ID);
this.lmsSetupId = postMap.getLong(EXAM.ATTR_LMS_SETUP_ID);
this.externalId = postMap.getString(EXAM.ATTR_EXTERNAL_ID);
this.lmsAvailable = true;
this.name = postMap.getString(EXAM.ATTR_QUIZ_NAME);
this.startTime = postMap.getDateTime(EXAM.ATTR_QUIZ_START_TIME);
this.endTime = postMap.getDateTime(EXAM.ATTR_QUIZ_END_TIME);
this.type = postMap.getEnum(EXAM.ATTR_TYPE, ExamType.class, ExamType.UNDEFINED);
this.owner = postMap.getString(EXAM.ATTR_OWNER);
this.status = postMap.getEnum(EXAM.ATTR_STATUS, ExamStatus.class, getStatusFromDate(this.startTime, this.endTime));
this.sebRestriction = null;
this.browserExamKeys = null;
this.active = postMap.getBoolean(EXAM.ATTR_ACTIVE);
this.supporter = postMap.getStringSet(EXAM.ATTR_SUPPORTER);
this.lastUpdate = null;
this.examTemplateId = postMap.getLong(EXAM.ATTR_EXAM_TEMPLATE_ID);
this.lastModified = null;
final Map<String, String> additionalAttributes = new HashMap<>();
if (postMap.contains(QuizData.QUIZ_ATTR_DESCRIPTION)) {
additionalAttributes.put(QuizData.QUIZ_ATTR_DESCRIPTION, postMap.getString(QuizData.QUIZ_ATTR_DESCRIPTION));
}
additionalAttributes.put(QuizData.QUIZ_ATTR_START_URL, postMap.getString(QuizData.QUIZ_ATTR_START_URL));
this.additionalAttributes = Utils.immutableMapOf(additionalAttributes);
this.checkASK = BooleanUtils
.toBoolean(this.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED));
this.allowedSEBVersions = initAllowedSEBVersions();
}
public Exam(final QuizData quizData) {
this(null, quizData, POSTMapper.EMPTY_MAP);
}
public Exam(final String modelId, final QuizData quizData, final POSTMapper mapper) {
final Map<String, String> additionalAttributes = new HashMap<>(quizData.getAdditionalAttributes());
@ -243,41 +276,13 @@ public final class Exam implements GrantEntity {
this.allowedSEBVersions = initAllowedSEBVersions();
}
public Exam(final QuizData quizData) {
this(null, quizData, POSTMapper.EMPTY_MAP);
}
public Exam(final Long id, final ExamStatus status) {
this.id = id;
this.institutionId = null;
this.lmsSetupId = null;
this.externalId = null;
this.lmsAvailable = true;
this.name = null;
this.startTime = null;
this.endTime = null;
this.type = null;
this.owner = null;
this.status = (status != null) ? status : getStatusFromDate(this.startTime, this.endTime);
this.sebRestriction = null;
this.browserExamKeys = null;
this.active = null;
this.supporter = null;
this.lastUpdate = null;
this.examTemplateId = null;
this.lastModified = null;
this.additionalAttributes = null;
this.checkASK = false;
this.allowedSEBVersions = null;
}
private List<AllowedSEBVersion> initAllowedSEBVersions() {
if (this.additionalAttributes.containsKey(ADDITIONAL_ATTR_ALLOWED_SEB_VERSIONS)) {
final String asvString = this.additionalAttributes.get(Exam.ADDITIONAL_ATTR_ALLOWED_SEB_VERSIONS);
final String[] split = StringUtils.split(asvString, Constants.LIST_SEPARATOR);
final List<AllowedSEBVersion> result = new ArrayList<>();
for (int i = 0; i < split.length; i++) {
final AllowedSEBVersion allowedSEBVersion = new AllowedSEBVersion(split[i]);
for (final String s : split) {
final AllowedSEBVersion allowedSEBVersion = new AllowedSEBVersion(s);
if (allowedSEBVersion.isValidFormat) {
result.add(allowedSEBVersion);
}
@ -401,7 +406,7 @@ public final class Exam implements GrantEntity {
}
public boolean additionalAttributesIncluded() {
return this.additionalAttributes != null;
return this.additionalAttributes != null && !this.additionalAttributes.isEmpty();
}
public String getAdditionalAttribute(final String attrName) {

View file

@ -13,6 +13,7 @@ import java.util.Comparator;
import java.util.Map;
import java.util.Objects;
import ch.ethz.seb.sebserver.gbl.api.API;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
@ -33,9 +34,6 @@ public final class QuizData implements GrantEntity {
public static final String FILTER_ATTR_QUIZ_NAME = "quiz_name";
public static final String FILTER_ATTR_START_TIME = "start_timestamp";
public static final String ATTR_ADDITIONAL_ATTRIBUTES = "ADDITIONAL_ATTRIBUTES";
public static final String QUIZ_ATTR_ID = "quiz_id";
public static final String QUIZ_ATTR_INSTITUTION_ID = Domain.EXAM.ATTR_INSTITUTION_ID;
public static final String QUIZ_ATTR_LMS_SETUP_ID = "lms_setup_id";
@ -81,7 +79,7 @@ public final class QuizData implements GrantEntity {
@JsonProperty(QUIZ_ATTR_START_URL)
public final String startURL;
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES)
@JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES)
public final Map<String, String> additionalAttributes;
@JsonCreator
@ -95,7 +93,7 @@ public final class QuizData implements GrantEntity {
@JsonProperty(QUIZ_ATTR_START_TIME) final DateTime startTime,
@JsonProperty(QUIZ_ATTR_END_TIME) final DateTime endTime,
@JsonProperty(QUIZ_ATTR_START_URL) final String startURL,
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
@JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
this.id = id;
this.institutionId = institutionId;

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gbl.model.user;
import java.util.Map;
import ch.ethz.seb.sebserver.gbl.api.API;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
@ -21,7 +22,6 @@ public class ExamineeAccountDetails {
public static final String ATTR_NAME = "name";
public static final String ATTR_USER_NAME = "username";
public static final String ATTR_EMAIL = "email";
public static final String ATTR_ADDITIONAL_ATTRIBUTES = "additionalAttributes";
@JsonProperty(ATTR_ID)
public final String id;
@ -35,7 +35,7 @@ public class ExamineeAccountDetails {
@JsonProperty(ATTR_EMAIL)
public final String email;
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES)
@JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES)
public final Map<String, String> additionalAttributes;
@JsonCreator
@ -44,7 +44,7 @@ public class ExamineeAccountDetails {
@JsonProperty(ATTR_NAME) final String name,
@JsonProperty(ATTR_USER_NAME) final String username,
@JsonProperty(ATTR_EMAIL) final String email,
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
@JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
this.id = id;
this.name = name;

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gbl.util;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
@ -244,6 +245,13 @@ public final class Result<T> {
}
}
public Result<T> whenDo(final Predicate<T> predicate, final Function<T, T> handler) {
if (this.error == null && predicate.test(this.value)) {
return Result.tryCatch(() -> handler.apply(this.value));
}
return this;
}
public Result<T> onSuccess(final Consumer<T> handler) {
if (this.error == null) {
handler.accept(this.value);

View file

@ -278,13 +278,20 @@ public final class Utils {
.getMillis());
}
public static String formatDate(final DateTime dateTime) {
public static String formatDateWithMilliseconds(final DateTime dateTime) {
if (dateTime == null) {
return Constants.EMPTY_NOTE;
}
return dateTime.toString(Constants.STANDARD_DATE_TIME_MILLIS_FORMATTER);
}
public static String formatDate(final DateTime dateTime) {
if (dateTime == null) {
return Constants.EMPTY_NOTE;
}
return dateTime.toString(Constants.STANDARD_DATE_TIME_FORMATTER);
}
public static Long dateTimeStringToTimestamp(final String startTime, final Long defaultValue) {
return dateTimeStringToTimestamp(startTime)
.getOr(defaultValue);

View file

@ -10,20 +10,17 @@ package ch.ethz.seb.sebserver.gui.content.exam;
import static ch.ethz.seb.sebserver.gbl.FeatureService.ConfigurableFeature.SCREEN_PROCTORING;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BooleanSupplier;
import java.util.*;
import java.util.function.Function;
import ch.ethz.seb.sebserver.gbl.FeatureService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.*;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
@ -65,13 +62,6 @@ import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.ArchiveExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamConsistency;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckSEBRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetScreenProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetDefaultExamTemplate;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplate;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetup;
@ -216,35 +206,33 @@ public class ExamForm implements TemplateComposer {
@Override
public void compose(final PageContext pageContext) {
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
final EntityKey entityKey = pageContext.getEntityKey();
final boolean readonly = pageContext.isReadonly();
final boolean newExamNoLMS = BooleanUtils.toBoolean(
pageContext.getAttribute(AttributeKeys.NEW_EXAM_NO_LMS));
final boolean importFromQuizData = BooleanUtils.toBoolean(
pageContext.getAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA));
// get or create model data
final Exam exam = (importFromQuizData
? createExamFromQuizData(pageContext)
: getExistingExam(pageContext))
.onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error))
.getOrThrow();
final Exam exam = newExamNoLMS
? this.newExamNoLMS()
: (importFromQuizData
? createExamFromQuizData(pageContext)
: getExistingExam(pageContext))
.onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error))
.getOrThrow();
// new PageContext with actual EntityKey
final EntityKey entityKey = (readonly || !newExamNoLMS) ? pageContext.getEntityKey() : null;
final PageContext formContext = pageContext.withEntityKey(exam.getEntityKey());
final BooleanSupplier isNew = () -> importFromQuizData;
final BooleanSupplier isNotNew = () -> !isNew.getAsBoolean();
final EntityGrantCheck entityGrantCheck = currentUser.entityGrantCheck(exam);
final boolean modifyGrant = entityGrantCheck.m();
final boolean writeGrant = entityGrantCheck.w();
final ExamStatus examStatus = exam.getStatus();
final boolean editable = modifyGrant &&
(examStatus == ExamStatus.UP_COMING || examStatus == ExamStatus.RUNNING);
(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 = testSEBRestrictionAPI(exam);
final boolean sebRestrictionAvailable = readonly && testSEBRestrictionAPI(exam);
final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService
.getBuilder(CheckSEBRestriction.class)
.withURIVariable(API.PARAM_MODEL_ID, exam.getModelId())
@ -281,140 +269,15 @@ public class ExamForm implements TemplateComposer {
}
// The Exam form
final FormHandle<Exam> formHandle = this.pageService.formBuilder(
formContext.copyOf(content), 8)
.withDefaultSpanLabel(1)
.withDefaultSpanInput(4)
.withDefaultSpanEmptyCell(3)
.readonly(readonly)
.putStaticValueIf(isNotNew,
Domain.EXAM.ATTR_ID,
exam.getModelId())
.putStaticValue(
Domain.EXAM.ATTR_INSTITUTION_ID,
String.valueOf(exam.getInstitutionId()))
.putStaticValueIf(isNotNew,
Domain.EXAM.ATTR_LMS_SETUP_ID,
String.valueOf(exam.lmsSetupId))
.putStaticValueIf(isNew,
QuizData.QUIZ_ATTR_LMS_SETUP_ID,
String.valueOf(exam.lmsSetupId))
.putStaticValueIf(isNotNew,
Domain.EXAM.ATTR_EXTERNAL_ID,
exam.externalId)
.putStaticValueIf(isNew,
QuizData.QUIZ_ATTR_ID,
exam.externalId)
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_NAME,
FORM_NAME_TEXT_KEY,
exam.name)
.readonly(true)
.withInputSpan(3)
.withEmptyCellSeparation(false))
.addField(FormBuilder.singleSelection(
Domain.EXAM.ATTR_LMS_SETUP_ID,
FORM_LMSSETUP_TEXT_KEY,
String.valueOf(exam.lmsSetupId),
this.resourceService::lmsSetupResource)
.readonly(true)
.withInputSpan(3)
.withEmptyCellSeparation(false))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_START_TIME,
FORM_START_TIME_TEXT_KEY,
i18nSupport.formatDisplayDateWithTimeZone(exam.startTime))
.readonly(true)
.withInputSpan(3)
.withEmptyCellSeparation(false))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_END_TIME,
FORM_END_TIME_TEXT_KEY,
i18nSupport.formatDisplayDateWithTimeZone(exam.endTime))
.readonly(true)
.withInputSpan(3)
.withEmptyCellSeparation(false))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_START_URL,
FORM_QUIZ_URL_TEXT_KEY,
exam.getStartURL())
.readonly(true)
.withInputSpan(7)
.withEmptyCellSeparation(false))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_DESCRIPTION,
FORM_DESCRIPTION_TEXT_KEY,
exam.getDescription())
.asHTML(50)
.readonly(true)
.withInputSpan(6)
.withEmptyCellSeparation(false))
.addField(FormBuilder.text(
Domain.EXAM.ATTR_EXTERNAL_ID,
FORM_QUIZ_ID_TEXT_KEY,
exam.externalId)
.readonly(true)
.withLabelSpan(2)
.withInputSpan(6)
.withEmptyCellSeparation(false))
.addField(FormBuilder.text(
Domain.EXAM.ATTR_STATUS + "_display",
FORM_STATUS_TEXT_KEY,
i18nSupport.getText(new LocTextKey("sebserver.exam.status." + examStatus.name())))
.readonly(true)
.withLabelSpan(2)
.withInputSpan(4)
.withEmptyCellSeparation(false))
.addFieldIf(
() -> importFromQuizData,
() -> FormBuilder.singleSelection(
Domain.EXAM.ATTR_EXAM_TEMPLATE_ID,
FORM_EXAM_TEMPLATE_TEXT_KEY,
(exam.examTemplateId == null)
? getDefaultExamTemplateId()
: String.valueOf(exam.examTemplateId),
this.resourceService::examTemplateResources)
.withSelectionListener(form -> this.processTemplateSelection(form, formContext))
.withLabelSpan(2)
.withInputSpan(4)
.withEmptyCellSpan(2))
.addField(FormBuilder.singleSelection(
Domain.EXAM.ATTR_TYPE,
FORM_TYPE_TEXT_KEY,
(exam.type != null) ? String.valueOf(exam.type) : Exam.ExamType.UNDEFINED.name(),
this.resourceService::examTypeResources)
.withLabelSpan(2)
.withInputSpan(4)
.withEmptyCellSpan(2)
.mandatory(!readonly))
.addField(FormBuilder.multiComboSelection(
Domain.EXAM.ATTR_SUPPORTER,
FORM_SUPPORTER_TEXT_KEY,
StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR),
this.resourceService::examSupporterResources)
.withLabelSpan(2)
.withInputSpan(4)
.withEmptyCellSpan(2))
.buildFor(importFromQuizData
? this.restService.getRestCall(ImportAsExam.class)
: this.restService.getRestCall(SaveExam.class));
final FormHandle<Exam> formHandle = readonly
? createReadOnlyForm(formContext, content, exam)
: createEditForm(formContext, content, exam);
if (importFromQuizData) {
this.processTemplateSelection(formHandle.getForm(), formContext);
}
final boolean proctoringEnabled = !importFromQuizData && this.restService
final boolean proctoringEnabled = readonly && this.restService
.getBuilder(GetExamProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
@ -422,7 +285,7 @@ public class ExamForm implements TemplateComposer {
.getOr(false);
final boolean spsFeatureEnabled = this.featureService.isEnabled(SCREEN_PROCTORING);
final boolean screenProctoringEnabled = spsFeatureEnabled && !importFromQuizData && this.restService
final boolean screenProctoringEnabled = readonly && spsFeatureEnabled && this.restService
.getBuilder(GetScreenProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
@ -450,7 +313,7 @@ public class ExamForm implements TemplateComposer {
.withEntityKey(entityKey)
.withConfirm(() -> EXAM_ARCHIVE_CONFIRM)
.withExec(this::archiveExam)
.publishIf(() -> writeGrant && readonly && examStatus == ExamStatus.FINISHED)
.publishIf(() -> writeGrant && readonly && exam.status == ExamStatus.FINISHED)
.newAction(ActionDefinition.EXAM_SAVE)
.withExec(action -> (importFromQuizData)
@ -527,7 +390,6 @@ public class ExamForm implements TemplateComposer {
.noEventPropagation()
.publishIf(
() -> spsFeatureEnabled && !screenProctoringEnabled && readonly)
;
// additional data in read-only view
@ -538,7 +400,7 @@ public class ExamForm implements TemplateComposer {
.copyOf(content)
.withAttribute(ATTR_READ_GRANT, String.valueOf(entityGrantCheck.r()))
.withAttribute(ATTR_EDITABLE, String.valueOf(editable))
.withAttribute(ATTR_EXAM_STATUS, examStatus.name()));
.withAttribute(ATTR_EXAM_STATUS, exam.status.name()));
// Indicators
this.examIndicatorsList.compose(
@ -546,7 +408,7 @@ public class ExamForm implements TemplateComposer {
.copyOf(content)
.withAttribute(ATTR_READ_GRANT, String.valueOf(entityGrantCheck.r()))
.withAttribute(ATTR_EDITABLE, String.valueOf(editable))
.withAttribute(ATTR_EXAM_STATUS, examStatus.name()));
.withAttribute(ATTR_EXAM_STATUS, exam.status.name()));
// Client Groups
this.examClientGroupList.compose(
@ -554,10 +416,248 @@ public class ExamForm implements TemplateComposer {
.copyOf(content)
.withAttribute(ATTR_READ_GRANT, String.valueOf(entityGrantCheck.r()))
.withAttribute(ATTR_EDITABLE, String.valueOf(editable))
.withAttribute(ATTR_EXAM_STATUS, examStatus.name()));
.withAttribute(ATTR_EXAM_STATUS, exam.status.name()));
}
}
private FormHandle<Exam> createReadOnlyForm(
final PageContext formContext,
final Composite content,
final Exam exam) {
final I18nSupport i18nSupport = formContext.getI18nSupport();
return this.pageService.formBuilder(
formContext.copyOf(content), 8)
.withDefaultSpanLabel(1)
.withDefaultSpanInput(4)
.withDefaultSpanEmptyCell(3)
.readonly(true)
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_NAME,
FORM_NAME_TEXT_KEY,
exam.name)
.readonly(true)
.withInputSpan(3)
.withEmptyCellSeparation(false))
.addField(FormBuilder.singleSelection(
Domain.EXAM.ATTR_LMS_SETUP_ID,
FORM_LMSSETUP_TEXT_KEY,
String.valueOf(exam.lmsSetupId),
this.resourceService::lmsSetupResource)
.readonly(true)
.withInputSpan(3)
.withEmptyCellSeparation(false))
.addField(FormBuilder.text(
Domain.EXAM.ATTR_STATUS + "_display",
FORM_STATUS_TEXT_KEY,
i18nSupport.getText(new LocTextKey("sebserver.exam.status." + exam.status.name())))
.readonly(true)
.withInputSpan(3)
.withEmptyCellSeparation(false))
.addField(FormBuilder.text(
Domain.EXAM.ATTR_EXTERNAL_ID,
FORM_QUIZ_ID_TEXT_KEY,
exam.externalId)
.readonly(true)
.withInputSpan(3)
.withEmptyCellSeparation(false))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_START_TIME,
FORM_START_TIME_TEXT_KEY,
i18nSupport.formatDisplayDateWithTimeZone(exam.startTime))
.readonly(true)
.withInputSpan(3)
.withEmptyCellSeparation(false))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_END_TIME,
FORM_END_TIME_TEXT_KEY,
i18nSupport.formatDisplayDateWithTimeZone(exam.endTime))
.readonly(true)
.withInputSpan(3)
.withEmptyCellSeparation(false))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_START_URL,
FORM_QUIZ_URL_TEXT_KEY,
exam.getStartURL())
.readonly(true)
.withInputSpan(7)
.withEmptyCellSeparation(false))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_DESCRIPTION,
FORM_DESCRIPTION_TEXT_KEY,
exam.getDescription())
.asHTML(50)
.readonly(true)
.withInputSpan(7)
.withEmptyCellSeparation(false))
.addField(FormBuilder.singleSelection(
Domain.EXAM.ATTR_TYPE,
FORM_TYPE_TEXT_KEY,
(exam.type != null) ? String.valueOf(exam.type) : Exam.ExamType.UNDEFINED.name(),
this.resourceService::examTypeResources)
.withInputSpan(7)
.withEmptyCellSeparation(false))
.addField(FormBuilder.multiComboSelection(
Domain.EXAM.ATTR_SUPPORTER,
FORM_SUPPORTER_TEXT_KEY,
StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR),
this.resourceService::examSupporterResources)
.withInputSpan(7)
.withEmptyCellSeparation(false))
.build();
}
private FormHandle<Exam> createEditForm(
final PageContext formContext,
final Composite content,
final Exam exam) {
final I18nSupport i18nSupport = formContext.getI18nSupport();
final boolean newExam = exam.id == null;
final boolean hasLMS = exam.lmsSetupId != null;
final boolean importFromLMS = newExam && hasLMS;
final DateTimeZone timeZone = this.pageService.getCurrentUser().get().timeZone;
final LocTextKey statusTitle = new LocTextKey("sebserver.exam.status." + exam.status.name());
return this.pageService.formBuilder(formContext.copyOf(content))
.putStaticValueIf(() -> !newExam,
Domain.EXAM.ATTR_ID,
exam.getModelId())
.putStaticValue(
Domain.EXAM.ATTR_INSTITUTION_ID,
String.valueOf(exam.getInstitutionId()))
.putStaticValueIf(() -> exam.lmsSetupId != null,
Domain.EXAM.ATTR_LMS_SETUP_ID,
String.valueOf(exam.lmsSetupId))
.putStaticValueIf(() -> exam.lmsSetupId != null,
QuizData.QUIZ_ATTR_LMS_SETUP_ID,
String.valueOf(exam.lmsSetupId))
.putStaticValueIf(() -> exam.externalId != null,
Domain.EXAM.ATTR_EXTERNAL_ID,
exam.externalId)
.putStaticValueIf(() -> exam.lmsSetupId != null,
QuizData.QUIZ_ATTR_ID,
exam.externalId)
.addField(FormBuilder.text(
Domain.EXAM.ATTR_STATUS + "_display",
FORM_STATUS_TEXT_KEY,
i18nSupport.getText(statusTitle))
.readonly(true))
.addFieldIf( () -> hasLMS,
() -> FormBuilder.singleSelection(
Domain.EXAM.ATTR_LMS_SETUP_ID,
FORM_LMSSETUP_TEXT_KEY,
String.valueOf(exam.lmsSetupId),
this.resourceService::lmsSetupResource)
.readonly(true))
.addFieldIf(() -> exam.id == null,
() -> FormBuilder.singleSelection(
Domain.EXAM.ATTR_EXAM_TEMPLATE_ID,
FORM_EXAM_TEMPLATE_TEXT_KEY,
(exam.examTemplateId == null)
? getDefaultExamTemplateId()
: String.valueOf(exam.examTemplateId),
this.resourceService::examTemplateResources)
.withSelectionListener(form -> this.processTemplateSelection(form, formContext)))
.addField(FormBuilder.text(
Domain.EXAM.ATTR_QUIZ_NAME,
FORM_NAME_TEXT_KEY,
exam.name)
.readonly(hasLMS)
.mandatory(!hasLMS))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_DESCRIPTION,
FORM_DESCRIPTION_TEXT_KEY,
exam.getDescription())
.asArea()
.readonly(hasLMS))
.withAdditionalValueMapping(
QuizData.QUIZ_ATTR_DESCRIPTION,
QuizData.QUIZ_ATTR_DESCRIPTION)
.addField(FormBuilder.dateTime(
Domain.EXAM.ATTR_QUIZ_START_TIME,
FORM_START_TIME_TEXT_KEY,
exam.startTime)
.readonly(hasLMS)
.mandatory(!hasLMS))
.addField(FormBuilder.dateTime(
Domain.EXAM.ATTR_QUIZ_END_TIME,
FORM_END_TIME_TEXT_KEY,
exam.endTime != null
? exam.endTime
: DateTime.now(timeZone).plusHours(1))
.readonly(hasLMS)
.mandatory(!hasLMS))
.addField(FormBuilder.text(
QuizData.QUIZ_ATTR_START_URL,
FORM_QUIZ_URL_TEXT_KEY,
exam.getStartURL())
.readonly(hasLMS)
.mandatory(!hasLMS))
.withAdditionalValueMapping(
QuizData.QUIZ_ATTR_START_URL,
QuizData.QUIZ_ATTR_START_URL)
.addField(FormBuilder.singleSelection(
Domain.EXAM.ATTR_TYPE,
FORM_TYPE_TEXT_KEY,
(exam.type != null) ? String.valueOf(exam.type) : Exam.ExamType.UNDEFINED.name(),
this.resourceService::examTypeResources)
.mandatory(true))
.addField(FormBuilder.multiComboSelection(
Domain.EXAM.ATTR_SUPPORTER,
FORM_SUPPORTER_TEXT_KEY,
StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR),
this.resourceService::examSupporterResources))
.buildFor(importFromLMS
? this.restService.getRestCall(ImportAsExam.class)
: newExam
? this.restService.getRestCall(NewExam.class)
: this.restService.getRestCall(SaveExam.class));
}
private Exam newExamNoLMS() {
return new Exam(
null,
this.pageService.getCurrentUser().get().institutionId,
null,
UUID.randomUUID().toString(),
true,
null,
null,
null,
Exam.ExamType.UNDEFINED,
null,
null,
ExamStatus.UP_COMING,
false,
null,
true,
null,
null,
null,
null);
}
private PageAction archiveExam(final PageAction action) {
this.restService.getBuilder(ArchiveExam.class)
@ -660,7 +760,7 @@ public class ExamForm implements TemplateComposer {
}
private boolean testSEBRestrictionAPI(final Exam exam) {
if (!exam.isLmsAvailable() || exam.status == ExamStatus.ARCHIVED) {
if (exam.lmsSetupId == null || !exam.isLmsAvailable() || exam.status == ExamStatus.ARCHIVED) {
return false;
}

View file

@ -8,10 +8,14 @@
package ch.ethz.seb.sebserver.gui.content.exam;
import static ch.ethz.seb.sebserver.gbl.FeatureService.ConfigurableFeature.EXAM_NO_LMS;
import static ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys.NEW_EXAM_NO_LMS;
import java.util.function.BiConsumer;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import ch.ethz.seb.sebserver.gbl.FeatureService;
import org.apache.commons.lang3.BooleanUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.widgets.Composite;
@ -145,6 +149,7 @@ public class ExamList implements TemplateComposer {
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
final FeatureService featureService = this.pageService.getFeatureService();
// content page layout with title
final Composite content = widgetFactory.defaultPageLayout(
@ -251,7 +256,7 @@ public class ExamList implements TemplateComposer {
table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION),
action -> modifyExam(action, table),
EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> userGrant.im(), false)
.publishIf(userGrant::im, false)
.newAction(ActionDefinition.EXAM_LIST_BULK_ARCHIVE)
.withSelect(
@ -259,7 +264,7 @@ public class ExamList implements TemplateComposer {
this.examBatchArchivePopup.popupCreationFunction(pageContext),
EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(() -> userGrant.im(), false)
.publishIf(userGrant::im, false)
.newAction(ActionDefinition.EXAM_LIST_BULK_DELETE)
.withSelect(
@ -267,9 +272,8 @@ public class ExamList implements TemplateComposer {
this.examBatchDeletePopup.popupCreationFunction(pageContext),
EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(() -> userGrant.iw(), false);
.publishIf(userGrant::iw, false)
actionBuilder
.newAction(ActionDefinition.EXAM_LIST_HIDE_MISSING)
.withExec(action -> hideMissingExams(action, table))
.noEventPropagation()
@ -278,8 +282,12 @@ public class ExamList implements TemplateComposer {
.withExec(action -> showMissingExams(action, table))
.noEventPropagation()
.create())
.publish();
.publish()
.newAction(ActionDefinition.EXAM_NEW)
.withAttribute(NEW_EXAM_NO_LMS, Constants.TRUE_STRING)
.publishIf(() -> userGrant.iw() && featureService.isEnabled(EXAM_NO_LMS))
;
}
private PageAction showMissingExams(final PageAction action, final EntityTable<Exam> table) {

View file

@ -0,0 +1,44 @@
package ch.ethz.seb.sebserver.gui.form;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.widget.DateTimeSelector;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.joda.time.DateTime;
public class DateTimeSelectorFieldBuilder extends FieldBuilder<DateTime> {
public DateTimeSelectorFieldBuilder(final String name, final LocTextKey label, final DateTime value) {
super(name, label, value);
}
@Override
void build(final FormBuilder builder) {
final boolean readonly = builder.readonly || this.readonly;
final Control titleLabel = createTitleLabel(builder.formParent, builder, this);
final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput);
if (readonly) {
final Text label = new Text(fieldGrid, SWT.NONE);
label.setText(builder.i18nSupport.formatDisplayDateTime(value) + " " + builder.i18nSupport.getUsersTimeZoneTitleSuffix());
label.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, true));
builder.form.putReadonlyField(this.name, titleLabel, label);
return;
}
final DateTimeSelector dateTimeSelector = new DateTimeSelector(
fieldGrid,
builder.widgetFactory,
builder.pageService.getCurrentUser().get().timeZone,
this.label.name,
label.name);
dateTimeSelector.setValue(value);
final Label errorLabel = createErrorLabel(fieldGrid);
builder.form.putField(this.name, titleLabel, dateTimeSelector, errorLabel);
}
}

View file

@ -31,7 +31,7 @@ public abstract class FieldBuilder<T> {
int spanLabel = -1;
int spanInput = -1;
int spanEmptyCell = -1;
int titleValign = SWT.TOP;
int titleValign = SWT.CENTER;
Boolean autoEmptyCellSeparation = null;
String group = null;
boolean readonly = false;
@ -39,7 +39,6 @@ public abstract class FieldBuilder<T> {
String defaultLabel = null;
boolean isMandatory = false;
boolean rightLabel = false;
final String name;
final LocTextKey label;
final LocTextKey tooltip;
@ -133,7 +132,7 @@ public abstract class FieldBuilder<T> {
gridLayout.marginWidth = 0;
gridLayout.marginRight = 0;
infoGrid.setLayout(gridLayout);
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
final GridData gridData = new GridData(SWT.FILL, fieldBuilder.titleValign, true, true);
gridData.horizontalSpan = (fieldBuilder.spanLabel > 0) ? fieldBuilder.spanLabel : 1;
infoGrid.setLayoutData(gridData);

View file

@ -8,25 +8,21 @@
package ch.ethz.seb.sebserver.gui.form;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gui.widget.*;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@ -40,12 +36,7 @@ import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Tuple;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.FormBinding;
import ch.ethz.seb.sebserver.gui.widget.FileUploadSelection;
import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection;
import ch.ethz.seb.sebserver.gui.widget.PasswordInput;
import ch.ethz.seb.sebserver.gui.widget.Selection;
import ch.ethz.seb.sebserver.gui.widget.Selection.Type;
import ch.ethz.seb.sebserver.gui.widget.ThresholdList;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
public final class Form implements FormBinding {
@ -55,9 +46,11 @@ public final class Form implements FormBinding {
private final ObjectNode objectRoot;
private final Map<String, String> staticValues = new LinkedHashMap<>();
private final Map<String, String> additionalAttributeMapping = new LinkedHashMap<>();
private final MultiValueMap<String, FormFieldAccessor> formFields = new LinkedMultiValueMap<>();
private final Map<String, Set<String>> groups = new LinkedHashMap<>();
Form(final JSONMapper jsonMapper, final Cryptor cryptor) {
this.jsonMapper = jsonMapper;
this.cryptor = cryptor;
@ -108,6 +101,10 @@ public final class Form implements FormBinding {
}
}
public void putAdditionalValueMapping(final String fieldName, final String attrName) {
this.additionalAttributeMapping.put(fieldName, attrName);
}
public String getStaticValue(final String name) {
return this.staticValues.get(name);
}
@ -175,14 +172,19 @@ public final class Form implements FormBinding {
return this;
}
Form putField(final String name, final Control label, final FileUploadSelection fileUpload,
final Label errorLabel) {
Form putField(final String name, final Control label, final FileUploadSelection fileUpload, final Label errorLabel) {
final FormFieldAccessor createAccessor = createAccessor(label, fileUpload, errorLabel);
fileUpload.setErrorHandler(createAccessor::setError);
this.formFields.add(name, createAccessor);
return this;
}
Form putField(final String name, final Control label, final DateTimeSelector dateTimeSelector, final Label errorLabel) {
final FormFieldAccessor createAccessor = createAccessor(label, dateTimeSelector, errorLabel);
this.formFields.add(name, createAccessor);
return this;
}
Form removeField(final String name) {
if (this.formFields.containsKey(name)) {
final List<FormFieldAccessor> list = this.formFields.remove(name);
@ -307,6 +309,21 @@ public final class Form implements FormBinding {
.filter(Form::valueApplicationFilter)
.forEach(ffa -> ffa.putJsonValue(entry.getKey(), this.objectRoot));
}
if (!this.additionalAttributeMapping.isEmpty()) {
final Map<String, String> additionalAttrs = new HashMap<>();
for (final Map.Entry<String, String> entry : this.additionalAttributeMapping.entrySet()) {
final String fieldValue = this.getFieldValue(entry.getKey());
if (fieldValue != null) {
additionalAttrs.put(entry.getValue(), fieldValue);
}
}
if (additionalAttrs != null) {
this.objectRoot.putIfAbsent(
API.PARAM_ADDITIONAL_ATTRIBUTES,
jsonMapper.valueToTree(additionalAttrs));
}
}
}
private static boolean valueApplicationFilter(final FormFieldAccessor ffa) {
@ -317,7 +334,7 @@ public final class Form implements FormBinding {
//@formatter:off
private FormFieldAccessor createReadonlyAccessor(final Control label, final Text field) {
return new FormFieldAccessor(label, field, null) {
@Override public String getStringValue() { return null; } // ensures that read-only fields do not send diplay values to the back-end
@Override public String getStringValue() { return null; } // ensures that read-only fields do not send display values to the back-end
@Override public void setStringValue(final String value) { field.setText( (value == null) ? StringUtils.EMPTY : value); }
};
}
@ -412,6 +429,14 @@ public final class Form implements FormBinding {
@Override public String getStringValue() { return fileUpload.getFileName(); }
};
}
private FormFieldAccessor createAccessor(final Control label, final DateTimeSelector dateTimeSelector, final Label errorLabel) {
return new FormFieldAccessor(label, dateTimeSelector, errorLabel) {
@Override public String getStringValue() { return dateTimeSelector.getValue(); }
@Override public void setStringValue(final String value) { dateTimeSelector.setValue(value); }
};
}
//@formatter:on
/*

View file

@ -20,6 +20,7 @@ import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.TabItem;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -146,6 +147,11 @@ public class FormBuilder {
return this;
}
public FormBuilder withAdditionalValueMapping(final String fieldName, final String attrName) {
this.form.putAdditionalValueMapping(fieldName, attrName);
return this;
}
public FormBuilder addFieldIf(
final BooleanSupplier condition,
final Supplier<FieldBuilder<?>> templateSupplier) {
@ -304,4 +310,8 @@ public class FormBuilder {
(supportedFiles != null) ? Arrays.asList(supportedFiles) : Collections.emptyList());
}
public static DateTimeSelectorFieldBuilder dateTime(final String name, final LocTextKey label, final DateTime dateTime) {
return new DateTimeSelectorFieldBuilder(name, label, dateTime);
}
}

View file

@ -25,6 +25,7 @@ public final class ImageUploadFieldBuilder extends FieldBuilder<String> {
ImageUploadFieldBuilder(final String name, final LocTextKey label, final String value) {
super(name, label, value);
super.titleValign = SWT.TOP;
}
public ImageUploadFieldBuilder withMaxWidth(final int width) {

View file

@ -50,6 +50,7 @@ public final class SelectionFieldBuilder extends FieldBuilder<String> {
super(name, label, value);
this.type = type;
this.itemsSupplier = itemsSupplier;
super.titleValign = SWT.TOP;
}
public SelectionFieldBuilder withSelectionListener(final Consumer<Form> selectionListener) {

View file

@ -63,28 +63,34 @@ public final class TextFieldBuilder extends FieldBuilder<String> {
public TextFieldBuilder asArea(final int minHeight) {
this.areaMinHeight = minHeight;
super.titleValign = SWT.TOP;
return asArea();
}
public TextFieldBuilder asArea() {
this.isArea = true;
this.titleValign = SWT.CENTER;
this.titleValign = SWT.TOP;
return this;
}
public TextFieldBuilder asHTML() {
this.isHTML = true;
super.titleValign = SWT.TOP;
return this;
}
public TextFieldBuilder asHTML(final int minHeight) {
this.isHTML = true;
this.areaMinHeight = minHeight;
super.titleValign = SWT.TOP;
return this;
}
public FieldBuilder<?> asHTML(final boolean html) {
this.isHTML = html;
if (html) {
super.titleValign = SWT.TOP;
}
return this;
}

View file

@ -45,7 +45,7 @@ public interface I18nSupport {
/** Format a DateTime to a text format to display.
* This uses the date-format defined by either the attribute 'sebserver.gui.date.display format'
* or the Constants.DEFAULT_DISPLAY_DATE_FORMAT
*
* <p>
* Adds time-zone offset information if the currents user time-zone is different from UTC
*
* @param date the DateTime instance
@ -55,7 +55,7 @@ public interface I18nSupport {
/** Format a DateTime to a text format to display with additional time zone name at the end.
* This uses the date-format defined by either the attribute 'sebserver.gui.date.display format'
* or the Constants.DEFAULT_DISPLAY_DATE_FORMAT
*
* <p>
* Adds time-zone offset information if the currents user time-zone is different from UTC
*
* @param date the DateTime instance
@ -67,7 +67,7 @@ public interface I18nSupport {
/** Format a time-stamp (milliseconds) to a text format to display.
* This uses the date-format defined by either the attribute 'sebserver.gui.date.display format'
* or the Constants.DEFAULT_DISPLAY_DATE_FORMAT
*
* <p>
* Adds time-zone information if the currents user time-zone is different from UTC
*
* @param timestamp the unix-timestamp in milliseconds
@ -79,7 +79,7 @@ public interface I18nSupport {
/** Format a DateTime to a text format to display.
* This uses the date-format defined by either the attribute 'sebserver.gui.datetime.display format'
* or the Constants.DEFAULT_DISPLAY_DATE_TIME_FORMAT
*
* <p>
* Adds time-zone information if the currents user time-zone is different from UTC
*
* @param date the DateTime instance
@ -89,7 +89,7 @@ public interface I18nSupport {
/** Format a time-stamp (milliseconds) to a text format to display.
* This uses the date-format defined by either the attribute 'sebserver.gui.datetime.display format'
* or the Constants.DEFAULT_DISPLAY_DATE_TIME_FORMAT
*
* <p>
* Adds time-zone information if the currents user time-zone is different from UTC
*
* @param timestamp the unix-timestamp in milliseconds
@ -101,7 +101,7 @@ public interface I18nSupport {
/** Format a DateTime to a text format to display.
* This uses the date-format defined by either the attribute 'sebserver.gui.time.display format'
* or the Constants.DEFAULT_DISPLAY_TIME_FORMAT
*
* <p>
* Adds time-zone information if the currents user time-zone is different from UTC
*
* @param date the DateTime instance
@ -111,7 +111,7 @@ public interface I18nSupport {
/** Format a time-stamp (milliseconds) to a text format to display.
* This uses the date-format defined by either the attribute 'sebserver.gui.time.display format'
* or the Constants.DEFAULT_DISPLAY_TIME_FORMAT
*
* <p>
* Adds time-zone information if the currents user time-zone is different from UTC
*
* @param timestamp the unix-timestamp in milliseconds

View file

@ -44,6 +44,7 @@ public interface PageContext {
String ENTITY_LIST_TYPE = "ENTITY_TYPE";
String IMPORT_FROM_QUIZ_DATA = "IMPORT_FROM_QUIZ_DATA";
String NEW_EXAM_NO_LMS = "NEW_EXAM_NO_LMS";
String COPY_AS_TEMPLATE = "COPY_AS_TEMPLATE";
String CREATE_FROM_TEMPLATE = "CREATE_FROM_TEMPLATE";

View file

@ -17,6 +17,7 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import ch.ethz.seb.sebserver.gbl.FeatureService;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.graphics.Point;
@ -72,6 +73,8 @@ public interface PageService {
Logger log = LoggerFactory.getLogger(PageService.class);
FeatureService getFeatureService();
/** Get the WidgetFactory service
*
* @return the WidgetFactory service */

View file

@ -220,13 +220,20 @@ public final class PageAction {
}
return Result.ofError(restCallError);
} catch (final FormPostException e) {
if (e.getCause() instanceof RestCallError) {
final RestCallError cause = (RestCallError) e.getCause();
if (cause.isUnexpectedError()) {
log.error("Failed to execute action: {} | error: {} | cause: {}",
PageAction.this.getName(),
cause.getMessage(),
Utils.getErrorCauseMessage(cause));
}
return Result.ofError(cause);
}
log.error("Failed to execute action: {} | error: {} | cause: {}",
PageAction.this.getName(),
e.getMessage(),
Utils.getErrorCauseMessage(e));
if (e.getCause() instanceof RestCallError) {
return Result.ofError((RestCallError) e.getCause());
}
return Result.ofError(e);
} catch (final Exception e) {
log.error("Failed to execute action: {} | error: {} | cause: {}",

View file

@ -20,6 +20,7 @@ import java.util.function.Supplier;
import javax.servlet.http.HttpSession;
import ch.ethz.seb.sebserver.gbl.FeatureService;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.widgets.TreeItem;
import org.slf4j.Logger;
@ -88,6 +89,7 @@ public class PageServiceImpl implements PageService {
private final ResourceService resourceService;
private final CurrentUser currentUser;
private final ServerPushService serverPushService;
private final FeatureService featureService;
public PageServiceImpl(
final Cryptor cryptor,
@ -96,7 +98,8 @@ public class PageServiceImpl implements PageService {
final PolyglotPageService polyglotPageService,
final ResourceService resourceService,
final CurrentUser currentUser,
final ServerPushService serverPushService) {
final ServerPushService serverPushService,
final FeatureService featureService) {
this.cryptor = cryptor;
this.jsonMapper = jsonMapper;
@ -105,6 +108,12 @@ public class PageServiceImpl implements PageService {
this.resourceService = resourceService;
this.currentUser = currentUser;
this.serverPushService = serverPushService;
this.featureService = featureService;
}
@Override
public FeatureService getFeatureService() {
return featureService;
}
@Override

View file

@ -0,0 +1,29 @@
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import com.fasterxml.jackson.core.type.TypeReference;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
@Lazy
@Component
@GuiProfile
public class NewExam extends RestCall<Exam> {
public NewExam() {
super(new TypeKey<>(
CallType.NEW,
EntityType.EXAM,
new TypeReference<Exam>() {
}),
HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_ADMINISTRATION_ENDPOINT);
}
}

View file

@ -8,6 +8,7 @@
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.FormBinding;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;

View file

@ -0,0 +1,80 @@
package ch.ethz.seb.sebserver.gui.widget;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.DateTime;
import org.eclipse.swt.widgets.Label;
import org.joda.time.DateTimeZone;
public class DateTimeSelector extends Composite {
private DateTime date;
private DateTime time;
private Label timeZoneLabel;
private final DateTimeZone timeZone;
private final String testKey;
public DateTimeSelector(
final Composite parent,
final WidgetFactory widgetFactory,
final DateTimeZone timeZone,
final String label,
final String testKey) {
super(parent, SWT.NONE);
this.timeZone = timeZone;
this.testKey = testKey;
final GridLayout gridLayout = new GridLayout(3, false);
gridLayout.verticalSpacing = 5;
gridLayout.marginLeft = 0;
gridLayout.marginHeight = 0;
gridLayout.marginWidth = 0;
setLayout(gridLayout);
this.date = widgetFactory.dateSelector(this, new LocTextKey(label), testKey);
this.time = widgetFactory.timeSelector(this, new LocTextKey(label), testKey);
this.timeZoneLabel = widgetFactory.label(this, timeZone.getID());
this.setValue(Utils.getMillisecondsNow());
}
public void setValue(final long timestamp) {
setDateTime(org.joda.time.DateTime.now(this.timeZone));
}
public void setValue(final String dateTimeString) {
if (dateTimeString == null) {
setDateTime(org.joda.time.DateTime.now(this.timeZone));
return;
}
setDateTime(Utils.toDateTime(dateTimeString).withZone(this.timeZone));
}
public void setValue(final org.joda.time.DateTime time) {
if (time == null) {
setDateTime(org.joda.time.DateTime.now(this.timeZone));
return;
}
setDateTime(time.withZone(this.timeZone));
}
private void setDateTime(final org.joda.time.DateTime time) {
this.date.setDate(time.getYear(), time.getMonthOfYear() - 1, time.getDayOfMonth());
this.time.setTime(time.getHourOfDay(), time.getMinuteOfHour(), time.getSecondOfMinute());
}
public String getValue() {
return Utils.formatDate(org.joda.time.DateTime.now(this.timeZone)
.withYear(this.date.getYear())
.withMonthOfYear(this.date.getMonth() + 1)
.withDayOfMonth(this.date.getDay())
.withHourOfDay((this.time != null) ? this.time.getHours() : 0)
.withMinuteOfHour((this.time != null) ? this.time.getMinutes() : 0)
.withSecondOfMinute((this.time != null) ? this.time.getSeconds() : 0));
}
}

View file

@ -151,7 +151,8 @@ public class WidgetFactory {
NO_SHIELD("no_shield.png"),
BACK("back.png"),
SCREEN_PROC_ON("screen_proc_on.png"),
SCREEN_PROC_OFF("screen_proc_off.png");
SCREEN_PROC_OFF("screen_proc_off.png"),
ADD_EXAM("add_exam.png");
public String fileName;
private ImageData image = null;
@ -882,6 +883,7 @@ public class WidgetFactory {
public DateTime dateSelector(final Composite parent, final LocTextKey label, final String testKey) {
RWT.setLocale(this.i18nSupport.getUsersFormatLocale());
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
gridData.widthHint = 120;
final DateTime dateTime = new DateTime(parent, SWT.DATE | SWT.BORDER | SWT.DROP_DOWN);
dateTime.setLayoutData(gridData);

View file

@ -88,7 +88,12 @@ public class UserServiceImpl implements UserService {
if (UserService.USERS_INSTITUTION_AS_DEFAULT.equals(text)) {
setValue(getCurrentUser().institutionId());
} else {
setValue((text == null) ? null : Long.decode(text));
try {
setValue((text == null) ? null : Long.decode(text));
} catch (final Exception e) {
log.error("Failed to set institution from user: ", e);
setValue(-1);
}
}
}
};

View file

@ -117,7 +117,7 @@ public interface AdditionalAttributesDAO {
/** Use this to save an additional attributes for a specific entity.
* If an additional attribute with specified name already exists for the specified entity
* this updates just the value for this additional attribute. Otherwise create a new instance
* this updates just the value for this additional attribute. Otherwise, create a new instance
* of additional attribute with the given data
*
* @param type the entity type

View file

@ -300,6 +300,9 @@ public class ExamDAOImpl implements ExamDAO {
.where(
ExamRecordDynamicSqlSupport.active,
isEqualTo(BooleanUtils.toInteger(true)))
.and(
ExamRecordDynamicSqlSupport.lmsSetupId,
isNotNull())
.and(
ExamRecordDynamicSqlSupport.status,
isNotEqualTo(ExamStatus.ARCHIVED.name()))

View file

@ -294,7 +294,10 @@ public class ExamRecordDAO {
null, // active
exam.examTemplateId,
Utils.getMillisecondsNow(),
null, null, null, null);
exam.lmsSetupId == null ? exam.name : null,
exam.lmsSetupId == null ? exam.startTime : null,
exam.lmsSetupId == null ? exam.endTime : null,
null);
this.examRecordMapper.updateByPrimaryKeySelective(examRecord);
return this.examRecordMapper.selectByPrimaryKey(exam.id);

View file

@ -8,10 +8,15 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.exam;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.*;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.validation.FieldError;
@ -19,14 +24,9 @@ import org.springframework.validation.FieldError;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup;
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroupData;
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroupData.ClientGroupType;
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroupData.ClientOS;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.Threshold;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringService;
@ -90,7 +90,7 @@ public interface ExamAdminService {
/** This indicates if proctoring is set and enabled for a certain exam.
*
* @param examId the exam instance
* @param exam the exam instance
* @return proctoring is enabled flag */
default boolean isProctoringEnabled(final Exam exam) {
if (exam == null || exam.id == null) {
@ -107,7 +107,7 @@ public interface ExamAdminService {
/** This indicates if screen proctoring is set and enabled for a certain exam.
*
* @param examId the exam instance
* @param exam the exam instance
* @return screen proctoring is enabled flag */
default boolean isScreenProctoringEnabled(final Exam exam) {
if (exam == null || exam.id == null) {
@ -163,10 +163,58 @@ public interface ExamAdminService {
* @param exam the exam that has been changed and saved */
void notifyExamSaved(Exam exam);
static void newExamFieldValidation(final POSTMapper postParams) {
final Collection<APIMessage> validationErrors = new ArrayList<>();
if (!postParams.contains(Domain.EXAM.ATTR_QUIZ_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();
if (length < 3 || length > 255) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_NAME,
"exam:quizName:size:3:255:" + length));
}
}
if (!postParams.contains(QuizData.QUIZ_ATTR_START_URL)) {
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();
} catch (final Exception e) {
validationErrors.add(APIMessage.fieldValidationError(
QuizData.QUIZ_ATTR_START_URL,
"exam:quiz_start_url:invalidURL"));
}
}
if (!postParams.contains(Domain.EXAM.ATTR_QUIZ_START_TIME)) {
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))) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_END_TIME,
"exam:quizEndTime:endBeforeStart"));
}
}
if (!validationErrors.isEmpty()) {
throw new APIMessageException(validationErrors);
}
}
/** Used to check threshold consistency for a given list of thresholds.
* Checks if all values are present (none null value)
* Checks if there are duplicates
*
* <p>
* If a check fails, the methods throws a APIMessageException with a FieldError to notify the caller
*
* @param thresholds List of Threshold */

View file

@ -110,9 +110,9 @@ public class SEBClientEventCSVExporter implements SEBClientEventExporter {
builder.append(Constants.COMMA);
builder.append(eventData.getNumericValue() != null ? eventData.getNumericValue() : "");
builder.append(Constants.COMMA);
builder.append(Utils.formatDate(Utils.toDateTimeUTC(eventData.getClientTime())));
builder.append(Utils.formatDateWithMilliseconds(Utils.toDateTimeUTC(eventData.getClientTime())));
builder.append(Constants.COMMA);
builder.append(Utils.formatDate(Utils.toDateTimeUTC(eventData.getServerTime())));
builder.append(Utils.formatDateWithMilliseconds(Utils.toDateTimeUTC(eventData.getServerTime())));
if (connectionData != null) {
builder.append(Constants.COMMA);
@ -129,9 +129,9 @@ public class SEBClientEventCSVExporter implements SEBClientEventExporter {
builder.append(Constants.COMMA);
builder.append(examData.getType().name());
builder.append(Constants.COMMA);
builder.append(Utils.formatDate(examData.getStartTime()));
builder.append(Utils.formatDateWithMilliseconds(examData.getStartTime()));
builder.append(Constants.COMMA);
builder.append(Utils.formatDate(examData.getEndTime()));
builder.append(Utils.formatDateWithMilliseconds(examData.getEndTime()));
}
builder.append(Constants.CARRIAGE_RETURN);

View file

@ -242,17 +242,18 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
log.debug("ExamDeletionEvent received, process releaseSEBClientRestriction...");
}
event.ids.stream().forEach(examId -> {
this.examDAO
.byPK(examId)
.onSuccess(exam -> {
releaseSEBClientRestriction(exam)
.onError(error -> log.error(
"Failed to release SEB restrictions for finished exam: {}",
exam,
error));
});
});
event.ids.stream().forEach(this::processExamDeletion);
}
private Result<Exam> processExamDeletion(final Long examId) {
return this.examDAO
.byPK(examId)
.whenDo(
exam -> exam.lmsSetupId != null,
exam -> releaseSEBClientRestriction(exam).getOrThrow()
).onError(error -> log.error(
"Failed to release SEB restrictions for finished exam: {}",
examId, error));
}
@Override

View file

@ -135,7 +135,6 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
protected SqlTable getSQLTableOfEntity() {
return ExamRecordDynamicSqlSupport.examRecord;
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT,
@ -173,11 +172,10 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
defaultValue = "false") final boolean includeRestriction) {
checkReadPrivilege(institutionId);
final Collection<APIMessage> result = this.examSessionService
return this.examSessionService
.checkExamConsistency(modelId)
.getOrThrow();
return result;
}
@RequestMapping(
@ -596,15 +594,21 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
final SEBServerUser currentUser = this.authorization.getUserService().getCurrentUser();
postParams.putIfAbsent(EXAM.ATTR_OWNER, currentUser.uuid());
return this.lmsAPIService
.getLmsAPITemplate(lmsSetupId)
.map(template -> {
this.authorization.checkRead(template.lmsSetup());
return template;
})
.flatMap(template -> template.getQuiz(quizId))
.map(quiz -> new Exam(null, quiz, postParams))
.getOrThrow();
// NO LMS based exam is possible since v1.6
if (quizId == null) {
ExamAdminService.newExamFieldValidation(postParams);
return new Exam(postParams);
} else {
return this.lmsAPIService
.getLmsAPITemplate(lmsSetupId)
.map(template -> {
this.authorization.checkRead(template.lmsSetup());
return template;
})
.flatMap(template -> template.getQuiz(quizId))
.map(quiz -> new Exam(null, quiz, postParams))
.getOrThrow();
}
}
@Override
@ -764,11 +768,13 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
});
}
static Function<Collection<Exam>, List<Exam>> pageSort(final String sort) {
final String sortBy = PageSortOrder.decode(sort);
return exams -> {
final List<Exam> list = exams.stream().collect(Collectors.toList());
final List<Exam> list = new ArrayList<>(exams);
if (StringUtils.isBlank(sort)) {
return list;
}
@ -789,5 +795,4 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
return list;
};
}
}

View file

@ -15,7 +15,7 @@ sebserver.gui.list.page.size=15
sebserver.gui.multilingual=false
sebserver.gui.supported.languages=en,de
sebserver.gui.date.displayformat=de
sebserver.gui.date.displayformat=en
sebserver.gui.seb.client.config.download.filename=SEBServerSettings.seb
sebserver.gui.seb.exam.config.download.filename=SEBExamSettings.seb

View file

@ -0,0 +1,4 @@
-- -----------------------------------------------------
-- Alter Table `exam`
-- -----------------------------------------------------
ALTER TABLE `exam` MODIFY `lms_setup_id` BIGINT UNSIGNED NULL;

View file

@ -108,21 +108,17 @@ sebserver.form.validation.fieldError.invalidURL=The input does not match the URL
sebserver.form.validation.fieldError.exists=This name already exists. Please choose another one
sebserver.form.validation.fieldError.email=Invalid mail address
sebserver.form.validation.fieldError.serverNotAvailable=No service seems to be available within the given URL
<<<<<<< HEAD
sebserver.form.validation.fieldError.url.invalid=Invalid URL. The given URL cannot be reached
sebserver.form.validation.fieldError.typeInvalid=This type is not implemented yet and cannot be used
sebserver.form.validation.fieldError.url.noservice=The expected service is not available within the given URL and API access
=======
sebserver.form.validation.fieldError.url.invalid=Invalid URL. The given URL cannot be reached.
sebserver.form.validation.fieldError.typeInvalid=This type is not implemented yet and cannot be used.
sebserver.form.validation.fieldError.url.noservice=The expected service is not available within the given URL and API access.
sebserver.form.validation.fieldError.url.noaccess=There has no access been granted by the service. Please check the given access credentials.
>>>>>>> refs/remotes/origin/rel-1.5.2
sebserver.form.validation.fieldError.thresholdDuplicate=There are duplicate threshold values.
sebserver.form.validation.fieldError.thresholdEmpty=There are missing values or colors for the threshold declaration
sebserver.form.validation.fieldError.invalidIP=Invalid IP v4. Please enter a valid IP-address (v4)
sebserver.form.validation.fieldError.invalidIPRange=Invalid IP-address range
sebserver.form.validation.fieldError.url.noAccess=Access was denied
sebserver.form.validation.fieldError.invalidDateRange=Invalid Date Range
sebserver.form.validation.fieldError.endBeforeStart=Invalid Date Range, End before Start
sebserver.error.unexpected=Unexpected Error
sebserver.page.message=Information
sebserver.dialog.confirm.title=Confirmation
@ -552,6 +548,7 @@ sebserver.exam.action.list.hide.missing=Hide Missing Exams
sebserver.exam.action.list.show.missing=Show Missing Exams
sebserver.exam.action.modify=Edit Exam
sebserver.exam.action.import=Import From Quizzes
sebserver.exam.action.new=Add Exam Without LMS
sebserver.exam.action.save=Save Exam
sebserver.exam.action.activate=Activate Exam
sebserver.exam.action.deactivate=Deactivate Exam
@ -572,10 +569,10 @@ sebserver.exam.form.title=Exam
sebserver.exam.form.title.subtitle=
sebserver.exam.form.lmssetup=LMS Setup
sebserver.exam.form.lmssetup.tooltip=The LMS setup that defines the LMS of the exam
sebserver.exam.form.quizid=LMS exam Identifier
sebserver.exam.form.quizid=Identifier
sebserver.exam.form.quizid.tooltip=The identifier that identifies the quiz of the exam on the corresponding LMS
sebserver.exam.form.quizurl=LMS exam URL
sebserver.exam.form.quizurl.tooltip=The direct URL link to the LMS exam
sebserver.exam.form.quizurl=Exam URL
sebserver.exam.form.quizurl.tooltip=The direct URL link to the exam
sebserver.exam.form.name=Name
sebserver.exam.form.name.tooltip=The name of the exam.<br/><br/>This name is defined on the corresponding LMS
sebserver.exam.form.description=Description

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

View file

@ -56,7 +56,7 @@ DROP TABLE IF EXISTS `exam` ;
CREATE TABLE IF NOT EXISTS `exam` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`institution_id` BIGINT UNSIGNED NOT NULL,
`lms_setup_id` BIGINT UNSIGNED NOT NULL,
`lms_setup_id` BIGINT UNSIGNED NULL,
`external_id` VARCHAR(255) NOT NULL,
`owner` VARCHAR(255) NOT NULL,
`supporter` VARCHAR(4000) NULL COMMENT 'comma separated list of user_uuid',