From c99132085025a22bcc8ae3d71a1f2f3ea090e8b3 Mon Sep 17 00:00:00 2001 From: srosse <stephane.rosse@frentix.com> Date: Mon, 2 Mar 2020 17:30:56 +0100 Subject: [PATCH] OO-4528: create test sections from item subjects in question pool --- .../ims/qti21/pool/QTI21ExportProcessor.java | 46 +++++++++-- .../qti21/pool/QTI21QPoolServiceProvider.java | 4 +- .../handlers/QTI21AssessmentTestHandler.java | 3 +- .../olat/modules/qpool/model/QItemList.java | 18 ++--- .../ui/CreateTestOverviewController.java | 79 +++++++++++++++++-- .../qpool/ui/QuestionListController.java | 9 ++- .../qpool/ui/_content/create_test.html | 3 + .../qpool/ui/_i18n/LocalStrings_de.properties | 1 + .../qpool/ui/_i18n/LocalStrings_en.properties | 1 + 9 files changed, 133 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/olat/ims/qti21/pool/QTI21ExportProcessor.java b/src/main/java/org/olat/ims/qti21/pool/QTI21ExportProcessor.java index 08f5c5d6beb..fce4ef6f6dd 100644 --- a/src/main/java/org/olat/ims/qti21/pool/QTI21ExportProcessor.java +++ b/src/main/java/org/olat/ims/qti21/pool/QTI21ExportProcessor.java @@ -22,6 +22,7 @@ package org.olat.ims.qti21.pool; 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.nio.file.FileVisitResult; @@ -29,8 +30,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.concurrent.atomic.DoubleAdder; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -60,6 +63,7 @@ import org.olat.modules.qpool.manager.QPoolFileStorage; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; 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.TestPart; import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer; @@ -104,9 +108,9 @@ public class QTI21ExportProcessor { .loadAndResolveAssessmentItemForCopy(assessmentItemUri, rootDirectory); enrichWithMetadata(qitem, resolvedAssessmentItem, manifestBuilder); - try { + try(OutputStream out = new ShieldOutputStream(zout)) { zout.putNextEntry(new ZipEntry(rootDir + "/imsmanifest.xml")); - manifestBuilder.write(new ShieldOutputStream(zout)); + manifestBuilder.write(out); zout.closeEntry(); } catch (Exception e) { log.error("", e); @@ -180,7 +184,7 @@ public class QTI21ExportProcessor { metadataBuilder.appendMetadataFrom(qitem, resolvedAssessmentItem, locale); } - public void assembleTest(String title, List<QuestionItemFull> fullItems, File directory) { + public void assembleTest(String title, List<QuestionItemFull> fullItems, boolean groupByTaxonomyLevel, File directory) { try { QtiSerializer qtiSerializer = qtiService.qtiSerializer(); //imsmanifest @@ -200,7 +204,9 @@ public class QTI21ExportProcessor { manifest.appendAssessmentTest(assessmentTestFilename); //make a section - AssessmentSection section = assessmentTest.getTestParts().get(0).getAssessmentSections().get(0); + final TestPart testPart = assessmentTest.getTestParts().get(0); + AssessmentSection defaultSection = testPart.getAssessmentSections().get(0); + Map<String,AssessmentSection> sectionByTitles = new HashMap<>(); //assessment items for(QuestionItemFull qitem:fullItems) { @@ -217,6 +223,12 @@ public class QTI21ExportProcessor { File newItemFile = new File(containerDir, assessmentItem.getIdentifier() + ".xml"); String newItemFilename = container + "/" + newItemFile.getName(); qtiService.persistAssessmentObject(newItemFile, assessmentItem); + + AssessmentSection section = defaultSection; + if(groupByTaxonomyLevel && StringHelper.containsNonWhitespace(qitem.getTaxonomyLevelName())) { + section = sectionByTitles.computeIfAbsent(qitem.getTaxonomyLevelName(), level + -> AssessmentTestFactory.appendAssessmentSection(level, testPart)); + } AssessmentTestFactory.appendAssessmentItem(section, newItemFilename); manifest.appendAssessmentItem(newItemFilename); @@ -242,6 +254,10 @@ public class QTI21ExportProcessor { } } + if(defaultSection.getSectionParts().isEmpty()) { + testPart.getChildAbstractParts().remove(defaultSection); + } + AssessmentTestBuilder assessmentTestBuilder = new AssessmentTestBuilder(assessmentTest); double sumMaxScore = atomicMaxScore.sum(); if(sumMaxScore > 0.0d) { @@ -263,7 +279,6 @@ public class QTI21ExportProcessor { public void assembleTest(List<QuestionItemFull> fullItems, ZipOutputStream zout) { try { - QtiSerializer qtiSerializer = qtiService.qtiSerializer(); //imsmanifest ManifestBuilder manifest = ManifestBuilder.createAssessmentTestBuilder(); @@ -310,14 +325,31 @@ public class QTI21ExportProcessor { } zout.putNextEntry(new ZipEntry(assessmentTestFilename)); - qtiSerializer.serializeJqtiObject(assessmentTest, new ShieldOutputStream(zout)); + serializeAssessmentTest(assessmentTest, zout); zout.closeEntry(); zout.putNextEntry(new ZipEntry("imsmanifest.xml")); - manifest.write(new ShieldOutputStream(zout)); + writeManifest(manifest, zout); zout.closeEntry(); } catch (IOException | URISyntaxException e) { log.error("", e); } } + + private void writeManifest(ManifestBuilder manifest, ZipOutputStream zout) { + try(OutputStream out = new ShieldOutputStream(zout)) { + manifest.write(out); + } catch(IOException e) { + log.error("Cannot write manifest", e); + } + } + + private void serializeAssessmentTest(AssessmentTest assessmentTest, ZipOutputStream zout) { + try(OutputStream out = new ShieldOutputStream(zout)) { + QtiSerializer qtiSerializer = qtiService.qtiSerializer(); + qtiSerializer.serializeJqtiObject(assessmentTest, out); + } catch(IOException e) { + log.error("Cannot write manifest", e); + } + } } diff --git a/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java b/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java index eaab55e6202..5cc21534b8e 100644 --- a/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java +++ b/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java @@ -482,10 +482,10 @@ public class QTI21QPoolServiceProvider implements QPoolSPI { * @param items The list of questions to export * @param locale The language */ - public void exportToEditorPackage(String testTitle, File exportDir, List<QuestionItemShort> items, Locale locale) { + public void exportToEditorPackage(String testTitle, File exportDir, List<QuestionItemShort> items, boolean groupByTaxonomyLevel, Locale locale) { List<QuestionItemFull> fullItems = loadQuestionFullItems(items); QTI21ExportProcessor processor = new QTI21ExportProcessor(qtiService, qpoolFileStorage, locale); - processor.assembleTest(testTitle, fullItems, exportDir); + processor.assembleTest(testTitle, fullItems, groupByTaxonomyLevel, exportDir); } private List<QuestionItemFull> loadQuestionFullItems(List<QuestionItemShort> items) { diff --git a/src/main/java/org/olat/ims/qti21/repository/handlers/QTI21AssessmentTestHandler.java b/src/main/java/org/olat/ims/qti21/repository/handlers/QTI21AssessmentTestHandler.java index 5165706b610..d5657989d2f 100644 --- a/src/main/java/org/olat/ims/qti21/repository/handlers/QTI21AssessmentTestHandler.java +++ b/src/main/java/org/olat/ims/qti21/repository/handlers/QTI21AssessmentTestHandler.java @@ -167,7 +167,8 @@ public class QTI21AssessmentTestHandler extends FileHandler { } if(createObject instanceof QItemList) { QItemList itemToImport = (QItemList)createObject; - qpoolServiceProvider.exportToEditorPackage(displayname, repositoryDir, itemToImport.getItems(), locale); + qpoolServiceProvider.exportToEditorPackage(displayname, repositoryDir, + itemToImport.getItems(), itemToImport.isGroupByTaxonomyLevel(), locale); } else if(createObject instanceof QTIEditorPackage) { QTIEditorPackage testToConvert = (QTIEditorPackage)createObject; QTI21DeliveryOptions options = qtiService.getDeliveryOptions(re); diff --git a/src/main/java/org/olat/modules/qpool/model/QItemList.java b/src/main/java/org/olat/modules/qpool/model/QItemList.java index 921a8d107ce..0976f861508 100644 --- a/src/main/java/org/olat/modules/qpool/model/QItemList.java +++ b/src/main/java/org/olat/modules/qpool/model/QItemList.java @@ -31,22 +31,20 @@ import org.olat.modules.qpool.QuestionItemShort; */ public class QItemList { - private List<QuestionItemShort> items; + private final boolean groupByTaxonomyLevel; + private final List<QuestionItemShort> items; - public QItemList() { - // - } - - public QItemList(List<QuestionItemShort> items) { + public QItemList(List<QuestionItemShort> items, boolean groupByTaxonomyLevel) { this.items = items; + this.groupByTaxonomyLevel = groupByTaxonomyLevel; } - public List<QuestionItemShort> getItems() { - return items; + public boolean isGroupByTaxonomyLevel() { + return groupByTaxonomyLevel; } - public void setItems(List<QuestionItemShort> items) { - this.items = items; + public List<QuestionItemShort> getItems() { + return items; } } diff --git a/src/main/java/org/olat/modules/qpool/ui/CreateTestOverviewController.java b/src/main/java/org/olat/modules/qpool/ui/CreateTestOverviewController.java index 7144d4f8c1d..e09a8e7776b 100644 --- a/src/main/java/org/olat/modules/qpool/ui/CreateTestOverviewController.java +++ b/src/main/java/org/olat/modules/qpool/ui/CreateTestOverviewController.java @@ -36,13 +36,14 @@ import org.olat.core.commons.services.license.ResourceLicense; import org.olat.core.commons.services.license.ui.LicenseUIFactory; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement; import org.olat.core.gui.components.form.flexible.impl.FormBasicController; import org.olat.core.gui.components.form.flexible.impl.elements.table.BooleanCellRenderer; import org.olat.core.gui.components.form.flexible.impl.elements.table.CSSIconFlexiCellRenderer; import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiColumnModel; import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiTableDataModel; -import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiColumnDef; import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiColumnModel; +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiSortableColumnDef; import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel; import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableDataModelFactory; import org.olat.core.gui.control.Controller; @@ -52,8 +53,10 @@ import org.olat.core.util.Formatter; import org.olat.core.util.StringHelper; import org.olat.modules.qpool.ExportFormatOptions; import org.olat.modules.qpool.QPoolSPI; +import org.olat.modules.qpool.QPoolSecurityCallback; import org.olat.modules.qpool.QuestionItemShort; import org.olat.modules.qpool.QuestionPoolModule; +import org.olat.modules.qpool.QuestionStatus; import org.olat.modules.qpool.manager.QuestionPoolLicenseHandler; import org.springframework.beans.factory.annotation.Autowired; @@ -64,12 +67,15 @@ import org.springframework.beans.factory.annotation.Autowired; * */ public class CreateTestOverviewController extends FormBasicController { + + private static final String[] groupByKeys = new String[] { "on" }; private final boolean withLicenses; + private final boolean withTaxonomy; private final ExportFormatOptions format; - private QItemDataModel itemsModel; - + private QItemDataModel itemsModel; + private MultipleSelectionElement groupByEl; @Autowired private LicenseModule licenseModule; @@ -78,11 +84,11 @@ public class CreateTestOverviewController extends FormBasicController { @Autowired private QuestionPoolLicenseHandler licenseHandler; - public CreateTestOverviewController(UserRequest ureq, WindowControl wControl, List<QuestionItemShort> items, - ExportFormatOptions format) { + ExportFormatOptions format, QPoolSecurityCallback secCallback) { super(ureq, wControl, "create_test"); this.format = format; + withTaxonomy = secCallback.canUseTaxonomy(); withLicenses = licenseModule.isEnabled(licenseHandler); initForm(ureq); loadModel(items); @@ -98,15 +104,26 @@ public class CreateTestOverviewController extends FormBasicController { new CSSIconFlexiCellRenderer("o_icon_failed")) )); columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.title)); + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.topic)); + if(withTaxonomy) { + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.taxonomyLevel)); + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(false, Cols.taxonomyPath)); + } + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.type)); columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.format)); + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.status, new QuestionStatusCellRenderer())); if(withLicenses) { columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.license)); } itemsModel = new QItemDataModel(columnsModel, format, getLocale()); uifactory.addTableElement(getWindowControl(), "shares", itemsModel, getTranslator(), formLayout); - uifactory.addFormSubmitButton("create.test", formLayout); + String[] groupByValues = new String[] { translate("group.by.taxonomy.level") }; + groupByEl = uifactory.addCheckboxesHorizontal("group.by", null, formLayout, groupByKeys, groupByValues); + groupByEl.setVisible(withTaxonomy); + uifactory.addFormCancelButton("cancel", formLayout, ureq, getWindowControl()); + uifactory.addFormSubmitButton("create.test", formLayout); } private void loadModel(List<QuestionItemShort> items) { @@ -156,6 +173,10 @@ public class CreateTestOverviewController extends FormBasicController { } return exportableItems; } + + public boolean isGroupByTaxonomyLevel() { + return groupByEl.isVisible() && groupByEl.isAtLeastSelected(1); + } @Override protected void formOK(UserRequest ureq) { @@ -218,6 +239,18 @@ public class CreateTestOverviewController extends FormBasicController { return question.getTitle(); } + public String getTopic() { + return question.getTopic(); + } + + public String getTaxonomyLevelName() { + return question.getTaxonomyLevelName(); + } + + public String getTaxonomyPath() { + return question.getTaxonomicPath(); + } + public String getFormat() { return question.getFormat(); } @@ -225,6 +258,18 @@ public class CreateTestOverviewController extends FormBasicController { public ResourceLicense getLicense() { return license; } + + public QuestionStatus getQuestionStatus() { + return question.getQuestionStatus(); + } + + public String getItemType() { + String type = question.getItemType(); + if(type == null) { + return ""; + } + return type; + } public QuestionItemShort getQuestion() { return question; @@ -261,7 +306,12 @@ public class CreateTestOverviewController extends FormBasicController { return Boolean.FALSE; } case title: return share.getTitle(); + case topic: return share.getTopic(); + case taxonomyLevel: return share.getTaxonomyLevelName(); + case taxonomyPath: return share.getTaxonomyPath(); case format: return share.getFormat(); + case type: return share.getItemType(); + case status: return share.getQuestionStatus(); case license: return shortenedLicense(share); default : return share; } @@ -286,10 +336,15 @@ public class CreateTestOverviewController extends FormBasicController { } } - private enum Cols implements FlexiColumnDef { + private enum Cols implements FlexiSortableColumnDef { accept("export.overview.accept"), title("general.title"), + topic("general.topic"), + taxonomyLevel("classification.taxonomy.level"), + taxonomyPath("classification.taxonomic.path"), format("technical.format"), + status("lifecycle.status"), + type("question.type"), license("rights.license"); private final String i18nKey; @@ -302,5 +357,15 @@ public class CreateTestOverviewController extends FormBasicController { public String i18nHeaderKey() { return i18nKey; } + + @Override + public boolean sortable() { + return false; + } + + @Override + public String sortKey() { + return name(); + } } } diff --git a/src/main/java/org/olat/modules/qpool/ui/QuestionListController.java b/src/main/java/org/olat/modules/qpool/ui/QuestionListController.java index 813f607bac5..4204cbd40b3 100644 --- a/src/main/java/org/olat/modules/qpool/ui/QuestionListController.java +++ b/src/main/java/org/olat/modules/qpool/ui/QuestionListController.java @@ -520,10 +520,11 @@ public class QuestionListController extends AbstractItemListController implement List<QuestionItemShort> items = createTestOverviewCtrl.getExportableQuestionItems(); String typeFormat = createTestOverviewCtrl.getResourceTypeFormat(); LicenseType licenseType = createTestOverviewCtrl.getLicenseType(); + boolean groupBy = createTestOverviewCtrl.isGroupByTaxonomyLevel(); cmc.deactivate(); cleanUp(); if (event == Event.DONE_EVENT) { - doOpenCreateRepositoryTest(ureq, items, typeFormat, licenseType); + doOpenCreateRepositoryTest(ureq, items, typeFormat, licenseType, groupBy); } } else if(source == exportWizard) { if(event == Event.CANCELLED_EVENT || event == Event.DONE_EVENT || event == Event.CHANGED_EVENT) { @@ -1042,7 +1043,7 @@ public class QuestionListController extends AbstractItemListController implement private void doShowCreateTestOverview(UserRequest ureq, List<QuestionItemShort> items, ExportFormatOptions format) { removeAsListenerAndDispose(createTestOverviewCtrl); - createTestOverviewCtrl = new CreateTestOverviewController(ureq, getWindowControl(), items, format); + createTestOverviewCtrl = new CreateTestOverviewController(ureq, getWindowControl(), items, format, getSecurityCallback()); listenTo(createTestOverviewCtrl); removeAsListenerAndDispose(cmc); @@ -1052,13 +1053,13 @@ public class QuestionListController extends AbstractItemListController implement listenTo(cmc); } - private void doOpenCreateRepositoryTest(UserRequest ureq, List<QuestionItemShort> items, String type, LicenseType licenseType) { + private void doOpenCreateRepositoryTest(UserRequest ureq, List<QuestionItemShort> items, String type, LicenseType licenseType, boolean groupBy) { removeAsListenerAndDispose(cmc); removeAsListenerAndDispose(addController); RepositoryHandler handler = repositoryHandlerFactory.getRepositoryHandler(type); addController = handler.createCreateRepositoryEntryController(ureq, getWindowControl()); - addController.setCreateObject(new QItemList(items)); + addController.setCreateObject(new QItemList(items, groupBy)); addController.setLicenseType(licenseType); listenTo(addController); cmc = new CloseableModalController(getWindowControl(), translate("close"), addController.getInitialComponent()); diff --git a/src/main/java/org/olat/modules/qpool/ui/_content/create_test.html b/src/main/java/org/olat/modules/qpool/ui/_content/create_test.html index 5e3ebd6d6bd..c0a5f52c911 100644 --- a/src/main/java/org/olat/modules/qpool/ui/_content/create_test.html +++ b/src/main/java/org/olat/modules/qpool/ui/_content/create_test.html @@ -3,6 +3,9 @@ $r.translate("warning.different.licenses")</div> #end $r.render("shares") +#if($r.visible("group.by")) + $r.render("group.by") +#end <div class="o_button_group"> $r.render("cancel") $r.render("create.test") diff --git a/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_de.properties index 0960dec9316..4cba538728e 100644 --- a/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_de.properties @@ -115,6 +115,7 @@ general.taxonomy.level=Fachbereich general.taxonomy.path={0} general.title=Titel general.topic=Thema +group.by.taxonomy.level=Fragen nach Fachbereich gruppieren import.excellike.12=QTI 1.2 Excelimport \u00FCber Copy&Paste import.excellike.21=QTI 2.1 Excelimport \u00FCber Copy&Paste import.failed=Frage konnte nicht importiert werden. diff --git a/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_en.properties index 11f9173265e..f514b7bec39 100644 --- a/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_en.properties @@ -115,6 +115,7 @@ general.taxonomy.level=Subject general.taxonomy.path={0} general.title=Title general.topic=Topic +group.by.taxonomy.level=Group items by subject import.excellike.12=QTI 1.2 Excel import via copy&paste import.excellike.21=QTI 2.1 Excel import via copy&paste import.failed=No questions were imported -- GitLab