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