From 32f246e9f35b7260f4810b5a4619a161f9db20dd Mon Sep 17 00:00:00 2001 From: srosse <stephane.rosse@frentix.com> Date: Thu, 11 Jun 2020 15:32:45 +0200 Subject: [PATCH] OO-4742: report with list of courses, tests and questions A report with the list of courses and tests with the questions used in the tests, their master reference inclusive ownership of tests, questions and master questions. --- .../org/olat/_spring/extensionContext.xml | 19 +- .../admin/_i18n/LocalStrings_de.properties | 2 + .../admin/_i18n/LocalStrings_en.properties | 2 + .../java/org/olat/ims/_spring/imsContext.xml | 20 + .../report/QuestionOriginMediaResource.java | 597 ++++++++++++++++++ .../QuestionOriginReportController.java | 78 +++ .../QuestionOriginReportSearchController.java | 64 ++ .../QuestionOriginReportTableController.java | 151 +++++ .../olat/ims/qti21/ui/report/SearchEvent.java | 34 + .../qti21/ui/report/_content/report_list.html | 6 + .../ims/qti21/ui/report/_content/reports.html | 3 + .../report/_i18n/LocalStrings_de.properties | 28 + .../report/_i18n/LocalStrings_en.properties | 31 + .../manager/RepositoryEntryQueries.java | 31 + .../SearchRepositoryEntryParameters.java | 9 + 15 files changed, 1073 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginMediaResource.java create mode 100644 src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginReportController.java create mode 100644 src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginReportSearchController.java create mode 100644 src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginReportTableController.java create mode 100644 src/main/java/org/olat/ims/qti21/ui/report/SearchEvent.java create mode 100644 src/main/java/org/olat/ims/qti21/ui/report/_content/report_list.html create mode 100644 src/main/java/org/olat/ims/qti21/ui/report/_content/reports.html create mode 100644 src/main/java/org/olat/ims/qti21/ui/report/_i18n/LocalStrings_de.properties create mode 100644 src/main/java/org/olat/ims/qti21/ui/report/_i18n/LocalStrings_en.properties diff --git a/src/main/java/org/olat/_spring/extensionContext.xml b/src/main/java/org/olat/_spring/extensionContext.xml index ecfaddd8630..ed71983e1d4 100644 --- a/src/main/java/org/olat/_spring/extensionContext.xml +++ b/src/main/java/org/olat/_spring/extensionContext.xml @@ -34,7 +34,7 @@ </property> </bean> - <!-- The Module parent node --> + <!-- The login parent node --> <bean class="org.olat.core.extensions.action.GenericActionExtension" init-method="initExtensionPoints"> <property name="order" value="7300" /> <property name="navigationKey" value="loginandsecurity" /> @@ -49,7 +49,7 @@ </property> </bean> - <!-- the "systemconfig" parent node --> + <!-- The core configuration parent node --> <bean class="org.olat.core.extensions.action.GenericActionExtension" init-method="initExtensionPoints"> <property name="order" value="7200" /> <property name="actionController"> @@ -99,6 +99,21 @@ </property> </bean> + <!-- The reports parent node --> + <bean class="org.olat.core.extensions.action.GenericActionExtension" init-method="initExtensionPoints"> + <property name="order" value="7400" /> + <property name="navigationKey" value="reports" /> + <property name="nodeIdentifierIfParent" value="reportsParent" /> + <property name="translationPackage" value="org.olat.admin" /> + <property name="i18nActionKey" value="menu.reports" /> + <property name="i18nDescriptionKey" value="menu.reports.alt" /> + <property name="extensionPoints"> + <list> + <value>org.olat.admin.SystemAdminMainController</value> + </list> + </property> + </bean> + <!-- The e-Assessment parent node --> <bean class="org.olat.core.extensions.action.GenericActionExtension" init-method="initExtensionPoints"> <property name="order" value="7420" /> diff --git a/src/main/java/org/olat/admin/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/admin/_i18n/LocalStrings_de.properties index bc3a551f01e..a82c8a32585 100644 --- a/src/main/java/org/olat/admin/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/admin/_i18n/LocalStrings_de.properties @@ -68,6 +68,8 @@ menu.quota=Quotaverwaltung menu.quota.alt=Quotaverwaltung menu.registration=Systemregistrierung menu.registration.alt=Registrieren Sie Ihr OpenOlat System bei OpenOlat.org +menu.reports=Reporte +menu.reports.alt=Reporte menu.restapi=REST API menu.restapi.alt=REST API Einstellungen menu.scheduler=Scheduler diff --git a/src/main/java/org/olat/admin/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/admin/_i18n/LocalStrings_en.properties index ac3f3284a0f..47f585cc6a1 100644 --- a/src/main/java/org/olat/admin/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/admin/_i18n/LocalStrings_en.properties @@ -68,6 +68,8 @@ menu.quota=Quota management menu.quota.alt=Quota management menu.registration=System registration menu.registration.alt=Please register your OpenOlat system at OpenOlat.org +menu.reports=Reports +menu.reports.alt=Reports menu.restapi=REST API menu.restapi.alt=REST API settings menu.scheduler=Scheduler diff --git a/src/main/java/org/olat/ims/_spring/imsContext.xml b/src/main/java/org/olat/ims/_spring/imsContext.xml index 1e81974c841..0892a53c6ca 100644 --- a/src/main/java/org/olat/ims/_spring/imsContext.xml +++ b/src/main/java/org/olat/ims/_spring/imsContext.xml @@ -30,4 +30,24 @@ </property> </bean> + <!-- Questions report panel --> + <bean class="org.olat.core.extensions.action.GenericActionExtension" init-method="initExtensionPoints"> + <property name="order" value="8210" /> + <property name="actionController"> + <bean class="org.olat.core.gui.control.creator.AutoCreator" scope="prototype"> + <property name="className" value="org.olat.ims.qti21.ui.report.QuestionOriginReportController"/> + </bean> + </property> + <property name="navigationKey" value="reportQuestions" /> + <property name="i18nActionKey" value="admin.menu.report.question.title"/> + <property name="i18nDescriptionKey" value="admin.menu.report.question.title.alt"/> + <property name="translationPackage" value="org.olat.ims.qti21.ui.report"/> + <property name="parentTreeNodeIdentifier" value="reportsParent" /> + <property name="extensionPoints"> + <list> + <value>org.olat.admin.SystemAdminMainController</value> + </list> + </property> + </bean> + </beans> \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginMediaResource.java b/src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginMediaResource.java new file mode 100644 index 00000000000..d290ea08302 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginMediaResource.java @@ -0,0 +1,597 @@ +/** + * <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.report; + +import java.io.File; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.logging.log4j.Logger; +import org.olat.basesecurity.GroupRoles; +import org.olat.core.CoreSpringFactory; +import org.olat.core.commons.persistence.DB; +import org.olat.core.gui.translator.Translator; +import org.olat.core.id.Identity; +import org.olat.core.logging.Tracing; +import org.olat.core.util.StringHelper; +import org.olat.core.util.openxml.OpenXMLWorkbook; +import org.olat.core.util.openxml.OpenXMLWorkbookResource; +import org.olat.core.util.openxml.OpenXMLWorksheet; +import org.olat.core.util.openxml.OpenXMLWorksheet.Row; +import org.olat.fileresource.FileResourceManager; +import org.olat.ims.qti21.QTI21Service; +import org.olat.ims.qti21.model.QTI21QuestionType; +import org.olat.ims.qti21.model.xml.ManifestBuilder; +import org.olat.ims.qti21.model.xml.ManifestMetadataBuilder; +import org.olat.imscp.xml.manifest.ResourceType; +import org.olat.modules.qpool.QPoolService; +import org.olat.modules.qpool.QuestionItem; +import org.olat.repository.RepositoryEntry; +import org.olat.repository.RepositoryEntryRelationType; +import org.olat.repository.RepositoryManager; +import org.olat.repository.RepositoryService; +import org.olat.resource.OLATResource; +import org.olat.resource.references.Reference; +import org.olat.resource.references.ReferenceManager; +import org.springframework.beans.factory.annotation.Autowired; + +import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; +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; + + +/** + * + * Initial date: 11 juin 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class QuestionOriginMediaResource extends OpenXMLWorkbookResource { + + private static final Logger log = Tracing.createLoggerFor(QuestionOriginMediaResource.class); + + private final Translator translator; + private final List<RepositoryEntry> testEntries; + private final ConcurrentMap<RepositoryEntry, TestHolder> holdersMap = new ConcurrentHashMap<>(); + + @Autowired + private DB dbInstance; + @Autowired + private QTI21Service qtiService; + @Autowired + private QPoolService qpoolService; + @Autowired + private ReferenceManager referenceManager; + @Autowired + private RepositoryManager repositoryManager; + @Autowired + private RepositoryService repositoryService; + + public QuestionOriginMediaResource(String label, List<RepositoryEntry> testEntries, Translator translator) { + super(label); + CoreSpringFactory.autowireObject(this); + this.translator = translator; + this.testEntries = testEntries; + } + + @Override + protected void generate(OutputStream out) { + try(OpenXMLWorkbook workbook = new OpenXMLWorkbook(out, 1)) { + OpenXMLWorksheet sheet = workbook.nextWorksheet(); + sheet.setHeaderRows(1); + generateHeaders(sheet); + generateData(sheet); + } catch (Exception e) { + log.error("", e); + } + } + + protected void generateHeaders(OpenXMLWorksheet sheet) { + Row headerRow = sheet.newRow(); + int col = 0; + + // course + headerRow.addCell(col++, translator.translate("report.course.id")); + headerRow.addCell(col++, translator.translate("report.course.displayname")); + headerRow.addCell(col++, translator.translate("report.course.externalref")); + + // test + headerRow.addCell(col++, translator.translate("report.test.id")); + headerRow.addCell(col++, translator.translate("report.test.displayname")); + headerRow.addCell(col++, translator.translate("report.test.externalref")); + headerRow.addCell(col++, translator.translate("report.test.author")); + + // question + headerRow.addCell(col++, translator.translate("report.question.title")); + headerRow.addCell(col++, translator.translate("report.question.author")); + headerRow.addCell(col++, translator.translate("report.question.identifier")); + headerRow.addCell(col++, translator.translate("report.question.keywords")); + headerRow.addCell(col++, translator.translate("report.question.taxonomy.path")); + headerRow.addCell(col++, translator.translate("report.question.topic")); + headerRow.addCell(col++, translator.translate("report.question.context")); + headerRow.addCell(col++, translator.translate("report.question.type")); + + // master + headerRow.addCell(col++, translator.translate("report.question.master.identifier")); + headerRow.addCell(col++, translator.translate("report.question.master.author")); + headerRow.addCell(col, translator.translate("report.question.master.keywords")); + } + + protected void generateData(OpenXMLWorksheet sheet) { + List<CourseToTestHolder> courseToTestList = loadCoursesAndTests(); + dbInstance.commitAndCloseSession(); + Collections.sort(courseToTestList); + + for(CourseToTestHolder courseToTest:courseToTestList) { + try { + TestHolder testHolder = getTestHolder(courseToTest.getTestEntry()); + List<QuestionInformations> questionList = testHolder.getQuestionList(); + for(QuestionInformations question:questionList) { + generateData(question, testHolder, courseToTest, sheet); + } + } catch (Exception e) { + log.error("", e); + } finally { + dbInstance.commitAndCloseSession(); + } + } + } + + protected void generateData(QuestionInformations question, TestHolder testHolder, CourseToTestHolder courseToTest, OpenXMLWorksheet sheet) { + Row row = sheet.newRow(); + + // course + int col = 0; + if(courseToTest.getCourseEntry() == null) { + col += 3; + } else { + RepositoryEntry courseEntry = courseToTest.getCourseEntry(); + row.addCell(col++, courseEntry.getKey().toString()); + row.addCell(col++, courseEntry.getDisplayname()); + row.addCell(col++, courseEntry.getExternalRef()); + } + + // test + RepositoryEntry testEntry = courseToTest.getTestEntry(); + row.addCell(col++, testEntry.getKey().toString()); + row.addCell(col++, testEntry.getDisplayname()); + row.addCell(col++, testEntry.getExternalRef()); + row.addCell(col++, testHolder.getOwners()); + + // question + row.addCell(col++, question.getTitle()); + row.addCell(col++, question.getAuthor()); + row.addCell(col++, question.getIdentifier()); + row.addCell(col++, question.getKeywords()); + row.addCell(col++, question.getTaxonomyPath()); + row.addCell(col++, question.getTopic()); + row.addCell(col++, question.getEducationalContextLevel()); + row.addCell(col++, question.getType()); + + // master question + row.addCell(col++, question.getMasterIdentifier()); + if(question.getMasterInformations() != null) { + MasterInformations masterInfos = question.getMasterInformations(); + row.addCell(col++, masterInfos.getMasterAuthor()); + row.addCell(col, masterInfos.getMasterKeywords()); + } + } + + private TestHolder getTestHolder(RepositoryEntry testEntry) { + return holdersMap.computeIfAbsent(testEntry, entry -> loadQuestionMetadata(testEntry)); + } + + private TestHolder loadQuestionMetadata(RepositoryEntry testEntry) { + TestHolder testHolder = new TestHolder(testEntry); + List<QuestionInformations> questionList = new ArrayList<>(32); + if(testHolder.isOk()) { + AssessmentTest assessmentTest = testHolder.getAssessmentTest(); + List<TestPart> parts = assessmentTest.getChildAbstractParts(); + for(TestPart part:parts) { + List<AssessmentSection> sections = part.getAssessmentSections(); + for(AssessmentSection section:sections) { + loadQuestionMetadata(section, questionList, testHolder); + } + } + } + testHolder.setQuestionList(questionList); + + String owners = getOwners(testEntry); + testHolder.setOwners(owners); + + return testHolder; + } + + private String getOwners(RepositoryEntry entry) { + List<Identity> owners = repositoryService + .getMembers(entry, RepositoryEntryRelationType.defaultGroup, GroupRoles.owner.name()); + return toString(owners); + } + + private String getAuthors(QuestionItem item) { + List<Identity> authors = qpoolService.getAuthors(item); + return toString(authors); + } + + private String toString(List<Identity> owners) { + StringBuilder sb = new StringBuilder(32); + if(owners != null && !owners.isEmpty()) { + for(Identity owner:owners) { + if(sb.length() > 0) sb.append(", "); + + String firstName = owner.getUser().getFirstName(); + boolean hasFirstName = StringHelper.containsNonWhitespace(firstName); + if(hasFirstName) { + sb.append(firstName); + } + + String lastName = owner.getUser().getLastName(); + if(StringHelper.containsNonWhitespace(lastName)) { + if(hasFirstName) sb.append(" "); + sb.append(lastName); + } + } + } + return sb.toString(); + } + + private void loadQuestionMetadata(AssessmentSection section, List<QuestionInformations> questionList, TestHolder testHolder) { + for(SectionPart part: section.getSectionParts()) { + if(part instanceof AssessmentItemRef) { + QuestionInformations infos = loadQuestionMetadata((AssessmentItemRef)part, testHolder); + if(infos != null) { + questionList.add(infos); + } + } else if(part instanceof AssessmentSection) { + loadQuestionMetadata((AssessmentSection)part, questionList, testHolder); + } + } + } + + private QuestionInformations loadQuestionMetadata(AssessmentItemRef itemRef, TestHolder testHolder) { + QuestionInformations infos = null; + ManifestMetadataBuilder metadata = testHolder.getManifestMetadataBuilder(itemRef); + if(metadata == null) { + AssessmentItem assessmentItem = testHolder.getAssessmentItem(itemRef); + if(assessmentItem != null) { + infos = new QuestionInformations(assessmentItem); + } + } else if(!StringHelper.containsNonWhitespace(metadata.getOpenOLATMetadataIdentifier())) { + AssessmentItem assessmentItem = testHolder.getAssessmentItem(itemRef); + if(assessmentItem != null) { + infos = new QuestionInformations(assessmentItem, metadata); + } else { + infos = new QuestionInformations(metadata); + } + } else { + String identifier = metadata.getOpenOLATMetadataIdentifier(); + List<QuestionItem> items = qpoolService.loadItemByIdentifier(identifier); + if(items.isEmpty()) { + infos = new QuestionInformations(metadata); + } else { + QuestionItem item = items.get(0); + String authors = getAuthors(item); + infos = new QuestionInformations(authors, item); + } + + String masterIdentifier = metadata.getOpenOLATMetadataMasterIdentifier(); + if(StringHelper.containsNonWhitespace(masterIdentifier)) { + List<QuestionItem> masterItems = qpoolService.loadItemByIdentifier(masterIdentifier); + if(!masterItems.isEmpty()) { + QuestionItem masterItem = masterItems.get(0); + String masterAuthors = getAuthors(masterItem); + MasterInformations masterInfos = new MasterInformations(masterAuthors, masterItem); + infos.setMasterInformations(masterInfos); + } + } + } + + return infos; + } + + private List<CourseToTestHolder> loadCoursesAndTests() { + List<CourseToTestHolder> courseToTestList = new ArrayList<>(128); + for(RepositoryEntry testEntry:testEntries) { + boolean hasOneReference = false; + List<Reference> references = referenceManager.getReferencesTo(testEntry.getOlatResource()); + for(Reference reference:references) { + OLATResource courseResource = reference.getSource(); + RepositoryEntry courseEntry = repositoryManager.lookupRepositoryEntry(courseResource, false); + if(courseEntry != null) { + courseToTestList.add(new CourseToTestHolder(courseEntry, testEntry)); + hasOneReference = true; + } + } + + if(!hasOneReference) { + courseToTestList.add(new CourseToTestHolder(null, testEntry)); + } + } + return courseToTestList; + } + + private class TestHolder { + + private final RepositoryEntry testEntry; + + private AssessmentTest assessmentTest; + private final ManifestBuilder manifestBuilder; + private final ResolvedAssessmentTest resolvedAssessmentTest; + + private final File unzippedDirRoot; + + private String owners; + private List<QuestionInformations> questionList; + + public TestHolder(RepositoryEntry testEntry) { + this.testEntry = testEntry; + + FileResourceManager frm = FileResourceManager.getInstance(); + unzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource()); + manifestBuilder = ManifestBuilder.read(new File(unzippedDirRoot, "imsmanifest.xml")); + resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(unzippedDirRoot, false, true); + if(resolvedAssessmentTest != null) { + assessmentTest = resolvedAssessmentTest.getRootNodeLookup().extractIfSuccessful(); + } + } + + public boolean isOk() { + return testEntry != null && manifestBuilder != null && assessmentTest != null; + } + + public String getOwners() { + return owners; + } + + public void setOwners(String owners) { + this.owners = owners; + } + + public List<QuestionInformations> getQuestionList() { + return questionList; + } + + public void setQuestionList(List<QuestionInformations> questionList) { + this.questionList = questionList; + } + + public AssessmentTest getAssessmentTest() { + return assessmentTest; + } + + public ManifestMetadataBuilder getManifestMetadataBuilder(AssessmentItemRef itemRef) { + RootNodeLookup<AssessmentItem> rootNode = getItemLookup(itemRef); + if(rootNode == null) return null; + File itemFile = new File(rootNode.getSystemId()); + String relativePathToManifest = unzippedDirRoot.toPath().relativize(itemFile.toPath()).toString(); + ResourceType resource = manifestBuilder.getResourceTypeByHref(relativePathToManifest); + return manifestBuilder.getMetadataBuilder(resource, true); + } + + public AssessmentItem getAssessmentItem(AssessmentItemRef itemRef) { + RootNodeLookup<AssessmentItem> rootNode = getItemLookup(itemRef); + if(rootNode == null) return null; + return rootNode.extractIfSuccessful(); + } + + public RootNodeLookup<AssessmentItem> getItemLookup(AssessmentItemRef itemRef) { + ResolvedAssessmentItem resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef); + if(resolvedAssessmentItem == null) return null; + return resolvedAssessmentItem.getItemLookup(); + } + } + + private static class QuestionInformations { + + private final String identifier; + private final String title; + private final String author; + private final String keywords; + private final String taxonomyPath; + private final String topic; + private final String context; + private final String type; + private final String masterIdentifier; + + private MasterInformations masterInformations; + + public QuestionInformations(String author, QuestionItem item) { + identifier = item.getIdentifier(); + title = item.getTitle(); + this.author = author; + keywords = item.getKeywords(); + taxonomyPath = item.getTaxonomicPath(); + topic = item.getTopic(); + context = item.getEducationalContextLevel(); + type = item.getItemType(); + masterIdentifier = item.getMasterIdentifier(); + } + + public QuestionInformations(ManifestMetadataBuilder metadata) { + identifier = metadata.getOpenOLATMetadataIdentifier(); + title = metadata.getTitle(); + author = null; + keywords = metadata.getGeneralKeywords(); + taxonomyPath = metadata.getClassificationTaxonomy(); + topic = metadata.getOpenOLATMetadataTopic(); + context = metadata.getEducationContext(); + type = metadata.getOpenOLATMetadataQuestionType(); + masterIdentifier = metadata.getOpenOLATMetadataMasterIdentifier(); + } + + public QuestionInformations(AssessmentItem item, ManifestMetadataBuilder metadata) { + identifier = metadata.getOpenOLATMetadataIdentifier(); + if(StringHelper.containsNonWhitespace(metadata.getTitle())) { + title = metadata.getTitle(); + } else { + title = item.getTitle(); + } + author = null; + keywords = metadata.getGeneralKeywords(); + taxonomyPath = metadata.getClassificationTaxonomy(); + topic = metadata.getOpenOLATMetadataTopic(); + context = metadata.getEducationContext(); + type = metadata.getOpenOLATMetadataQuestionType(); + masterIdentifier = metadata.getOpenOLATMetadataMasterIdentifier(); + } + + public QuestionInformations(AssessmentItem assessmentItem) { + identifier = null; + title = assessmentItem.getTitle(); + author = null; + keywords = null; + taxonomyPath = null; + topic = assessmentItem.getLabel(); + context = null; + masterIdentifier = null; + + QTI21QuestionType qType = QTI21QuestionType.getType(assessmentItem); + if(qType == null) { + type = null; + } else { + type = qType.name(); + } + } + + public String getAuthor() { + return author; + } + + public String getIdentifier() { + return identifier; + } + + public String getTitle() { + return title; + } + + public String getKeywords() { + return keywords; + } + + public String getTaxonomyPath() { + return taxonomyPath; + } + + public String getTopic() { + return topic; + } + + public String getEducationalContextLevel() { + return context; + } + + public String getType() { + return type; + } + + public String getMasterIdentifier() { + return masterIdentifier; + } + + public MasterInformations getMasterInformations() { + return masterInformations; + } + + public void setMasterInformations(MasterInformations masterInformations) { + this.masterInformations = masterInformations; + } + } + + private static class MasterInformations { + private final String masterAuthor; + private final String masterKeywords; + + public MasterInformations(String masterAuthor, QuestionItem item) { + this.masterAuthor = masterAuthor; + this.masterKeywords = item.getKeywords(); + } + + public String getMasterAuthor() { + return masterAuthor; + } + + public String getMasterKeywords() { + return masterKeywords; + } + } + + private static class CourseToTestHolder implements Comparable<CourseToTestHolder> { + + private final RepositoryEntry courseEntry; + private final RepositoryEntry testEntry; + + public CourseToTestHolder(RepositoryEntry courseEntry, RepositoryEntry testEntry) { + this.courseEntry = courseEntry; + this.testEntry = testEntry; + } + + public RepositoryEntry getCourseEntry() { + return courseEntry; + } + + public RepositoryEntry getTestEntry() { + return testEntry; + } + + @Override + public int hashCode() { + return (courseEntry == null ? 2671 : courseEntry.hashCode()) + + (testEntry == null ? 9148 : testEntry.hashCode()); + } + + @Override + public boolean equals(Object obj) { + if(obj instanceof CourseToTestHolder) { + CourseToTestHolder ctot = (CourseToTestHolder)obj; + return ((courseEntry == null && ctot.getCourseEntry() == null) || (courseEntry != null && courseEntry.equals(ctot.getCourseEntry()))) + && ((testEntry == null && ctot.getTestEntry() == null) || (testEntry != null && testEntry.equals(ctot.getTestEntry()))); + } + return false; + } + + @Override + public int compareTo(CourseToTestHolder courseToTest) { + int c = 0; + if(courseEntry != null && courseToTest.getCourseEntry() != null) { + c = courseEntry.getKey().compareTo(courseToTest.getCourseEntry().getKey()); + } else if(courseEntry != null && courseToTest.getCourseEntry() == null) { + c = 1; + } else if(courseEntry == null && courseToTest.getCourseEntry() != null) { + c = -1; + } + + if(c == 0) { + c = testEntry.getKey().compareTo(courseToTest.getTestEntry().getKey()); + } + return c; + } + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginReportController.java b/src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginReportController.java new file mode 100644 index 00000000000..b41acbbb2cc --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginReportController.java @@ -0,0 +1,78 @@ +/** + * <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.report; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.Component; +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.WindowControl; +import org.olat.core.gui.control.controller.BasicController; + +/** + * + * Initial date: 11 juin 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class QuestionOriginReportController extends BasicController { + + private final QuestionOriginReportTableController tableCtrl; + private final QuestionOriginReportSearchController searchCtrl; + + public QuestionOriginReportController(UserRequest ureq, WindowControl wControl) { + super(ureq, wControl); + + searchCtrl = new QuestionOriginReportSearchController(ureq, wControl); + listenTo(searchCtrl); + tableCtrl = new QuestionOriginReportTableController(ureq, wControl); + listenTo(tableCtrl); + + VelocityContainer mainVC = createVelocityContainer("reports"); + mainVC.put("search", searchCtrl.getInitialComponent()); + mainVC.put("table", tableCtrl.getInitialComponent()); + putInitialPanel(mainVC); + } + + @Override + protected void doDispose() { + // + } + + @Override + protected void event(UserRequest ureq, Controller source, Event event) { + if(searchCtrl == source) { + if(event instanceof SearchEvent) { + doSearch(ureq, (SearchEvent)event); + } + } + super.event(ureq, source, event); + } + + @Override + protected void event(UserRequest ureq, Component source, Event event) { + // + } + + private void doSearch(UserRequest ureq, SearchEvent searchEvent) { + tableCtrl.loadModel(ureq, searchEvent.getSearchString(), searchEvent.getAuthor()); + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginReportSearchController.java b/src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginReportSearchController.java new file mode 100644 index 00000000000..042f8ec3401 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginReportSearchController.java @@ -0,0 +1,64 @@ +/** + * <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.report; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.TextElement; +import org.olat.core.gui.components.form.flexible.impl.FormBasicController; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.WindowControl; + +/** + * + * Initial date: 11 juin 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class QuestionOriginReportSearchController extends FormBasicController { + + private TextElement searchStringEl; + private TextElement authorEl; + + public QuestionOriginReportSearchController(UserRequest ureq, WindowControl wControl) { + super(ureq, wControl); + + initForm(ureq); + } + + @Override + protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { + searchStringEl = uifactory.addTextElement("search.text", 2000, null, formLayout); + authorEl = uifactory.addTextElement("search.author", 2000, null, formLayout); + uifactory.addFormSubmitButton("search", formLayout); + } + + @Override + protected void doDispose() { + // + } + + @Override + protected void formOK(UserRequest ureq) { + String searchString = searchStringEl.getValue(); + String author = authorEl.getValue(); + fireEvent(ureq, new SearchEvent(searchString, author)); + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginReportTableController.java b/src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginReportTableController.java new file mode 100644 index 00000000000..b498e05572f --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/report/QuestionOriginReportTableController.java @@ -0,0 +1,151 @@ +/** + * <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.report; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItem; +import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.FlexiTableElement; +import org.olat.core.gui.components.form.flexible.elements.FormLink; +import org.olat.core.gui.components.form.flexible.impl.FormBasicController; +import org.olat.core.gui.components.form.flexible.impl.FormEvent; +import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiColumnModel; +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.components.link.Link; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.WindowControl; +import org.olat.core.gui.media.MediaResource; +import org.olat.core.util.Formatter; +import org.olat.core.util.StringHelper; +import org.olat.core.util.Util; +import org.olat.fileresource.types.ImsQTI21Resource; +import org.olat.ims.qti21.ui.AssessmentTestDisplayController; +import org.olat.repository.RepositoryEntry; +import org.olat.repository.RepositoryManager; +import org.olat.repository.RepositoryService; +import org.olat.repository.model.SearchRepositoryEntryParameters; +import org.olat.repository.ui.RepositoryEntryACColumnDescriptor; +import org.olat.repository.ui.RepositoryFlexiTableModel; +import org.olat.repository.ui.RepositoryFlexiTableModel.RepoCols; +import org.olat.repository.ui.author.TypeRenderer; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * + * Initial date: 11 juin 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class QuestionOriginReportTableController extends FormBasicController { + + private FormLink generateReportButton; + + private FlexiTableElement tableEl; + private RepositoryFlexiTableModel tableModel; + + @Autowired + private RepositoryManager repositoryManager; + + public QuestionOriginReportTableController(UserRequest ureq, WindowControl wControl) { + super(ureq, wControl, "report_list", Util.createPackageTranslator(AssessmentTestDisplayController.class, ureq.getLocale(), + Util.createPackageTranslator(RepositoryService.class, ureq.getLocale()))); + + initForm(ureq); + } + + @Override + protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { + FlexiTableColumnModel columnsModel = FlexiTableDataModelFactory.createFlexiTableColumnModel(); + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(RepoCols.ac, new RepositoryEntryACColumnDescriptor())); + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(RepoCols.repoEntry, new TypeRenderer())); + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(false, RepoCols.externalId));// visible if managed + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(RepoCols.externalRef)); + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(RepoCols.displayname)); + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(RepoCols.author)); + + tableModel = new RepositoryFlexiTableModel(columnsModel, getLocale()); + tableEl = uifactory.addTableElement(getWindowControl(), "table", tableModel, 20, false, getTranslator(), formLayout); + tableEl.setExportEnabled(true); + tableEl.setSelectAllEnable(true); + tableEl.setMultiSelect(true); + tableEl.setAndLoadPersistedPreferences(ureq, "curriculum-element-resource-list"); + tableEl.setEmtpyTableMessageKey("search.empty"); + + generateReportButton = uifactory.addFormLink("report.question.to.course", "report.question.to.course", null, formLayout, Link.BUTTON); + } + + @Override + protected void doDispose() { + // + } + + protected void loadModel(UserRequest ureq, String searchString, String author) { + SearchRepositoryEntryParameters params = new SearchRepositoryEntryParameters(); + params.addResourceTypes(ImsQTI21Resource.TYPE_NAME); + params.setIdentity(getIdentity()); + params.setRoles(ureq.getUserSession().getRoles()); + params.setIdRefsAndTitle(searchString); + params.setAuthor(author); + + List<RepositoryEntry> entries = repositoryManager.genericANDQueryWithRolesRestriction(params, 0, -1, true); + tableModel.setObjects(entries); + tableEl.reset(true, true, true); + } + + @Override + protected void formOK(UserRequest ureq) { + // + } + + @Override + protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) { + if(generateReportButton == source) { + doReport(ureq); + } + super.formInnerEvent(ureq, source, event); + } + + private void doReport(UserRequest ureq) { + List<RepositoryEntry> entries = getSelectedEntries(); + if(entries.isEmpty()) { + showWarning("warning.at.least.one.test"); + } else { + String filename = "Questions_" + Formatter.formatDatetimeWithMinutes(ureq.getRequestTimestamp()); + filename = StringHelper.transformDisplayNameToFileSystemName(filename); + MediaResource report = new QuestionOriginMediaResource(filename, entries, getTranslator()); + ureq.getDispatchResult().setResultingMediaResource(report); + } + } + + private List<RepositoryEntry> getSelectedEntries() { + Set<Integer> selectedIndexes = tableEl.getMultiSelectedIndex(); + List<RepositoryEntry> selectedEntries = new ArrayList<>(selectedIndexes.size()); + for(Integer selectedIndex:selectedIndexes) { + RepositoryEntry entry = tableModel.getObject(selectedIndex.intValue()); + selectedEntries.add(entry); + } + return selectedEntries; + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/report/SearchEvent.java b/src/main/java/org/olat/ims/qti21/ui/report/SearchEvent.java new file mode 100644 index 00000000000..3aec5474a63 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/report/SearchEvent.java @@ -0,0 +1,34 @@ +package org.olat.ims.qti21.ui.report; + +import org.olat.core.gui.control.Event; + +/** + * + * Initial date: 11 juin 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class SearchEvent extends Event { + + private static final long serialVersionUID = -6345403305741867680L; + + public static final String SEARCH_EVENT = "search-question-test"; + + private final String author; + private final String searchString; + + public SearchEvent(String searchString, String author) { + super(SEARCH_EVENT); + this.author = author; + this.searchString = searchString; + } + + public String getAuthor() { + return author; + } + + public String getSearchString() { + return searchString; + } + +} diff --git a/src/main/java/org/olat/ims/qti21/ui/report/_content/report_list.html b/src/main/java/org/olat/ims/qti21/ui/report/_content/report_list.html new file mode 100644 index 00000000000..e402cfc553f --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/report/_content/report_list.html @@ -0,0 +1,6 @@ +$r.render("table") +<div class="o_button_group"> +#if($r.available("report.question.to.course")) + $r.render("report.question.to.course") +#end +</div> \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/ui/report/_content/reports.html b/src/main/java/org/olat/ims/qti21/ui/report/_content/reports.html new file mode 100644 index 00000000000..8709ff7e577 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/report/_content/reports.html @@ -0,0 +1,3 @@ +<div class="o_info">$r.translate("report.explain")</div> +$r.render("search") +$r.render("table") \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/ui/report/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/ui/report/_i18n/LocalStrings_de.properties new file mode 100644 index 00000000000..e9aab99109e --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/report/_i18n/LocalStrings_de.properties @@ -0,0 +1,28 @@ +#Wed Dec 19 17:16:29 CET 2018 +admin.menu.report.question.title=Fragen zu Test +admin.menu.report.question.title.alt=Fragen zu Test +report.course.displayname=Titel Kurs +report.course.externalref=Kennzeichen Kurs +report.course.id=Kurs ID +report.explain=Report alle Fragen in gew\u00E4hlten Test +report.question.title=Titel der Frage +report.question.author=Ersteller der Frage (Besitzer) +report.question.identifier=Frage ID +report.question.keywords=Schlagwort +report.question.taxonomy.path=Fachbereichpfad +report.question.topic=Thema +report.question.context=Stufe +report.question.type=Typ +report.question.master.identifier=Frage Master ID +report.question.master.author=Ersteller der Frage (Besitzer MasterID) +report.question.master.keywords=Schlagwort (Frage MasterID) +report.question.to.course=Report Fragen +report.test.id=Test ID (Lernressource) +report.test.displayname=Titel der Lernressource Test +report.test.externalref=Test Kennzeichen +report.test.author=Ersteller Test (Besitzer) +search=Suchen +search.empty=Es wurden keine Test gefunden die Ihren Kriterien entsprechen. +search.text=Titel / Kennzeichen / ID +search.author=Autor / Besitzer +warning.at.least.one.test=Sie m\u00FCssen mindestens ein Test w\u00E4hlen. diff --git a/src/main/java/org/olat/ims/qti21/ui/report/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti21/ui/report/_i18n/LocalStrings_en.properties new file mode 100644 index 00000000000..6ec9dd87536 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/report/_i18n/LocalStrings_en.properties @@ -0,0 +1,31 @@ +#Thu Dec 07 19:13:19 CET 2017 +admin.menu.report.question.title=Questions to tests +admin.menu.report.question.title.alt=Questions to tests +atleastone.test=Sie m\u00FCssen mindestens ein Test w\u00E4hlen. +report.course.displayname=Course title +report.course.externalref=Course reference +report.course.id=Course ID +report.explain=Report all questions in the selected tests. +report.question.title=Question's title +report.question.author=Question's creator (owner) +report.question.identifier=Question ID +report.question.keywords=Keywords +report.question.taxonomy.path=Subject path +report.question.topic=Topic +report.question.context=Levele +report.question.type=Type +report.question.master.identifier=Question master ID +report.question.master.author=Creator (owner master ID) +report.question.master.keywords=Keywords (question master ID) +report.question.to.course=Report questions +report.test.id=Test ID (learn resource) +report.test.displayname=Title of the learn resource test +report.test.externalref=Test reference +report.test.author=Test creator (owner) +search=Search +search.empty=No tests found that matches the given criteria. +search.text=Title / Ext. Ref. / ID +search.author=Author / owner +warning.at.least.one.meeting=You must select at least one test. + + diff --git a/src/main/java/org/olat/repository/manager/RepositoryEntryQueries.java b/src/main/java/org/olat/repository/manager/RepositoryEntryQueries.java index fbd6171d2f2..8278f3b9335 100644 --- a/src/main/java/org/olat/repository/manager/RepositoryEntryQueries.java +++ b/src/main/java/org/olat/repository/manager/RepositoryEntryQueries.java @@ -123,6 +123,28 @@ public class RepositoryEntryQueries { query.append(" and "); PersistenceHelper.appendFuzzyLike(query, "v.description", "desc", dbInstance.getDbVendor()); } + + //quick search + Long quickId = null; + String quickRefs = null; + String quickText = null; + if(StringHelper.containsNonWhitespace(params.getIdRefsAndTitle())) { + quickRefs = params.getIdRefsAndTitle(); + query.append(" and (v.externalId=:quickRef or "); + PersistenceHelper.appendFuzzyLike(query, "v.externalRef", "quickText", dbInstance.getDbVendor()); + query.append(" or v.softkey=:quickRef or "); + quickText = PersistenceHelper.makeFuzzyQueryString(quickRefs); + PersistenceHelper.appendFuzzyLike(query, "v.displayname", "quickText", dbInstance.getDbVendor()); + if(StringHelper.isLong(quickRefs)) { + try { + quickId = Long.parseLong(quickRefs); + query.append(" or v.key=:quickVKey or res.resId=:quickVKey"); + } catch (NumberFormatException e) { + // + } + } + query.append(")"); + } if (resourceTypes != null && !resourceTypes.isEmpty()) { query.append(" and res.resName in (:resourcetypes)"); @@ -166,6 +188,15 @@ public class RepositoryEntryQueries { if (StringHelper.containsNonWhitespace(desc)) { dbQuery.setParameter("desc", desc); } + if(quickId != null) { + dbQuery.setParameter("quickVKey", quickId); + } + if(quickRefs != null) { + dbQuery.setParameter("quickRef", quickRefs); + } + if(quickText != null) { + dbQuery.setParameter("quickText", quickText); + } if (resourceTypes != null && !resourceTypes.isEmpty()) { dbQuery.setParameter("resourcetypes", resourceTypes); } diff --git a/src/main/java/org/olat/repository/model/SearchRepositoryEntryParameters.java b/src/main/java/org/olat/repository/model/SearchRepositoryEntryParameters.java index 632463e8194..f6050baaada 100644 --- a/src/main/java/org/olat/repository/model/SearchRepositoryEntryParameters.java +++ b/src/main/java/org/olat/repository/model/SearchRepositoryEntryParameters.java @@ -39,6 +39,7 @@ public class SearchRepositoryEntryParameters { private String displayName; private String author; private String desc; + private String idRefsAndTitle; private List<String> resourceTypes; private Identity identity; private Roles roles; @@ -77,6 +78,14 @@ public class SearchRepositoryEntryParameters { this.roles = roles; } + public String getIdRefsAndTitle() { + return idRefsAndTitle; + } + + public void setIdRefsAndTitle(String idRefsAndTitle) { + this.idRefsAndTitle = idRefsAndTitle; + } + public String getDisplayName() { return displayName; } -- GitLab