From dc8c68796aa69ea9d8212c517cb0ce58c24aad17 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Tue, 14 Feb 2017 15:06:52 +0100
Subject: [PATCH] OO-2531: implement print and download raw dat for QTI 2.1 in
 statistics views, refine list of users seen by admin, authors, coaches in
 statistics (courses and resources) and in asessment tool

---
 .../IdentityListCourseNodeController.java     |  35 ++----
 .../olat/course/nodes/IQTESTCourseNode.java   |  23 ++--
 .../course/run/CourseRuntimeController.java   |   2 +-
 .../userview/UserCourseEnvironmentImpl.java   |  22 ----
 .../StatisticCourseNodesController.java       |  29 +++--
 .../org/olat/ims/qti/export/QTIArchiver.java  |   7 +-
 .../ui/QTI12StatisticsToolController.java     |   8 +-
 .../qti21/manager/AssessmentResponseDAO.java  |  90 ++++++++++-----
 .../manager/QTI21StatisticsManagerImpl.java   |  16 ++-
 .../manager/archive/QTI21ArchiveFormat.java   |  81 +++++++-------
 .../model/QTI21StatisticSearchParams.java     |  49 +++++++++
 .../qti21/ui/QTI21ResetToolController.java    |   5 +-
 .../ims/qti21/ui/QTI21RuntimeController.java  |   2 +-
 .../ui/QTI21RuntimeStatisticsController.java  |  24 ++--
 ...I21AssessmentItemStatisticsController.java |   5 +-
 ...I21AssessmentTestStatisticsController.java |  85 ++++++++++++--
 .../ui/statistics/QTI21PrintController.java   | 104 ++++++++++++++++++
 .../QTI21StatisticResourceResult.java         |  25 +++--
 .../statistics/QTI21StatisticsResource.java   |  58 ++++++++++
 .../QTI21StatisticsToolController.java        |  24 +++-
 .../qti21/ui/statistics/_content/print.html   |  14 +++
 .../_content/statistics_assessment_test.html  |   2 +-
 .../_i18n/LocalStrings_de.properties          |   1 +
 .../_i18n/LocalStrings_en.properties          |   1 +
 .../_i18n/LocalStrings_fr.properties          |   1 +
 .../_i18n/LocalStrings_it.properties          |   1 +
 .../_i18n/LocalStrings_pt_BR.properties       |   1 +
 .../assessment/AssessmentToolOptions.java     |  48 ++++----
 .../manager/RepositoryEntryRelationDAO.java   |   1 -
 .../document/file/FileTypeDetector.java       |   2 +-
 30 files changed, 554 insertions(+), 212 deletions(-)
 create mode 100644 src/main/java/org/olat/ims/qti21/ui/statistics/QTI21PrintController.java
 create mode 100644 src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticsResource.java
 create mode 100644 src/main/java/org/olat/ims/qti21/ui/statistics/_content/print.html

diff --git a/src/main/java/org/olat/course/assessment/ui/tool/IdentityListCourseNodeController.java b/src/main/java/org/olat/course/assessment/ui/tool/IdentityListCourseNodeController.java
index 6a7d93d4b49..f9c11de8f08 100644
--- a/src/main/java/org/olat/course/assessment/ui/tool/IdentityListCourseNodeController.java
+++ b/src/main/java/org/olat/course/assessment/ui/tool/IdentityListCourseNodeController.java
@@ -358,8 +358,12 @@ public class IdentityListCourseNodeController extends FormBasicController implem
 			AssessmentToolOptions options = new AssessmentToolOptions();
 			options.setAdmin(assessmentCallback.isAdmin());
 			if(group == null) {
-				options.setIdentities(assessedIdentities);
-				fillAlternativeToAssessableIdentityList(options);
+				if(assessmentCallback.isAdmin()) {
+					options.setNonMembers(params.isNonMembers());
+				} else {
+					options.setIdentities(assessedIdentities);
+					fillAlternativeToAssessableIdentityList(options, params);
+				}
 			} else {
 				options.setGroup(group);
 			}
@@ -383,29 +387,7 @@ public class IdentityListCourseNodeController extends FormBasicController implem
 		flc.contextPut("toolCmpNames", toolCmpNames);
 	}
 	
