diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java index 314e9a62..98fff84d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java @@ -8,13 +8,10 @@ package ch.ethz.seb.sebserver.gbl.api; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -38,23 +35,9 @@ public class POSTMapper { public POSTMapper(final MultiValueMap params) { super(); - this.params = new LinkedMultiValueMap<>(); - if (params != null) { - for (final Map.Entry> entry : params.entrySet()) { - this.params.put( - entry.getKey(), - entry.getValue() - .stream() - .map(encoded -> { - try { - return URLDecoder.decode(encoded, "UTF-8"); - } catch (final UnsupportedEncodingException e) { - return encoded; - } - }) - .collect(Collectors.toList())); - } - } + this.params = params != null + ? new LinkedMultiValueMap<>(params) + : new LinkedMultiValueMap<>(); } public String getString(final String name) { 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 47cf0309..1750e26a 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 @@ -14,6 +14,7 @@ import java.util.Collections; import javax.validation.constraints.NotNull; import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -27,15 +28,15 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity; public final class Exam implements GrantEntity, Activatable { + public static final String ATTR_STATUS = "examStatus"; public static final String FILTER_ATTR_TYPE = "type"; public static final String FILTER_ATTR_FROM = "from"; -// public enum ExamStatus { -// ON_CREATION, -// READY, -// RUNNING, -// FINISHED -// } + public enum ExamStatus { + UP_COMING, + RUNNING, + FINISHED + } public enum ExamType { UNDEFINED, @@ -82,7 +83,6 @@ public final class Exam implements GrantEntity, Activatable { public final String quitPassword; @JsonProperty(EXAM.ATTR_OWNER) - @NotNull public final String owner; @JsonProperty(EXAM.ATTR_SUPPORTER) @@ -221,6 +221,24 @@ public final class Exam implements GrantEntity, Activatable { return this.quitPassword; } + @JsonIgnore + public ExamStatus getStatus() { + if (this.startTime == null) { + return ExamStatus.UP_COMING; + } + + final DateTime now = DateTime.now(DateTimeZone.UTC); + if (this.startTime.isBefore(now)) { + if (this.endTime == null || this.endTime.isAfter(now)) { + return ExamStatus.RUNNING; + } else { + return ExamStatus.FINISHED; + } + } + + return ExamStatus.UP_COMING; + } + @Override public boolean isActive() { return this.active; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java index cd644855..f6de331c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java @@ -23,6 +23,7 @@ import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; @@ -40,10 +41,7 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; -import ch.ethz.seb.sebserver.gui.service.page.impl.PageUtils; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.ActivateExam; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeactivateExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteIndicator; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicators; @@ -128,8 +126,9 @@ public class ExamForm implements TemplateComposer { final BooleanSupplier isNew = () -> importFromQuizData; final BooleanSupplier isNotNew = () -> !isNew.getAsBoolean(); final EntityGrantCheck userGrantCheck = currentUser.entityGrantCheck(exam); - final boolean writeGrant = userGrantCheck.w(); final boolean modifyGrant = userGrantCheck.m(); + final ExamStatus examStatus = exam.getStatus(); + final boolean editable = examStatus == ExamStatus.UP_COMING; // The Exam form final FormHandle formHandle = this.pageService.formBuilder( @@ -191,6 +190,11 @@ public class ExamForm implements TemplateComposer { "sebserver.exam.form.type", String.valueOf(exam.type), this.resourceService::examTypeResources)) + .addField(FormBuilder.text( + Exam.ATTR_STATUS, + "sebserver.exam.form.status", + i18nSupport.getText(new LocTextKey("sebserver.exam.status." + examStatus.name()))) + .readonly(true)) .addField(FormBuilder.multiComboSelection( Domain.EXAM.ATTR_SUPPORTER, "sebserver.exam.form.supporter", @@ -210,7 +214,7 @@ public class ExamForm implements TemplateComposer { .newAction(ActionDefinition.EXAM_MODIFY) .withEntityKey(entityKey) - .publishIf(() -> modifyGrant && readonly) + .publishIf(() -> modifyGrant && readonly && editable) .newAction(ActionDefinition.EXAM_SAVE) .withExec(formHandle::processFormSave) @@ -221,18 +225,18 @@ public class ExamForm implements TemplateComposer { .withEntityKey(entityKey) .withAttribute(AttributeKeys.IMPORT_FROM_QUIZZ_DATA, String.valueOf(importFromQuizData)) .withExec(this::cancelModify) - .publishIf(() -> !readonly) + .publishIf(() -> !readonly); - .newAction(ActionDefinition.EXAM_DEACTIVATE) - .withEntityKey(entityKey) - .withSimpleRestCall(restService, DeactivateExam.class) - .withConfirm(PageUtils.confirmDeactivation(exam, restService)) - .publishIf(() -> writeGrant && readonly && exam.isActive()) - - .newAction(ActionDefinition.EXAM_ACTIVATE) - .withEntityKey(entityKey) - .withSimpleRestCall(restService, ActivateExam.class) - .publishIf(() -> writeGrant && readonly && !exam.isActive()); +// .newAction(ActionDefinition.EXAM_DEACTIVATE) +// .withEntityKey(entityKey) +// .withSimpleRestCall(restService, DeactivateExam.class) +// .withConfirm(PageUtils.confirmDeactivation(exam, restService)) +// .publishIf(() -> writeGrant && readonly && exam.isActive()) +// +// .newAction(ActionDefinition.EXAM_ACTIVATE) +// .withEntityKey(entityKey) +// .withSimpleRestCall(restService, ActivateExam.class) +// .publishIf(() -> writeGrant && readonly && !exam.isActive()); // additional data in read-only view if (readonly) { @@ -274,17 +278,17 @@ public class ExamForm implements TemplateComposer { .newAction(ActionDefinition.EXAM_INDICATOR_NEW) .withParentEntityKey(entityKey) - .publishIf(() -> modifyGrant) + .publishIf(() -> modifyGrant && editable) .newAction(ActionDefinition.EXAM_INDICATOR_MODIFY_FROM_LIST) .withParentEntityKey(entityKey) .withSelect(indicatorTable::getSelection, PageAction::applySingleSelection, emptySelectionTextKey) - .publishIf(() -> modifyGrant && indicatorTable.hasAnyContent()) + .publishIf(() -> modifyGrant && indicatorTable.hasAnyContent() && editable) .newAction(ActionDefinition.EXAM_INDICATOR_DELETE_FROM_LIST) .withEntityKey(entityKey) .withSelect(indicatorTable::getSelection, this::deleteSelectedIndicator, emptySelectionTextKey) - .publishIf(() -> modifyGrant && indicatorTable.hasAnyContent()); + .publishIf(() -> modifyGrant && indicatorTable.hasAnyContent() && editable); // TODO List of attached SEB Configurations diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/QuizDiscoveryList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/QuizDiscoveryList.java index 3f852e53..53850e23 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/QuizDiscoveryList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/QuizDiscoveryList.java @@ -11,6 +11,8 @@ package ch.ethz.seb.sebserver.gui.content; import java.util.function.Function; import org.eclipse.swt.widgets.Composite; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @@ -27,6 +29,7 @@ import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys; +import ch.ethz.seb.sebserver.gui.service.page.PageMessageException; import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; @@ -60,6 +63,8 @@ public class QuizDiscoveryList implements TemplateComposer { new LocTextKey("sebserver.quizdiscovery.list.column.name"); private final static LocTextKey DETAILS_TITLE_TEXT_KEY = new LocTextKey("sebserver.quizdiscovery.quiz.details.title"); + private final static LocTextKey NO_IMPORT_OF_OUT_DATED_QUIZ = + new LocTextKey("sebserver.quizdiscovery.quiz.import.out.dated"); // filter attribute models private final TableFilterAttribute lmsFilter; @@ -174,6 +179,13 @@ public class QuizDiscoveryList implements TemplateComposer { private PageAction importQuizData(final PageAction action, final EntityTable table) { final QuizData selectedROWData = table.getSelectedROWData(); + if (selectedROWData.endTime != null) { + final DateTime now = DateTime.now(DateTimeZone.UTC); + if (selectedROWData.endTime.isBefore(now)) { + throw new PageMessageException(NO_IMPORT_OF_OUT_DATED_QUIZ); + } + } + return action .withEntityKey(action.getSingleSelection()) .withParentEntityKey(new EntityKey(selectedROWData.lmsSetupId, EntityType.LMS_SETUP)) @@ -196,7 +208,8 @@ public class QuizDiscoveryList implements TemplateComposer { } private void createDetailsForm(final QuizData quizData, final PageContext pc) { - this.pageService.formBuilder(pc, 4) + this.pageService.formBuilder(pc, 3) + .withEmptyCellSeparation(false) .readonly(true) .addField(FormBuilder.singleSelection( QuizData.QUIZ_ATTR_LMS_SETUP_ID, 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 cef06823..40141fa1 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,6 +8,8 @@ package ch.ethz.seb.sebserver.gui.form; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; @@ -266,11 +268,11 @@ public final class Form implements FormBinding { final String[] nameValue = StringUtils.split(split[i], Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR); buffer.append(nameValue[0]) .append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR) - .append(nameValue[1]); + .append(URLEncoder.encode(nameValue[1], StandardCharsets.UTF_8)); } else { buffer.append(name) .append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR) - .append(split[i]); + .append(URLEncoder.encode(split[i], StandardCharsets.UTF_8)); } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java index 63bf0381..35c57745 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java @@ -130,7 +130,7 @@ public class ModalInputDialog extends Dialog { shell.pack(); final Rectangle bounds = shell.getBounds(); final Rectangle bounds2 = super.getParent().getDisplay().getBounds(); - bounds.width = 400; + //bounds.width = bounds.width; bounds.x = (bounds2.width - bounds.width) / 2; bounds.y = (bounds2.height - bounds.height) / 2; shell.setBounds(bounds); 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 9715bdb0..62a54972 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 @@ -15,6 +15,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; @@ -199,7 +200,7 @@ public class ExamDAOImpl implements ExamDAO { null, (exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(), null, - BooleanUtils.toInteger(false)); + BooleanUtils.toInteger(true)); this.examRecordMapper.insert(examRecord); return examRecord; @@ -353,29 +354,35 @@ public class ExamDAOImpl implements ExamDAO { private Result> toDomainModel(final Long lmsSetupId, final Collection records) { return Result.tryCatch(() -> { - final HashMap recordMapping = records - .stream() - .reduce(new HashMap(), - (map, record) -> Utils.mapPut(map, record.getExternalId(), record), - (map1, map2) -> Utils.mapPutAll(map1, map2)); - return this.lmsAPIService + // map records + final Map recordMapping = records + .stream() + .collect(Collectors.toMap(r -> r.getExternalId(), Function.identity())); + + // get and map quizzes + final Map quizzes = this.lmsAPIService .getLmsAPITemplate(lmsSetupId) .map(template -> template.getQuizzes(recordMapping.keySet())) .getOrThrow() .stream() - .map(result -> result.flatMap(quiz -> toDomainModel(recordMapping, quiz)).getOrThrow()) + .flatMap(Result::skipOnError) + .collect(Collectors.toMap(q -> q.id, Function.identity())); + + // collect Exam's + return recordMapping.entrySet() + .stream() + .map(entry -> toDomainModel(entry.getValue(), quizzes.get(entry.getKey())).getOrThrow()) .collect(Collectors.toList()); }); } private Result toDomainModel( - final HashMap recordMapping, + final ExamRecord record, final QuizData quizData) { return Result.tryCatch(() -> { - final ExamRecord record = recordMapping.get(quizData.id); final Collection supporter = (StringUtils.isNoneBlank(record.getSupporter())) ? Arrays.asList(StringUtils.split(record.getSupporter(), Constants.LIST_SEPARATOR_CHAR)) : null; @@ -384,17 +391,17 @@ public class ExamDAOImpl implements ExamDAO { record.getId(), record.getInstitutionId(), record.getLmsSetupId(), - quizData.id, - quizData.name, - quizData.description, - quizData.startTime, - quizData.endTime, - quizData.startURL, + record.getExternalId(), + (quizData != null) ? quizData.name : Constants.EMPTY_NOTE, + (quizData != null) ? quizData.description : Constants.EMPTY_NOTE, + (quizData != null) ? quizData.startTime : null, + (quizData != null) ? quizData.endTime : null, + (quizData != null) ? quizData.startURL : Constants.EMPTY_NOTE, ExamType.valueOf(record.getType()), record.getQuitPassword(), record.getOwner(), supporter, - BooleanUtils.toBooleanObject(record.getActive())); + BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : 0)); }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java index 9508a5a0..d15c88e1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java @@ -16,7 +16,6 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; @@ -64,12 +63,12 @@ public interface LmsAPIService { public static Predicate quizFilterFunction(final FilterMap filterMap) { final String name = filterMap.getQuizName(); final DateTime from = filterMap.getQuizFromTime(); - final DateTime now = DateTime.now(DateTimeZone.UTC); + //final DateTime now = DateTime.now(DateTimeZone.UTC); return q -> { final boolean nameFilter = StringUtils.isBlank(name) || (q.name != null && q.name.contains(name)); final boolean startTimeFilter = (from == null) || (q.startTime != null && q.startTime.isAfter(from)); - final boolean endTimeFilter = (now == null) || (q.endTime != null && q.endTime.isAfter(now)); - return nameFilter && startTimeFilter && endTimeFilter; +// final boolean endTimeFilter = (now == null) || (q.endTime != null && q.endTime.isAfter(now)); + return nameFilter && startTimeFilter /* && endTimeFilter */; }; } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/OpenEdxLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/OpenEdxLmsAPITemplate.java index 3580d3fc..95bd8a9c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/OpenEdxLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/OpenEdxLmsAPITemplate.java @@ -11,11 +11,13 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -93,7 +95,7 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate { this.knownTokenAccessPaths.addAll(Arrays.asList(alternativeTokenRequestPaths)); } - this.allQuizzesSupplier = asyncService.createMemoizingCircuitBreaker(allQuizzesSupplier(lmsSetup)); + this.allQuizzesSupplier = asyncService.createMemoizingCircuitBreaker(allQuizzesSupplier()); } @Override @@ -143,8 +145,13 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate { @Override public Collection> getQuizzes(final Set ids) { - // TODO Auto-generated method stub - return null; + // TODO this can be improved in the future + return getQuizzes(new FilterMap()) + .getOrElse(() -> Collections.emptyList()) + .stream() + .filter(quiz -> ids.contains(quiz.id)) + .map(quiz -> Result.of(quiz)) + .collect(Collectors.toList()); } private Result initRestTemplateAndRequestAccessToken() { @@ -209,7 +216,7 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate { return template; } - private Supplier> allQuizzesSupplier(final LmsSetup lmsSetup) { + private Supplier> allQuizzesSupplier() { return () -> { return initRestTemplateAndRequestAccessToken() .map(this::collectAllQuizzes) diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 2db78ae4..8deadddc 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -210,6 +210,7 @@ sebserver.quizdiscovery.info.pleaseSelect=Please Select a Quiz first sebserver.quizdiscovery.action.list=Quiz Discovery sebserver.quizdiscovery.action.import=Import as Exam +sebserver.quizdiscovery.quiz.import.out.dated=The Selected Quiz is in the past and can't be imported sebserver.quizdiscovery.action.details=Show Details sebserver.quizdiscovery.quiz.details.title=Quiz Details @@ -262,6 +263,10 @@ sebserver.exam.type.MANAGED=Managed sebserver.exam.type.BYOD=Bring Your Own Device sebserver.exam.type.VDI=VDI (Virtual Desktop Infrastructure) +sebserver.exam.status.UP_COMING=Up Coming +sebserver.exam.status.RUNNING=Running +sebserver.exam.status.FINISHED=Finished + sebserver.exam.indicator.list.actions=Selected Indicator sebserver.exam.indicator.list.title=Indicators sebserver.exam.indicator.list.column.type=Type diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/QuizDataTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/QuizDataTest.java index bc797b1e..52ff1175 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/QuizDataTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/QuizDataTest.java @@ -59,7 +59,7 @@ public class QuizDataTest extends AdministrationAPIIntegrationTester { }); assertNotNull(quizzes); - assertTrue(quizzes.content.size() == 5); + assertTrue(quizzes.content.size() == 7); // for the inactive LmsSetup we should'nt get any quizzes quizzes = new RestAPITestHelper() @@ -110,7 +110,7 @@ public class QuizDataTest extends AdministrationAPIIntegrationTester { }); assertNotNull(quizzes); - assertTrue(quizzes.content.size() == 5); + assertTrue(quizzes.content.size() == 7); } @Test