add warning if course imported twice and seb restriction readonly view

This commit is contained in:
anhefti 2020-03-30 13:41:54 +02:00
parent b6466d8f1d
commit be7a7cef77
14 changed files with 174 additions and 42 deletions

View file

@ -118,6 +118,7 @@ public final class API {
public static final String EXAM_ADMINISTRATION_CONSISTENCY_CHECK_PATH_SEGMENT = "/check-consistency";
public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_PATH_SEGMENT = "/seb-restriction";
public static final String EXAM_ADMINISTRATION_CHECK_RESTRICTION_PATH_SEGMENT = "/check-seb-restriction";
public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported";
public static final String EXAM_INDICATOR_ENDPOINT = "/indicator";

View file

@ -400,6 +400,7 @@ public class ExamForm implements TemplateComposer {
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(exam.lmsSetupId))
.call()
.getOrThrow().lmsType.name())
.withAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY, String.valueOf(!modifyGrant))
.noEventPropagation()
.publishIf(() -> sebRestrictionAvailable && readonly)

View file

@ -16,6 +16,7 @@ import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.widgets.Composite;
@ -104,6 +105,12 @@ public class ExamSebRestrictionSettings {
final PageContext pageContext,
final FormHandle<?> formHandle) {
final boolean isReadonly = BooleanUtils.toBoolean(
pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
if (isReadonly) {
return true;
}
final EntityKey entityKey = pageContext.getEntityKey();
final LmsType lmsType = getLmsType(pageContext);
SebRestriction bodyValue = null;
@ -170,6 +177,8 @@ public class ExamSebRestrictionSettings {
final ResourceService resourceService = this.pageService.getResourceService();
final EntityKey entityKey = this.pageContext.getEntityKey();
final LmsType lmsType = getLmsType(this.pageContext);
final boolean isReadonly = BooleanUtils.toBoolean(
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
final Composite content = this.pageService
.getWidgetFactory()
@ -189,12 +198,12 @@ public class ExamSebRestrictionSettings {
formContext)
.withDefaultSpanInput(6)
.withEmptyCellSeparation(false)
.readonly(false)
.readonly(isReadonly)
.addField(FormBuilder.text(
"Info",
SEB_RESTRICTION_FORM_INFO,
pageService.getI18nSupport().getText(SEB_RESTRICTION_FORM_INFO_TEXT))
this.pageService.getI18nSupport().getText(SEB_RESTRICTION_FORM_INFO_TEXT))
.asArea(50)
.asHTML()
.readonly(true))
@ -254,6 +263,7 @@ public class ExamSebRestrictionSettings {
return () -> formHandle;
}
}
private static LmsType getLmsType(final PageContext pageContext) {

View file

@ -13,7 +13,6 @@ import java.util.Collection;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import ch.ethz.seb.sebserver.gbl.Constants;
import org.eclipse.swt.widgets.Composite;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
@ -21,6 +20,8 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
@ -43,6 +44,7 @@ import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamImported;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.GetQuizPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.GrantCheck;
@ -55,7 +57,7 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@Lazy
@Component
@GuiProfile
public class QuizDiscoveryList implements TemplateComposer {
public class QuizLookupList implements TemplateComposer {
// localized text keys
@ -93,6 +95,8 @@ public class QuizDiscoveryList implements TemplateComposer {
new LocTextKey("sebserver.quizdiscovery.quiz.details.endtime");
private final static LocTextKey NO_IMPORT_OF_OUT_DATED_QUIZ =
new LocTextKey("sebserver.quizdiscovery.quiz.import.out.dated");
private final static LocTextKey TEXT_KEY_CONFIRM_EXISTING =
new LocTextKey("sebserver.quizdiscovery.quiz.import.existing.confirm");
private final static String TEXT_KEY_ADDITIONAL_ATTR_PREFIX =
"sebserver.quizdiscovery.quiz.details.additional.";
@ -109,7 +113,7 @@ public class QuizDiscoveryList implements TemplateComposer {
private final PageService pageService;
private final int pageSize;
protected QuizDiscoveryList(
protected QuizLookupList(
final PageService pageService,
final ResourceService resourceService,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
@ -232,6 +236,7 @@ public class QuizDiscoveryList implements TemplateComposer {
.publishIf(table::hasAnyContent, false)
.newAction(ActionDefinition.QUIZ_DISCOVERY_EXAM_IMPORT)
.withConfirm(importQuizConfirm(table, restService))
.withSelect(
table::getSelection,
action -> this.importQuizData(action, table),
@ -244,7 +249,31 @@ public class QuizDiscoveryList implements TemplateComposer {
.apply(String.valueOf(quizData.lmsSetupId));
}
private PageAction importQuizData(final PageAction action, final EntityTable<QuizData> table) {
private Function<PageAction, LocTextKey> importQuizConfirm(
final EntityTable<QuizData> table,
final RestService restService) {
return action -> {
action.getSingleSelection();
final QuizData selectedROWData = table.getSingleSelectedROWData();
final Collection<EntityKey> existingImports = restService.getBuilder(CheckExamImported.class)
.withURIVariable(API.PARAM_MODEL_ID, selectedROWData.id)
.call()
.getOrThrow();
if (existingImports != null && !existingImports.isEmpty()) {
return TEXT_KEY_CONFIRM_EXISTING;
} else {
return null;
}
};
}
private PageAction importQuizData(
final PageAction action,
final EntityTable<QuizData> table) {
action.getSingleSelection();
final QuizData selectedROWData = table.getSingleSelectedROWData();

View file

@ -21,7 +21,7 @@ import ch.ethz.seb.sebserver.gui.content.LmsSetupList;
import ch.ethz.seb.sebserver.gui.content.MonitoringClientConnection;
import ch.ethz.seb.sebserver.gui.content.MonitoringRunningExam;
import ch.ethz.seb.sebserver.gui.content.MonitoringRunningExamList;
import ch.ethz.seb.sebserver.gui.content.QuizDiscoveryList;
import ch.ethz.seb.sebserver.gui.content.QuizLookupList;
import ch.ethz.seb.sebserver.gui.content.SebClientConfigForm;
import ch.ethz.seb.sebserver.gui.content.SebClientConfigList;
import ch.ethz.seb.sebserver.gui.content.SebClientLogs;
@ -52,7 +52,7 @@ public enum PageStateDefinitionImpl implements PageStateDefinition {
LMS_SETUP_VIEW(Type.FORM_VIEW, LmsSetupForm.class, ActivityDefinition.LMS_SETUP),
LMS_SETUP_EDIT(Type.FORM_EDIT, LmsSetupForm.class, ActivityDefinition.LMS_SETUP),
QUIZ_LIST(Type.LIST_VIEW, QuizDiscoveryList.class, ActivityDefinition.QUIZ_DISCOVERY),
QUIZ_LIST(Type.LIST_VIEW, QuizLookupList.class, ActivityDefinition.QUIZ_DISCOVERY),
EXAM_LIST(Type.LIST_VIEW, ExamList.class, ActivityDefinition.EXAM),
EXAM_VIEW(Type.FORM_VIEW, ExamForm.class, ActivityDefinition.EXAM),

View file

@ -14,7 +14,6 @@ import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
@ -28,6 +27,7 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.util.Tuple;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.widget.Selection;
import ch.ethz.seb.sebserver.gui.widget.Selection.Type;
@ -73,7 +73,7 @@ public final class SelectionFieldBuilder extends FieldBuilder<String> {
this.type,
fieldGrid,
this.itemsSupplier,
(builder.pageService.getFormTooltipMode() == PageService.FormTooltipMode.INPUT) ? tooltip : null,
(builder.pageService.getFormTooltipMode() == PageService.FormTooltipMode.INPUT) ? this.tooltip : null,
null,
actionKey);
@ -108,7 +108,7 @@ public final class SelectionFieldBuilder extends FieldBuilder<String> {
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true);
label.setLayoutData(gridData);
label.setText(this.value);
label.setText((this.value != null) ? this.value : Constants.EMPTY_NOTE);
} else {
final Collection<String> keys = Arrays.asList(StringUtils.split(this.value, Constants.LIST_SEPARATOR));
this.itemsSupplier.get()

View file

@ -32,6 +32,7 @@ public interface PageContext {
String PAGE_TEMPLATE_COMPOSER_NAME = "ATTR_PAGE_TEMPLATE_COMPOSER_NAME";
String READ_ONLY = "READ_ONLY";
String FORCE_READ_ONLY = "FORCE_READ_ONLY";
String READ_ONLY_FROM = "READ_ONLY_FROM";
String ENTITY_ID = "ENTITY_ID";

View file

@ -129,8 +129,9 @@ public interface PageService {
*
* @param table the entity table
* @param noSelectionText LocTextKey for missing selection message
* @param testBeforeActivation a function to test before activation. This function shall throw an error if test fails.
* My be null if no specific test is needed before activation
* @param testBeforeActivation a function to test before activation. This function shall throw an error if test
* fails.
* My be null if no specific test is needed before activation
* @return page action execution function for switching the activity */
<T extends Entity & Activatable> Function<PageAction, PageAction> activationToggleActionFunction(
EntityTable<T> table,
@ -144,8 +145,8 @@ public interface PageService {
* @param noSelectionText LocTextKey for missing selection message
* @return page action execution function for switching the activity */
default <T extends Entity & Activatable> Function<PageAction, PageAction> activationToggleActionFunction(
EntityTable<T> table,
LocTextKey noSelectionText) {
final EntityTable<T> table,
final LocTextKey noSelectionText) {
return this.activationToggleActionFunction(table, noSelectionText, null);
}
@ -399,7 +400,7 @@ public interface PageService {
private PageContext pageContext;
private ActionDefinition definition;
private Supplier<LocTextKey> confirm;
private Function<PageAction, LocTextKey> confirm;
private LocTextKey successMessage;
private Supplier<Set<EntityKey>> selectionSupplier;
private LocTextKey noSelectionMessage;
@ -508,6 +509,11 @@ public interface PageService {
}
public PageActionBuilder withConfirm(final Supplier<LocTextKey> confirm) {
this.confirm = action -> confirm.get();
return this;
}
public PageActionBuilder withConfirm(final Function<PageAction, LocTextKey> confirm) {
this.confirm = confirm;
return this;
}

View file

@ -35,7 +35,7 @@ public final class PageAction {
private static final Logger log = LoggerFactory.getLogger(PageAction.class);
public final ActionDefinition definition;
private final Supplier<LocTextKey> confirm;
private final Function<PageAction, LocTextKey> confirm;
private final Supplier<Set<EntityKey>> selectionSupplier;
private final LocTextKey noSelectionMessage;
private PageContext pageContext;
@ -48,7 +48,7 @@ public final class PageAction {
public PageAction(
final ActionDefinition definition,
final Supplier<LocTextKey> confirm,
final Function<PageAction, LocTextKey> confirm,
final LocTextKey successMessage,
final Supplier<Set<EntityKey>> selectionSupplier,
final LocTextKey noSelectionMessage,
@ -154,7 +154,7 @@ public final class PageAction {
}
}
final LocTextKey confirmMessage = this.confirm.get();
final LocTextKey confirmMessage = this.confirm.apply(this);
if (confirmMessage != null) {
this.pageContext.applyConfirmDialog(confirmMessage,
confirm -> callback.accept((confirm)
@ -198,9 +198,6 @@ public final class PageAction {
PageAction.this.getName(),
e.getMessage(),
Utils.getErrorCauseMessage(e));
PageAction.this.pageContext.notifyError(
PageContext.UNEXPECTED_ERROR_KEY,
e);
return Result.ofError(e);
} catch (final Exception e) {
log.error("Failed to execute action: {} | error: {} | cause: {}",

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2020 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.gui.service.remote.webservice.api.exam;
import java.util.Collection;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class CheckExamImported extends RestCall<Collection<EntityKey>> {
public CheckExamImported() {
super(new TypeKey<>(
CallType.UNDEFINED,
EntityType.EXAM,
new TypeReference<Collection<EntityKey>>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_ADMINISTRATION_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT);
}
}

View file

@ -28,6 +28,8 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
* happened */
Result<Collection<Long>> allIdsOfInstitution(Long institutionId);
Result<Collection<Long>> allByQuizId(String quizId);
/** Updates the exam status for specified exam
*
* @param examId The exam identifier

View file

@ -111,6 +111,24 @@ public class ExamDAOImpl implements ExamDAO {
.flatMap(this::toDomainModel);
}
@Override
public Result<Collection<Long>> allByQuizId(final String quizId) {
return Result.tryCatch(() -> {
return this.examRecordMapper.selectByExample()
.where(
ExamRecordDynamicSqlSupport.externalId,
isEqualToWhenPresent(quizId))
.and(
ExamRecordDynamicSqlSupport.active,
isEqualToWhenPresent(BooleanUtils.toIntegerObject(true)))
.build()
.execute()
.stream()
.map(rec -> rec.getId())
.collect(Collectors.toList());
});
}
@Override
@Transactional(readOnly = true)
public Result<Collection<Exam>> allMatching(final FilterMap filterMap, final Predicate<Exam> predicate) {
@ -258,24 +276,24 @@ public class ExamDAOImpl implements ExamDAO {
// used to save instead of create a new one
if (records != null && records.size() > 0) {
final ExamRecord examRecord = records.get(0);
// if another institution tries to import an exam that already exists
if (!exam.institutionId.equals(examRecord.getInstitutionId())) {
throw new IllegalStateException("Exam cannot be imported twice from different institutions");
}
final ExamRecord newRecord = new ExamRecord(
examRecord.getId(),
null, null, null, null, null,
(exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(),
null, // quitPassword
null, // browser keys
null, // status
null, // lmsSebRestriction (deprecated)
null, // updating
null, // lastUpdate
BooleanUtils.toIntegerObject(exam.active));
// if the same institution tries to import an exam that already exists
// open the existing. otherwise create new one if requested
if (exam.institutionId.equals(examRecord.getInstitutionId())) {
final ExamRecord newRecord = new ExamRecord(
examRecord.getId(),
null, null, null, null, null,
(exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(),
null, // quitPassword
null, // browser keys
null, // status
null, // lmsSebRestriction (deprecated)
null, // updating
null, // lastUpdate
BooleanUtils.toIntegerObject(exam.active));
this.examRecordMapper.updateByPrimaryKeySelective(newRecord);
return this.examRecordMapper.selectByPrimaryKey(examRecord.getId());
this.examRecordMapper.updateByPrimaryKeySelective(newRecord);
return this.examRecordMapper.selectByPrimaryKey(examRecord.getId());
}
}
final ExamRecord examRecord = new ExamRecord(

View file

@ -45,6 +45,7 @@ import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.Domain.EXAM;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.PageSortOrder;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
@ -208,6 +209,27 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
}
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Collection<EntityKey> checkImported(
@PathVariable final String modelId,
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
checkReadPrivilege(institutionId);
return this.examDAO.allByQuizId(modelId)
.map(ids -> ids
.stream()
.map(id -> new EntityKey(id, EntityType.EXAM))
.collect(Collectors.toList()))
.getOrThrow();
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_CONSISTENCY_CHECK_PATH_SEGMENT,
@ -226,6 +248,9 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
.getOrThrow();
}
// ****************************************************************************
// **** SEB Restriction
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_CHECK_RESTRICTION_PATH_SEGMENT,
@ -244,9 +269,6 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
.getOrThrow();
}
// ****************************************************************************
// **** SEB Restriction
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_SEB_RESTRICTION_PATH_SEGMENT,

View file

@ -350,6 +350,7 @@ sebserver.quizdiscovery.action.list=LMS Exam Lookup
sebserver.quizdiscovery.action.import=Import as Exam
sebserver.quizdiscovery.quiz.import.out.dated=The Selected LMS exam is is already finished and can't be imported
sebserver.quizdiscovery.action.details=Show LMS Exam Details
sebserver.quizdiscovery.quiz.import.existing.confirm=This course was already imported and import it twice may lead to<br/> unexpected behavior within automated SEB restriction on LMS.<br/><br/> Do you want to import this course as exam anyways?
sebserver.quizdiscovery.quiz.details.title=LMS Exam Details
sebserver.quizdiscovery.quiz.details.institution=Institution