-	/*
-	private boolean accept(AssessmentEntry entry, SearchAssessedIdentityParams params) {
-		boolean ok = true;
-		
-		if(params.isPassed() && (entry == null || entry.getPassed() == null || !entry.getPassed().booleanValue())) {
-			ok &= false;
-		}
-		
-		if(params.isFailed() && (entry == null || entry.getPassed() == null || entry.getPassed().booleanValue())) {
-			ok &= false;
-		}
-		
-		if(params.getAssessmentStatus() != null && params.getAssessmentStatus().size() > 0) {
-			if(entry == null || entry.getAssessmentStatus() == null) {
-				ok &= false;
-			} else {
-				ok &= !params.getAssessmentStatus().contains(entry.getAssessmentStatus());
-			}
-		}
-		return ok;
-	}*/
-	
-	private void fillAlternativeToAssessableIdentityList(AssessmentToolOptions options) {
+	private void fillAlternativeToAssessableIdentityList(AssessmentToolOptions options, SearchAssessedIdentityParams params) {
 		List<Group> baseGroups = new ArrayList<>();
 		if((assessmentCallback.canAssessRepositoryEntryMembers()
 				&& (assessmentCallback.getCoachedGroups() == null || assessmentCallback.getCoachedGroups().isEmpty()))
@@ -417,7 +399,8 @@ public class IdentityListCourseNodeController extends FormBasicController implem
 				baseGroups.add(coachedGroup.getBaseGroup());
 			}
 		}
-		options.setAlternativeToIdentities(baseGroups, assessmentCallback.canAssessNonMembers());
+		options.setGroups(baseGroups);
+		options.setNonMembers(params.isNonMembers());
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java b/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java
index 220633aebe0..f9f1c651549 100644
--- a/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java
+++ b/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java
@@ -162,7 +162,7 @@ public class IQTESTCourseNode extends AbstractAccessableCourseNode implements Pe
 		Roles roles = ureq.getUserSession().getRoles();
 		Translator trans = Util.createPackageTranslator(IQTESTCourseNode.class, ureq.getLocale());
 		if (roles.isGuestOnly()) {
-			if(isGuestAllowedForQTI21()) {
+			if(isGuestAllowedForQTI21(getReferencedRepositoryEntry())) {
 				controller = new QTI21AssessmentRunController(ureq, wControl, userCourseEnv, this);
 			} else {
 				String title = trans.translate("guestnoaccess.title");
@@ -200,8 +200,7 @@ public class IQTESTCourseNode extends AbstractAccessableCourseNode implements Pe
 		return new NodeRunConstructionResult(ctrl);
 	}
 	
-	private boolean isGuestAllowedForQTI21() {
-		RepositoryEntry testEntry = getReferencedRepositoryEntry();
+	private boolean isGuestAllowedForQTI21(RepositoryEntry testEntry) {
 		OLATResource ores = testEntry.getOlatResource();
 		if(ImsQTI21Resource.TYPE_NAME.equals(ores.getResourceableTypeName())) {
 			QTI21DeliveryOptions options = CoreSpringFactory.getImpl(QTI21Service.class).getDeliveryOptions(testEntry);
@@ -291,10 +290,11 @@ public class IQTESTCourseNode extends AbstractAccessableCourseNode implements Pe
 		if(ImsQTI21Resource.TYPE_NAME.equals(qtiTestEntry.getOlatResource().getResourceableTypeName())) {
 			RepositoryEntry courseEntry = userCourseEnv.getCourseEnvironment().getCourseGroupManager().getCourseEntry();
 			QTI21StatisticSearchParams searchParams = new QTI21StatisticSearchParams(qtiTestEntry, courseEntry, getIdent());
-			QTI21DeliveryOptions deliveryOptions = CoreSpringFactory.getImpl(QTI21Service.class)
-					.getDeliveryOptions(qtiTestEntry);
 			boolean admin = userCourseEnv.isAdmin();
-			QTI21StatisticsSecurityCallback secCallback = new QTI21StatisticsSecurityCallback(admin, admin && deliveryOptions.isAllowAnonym());
+			if(options.getParticipantsGroups() != null) {
+				searchParams.setLimitToGroups(options.getParticipantsGroups());
+			}
+			QTI21StatisticsSecurityCallback secCallback = new QTI21StatisticsSecurityCallback(admin, admin && isGuestAllowedForQTI21(qtiTestEntry));
 			return new QTI21StatisticResourceResult(qtiTestEntry, courseEntry, this, searchParams, secCallback);
 		}
 		
@@ -637,7 +637,6 @@ public class IQTESTCourseNode extends AbstractAccessableCourseNode implements Pe
 
 		// 1) prepare result export
 		CourseEnvironment courseEnv = course.getCourseEnvironment();
-		List<Identity> identities = ScoreAccountingHelper.loadUsers(courseEnv, options);
 		//create SyntheticUserRequest with UserSession to avoid Nullpointer in AssessmentResultController
 		UserRequest ureq = new SyntheticUserRequest(new TransientIdentity(), locale, new UserSession());
 		Roles roles = new Roles(false, false, false, false, false, false, false);
@@ -656,14 +655,18 @@ public class IQTESTCourseNode extends AbstractAccessableCourseNode implements Pe
 			} else if(ImsQTI21Resource.TYPE_NAME.equals(re.getOlatResource().getResourceableTypeName())) {
 				// 2a) create export resource
 				QTI21Service qtiService = CoreSpringFactory.getImpl(QTI21Service.class);
+
+				List<Identity> identities = ScoreAccountingHelper.loadUsers(courseEnv, options);
 				new QTI21ResultsExportMediaResource(courseEnv, identities, this, qtiService, ureq, locale).exportTestResults(exportStream);
-				// excel results				
-				QTI21ArchiveFormat qaf = new QTI21ArchiveFormat(locale, true, true, true);
+				// excel results
 				RepositoryEntry courseEntry = course.getCourseEnvironment().getCourseGroupManager().getCourseEntry();
-				qaf.export(courseEntry, getIdent(), re, exportStream);
+				QTI21StatisticSearchParams searchParams = new QTI21StatisticSearchParams(options, re, courseEntry, getIdent());
+				QTI21ArchiveFormat qaf = new QTI21ArchiveFormat(locale, searchParams);
+				qaf.exportCourseElement(exportStream);
 				return true;	
 			} else {
 				// 2b) create export resource
+				List<Identity> identities = ScoreAccountingHelper.loadUsers(courseEnv, options);
 				new QTI12ResultsExportMediaResource(courseEnv, locale, identities, this).exportTestResults(exportStream);
 				// excel results
 				String shortTitle = getShortTitle();
diff --git a/src/main/java/org/olat/course/run/CourseRuntimeController.java b/src/main/java/org/olat/course/run/CourseRuntimeController.java
index 01a5aabfe9f..501c0288b12 100644
--- a/src/main/java/org/olat/course/run/CourseRuntimeController.java
+++ b/src/main/java/org/olat/course/run/CourseRuntimeController.java
@@ -1432,7 +1432,7 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im
 		if (reSecurity.isEntryAdmin() || reSecurity.isCourseCoach() || reSecurity.isGroupCoach() || hasCourseRight(CourseRights.RIGHT_STATISTICS)) {
 			removeCustomCSS();
 			UserCourseEnvironmentImpl uce = getUserCourseEnvironment();
-			StatisticCourseNodesController ctrl = new StatisticCourseNodesController(ureq, swControl, toolbarPanel, uce, types);
+			StatisticCourseNodesController ctrl = new StatisticCourseNodesController(ureq, swControl, toolbarPanel,  reSecurity, uce, types);
 			listenTo(ctrl);
 			statsToolCtr = pushController(ureq, translate(i18nCrumbKey), ctrl);
 			currentToolCtr = statsToolCtr;
diff --git a/src/main/java/org/olat/course/run/userview/UserCourseEnvironmentImpl.java b/src/main/java/org/olat/course/run/userview/UserCourseEnvironmentImpl.java
index e88fbaddeb1..71d93674f52 100644
--- a/src/main/java/org/olat/course/run/userview/UserCourseEnvironmentImpl.java
+++ b/src/main/java/org/olat/course/run/userview/UserCourseEnvironmentImpl.java
@@ -25,11 +25,9 @@
 
 package org.olat.course.run.userview;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
-import org.olat.basesecurity.Group;
 import org.olat.core.CoreSpringFactory;
 import org.olat.core.commons.persistence.PersistenceHelper;
 import org.olat.core.gui.control.WindowControl;
@@ -233,26 +231,6 @@ public class UserCourseEnvironmentImpl implements UserCourseEnvironment {
 		return courseRepoEntry;
 	}
 	
-	public List<Group> getCoachedBaseGroups(boolean withRepo, boolean withBusinessGroups) {
-		List<Group> groups;
-		if(isCoach()) {
-			boolean repoCoach = false;
-			groups = new ArrayList<Group>();
-			if(withBusinessGroups && sizeCoachedGroups() > 0) {
-				for(BusinessGroup businessGroup: getCoachedGroups()) {
-					groups.add(businessGroup.getBaseGroup());
-				}
-			}
-			
-			if(withRepo && repoCoach) {
-				//TODO groups 
-			}
-		} else {
-			groups = Collections.emptyList();
-		}
-		return groups;
-	}
-	
 	public int sizeCoachedGroups() {
 		return coachedGroups == null ? 0 : coachedGroups.size();
 	}
diff --git a/src/main/java/org/olat/course/statistic/StatisticCourseNodesController.java b/src/main/java/org/olat/course/statistic/StatisticCourseNodesController.java
index 7c92ed6a472..e580f83eec8 100644
--- a/src/main/java/org/olat/course/statistic/StatisticCourseNodesController.java
+++ b/src/main/java/org/olat/course/statistic/StatisticCourseNodesController.java
@@ -51,7 +51,11 @@ import org.olat.course.ICourse;
 import org.olat.course.nodes.CourseNode;
 import org.olat.course.run.userview.UserCourseEnvironment;
 import org.olat.course.run.userview.UserCourseEnvironmentImpl;
+import org.olat.group.BusinessGroup;
 import org.olat.ims.qti.statistics.QTIType;
+import org.olat.repository.RepositoryService;
+import org.olat.repository.model.RepositoryEntrySecurity;
+import org.springframework.beans.factory.annotation.Autowired;
 
 /**
  * 
@@ -67,22 +71,31 @@ public class StatisticCourseNodesController extends BasicController implements A
 	private final QTIType[] types;
 	private final StatisticResourceOption options;
 	
+	@Autowired
+	private RepositoryService repositoryService;
+	
 	public StatisticCourseNodesController(UserRequest ureq, WindowControl wControl, TooledStackedPanel stackPanel,
-			UserCourseEnvironment userCourseEnv, QTIType ... types) {
+			RepositoryEntrySecurity reSecurity, UserCourseEnvironment userCourseEnv, QTIType ... types) {
 		super(ureq, wControl);
 
 		this.types = types;
 		this.stackPanel = stackPanel;
 		options = new StatisticResourceOption();
-		
-		boolean admin = userCourseEnv.isAdmin();
-		boolean coach = userCourseEnv.isCoach();
-		if(coach && !admin) {
+
+		if(!reSecurity.isEntryAdmin() && !reSecurity.isOwner()) {
+			List<Group> groups = new ArrayList<>();
 			UserCourseEnvironmentImpl userCourseEnvImpl = (UserCourseEnvironmentImpl)userCourseEnv;
-			List<Group> coachedGroups = userCourseEnvImpl.getCoachedBaseGroups(true, true);
-			if(coachedGroups == null || coachedGroups.isEmpty()) {
-				options.setParticipantsGroups(coachedGroups);
+			if(reSecurity.isCourseCoach()) {
+				Group bGroup = repositoryService.getDefaultGroup(userCourseEnv.getCourseEnvironment().getCourseGroupManager().getCourseEntry());
+				groups.add(bGroup);
+			}
+			if(reSecurity.isGroupCoach()) {
+				List<BusinessGroup> businessGroups = userCourseEnvImpl.getCoachedGroups();
+				for(BusinessGroup businessGroup:businessGroups) {
+					groups.add(businessGroup.getBaseGroup());
+				}
 			}
+			options.setParticipantsGroups(groups);
 		}
 
 		courseTree = new MenuTree("assessmentStatisticsTree");
diff --git a/src/main/java/org/olat/ims/qti/export/QTIArchiver.java b/src/main/java/org/olat/ims/qti/export/QTIArchiver.java
index 5ada2fa7b8b..0d3c20272ea 100644
--- a/src/main/java/org/olat/ims/qti/export/QTIArchiver.java
+++ b/src/main/java/org/olat/ims/qti/export/QTIArchiver.java
@@ -64,6 +64,7 @@ import org.olat.ims.qti.editor.beecom.parser.ItemParser;
 import org.olat.ims.qti.export.helper.QTIItemObject;
 import org.olat.ims.qti.export.helper.QTIObjectTreeBuilder;
 import org.olat.ims.qti21.manager.archive.QTI21ArchiveFormat;
+import org.olat.ims.qti21.model.QTI21StatisticSearchParams;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.handlers.RepositoryHandler;
 import org.olat.repository.handlers.RepositoryHandlerFactory;
@@ -223,7 +224,8 @@ public class QTIArchiver {
 	    } else if(ImsQTI21Resource.TYPE_NAME.equals(testRe.getOlatResource().getResourceableTypeName())) {
 	    	type = Type.qti21;
 	    	RepositoryEntry courseEntry = course.getCourseEnvironment().getCourseGroupManager().getCourseEntry();
-	    	success = new QTI21ArchiveFormat(locale, participants, allUsers, anonymUsers).hasResults(courseEntry, courseNode.getIdent(), testRe);
+	    	QTI21StatisticSearchParams searchParams = new QTI21StatisticSearchParams(testRe, courseEntry, courseNode.getIdent(), allUsers, anonymUsers);
+	    	success = new QTI21ArchiveFormat(locale, searchParams).hasResults();
 	    } else {
 	    	type = Type.qti12;
 			success = qrm.hasResultSets(courseOres.getResourceableId(), courseNode.getIdent(), testRe.getKey());
@@ -268,7 +270,8 @@ public class QTIArchiver {
 		ICourse course = CourseFactory.loadCourse(courseOres);
 		RepositoryEntry testRe = courseNode.getReferencedRepositoryEntry();
     	RepositoryEntry courseEntry = course.getCourseEnvironment().getCourseGroupManager().getCourseEntry();
-		return (new QTI21ArchiveFormat(locale, participants, allUsers, anonymUsers)).export(courseEntry, courseNode.getIdent(), testRe);
+    	QTI21StatisticSearchParams searchParams = new QTI21StatisticSearchParams(testRe, courseEntry, courseNode.getIdent(), allUsers, anonymUsers);
+		return new QTI21ArchiveFormat(locale, searchParams).exportCourseElement();
 	}
 	
 	public MediaResource exportQTI12() throws IOException {
diff --git a/src/main/java/org/olat/ims/qti/statistics/ui/QTI12StatisticsToolController.java b/src/main/java/org/olat/ims/qti/statistics/ui/QTI12StatisticsToolController.java
index bd6d42ec216..b4a8ab9cfe8 100644
--- a/src/main/java/org/olat/ims/qti/statistics/ui/QTI12StatisticsToolController.java
+++ b/src/main/java/org/olat/ims/qti/statistics/ui/QTI12StatisticsToolController.java
@@ -51,7 +51,6 @@ import org.olat.course.statistic.StatisticResourceNode;
 import org.olat.ims.qti.statistics.QTIStatisticResourceResult;
 import org.olat.ims.qti.statistics.QTIStatisticSearchParams;
 import org.olat.modules.assessment.AssessmentToolOptions;
-import org.olat.modules.assessment.AssessmentToolOptions.AlternativeToIdentities;
 import org.olat.repository.RepositoryEntry;
 import org.olat.resource.OLATResource;
 
@@ -90,10 +89,9 @@ public class QTI12StatisticsToolController extends BasicController implements Ac
 		if(asOptions.getGroup() != null) {
 			List<Group> bGroups = Collections.singletonList(asOptions.getGroup().getBaseGroup());
 			searchParams.setLimitToGroups(bGroups);
-		} else if(asOptions.getAlternativeToIdentities() != null) {
-			AlternativeToIdentities alt = asOptions.getAlternativeToIdentities();
-			searchParams.setMayViewAllUsersAssessments(alt.isMayViewAllUsersAssessments());
-			searchParams.setLimitToGroups(alt.getGroups());
+		} else if(asOptions.getGroups() != null) {
+			searchParams.setMayViewAllUsersAssessments(asOptions.isNonMembers());
+			searchParams.setLimitToGroups(asOptions.getGroups());
 		}
 		
 		statsButton = LinkFactory.createButton("menu.title", null, this);
diff --git a/src/main/java/org/olat/ims/qti21/manager/AssessmentResponseDAO.java b/src/main/java/org/olat/ims/qti21/manager/AssessmentResponseDAO.java
index 63fd36db8c7..5a1248fa6f1 100644
--- a/src/main/java/org/olat/ims/qti21/manager/AssessmentResponseDAO.java
+++ b/src/main/java/org/olat/ims/qti21/manager/AssessmentResponseDAO.java
@@ -22,6 +22,7 @@ package org.olat.ims.qti21.manager;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import javax.persistence.TypedQuery;
 
@@ -31,9 +32,9 @@ import org.olat.core.util.StringHelper;
 import org.olat.ims.qti21.AssessmentItemSession;
 import org.olat.ims.qti21.AssessmentResponse;
 import org.olat.ims.qti21.AssessmentTestSession;
+import org.olat.ims.qti21.model.QTI21StatisticSearchParams;
 import org.olat.ims.qti21.model.ResponseLegality;
 import org.olat.ims.qti21.model.jpa.AssessmentResponseImpl;
-import org.olat.repository.RepositoryEntryRef;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -95,8 +96,7 @@ public class AssessmentResponseDAO {
 	 * @param testEntry
 	 * @return
 	 */
-	public boolean hasResponses(RepositoryEntryRef courseEntry, String subIdent, RepositoryEntryRef testEntry,
-			boolean participant, boolean users, boolean anonymUsers) {
+	public boolean hasResponses(QTI21StatisticSearchParams searchParams) {
 		StringBuilder sb = new StringBuilder();
 		sb.append("select response.key from qtiassessmentresponse response ")
 		  .append(" inner join response.assessmentItemSession itemSession")
@@ -104,35 +104,44 @@ public class AssessmentResponseDAO {
 		  .append(" where testSession.repositoryEntry.key=:repoEntryKey")
 		  .append("  and testSession.testEntry.key=:testEntryKey")
 		  .append("  and testSession.subIdent=:subIdent")
-		  .append("  and testSession.finishTime is not null")
+		  .append("  and testSession.finishTime is not null and testSession.authorMode=false")
 		  .append("  and (");
-		if(users) {
+
+		if(searchParams.isViewAllUsers()) {
 			sb.append(" testSession.identity.key is not null");
-		} else if(participant) {
+		} else if(searchParams.getLimitToGroups() != null) {
+			sb.append(" testSession.identity.key in (select membership.identity.key from  bgroupmember as membership, repoentrytogroup as rel")
+			  .append("   where rel.entry.key=:repoEntryKey and rel.group.key=membership.group.key and rel.group.key in (:limitGroupKeys)")
+			  .append("   and membership.role='").append(GroupRoles.participant.name()).append("'")
+			  .append(" )");
+		} else if(searchParams.getLimitToIdentities() != null) {
+			sb.append(" testSession.identity.key in (select membership.identity.key from  bgroupmember as membership, repoentrytogroup as rel")
+			  .append("   where rel.entry.key=:repoEntryKey and rel.group.key=membership.group.key and membership.identity.key in (:limitIdentityKeys)")
+			  .append("   and membership.role='").append(GroupRoles.participant.name()).append("'")
+			  .append(" )");
+		} else {
 			sb.append(" testSession.identity.key in (select membership.identity.key from  bgroupmember as membership, repoentrytogroup as rel")
 			  .append("   where rel.entry.key=:repoEntryKey and rel.group.key=membership.group.key ")
 			  .append("   and membership.role='").append(GroupRoles.participant.name()).append("'")
 			  .append(" )");
 		}
-		if(anonymUsers) {
-			if(participant || users) sb.append(" or ");
-			sb.append(" testSession.anonymousIdentifier is not null");
+		if(searchParams.isViewAnonymUsers()) {
+			sb.append(" or testSession.anonymousIdentifier is not null");
 		}
-		sb.append("))");
+		sb.append(")");
 		
 		List<Long> responses = dbInstance.getCurrentEntityManager()
 				.createQuery(sb.toString(), Long.class)
-				.setParameter("repoEntryKey", courseEntry.getKey())
-				.setParameter("testEntryKey", testEntry.getKey())
-				.setParameter("subIdent", subIdent)
+				.setParameter("repoEntryKey", searchParams.getCourseEntry().getKey())
+				.setParameter("testEntryKey", searchParams.getTestEntry().getKey())
+				.setParameter("subIdent", searchParams.getNodeIdent())
 				.setFirstResult(0)
 				.setMaxResults(1)
 				.getResultList();
 		return responses.size() > 0 && responses.get(0) != null;
 	}
 	
-	public List<AssessmentResponse> getResponse(RepositoryEntryRef courseEntry, String subIdent, RepositoryEntryRef testEntry,
-			boolean participant, boolean users, boolean anonymUsers) {
+	public List<AssessmentResponse> getResponse(QTI21StatisticSearchParams searchParams) {
 		StringBuilder sb = new StringBuilder();
 		sb.append("select response from qtiassessmentresponse response ")
 		  .append(" inner join fetch response.assessmentItemSession itemSession")
@@ -141,26 +150,37 @@ public class AssessmentResponseDAO {
 		  .append(" left join assessmentEntry.identity as ident")
 		  .append(" left join ident.user as usr")
 		  .append(" where testSession.testEntry.key=:testEntryKey")
-		  .append("  and testSession.finishTime is not null");
-		if(courseEntry != null) {
+		  .append("  and testSession.finishTime is not null and testSession.authorMode=false");
+		if(searchParams.getCourseEntry() != null) {
 			sb.append(" and testSession.repositoryEntry.key=:repoEntryKey");
 		}
-		if(StringHelper.containsNonWhitespace(subIdent)) {
+		if(StringHelper.containsNonWhitespace(searchParams.getNodeIdent())) {
 			sb.append(" and testSession.subIdent=:subIdent");
 		}
-		
 		sb.append(" and (");
-		if(users) {
+		
+		if(searchParams.getLimitToGroups() != null) {
+			sb.append(" testSession.identity.key in (select membership.identity.key from  bgroupmember as membership, repoentrytogroup as rel")
+			  .append("   where rel.entry.key=:repoEntryKey and rel.group.key=membership.group.key and rel.group.key in (:limitGroupKeys)");
+			if(!searchParams.isViewAllUsers()) {
+				sb.append(" and membership.role='").append(GroupRoles.participant.name()).append("'");
+			}
+			sb.append(" )");
+		} else if(searchParams.getLimitToIdentities() != null) {
+			sb.append(" testSession.identity.key in (select membership.identity.key from  bgroupmember as membership, repoentrytogroup as rel")
+			  .append("   where rel.entry.key=:repoEntryKey and rel.group.key=membership.group.key and membership.identity.key in (:limitIdentityKeys)")
+			  .append("   and membership.role='").append(GroupRoles.participant.name()).append("'")
+			  .append(" )");
+		} else if(searchParams.isViewAllUsers()) {
 			sb.append(" testSession.identity.key is not null");
-		} else if(participant) {
+		} else {
 			sb.append(" testSession.identity.key in (select membership.identity.key from  bgroupmember as membership, repoentrytogroup as rel")
 			  .append("   where rel.entry.key=:repoEntryKey and rel.group.key=membership.group.key ")
 			  .append("   and membership.role='").append(GroupRoles.participant.name()).append("'")
 			  .append(" )");
 		}
-		if(anonymUsers) {
-			if(participant || users) sb.append(" or ");
-			sb.append(" testSession.anonymousIdentifier is not null");
+		if(searchParams.isViewAnonymUsers()) {
+			sb.append(" or testSession.anonymousIdentifier is not null");
 		}
 		sb.append(")");
 
@@ -169,12 +189,24 @@ public class AssessmentResponseDAO {
 		
 		TypedQuery<AssessmentResponse> query = dbInstance.getCurrentEntityManager()
 				.createQuery(sb.toString(), AssessmentResponse.class)
-				.setParameter("testEntryKey", testEntry.getKey());
-		if(courseEntry != null) {
-			query.setParameter("repoEntryKey", courseEntry.getKey());
+				.setParameter("testEntryKey", searchParams.getTestEntry().getKey());
+		if(searchParams.getCourseEntry() != null) {
+			query.setParameter("repoEntryKey", searchParams.getCourseEntry().getKey());
+		} else {
+			query.setParameter("repoEntryKey", searchParams.getTestEntry().getKey());
+		}
+		if(StringHelper.containsNonWhitespace(searchParams.getNodeIdent())) {
+			query.setParameter("subIdent", searchParams.getNodeIdent());
+		}
+		if(searchParams.getLimitToGroups() != null && searchParams.getLimitToGroups().size() > 0) {
+			List<Long> keys = searchParams.getLimitToGroups().stream()
+					.map(group -> group.getKey()).collect(Collectors.toList());
+			query.setParameter("limitGroupKeys", keys);
 		}
-		if(StringHelper.containsNonWhitespace(subIdent)) {
-			query.setParameter("subIdent", subIdent);
+		if(searchParams.getLimitToIdentities() != null && searchParams.getLimitToIdentities().size() > 0) {
+			List<Long> keys = searchParams.getLimitToIdentities().stream()
+					.map(group -> group.getKey()).collect(Collectors.toList());
+			query.setParameter("limitIdentityKeys", keys);
 		}
 		return query.getResultList();
 	}
diff --git a/src/main/java/org/olat/ims/qti21/manager/QTI21StatisticsManagerImpl.java b/src/main/java/org/olat/ims/qti21/manager/QTI21StatisticsManagerImpl.java
index 82ea2eaca94..239b9606b34 100644
--- a/src/main/java/org/olat/ims/qti21/manager/QTI21StatisticsManagerImpl.java
+++ b/src/main/java/org/olat/ims/qti21/manager/QTI21StatisticsManagerImpl.java
@@ -116,13 +116,21 @@ public class QTI21StatisticsManagerImpl implements QTI21StatisticsManager {
 			  .append(" )");
 		} else if(searchParams.getLimitToGroups() != null && searchParams.getLimitToGroups().size() > 0) {
 			sb.append(" and asession.identity.key in ( select membership.identity.key from bgroupmember membership")
-			  .append("   where membership.group in (:baseGroups)")
+			  .append("   where membership.group in (:baseGroups) and membership.role='").append(GroupRole.participant).append("'")
 			  .append(" )");
 		} else {
 			//limit to participants
-			sb.append(" and asession.identity.key in ( select membership.identity.key from repoentrytogroup as rel, bgroup as reBaseGroup, bgroupmember membership ")
-			  .append("   where rel.entry.key=:repositoryEntryKey and rel.group.key=reBaseGroup.key and membership.group.key=reBaseGroup.key and membership.role='").append(GroupRole.participant).append("'")
-			  .append(" )");
+			sb.append(" and (asession.identity.key in ( select membership.identity.key from repoentrytogroup as rel, bgroupmember membership ")
+			  .append("   where rel.entry.key=:repositoryEntryKey and rel.group.key=membership.group.key and membership.role='").append(GroupRole.participant).append("'")
+			  //.append("   where rel.entry.key=:repositoryEntryKey and rel.group.key=reBaseGroup.key and membership.group.key=reBaseGroup.key and membership.role='").append(GroupRole.participant).append("'")
+			   .append(" )");
+			// add non members
+			if(searchParams.isViewNonMembers()) {
+				sb.append(" or asession.identity.key not in (select membership.identity.key from repoentrytogroup as rel, bgroupmember as membership")
+			      .append("    where rel.entry.key=:repositoryEntryKey and rel.group.key=membership.group.key")
+			      .append(" )");
+			}
+			sb.append(")");
 		}
 
 		return sb;
diff --git a/src/main/java/org/olat/ims/qti21/manager/archive/QTI21ArchiveFormat.java b/src/main/java/org/olat/ims/qti21/manager/archive/QTI21ArchiveFormat.java
index 33b668a1ee3..4c279200707 100644
--- a/src/main/java/org/olat/ims/qti21/manager/archive/QTI21ArchiveFormat.java
+++ b/src/main/java/org/olat/ims/qti21/manager/archive/QTI21ArchiveFormat.java
@@ -32,7 +32,6 @@ import java.util.Map;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
-import org.apache.commons.io.IOUtils;
 import org.olat.core.CoreSpringFactory;
 import org.olat.core.gui.media.MediaResource;
 import org.olat.core.gui.translator.Translator;
@@ -83,6 +82,7 @@ import org.olat.ims.qti21.manager.archive.interactions.PositionObjectInteraction
 import org.olat.ims.qti21.manager.archive.interactions.SelectPointInteractionArchive;
 import org.olat.ims.qti21.manager.archive.interactions.SliderInteractionArchive;
 import org.olat.ims.qti21.manager.archive.interactions.TextEntryInteractionArchive;
+import org.olat.ims.qti21.model.QTI21StatisticSearchParams;
 import org.olat.ims.qti21.ui.QTI21RuntimeController;
 import org.olat.modules.assessment.AssessmentEntry;
 import org.olat.repository.RepositoryEntry;
@@ -133,9 +133,7 @@ public class QTI21ArchiveFormat {
 	private List<UserPropertyHandler> userPropertyHandlers;
 	private IdentityAnonymizerCallback anonymizerCallback;
 
-	private boolean participants;
-	private boolean allUsers;
-	private boolean anonymUsers;
+	private final QTI21StatisticSearchParams searchParams;
 	
 	private List<ItemInfos> itemInfos;
 	private final Map<String, InteractionArchive> interactionArchiveMap = new HashMap<>();
@@ -144,10 +142,8 @@ public class QTI21ArchiveFormat {
 	private final UserManager userManager;
 	private final AssessmentResponseDAO responseDao;
 	
-	public QTI21ArchiveFormat(Locale locale, boolean participants, boolean allUsers, boolean anonymUsers) {
-		this.participants = participants;
-		this.allUsers = allUsers;
-		this.anonymUsers = anonymUsers;
+	public QTI21ArchiveFormat(Locale locale, QTI21StatisticSearchParams searchParams) {
+		this.searchParams = searchParams;
 		
 		userManager = CoreSpringFactory.getImpl(UserManager.class);
 		qtiService = CoreSpringFactory.getImpl(QTI21ServiceImpl.class);
@@ -187,17 +183,17 @@ public class QTI21ArchiveFormat {
 		interactionArchiveMap.put(TextEntryInteraction.QTI_CLASS_NAME, new TextEntryInteractionArchive());					//ok
 	}
 	
-	public boolean hasResults(RepositoryEntry courseEntry, String subIdent, RepositoryEntry testEntry) {
-		return responseDao.hasResponses(courseEntry, subIdent, testEntry, participants, allUsers, anonymUsers);
+	public boolean hasResults() {
+		return responseDao.hasResponses(searchParams);
 	}
 
-	public void export(RepositoryEntry courseEntry, String subIdent, RepositoryEntry testEntry, ZipOutputStream exportStream) {
-		FileResourceManager frm = FileResourceManager.getInstance();
-		File unzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource());
-		resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(unzippedDirRoot, false, false);
-		
-		ICourse course = CourseFactory.loadCourse(courseEntry);
-		CourseNode courseNode = course.getRunStructure().getNode(subIdent);
+	/**
+	 * 
+	 * @param exportStream
+	 */
+	public void exportCourseElement(ZipOutputStream exportStream) {
+		ICourse course = CourseFactory.loadCourse(searchParams.getCourseEntry());
+		CourseNode courseNode = course.getRunStructure().getNode(searchParams.getNodeIdent());
 		String label = StringHelper.transformDisplayNameToFileSystemName(courseNode.getShortName())
 				+ "_" + Formatter.formatDatetimeSave(new Date())
 				+ ".xlsx";
@@ -206,49 +202,53 @@ public class QTI21ArchiveFormat {
 			anonymizerCallback = course.getCourseEnvironment().getCoursePropertyManager();
 		}
 		
-		export(courseEntry, subIdent, testEntry, label, exportStream);
+		export(label, exportStream);
 	}
 	
-	public void export(RepositoryEntry testEntry, ZipOutputStream exportStream) {
-		FileResourceManager frm = FileResourceManager.getInstance();
-		File unzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource());
-		resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(unzippedDirRoot, false, false);
-		
+	public void exportResource(ZipOutputStream exportStream) {
 		String archiveName = "qti21test_"
-				+ StringHelper.transformDisplayNameToFileSystemName(testEntry.getDisplayname())
+				+ StringHelper.transformDisplayNameToFileSystemName(searchParams.getTestEntry().getDisplayname())
 				+ "_" + Formatter.formatDatetimeFilesystemSave(new Date(System.currentTimeMillis())) + ".xlsx";
-		export(null, null, testEntry, archiveName, exportStream);
+		export(archiveName, exportStream);
 	}
 	
-	private void export(RepositoryEntry courseEntry, String subIdent, RepositoryEntry testEntry, String filename,  ZipOutputStream exportStream) {
-		//content
-		final List<AssessmentResponse> responses = responseDao.getResponse(courseEntry, subIdent, testEntry, participants, allUsers, anonymUsers);
+	private void export(String filename,  ZipOutputStream exportStream) {
 		try {
 			exportStream.putNextEntry(new ZipEntry(filename));
-			OpenXMLWorkbook workbook = new OpenXMLWorkbook(new ShieldOutputStream(exportStream), 1);
-			
+			exportWorkbook(new ShieldOutputStream(exportStream));
+			exportStream.closeEntry();
+		} catch (IOException e) {
+			log.error("", e);
+		}
+	}
+	
+	public void exportWorkbook(OutputStream exportStream) {
+		RepositoryEntry testEntry = searchParams.getTestEntry();
+		FileResourceManager frm = FileResourceManager.getInstance();
+		File unzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource());
+		resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(unzippedDirRoot, false, false);
+		
+		//content
+		List<AssessmentResponse> responses = responseDao.getResponse(searchParams);
+		try(OpenXMLWorkbook workbook = new OpenXMLWorkbook(exportStream, 1)) {
 			//headers
 			OpenXMLWorksheet exportSheet = workbook.nextWorksheet();
 			exportSheet.setHeaderRows(2);
 			writeHeaders_1(exportSheet, workbook);
 			writeHeaders_2(exportSheet, workbook);
 			writeData(responses, exportSheet, workbook);
-			
-			IOUtils.closeQuietly(workbook);
-
-			exportStream.closeEntry();
-		} catch (IOException e) {
+		} catch(Exception e) {
 			log.error("", e);
 		}
 	}
 	
-	public MediaResource export(RepositoryEntry courseEntry, String subIdent, RepositoryEntry testEntry) {
+	public MediaResource exportCourseElement() {
 		FileResourceManager frm = FileResourceManager.getInstance();
-		File unzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource());
+		File unzippedDirRoot = frm.unzipFileResource(searchParams.getTestEntry().getOlatResource());
 		resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(unzippedDirRoot, false, false);
 		
-		ICourse course = CourseFactory.loadCourse(courseEntry);
-		CourseNode courseNode = course.getRunStructure().getNode(subIdent);
+		ICourse course = CourseFactory.loadCourse(searchParams.getCourseEntry());
+		CourseNode courseNode = course.getRunStructure().getNode(searchParams.getNodeIdent());
 		String label = courseNode.getType() + "_"
 				+ StringHelper.transformDisplayNameToFileSystemName(courseNode.getShortName())
 				+ "_" + Formatter.formatDatetimeFilesystemSave(new Date(System.currentTimeMillis()))
@@ -259,8 +259,7 @@ public class QTI21ArchiveFormat {
 		}
 		
 		//content
-		final List<AssessmentResponse> responses = responseDao.getResponse(courseEntry, subIdent, testEntry, participants, allUsers, anonymUsers);
-
+		final List<AssessmentResponse> responses = responseDao.getResponse(searchParams);
 		return new OpenXMLWorkbookResource(label) {
 			@Override
 			protected void generate(OutputStream out) {
diff --git a/src/main/java/org/olat/ims/qti21/model/QTI21StatisticSearchParams.java b/src/main/java/org/olat/ims/qti21/model/QTI21StatisticSearchParams.java
index 67f1b7e0756..248832aef4f 100644
--- a/src/main/java/org/olat/ims/qti21/model/QTI21StatisticSearchParams.java
+++ b/src/main/java/org/olat/ims/qti21/model/QTI21StatisticSearchParams.java
@@ -19,9 +19,12 @@
  */
 package org.olat.ims.qti21.model;
 
+import java.util.ArrayList;
 import java.util.List;
 
 import org.olat.basesecurity.Group;
+import org.olat.core.id.Identity;
+import org.olat.course.nodes.ArchiveOptions;
 import org.olat.repository.RepositoryEntry;
 
 /**
@@ -37,9 +40,30 @@ public class QTI21StatisticSearchParams {
 	private final RepositoryEntry testEntry;
 
 	private List<Group> limitToGroups;
+	private List<Identity> limitToIdentities;
 	
 	private boolean viewAnonymUsers;
 	private boolean viewAllUsers;
+	private boolean viewNonMembers;
+	
+	public QTI21StatisticSearchParams(ArchiveOptions options, RepositoryEntry testEntry, RepositoryEntry courseEntry, String nodeIdent) {
+		this.testEntry = testEntry;
+		this.courseEntry = courseEntry;
+		this.nodeIdent = nodeIdent;
+		
+		if(options == null) {
+			viewAnonymUsers = true;
+			viewAllUsers = true;
+		} else if(options.getGroup() != null) {
+			limitToGroups = new ArrayList<>(2);
+			limitToGroups.add(options.getGroup().getBaseGroup());
+		} else if(options.getIdentities() != null) {
+			limitToIdentities = new ArrayList<>(options.getIdentities());
+		} else {
+			viewAnonymUsers = true;
+			viewAllUsers = true;
+		}
+	}
 	
 	public QTI21StatisticSearchParams(RepositoryEntry testEntry, RepositoryEntry courseEntry, String nodeIdent) {
 		this.nodeIdent = nodeIdent;
@@ -47,6 +71,15 @@ public class QTI21StatisticSearchParams {
 		this.testEntry = testEntry;
 	}
 	
+	public QTI21StatisticSearchParams(RepositoryEntry testEntry, RepositoryEntry courseEntry, String nodeIdent,
+			boolean viewAllUsers, boolean viewAnonymUsers) {
+		this.nodeIdent = nodeIdent;
+		this.courseEntry = courseEntry;
+		this.testEntry = testEntry;
+		this.viewAllUsers = viewAllUsers;
+		this.viewAnonymUsers = viewAnonymUsers;
+	}
+	
 	public RepositoryEntry getTestEntry() {
 		return testEntry;
 	}
@@ -67,6 +100,14 @@ public class QTI21StatisticSearchParams {
 		this.limitToGroups = limitToGroups;
 	}
 	
+	public List<Identity> getLimitToIdentities() {
+		return limitToIdentities;
+	}
+
+	public void setLimitToIdentities(List<Identity> limitToIdentities) {
+		this.limitToIdentities = limitToIdentities;
+	}
+	
 	public boolean isViewAllUsers() {
 		return viewAllUsers;
 	}
@@ -75,6 +116,14 @@ public class QTI21StatisticSearchParams {
 		this.viewAllUsers = view;
 	}
 
+	public boolean isViewNonMembers() {
+		return viewNonMembers;
+	}
+
+	public void setViewNonMembers(boolean viewNonMembers) {
+		this.viewNonMembers = viewNonMembers;
+	}
+
 	public boolean isViewAnonymUsers() {
 		return viewAnonymUsers;
 	}
diff --git a/src/main/java/org/olat/ims/qti21/ui/QTI21ResetToolController.java b/src/main/java/org/olat/ims/qti21/ui/QTI21ResetToolController.java
index bfd68fe6336..be1439bb4c5 100644
--- a/src/main/java/org/olat/ims/qti21/ui/QTI21ResetToolController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/QTI21ResetToolController.java
@@ -59,6 +59,7 @@ import org.olat.course.run.userview.UserCourseEnvironmentImpl;
 import org.olat.group.BusinessGroupService;
 import org.olat.ims.qti21.QTI21Service;
 import org.olat.ims.qti21.manager.archive.QTI21ArchiveFormat;
+import org.olat.ims.qti21.model.QTI21StatisticSearchParams;
 import org.olat.modules.assessment.AssessmentToolOptions;
 import org.olat.repository.RepositoryEntry;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -218,7 +219,9 @@ public class QTI21ResetToolController extends BasicController {
 		
 		try(FileOutputStream fileStream = new FileOutputStream(exportFile);
 			ZipOutputStream exportStream = new ZipOutputStream(fileStream)) {
-			new QTI21ArchiveFormat(getLocale(), true, true, true).export(testEntry, exportStream);
+			//author can do this, also they can archive all users and anonyme users
+	    	QTI21StatisticSearchParams searchParams = new QTI21StatisticSearchParams(testEntry, null, null, true, true);
+			new QTI21ArchiveFormat(getLocale(), searchParams).exportResource(exportStream);
 		} catch (IOException e) {
 			logError("", e);
 		}
diff --git a/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeController.java b/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeController.java
index 3464607d7fd..b62a69bc088 100644
--- a/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeController.java
@@ -190,7 +190,7 @@ public class QTI21RuntimeController extends RepositoryEntryRuntimeController  {
 		if (reSecurity.isEntryAdmin() || reSecurity.isCourseCoach() || reSecurity.isGroupCoach()) {
 			AssessmentToolOptions asOptions = new AssessmentToolOptions();
 			asOptions.setAdmin(reSecurity.isEntryAdmin());
-			QTI21RuntimeStatisticsController ctrl = new QTI21RuntimeStatisticsController(ureq, swControl,
+			QTI21RuntimeStatisticsController ctrl = new QTI21RuntimeStatisticsController(ureq, swControl, toolbarPanel,
 					getRepositoryEntry(), asOptions);
 			listenTo(ctrl);
 
diff --git a/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeStatisticsController.java b/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeStatisticsController.java
index 6151fecb71f..5bd3236f7ff 100644
--- a/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeStatisticsController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/QTI21RuntimeStatisticsController.java
@@ -27,6 +27,8 @@ import org.olat.core.commons.fullWebApp.LayoutMain3ColsController;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.panel.Panel;
+import org.olat.core.gui.components.stack.TooledController;
+import org.olat.core.gui.components.stack.TooledStackedPanel;
 import org.olat.core.gui.components.tree.MenuTree;
 import org.olat.core.gui.components.tree.TreeEvent;
 import org.olat.core.gui.components.tree.TreeModel;
@@ -46,7 +48,6 @@ import org.olat.ims.qti21.model.QTI21StatisticSearchParams;
 import org.olat.ims.qti21.ui.statistics.QTI21StatisticResourceResult;
 import org.olat.ims.qti21.ui.statistics.QTI21StatisticsSecurityCallback;
 import org.olat.modules.assessment.AssessmentToolOptions;
-import org.olat.modules.assessment.AssessmentToolOptions.AlternativeToIdentities;
 import org.olat.repository.RepositoryEntry;
 import org.springframework.beans.factory.annotation.Autowired;
 
@@ -56,9 +57,11 @@ import org.springframework.beans.factory.annotation.Autowired;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class QTI21RuntimeStatisticsController extends BasicController implements Activateable2 {
+public class QTI21RuntimeStatisticsController extends BasicController implements Activateable2, TooledController {
 
 	private MenuTree courseTree;
+	private TooledStackedPanel stackPanel;
+	
 	private Controller currentCtrl;
 	private LayoutMain3ColsController layoutCtr;
 
@@ -70,9 +73,10 @@ public class QTI21RuntimeStatisticsController extends BasicController implements
 	@Autowired
 	private QTI21Service qtiService;
 	
-	public QTI21RuntimeStatisticsController(UserRequest ureq, WindowControl wControl, 
+	public QTI21RuntimeStatisticsController(UserRequest ureq, WindowControl wControl, TooledStackedPanel stackPanel,
 			RepositoryEntry testEntry, AssessmentToolOptions asOptions) {
 		super(ureq, wControl);
+		this.stackPanel = stackPanel;
 		options = new ArchiveOptions();
 		options.setGroup(asOptions.getGroup());
 		options.setIdentities(asOptions.getIdentities());
@@ -81,9 +85,8 @@ public class QTI21RuntimeStatisticsController extends BasicController implements
 		if(asOptions.getGroup() != null) {
 			List<Group> bGroups = Collections.singletonList(asOptions.getGroup().getBaseGroup());
 			searchParams.setLimitToGroups(bGroups);
-		} else if(asOptions.getAlternativeToIdentities() != null) {
-			AlternativeToIdentities alt = asOptions.getAlternativeToIdentities();
-			searchParams.setLimitToGroups(alt.getGroups());
+		} else if(asOptions.getGroups() != null) {
+			searchParams.setLimitToGroups(asOptions.getGroups());
 		}
 		
 		QTI21DeliveryOptions deliveryOptions = qtiService.getDeliveryOptions(testEntry);
@@ -102,6 +105,13 @@ public class QTI21RuntimeStatisticsController extends BasicController implements
 		TreeNode rootNode = courseTree.getTreeModel().getRootNode();
 		doSelectNode(ureq, rootNode);
 	}
+	
+	@Override
+	public void initTools() {
+		if(currentCtrl instanceof TooledController) {
+			((TooledController)currentCtrl).initTools();
+		}
+	}
 
 	@Override
 	protected void doDispose() {
@@ -141,7 +151,7 @@ public class QTI21RuntimeStatisticsController extends BasicController implements
 	private void doSelectNode(UserRequest ureq, TreeNode selectedNode) {
 		removeAsListenerAndDispose(currentCtrl);
 		WindowControl swControl = addToHistory(ureq, OresHelper.createOLATResourceableInstance(selectedNode.getIdent(), 0l), null);
-		currentCtrl = resourceResult.getController(ureq, swControl, selectedNode, false);
+		currentCtrl = resourceResult.getController(ureq, swControl, stackPanel, selectedNode, false);
 		if(currentCtrl != null) {
 			listenTo(currentCtrl);
 			layoutCtr.setCol3(currentCtrl.getInitialComponent());
diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21AssessmentItemStatisticsController.java b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21AssessmentItemStatisticsController.java
index 1e346ff971e..b5a0f06564b 100644
--- a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21AssessmentItemStatisticsController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21AssessmentItemStatisticsController.java
@@ -80,7 +80,8 @@ public class QTI21AssessmentItemStatisticsController extends BasicController {
 	private QTI21StatisticsManager qtiStatisticsManager;
 	
 	public QTI21AssessmentItemStatisticsController(UserRequest ureq, WindowControl wControl,
-			AssessmentItemRef itemRef, AssessmentItem item, String sectionTitle, QTI21StatisticResourceResult resourceResult, boolean printMode) {
+			AssessmentItemRef itemRef, AssessmentItem item, String sectionTitle, QTI21StatisticResourceResult resourceResult,
+			boolean withFilter, boolean printMode) {
 		super(ureq, wControl);
 		
 		this.item = item;
@@ -103,7 +104,7 @@ public class QTI21AssessmentItemStatisticsController extends BasicController {
 			mainVC.contextPut("itemCss", "o_mi_qtiunkown");
 		}
 		
-		if(resourceResult.canViewAnonymousUsers() || resourceResult.canViewNonParticipantUsers()) {
+		if(withFilter && (resourceResult.canViewAnonymousUsers() || resourceResult.canViewNonParticipantUsers())) {
 			filterCtrl = new UserFilterController(ureq, getWindowControl(),
 					resourceResult.canViewNonParticipantUsers(), resourceResult.canViewAnonymousUsers(),
 					resourceResult.isViewNonParticipantUsers(), resourceResult.isViewAnonymousUsers());
diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21AssessmentTestStatisticsController.java b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21AssessmentTestStatisticsController.java
index 1d7c7190e28..3f61d84f964 100644
--- a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21AssessmentTestStatisticsController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21AssessmentTestStatisticsController.java
@@ -23,20 +23,33 @@ import static org.olat.ims.qti.statistics.ui.StatisticFormatter.duration;
 import static org.olat.ims.qti.statistics.ui.StatisticFormatter.format;
 import static org.olat.ims.qti.statistics.ui.StatisticFormatter.getModeString;
 
+import java.util.Date;
 import java.util.List;
 
+import org.olat.core.commons.fullWebApp.popup.BaseFullWebappPopupLayoutFactory;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.chart.BarSeries;
 import org.olat.core.gui.components.chart.StatisticsComponent;
+import org.olat.core.gui.components.link.Link;
+import org.olat.core.gui.components.link.LinkFactory;
+import org.olat.core.gui.components.link.LinkPopupSettings;
+import org.olat.core.gui.components.stack.TooledController;
+import org.olat.core.gui.components.stack.TooledStackedPanel;
+import org.olat.core.gui.components.stack.TooledStackedPanel.Align;
 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;
+import org.olat.core.gui.control.creator.ControllerCreator;
 import org.olat.core.gui.control.generic.dtabs.Activateable2;
+import org.olat.core.gui.media.MediaResource;
 import org.olat.core.id.context.ContextEntry;
 import org.olat.core.id.context.StateEntry;
+import org.olat.core.util.CodeHelper;
+import org.olat.core.util.Formatter;
+import org.olat.core.util.StringHelper;
 import org.olat.course.nodes.QTICourseNode;
 import org.olat.course.nodes.iq.IQEditController;
 import org.olat.ims.qti.statistics.QTIType;
@@ -49,9 +62,11 @@ import org.olat.modules.assessment.ui.event.UserFilterEvent;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class QTI21AssessmentTestStatisticsController extends BasicController implements Activateable2 {
-	
+public class QTI21AssessmentTestStatisticsController extends BasicController implements Activateable2, TooledController {
+
 	private final VelocityContainer mainVC;
+	private final TooledStackedPanel stackPanel;
+	private final Link printLink, downloadRawLink;
 	
 	private UserFilterController filterCtrl;
 	
@@ -59,9 +74,10 @@ public class QTI21AssessmentTestStatisticsController extends BasicController imp
 	private QTICourseNode courseNode;
 	private final QTI21StatisticResourceResult resourceResult;
 
-	public QTI21AssessmentTestStatisticsController(UserRequest ureq, WindowControl wControl,
-			QTI21StatisticResourceResult resourceResult, boolean printMode) {
+	public QTI21AssessmentTestStatisticsController(UserRequest ureq, WindowControl wControl, TooledStackedPanel stackPanel,
+			QTI21StatisticResourceResult resourceResult, boolean withFilter, boolean printMode) {
 		super(ureq, wControl);
+		this.stackPanel = stackPanel;
 		this.resourceResult = resourceResult;
 		courseNode = resourceResult.getTestCourseNode();
 		type = resourceResult.getType();
@@ -73,8 +89,24 @@ public class QTI21AssessmentTestStatisticsController extends BasicController imp
 			mainVC.contextPut("courseId", resourceResult.getCourseEntry().getKey());
 		}
 		mainVC.contextPut("testId", resourceResult.getTestEntry().getKey());
+		if(stackPanel != null) {
+			printLink = LinkFactory.createToolLink("print" + CodeHelper.getRAMUniqueID(), translate("print"), this);
+			printLink.setIconLeftCSS("o_icon o_icon_print o_icon-lg");
+			printLink.setPopup(new LinkPopupSettings(680, 500, "qti-stats"));
+			stackPanel.addTool(printLink, Align.right);
+
+			downloadRawLink = LinkFactory.createToolLink("download" + CodeHelper.getRAMUniqueID(), translate("download.raw.data"), this);
+			stackPanel.addTool(downloadRawLink, Align.right);
+		} else {
+			printLink = null;
+			downloadRawLink = LinkFactory.createLink("download.raw.data", mainVC, this);
+			downloadRawLink.setCustomEnabledLinkCSS("o_content_download");
+			mainVC.put("download", downloadRawLink);
+		}
+		downloadRawLink.setIconLeftCSS("o_icon o_icon_download o_icon-lg");
+
 		
-		if(resourceResult.canViewAnonymousUsers() || resourceResult.canViewNonParticipantUsers()) {
+		if(withFilter && (resourceResult.canViewAnonymousUsers() || resourceResult.canViewNonParticipantUsers())) {
 			filterCtrl = new UserFilterController(ureq, getWindowControl(),
 					resourceResult.canViewNonParticipantUsers(), resourceResult.canViewAnonymousUsers(),
 					resourceResult.isViewNonParticipantUsers(), resourceResult.isViewAnonymousUsers());
@@ -88,9 +120,20 @@ public class QTI21AssessmentTestStatisticsController extends BasicController imp
 	
 	@Override
 	protected void doDispose() {
-		//
+		if(stackPanel != null) {
+			stackPanel.removeTool(downloadRawLink);
+			stackPanel.removeTool(printLink);
+		}
 	}
 	
+	@Override
+	public void initTools() {
+		if(stackPanel != null) {
+			stackPanel.addTool(printLink, Align.right);
+			stackPanel.addTool(downloadRawLink, Align.right);
+		}
+	}
+
 	private void updateData() {
 		StatisticAssessment stats = resourceResult.getQTIStatisticAssessment();
 		initScoreHistogram(stats);
@@ -189,6 +232,34 @@ public class QTI21AssessmentTestStatisticsController extends BasicController imp
 
 	@Override
 	protected void event(UserRequest ureq, Component source, Event event) {
-		//
+		if(printLink == source) {
+			printPages(ureq);
+		} else if(downloadRawLink == source) {
+			doDownloadRawData(ureq);
+		}
+	}
+	
+	private void printPages(UserRequest ureq) {
+		ControllerCreator printControllerCreator = new ControllerCreator() {
+			@Override
+			public Controller createController(UserRequest lureq, WindowControl lwControl) {
+				return new QTI21PrintController(lureq, lwControl, resourceResult);
+			}					
+		};
+		ControllerCreator layoutCtrlr = BaseFullWebappPopupLayoutFactory.createPrintPopupLayout(printControllerCreator);
+		openInNewBrowserWindow(ureq, layoutCtrlr);
+	}
+	
+	private void doDownloadRawData(UserRequest ureq) {
+		String label;
+		if(courseNode == null) {
+			label = StringHelper.transformDisplayNameToFileSystemName(resourceResult.getTestEntry().getDisplayname());
+		} else {
+			label = courseNode.getType() + "_"
+					+ StringHelper.transformDisplayNameToFileSystemName(courseNode.getShortName());
+		}
+		label += "_" + Formatter.formatDatetimeFilesystemSave(new Date()) + ".xlsx";
+		MediaResource resource = new QTI21StatisticsResource(resourceResult, label, getLocale());
+		ureq.getDispatchResult().setResultingMediaResource(resource);
 	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21PrintController.java b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21PrintController.java
new file mode 100644
index 00000000000..8847e50edc9
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21PrintController.java
@@ -0,0 +1,104 @@
+/**
+ * <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.statistics;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.panel.MainPanel;
+import org.olat.core.gui.components.tree.TreeNode;
+import org.olat.core.gui.components.velocity.VelocityContainer;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.control.controller.BasicController;
+import org.olat.core.util.nodes.INode;
+import org.olat.course.CourseFactory;
+import org.olat.course.ICourse;
+import org.olat.course.statistic.StatisticResourceNode;
+
+/**
+ * 
+ * Initial date: 13 févr. 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class QTI21PrintController extends BasicController {
+
+	private final VelocityContainer mainVC;
+	
+	public QTI21PrintController(UserRequest ureq, WindowControl wControl, QTI21StatisticResourceResult resourceResult) {
+		super(ureq, wControl);
+		
+		mainVC = createVelocityContainer("print");
+		initView(ureq, resourceResult);
+		
+		MainPanel mainPanel = new MainPanel("statsPrintPanel");
+		mainPanel.setContent(mainVC);
+		mainPanel.setCssClass("o_qti_print");
+		putInitialPanel(mainPanel);
+	}
+	
+	private void initView(UserRequest ureq, QTI21StatisticResourceResult resourceResult) {
+		StatisticResourceNode rootNode = (StatisticResourceNode)resourceResult.getSubTreeModel().getRootNode();
+		
+		if(resourceResult.getCourseEntry() != null) {
+			ICourse course = CourseFactory.loadCourse(resourceResult.getCourseEntry());
+			mainVC.contextPut("courseTitle", course.getCourseTitle());
+		}
+		String testTitle = resourceResult.getTestEntry().getDisplayname();
+		mainVC.contextPut("testTitle", testTitle);
+		
+		int count = 0;
+		List<String> pageNames = new ArrayList<>();
+
+		Controller assessmentCtrl = resourceResult.getController(ureq, getWindowControl(), null, rootNode, true);
+		
+		String pageName = "page" + count++;
+		mainVC.put(pageName, assessmentCtrl.getInitialComponent());
+		pageNames.add(pageName);
+
+		for(int i=0; i<rootNode.getChildCount(); i++) {
+			INode sectionNode = rootNode.getChildAt(i);
+			for(int j=0; j<sectionNode.getChildCount(); j++) {
+				TreeNode itemNode = (TreeNode)sectionNode.getChildAt(j);
+				Controller itemCtrl = resourceResult.getController(ureq, getWindowControl(), null, itemNode, true);
+				
+				String itemPageName = "page" + count++;
+				mainVC.put(itemPageName, itemCtrl.getInitialComponent());
+				pageNames.add(itemPageName);
+			}
+		}
+
+		mainVC.contextPut("pageNames", pageNames);
+	}
+	
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+	@Override
+	protected void event(UserRequest ureq, Component source, Event event) {
+		//
+	}
+}
diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticResourceResult.java b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticResourceResult.java
index 4599cc5293f..4842d9cdef1 100644
--- a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticResourceResult.java
+++ b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticResourceResult.java
@@ -77,6 +77,8 @@ public class QTI21StatisticResourceResult implements StatisticResourceResult {
 	
 	private final QTI21Service qtiService;
 	private final QTI21StatisticsManager qtiStatisticsManager;
+	
+	private boolean withFilter = true;
 
 	public QTI21StatisticResourceResult(RepositoryEntry testEntry, QTI21StatisticSearchParams searchParams,
 			QTI21StatisticsSecurityCallback secCallback) {
@@ -159,6 +161,14 @@ public class QTI21StatisticResourceResult implements StatisticResourceResult {
 		searchParams.setViewAllUsers(view);
 	}
 	
+	public boolean isWithFilter() {
+		return withFilter;
+	}
+
+	public void setWithFilter(boolean withFilter) {
+		this.withFilter = withFilter;
+	}
+
 	/**
 	 * Return the tree model for a test learn resource.
 	 * 
@@ -212,7 +222,6 @@ public class QTI21StatisticResourceResult implements StatisticResourceResult {
 					firstItem = itemNode;
 				}
 			}
-			rootTreeNode.setDelegate(firstItem);
 		} else {
 			int counter = 0;
 			for(TestPart part:parts) {
@@ -288,13 +297,13 @@ public class QTI21StatisticResourceResult implements StatisticResourceResult {
 
 	@Override
 	public Controller getController(UserRequest ureq, WindowControl wControl, TooledStackedPanel stackPanel, TreeNode selectedNode) {
-		return getController(ureq, wControl, selectedNode, false);
+		return getController(ureq, wControl, stackPanel, selectedNode, false);
 	}
 	
-	public Controller getController(UserRequest ureq, WindowControl wControl,
+	public Controller getController(UserRequest ureq, WindowControl wControl, TooledStackedPanel stackPanel,
 			TreeNode selectedNode, boolean printMode) {	
 		if(selectedNode instanceof StatisticResourceNode) {
-			return createAssessmentController(ureq, wControl, printMode);
+			return createAssessmentController(ureq, wControl, stackPanel, printMode);
 		} else {
 			Object uobject = selectedNode.getUserObject();
 			
@@ -304,14 +313,14 @@ public class QTI21StatisticResourceResult implements StatisticResourceResult {
 				return createAssessmentItemController(ureq, wControl,
 						(AssessmentItemRef)uobject, sectionTitle, printMode);
 			} else if(uobject instanceof AssessmentTest) {
-				return createAssessmentController(ureq, wControl, printMode);
+				return createAssessmentController(ureq, wControl, stackPanel, printMode);
 			}
 		}
 		return null;
 	}
 	
-	private Controller createAssessmentController(UserRequest ureq, WindowControl wControl, boolean printMode) {
-		Controller ctrl = new QTI21AssessmentTestStatisticsController(ureq, wControl, this, printMode);
+	private Controller createAssessmentController(UserRequest ureq, WindowControl wControl, TooledStackedPanel stackPanel, boolean printMode) {
+		Controller ctrl = new QTI21AssessmentTestStatisticsController(ureq, wControl, stackPanel, this, withFilter, printMode);
 		if(courseNode != null) {
 			CourseNodeConfiguration cnConfig = CourseNodeFactory.getInstance()
 					.getCourseNodeConfigurationEvenForDisabledBB(courseNode.getType());
@@ -325,7 +334,7 @@ public class QTI21StatisticResourceResult implements StatisticResourceResult {
 			AssessmentItemRef assessmentItemRef, String sectionTitle, boolean printMode) {
 		ResolvedAssessmentItem resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(assessmentItemRef);
 		AssessmentItem assessmentItem = resolvedAssessmentItem.getItemLookup().getRootNodeHolder().getRootNode();
-		Controller ctrl = new QTI21AssessmentItemStatisticsController(ureq, wControl, assessmentItemRef, assessmentItem, sectionTitle, this, printMode);
+		Controller ctrl = new QTI21AssessmentItemStatisticsController(ureq, wControl, assessmentItemRef, assessmentItem, sectionTitle, this, withFilter, printMode);
 		String iconCssClass = "o_mi_qtisc";
 		if(courseNode != null) {
 			ctrl = TitledWrapperHelper.getWrapper(ureq, wControl, ctrl, courseNode, iconCssClass);
diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticsResource.java b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticsResource.java
new file mode 100644
index 00000000000..82278a035fb
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticsResource.java
@@ -0,0 +1,58 @@
+/**
+ * <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.statistics;
+
+import java.io.OutputStream;
+import java.util.Locale;
+
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.openxml.OpenXMLWorkbookResource;
+import org.olat.ims.qti21.manager.archive.QTI21ArchiveFormat;
+
+/**
+ * 
+ * Initial date: 13 févr. 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class QTI21StatisticsResource extends OpenXMLWorkbookResource {
+	
+	private static final OLog log = Tracing.createLoggerFor(QTI21StatisticsResource.class);
+	
+	private final Locale locale;
+	private final QTI21StatisticResourceResult resourceResult;
+	
+	public QTI21StatisticsResource(QTI21StatisticResourceResult resourceResult, String label, Locale locale) {
+		super(label);
+		this.locale = locale;
+		this.resourceResult = resourceResult;
+	}
+
+	@Override
+	protected void generate(OutputStream out) {
+		try {
+			new QTI21ArchiveFormat(locale, resourceResult.getSearchParams())
+				.exportWorkbook(out);
+		} catch (Exception e) {
+			log.error("", e);
+		}
+	}
+}
diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticsToolController.java b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticsToolController.java
index 29709d026e3..e6e8deedc55 100644
--- a/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticsToolController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/statistics/QTI21StatisticsToolController.java
@@ -52,7 +52,6 @@ import org.olat.ims.qti21.QTI21DeliveryOptions;
 import org.olat.ims.qti21.QTI21Service;
 import org.olat.ims.qti21.model.QTI21StatisticSearchParams;
 import org.olat.modules.assessment.AssessmentToolOptions;
-import org.olat.modules.assessment.AssessmentToolOptions.AlternativeToIdentities;
 import org.olat.repository.RepositoryEntry;
 import org.springframework.beans.factory.annotation.Autowired;
 
@@ -82,6 +81,16 @@ public class QTI21StatisticsToolController extends BasicController implements Ac
 	@Autowired
 	private QTI21Service qtiService;
 	
+	/**
+	 * This is the statistics tool for the assessment tool.
+	 * 
+	 * @param ureq
+	 * @param wControl
+	 * @param stackPanel
+	 * @param courseEnv
+	 * @param asOptions
+	 * @param courseNode
+	 */
 	public QTI21StatisticsToolController(UserRequest ureq, WindowControl wControl, 
 			TooledStackedPanel stackPanel, CourseEnvironment courseEnv,
 			AssessmentToolOptions asOptions, QTICourseNode courseNode) {
@@ -98,14 +107,16 @@ public class QTI21StatisticsToolController extends BasicController implements Ac
 		secCallback = new QTI21StatisticsSecurityCallback(asOptions.isAdmin(), asOptions.isAdmin() && deliveryOptions.isAllowAnonym());
 		
 		searchParams = new QTI21StatisticSearchParams(testEntry, courseEntry, courseNode.getIdent());
-		searchParams.setViewAnonymUsers(secCallback.canViewAnonymousUsers());
+		searchParams.setViewAnonymUsers(false);//In assessment tool, no user allowed
+		searchParams.setViewAllUsers(false);
 		
-		if(asOptions.getGroup() != null) {
+		if(asOptions.getGroup() != null) {// filter by business group
 			List<Group> bGroups = Collections.singletonList(asOptions.getGroup().getBaseGroup());
 			searchParams.setLimitToGroups(bGroups);
-		} else if(asOptions.getAlternativeToIdentities() != null) {
-			AlternativeToIdentities alt = asOptions.getAlternativeToIdentities();
-			searchParams.setLimitToGroups(alt.getGroups());
+		} else if(asOptions.getGroups() != null) {
+			searchParams.setLimitToGroups(asOptions.getGroups());
+		} else {
+			searchParams.setViewNonMembers(asOptions.isNonMembers());
 		}
 		
 		statsButton = LinkFactory.createButton("menu.title", null, this);
@@ -168,6 +179,7 @@ public class QTI21StatisticsToolController extends BasicController implements Ac
 	private void doLaunchStatistics(UserRequest ureq, WindowControl wControl) {
 		if(result == null) {
 			result = new QTI21StatisticResourceResult(testEntry, courseEntry, courseNode, searchParams, secCallback);
+			result.setWithFilter(false);
 		}
 		
 		GenericTreeModel treeModel = new GenericTreeModel();
diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/_content/print.html b/src/main/java/org/olat/ims/qti21/ui/statistics/_content/print.html
new file mode 100644
index 00000000000..ca3a74b41cf
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/ui/statistics/_content/print.html
@@ -0,0 +1,14 @@
+#if($r.isNotNull($courseTitle))
+<h3>$courseTitle</h3>
+#end
+<h3>$testTitle</h3>
+#foreach($pageName in $pageNames)
+	<div #if ($velocityCount > 1) class="o_print_break_before" #end>
+	$r.render($pageName)
+	</div>
+#end
+<script type='text/javascript'>
+/* <![CDATA[ */
+	window.print();
+/* ]]> */
+</script>
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/_content/statistics_assessment_test.html b/src/main/java/org/olat/ims/qti21/ui/statistics/_content/statistics_assessment_test.html
index 5262c5fe3ca..8fb5fd7bc5d 100644
--- a/src/main/java/org/olat/ims/qti21/ui/statistics/_content/statistics_assessment_test.html
+++ b/src/main/java/org/olat/ims/qti21/ui/statistics/_content/statistics_assessment_test.html
@@ -1,4 +1,4 @@
-#if($r.available("filter"))
+#if($r.available("filter") && !$printMode)
 <div class="o_button_group o_button_group_right">
 	$r.render("filter")
 </div>
diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_de.properties
index 7b7098754bc..23b9f93b3f2 100644
--- a/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_de.properties
@@ -13,6 +13,7 @@ chart.percent.participants.num=$org.olat.ims.qti.statistics.ui\:chart.percent.pa
 chart.points=$org.olat.ims.qti.statistics.ui\:chart.points
 chart.responses=$org.olat.ims.qti.statistics.ui\:chart.responses
 chart.score.histogramm=$org.olat.ims.qti.statistics.ui\:chart.score.histogramm
+download.raw.data=$org.olat.ims.qti.statistics.ui\:download.raw.data
 fib.wrong.answer=$org.olat.ims.qti.statistics.ui\:fib.wrong.answer
 fig.averagedur=$org.olat.ims.qti.statistics.ui\:fig.averagedur
 fig.averagescore=$org.olat.ims.qti.statistics.ui\:fig.averagescore
diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_en.properties
index 28713a5eaad..59e86626a82 100644
--- a/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_en.properties
@@ -13,6 +13,7 @@ chart.percent.participants.num=$org.olat.ims.qti.statistics.ui\:chart.percent.pa
 chart.points=$org.olat.ims.qti.statistics.ui\:chart.points
 chart.responses=$org.olat.ims.qti.statistics.ui\:chart.responses
 chart.score.histogramm=$org.olat.ims.qti.statistics.ui\:chart.score.histogramm
+download.raw.data=$org.olat.ims.qti.statistics.ui\:download.raw.data
 fib.wrong.answer=$org.olat.ims.qti.statistics.ui\:fib.wrong.answer
 fig.averagedur=$org.olat.ims.qti.statistics.ui\:fig.averagedur
 fig.averagescore=$org.olat.ims.qti.statistics.ui\:fig.averagescore
diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_fr.properties b/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_fr.properties
index 274894623d0..b257fa0cc48 100644
--- a/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_fr.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_fr.properties
@@ -13,6 +13,7 @@ chart.percent.participants.num=$org.olat.ims.qti.statistics.ui\:chart.percent.pa
 chart.points=$org.olat.ims.qti.statistics.ui\:chart.points
 chart.responses=$org.olat.ims.qti.statistics.ui\:chart.responses
 chart.score.histogramm=$org.olat.ims.qti.statistics.ui\:chart.score.histogramm
+download.raw.data=$org.olat.ims.qti.statistics.ui\:download.raw.data
 fib.wrong.answer=$org.olat.ims.qti.statistics.ui\:fib.wrong.answer
 fig.averagedur=$org.olat.ims.qti.statistics.ui\:fig.averagedur
 fig.averagescore=$org.olat.ims.qti.statistics.ui\:fig.averagescore
diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_it.properties b/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_it.properties
index b2c70ee9a17..063bf578486 100644
--- a/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_it.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_it.properties
@@ -13,6 +13,7 @@ chart.percent.participants.num=$org.olat.ims.qti.statistics.ui\:chart.percent.pa
 chart.points=$org.olat.ims.qti.statistics.ui\:chart.points
 chart.responses=$org.olat.ims.qti.statistics.ui\:chart.responses
 chart.score.histogramm=$org.olat.ims.qti.statistics.ui\:chart.score.histogramm
+download.raw.data=$org.olat.ims.qti.statistics.ui\:download.raw.data
 fib.wrong.answer=$org.olat.ims.qti.statistics.ui\:fib.wrong.answer
 fig.averagedur=$org.olat.ims.qti.statistics.ui\:fig.averagedur
 fig.averagescore=$org.olat.ims.qti.statistics.ui\:fig.averagescore
diff --git a/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_pt_BR.properties b/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_pt_BR.properties
index eaa4b5d5e2f..79adf0d28a8 100644
--- a/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_pt_BR.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/statistics/_i18n/LocalStrings_pt_BR.properties
@@ -13,6 +13,7 @@ chart.percent.participants.num=$org.olat.ims.qti.statistics.ui\:chart.percent.pa
 chart.points=$org.olat.ims.qti.statistics.ui\:chart.points
 chart.responses=$org.olat.ims.qti.statistics.ui\:chart.responses
 chart.score.histogramm=$org.olat.ims.qti.statistics.ui\:chart.score.histogramm
+download.raw.data=$org.olat.ims.qti.statistics.ui\:download.raw.data
 fib.wrong.answer=$org.olat.ims.qti.statistics.ui\:fib.wrong.answer
 fig.averagedur=$org.olat.ims.qti.statistics.ui\:fig.averagedur
 fig.averagescore=$org.olat.ims.qti.statistics.ui\:fig.averagescore
diff --git a/src/main/java/org/olat/modules/assessment/AssessmentToolOptions.java b/src/main/java/org/olat/modules/assessment/AssessmentToolOptions.java
index 3a455f0419e..9b11a2ef87b 100644
--- a/src/main/java/org/olat/modules/assessment/AssessmentToolOptions.java
+++ b/src/main/java/org/olat/modules/assessment/AssessmentToolOptions.java
@@ -35,8 +35,10 @@ public class AssessmentToolOptions {
 	
 	private boolean admin;
 	private BusinessGroup group;
+	private List<Group> groups;
 	private List<Identity> identities;
-	private AlternativeToIdentities alternativeToIdentities;
+	
+	private boolean nonMembers;
 	
 	public AssessmentToolOptions() {
 		//
@@ -50,6 +52,14 @@ public class AssessmentToolOptions {
 		this.admin = admin;
 	}
 
+	public boolean isNonMembers() {
+		return nonMembers;
+	}
+
+	public void setNonMembers(boolean nonMembers) {
+		this.nonMembers = nonMembers;
+	}
+
 	public BusinessGroup getGroup() {
 		return group;
 	}
@@ -65,35 +75,15 @@ public class AssessmentToolOptions {
 	public void setIdentities(List<Identity> identities) {
 		this.identities = identities;
 	}
-	
-	public AlternativeToIdentities getAlternativeToIdentities() {
-		return alternativeToIdentities;
-	}
-	
-	public void setAlternativeToIdentities(List<Group> groups, boolean mayViewAllUsersAssessments) {
-		alternativeToIdentities = new AlternativeToIdentities();
-		alternativeToIdentities.setGroups(groups);
-		alternativeToIdentities.setMayViewAllUsersAssessments(mayViewAllUsersAssessments);
+
+	public List<Group> getGroups() {
+		return groups;
 	}
-	
-	public static class AlternativeToIdentities {
-		private List<Group> groups;
-		private boolean mayViewAllUsersAssessments;
 
-		public List<Group> getGroups() {
-			return groups;
-		}
-		
-		public void setGroups(List<Group> groups) {
-			this.groups = groups;
-		}
-		
-		public boolean isMayViewAllUsersAssessments() {
-			return mayViewAllUsersAssessments;
-		}
-		
-		public void setMayViewAllUsersAssessments(boolean mayViewAllUsersAssessments) {
-			this.mayViewAllUsersAssessments = mayViewAllUsersAssessments;
-		}	
+	public void setGroups(List<Group> groups) {
+		this.groups = groups;
 	}
+	
+	
+	
 }
diff --git a/src/main/java/org/olat/repository/manager/RepositoryEntryRelationDAO.java b/src/main/java/org/olat/repository/manager/RepositoryEntryRelationDAO.java
index 6d7bebaca35..81def30930e 100644
--- a/src/main/java/org/olat/repository/manager/RepositoryEntryRelationDAO.java
+++ b/src/main/java/org/olat/repository/manager/RepositoryEntryRelationDAO.java
@@ -198,7 +198,6 @@ public class RepositoryEntryRelationDAO {
 
 		return dbInstance.getCurrentEntityManager().createQuery(sb.toString(), Group.class)
 				.setParameter("repoKey", re.getKey())
-				.setHint("org.hibernate.cacheable", Boolean.TRUE)
 				.getSingleResult();
 	}
 	
diff --git a/src/main/java/org/olat/search/service/document/file/FileTypeDetector.java b/src/main/java/org/olat/search/service/document/file/FileTypeDetector.java
index d30d2686cb1..65b58ccb2c2 100644
--- a/src/main/java/org/olat/search/service/document/file/FileTypeDetector.java
+++ b/src/main/java/org/olat/search/service/document/file/FileTypeDetector.java
@@ -55,7 +55,7 @@ public class FileTypeDetector {
 		if("doc".equals(suffix)) {
 			if(checkMagicBytes(leaf, ZIP)) return "docx";
 		} else if("xls".equals(suffix)) {
-			if(checkMagicBytes(leaf, ZIP)) return "xslx";
+			if(checkMagicBytes(leaf, ZIP)) return "xlsx";
 		} else if("ppt".equals(suffix)) {
 			if(checkMagicBytes(leaf, ZIP)) return "pptx";
 		}
-- 
GitLab