From 0544d7a799d8a5635b5be5786d6c9ba719487942 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 14 Dec 2023 08:19:33 +0100 Subject: [PATCH] SEBSERV-457 implementation --- .../ch/ethz/seb/sebserver/gbl/api/API.java | 1 + .../seb/sebserver/gbl/api/JSONMapper.java | 15 + .../seb/sebserver/gbl/model/exam/Exam.java | 77 ++-- .../sebserver/gbl/model/exam/QuizData.java | 8 +- .../model/user/ExamineeAccountDetails.java | 6 +- .../ethz/seb/sebserver/gbl/util/Result.java | 8 + .../ch/ethz/seb/sebserver/gbl/util/Utils.java | 9 +- .../sebserver/gui/content/exam/ExamForm.java | 428 +++++++++++------- .../sebserver/gui/content/exam/ExamList.java | 18 +- .../form/DateTimeSelectorFieldBuilder.java | 44 ++ .../seb/sebserver/gui/form/FieldBuilder.java | 5 +- .../ch/ethz/seb/sebserver/gui/form/Form.java | 59 ++- .../seb/sebserver/gui/form/FormBuilder.java | 10 + .../gui/form/ImageUploadFieldBuilder.java | 1 + .../gui/form/SelectionFieldBuilder.java | 1 + .../sebserver/gui/form/TextFieldBuilder.java | 8 +- .../gui/service/i18n/I18nSupport.java | 14 +- .../gui/service/page/PageContext.java | 1 + .../gui/service/page/PageService.java | 3 + .../gui/service/page/impl/PageAction.java | 13 +- .../service/page/impl/PageServiceImpl.java | 11 +- .../remote/webservice/api/exam/NewExam.java | 29 ++ .../remote/webservice/api/exam/SaveExam.java | 1 + .../gui/widget/DateTimeSelector.java | 80 ++++ .../sebserver/gui/widget/WidgetFactory.java | 4 +- .../authorization/impl/UserServiceImpl.java | 7 +- .../dao/AdditionalAttributesDAO.java | 2 +- .../servicelayer/dao/impl/ExamDAOImpl.java | 3 + .../servicelayer/dao/impl/ExamRecordDAO.java | 5 +- .../servicelayer/exam/ExamAdminService.java | 64 ++- .../exam/impl/SEBClientEventCSVExporter.java | 8 +- .../lms/impl/SEBRestrictionServiceImpl.java | 23 +- .../api/ExamAdministrationController.java | 35 +- .../config/application-dev-gui.properties | 2 +- .../sql/base/V26__exam_lms_nullable_v1_6.sql | 4 + src/main/resources/messages.properties | 15 +- src/main/resources/static/images/add_exam.png | Bin 0 -> 190 bytes src/test/resources/schema-test.sql | 2 +- 38 files changed, 725 insertions(+), 299 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/form/DateTimeSelectorFieldBuilder.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/NewExam.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/widget/DateTimeSelector.java create mode 100644 src/main/resources/config/sql/base/V26__exam_lms_nullable_v1_6.sql create mode 100644 src/main/resources/static/images/add_exam.png diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 2f0ed7dd..3f824401 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -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); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/JSONMapper.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/JSONMapper.java index 3f9d03ba..76f1550c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/JSONMapper.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/JSONMapper.java @@ -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; + } + } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java index 8f7e10ce..d4cefce4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java @@ -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 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 additionalAttributes) { + @JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES) final Map 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 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 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 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 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) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/QuizData.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/QuizData.java index 37303667..d4838135 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/QuizData.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/QuizData.java @@ -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 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 additionalAttributes) { + @JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES) final Map additionalAttributes) { this.id = id; this.institutionId = institutionId; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/ExamineeAccountDetails.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/ExamineeAccountDetails.java index ea1d333d..2cec56de 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/ExamineeAccountDetails.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/ExamineeAccountDetails.java @@ -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 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 additionalAttributes) { + @JsonProperty(API.PARAM_ADDITIONAL_ATTRIBUTES) final Map additionalAttributes) { this.id = id; this.name = name; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java index 85236ad6..d114bfa7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java @@ -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 { } } + public Result whenDo(final Predicate predicate, final Function handler) { + if (this.error == null && predicate.test(this.value)) { + return Result.tryCatch(() -> handler.apply(this.value)); + } + return this; + } + public Result onSuccess(final Consumer handler) { if (this.error == null) { handler.accept(this.value); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java index af87fc40..5f558cdc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java @@ -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); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java index feb2d947..2ba377c1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java @@ -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 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 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 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 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; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamList.java index bc693d15..4c3f7550 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamList.java @@ -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 table) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/DateTimeSelectorFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/DateTimeSelectorFieldBuilder.java new file mode 100644 index 00000000..e43711c5 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/DateTimeSelectorFieldBuilder.java @@ -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 { + + 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); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java index e196febb..6826a305 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java @@ -31,7 +31,7 @@ public abstract class FieldBuilder { 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 { 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 { 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); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java index af133611..5502158b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java @@ -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 staticValues = new LinkedHashMap<>(); + private final Map additionalAttributeMapping = new LinkedHashMap<>(); private final MultiValueMap formFields = new LinkedMultiValueMap<>(); private final Map> 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 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 additionalAttrs = new HashMap<>(); + for (final Map.Entry 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 /* diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java index 3b162147..262f14b6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java @@ -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> 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); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/ImageUploadFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/ImageUploadFieldBuilder.java index ce43b97b..9e2b89b2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/ImageUploadFieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/ImageUploadFieldBuilder.java @@ -25,6 +25,7 @@ public final class ImageUploadFieldBuilder extends FieldBuilder { ImageUploadFieldBuilder(final String name, final LocTextKey label, final String value) { super(name, label, value); + super.titleValign = SWT.TOP; } public ImageUploadFieldBuilder withMaxWidth(final int width) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/SelectionFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/SelectionFieldBuilder.java index f2120b0c..fdb121e4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/SelectionFieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/SelectionFieldBuilder.java @@ -50,6 +50,7 @@ public final class SelectionFieldBuilder extends FieldBuilder { super(name, label, value); this.type = type; this.itemsSupplier = itemsSupplier; + super.titleValign = SWT.TOP; } public SelectionFieldBuilder withSelectionListener(final Consumer
selectionListener) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java index 6ec5b91b..c269ae97 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java @@ -63,28 +63,34 @@ public final class TextFieldBuilder extends FieldBuilder { 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; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/I18nSupport.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/I18nSupport.java index cfee7c7e..ca60505e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/I18nSupport.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/I18nSupport.java @@ -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 - * + *

* 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 - * + *

* 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 - * + *

* 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 - * + *

* 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 - * + *

* 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 - * + *

* 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 - * + *

* Adds time-zone information if the currents user time-zone is different from UTC * * @param timestamp the unix-timestamp in milliseconds diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java index 1c9a24a2..d6a461cd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java @@ -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"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java index 319a0161..be586fab 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java @@ -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 */ diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java index 03149686..abf8c57b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java @@ -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: {}", diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java index dbf68193..46305862 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java @@ -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 diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/NewExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/NewExam.java new file mode 100644 index 00000000..b1979649 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/NewExam.java @@ -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 { + + public NewExam() { + super(new TypeKey<>( + CallType.NEW, + EntityType.EXAM, + new TypeReference() { + }), + HttpMethod.POST, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_ADMINISTRATION_ENDPOINT); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/SaveExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/SaveExam.java index 4a1fcf02..4979c3cb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/SaveExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/SaveExam.java @@ -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; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/DateTimeSelector.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/DateTimeSelector.java new file mode 100644 index 00000000..efa81093 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/DateTimeSelector.java @@ -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)); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java index f1364f33..33c16aac 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java @@ -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); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/UserServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/UserServiceImpl.java index 812d18b3..af73be38 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/UserServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/UserServiceImpl.java @@ -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); + } } } }; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/AdditionalAttributesDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/AdditionalAttributesDAO.java index 60995295..41c1ce35 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/AdditionalAttributesDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/AdditionalAttributesDAO.java @@ -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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java index c983c656..161313e3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java @@ -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())) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java index b583c779..23682e39 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java @@ -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); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java index dc078cdf..43d5834f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java @@ -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 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 - * + *

* If a check fails, the methods throws a APIMessageException with a FieldError to notify the caller * * @param thresholds List of Threshold */ diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventCSVExporter.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventCSVExporter.java index 7a38f8b6..48f91b38 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventCSVExporter.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventCSVExporter.java @@ -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); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java index 07b513a2..4cf2b7e8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java @@ -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 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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index ff01c929..3076a7a7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -135,7 +135,6 @@ public class ExamAdministrationController extends EntityController { 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 { defaultValue = "false") final boolean includeRestriction) { checkReadPrivilege(institutionId); - final Collection result = this.examSessionService + + return this.examSessionService .checkExamConsistency(modelId) .getOrThrow(); - - return result; } @RequestMapping( @@ -596,15 +594,21 @@ public class ExamAdministrationController extends EntityController { 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 { }); } + + static Function, List> pageSort(final String sort) { final String sortBy = PageSortOrder.decode(sort); return exams -> { - final List list = exams.stream().collect(Collectors.toList()); + final List list = new ArrayList<>(exams); if (StringUtils.isBlank(sort)) { return list; } @@ -789,5 +795,4 @@ public class ExamAdministrationController extends EntityController { return list; }; } - } diff --git a/src/main/resources/config/application-dev-gui.properties b/src/main/resources/config/application-dev-gui.properties index 8069b264..58cfaa5b 100644 --- a/src/main/resources/config/application-dev-gui.properties +++ b/src/main/resources/config/application-dev-gui.properties @@ -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 \ No newline at end of file diff --git a/src/main/resources/config/sql/base/V26__exam_lms_nullable_v1_6.sql b/src/main/resources/config/sql/base/V26__exam_lms_nullable_v1_6.sql new file mode 100644 index 00000000..7c76f6ed --- /dev/null +++ b/src/main/resources/config/sql/base/V26__exam_lms_nullable_v1_6.sql @@ -0,0 +1,4 @@ +-- ----------------------------------------------------- +-- Alter Table `exam` +-- ----------------------------------------------------- +ALTER TABLE `exam` MODIFY `lms_setup_id` BIGINT UNSIGNED NULL; \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 36f94456..0bc88f41 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -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.

This name is defined on the corresponding LMS sebserver.exam.form.description=Description diff --git a/src/main/resources/static/images/add_exam.png b/src/main/resources/static/images/add_exam.png new file mode 100644 index 0000000000000000000000000000000000000000..97302523602a497e9bb572e207f40eeee48a1164 GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+iR!KDqOp;#m1#LV)EP^UGkKOEXRtW==@k!J6`y;n8s(ts^^S z7qwJWU{u>2s)R^#x|A`=rKmdKI;Vst09ltsxc~qF literal 0 HcmV?d00001 diff --git a/src/test/resources/schema-test.sql b/src/test/resources/schema-test.sql index d590fa62..12a844b4 100644 --- a/src/test/resources/schema-test.sql +++ b/src/test/resources/schema-test.sql @@ -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',