/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.ims.qti21.ui.editor;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.DoubleAdder;
import org.olat.core.commons.fullWebApp.LayoutMain3ColsController;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.dropdown.Dropdown;
import org.olat.core.gui.components.link.Link;
import org.olat.core.gui.components.link.LinkFactory;
import org.olat.core.gui.components.panel.Panel;
import org.olat.core.gui.components.panel.SimpleStackedPanel;
import org.olat.core.gui.components.panel.StackedPanel;
import org.olat.core.gui.components.stack.TooledStackedPanel;
import org.olat.core.gui.components.stack.TooledStackedPanel.Align;
import org.olat.core.gui.components.tree.GenericTreeNode;
import org.olat.core.gui.components.tree.MenuTree;
import org.olat.core.gui.components.tree.TreeDropEvent;
import org.olat.core.gui.components.tree.TreeEvent;
import org.olat.core.gui.components.tree.TreeNode;
import org.olat.core.gui.components.velocity.VelocityContainer;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.VetoableCloseController;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.controller.MainLayoutBasicController;
import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
import org.olat.core.gui.control.generic.messages.MessageController;
import org.olat.core.gui.control.generic.messages.MessageUIFactory;
import org.olat.core.gui.control.generic.modal.DialogBoxController;
import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory;
import org.olat.core.gui.control.generic.wizard.Step;
import org.olat.core.gui.control.generic.wizard.StepRunnerCallback;
import org.olat.core.gui.control.generic.wizard.StepsMainRunController;
import org.olat.core.gui.control.generic.wizard.StepsRunContext;
import org.olat.core.gui.media.MediaResource;
import org.olat.core.helpers.Settings;
import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
import org.olat.core.util.Formatter;
import org.olat.core.util.Util;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.coordinate.LockResult;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.fileresource.FileResourceManager;
import org.olat.ims.qti21.AssessmentTestHelper;
import org.olat.ims.qti21.AssessmentTestHelper.AssessmentTestVisitor;
import org.olat.ims.qti21.QTI21Constants;
import org.olat.ims.qti21.QTI21LoggingAction;
import org.olat.ims.qti21.QTI21Service;
import org.olat.ims.qti21.manager.openxml.QTI21WordExport;
import org.olat.ims.qti21.model.IdentifierGenerator;
import org.olat.ims.qti21.model.QTI21QuestionType;
import org.olat.ims.qti21.model.xml.AssessmentItemBuilder;
import org.olat.ims.qti21.model.xml.AssessmentTestBuilder;
import org.olat.ims.qti21.model.xml.AssessmentTestFactory;
import org.olat.ims.qti21.model.xml.ManifestBuilder;
import org.olat.ims.qti21.model.xml.ManifestMetadataBuilder;
import org.olat.ims.qti21.model.xml.QtiNodesExtractor;
import org.olat.ims.qti21.model.xml.interactions.DrawingAssessmentItemBuilder;
import org.olat.ims.qti21.model.xml.interactions.EssayAssessmentItemBuilder;
import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder;
import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.EntryType;
import org.olat.ims.qti21.model.xml.interactions.HotspotAssessmentItemBuilder;
import org.olat.ims.qti21.model.xml.interactions.HottextAssessmentItemBuilder;
import org.olat.ims.qti21.model.xml.interactions.KPrimAssessmentItemBuilder;
import org.olat.ims.qti21.model.xml.interactions.MatchAssessmentItemBuilder;
import org.olat.ims.qti21.model.xml.interactions.MultipleChoiceAssessmentItemBuilder;
import org.olat.ims.qti21.model.xml.interactions.SingleChoiceAssessmentItemBuilder;
import org.olat.ims.qti21.model.xml.interactions.UploadAssessmentItemBuilder;
import org.olat.ims.qti21.pool.QTI21QPoolServiceProvider;
import org.olat.ims.qti21.questionimport.AssessmentItemAndMetadata;
import org.olat.ims.qti21.questionimport.AssessmentItemsPackage;
import org.olat.ims.qti21.questionimport.ImportOptions;
import org.olat.ims.qti21.questionimport.QImport_1_InputStep;
import org.olat.ims.qti21.ui.AssessmentTestDisplayController;
import org.olat.ims.qti21.ui.editor.events.AssessmentItemEvent;
import org.olat.ims.qti21.ui.editor.events.AssessmentSectionEvent;
import org.olat.ims.qti21.ui.editor.events.AssessmentTestEvent;
import org.olat.ims.qti21.ui.editor.events.AssessmentTestPartEvent;
import org.olat.imscp.xml.manifest.FileType;
import org.olat.imscp.xml.manifest.ResourceType;
import org.olat.modules.qpool.QuestionItemView;
import org.olat.modules.qpool.ui.SelectItemController;
import org.olat.modules.qpool.ui.events.QItemViewEvent;
import org.olat.repository.RepositoryEntry;
import org.olat.repository.ui.RepositoryEntryRuntimeController.ToolbarAware;
import org.olat.user.UserManager;
import org.olat.util.logging.activity.LoggingResourceable;
import org.springframework.beans.factory.annotation.Autowired;
import uk.ac.ed.ph.jqtiplus.node.QtiNode;
import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem;
import uk.ac.ed.ph.jqtiplus.node.test.AbstractPart;
import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef;
import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection;
import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest;
import uk.ac.ed.ph.jqtiplus.node.test.SectionPart;
import uk.ac.ed.ph.jqtiplus.node.test.TestPart;
import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem;
import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentTest;
import uk.ac.ed.ph.jqtiplus.resolution.RootNodeLookup;
import uk.ac.ed.ph.jqtiplus.types.Identifier;
import uk.ac.ed.ph.jqtiplus.utils.QueryUtils;
import uk.ac.ed.ph.jqtiplus.utils.TreeWalkNodeHandler;
/**
* Assessment test editor and composer.
*
*
* Initial date: 22.05.2015<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class AssessmentTestComposerController extends MainLayoutBasicController implements VetoableCloseController, ToolbarAware {
private MenuTree menuTree;
private Dropdown exportItemTools, addItemTools, changeItemTools;
private Link newTestPartLink, newSectionLink, newSingleChoiceLink, newMultipleChoiceLink, newKPrimLink, newMatchLink,
newFIBLink, newNumericalLink, newHotspotLink, newHottextLink, newEssayLink, newUploadLink, newDrawingLink;
private Link importFromPoolLink, importFromTableLink, exportToPoolLink, exportToDocxLink;
private Link reloadInCacheLink, deleteLink, copyLink;
private final TooledStackedPanel toolbar;
private VelocityContainer mainVC;
private Controller currentEditorCtrl;
private CloseableModalController cmc;
private SelectItemController selectQItemCtrl;
private DialogBoxController confirmDeleteCtrl;
private StepsMainRunController importTableWizard;
private final LayoutMain3ColsController columnLayoutCtr;
private File unzippedDirRoot;
private VFSContainer unzippedContRoot;
private final RepositoryEntry testEntry;
private ManifestBuilder manifestBuilder;
private ResolvedAssessmentTest resolvedAssessmentTest;
private AssessmentTestBuilder assessmentTestBuilder;
private final boolean survey = false;
private final boolean restrictedEdit;
private boolean assessmentChanged = false;
private LockResult lockEntry;
private LockResult activeSessionLock;
private CountDownLatch exportLatch;
@Autowired
private QTI21Service qtiService;
@Autowired
private UserManager userManager;
@Autowired
private QTI21QPoolServiceProvider qti21QPoolServiceProvider;
public AssessmentTestComposerController(UserRequest ureq, WindowControl wControl, TooledStackedPanel toolbar,
RepositoryEntry testEntry) {
super(ureq, wControl, Util.createPackageTranslator(AssessmentTestDisplayController.class, ureq.getLocale()));
this.toolbar = toolbar;
this.testEntry = testEntry;
restrictedEdit = qtiService.isAssessmentTestActivelyUsed(testEntry);
lockEntry = CoordinatorManager.getInstance().getCoordinator().getLocker().aquirePersistentLock(testEntry.getOlatResource(), getIdentity(), null);
if (lockEntry.isSuccess()) {
// acquired a lock for the duration of the session only
//fileResource has the RepositoryEntre.getOlatResource within, which is used in qtiPackage
activeSessionLock = CoordinatorManager.getInstance().getCoordinator().getLocker().acquireLock(testEntry.getOlatResource(), getIdentity(), null);
} else {
String fullName = userManager.getUserDisplayName(lockEntry.getOwner());
String msg = translate("error.lock", new String[] { fullName, Formatter.formatDatetime(new Date(lockEntry.getLockAquiredTime())) });
wControl.setWarning(msg);
MessageController contentCtr = MessageUIFactory.createInfoMessage(ureq, getWindowControl(), translate("error.lock.title"), msg);
listenTo(contentCtr);
columnLayoutCtr = new LayoutMain3ColsController(ureq, getWindowControl(), contentCtr);
listenTo(columnLayoutCtr); // auto dispose later
putInitialPanel(columnLayoutCtr.getInitialComponent());
return;
}
addLoggingResourceable(LoggingResourceable.wrapTest(testEntry));
// test structure
menuTree = new MenuTree("atTree");
menuTree.setExpandSelectedNode(false);
menuTree.setDropSiblingEnabled(!restrictedEdit);
menuTree.setDndAcceptJSMethod("treeAcceptDrop_notWithChildren");
menuTree.setElementCssClass("o_assessment_test_editor_menu");
menuTree.addListener(this);
FileResourceManager frm = FileResourceManager.getInstance();
unzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource());
unzippedContRoot = frm.unzipContainerResource(testEntry.getOlatResource());
updateTreeModel(false);
manifestBuilder = ManifestBuilder.read(new File(unzippedDirRoot, "imsmanifest.xml"));
//is the test editable ?
menuTree.setDragEnabled(!restrictedEdit && assessmentTestBuilder.isEditable());
menuTree.setDropEnabled(!restrictedEdit && assessmentTestBuilder.isEditable());
//add elements
addItemTools = new Dropdown("editTools", "new.elements", false, getTranslator());
addItemTools.setIconCSS("o_icon o_icon-fw o_icon_add");
addItemTools.setElementCssClass("o_sel_qti_elements");
addItemTools.setVisible(!restrictedEdit);
newSectionLink = LinkFactory.createToolLink("new.section", translate("new.section"), this, "o_mi_qtisection");
newSectionLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(newSectionLink);
newTestPartLink = LinkFactory.createToolLink("new.testpart", translate("new.testpart"), this, "o_qtiassessment_icon");
newTestPartLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(newTestPartLink);
addItemTools.addComponent(new Dropdown.Spacer("sep-struct"));
//items
newSingleChoiceLink = LinkFactory.createToolLink("new.sc", translate("new.sc"), this, "o_mi_qtisc");
newSingleChoiceLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(newSingleChoiceLink);
newMultipleChoiceLink = LinkFactory.createToolLink("new.mc", translate("new.mc"), this, "o_mi_qtimc");
newMultipleChoiceLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(newMultipleChoiceLink);
newKPrimLink = LinkFactory.createToolLink("new.kprim", translate("new.kprim"), this, "o_mi_qtikprim");
newKPrimLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(newKPrimLink);
newMatchLink = LinkFactory.createToolLink("new.match", translate("new.match"), this, "o_mi_qtimatch");
newMatchLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(newMatchLink);
newFIBLink = LinkFactory.createToolLink("new.fib", translate("new.fib"), this, "o_mi_qtifib");
newFIBLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(newFIBLink);
newNumericalLink = LinkFactory.createToolLink("new.fib.numerical", translate("new.fib.numerical"), this, "o_mi_qtinumerical");
newNumericalLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(newNumericalLink);
newHottextLink = LinkFactory.createToolLink("new.hottext", translate("new.hottext"), this, "o_mi_qtihottext");
newHottextLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(newHottextLink);
newHotspotLink = LinkFactory.createToolLink("new.hotspot", translate("new.hotspot"), this, "o_mi_qtihotspot");
newHotspotLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(newHotspotLink);
newEssayLink = LinkFactory.createToolLink("new.essay", translate("new.essay"), this, "o_mi_qtiessay");
newEssayLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(newEssayLink);
newUploadLink = LinkFactory.createToolLink("new.upload", translate("new.upload"), this, "o_mi_qtiupload");
newUploadLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(newUploadLink);
newDrawingLink = LinkFactory.createToolLink("new.drawing", translate("new.drawing"), this, "o_mi_qtidrawing");
newDrawingLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(newDrawingLink);
addItemTools.addComponent(new Dropdown.Spacer("sep-import"));
//import
importFromPoolLink = LinkFactory.createToolLink("import.pool", translate("tools.import.qpool"), this, "o_mi_qpool_import");
importFromPoolLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(importFromPoolLink);
importFromTableLink = LinkFactory.createToolLink("import.table", translate("tools.import.table"), this, "o_mi_table_import");
importFromTableLink.setIconLeftCSS("o_icon o_icon_table o_icon-fw");
importFromTableLink.setDomReplacementWrapperRequired(false);
addItemTools.addComponent(importFromTableLink);
exportItemTools = new Dropdown("exportTools", "tools.export.header", false, getTranslator());
exportItemTools.setIconCSS("o_icon o_icon_export");
//export
exportToPoolLink = LinkFactory.createToolLink("export.pool", translate("tools.export.qpool"), this, "o_mi_qpool_export");
exportToPoolLink.setIconLeftCSS("o_icon o_icon_table o_icon-fw");
exportToPoolLink.setDomReplacementWrapperRequired(false);
exportItemTools.addComponent(exportToPoolLink);
exportToDocxLink = LinkFactory.createToolLink("export.pool", translate("tools.export.docx"), this, "o_mi_docx_export");
exportToDocxLink.setIconLeftCSS("o_icon o_icon_download o_icon-fw");
exportToDocxLink.setDomReplacementWrapperRequired(false);
exportItemTools.addComponent(exportToDocxLink);
//changes
changeItemTools = new Dropdown("changeTools", "change.elements", false, getTranslator());
changeItemTools.setIconCSS("o_icon o_icon-fw o_icon_customize");
changeItemTools.setVisible(!restrictedEdit);
changeItemTools.setElementCssClass("o_sel_qti_change_node");
if(ureq.getUserSession().getRoles().isOLATAdmin()) {
reloadInCacheLink = LinkFactory.createToolLink("replace.in.cache.pool", translate("tools.reload.from.files"), this, "o_icon_refresh");
reloadInCacheLink.setTooltip(translate("tools.reload.from.files.tooltip"));
reloadInCacheLink.setDomReplacementWrapperRequired(false);
changeItemTools.addComponent(reloadInCacheLink);
}
deleteLink = LinkFactory.createToolLink("tools.delete", translate("tools.change.delete"), this, "o_icon_delete_item");
deleteLink.setDomReplacementWrapperRequired(false);
changeItemTools.addComponent(deleteLink);
copyLink = LinkFactory.createToolLink("import.table", translate("tools.change.copy"), this, "o_icon_copy");
copyLink.setDomReplacementWrapperRequired(false);
changeItemTools.addComponent(copyLink);
// main layout
mainVC = createVelocityContainer("assessment_test_composer");
columnLayoutCtr = new LayoutMain3ColsController(ureq, getWindowControl(), menuTree, mainVC, "at" + testEntry.getKey());
columnLayoutCtr.addCssClassToMain("o_editor");
listenTo(columnLayoutCtr);
StackedPanel initPanel = new SimpleStackedPanel("qti21editpanel", "o_edit_mode");
initPanel.setContent(columnLayoutCtr.getInitialComponent());
putInitialPanel(initPanel);
// init
TreeNode selectedNode = doOpenFirstItem();
if(selectedNode == null) {
selectedNode = menuTree.getTreeModel().getRootNode();
}
partEditorFactory(ureq, selectedNode);
}
private void updateTreeModel(boolean forceReload) {
resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(unzippedDirRoot, forceReload, true);
menuTree.setTreeModel(new AssessmentTestEditorAndComposerTreeModel(resolvedAssessmentTest));
assessmentTestBuilder = new AssessmentTestBuilder(resolvedAssessmentTest.getRootNodeLookup().extractIfSuccessful());
}
public boolean hasChanges() {
return assessmentChanged;
}
@Override
protected void doDispose() {
if (lockEntry != null && lockEntry.isSuccess()) {
CoordinatorManager.getInstance().getCoordinator().getLocker().releasePersistentLock(lockEntry);
}
if (activeSessionLock != null && activeSessionLock.isSuccess()) {
CoordinatorManager.getInstance().getCoordinator().getLocker().releaseLock(activeSessionLock);
}
}
@Override
public void initToolbar() {
toolbar.addTool(exportItemTools, Align.left);
toolbar.addTool(addItemTools, Align.left);
toolbar.addTool(changeItemTools, Align.left);
}
@Override
public boolean requestForClose(UserRequest ureq) {
return true;
}
@Override
protected void event(UserRequest ureq, Controller source, Event event) {
if(event instanceof AssessmentTestEvent) {
AssessmentTestEvent ate = (AssessmentTestEvent)event;
if(ate == AssessmentTestEvent.ASSESSMENT_TEST_CHANGED_EVENT) {
doSaveAssessmentTest(null);
}
} else if(event instanceof AssessmentTestPartEvent) {
AssessmentTestPartEvent atpe = (AssessmentTestPartEvent)event;
if(atpe == AssessmentTestPartEvent.ASSESSMENT_TEST_PART_CHANGED_EVENT) {
doSaveAssessmentTest(null);
}
} else if(event instanceof AssessmentSectionEvent) {
AssessmentSectionEvent ase = (AssessmentSectionEvent)event;
if(AssessmentSectionEvent.ASSESSMENT_SECTION_CHANGED.equals(ase.getCommand())) {
doSaveAssessmentTest(null);
doUpdate(ase.getSection().getIdentifier(), ase.getSection().getTitle());
doSaveManifest();
}
} else if(event instanceof AssessmentItemEvent) {
AssessmentItemEvent aie = (AssessmentItemEvent)event;
if(AssessmentItemEvent.ASSESSMENT_ITEM_CHANGED.equals(aie.getCommand())) {
assessmentChanged = true;
doSaveAssessmentTest(null);
doUpdate(aie.getAssessmentItemRef().getIdentifier(), aie.getAssessmentItem().getTitle());
doSaveManifest();
} else if(AssessmentItemEvent.ASSESSMENT_ITEM_METADATA_CHANGED.equals(aie.getCommand())) {
doSaveManifest();
}
} else if(selectQItemCtrl == source) {
cmc.deactivate();
cleanUp();
if(event instanceof QItemViewEvent) {
QItemViewEvent e = (QItemViewEvent)event;
List<QuestionItemView> items = e.getItemList();
doInsert(ureq, items);
}
} else if(importTableWizard == source) {
AssessmentItemsPackage importPackage = (AssessmentItemsPackage)importTableWizard.getRunContext().get("importPackage");
getWindowControl().pop();
cleanUp();
if(event == Event.DONE_EVENT || event == Event.CHANGED_EVENT) {
doInsert(ureq, importPackage);
}
} else if(confirmDeleteCtrl == source) {
if (DialogBoxUIFactory.isYesEvent(event)) { // yes, delete
doDelete(ureq, (TreeNode)confirmDeleteCtrl.getUserObject());
}
cleanUp();
} else if(cmc == source) {
cleanUp();
}
super.event(ureq, source, event);
}
private void cleanUp() {
removeAsListenerAndDispose(confirmDeleteCtrl);
removeAsListenerAndDispose(selectQItemCtrl);
removeAsListenerAndDispose(cmc);
confirmDeleteCtrl = null;
selectQItemCtrl = null;
cmc = null;
}
@Override
protected void event(UserRequest ureq, Component source, Event event) {
if(menuTree == source) {
if (event instanceof TreeEvent) {
TreeEvent te = (TreeEvent)event;
String cmd = te.getCommand();
if (MenuTree.COMMAND_TREENODE_CLICKED.equals(cmd)) {
TreeNode selectedNode = menuTree.getTreeModel()
.getNodeById(te.getNodeId());
partEditorFactory(ureq, selectedNode);
}
} else if(event.getCommand().equals(MenuTree.COMMAND_TREENODE_DROP)) {
TreeDropEvent tde = (TreeDropEvent) event;
doDrop(ureq, tde.getDroppedNodeId(), tde.getTargetNodeId(), tde.isAsChild());
}
} else if(newSectionLink == source) {
doNewSection(ureq, menuTree.getSelectedNode());
} else if(newTestPartLink == source) {
doNewTestPart(ureq);
} else if(newSingleChoiceLink == source) {
doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new SingleChoiceAssessmentItemBuilder(translate("new.sc"), translate("new.answer"), qtiService.qtiSerializer()));
} else if(newMultipleChoiceLink == source) {
doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new MultipleChoiceAssessmentItemBuilder(translate("new.mc"), translate("new.answer"), qtiService.qtiSerializer()));
} else if(newKPrimLink == source) {
doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new KPrimAssessmentItemBuilder(translate("new.kprim"), translate("new.answer"), qtiService.qtiSerializer()));
} else if(newMatchLink == source) {
doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new MatchAssessmentItemBuilder(translate("new.match"), qtiService.qtiSerializer()));
} else if(newFIBLink == source) {
doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new FIBAssessmentItemBuilder(translate("new.fib"), EntryType.text, qtiService.qtiSerializer()));
} else if(newNumericalLink == source) {
doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new FIBAssessmentItemBuilder(translate("new.fib.numerical"), EntryType.numerical, qtiService.qtiSerializer()));
} else if(newHotspotLink == source) {
doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new HotspotAssessmentItemBuilder(translate("new.hotspot"), qtiService.qtiSerializer()));
} else if(newHottextLink == source) {
doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new HottextAssessmentItemBuilder(translate("new.hottext"), translate("new.hottext.start"), translate("new.hottext.text"), qtiService.qtiSerializer()));
} else if(newEssayLink == source) {
doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new EssayAssessmentItemBuilder(translate("new.essay"), qtiService.qtiSerializer()));
} else if(newUploadLink == source) {
doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new UploadAssessmentItemBuilder(translate("new.upload"), qtiService.qtiSerializer()));
} else if(newDrawingLink == source) {
doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new DrawingAssessmentItemBuilder(translate("new.drawing"), qtiService.qtiSerializer()));
} else if(importFromPoolLink == source) {
doSelectQItem(ureq);
} else if(importFromTableLink == source) {
doImportTable(ureq);
} else if(exportToPoolLink == source) {
doExportPool();
} else if(exportToDocxLink == source) {
doExportDocx(ureq);
} else if(deleteLink == source) {
doConfirmDelete(ureq);
} else if(copyLink == source) {
doCopy(ureq);
} else if(reloadInCacheLink == source) {
doForceReloadFiles();
}
}
private void doSelectQItem(UserRequest ureq) {
removeAsListenerAndDispose(cmc);
removeAsListenerAndDispose(selectQItemCtrl);
selectQItemCtrl = new SelectItemController(ureq, getWindowControl(), QTI21Constants.QTI_21_FORMAT);
listenTo(selectQItemCtrl);
cmc = new CloseableModalController(getWindowControl(), translate("close"), selectQItemCtrl.getInitialComponent(), true, translate("title.add") );
cmc.activate();
listenTo(cmc);
}
private void doDrop(UserRequest ureq, String droppedNodeId, String targetnodeId, boolean asChild) {
TreeNode droppedNode = menuTree.getTreeModel().getNodeById(droppedNodeId);
TreeNode targetNode = menuTree.getTreeModel().getNodeById(targetnodeId);
if(droppedNode == null || targetNode == null) return;
Object droppedObject = droppedNode.getUserObject();
Object targetObject = targetNode.getUserObject();
if(droppedObject == null || targetObject == null || droppedObject == targetObject) return;
if(asChild) {
if(droppedObject instanceof AssessmentItemRef
&& (targetObject instanceof AssessmentSection || targetObject instanceof AssessmentItemRef)) {
AssessmentItemRef droppedItemRef = (AssessmentItemRef)droppedObject;
droppedItemRef.getParentSection().getSectionParts().remove(droppedItemRef);
if(targetObject instanceof AssessmentSection) {
AssessmentSection targetSection = (AssessmentSection)targetObject;
targetSection.getSectionParts().add(droppedItemRef);
} else if(targetObject instanceof AssessmentItemRef) {
AssessmentItemRef targetItemRef = (AssessmentItemRef)targetObject;
AssessmentSection targetSection = targetItemRef.getParentSection();
int pos = targetSection.getChildAbstractParts().indexOf(targetItemRef);
targetSection.getChildAbstractParts().add(pos, droppedItemRef);
}
} else if(droppedObject instanceof AssessmentSection
&& (targetObject instanceof AssessmentSection || targetObject instanceof TestPart
|| (targetObject instanceof AssessmentTest && ((AssessmentTest)targetObject).getTestParts().size() == 1))) {
AssessmentSection droppedSection = (AssessmentSection)droppedObject;
if(droppedSection.getParentSection() != null) {
droppedSection.getParentSection().getSectionParts().remove(droppedSection);
} else {
droppedSection.getParent().getChildAbstractParts().remove(droppedSection);
}
if(targetObject instanceof AssessmentSection) {
AssessmentSection targetSection = (AssessmentSection)targetObject;
targetSection.getChildAbstractParts().add(droppedSection);
} else if(targetObject instanceof TestPart) {
TestPart targetTestPart = (TestPart)targetObject;
targetTestPart.getAssessmentSections().add(droppedSection);
} else if(targetObject instanceof AssessmentTest) {
TestPart targetTestPart = ((AssessmentTest)targetObject).getTestParts().get(0);
targetTestPart.getAssessmentSections().add(droppedSection);
}
}
} else {
if(droppedObject instanceof AssessmentItemRef && targetObject instanceof AssessmentItemRef) {
AssessmentItemRef droppedItemRef = (AssessmentItemRef)droppedObject;
droppedItemRef.getParentSection().getSectionParts().remove(droppedItemRef);
AssessmentItemRef targetItemRef = (AssessmentItemRef)targetObject;
AssessmentSection targetSection = targetItemRef.getParentSection();
int pos = targetSection.getChildAbstractParts().indexOf(targetItemRef) + 1;
if(pos < 0) {
targetSection.getChildAbstractParts().add(droppedItemRef);
} else if(pos >= targetSection.getChildAbstractParts().size()) {
targetSection.getChildAbstractParts().add(droppedItemRef);
} else {
targetSection.getChildAbstractParts().add(pos, droppedItemRef);
}
} else if(droppedObject instanceof AssessmentSection
&& targetObject instanceof AssessmentSection) {
AssessmentSection droppedSection = (AssessmentSection)droppedObject;
if(droppedSection.getParentSection() != null) {
droppedSection.getParentSection().getSectionParts().remove(droppedSection);
} else {
droppedSection.getParent().getChildAbstractParts().remove(droppedSection);
}
AssessmentSection targetSection = (AssessmentSection)targetObject;
if(targetSection.getParentSection() != null) {
AssessmentSection targetParentSection = targetSection.getParentSection();
int pos = targetParentSection.getChildAbstractParts().indexOf(targetSection) + 1;
if(pos >= targetParentSection.getChildAbstractParts().size()) {
targetParentSection.getChildAbstractParts().add(droppedSection);
} else {
targetParentSection.getChildAbstractParts().add(pos, droppedSection);
}
} else if(targetSection.getParent() instanceof TestPart) {
TestPart targetTestPart = (TestPart)targetSection.getParent();
int pos = targetTestPart.getChildAbstractParts().indexOf(targetSection) + 1;
if(pos >= targetTestPart.getChildAbstractParts().size()) {
targetTestPart.getChildAbstractParts().add(droppedSection);
} else {
targetTestPart.getChildAbstractParts().add(pos, droppedSection);
}
}
}
}
//quickly saved the assessment test with wrong parent
doSaveAssessmentTest(null);
//reload a clean instance
updateTreeModel(false);
TreeNode droppedItemNode = menuTree.getTreeModel().getNodeById(droppedNode.getIdent());
if(droppedItemNode != null) {
menuTree.setSelectedNode(droppedItemNode);
menuTree.open(droppedItemNode);
partEditorFactory(ureq, droppedItemNode);
}
}
private void doExportPool() {
TreeNode selectedNode = menuTree.getSelectedNode();
if(selectedNode == null) return;
AtomicInteger counter = new AtomicInteger();
Object uobject = selectedNode.getUserObject();
if(uobject instanceof AssessmentItemRef) {
doExportPool((AssessmentItemRef)uobject);
counter.incrementAndGet();
} else if(uobject instanceof QtiNode) {
QtiNode qtiNode = (QtiNode)uobject;
QueryUtils.walkTree(new TreeWalkNodeHandler() {
@Override
public boolean handleNode(QtiNode node) {
if(node instanceof AssessmentItemRef) {
doExportPool((AssessmentItemRef)node);
counter.incrementAndGet();
}
return true;
}
}, qtiNode);
}
if(counter.get() > 0) {
showInfo("export.qpool.successful", counter.toString());
}
}
private void doExportPool(AssessmentItemRef itemRef) {
ResolvedAssessmentItem resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef);
RootNodeLookup<AssessmentItem> rootNode = resolvedAssessmentItem.getItemLookup();
AssessmentItem assessmentItem = rootNode.extractIfSuccessful();
ManifestBuilder clonedManifestBuilder = ManifestBuilder.read(new File(unzippedDirRoot, "imsmanifest.xml"));
ResourceType resource = getResourceType(clonedManifestBuilder, itemRef);
ManifestMetadataBuilder metadata = clonedManifestBuilder.getMetadataBuilder(resource, true);
if(metadata == null) {
metadata = new ManifestMetadataBuilder();// not in imsmanifest.xml?
}
File itemFile = new File(rootNode.getSystemId());
qti21QPoolServiceProvider
.importAssessmentItemRef(getIdentity(), assessmentItem, itemFile, metadata, getLocale());
}
private void doExportDocx(UserRequest ureq) {
exportLatch = new CountDownLatch(1);
MediaResource mr = new QTI21WordExport(resolvedAssessmentTest, unzippedContRoot, getLocale(), "UTF-8", exportLatch);
ureq.getDispatchResult().setResultingMediaResource(mr);
}
private void doImportTable(UserRequest ureq) {
removeAsListenerAndDispose(importTableWizard);
final AssessmentItemsPackage importPackage = new AssessmentItemsPackage();
final ImportOptions options = new ImportOptions();
options.setShuffle(!survey);
Step start = new QImport_1_InputStep(ureq, importPackage, options, null);
StepRunnerCallback finish = new StepRunnerCallback() {
@Override
public Step execute(UserRequest uureq, WindowControl wControl, StepsRunContext runContext) {
runContext.put("importPackage", importPackage);
return StepsMainRunController.DONE_MODIFIED;
}
};
importTableWizard = new StepsMainRunController(ureq, getWindowControl(), start, finish, null,
translate("tools.import.table"), "o_mi_table_import_wizard");
listenTo(importTableWizard);
getWindowControl().pushAsModalDialog(importTableWizard.getInitialComponent());
}
private void doInsert(UserRequest ureq, List<QuestionItemView> items) {
TreeNode selectedNode = menuTree.getSelectedNode();
TreeNode sectionNode = getNearestSection(selectedNode);
String firstItemId = null;
Map<AssessmentItemRef,AssessmentItem> flyingObjects = new HashMap<>();
try {
AssessmentSection section = (AssessmentSection)sectionNode.getUserObject();
for(QuestionItemView item:items) {
AssessmentItem assessmentItem = qti21QPoolServiceProvider.exportToQTIEditor(item, getLocale(), unzippedDirRoot);
AssessmentItemRef itemRef = doInsert(section, assessmentItem);
if(firstItemId == null) {
firstItemId = itemRef.getIdentifier().toString();
}
flyingObjects.put(itemRef, assessmentItem);
}
} catch (IOException | URISyntaxException e) {
showError("error.import.question");
logError("", e);
}
if(firstItemId != null) {
//persist metadata
doSaveAssessmentTest(flyingObjects);
doSaveManifest();
updateTreeModel(false);
TreeNode newItemNode = menuTree.getTreeModel().getNodeById(firstItemId);
menuTree.setSelectedNode(newItemNode);
menuTree.open(newItemNode);
partEditorFactory(ureq, newItemNode);
}
}
private void doInsert(UserRequest ureq, AssessmentItemsPackage importPackage) {
TreeNode selectedNode = menuTree.getSelectedNode();
TreeNode sectionNode = getNearestSection(selectedNode);
String firstItemId = null;
boolean errorOnImport = false;
Map<AssessmentItemRef,AssessmentItem> flyingObjects = new HashMap<>();
try {
AssessmentSection section = (AssessmentSection)sectionNode.getUserObject();
List<AssessmentItemAndMetadata> itemsAndMetadata = importPackage.getItems();
for(AssessmentItemAndMetadata itemAndMetadata:itemsAndMetadata) {
if(itemAndMetadata.isHasError()) {
errorOnImport = true;
continue;
}
AssessmentItemBuilder itemBuilder = itemAndMetadata.getItemBuilder();
AssessmentItem assessmentItem = itemBuilder.getAssessmentItem();
AssessmentItemRef itemRef = doInsert(section, assessmentItem);
ManifestMetadataBuilder metadata = manifestBuilder.getResourceBuilderByHref(itemRef.getHref().toString());
metadata.setQtiMetadata(itemBuilder.getInteractionNames());
itemAndMetadata.toBuilder(metadata, getLocale());
if(firstItemId == null) {
firstItemId = itemRef.getIdentifier().toString();
}
flyingObjects.put(itemRef, assessmentItem);
}
} catch (URISyntaxException e) {
errorOnImport = true;
logError("", e);
}
if(errorOnImport) {
showError("error.import.question");
}
if(firstItemId != null) {
//persist metadata
doSaveAssessmentTest(flyingObjects);
doSaveManifest();
updateTreeModel(false);
TreeNode newItemNode = menuTree.getTreeModel().getNodeById(firstItemId);
menuTree.setSelectedNode(newItemNode);
menuTree.open(newItemNode);
partEditorFactory(ureq, newItemNode);
}
}
private AssessmentItemRef doInsert(AssessmentSection section, AssessmentItem assessmentItem)
throws URISyntaxException {
AssessmentItemRef itemRef = new AssessmentItemRef(section);
String itemId = assessmentItem.getIdentifier();
itemRef.setIdentifier(Identifier.parseString(itemId));
File itemFile = new File(unzippedDirRoot, itemId + ".xml");
itemRef.setHref(new URI(itemFile.getName()));
section.getSectionParts().add(itemRef);
qtiService.persistAssessmentObject(itemFile, assessmentItem);
URI testUri = resolvedAssessmentTest.getTestLookup().getSystemId();
File testFile = new File(testUri);
qtiService.updateAssesmentObject(testFile, resolvedAssessmentTest);
manifestBuilder.appendAssessmentItem(itemFile.getName());
doSaveManifest();
return itemRef;
}
private TreeNode doOpenFirstItem() {
TreeNode node = menuTree.getTreeModel().getRootNode();
if(node.getChildCount() > 0) {
return doOpenFirstItem((TreeNode)node.getChildAt(0));
}
return node;
}
private TreeNode doOpenFirstItem(TreeNode node) {
if(node.getUserObject() instanceof AssessmentItemRef) {
menuTree.setSelectedNode(node);
menuTree.open(node);
return node;
}
if(node.getChildCount() > 0) {
return doOpenFirstItem((TreeNode)node.getChildAt(0));
}
return null;
}
/**
* Create a new test part and a section. Test part need a section,
* section ref as children, it's mandatory.
*
* @param ureq
*/
private void doNewTestPart(UserRequest ureq) {
TestPart testPart = AssessmentTestFactory.createTestPart(assessmentTestBuilder.getAssessmentTest());
AssessmentTestFactory.appendAssessmentSection(translate("new.section"), testPart);
//save the test
doSaveAssessmentTest(null);
//reload the test
updateTreeModel(false);
TreeNode newTestPartNode = menuTree.getTreeModel().getNodeById(testPart.getIdentifier().toString());
menuTree.setSelectedNode(newTestPartNode);
menuTree.open(newTestPartNode);
partEditorFactory(ureq, newTestPartNode);
}
private void doNewSection(UserRequest ureq, TreeNode selectedNode) {
AbstractPart parentPart;
TreeNode sectionNode = getNearestSection(selectedNode);
if(sectionNode != null) {
AssessmentSection section = (AssessmentSection)sectionNode.getUserObject();
parentPart = section.getParent();
} else if(selectedNode.getUserObject() instanceof TestPart) {
parentPart = (TestPart)selectedNode.getUserObject();
} else {
TreeNode rootNode = menuTree.getTreeModel().getRootNode();
AssessmentTest assessmentTest = (AssessmentTest)rootNode.getUserObject();
List<TestPart> parts = assessmentTest.getTestParts();
if(parts != null && parts.size() > 0) {
parentPart = parts.get(0);
} else {
showWarning("error.cannot.create.section");
return;
}
}
AssessmentSection newSection;
if(parentPart instanceof TestPart) {
newSection = AssessmentTestFactory.appendAssessmentSection(translate("new.section"), (TestPart)parentPart);
} else if(parentPart instanceof AssessmentSection) {
newSection = AssessmentTestFactory.appendAssessmentSection(translate("new.section"), (AssessmentSection)parentPart);
} else {
showWarning("error.cannot.create.section");
return;
}
//save the test
URI testUri = resolvedAssessmentTest.getTestLookup().getSystemId();
File testFile = new File(testUri);
qtiService.updateAssesmentObject(testFile, resolvedAssessmentTest);
assessmentChanged = true;
//reload the test
updateTreeModel(false);
TreeNode newSectionNode = menuTree.getTreeModel().getNodeById(newSection.getIdentifier().toString());
menuTree.setSelectedNode(newSectionNode);
menuTree.open(newSectionNode);
partEditorFactory(ureq, newSectionNode);
}
/**
* The method create a simple single choice, save the assessment item
* and append it to the test, update the manifest file.
*
* @param ureq
* @param selectedNode
*/
private void doNewAssessmentItem(UserRequest ureq, TreeNode selectedNode, AssessmentItemBuilder itemBuilder) {
try {
TreeNode sectionNode = getNearestSection(selectedNode);
AssessmentSection section = (AssessmentSection)sectionNode.getUserObject();
AssessmentItemRef itemRef = new AssessmentItemRef(section);
String itemId = IdentifierGenerator.newAsString(itemBuilder.getQuestionType().getPrefix());
itemRef.setIdentifier(Identifier.parseString(itemId));
File itemFile = new File(unzippedDirRoot, itemId + ".xml");
itemRef.setHref(new URI(itemFile.getName()));
section.getSectionParts().add(itemRef);
AssessmentItem assessmentItem = itemBuilder.getAssessmentItem();
qtiService.persistAssessmentObject(itemFile, assessmentItem);
Map<AssessmentItemRef,AssessmentItem> flyingObjects = Collections.singletonMap(itemRef, assessmentItem);
doSaveAssessmentTest(flyingObjects);
manifestBuilder.appendAssessmentItem(itemFile.getName());
doSaveManifest();
updateTreeModel(false);
TreeNode newItemNode = menuTree.getTreeModel().getNodeById(itemId);
menuTree.setSelectedNode(newItemNode);
menuTree.open(newItemNode);
partEditorFactory(ureq, newItemNode);
} catch (URISyntaxException e) {
logError("", e);
}
}
private TreeNode getNearestSection(TreeNode node) {
if(node == null) {
node = menuTree.getTreeModel().getRootNode();
}
if(node.getUserObject() instanceof AssessmentTest) {
//choose the first test part or section
if(node.getChildCount() > 0) {
node = (TreeNode)node.getChildAt(0);
}
}
if(node.getUserObject() instanceof AssessmentSection) {
return node;
}
if(node.getUserObject() instanceof AssessmentItemRef) {
return (TreeNode)node.getParent();
}
if(node.getUserObject() instanceof TestPart) {
if(node.getChildCount() > 0) {
return (TreeNode)node.getChildAt(0);
}
}
return null;
}
/**
*
* @param flyingObjects A list of assessmentItems which are not part of the test but will be.
*/
private void doSaveAssessmentTest(Map<AssessmentItemRef,AssessmentItem> flyingObjects) {
assessmentChanged = true;
recalculateMaxScoreAssessmentTest(flyingObjects);
assessmentTestBuilder.build();
URI testURI = resolvedAssessmentTest.getTestLookup().getSystemId();
File testFile = new File(testURI);
qtiService.updateAssesmentObject(testFile, resolvedAssessmentTest);
ThreadLocalUserActivityLogger.log(QTI21LoggingAction.QTI_EDIT_RESOURCE, getClass());
}
private void recalculateMaxScoreAssessmentTest(Map<AssessmentItemRef,AssessmentItem> flyingObjects) {
DoubleAdder atomicMaxScore = new DoubleAdder();
AssessmentTest assessmentTest = (AssessmentTest)menuTree.getTreeModel().getRootNode().getUserObject();
AssessmentTestHelper.visit(assessmentTest, new AssessmentTestVisitor() {
@Override
public void visit(TestPart testPart) { /* */ }
@Override
public void visit(SectionPart sectionPart) {
if(sectionPart instanceof AssessmentItemRef) {
AssessmentItemRef itemRef = (AssessmentItemRef)sectionPart;
ResolvedAssessmentItem resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef);
checkAndFixAbsolutePath(itemRef);
AssessmentItem assessmentItem = null;
if(resolvedAssessmentItem != null) {
assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful();
}
if(assessmentItem == null && flyingObjects != null && flyingObjects.containsKey(itemRef)) {
assessmentItem = flyingObjects.get(itemRef);
}
if(assessmentItem != null) {
Double maxScore = QtiNodesExtractor.extractMaxScore(assessmentItem);
if(maxScore != null) {
atomicMaxScore.add(maxScore.doubleValue());
}
}
}
}
});
double sumMaxScore = atomicMaxScore.sum();
if(sumMaxScore > 0.0d) {
assessmentTestBuilder.setMaxScore(sumMaxScore);
} else {
assessmentTestBuilder.setMaxScore(null);
}
}
private void checkAndFixAbsolutePath(AssessmentItemRef itemRef) {
if(itemRef == null || itemRef.getHref() == null) return;
String href = itemRef.getHref().toString();
if(isAbsolutePath(href)) {
try {
String relativeHref = fixAbsolutePath(href);
itemRef.setHref(new URI(relativeHref));
} catch (URISyntaxException e) {
logError("", e);
}
}
}
private void checkAndFixAbsolutePath(ResourceType resource) {
if(resource == null) return;
if(isAbsolutePath(resource.getHref())) {
resource.setHref(fixAbsolutePath(resource.getHref()));
List<FileType> files = resource.getFile();
if(files != null) {
for(FileType file:files) {
if(isAbsolutePath(file.getHref())) {
file.setHref(fixAbsolutePath(file.getHref()));
}
}
}
}
}
/**
* It check if the path is absolute and in the form of a absolute within an openolat instance.
* @param href
* @return
*/
private boolean isAbsolutePath(String href) {
return href != null && href.startsWith("/") && href.contains("/bcroot/repository/") && href.contains("/_unzipped_/");
}
private String fixAbsolutePath(String href) {
int index = href.indexOf("/_unzipped_/") + ("/_unzipped_/").length();
return href.substring(index);
}
private void doSaveManifest() {
List<ResourceType> resources = manifestBuilder.getResourceList();
for(ResourceType resource:resources) {
checkAndFixAbsolutePath(resource);
}
manifestBuilder.write(new File(unzippedDirRoot, "imsmanifest.xml"));
}
private void doUpdate(Identifier identifier, String newTitle) {
TreeNode node = menuTree.getTreeModel()
.getNodeById(identifier.toString());
if(node instanceof GenericTreeNode) {
GenericTreeNode itemNode = (GenericTreeNode)node;
if(!newTitle.equals(itemNode.getTitle())) {
itemNode.setTitle(newTitle);
menuTree.setDirty(true);
}
}
}
private void partEditorFactory(UserRequest ureq, TreeNode selectedNode) {
//remove old one
if(currentEditorCtrl != null) {
mainVC.remove(currentEditorCtrl.getInitialComponent());
removeAsListenerAndDispose(currentEditorCtrl);
currentEditorCtrl = null;
}
//fill with the new
mainVC.contextPut("cssClass", selectedNode.getIconCssClass());
if(Settings.isDebuging()) {
mainVC.contextPut("identifier", selectedNode.getIdent());
}
mainVC.contextPut("title", selectedNode.getTitle());
mainVC.contextPut("restrictedEdit", restrictedEdit);
Object uobject = selectedNode.getUserObject();
if(uobject instanceof AssessmentTest) {
AssessmentTest test = (AssessmentTest)uobject;
URI testURI = resolvedAssessmentTest.getTestLookup().getSystemId();
File testFile = new File(testURI);
TestPart uniqueTestPart = test.getTestParts().size() == 1 ? test.getTestParts().get(0) : null;
currentEditorCtrl = new AssessmentTestEditorController(ureq, getWindowControl(), assessmentTestBuilder, uniqueTestPart,
unzippedDirRoot, unzippedContRoot, testFile, restrictedEdit);
} else if(uobject instanceof TestPart) {
currentEditorCtrl = new AssessmentTestPartEditorController(ureq, getWindowControl(), (TestPart)uobject,
restrictedEdit, assessmentTestBuilder.isEditable());
} else if(uobject instanceof AssessmentSection) {
currentEditorCtrl = new AssessmentSectionEditorController(ureq, getWindowControl(), (AssessmentSection)uobject,
restrictedEdit, assessmentTestBuilder.isEditable());
} else if(uobject instanceof AssessmentItemRef) {
AssessmentItemRef itemRef = (AssessmentItemRef)uobject;
ResolvedAssessmentItem item = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef);
if(item == null || item.getItemLookup() == null) {
currentEditorCtrl = new BadResourceController(ureq, getWindowControl(),
null, unzippedDirRoot, itemRef.getHref());
} else if(item.getItemLookup().getBadResourceException() != null) {
currentEditorCtrl = new BadResourceController(ureq, getWindowControl(),
item.getItemLookup().getBadResourceException(), unzippedDirRoot, itemRef.getHref());
} else {
URI itemUri = resolvedAssessmentTest.getSystemIdByItemRefMap().get(itemRef);
File itemFile = new File(itemUri);
ManifestMetadataBuilder metadata = getMetadataBuilder(itemRef);
currentEditorCtrl = new AssessmentItemEditorController(ureq, getWindowControl(), testEntry,
item, itemRef, metadata, unzippedDirRoot, unzippedContRoot, itemFile, restrictedEdit);
}
}
if(deleteLink != null) {
if(uobject instanceof AssessmentSection || uobject instanceof AssessmentItemRef) {
deleteLink.setEnabled(true);
} else if(uobject instanceof TestPart) {
TestPart testPart = (TestPart)uobject;
deleteLink.setEnabled(testPart.getParent().getTestParts().size() > 1);
} else {
deleteLink.setEnabled(false);
}
}
if(copyLink != null) {
copyLink.setEnabled(uobject instanceof AssessmentItemRef);
}
if(currentEditorCtrl != null) {
listenTo(currentEditorCtrl);
mainVC.put("content", currentEditorCtrl.getInitialComponent());
} else {
mainVC.put("content", new Panel("empty"));
}
}
private void doCopy(UserRequest ureq) {
TreeNode selectedNode = menuTree.getSelectedNode();
if(selectedNode == null || !(selectedNode.getUserObject() instanceof AssessmentItemRef)) return;
AssessmentItemRef itemRefToCopy = (AssessmentItemRef)selectedNode.getUserObject();
AssessmentSection section = itemRefToCopy.getParentSection();
ResolvedAssessmentItem resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(itemRefToCopy);
AssessmentItem originalAssessmentItem = resolvedAssessmentItem.getItemLookup().extractIfSuccessful();
QTI21QuestionType type = QTI21QuestionType.getType(originalAssessmentItem);
File itemFile = null;
try {
AssessmentItemRef itemRef = new AssessmentItemRef(section);
String itemId = IdentifierGenerator.newAsString(type.getPrefix());
itemRef.setIdentifier(Identifier.parseString(itemId));
itemFile = new File(unzippedDirRoot, itemId + ".xml");
itemRef.setHref(new URI(itemFile.getName()));
try(OutputStream out = new FileOutputStream(itemFile)) {
//make the copy
qtiService.qtiSerializer().serializeJqtiObject(originalAssessmentItem, out);
//change identifier and title
ResolvedAssessmentItem resolvedCopyItem = qtiService.loadAndResolveAssessmentItemForCopy(itemFile.toURI(), unzippedDirRoot);
AssessmentItem copiedAssessmentItem = resolvedCopyItem.getRootNodeLookup().extractIfSuccessful();
copiedAssessmentItem.setIdentifier(IdentifierGenerator.newAsString(type.getPrefix()));
copiedAssessmentItem.setTitle(originalAssessmentItem.getTitle() + " (Copy)");
qtiService.updateAssesmentObject(itemFile, resolvedCopyItem);
//add to section
section.getSectionParts().add(itemRef);
Map<AssessmentItemRef, AssessmentItem> flyingObjects = Collections.singletonMap(itemRef, copiedAssessmentItem);
doSaveAssessmentTest(flyingObjects);
manifestBuilder.appendAssessmentItem(itemFile.getName());
doSaveManifest();
} catch (Exception e) {
logError("", e);
}
updateTreeModel(false);
TreeNode newItemNode = menuTree.getTreeModel().getNodeById(itemId);
menuTree.setSelectedNode(newItemNode);
menuTree.open(newItemNode);
partEditorFactory(ureq, newItemNode);
} catch (URISyntaxException e) {
logError("", e);
}
}
private void doForceReloadFiles() {
updateTreeModel(true);
assessmentChanged = true;
}
private void doConfirmDelete(UserRequest ureq) {
if(confirmDeleteCtrl != null) return;
TreeNode selectedNode = menuTree.getSelectedNode();
if(selectedNode == null) {
showWarning("warning.atleastone");
return;
}
Object uobject = selectedNode.getUserObject();
if(uobject instanceof AssessmentTest) {
showWarning("error.cannot.delete");
} else if(uobject instanceof TestPart) {
TestPart testPart = (TestPart)uobject;
if(testPart.getParent().getTestParts().size() == 1) {
showWarning("error.cannot.delete");
}
String msg = translate("delete.testPart");
confirmDeleteCtrl = activateYesNoDialog(ureq, translate("tools.change.delete"), msg, confirmDeleteCtrl);
confirmDeleteCtrl.setUserObject(selectedNode);
} else {
String msg;
if(uobject instanceof AssessmentSection) {
AssessmentSection section = (AssessmentSection)uobject;
if(checkAtLeastOneSection(section)) {
msg = translate("delete.section", selectedNode.getTitle());
} else {
showWarning("warning.atleastonesection");
return;
}
} else if(uobject instanceof AssessmentItemRef) {
msg = translate("delete.item", selectedNode.getTitle());
} else {
showError("error.cannot.delete");
return;
}
confirmDeleteCtrl = activateYesNoDialog(ureq, translate("tools.change.delete"), msg, confirmDeleteCtrl);
confirmDeleteCtrl.setUserObject(selectedNode);
}
}
private void doDelete(UserRequest ureq, TreeNode selectedNode) {
Object uobject = selectedNode.getUserObject();
if(uobject instanceof TestPart) {
doDeleteTestPart((TestPart)uobject);
} else if(uobject instanceof AssessmentSection) {
AssessmentSection section = (AssessmentSection)uobject;
if(checkAtLeastOneSection(section)) {
doDeleteAssessmentSection(section);
} else {
showWarning("warning.atleastonesection");
}
} else if(uobject instanceof AssessmentItemRef) {
doDeleteAssessmentItemRef((AssessmentItemRef)uobject);
} else {
return;//cannot delete test or test part
}
doSaveAssessmentTest(null);
doSaveManifest();
updateTreeModel(false);
if(selectedNode != null && selectedNode.getParent() != null) {
TreeNode parentNode = (TreeNode)selectedNode.getParent();
menuTree.setSelectedNode(parentNode);
menuTree.open(parentNode);
partEditorFactory(ureq, parentNode);
}
}
private boolean checkAtLeastOneSection(AssessmentSection section) {
AbstractPart parent = section.getParent();
if(parent instanceof TestPart) {
TestPart testPart = (TestPart)parent;
for(AssessmentSection testPartSection:testPart.getAssessmentSections()) {
if(testPartSection != section) {
return true;
}
}
return false;
}
return true;
}
private void doDeleteAssessmentItemRef(AssessmentItemRef itemRef) {
ResourceType resource = getResourceType(itemRef);
if(resource != null) {
manifestBuilder.remove(resource);
}
boolean deleted = false;
boolean removed = itemRef.getParentSection().getSectionParts().remove(itemRef);
ResolvedAssessmentItem resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef);
if(resolvedAssessmentItem != null) {
RootNodeLookup<AssessmentItem> rootNode = resolvedAssessmentItem.getItemLookup();
if(rootNode != null) {
URI itemUri = rootNode.getSystemId();
List<AssessmentItemRef> itemRefs = resolvedAssessmentTest.getItemRefsBySystemIdMap().get(itemUri);
if(itemRefs.size() <= 1) {
File itemFile = new File(itemUri);
deleted = itemFile.delete();
}
}
}
if(deleted) {
assessmentChanged = true;
}
logAudit(removed + " " + deleted + " removed item ref", null);
}
private void doDeleteAssessmentSection(AssessmentSection assessmentSection) {
List<SectionPart> parts = new ArrayList<>(assessmentSection.getSectionParts());
for(SectionPart part:parts) {
if(part instanceof AssessmentItemRef) {
doDeleteAssessmentItemRef((AssessmentItemRef)part);
} else if(part instanceof AssessmentSection) {
doDeleteAssessmentSection((AssessmentSection)part);
}
}
if(assessmentSection.getParentSection() != null) {
assessmentSection.getParentSection().getSectionParts().remove(assessmentSection);
} else {
assessmentSection.getParent().getChildAbstractParts().remove(assessmentSection);
}
}
private void doDeleteTestPart(TestPart testPart) {
List<AssessmentSection> sections = new ArrayList<>(testPart.getAssessmentSections());
for(AssessmentSection section:sections) {
doDeleteAssessmentSection(section);
}
testPart.getParent().getTestParts().remove(testPart);
}
private ResourceType getResourceType(AssessmentItemRef itemRef) {
return getResourceType(manifestBuilder, itemRef);
}
private ResourceType getResourceType(ManifestBuilder builder, AssessmentItemRef itemRef) {
ResolvedAssessmentItem resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef);
if(resolvedAssessmentItem == null) return null;
RootNodeLookup<AssessmentItem> rootNode = resolvedAssessmentItem.getItemLookup();
if(rootNode == null) return null;
URI itemUri = rootNode.getSystemId();
File itemFile = new File(itemUri);
String relativePathToManifest = unzippedDirRoot.toPath().relativize(itemFile.toPath()).toString();
return builder.getResourceTypeByHref(relativePathToManifest);
}
private ManifestMetadataBuilder getMetadataBuilder(AssessmentItemRef itemRef) {
ResourceType resource = getResourceType(itemRef);
return manifestBuilder.getMetadataBuilder(resource, true);
}
}