SEBSERV-338 and SEBSERV-301

This commit is contained in:
anhefti 2022-12-15 14:35:27 +01:00
parent 71ae9fc755
commit 7169fa0808
35 changed files with 1250 additions and 316 deletions

View file

@ -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();
}
}

View file

@ -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 <T> the type of the CircuitBreaker

View file

@ -27,6 +27,7 @@ public final class Page<T> {
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<T> {
public final Integer pageSize;
@JsonProperty(ATTR_SORT)
public final String sort;
@JsonProperty(ATTR_COMPLETE)
public final boolean complete;
@JsonProperty(ATTR_CONTENT)
public final List<T> content;
@ -46,13 +49,29 @@ public final class Page<T> {
@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<T> content) {
@JsonProperty(ATTR_CONTENT) final Collection<T> 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<T> 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<T> {
return (this.pageSize != null) ? this.pageSize : -1;
}
public boolean isComplete() {
return this.complete;
}
public Collection<T> getContent() {
return this.content;
}

View file

@ -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<QuizData> 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<QuizData> 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<QuizData> 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<QuizData> 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<QuizData> 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);
}
}

View file

@ -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);
}
}

View file

@ -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<QuizData> 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<QuizData> 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);
}
}

View file

@ -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 */

View file

@ -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;

View file

@ -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();

View file

