SEBSERV-338 and SEBSERV-301
This commit is contained in:
parent
71ae9fc755
commit
7169fa0808
35 changed files with 1250 additions and 316 deletions
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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(() -> {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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(() -> {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -114,6 +114,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
|
||||
final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess(
|
||||
this.jsonMapper,
|
||||
this.asyncService,
|
||||
moodleRestTemplateFactory,
|
||||
asyncLoaderPrototype,
|
||||
this.environment);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(() -> {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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/> 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
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue