From 7169fa0808f41a1da773487fb7871b930436f665 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 15 Dec 2022 14:35:27 +0100 Subject: [PATCH] SEBSERV-338 and SEBSERV-301 --- .../seb/sebserver/gbl/async/AsyncRunner.java | 7 +- .../seb/sebserver/gbl/async/AsyncService.java | 4 + .../ch/ethz/seb/sebserver/gbl/model/Page.java | 25 +- .../sebserver/gbl/model/exam/QuizData.java | 55 +-- .../ch/ethz/seb/sebserver/gbl/util/Utils.java | 30 ++ .../gui/content/exam/QuizLookupList.java | 46 +++ .../gui/service/page/PageService.java | 3 + .../service/page/impl/PageServiceImpl.java | 11 +- .../gui/service/push/ServerPushContext.java | 4 + .../seb/sebserver/gui/table/EntityTable.java | 22 +- .../seb/sebserver/gui/table/TableBuilder.java | 9 +- .../sebserver/gui/table/TableNavigator.java | 8 +- .../sebserver/gui/widget/WidgetFactory.java | 7 +- .../servicelayer/lms/CourseAccessAPI.java | 25 +- .../servicelayer/lms/LmsAPIService.java | 66 +--- .../servicelayer/lms/QuizLookupService.java | 129 +++++++ .../lms/impl/LmsAPIServiceImpl.java | 96 +++-- .../lms/impl/LmsAPITemplateAdapter.java | 52 +-- .../lms/impl/QuizLookupServiceImpl.java | 347 ++++++++++++++++++ .../lms/impl/ans/AnsLmsAPITemplate.java | 13 + .../lms/impl/edx/OpenEdxCourseAccess.java | 52 +++ .../lms/impl/mockup/MockCourseAccessAPI.java | 38 ++ .../moodle/legacy/MoodleCourseAccess.java | 339 ++++++++++++++--- .../legacy/MoodleCourseDataAsyncLoader.java | 53 +-- .../legacy/MoodleLmsAPITemplateFactory.java | 1 + .../plugin/MoodlePluginCourseAccess.java | 6 + .../MooldePluginLmsAPITemplateFactory.java | 7 - .../lms/impl/olat/OlatLmsAPITemplate.java | 13 + .../config/application-dev.properties | 1 + src/main/resources/messages.properties | 1 + .../gbl/async/AsyncBlockingTest.java | 75 ++++ .../model/institution/InstitutionTest.java | 2 +- .../gbl/model/user/UserInfoTest.java | 2 +- .../integration/UseCasesIntegrationTest.java | 9 + .../moodle/legacy/MoodleCourseAccessTest.java | 8 + 35 files changed, 1250 insertions(+), 316 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/QuizLookupService.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/QuizLookupServiceImpl.java create mode 100644 src/test/java/ch/ethz/seb/sebserver/gbl/async/AsyncBlockingTest.java diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncRunner.java b/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncRunner.java index 4b7ebda6..3946fa6b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncRunner.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncRunner.java @@ -22,7 +22,8 @@ import org.springframework.stereotype.Component; public class AsyncRunner { /** Calls a given Supplier asynchronously in a new thread and returns a CompletableFuture - * to get and handle the result later + * to get and handle the result later. + * * * @param supplier The Supplier that gets called asynchronously * @return CompletableFuture of the result of the Supplier */ @@ -32,8 +33,8 @@ public class AsyncRunner { } @Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) - public void runAsync(final Runnable block) { - block.run(); + public void runAsync(final Runnable runnable) { + runnable.run(); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncService.java b/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncService.java index a6a1d46e..5c145fb6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncService.java @@ -24,6 +24,10 @@ public class AsyncService { this.asyncRunner = asyncRunner; } + public AsyncRunner getAsyncRunner() { + return this.asyncRunner; + } + /** Create a CircuitBreaker of specified type with the default parameter defined in the CircuitBreaker class * * @param the type of the CircuitBreaker diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/Page.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/Page.java index e669ad9e..a1735129 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/Page.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/Page.java @@ -27,6 +27,7 @@ public final class Page { public static final String ATTR_PAGE_NUMBER = "page_number"; public static final String ATTR_PAGE_SIZE = "page_size"; public static final String ATTR_SORT = "sort"; + public static final String ATTR_COMPLETE = "complete"; public static final String ATTR_CONTENT = "content"; @JsonProperty(ATTR_NUMBER_OF_PAGES) @@ -37,6 +38,8 @@ public final class Page { public final Integer pageSize; @JsonProperty(ATTR_SORT) public final String sort; + @JsonProperty(ATTR_COMPLETE) + public final boolean complete; @JsonProperty(ATTR_CONTENT) public final List content; @@ -46,13 +49,29 @@ public final class Page { @JsonProperty(ATTR_NUMBER_OF_PAGES) final Integer numberOfPages, @JsonProperty(ATTR_PAGE_NUMBER) final Integer pageNumber, @JsonProperty(ATTR_SORT) final String sort, - @JsonProperty(ATTR_CONTENT) final Collection content) { + @JsonProperty(ATTR_CONTENT) final Collection content, + @JsonProperty(ATTR_COMPLETE) final boolean complet) { this.numberOfPages = numberOfPages; this.pageNumber = pageNumber; this.content = Utils.immutableListOf(content); this.pageSize = content.size(); this.sort = sort; + this.complete = complet; + } + + public Page( + final Integer numberOfPages, + final Integer pageNumber, + final String sort, + final Collection content) { + + this.numberOfPages = numberOfPages; + this.pageNumber = pageNumber; + this.content = Utils.immutableListOf(content); + this.pageSize = content.size(); + this.sort = sort; + this.complete = true; } public int getNumberOfPages() { @@ -67,6 +86,10 @@ public final class Page { return (this.pageSize != null) ? this.pageSize : -1; } + public boolean isComplete() { + return this.complete; + } + public Collection getContent() { return this.content; } 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 68ea3ac1..a7eabb7a 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 @@ -12,6 +12,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.LocalDateTime; @@ -235,62 +236,20 @@ public final class QuizData implements GrantEntity { return builder.toString(); } - public static Comparator getIdComparator(final boolean descending) { - return (qd1, qd2) -> ((qd1 == qd2) - ? 0 - : (qd1 == null || qd1.id == null) - ? 1 - : (qd2 == null || qd2.id == null) - ? -1 - : qd1.id.compareTo(qd2.id)) - * ((descending) ? -1 : 1); - } - - public static Comparator getNameComparator(final boolean descending) { - return (qd1, qd2) -> ((qd1 == qd2) - ? 0 - : (qd1 == null || qd1.name == null) - ? 1 - : (qd2 == null || qd2.name == null) - ? -1 - : qd1.name.compareTo(qd2.name)) - * ((descending) ? -1 : 1); - } - - public static Comparator getStartTimeComparator(final boolean descending) { - return (qd1, qd2) -> ((qd1 == qd2) - ? 0 - : (qd1 == null || qd1.startTime == null) - ? 1 - : (qd2 == null || qd2.startTime == null) - ? -1 - : qd1.startTime.compareTo(qd2.startTime)) - * ((descending) ? -1 : 1); - } - - public static Comparator getEndTimeComparator(final boolean descending) { - return (qd1, qd2) -> ((qd1 == qd2) - ? 0 - : (qd1 == null || qd1.endTime == null) - ? 1 - : (qd2 == null || qd2.endTime == null) - ? -1 - : qd1.endTime.compareTo(qd2.endTime)) - * ((descending) ? -1 : 1); - } - public static Comparator getComparator(final String sort) { final boolean descending = PageSortOrder.getSortOrder(sort) == PageSortOrder.DESCENDING; final String sortParam = PageSortOrder.decode(sort); if (QUIZ_ATTR_NAME.equals(sortParam)) { - return getNameComparator(descending); + return (qd1, qd2) -> StringUtils.compare(qd1.name, qd2.name) * ((descending) ? -1 : 1); } else if (QUIZ_ATTR_START_TIME.equals(sortParam)) { - return getStartTimeComparator(descending); + return (qd1, qd2) -> Utils.compareDateTime(qd1.startTime, qd2.startTime, descending); } else if (QUIZ_ATTR_END_TIME.equals(sortParam)) { - return getEndTimeComparator(descending); + return (qd1, qd2) -> Utils.compareDateTime(qd1.endTime, qd2.endTime, descending); + } else if (QUIZ_ATTR_LMS_SETUP_ID.equals(sortParam)) { + return (qd1, qd2) -> Utils.compareIds(qd1.lmsSetupId, qd2.lmsSetupId, descending); } - return getIdComparator(descending); + return (qd1, qd2) -> StringUtils.compare(qd1.id, qd2.id) * ((descending) ? -1 : 1); } } 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 b2aeab9c..a0449efb 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 @@ -858,4 +858,34 @@ public final class Utils { return new LocTextKey(key.name + ".filter" + Constants.TOOLTIP_TEXT_KEY_SUFFIX); } + public static void sleep(final int i) { + try { + Thread.sleep(i); + } catch (final Exception e) { + // silently + } + } + + public static int compareDateTime(final DateTime dt1, final DateTime dt2, final boolean descending) { + return ((dt1 == dt2) + ? 0 + : (dt1 == null || dt1 == null) + ? 1 + : (dt2 == null || dt2 == null) + ? -1 + : dt1.compareTo(dt2)) + * ((descending) ? -1 : 1); + } + + public static int compareIds(final Long id1, final Long id2, final boolean descending) { + return ((id1 == id2) + ? 0 + : (id1 == null || id1 == null) + ? 1 + : (id2 == null || id2 == null) + ? -1 + : id1.compareTo(id2)) + * ((descending) ? -1 : 1); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/QuizLookupList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/QuizLookupList.java index e007cf31..c95b2c99 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/QuizLookupList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/QuizLookupList.java @@ -14,7 +14,11 @@ import java.util.function.BooleanSupplier; import java.util.function.Function; import java.util.stream.Collectors; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.springframework.beans.factory.annotation.Value; @@ -103,6 +107,8 @@ public class QuizLookupList implements TemplateComposer { new LocTextKey("sebserver.quizdiscovery.quiz.import.existing.confirm"); private final static LocTextKey TEXT_KEY_EXISTING = new LocTextKey("sebserver.quizdiscovery.quiz.import.existing"); + private final static LocTextKey TEXT_FETCH_NOTE = + new LocTextKey("sebserver.quizdiscovery.list.fetchnote"); private final static String TEXT_KEY_ADDITIONAL_ATTR_PREFIX = "sebserver.quizdiscovery.quiz.details.additional."; @@ -156,6 +162,10 @@ public class QuizLookupList implements TemplateComposer { final I18nSupport i18nSupport = this.resourceService.getI18nSupport(); final Long institutionId = currentUser.get().institutionId; + final Composite notePanel = this.widgetFactory.voidComposite(pageContext.getParent()); + final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false); + notePanel.setLayoutData(gridData); + // content page layout with title final Composite content = this.widgetFactory.defaultPageLayout( pageContext.getParent(), @@ -173,6 +183,7 @@ public class QuizLookupList implements TemplateComposer { // table final EntityTable table = this.pageService.entityTableBuilder(restService.getRestCall(GetQuizPage.class)) + .withPageReloadListener(t -> this.handelPageReload(notePanel, t)) .withEmptyMessage(EMPTY_LIST_TEXT_KEY) .withPaging(this.pageSize) @@ -437,4 +448,39 @@ public class QuizLookupList implements TemplateComposer { } } + private boolean showingFetchNote = false; + + private void handelPageReload( + final Composite notePanel, + final EntityTable table) { + + if (table.isComplete()) { + PageService.clearComposite(notePanel); + this.showingFetchNote = false; + } else { + if (!this.showingFetchNote) { + final Composite warningPanel = this.widgetFactory.createWarningPanel(notePanel, 15, true); + GridData gridData = new GridData(SWT.CENTER, SWT.CENTER, false, true); + gridData.heightHint = 28; + gridData.widthHint = 25; + gridData.verticalIndent = 5; + final Label action = new Label(warningPanel, SWT.NONE); + action.setImage(WidgetFactory.ImageIcon.SWITCH.getImage(notePanel.getDisplay())); + action.setLayoutData(gridData); + action.addListener(SWT.MouseDown, event -> { + table.applyFilter(); + }); + + final Label text = new Label(warningPanel, SWT.NONE); + text.setData(RWT.MARKUP_ENABLED, Boolean.TRUE); + text.setText(this.pageService.getI18nSupport().getText(TEXT_FETCH_NOTE)); + gridData = new GridData(SWT.LEFT, SWT.FILL, true, true); + gridData.heightHint = 16; + text.setLayoutData(gridData); + this.showingFetchNote = true; + } + } + notePanel.getParent().layout(true, true); + } + } 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 42d5333c..11c1883e 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 @@ -47,6 +47,7 @@ import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent; import ch.ethz.seb.sebserver.gui.service.page.event.PageEvent; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; import ch.ethz.seb.sebserver.gui.service.page.impl.PageState; +import ch.ethz.seb.sebserver.gui.service.push.ServerPushService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder; @@ -90,6 +91,8 @@ public interface PageService { * @return the I18nSupport (internationalization support) service */ I18nSupport getI18nSupport(); + ServerPushService getServerPushService(); + /** Get the ResourceService * * @return the ResourceService */ 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 3c4528e1..8c1cf0c1 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 @@ -54,6 +54,7 @@ import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent; import ch.ethz.seb.sebserver.gui.service.page.event.ActionPublishEvent; import ch.ethz.seb.sebserver.gui.service.page.event.PageEvent; import ch.ethz.seb.sebserver.gui.service.page.event.PageEventListener; +import ch.ethz.seb.sebserver.gui.service.push.ServerPushService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall.CallType; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; @@ -86,6 +87,7 @@ public class PageServiceImpl implements PageService { private final PolyglotPageService polyglotPageService; private final ResourceService resourceService; private final CurrentUser currentUser; + private final ServerPushService serverPushService; public PageServiceImpl( final Cryptor cryptor, @@ -93,7 +95,8 @@ public class PageServiceImpl implements PageService { final WidgetFactory widgetFactory, final PolyglotPageService polyglotPageService, final ResourceService resourceService, - final CurrentUser currentUser) { + final CurrentUser currentUser, + final ServerPushService serverPushService) { this.cryptor = cryptor; this.jsonMapper = jsonMapper; @@ -101,6 +104,7 @@ public class PageServiceImpl implements PageService { this.polyglotPageService = polyglotPageService; this.resourceService = resourceService; this.currentUser = currentUser; + this.serverPushService = serverPushService; } @Override @@ -128,6 +132,11 @@ public class PageServiceImpl implements PageService { return this.resourceService; } + @Override + public ServerPushService getServerPushService() { + return this.serverPushService; + } + @Override public JSONMapper getJSONMapper() { return this.jsonMapper; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/push/ServerPushContext.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/push/ServerPushContext.java index f103e6c4..3892ba5e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/push/ServerPushContext.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/push/ServerPushContext.java @@ -58,6 +58,10 @@ public final class ServerPushContext { return this.anchor; } + public void stop() { + this.internalStop = true; + } + public void layout() { this.anchor.pack(); this.anchor.layout(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java index 7d785095..50079e64 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java @@ -77,6 +77,7 @@ public class EntityTable { private final String sortOrderAttrName; private final String currentPageAttrName; private final boolean markupEnabled; + private final Consumer> pageReloadListener; final PageService pageService; final WidgetFactory widgetFactory; @@ -109,6 +110,7 @@ public class EntityTable { PageSortOrder sortOrder = PageSortOrder.ASCENDING; boolean columnsWithSameWidth = true; boolean hideNavigation; + boolean isComplete = true; EntityTable( final String name, @@ -128,7 +130,8 @@ public class EntityTable { final Consumer> selectionListener, final Consumer contentChangeListener, final String defaultSortColumn, - final PageSortOrder defaultSortOrder) { + final PageSortOrder defaultSortOrder, + final Consumer> pageReloadListener) { this.name = name; this.filterAttrName = name + "_filter"; @@ -139,6 +142,7 @@ public class EntityTable { this.defaultSortColumn = defaultSortColumn; this.defaultSortOrder = defaultSortOrder; + this.pageReloadListener = pageReloadListener; this.composite = new Composite(pageContext.getParent(), SWT.NONE); this.pageService = pageService; @@ -164,12 +168,14 @@ public class EntityTable { this.selectionListener = selectionListener; this.contentChangeListener = contentChangeListener; this.pageSize = pageSize; + this.filter = columns .stream() .map(ColumnDefinition::getFilterAttribute) .anyMatch(Objects::nonNull) ? new TableFilter<>(this) : null; this.table = this.widgetFactory.tableLocalized(this.composite, type); + final GridLayout gridLayout = new GridLayout(columns.size(), true); this.table.setLayout(gridLayout); gridData = new GridData(SWT.FILL, SWT.TOP, true, false); @@ -236,6 +242,10 @@ public class EntityTable { return this.name; } + public boolean isComplete() { + return this.isComplete; + } + public String getSortColumn() { return this.sortColumn; } @@ -510,7 +520,7 @@ public class EntityTable { this.table.removeAll(); // get page data and create rows - this.pageSupplier.newBuilder() + final Page page = this.pageSupplier.newBuilder() .withPaging(pageNumber, pageSize) .withSorting(sortColumn, sortOrder) .withQueryParams((this.filter != null) ? this.filter.getFilterParameter() : null) @@ -519,12 +529,18 @@ public class EntityTable { .getPage() .map(this::createTableRowsFromPage) .map(this.navigator::update) - .onError(this.pageContext::notifyUnexpectedError); + .onError(this.pageContext::notifyUnexpectedError) + .getOr(null); + this.isComplete = page.complete; this.composite.getParent().layout(true, true); PageService.updateScrolledComposite(this.composite); this.notifyContentChange(); this.notifySelectionChange(); + + if (page != null && this.pageReloadListener != null) { + this.pageReloadListener.accept(this); + } } private Page createTableRowsFromPage(final Page page) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java index 3542835b..b399fc81 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java @@ -52,6 +52,7 @@ public class TableBuilder { private boolean markupEnabled = false; private String defaultSortColumn = null; private PageSortOrder defaultSortOrder = PageSortOrder.ASCENDING; + private Consumer> pageReloadListener; public TableBuilder( final String name, @@ -203,6 +204,11 @@ public class TableBuilder { return this; } + public TableBuilder withPageReloadListener(final Consumer> pageReloadListener) { + this.pageReloadListener = pageReloadListener; + return this; + } + public EntityTable compose(final PageContext pageContext) { return new EntityTable<>( this.name, @@ -224,7 +230,8 @@ public class TableBuilder { this.selectionListener, this.contentChangeListener, this.defaultSortColumn, - this.defaultSortOrder); + this.defaultSortOrder, + this.pageReloadListener); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableNavigator.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableNavigator.java index 24bb6867..a85832a4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableNavigator.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableNavigator.java @@ -15,6 +15,7 @@ import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; +import ch.ethz.seb.sebserver.gbl.model.ModelIdAware; import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; @@ -37,13 +38,13 @@ public class TableNavigator { this.composite = new Composite(entityTable.composite, SWT.NONE); final GridData gridData = new GridData(SWT.LEFT, SWT.CENTER, true, true); this.composite.setLayoutData(gridData); - final GridLayout layout = new GridLayout(3, false); + final GridLayout layout = new GridLayout(4, false); this.composite.setLayout(layout); this.entityTable = entityTable; } - public Page update(final Page pageData) { + public Page update(final Page pageData) { if (this.composite == null) { return pageData; } @@ -108,7 +109,8 @@ public class TableNavigator { gridData.minimumWidth = 100; gridData.heightHint = 16; pageHeader.setLayoutData(gridData); - pageHeader.setText("Page " + page + " / " + of); + final String headerText = "Page " + page + " / " + of; + pageHeader.setText(headerText); } private void createPageNumberLabel( 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 cf5ad244..45ba6f1a 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 @@ -377,10 +377,15 @@ public class WidgetFactory { } public Composite createWarningPanel(final Composite parent, final int margin) { + return createWarningPanel(parent, margin, false); + } + + public Composite createWarningPanel(final Composite parent, final int margin, final boolean horizontal) { final Composite composite = new Composite(parent, SWT.NONE); final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); composite.setLayoutData(gridData); - final GridLayout gridLayout = new GridLayout(1, true); + + final GridLayout gridLayout = horizontal ? new GridLayout(2, false) : new GridLayout(1, true); gridLayout.marginWidth = margin; gridLayout.marginHeight = margin; composite.setLayout(gridLayout); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java index 026c23e5..11ea818c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java @@ -8,6 +8,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; @@ -61,8 +62,11 @@ public interface CourseAccessAPI { * * @return Result of an unsorted List of filtered {@link QuizData } from the LMS course/quiz API * or refer to an error when happened */ + @Deprecated Result> getQuizzes(FilterMap filterMap); + void fetchQuizzes(FilterMap filterMap, AsyncQuizFetchBuffer asyncQuizFetchBuffer); + /** Get all {@link QuizData } for the set of {@link QuizData } identifiers from LMS API in a collection * of Result. If particular quizzes cannot be loaded because of errors or deletion, * the the referencing QuizData will not be in the resulting list and an error is logged. @@ -109,8 +113,25 @@ public interface CourseAccessAPI { * @return Result referencing to the Chapters model for the given course or to an error when happened. */ Result getCourseChapters(String courseId); - default FetchStatus getFetchStatus() { - return FetchStatus.ALL_FETCHED; + static class AsyncQuizFetchBuffer { + public List buffer = new ArrayList<>(); + public boolean finished = false; + public boolean canceled = false; + public Exception error = null; + + public void finish() { + this.finished = true; + } + + public void finish(final Exception error) { + this.error = error; + finish(); + } + + public void cancel() { + this.canceled = true; + } + } } 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 285a185e..187c144b 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 @@ -8,7 +8,6 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms; -import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.function.Predicate; @@ -115,6 +114,8 @@ public interface LmsAPIService { from == null || (q.startTime != null && (q.startTime.isEqual(from) || q.startTime.isAfter(from))); final DateTime endTime = now.isAfter(from) ? now : from; final boolean fromTimeFilter = (endTime == null || q.endTime == null || endTime.isBefore(q.endTime)); + System.out + .println("************ name: " + name + " " + (nameFilter && (startTimeFilter || fromTimeFilter))); return nameFilter && (startTimeFilter || fromTimeFilter); }; } @@ -131,67 +132,4 @@ public interface LmsAPIService { .collect(Collectors.toList()); } - /** Closure that gives a Function to create a Page of QuizData from a given List of QuizData with the - * attributes, pageNumber, pageSize and sort. - * - * NOTE: this is not sorting the QuizData list but uses the sortAttribute for the page creation - * - * @param sortAttribute the sort attribute for the new Page - * @param pageNumber the number of the Page to build - * @param pageSize the size of the Page to build - * @return A Page of QuizData extracted from a given list of QuizData */ - static Function, Page> quizzesToPageFunction( - final String sortAttribute, - final int pageNumber, - final int pageSize) { - - return quizzes -> { - if (quizzes.isEmpty()) { - return new Page<>(0, 1, sortAttribute, Collections.emptyList()); - } - - int start = (pageNumber - 1) * pageSize; - int end = start + pageSize; - if (end > quizzes.size()) { - end = quizzes.size(); - } - if (start >= end) { - start = end - pageSize; - if (start < 0) { - start = 0; - } - - return new Page<>( - (quizzes.size() <= pageSize) ? 1 : quizzes.size() / pageSize + 1, - start / pageSize + 1, - sortAttribute, - quizzes.subList(start, end)); - } - - final int mod = quizzes.size() % pageSize; - return new Page<>( - (quizzes.size() <= pageSize) - ? 1 - : (mod > 0) - ? quizzes.size() / pageSize + 1 - : quizzes.size() / pageSize, - pageNumber, - sortAttribute, - quizzes.subList(start, end)); - }; - } - - /** Closure that gives a Function to sort a List of QuizData by a certain sort criteria. - * The sort criteria is the name of the QuizData attribute plus a leading '-' sign for - * descending sort order indication. - * - * @param sort the sort criteria ( ['-']{attributeName} ) - * @return A Function to sort a List of QuizData by a certain sort criteria */ - static Function, List> quizzesSortFunction(final String sort) { - return quizzes -> { - quizzes.sort(QuizData.getComparator(sort)); - return quizzes; - }; - } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/QuizLookupService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/QuizLookupService.java new file mode 100644 index 00000000..1d386429 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/QuizLookupService.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.lms; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import ch.ethz.seb.sebserver.gbl.model.Page; +import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; + +public interface QuizLookupService { + + boolean isLookupRunning(); + + /** Used to get a specified page of QuizData from all active LMS Setup of the current users + * institution, filtered by the given FilterMap. + * + * @param pageNumber the page number from the QuizData list to get + * @param pageSize the page size + * @param sort the sort parameter + * @param filterMap the FilterMap containing all filter criteria + * @return the specified Page of QuizData from all active LMS Setups of the current users institution */ + Result> requestQuizDataPage( + final int pageNumber, + final int pageSize, + final String sort, + final FilterMap filterMap, + Function> lmsAPITemplateSupplier); + + void clear(); + + void clear(Long institutionId); + + default LookupResult emptyLookupResult() { + return new LookupResult(Collections.emptyList(), true); + } + + public final static class LookupResult { + public final List quizData; + public final boolean completed; + public final long timestamp; + + public LookupResult( + final List quizData, + final boolean completed) { + + this.quizData = quizData; + this.completed = completed; + this.timestamp = Utils.getMillisecondsNow(); + } + } + + /** Closure that gives a Function to create a Page of QuizData from a given List of QuizData with the + * attributes, pageNumber, pageSize and sort. + * + * NOTE: this is not sorting the QuizData list but uses the sortAttribute for the page creation + * + * @param sortAttribute the sort attribute for the new Page + * @param pageNumber the number of the Page to build + * @param pageSize the size of the Page to build + * @param complete indicates if the quiz lookup that uses this page function has been completed yet + * @return A Page of QuizData extracted from a given list of QuizData */ + static Function> quizzesToPageFunction( + final String sortAttribute, + final int pageNumber, + final int pageSize) { + + return lookupResult -> { + final List quizzes = lookupResult.quizData; + if (quizzes.isEmpty()) { + return new Page<>(0, 1, sortAttribute, Collections.emptyList(), lookupResult.completed); + } + + int start = (pageNumber - 1) * pageSize; + int end = start + pageSize; + if (end > quizzes.size()) { + end = quizzes.size(); + } + if (start >= end) { + start = end - pageSize; + if (start < 0) { + start = 0; + } + + return new Page<>( + (quizzes.size() <= pageSize) ? 1 : quizzes.size() / pageSize + 1, + start / pageSize + 1, + sortAttribute, + quizzes.subList(start, end)); + } + + final int mod = quizzes.size() % pageSize; + return new Page<>( + (quizzes.size() <= pageSize) + ? 1 + : (mod > 0) + ? quizzes.size() / pageSize + 1 + : quizzes.size() / pageSize, + pageNumber, + sortAttribute, + quizzes.subList(start, end), + lookupResult.completed); + }; + } + + /** Closure that gives a Function to sort a List of QuizData by a certain sort criteria. + * The sort criteria is the name of the QuizData attribute plus a leading '-' sign for + * descending sort order indication. + * + * @param sort the sort criteria ( ['-']{attributeName} ) + * @return A Function to sort a List of QuizData by a certain sort criteria */ + static Function quizzesSortFunction(final String sort) { + return lookupResult -> { + lookupResult.quizData.sort(QuizData.getComparator(sort)); + return lookupResult; + }; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java index 8fa65052..822a8b50 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java @@ -10,9 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.EnumMap; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -45,6 +43,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.QuizLookupService; @Lazy @Service @@ -56,20 +55,22 @@ public class LmsAPIServiceImpl implements LmsAPIService { private final WebserviceInfo webserviceInfo; private final LmsSetupDAO lmsSetupDAO; private final ClientCredentialService clientCredentialService; + private final QuizLookupService quizLookupService; private final EnumMap templateFactories; - // TODO use also EHCache here private final Map cache = new ConcurrentHashMap<>(); public LmsAPIServiceImpl( final WebserviceInfo webserviceInfo, final LmsSetupDAO lmsSetupDAO, final ClientCredentialService clientCredentialService, + final QuizLookupService quizLookupService, final Collection lmsAPITemplateFactories) { this.webserviceInfo = webserviceInfo; this.lmsSetupDAO = lmsSetupDAO; this.clientCredentialService = clientCredentialService; + this.quizLookupService = quizLookupService; final Map factories = lmsAPITemplateFactories .stream() @@ -99,6 +100,7 @@ public class LmsAPIServiceImpl implements LmsAPIService { if (removedTemplate != null) { removedTemplate.clearCourseCache(); } + this.quizLookupService.clear(lmsSetup.institutionId); } @Override @@ -119,9 +121,12 @@ public class LmsAPIServiceImpl implements LmsAPIService { final String sort, final FilterMap filterMap) { - return getAllQuizzesFromLMSSetups(filterMap) - .map(LmsAPIService.quizzesSortFunction(sort)) - .map(LmsAPIService.quizzesToPageFunction(sort, pageNumber, pageSize)); + return this.quizLookupService.requestQuizDataPage( + pageNumber, + pageSize, + sort, + filterMap, + this::getLmsAPITemplate); } @Override @@ -178,51 +183,39 @@ public class LmsAPIServiceImpl implements LmsAPIService { return LmsSetupTestResult.ofOkay(lmsSetupTemplate.lmsSetup().getLmsType()); } - /** Collect all QuizData from all affecting LmsSetup. - * If filterMap contains a LmsSetup identifier, only the QuizData from that LmsSetup is collected. - * Otherwise QuizData from all active LmsSetup of the current institution are collected. - * - * @param filterMap the FilterMap containing either an LmsSetup identifier or an institution identifier - * @return list of QuizData from all affecting LmsSetup */ - private Result> getAllQuizzesFromLMSSetups(final FilterMap filterMap) { - - return Result.tryCatch(() -> { - // case 1. if lmsSetupId is available only get quizzes from specified LmsSetup - final Long lmsSetupId = filterMap.getLmsSetupId(); - if (lmsSetupId != null) { - try { - final Long institutionId = filterMap.getInstitutionId(); - - final LmsAPITemplate template = getLmsAPITemplate(lmsSetupId) - .getOrThrow(); - - if (institutionId != null && !institutionId.equals(template.lmsSetup().institutionId)) { - return Collections.emptyList(); - } - return template - .getQuizzes(filterMap) - .getOrThrow(); - } catch (final Exception e) { - log.error("Failed to get quizzes from LMS Setup: {}", lmsSetupId, e); - return Collections.emptyList(); - } - } - - // case 2. get quizzes from all LmsSetups of specified institution - final Long institutionId = filterMap.getInstitutionId(); - return this.lmsSetupDAO.all(institutionId, true) - .getOrThrow() - .parallelStream() - .map(LmsSetup::getModelId) - .map(this::getLmsAPITemplate) - .flatMap(Result::onErrorLogAndSkip) - .map(template -> template.getQuizzes(filterMap)) - .flatMap(Result::onErrorLogAndSkip) - .flatMap(List::stream) - .distinct() - .collect(Collectors.toList()); - }); - } +// /** Collect all QuizData from all affecting LmsSetup. +// * If filterMap contains a LmsSetup identifier, only the QuizData from that LmsSetup is collected. +// * Otherwise QuizData from all active LmsSetup of the current institution are collected. +// * +// * @param filterMap the FilterMap containing either an LmsSetup identifier or an institution identifier +// * @return list of QuizData from all affecting LmsSetup */ +// private Result> getAllQuizzesFromLMSSetups(final FilterMap filterMap) { +// +// // case 1. if lmsSetupId is available only get quizzes from specified LmsSetup +// final Long lmsSetupId = filterMap.getLmsSetupId(); +// if (lmsSetupId != null) { +// return getQuizzesForSingleLMS(filterMap, lmsSetupId); +// } +// +// // case 2. get quizzes from all LmsSetups of specified institution +// return this.quizLookupService.getAllQuizzesFromLMSSetups( +// filterMap, +// this::getLmsAPITemplate); +// +//// final Long institutionId = filterMap.getInstitutionId(); +//// return this.lmsSetupDAO.all(institutionId, true) +//// .getOrThrow() +//// .parallelStream() +//// .map(LmsSetup::getModelId) +//// .map(this::getLmsAPITemplate) +//// .flatMap(Result::onErrorLogAndSkip) +//// .map(template -> template.getQuizzes(filterMap)) +//// .flatMap(Result::onErrorLogAndSkip) +//// .flatMap(List::stream) +//// .distinct() +//// .collect(Collectors.toList()); +// +// } private LmsAPITemplate getFromCache(final String lmsSetupId) { // first cleanup the cache by removing old instances @@ -244,6 +237,7 @@ public class LmsAPIServiceImpl implements LmsAPIService { final LmsSetup lmsSetup = lmsAPITemplate.lmsSetup(); if (!this.lmsSetupDAO.isUpToDate(lmsSetup)) { this.cache.remove(cacheKey); + this.quizLookupService.clear(lmsSetup.institutionId); return null; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java index 888ad639..0ec53df8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java @@ -19,7 +19,6 @@ import org.springframework.core.env.Environment; import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; -import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker.State; import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; @@ -46,8 +45,6 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { /** CircuitBreaker for protected lmsTestRequest */ private final CircuitBreaker lmsTestRequest; /** CircuitBreaker for protected quiz and course data requests */ - private final CircuitBreaker> allQuizzesRequest; - /** CircuitBreaker for protected quiz and course data requests */ private final CircuitBreaker> quizzesRequest; /** CircuitBreaker for protected quiz and course data requests */ private final CircuitBreaker quizRequest; @@ -84,20 +81,6 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { Long.class, 0L)); - this.allQuizzesRequest = asyncService.createCircuitBreaker( - environment.getProperty( - "sebserver.webservice.circuitbreaker.quizzesRequest.attempts", - Integer.class, - 1), - environment.getProperty( - "sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime", - Long.class, - Constants.SECOND_IN_MILLIS * 20), - environment.getProperty( - "sebserver.webservice.circuitbreaker.quizzesRequest.timeToRecover", - Long.class, - 0L)); - this.quizzesRequest = asyncService.createCircuitBreaker( environment.getProperty( "sebserver.webservice.circuitbreaker.quizzesRequest.attempts", @@ -223,19 +206,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { } @Override - public FetchStatus getFetchStatus() { - if (this.courseAccessAPI == null) { - return FetchStatus.FETCH_ERROR; - } - - if (this.allQuizzesRequest.getState() != State.CLOSED) { - return FetchStatus.FETCH_ERROR; - } - - return this.courseAccessAPI.getFetchStatus(); - } - - @Override + @Deprecated public Result> getQuizzes(final FilterMap filterMap) { if (this.courseAccessAPI == null) { @@ -247,12 +218,27 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { log.debug("Get quizzes for LMSSetup: {}", lmsSetup()); } - return this.allQuizzesRequest.protectedRun(() -> this.courseAccessAPI + return this.courseAccessAPI .getQuizzes(filterMap) .onError(error -> log.error( "Failed to run protectedQuizzesRequest: {}", - error.getMessage())) - .getOrThrow()); + error.getMessage())); + } + + @Override + public void fetchQuizzes(final FilterMap filterMap, final AsyncQuizFetchBuffer asyncQuizFetchBuffer) { + if (this.courseAccessAPI == null) { + asyncQuizFetchBuffer.finish(new UnsupportedOperationException( + "Course API Not Supported For: " + getType().name())); + return; + } + + if (log.isDebugEnabled()) { + log.debug("Get quizzes for LMSSetup: {}", lmsSetup()); + } + + this.courseAccessAPI.fetchQuizzes(filterMap, asyncQuizFetchBuffer); + asyncQuizFetchBuffer.finish(); } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/QuizLookupServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/QuizLookupServiceImpl.java new file mode 100644 index 00000000..e74deed9 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/QuizLookupServiceImpl.java @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.async.AsyncRunner; +import ch.ethz.seb.sebserver.gbl.async.AsyncService; +import ch.ethz.seb.sebserver.gbl.model.Page; +import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI.AsyncQuizFetchBuffer; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.QuizLookupService; + +@Lazy +@Service +@WebServiceProfile +public class QuizLookupServiceImpl implements QuizLookupService { + + private static final Logger log = LoggerFactory.getLogger(QuizLookupServiceImpl.class); + + private final Map lookups = new ConcurrentHashMap<>(); + + private final UserService userService; + private final LmsSetupDAO lmsSetupDAO; + private final AsyncRunner asyncRunner; + + public QuizLookupServiceImpl( + final UserService userService, + final LmsSetupDAO lmsSetupDAO, + final AsyncService asyncService, + final Environment environment) { + + this.userService = userService; + this.lmsSetupDAO = lmsSetupDAO; + this.asyncRunner = asyncService.getAsyncRunner(); + } + + @Override + public void clear(final Long institutionId) { + final Set toRemove = this.lookups.values() + .stream() + .filter(al -> al.institutionId == institutionId.longValue() || !al.isUpToDate()) + .map(al -> al.userId) + .collect(Collectors.toSet()); + + if (log.isDebugEnabled()) { + log.debug("Remove invalid async lookups: {}", toRemove); + } + + toRemove.stream() + .forEach(this::removeFromCache); + } + + @Override + public void clear() { + final Set toRemove = this.lookups.values() + .stream() + .filter(al -> !al.isUpToDate()) + .map(al -> al.userId) + .collect(Collectors.toSet()); + + if (log.isDebugEnabled()) { + log.debug("Remove invalid async lookups: {}", toRemove); + } + + toRemove.stream() + .forEach(this::removeFromCache); + } + + @Override + public boolean isLookupRunning() { + final String userId = this.userService.getCurrentUser().uuid(); + if (!this.lookups.containsKey(userId)) { + return false; + } + return this.lookups.get(userId).isRunning(); + } + + @Override + public Result> requestQuizDataPage( + final int pageNumber, + final int pageSize, + final String sort, + final FilterMap filterMap, + final Function> lmsAPITemplateSupplier) { + + return getAllQuizzesFromLMSSetups(filterMap, lmsAPITemplateSupplier) + .map(QuizLookupService.quizzesSortFunction(sort)) + .map(QuizLookupService.quizzesToPageFunction( + sort, + pageNumber, + pageSize)); + } + + private Result getAllQuizzesFromLMSSetups( + final FilterMap filterMap, + final Function> lmsAPITemplateSupplier) { + + return Result.tryCatch(() -> { + + final String userId = this.userService.getCurrentUser().uuid(); + + if (log.isDebugEnabled()) { + log.debug("Get all quizzes for user: {}", userId); + } + + final AsyncLookup asyncLookup = getAsyncLookup(userId, filterMap, lmsAPITemplateSupplier); + if (asyncLookup != null) { + return asyncLookup.getAvailable(); + } + + return emptyLookupResult(); + }); + } + + private AsyncLookup getAsyncLookup( + final String userId, + final FilterMap filterMap, + final Function> lmsAPITemplateSupplier) { + + if (!this.lookups.containsKey(userId)) { + this.createNewAsyncLookup(userId, filterMap, lmsAPITemplateSupplier); + } + + final AsyncLookup asyncLookup = this.lookups.get(userId); + if (asyncLookup == null) { + return null; + } + + if (!asyncLookup.isValid(filterMap)) { + this.lookups.remove(userId); + this.createNewAsyncLookup(userId, filterMap, lmsAPITemplateSupplier); + } + + return this.lookups.get(userId); + } + + private void createNewAsyncLookup( + final String userId, + final FilterMap filterMap, + final Function> lmsAPITemplateSupplier) { + + try { + final Long institutionId = filterMap.getInstitutionId(); + Long userInstitutionId = institutionId; + if (userInstitutionId == null) { + userInstitutionId = this.userService.getCurrentUser().getUserInfo().institutionId; + } + final Long lmsSetupId = filterMap.getLmsSetupId(); + final Result> tasks = this.lmsSetupDAO + .all(institutionId, true) + .map(lmsSetups -> lmsSetups + .stream() + .filter(lmsSetup -> lmsSetupId == null || lmsSetupId.longValue() == lmsSetup.id.longValue()) + .map(lmsSetup -> lmsAPITemplateSupplier.apply(lmsSetup.getModelId())) + .flatMap(Result::onErrorLogAndSkip) + .map(lmsAPITemplate -> spawnTask(filterMap, lmsAPITemplate)) + .collect(Collectors.toList())); + + if (tasks.hasError()) { + log.error("Failed to spawn LMS quizzes lookup tasks: ", tasks.getError()); + return; + } + + final List buffers = tasks.getOr(Collections.emptyList()); + if (buffers.isEmpty()) { + return; + } + + final LookupFilterCriteria criteria = new LookupFilterCriteria(filterMap); + final AsyncLookup asyncLookup = new AsyncLookup(userInstitutionId, userId, criteria, buffers); + + if (log.isDebugEnabled()) { + log.debug("Create new AsyncLookup: user={} criteria={}", userId, criteria); + } + + this.lookups.put(asyncLookup.userId, asyncLookup); + + // Note: wait for about max five second to finish up + int tryWait = 0; + while (asyncLookup.isRunning() && tryWait < 10) { + tryWait++; + Utils.sleep(500); + } + } catch (final Exception e) { + log.error( + "Unexpected error while create new AsyncLookup for user: {}, filterMap: {}, error:", + userId, + filterMap, + e); + } + } + + private AsyncQuizFetchBuffer spawnTask(final FilterMap filterMap, final LmsAPITemplate lmsAPITemplate) { + final AsyncQuizFetchBuffer asyncQuizFetchBuffer = new AsyncQuizFetchBuffer(); + this.asyncRunner.runAsync(() -> lmsAPITemplate.fetchQuizzes(filterMap, asyncQuizFetchBuffer)); + return asyncQuizFetchBuffer; + } + + private void removeFromCache(final String userId) { + final AsyncLookup removed = this.lookups.remove(userId); + if (removed != null) { + removed.cancel(); + } + } + + private static final class LookupFilterCriteria { + public final Long institutionId; + public final Long lmsId; + public final String name; + public final DateTime startTime; + + public LookupFilterCriteria(final FilterMap filterMap) { + + this.institutionId = filterMap.getInstitutionId(); + this.lmsId = filterMap.getLmsSetupId(); + this.name = filterMap.getQuizName(); + this.startTime = filterMap.getQuizFromTime(); + } + + boolean equals(final FilterMap filterMap) { + return Utils.isEqualsWithEmptyCheck(filterMap.getQuizName(), this.name) && + Objects.equals(filterMap.getQuizFromTime(), this.startTime) && + Objects.equals(filterMap.getLmsSetupId(), this.lmsId) && + Objects.equals(filterMap.getInstitutionId(), this.institutionId); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("LookupFilterCriteria [institutionId="); + builder.append(this.institutionId); + builder.append(", lmsId="); + builder.append(this.lmsId); + builder.append(", name="); + builder.append(this.name); + builder.append(", startTime="); + builder.append(this.startTime); + builder.append("]"); + return builder.toString(); + } + } + + private static final class AsyncLookup { + final long institutionId; + final String userId; + final LookupFilterCriteria lookupFilterCriteria; + final Collection asyncBuffers; + final long timeCreated; + long timeCompleted = Long.MAX_VALUE; + + public AsyncLookup( + final long institutionId, + final String userId, + final LookupFilterCriteria lookupFilterCriteria, + final Collection asyncBuffers) { + + this.institutionId = institutionId; + this.userId = userId; + this.lookupFilterCriteria = lookupFilterCriteria; + this.asyncBuffers = asyncBuffers; + this.timeCreated = Utils.getMillisecondsNow(); + } + + LookupResult getAvailable() { + boolean running = false; + final List result = new ArrayList<>(); + for (final AsyncQuizFetchBuffer buffer : this.asyncBuffers) { + running = running || !buffer.finished; + result.addAll(buffer.buffer); + } + if (!running) { + this.timeCompleted = Utils.getMillisecondsNow(); + } + return new LookupResult(result, !running); + } + + boolean isUpToDate() { + final long now = Utils.getMillisecondsNow(); + if (now - this.timeCreated > 5 * Constants.MINUTE_IN_MILLIS) { + return false; + } + if (now - this.timeCompleted > Constants.MINUTE_IN_MILLIS) { + return false; + } + return true; + } + + boolean isValid(final FilterMap filterMap) { + if (!isUpToDate()) { + return false; + } + + return this.lookupFilterCriteria.equals(filterMap); + } + + boolean isRunning() { + if (this.timeCompleted < Long.MAX_VALUE) { + return false; + } + final boolean running = this.asyncBuffers + .stream() + .filter(b -> !b.finished) + .findFirst() + .isPresent(); + if (!running) { + this.timeCompleted = Utils.getMillisecondsNow(); + } + return running; + } + + void cancel() { + this.asyncBuffers.stream().forEach(AsyncQuizFetchBuffer::cancel); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java index 3cca3c40..2fc3ec6a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java @@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -171,6 +172,18 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms .collect(Collectors.toList())); } + @Override + public void fetchQuizzes(final FilterMap filterMap, final AsyncQuizFetchBuffer asyncQuizFetchBuffer) { + this.allQuizzesRequest(filterMap) + .onError(error -> asyncQuizFetchBuffer.finish(error)) + .getOr(Collections.emptyList()) + .stream() + .filter(LmsAPIService.quizFilterPredicate(filterMap)) + .forEach(qd -> asyncQuizFetchBuffer.buffer.add(qd)); + + asyncQuizFetchBuffer.finish(); + } + @Override public Result> getQuizzes(final Set ids) { return Result.tryCatch(() -> { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java index 57d2f0a3..a6ed36a9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -55,6 +56,7 @@ import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess; /** Implements the LmsAPITemplate for Open edX LMS Course API access. @@ -142,6 +144,42 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess implements Co return getRestTemplate().map(this::collectAllQuizzes); } + @Override + public void fetchQuizzes(final FilterMap filterMap, final AsyncQuizFetchBuffer asyncQuizFetchBuffer) { + try { + final OAuth2RestTemplate restTemplate = getRestTemplate().getOrThrow(); + final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); + final String externalStartURI = getExternalLMSServerAddress(lmsSetup); + final String edxAPI = lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT; + + EdXPage page = getEdxPage(edxAPI, restTemplate).getBody(); + if (page != null) { + asyncQuizFetchBuffer.buffer.addAll( + coursesToQuizzes(lmsSetup, externalStartURI, page.results) + .filter(LmsAPIService.quizFilterPredicate(filterMap)) + .collect(Collectors.toList())); + while (!asyncQuizFetchBuffer.canceled && page != null && StringUtils.isNotBlank(page.next)) { + if (asyncQuizFetchBuffer.canceled) { + asyncQuizFetchBuffer.finish(); + return; + } + + page = getEdxPage(page.next, restTemplate).getBody(); + if (page != null) { + asyncQuizFetchBuffer.buffer.addAll( + coursesToQuizzes(lmsSetup, externalStartURI, page.results) + .filter(LmsAPIService.quizFilterPredicate(filterMap)) + .collect(Collectors.toList())); + } + } + } + + asyncQuizFetchBuffer.finish(); + } catch (final Exception e) { + asyncQuizFetchBuffer.finish(e); + } + } + @Override public Result getQuiz(final String id) { return Result.tryCatch(() -> { @@ -597,4 +635,18 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess implements Co return Result.of(this.restTemplate); } + private Stream coursesToQuizzes( + final LmsSetup lmsSetup, + final String externalStartURI, + final Collection courses) { + + if (courses == null) { + return Stream.empty(); + } + + return courses + .stream() + .map(cd -> quizDataOf(lmsSetup, cd, externalStartURI)); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java index 7e16e634..7becaef1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java @@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Random; import java.util.Set; import java.util.stream.Collectors; @@ -29,6 +30,7 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; @@ -40,6 +42,8 @@ public class MockCourseAccessAPI implements CourseAccessAPI { private final Collection mockups; private final WebserviceInfo webserviceInfo; private final APITemplateDataSupplier apiTemplateDataSupplier; + private final Random random = new Random(); + private final boolean simulateLatency = false; public MockCourseAccessAPI( final APITemplateDataSupplier apiTemplateDataSupplier, @@ -162,6 +166,12 @@ public class MockCourseAccessAPI implements CourseAccessAPI { throw new IllegalArgumentException("Wrong clientId or secret"); } + if (this.simulateLatency) { + final int seconds = this.random.nextInt(20); + System.out.println("************ Mockup LMS wait for " + seconds + " seconds before respond"); + Thread.sleep(seconds * 1000); + } + return this.mockups .stream() .map(this::getExternalAddressAlias) @@ -170,6 +180,34 @@ public class MockCourseAccessAPI implements CourseAccessAPI { }); } + @Override + public void fetchQuizzes(final FilterMap filterMap, final AsyncQuizFetchBuffer asyncQuizFetchBuffer) { + if (!authenticate()) { + asyncQuizFetchBuffer.finish(new IllegalArgumentException("Wrong clientId or secret")); + return; + } + + final List collect = this.mockups + .stream() + .map(this::getExternalAddressAlias) + .filter(LmsAPIService.quizFilterPredicate(filterMap)) + .collect(Collectors.toList()); + + for (final QuizData quizData : collect) { + if (asyncQuizFetchBuffer.canceled) { + return; + } + if (this.simulateLatency) { + final int seconds = this.random.nextInt(5); + System.out.println("************ Mockup LMS wait for " + seconds + " seconds before respond"); + Utils.sleep(seconds * 1000); + } + asyncQuizFetchBuffer.buffer.add(quizData); + } + + asyncQuizFetchBuffer.finish(); + } + @Override public Result> getQuizzes(final Set ids) { return Result.tryCatch(() -> { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java index 44086d7d..989fe064 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java @@ -8,6 +8,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -16,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -23,6 +25,7 @@ import java.util.stream.Stream; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; @@ -32,10 +35,14 @@ import org.springframework.util.MultiValueMap; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonMappingException; import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.JSONMapper; +import ch.ethz.seb.sebserver.gbl.async.AsyncService; +import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; @@ -94,11 +101,15 @@ public class MoodleCourseAccess implements CourseAccessAPI { private final MoodleRestTemplateFactory moodleRestTemplateFactory; private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; private final boolean prependShortCourseName; + private final CircuitBreaker protectedMoodlePageCall; + private final int pageSize; + private final int maxSize; private MoodleAPIRestTemplate restTemplate; public MoodleCourseAccess( final JSONMapper jsonMapper, + final AsyncService asyncService, final MoodleRestTemplateFactory moodleRestTemplateFactory, final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader, final Environment environment) { @@ -110,6 +121,24 @@ public class MoodleCourseAccess implements CourseAccessAPI { this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty( "sebserver.webservice.lms.moodle.prependShortCourseName", Constants.TRUE_STRING)); + + this.protectedMoodlePageCall = asyncService.createCircuitBreaker( + environment.getProperty( + "sebserver.webservice.circuitbreaker.moodleRestCall.attempts", + Integer.class, + 2), + environment.getProperty( + "sebserver.webservice.circuitbreaker.moodleRestCall.blockingTime", + Long.class, + Constants.SECOND_IN_MILLIS * 20), + environment.getProperty( + "sebserver.webservice.circuitbreaker.moodleRestCall.timeToRecover", + Long.class, + Constants.MINUTE_IN_MILLIS)); + this.maxSize = + environment.getProperty("sebserver.webservice.cache.moodle.course.maxSize", Integer.class, 10000); + this.pageSize = + environment.getProperty("sebserver.webservice.cache.moodle.course.pageSize", Integer.class, 500); } APITemplateDataSupplier getApiTemplateDataSupplier() { @@ -155,6 +184,102 @@ public class MoodleCourseAccess implements CourseAccessAPI { .getOr(Collections.emptyList())); } + @Override + public void fetchQuizzes(final FilterMap filterMap, final AsyncQuizFetchBuffer asyncQuizFetchBuffer) { + try { + int page = 0; + final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); + final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) + ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH + : lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; + + while (!asyncQuizFetchBuffer.finished && !asyncQuizFetchBuffer.canceled) { + final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow(); + // first get courses from Moodle for page + final Map courseData = new HashMap<>(); + final Collection coursesPage = getCoursesPage(restTemplate, page, this.pageSize); + + if (coursesPage == null || coursesPage.isEmpty()) { + asyncQuizFetchBuffer.finish(); + continue; + } + + courseData.putAll(coursesPage + .stream() + .collect(Collectors.toMap(cd -> cd.id, Function.identity()))); + + // then get all quizzes of courses and filter + final LinkedMultiValueMap attributes = new LinkedMultiValueMap<>(); + final List courseIds = new ArrayList<>(courseData.keySet()); + if (courseIds.size() == 1) { + // NOTE: This is a workaround because the Moodle API do not support lists with only one element. + courseIds.add("0"); + } + attributes.put( + MoodleCourseAccess.MOODLE_COURSE_API_COURSE_IDS, + courseIds); + + final String quizzesJSON = this.protectedMoodlePageCall + .protectedRun(() -> restTemplate.callMoodleAPIFunction( + MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME, + attributes)) + .getOrThrow(); + + final CourseQuizData courseQuizData = this.jsonMapper.readValue( + quizzesJSON, + CourseQuizData.class); + + if (courseQuizData == null) { + // return false; SEBSERV-361 + page++; + continue; + } + + if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) { + log.warn( + "There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}", + lmsSetup.name, + MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME, + courseQuizData.warnings.size(), + courseQuizData.warnings.iterator().next().toString()); + if (log.isTraceEnabled()) { + log.trace("All warnings from Moodle: {}", courseQuizData.warnings.toString()); + } + } + + if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) { + // no quizzes on this page + page++; + continue; + } + + courseQuizData.quizzes + .stream() + .filter(getQuizFilter()) + .forEach(quiz -> { + final CourseData data = courseData.get(quiz.course); + if (data != null) { + data.quizzes.add(quiz); + } + }); + + courseData.values().stream() + .filter(c -> !c.quizzes.isEmpty()) + .forEach(c -> asyncQuizFetchBuffer.buffer.addAll( + quizDataOf(lmsSetup, c, urlPrefix).stream() + .filter(LmsAPIService.quizFilterPredicate(filterMap)) + .collect(Collectors.toList()))); + + page++; + } + + asyncQuizFetchBuffer.finish(); + + } catch (final Exception e) { + asyncQuizFetchBuffer.finish(e); + } + } + @Override public Result> getQuizzes(final Set ids) { return Result.tryCatch(() -> { @@ -209,7 +334,6 @@ public class MoodleCourseAccess implements CourseAccessAPI { @Override public void clearCourseCache() { - // TODO Auto-generated method stub } @@ -283,16 +407,6 @@ public class MoodleCourseAccess implements CourseAccessAPI { return Result.ofError(new UnsupportedOperationException("not available yet")); } - @Override - public FetchStatus getFetchStatus() { - - if (this.moodleCourseDataAsyncLoader.isRunning()) { - return FetchStatus.ASYNC_FETCH_RUNNING; - } - - return FetchStatus.ALL_FETCHED; - } - public void clearCache() { this.moodleCourseDataAsyncLoader.clearCache(); } @@ -353,29 +467,10 @@ public class MoodleCourseAccess implements CourseAccessAPI { return Collections.emptyList(); } - return reduceCoursesToQuizzes(urlPrefix, courseQuizData); - } - - private ArrayList reduceCoursesToQuizzes( - final String urlPrefix, - final Collection courseQuizData) { - - final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); return courseQuizData .stream() - .reduce( - new ArrayList<>(), - (list, courseData) -> { - list.addAll(quizDataOf( - lmsSetup, - courseData, - urlPrefix)); - return list; - }, - (list1, list2) -> { - list1.addAll(list2); - return list1; - }); + .flatMap(courseData -> quizDataOf(lmsSetup, courseData, urlPrefix).stream()) + .collect(Collectors.toList()); } private List getQuizzesForIds( @@ -442,19 +537,8 @@ public class MoodleCourseAccess implements CourseAccessAPI { return courseData.values() .stream() .filter(c -> !c.quizzes.isEmpty()) - .reduce( - new ArrayList<>(), - (list, cd) -> { - list.addAll(quizDataOf( - lmsSetup, - cd, - urlPrefix)); - return list; - }, - (list1, list2) -> { - list1.addAll(list2); - return list1; - }); + .flatMap(cd -> quizDataOf(lmsSetup, cd, urlPrefix).stream()) + .collect(Collectors.toList()); } catch (final Exception e) { log.error("Unexpected error while trying to get quizzes for ids", e); @@ -723,6 +807,120 @@ public class MoodleCourseAccess implements CourseAccessAPI { .find(); } + private Collection getCoursesPage( + final MoodleAPIRestTemplate restTemplate, + final int page, + final int size) throws JsonParseException, JsonMappingException, IOException { + + final String lmsName = getApiTemplateDataSupplier().getLmsSetup().name; + try { + // get course ids per page + final LinkedMultiValueMap attributes = new LinkedMultiValueMap<>(); + attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_CRITERIA_NAME, "search"); + attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_CRITERIA_VALUE, ""); + attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_PAGE, String.valueOf(page)); + attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_PAGE_SIZE, String.valueOf(size)); + + final String courseKeyPageJSON = this.protectedMoodlePageCall + .protectedRun(() -> restTemplate.callMoodleAPIFunction( + MoodleCourseAccess.MOODLE_COURSE_SEARCH_API_FUNCTION_NAME, + attributes)) + .getOrThrow(); + + final CoursePage keysPage = this.jsonMapper.readValue( + courseKeyPageJSON, + CoursePage.class); + + if (keysPage == null) { + log.error("No CoursePage Response"); + return Collections.emptyList(); + } + + if (keysPage.warnings != null && !keysPage.warnings.isEmpty()) { + log.warn( + "There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}", + lmsName, + MoodleCourseAccess.MOODLE_COURSE_SEARCH_API_FUNCTION_NAME, + keysPage.warnings.size(), + keysPage.warnings.iterator().next().toString()); + if (log.isTraceEnabled()) { + log.trace("All warnings from Moodle: {}", keysPage.warnings.toString()); + } + } + + if (keysPage.courseKeys == null || keysPage.courseKeys.isEmpty()) { + if (log.isDebugEnabled()) { + log.debug("LMS Setup: {} No courses found on page: {}", lmsName, page); + if (log.isTraceEnabled()) { + log.trace("Moodle response: {}", courseKeyPageJSON); + } + } + return Collections.emptyList(); + } + + // get courses + final Set ids = keysPage.courseKeys + .stream() + .map(key -> key.id) + .collect(Collectors.toSet()); + + final Collection result = getCoursesForIds(restTemplate, ids) + .stream() + .filter(getCourseFilter()) + .collect(Collectors.toList()); + + if (log.isDebugEnabled()) { + log.debug("course page with {} courses, after filtering {} left", + keysPage.courseKeys.size(), + result.size()); + } + + return result; + } catch (final Exception e) { + log.error("LMS Setup: {} Unexpected error while trying to get courses page: ", lmsName, e); + return Collections.emptyList(); + } + } + + private Predicate getCourseFilter() { + final long now = Utils.getSecondsNow(); + return course -> { + if (course.start_date != null + && course.start_date < Utils.toUnixTimeInSeconds(DateTime.now(DateTimeZone.UTC).minusYears(3))) { + return false; + } + + if (course.end_date == null || course.end_date == 0 || course.end_date > now) { + return true; + } + + if (log.isDebugEnabled()) { + log.info("remove course {} end_time {} now {}", + course.short_name, + course.end_date, + now); + } + return false; + }; + } + + private Predicate getQuizFilter() { + final long now = Utils.getSecondsNow(); + return quiz -> { + if (quiz.time_close == null || quiz.time_close == 0 || quiz.time_close > now) { + return true; + } + + if (log.isDebugEnabled()) { + log.debug("remove quiz {} end_time {} now {}", + quiz.name, + quiz.time_close, + now); + } + return false; + }; + } + // ---- Mapping Classes --- /** Maps the Moodle course API course data */ @@ -887,4 +1085,55 @@ public class MoodleCourseAccess implements CourseAccessAPI { } } + @JsonIgnoreProperties(ignoreUnknown = true) + static final class CoursePage { + final Collection courseKeys; + final Collection warnings; + + public CoursePage( + @JsonProperty(value = "courses") final Collection courseKeys, + @JsonProperty(value = "warnings") final Collection warnings) { + + this.courseKeys = courseKeys; + this.warnings = warnings; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static final class CourseKey { + final String id; + final String short_name; + final String category_name; + final String sort_order; + + @JsonCreator + protected CourseKey( + @JsonProperty(value = "id") final String id, + @JsonProperty(value = "shortname") final String short_name, + @JsonProperty(value = "categoryname") final String category_name, + @JsonProperty(value = "sortorder") final String sort_order) { + + this.id = id; + this.short_name = short_name; + this.category_name = category_name; + this.sort_order = sort_order; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("CourseKey [id="); + builder.append(this.id); + builder.append(", short_name="); + builder.append(this.short_name); + builder.append(", category_name="); + builder.append(this.category_name); + builder.append(", sort_order="); + builder.append(this.sort_order); + builder.append("]"); + return builder.toString(); + } + + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseDataAsyncLoader.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseDataAsyncLoader.java index b54ff140..bd54b38c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseDataAsyncLoader.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseDataAsyncLoader.java @@ -49,6 +49,7 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseAccess.CoursePage; @Lazy @Component @@ -56,6 +57,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRe @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) /** This implements the (temporary) asynchronous fetch strategy to fetch * course and quiz data within a background task and fill up a shared cache. */ +@Deprecated public class MoodleCourseDataAsyncLoader { private static final Logger log = LoggerFactory.getLogger(MoodleCourseDataAsyncLoader.class); @@ -484,57 +486,6 @@ public class MoodleCourseDataAsyncLoader { this.newIds.clear(); } - @JsonIgnoreProperties(ignoreUnknown = true) - static final class CoursePage { - final Collection courseKeys; - final Collection warnings; - - public CoursePage( - @JsonProperty(value = "courses") final Collection courseKeys, - @JsonProperty(value = "warnings") final Collection warnings) { - - this.courseKeys = courseKeys; - this.warnings = warnings; - } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static final class CourseKey { - final String id; - final String short_name; - final String category_name; - final String sort_order; - - @JsonCreator - protected CourseKey( - @JsonProperty(value = "id") final String id, - @JsonProperty(value = "shortname") final String short_name, - @JsonProperty(value = "categoryname") final String category_name, - @JsonProperty(value = "sortorder") final String sort_order) { - - this.id = id; - this.short_name = short_name; - this.category_name = category_name; - this.sort_order = sort_order; - } - - @Override - public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append("CourseKey [id="); - builder.append(this.id); - builder.append(", short_name="); - builder.append(this.short_name); - builder.append(", category_name="); - builder.append(this.category_name); - builder.append(", sort_order="); - builder.append(this.sort_order); - builder.append("]"); - return builder.toString(); - } - - } - /** Maps the Moodle course API course data */ @JsonIgnoreProperties(ignoreUnknown = true) static final class CourseDataShort { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleLmsAPITemplateFactory.java index 8e55f800..ee49d8e5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleLmsAPITemplateFactory.java @@ -114,6 +114,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( this.jsonMapper, + this.asyncService, moodleRestTemplateFactory, asyncLoaderPrototype, this.environment); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java index 001ca9a8..7e077a89 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java @@ -106,6 +106,12 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme return null; } + @Override + public void fetchQuizzes(final FilterMap filterMap, final AsyncQuizFetchBuffer asyncQuizFetchBuffer) { + // TODO Auto-generated method stub + + } + @Override public Result getQuiz(final String id) { // TODO Auto-generated method stub diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MooldePluginLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MooldePluginLmsAPITemplateFactory.java index a7016753..a27d121b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MooldePluginLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MooldePluginLmsAPITemplateFactory.java @@ -21,7 +21,6 @@ import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService; -import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -30,7 +29,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader; @Lazy @Service @@ -80,11 +78,6 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory public Result create(final APITemplateDataSupplier apiTemplateDataSupplier) { return Result.tryCatch(() -> { - final LmsSetup lmsSetup = apiTemplateDataSupplier.getLmsSetup(); - final MoodleCourseDataAsyncLoader asyncLoaderPrototype = this.applicationContext - .getBean(MoodleCourseDataAsyncLoader.class); - asyncLoaderPrototype.init(lmsSetup.getModelId()); - final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory( this.jsonMapper, apiTemplateDataSupplier, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java index 58933c0e..f7a02766 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java @@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -182,6 +183,18 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm .collect(Collectors.toList())); } + @Override + public void fetchQuizzes(final FilterMap filterMap, final AsyncQuizFetchBuffer asyncQuizFetchBuffer) { + this.allQuizzesRequest(filterMap) + .onError(error -> asyncQuizFetchBuffer.finish(error)) + .getOr(Collections.emptyList()) + .stream() + .filter(LmsAPIService.quizFilterPredicate(filterMap)) + .forEach(qd -> asyncQuizFetchBuffer.buffer.add(qd)); + + asyncQuizFetchBuffer.finish(); + } + @Override public Result> getQuizzes(final Set ids) { return Result.tryCatch(() -> { diff --git a/src/main/resources/config/application-dev.properties b/src/main/resources/config/application-dev.properties index 3164cc4b..adbcaac7 100644 --- a/src/main/resources/config/application-dev.properties +++ b/src/main/resources/config/application-dev.properties @@ -27,3 +27,4 @@ logging.level.com.zaxxer.hikari=INFO sebserver.http.client.connect-timeout=15000 sebserver.http.client.connection-request-timeout=10000 sebserver.http.client.read-timeout=60000 + diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index f8dadeb0..b4ad9cd9 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -438,6 +438,7 @@ sebserver.quizdiscovery.list.column.endtime=End Time {0} sebserver.quizdiscovery.list.column.endtime.tooltip=The end time of the LMS exam

{0} sebserver.quizdiscovery.info.pleaseSelect=At first please select an LMS exam from the list sebserver.quizdiscovery.list.action.no.modify.privilege=No Access: A LMS exam from other institution cannot be imported. +sebserver.quizdiscovery.list.fetchnote=Note: This list is not complete yet since the service is still fetching data from LMS.
            Use the reload button on the left or the search icon from the list for update. sebserver.quizdiscovery.action.list=LMS Exam Lookup sebserver.quizdiscovery.action.import=Import as Exam diff --git a/src/test/java/ch/ethz/seb/sebserver/gbl/async/AsyncBlockingTest.java b/src/test/java/ch/ethz/seb/sebserver/gbl/async/AsyncBlockingTest.java new file mode 100644 index 00000000..d2cb9d65 --- /dev/null +++ b/src/test/java/ch/ethz/seb/sebserver/gbl/async/AsyncBlockingTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gbl.async; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.Future; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = { AsyncServiceSpringConfig.class, AsyncRunner.class, AsyncService.class }) +public class AsyncBlockingTest { + + private static final int TASKS = 10; + + @Autowired + AsyncRunner asyncRunner; + + @Test + public void testNoneBlocking() { + final Collection> features = new ArrayList<>(); + for (int i = 0; i < TASKS; i++) { + final Future runAsync = this.asyncRunner.runAsync(this::doAsync); + //System.out.println("*********** run async: " + i); + features.add(runAsync); + } + assertEquals(TASKS, features.size()); + final String reduce = features.stream() + .map(this::getFromFuture) + .reduce("", (s1, s2) -> s1 + s2); + //System.out.println(reduce); + final int countMatches = StringUtils.countMatches(reduce, "DONE"); + assertEquals(TASKS, countMatches); + + // try to get again, are they cached? --> yes they are! + final String reduce2 = features.stream() + .map(this::getFromFuture) + .reduce("", (s1, s2) -> s1 + s2); + //System.out.println(reduce2); + final int countMatches2 = StringUtils.countMatches(reduce2, "DONE"); + assertEquals(TASKS, countMatches2); + } + + private String getFromFuture(final Future future) { + try { + return future.get(); + } catch (final Exception e) { + return e.getMessage(); + } + } + + private String doAsync() { + try { + Thread.sleep(1000); + } catch (final InterruptedException e) { + e.printStackTrace(); + } + return Thread.currentThread().getName() + " --> DONE\n"; + } + +} diff --git a/src/test/java/ch/ethz/seb/sebserver/gbl/model/institution/InstitutionTest.java b/src/test/java/ch/ethz/seb/sebserver/gbl/model/institution/InstitutionTest.java index 8928cf6e..ece1852b 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gbl/model/institution/InstitutionTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gbl/model/institution/InstitutionTest.java @@ -49,7 +49,7 @@ public class InstitutionTest { //final ObjectWriter writerWithDefaultPrettyPrinter = jsonMapper.writerWithDefaultPrettyPrinter(); String json = jsonMapper.writeValueAsString(page); assertEquals( - "{\"number_of_pages\":2,\"page_number\":1,\"sort\":\"name\",\"content\":[{\"id\":1,\"name\":\"InstOne\",\"urlSuffix\":\"one\",\"logoImage\":\"\",\"themeName\":\"\",\"active\":true},{\"id\":2,\"name\":\"InstTwo\",\"urlSuffix\":\"two\",\"logoImage\":\"\",\"themeName\":\"\",\"active\":true},{\"id\":3,\"name\":\"InstThree\",\"urlSuffix\":\"three\",\"logoImage\":\"\",\"themeName\":\"\",\"active\":true}],\"page_size\":3}", + "{\"number_of_pages\":2,\"page_number\":1,\"sort\":\"name\",\"content\":[{\"id\":1,\"name\":\"InstOne\",\"urlSuffix\":\"one\",\"logoImage\":\"\",\"themeName\":\"\",\"active\":true},{\"id\":2,\"name\":\"InstTwo\",\"urlSuffix\":\"two\",\"logoImage\":\"\",\"themeName\":\"\",\"active\":true},{\"id\":3,\"name\":\"InstThree\",\"urlSuffix\":\"three\",\"logoImage\":\"\",\"themeName\":\"\",\"active\":true}],\"complete\":true,\"page_size\":3}", json); final List namesList = page.content.stream() diff --git a/src/test/java/ch/ethz/seb/sebserver/gbl/model/user/UserInfoTest.java b/src/test/java/ch/ethz/seb/sebserver/gbl/model/user/UserInfoTest.java index 665dddbe..e5a8d915 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gbl/model/user/UserInfoTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gbl/model/user/UserInfoTest.java @@ -43,7 +43,7 @@ public class UserInfoTest { //final ObjectWriter writerWithDefaultPrettyPrinter = jsonMapper.writerWithDefaultPrettyPrinter(); final String json = jsonMapper.writeValueAsString(page); assertEquals( - "{\"number_of_pages\":2,\"page_number\":1,\"sort\":\"name\",\"content\":[{\"uuid\":\"id1\",\"institutionId\":1,\"creationDate\":\"1970-01-01T00:00:00.000Z\",\"name\":\"user1\",\"surname\":\"\",\"username\":\"user1\",\"email\":\"user1@inst2.none\",\"active\":true,\"language\":\"en\",\"timezone\":\"UTC\",\"userRoles\":[\"EXAM_ADMIN\"]},{\"uuid\":\"id2\",\"institutionId\":3,\"creationDate\":\"1970-01-01T00:00:00.000Z\",\"name\":\"user2\",\"surname\":\"\",\"username\":\"user2\",\"email\":\"user2@inst2.none\",\"active\":true,\"language\":\"en\",\"timezone\":\"UTC\",\"userRoles\":[\"EXAM_ADMIN\"]},{\"uuid\":\"id3\",\"institutionId\":4,\"creationDate\":\"1970-01-01T00:00:00.000Z\",\"name\":\"user3\",\"surname\":\"\",\"username\":\"user3\",\"email\":\"user3@inst2.none\",\"active\":false,\"language\":\"de\",\"timezone\":\"UTC\",\"userRoles\":[\"EXAM_ADMIN\"]}],\"page_size\":3}", + "{\"number_of_pages\":2,\"page_number\":1,\"sort\":\"name\",\"content\":[{\"uuid\":\"id1\",\"institutionId\":1,\"creationDate\":\"1970-01-01T00:00:00.000Z\",\"name\":\"user1\",\"surname\":\"\",\"username\":\"user1\",\"email\":\"user1@inst2.none\",\"active\":true,\"language\":\"en\",\"timezone\":\"UTC\",\"userRoles\":[\"EXAM_ADMIN\"]},{\"uuid\":\"id2\",\"institutionId\":3,\"creationDate\":\"1970-01-01T00:00:00.000Z\",\"name\":\"user2\",\"surname\":\"\",\"username\":\"user2\",\"email\":\"user2@inst2.none\",\"active\":true,\"language\":\"en\",\"timezone\":\"UTC\",\"userRoles\":[\"EXAM_ADMIN\"]},{\"uuid\":\"id3\",\"institutionId\":4,\"creationDate\":\"1970-01-01T00:00:00.000Z\",\"name\":\"user3\",\"surname\":\"\",\"username\":\"user3\",\"email\":\"user3@inst2.none\",\"active\":false,\"language\":\"de\",\"timezone\":\"UTC\",\"userRoles\":[\"EXAM_ADMIN\"]}],\"complete\":true,\"page_size\":3}", json); } diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java index dff620f6..31fd68e6 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java @@ -724,6 +724,15 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { assertNotNull(quizPageCall); assertFalse(quizPageCall.hasError()); quizPage = quizPageCall.get(); + while (!quizPage.complete) { + // get again to complete + quizPageCall = restService + .getBuilder(GetQuizPage.class) + .call(); + assertNotNull(quizPageCall); + assertFalse(quizPageCall.hasError()); + quizPage = quizPageCall.get(); + } assertFalse(quizPage.isEmpty()); // change the name of LMS Setup and check modification update diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccessTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccessTest.java index debbd68d..db032f87 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccessTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccessTest.java @@ -22,6 +22,8 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import ch.ethz.seb.sebserver.gbl.api.JSONMapper; +import ch.ethz.seb.sebserver.gbl.async.AsyncRunner; +import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType; @@ -34,6 +36,8 @@ public class MoodleCourseAccessTest { @Mock Environment env = new MockEnvironment(); + @Mock + AsyncService asyncService = new AsyncService(new AsyncRunner()); @Test public void testGetExamineeAccountDetails() { @@ -72,6 +76,7 @@ public class MoodleCourseAccessTest { final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( new JSONMapper(), + this.asyncService, moodleRestTemplateFactory, null, this.env); @@ -119,6 +124,7 @@ public class MoodleCourseAccessTest { final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( new JSONMapper(), + this.asyncService, moodleRestTemplateFactory, null, this.env); @@ -140,6 +146,7 @@ public class MoodleCourseAccessTest { final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( new JSONMapper(), + this.asyncService, moodleRestTemplateFactory, null, this.env); @@ -160,6 +167,7 @@ public class MoodleCourseAccessTest { final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( new JSONMapper(), + this.asyncService, moodleRestTemplateFactory, null, this.env);