@ -77,6 +77,7 @@ public class EntityTable<ROW extends ModelIdAware> {
private final String sortOrderAttrName;
private final String currentPageAttrName;
private final boolean markupEnabled;
private final Consumer<EntityTable<ROW>> pageReloadListener;
final PageService pageService;
final WidgetFactory widgetFactory;
@ -109,6 +110,7 @@ public class EntityTable<ROW extends ModelIdAware> {
PageSortOrder sortOrder = PageSortOrder.ASCENDING;
boolean columnsWithSameWidth = true;
boolean hideNavigation;
boolean isComplete = true;
EntityTable(
final String name,
@ -128,7 +130,8 @@ public class EntityTable<ROW extends ModelIdAware> {
final Consumer<EntityTable<ROW>> selectionListener,
final Consumer<Integer> contentChangeListener,
final String defaultSortColumn,
final PageSortOrder defaultSortOrder) {
final PageSortOrder defaultSortOrder,
final Consumer<EntityTable<ROW>> pageReloadListener) {
this.name = name;
this.filterAttrName = name + "_filter";
@ -139,6 +142,7 @@ public class EntityTable<ROW extends ModelIdAware> {
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<ROW extends ModelIdAware> {
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<ROW extends ModelIdAware> {
return this.name;
}
public boolean isComplete() {
return this.isComplete;
}
public String getSortColumn() {
return this.sortColumn;
}
@ -510,7 +520,7 @@ public class EntityTable<ROW extends ModelIdAware> {
this.table.removeAll();
// get page data and create rows
this.pageSupplier.newBuilder()
final Page<ROW> 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<ROW extends ModelIdAware> {
.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<ROW> createTableRowsFromPage(final Page<ROW> page) {

View file

@ -52,6 +52,7 @@ public class TableBuilder<ROW extends ModelIdAware> {
private boolean markupEnabled = false;
private String defaultSortColumn = null;
private PageSortOrder defaultSortOrder = PageSortOrder.ASCENDING;
private Consumer<EntityTable<ROW>> pageReloadListener;
public TableBuilder(
final String name,
@ -203,6 +204,11 @@ public class TableBuilder<ROW extends ModelIdAware> {
return this;
}
public TableBuilder<ROW> withPageReloadListener(final Consumer<EntityTable<ROW>> pageReloadListener) {
this.pageReloadListener = pageReloadListener;
return this;
}
public EntityTable<ROW> compose(final PageContext pageContext) {
return new EntityTable<>(
this.name,
@ -224,7 +230,8 @@ public class TableBuilder<ROW extends ModelIdAware> {
this.selectionListener,
this.contentChangeListener,
this.defaultSortColumn,
this.defaultSortOrder);
this.defaultSortOrder,
this.pageReloadListener);
}
}

View file

@ -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 <ROW extends ModelIdAware> Page<ROW> update(final Page<ROW> 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(

View file

@ -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);

View file

@ -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<List<QuizData>> 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<Chapters> getCourseChapters(String courseId);
default FetchStatus getFetchStatus() {
return FetchStatus.ALL_FETCHED;
static class AsyncQuizFetchBuffer {
public List<QuizData> 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;
}
}
}

View file

@ -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<List<QuizData>, Page<QuizData>> 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<QuizData>, List<QuizData>> quizzesSortFunction(final String sort) {
return quizzes -> {
quizzes.sort(QuizData.getComparator(sort));
return quizzes;
};
}
}

View file

@ -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<Page<QuizData>> requestQuizDataPage(
final int pageNumber,
final int pageSize,
final String sort,
final FilterMap filterMap,
Function<String, Result<LmsAPITemplate>> 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> quizData;
public final boolean completed;
public final long timestamp;
public LookupResult(
final List<QuizData> 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<LookupResult, Page<QuizData>> quizzesToPageFunction(
final String sortAttribute,
final int pageNumber,
final int pageSize) {
return lookupResult -> {
final List<QuizData> 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<LookupResult, LookupResult> quizzesSortFunction(final String sort) {
return lookupResult -> {
lookupResult.quizData.sort(QuizData.getComparator(sort));
return lookupResult;
};
}
}

View file

@ -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<LmsType, LmsAPITemplateFactory> templateFactories;
// TODO use also EHCache here
private final Map<CacheKey, LmsAPITemplate> cache = new ConcurrentHashMap<>();
public LmsAPIServiceImpl(
final WebserviceInfo webserviceInfo,
final LmsSetupDAO lmsSetupDAO,
final ClientCredentialService clientCredentialService,
final QuizLookupService quizLookupService,
final Collection<LmsAPITemplateFactory> lmsAPITemplateFactories) {
this.webserviceInfo = webserviceInfo;
this.lmsSetupDAO = lmsSetupDAO;
this.clientCredentialService = clientCredentialService;
this.quizLookupService = quizLookupService;
final Map<LmsType, LmsAPITemplateFactory> 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<List<QuizData>> 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<List<QuizData>> 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;
}
}

View file

@ -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<LmsSetupTestResult> lmsTestRequest;
/** CircuitBreaker for protected quiz and course data requests */
private final CircuitBreaker<List<QuizData>> allQuizzesRequest;
/** CircuitBreaker for protected quiz and course data requests */
private final CircuitBreaker<Collection<QuizData>> quizzesRequest;
/** CircuitBreaker for protected quiz and course data requests */
private final CircuitBreaker<QuizData> 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<List<QuizData>> 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

View file

@ -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<String, AsyncLookup> 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<String> 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<String> 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<Page<QuizData>> requestQuizDataPage(
final int pageNumber,
final int pageSize,
final String sort,
final FilterMap filterMap,
final Function<String, Result<LmsAPITemplate>> lmsAPITemplateSupplier) {
return getAllQuizzesFromLMSSetups(filterMap, lmsAPITemplateSupplier)
.map(QuizLookupService.quizzesSortFunction(sort))
.map(QuizLookupService.quizzesToPageFunction(
sort,
pageNumber,
pageSize));
}
private Result<LookupResult> getAllQuizzesFromLMSSetups(
final FilterMap filterMap,
final Function<String, Result<LmsAPITemplate>> 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<String, Result<LmsAPITemplate>> 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<String, Result<LmsAPITemplate>> 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<List<AsyncQuizFetchBuffer>> 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<AsyncQuizFetchBuffer> 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<AsyncQuizFetchBuffer> asyncBuffers;
final long timeCreated;
long timeCompleted = Long.MAX_VALUE;
public AsyncLookup(
final long institutionId,
final String userId,
final LookupFilterCriteria lookupFilterCriteria,
final Collection<AsyncQuizFetchBuffer> asyncBuffers) {
this.institutionId = institutionId;
this.userId = userId;
this.lookupFilterCriteria = lookupFilterCriteria;
this.asyncBuffers = asyncBuffers;
this.timeCreated = Utils.getMillisecondsNow();
}
LookupResult getAvailable() {
boolean running = false;
final List<QuizData> 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);
}
}
}

View file

@ -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<Collection<QuizData>> getQuizzes(final Set<String> ids) {
return Result.tryCatch(() -> {

View file

@ -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<QuizData> 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<QuizData> coursesToQuizzes(
final LmsSetup lmsSetup,
final String externalStartURI,
final Collection<CourseData> courses) {
if (courses == null) {
return Stream.empty();
}
return courses
.stream()
.map(cd -> quizDataOf(lmsSetup, cd, externalStartURI));
}
}

View file

@ -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<QuizData> 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<QuizData> 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<Collection<QuizData>> getQuizzes(final Set<String> ids) {
return Result.tryCatch(() -> {

View file

@ -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<String> 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<String, CourseData> courseData = new HashMap<>();
final Collection<CourseData> 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<String, String> attributes = new LinkedMultiValueMap<>();
final List<String> 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<Collection<QuizData>> getQuizzes(final Set<String> 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<QuizData> reduceCoursesToQuizzes(
final String urlPrefix,
final Collection<CourseDataShort> 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<QuizData> 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<CourseData> 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<String, String> 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<String> ids = keysPage.courseKeys
.stream()
.map(key -> key.id)
.collect(Collectors.toSet());
final Collection<CourseData> 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<CourseData> 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<CourseQuiz> 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<CourseKey> courseKeys;
final Collection<Warning> warnings;
public CoursePage(
@JsonProperty(value = "courses") final Collection<CourseKey> courseKeys,
@JsonProperty(value = "warnings") final Collection<Warning> 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();
}
}
}

View file

@ -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<CourseKey> courseKeys;
final Collection<Warning> warnings;
public CoursePage(
@JsonProperty(value = "courses") final Collection<CourseKey> courseKeys,
@JsonProperty(value = "warnings") final Collection<Warning> 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 {

View file

@ -114,6 +114,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess(
this.jsonMapper,
this.asyncService,
moodleRestTemplateFactory,
asyncLoaderPrototype,
this.environment);

View file

@ -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<QuizData> getQuiz(final String id) {
// TODO Auto-generated method stub

View file

@ -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<LmsAPITemplate> 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,

View file

@ -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<Collection<QuizData>> getQuizzes(final Set<String> ids) {
return Result.tryCatch(() -> {

View file

@ -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

View file

@ -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<br/><br/>{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=<b>Note:</b> This list is not complete yet since the service is still fetching data from LMS.<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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

View file

@ -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<Future<String>> features = new ArrayList<>();
for (int i = 0; i < TASKS; i++) {
final Future<String> 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<String> 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";
}
}

View file

@ -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<EntityName> namesList = page.content.stream()

View file

@ -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);
}

View file

@ -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

View file

@ -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);