SEBSERV-212 prevent double-creation of exam for a quiz on the same

institution. Do also not forward and load the existing one. This seems
to cause some trouble when be done sometimes.
This commit is contained in:
anhefti 2021-07-14 16:33:33 +02:00
parent eb7042acf6
commit 086bc5ef3b
7 changed files with 73 additions and 32 deletions

View file

@ -12,6 +12,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.swt.widgets.Composite;
import org.joda.time.DateTime;
@ -100,6 +101,8 @@ public class QuizLookupList implements TemplateComposer {
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 LocTextKey TEXT_KEY_EXISTING =
new LocTextKey("sebserver.quizdiscovery.quiz.import.existing");
private final static String TEXT_KEY_ADDITIONAL_ATTR_PREFIX =
"sebserver.quizdiscovery.quiz.details.additional.";
@ -147,6 +150,7 @@ public class QuizLookupList implements TemplateComposer {
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
final Long institutionId = currentUser.get().institutionId;
// content page layout with title
final Composite content = this.widgetFactory.defaultPageLayout(
@ -242,10 +246,10 @@ public class QuizLookupList implements TemplateComposer {
.publish(false)
.newAction(ActionDefinition.QUIZ_DISCOVERY_EXAM_IMPORT)
.withConfirm(importQuizConfirm(table, restService))
.withConfirm(importQuizConfirm(institutionId, table, restService))
.withSelect(
table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION),
action -> this.importQuizData(action, table),
action -> this.importQuizData(institutionId, action, table, restService),
EMPTY_SELECTION_TEXT)
.publishIf(() -> examGrant.im(), false);
}
@ -257,6 +261,7 @@ public class QuizLookupList implements TemplateComposer {
}
private Function<PageAction, LocTextKey> importQuizConfirm(
final Long institutionId,
final EntityTable<QuizData> table,
final RestService restService) {
@ -264,12 +269,15 @@ public class QuizLookupList implements TemplateComposer {
action.getSingleSelection();
final QuizData selectedROWData = table.getSingleSelectedROWData();
final Collection<EntityKey> existingImports = restService.getBuilder(CheckExamImported.class)
final Collection<Long> existingImports = restService.getBuilder(CheckExamImported.class)
.withURIVariable(API.PARAM_MODEL_ID, selectedROWData.id)
.call()
.getOrThrow();
.getOrThrow()
.stream()
.map(key -> Long.valueOf(key.modelId))
.collect(Collectors.toList());
if (existingImports != null && !existingImports.isEmpty()) {
if (existingImports != null && !existingImports.contains(institutionId)) {
return TEXT_KEY_CONFIRM_EXISTING;
} else {
return null;
@ -278,8 +286,10 @@ public class QuizLookupList implements TemplateComposer {
}
private PageAction importQuizData(
final Long institutionId,
final PageAction action,
final EntityTable<QuizData> table) {
final EntityTable<QuizData> table,
final RestService restService) {
action.getSingleSelection();
final QuizData selectedROWData = table.getSingleSelectedROWData();
@ -291,6 +301,18 @@ public class QuizLookupList implements TemplateComposer {
}
}
final Collection<Long> existingImports = restService.getBuilder(CheckExamImported.class)
.withURIVariable(API.PARAM_MODEL_ID, selectedROWData.id)
.call()
.getOrThrow()
.stream()
.map(key -> Long.valueOf(key.modelId))
.collect(Collectors.toList());
if (existingImports.contains(institutionId)) {
throw new PageMessageException(TEXT_KEY_EXISTING);
}
return action
.withEntityKey(action.getSingleSelection())
.withParentEntityKey(new EntityKey(selectedROWData.lmsSetupId, EntityType.LMS_SETUP))

View file

@ -232,7 +232,7 @@ public class UserAccountList implements TemplateComposer {
.newAction(ActionDefinition.USER_ACCOUNT_TOGGLE_ACTIVITY)
.withExec(this.pageService.activationToggleActionFunction(table, EMPTY_SELECTION_TEXT_KEY))
.withConfirm(this.pageService.confirmDeactivation(table))
.publishIf(() -> userGrant.m(), false);
.publishIf(() -> userGrant.im(), false);
}
private PageAction editAction(final PageAction pageAction) {

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2021 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.dao;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
public class DuplicateResourceException extends RuntimeException {
private static final long serialVersionUID = 2935680103812281185L;
/** The entity key of the resource that was requested */
public final EntityKey entityKey;
public DuplicateResourceException(final EntityType entityType, final String modelId) {
super("Resource " + entityType + " with ID: " + modelId + " already exists");
this.entityKey = new EntityKey(modelId, entityType);
}
public DuplicateResourceException(final EntityType entityType, final String modelId, final Throwable cause) {
super("Resource " + entityType + " with ID: " + modelId + " not found", cause);
this.entityKey = new EntityKey(modelId, entityType);
}
}

View file

@ -44,7 +44,11 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
* happened */
Result<Collection<Long>> allIdsOfInstitution(Long institutionId);
Result<Collection<Long>> allByQuizId(String quizId);
/** Get all institution ids for that a specified exam for given quiz id already exists
*
* @param quizId The quiz or external identifier of the exam (LMS)
* @return Result refer to a collection of primary keys of the institutions or to an error when happened */
Result<Collection<Long>> allInstitutionIdsByQuizId(String quizId);
/** Updates the exam status for specified exam
*

View file

@ -59,6 +59,7 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordDyn
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ExamRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DuplicateResourceException;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException;
@ -139,7 +140,7 @@ public class ExamDAOImpl implements ExamDAO {
}
@Override
public Result<Collection<Long>> allByQuizId(final String quizId) {
public Result<Collection<Long>> allInstitutionIdsByQuizId(final String quizId) {
return Result.tryCatch(() -> {
return this.examRecordMapper.selectByExample()
.where(
@ -151,7 +152,7 @@ public class ExamDAOImpl implements ExamDAO {
.build()
.execute()
.stream()
.map(rec -> rec.getId())
.map(rec -> rec.getInstitutionId())
.collect(Collectors.toList());
});
}
@ -331,23 +332,9 @@ 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 the same institution tries to import an exam that already exists
// open the existing. otherwise create new one if requested
// if the same institution tries to import an exam that already exists throw an error
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());
throw new DuplicateResourceException(EntityType.EXAM, exam.externalId);
}
}

View file

@ -228,10 +228,10 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
checkReadPrivilege(institutionId);
return this.examDAO.allByQuizId(modelId)
return this.examDAO.allInstitutionIdsByQuizId(modelId)
.map(ids -> ids
.stream()
.map(id -> new EntityKey(id, EntityType.EXAM))
.map(id -> new EntityKey(id, EntityType.INSTITUTION))
.collect(Collectors.toList()))
.getOrThrow();
}
@ -256,10 +256,6 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
.checkExamConsistency(modelId)
.getOrThrow();
if (includeRestriction) {
// TODO include seb restriction check and status
}
return result;
}

View file

@ -380,6 +380,7 @@ sebserver.quizdiscovery.action.import=Import as Exam
sebserver.quizdiscovery.quiz.import.out.dated=The Selected LMS exam 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 importing 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 anyway?
sebserver.quizdiscovery.quiz.import.existing=This course was already imported as an exam.<br/> You will find it in the Exam section.
sebserver.quizdiscovery.quiz.details.title=LMS Exam Details
sebserver.quizdiscovery.quiz.details.institution=Institution