From 858edf5c4def032e6020e1468901581b599c89b1 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Fri, 19 Jan 2018 16:02:19 +0100
Subject: [PATCH] OO-3217: add archive excel for check list, add assessment
 documents to archives of course elements

---
 .../core/logging/activity/ActionObject.java   |   3 +-
 .../activity/StringResourceableType.java      |   4 +
 .../java/org/olat/course/CourseFactory.java   |   8 +-
 .../ScoreAccountingArchiveController.java     |  18 +-
 .../archiver/ScoreAccountingHelper.java       |  43 ++++
 .../archiver/_i18n/LocalStrings_de.properties |   1 +
 .../archiver/_i18n/LocalStrings_en.properties |   1 +
 .../assessment/AssessmentLoggingAction.java   |  10 +
 .../manager/CourseAssessmentManagerImpl.java  |  22 ++
 .../course/nodes/CheckListCourseNode.java     |  35 ++-
 .../org/olat/course/nodes/GTACourseNode.java  |  40 ++-
 .../org/olat/course/nodes/MSCourseNode.java   |   7 +-
 .../org/olat/course/nodes/TACourseNode.java   |  20 +-
 .../cl/ui/CheckListAssessmentController.java  |   2 +-
 .../nodes/cl/ui/CheckListExcelExport.java     | 231 ++++++++++++++++++
 .../nodes/cl/ui/CheckboxEditController.java   |  11 +-
 .../cl/ui/_i18n/LocalStrings_de.properties    |   5 +
 .../cl/ui/_i18n/LocalStrings_en.properties    |   4 +
 .../cl/ui/_i18n/LocalStrings_fr.properties    |   4 +
 .../cl/ui/_i18n/LocalStrings_it.properties    |   2 +
 .../cl/ui/_i18n/LocalStrings_pt_BR.properties |   4 +
 .../gta/ui/GroupBulkDownloadResource.java     |  14 +-
 .../course/nodes/ms/MSEditFormController.java |   7 +-
 .../course/statistic/AsyncExportManager.java  |   3 +-
 .../manager/archive/QTI21ArchiveFormat.java   |   4 +-
 .../QTI21ResultsExportMediaResource.java      |  40 ++-
 .../resultexport/_content/qtiListing.html     |  31 ++-
 .../_i18n/LocalStrings_de.properties          |   3 +-
 .../_i18n/LocalStrings_en.properties          |   1 +
 .../_i18n/LocalStrings_fr.properties          |   1 +
 .../_i18n/LocalStrings_pt_BR.properties       |   1 +
 .../ui/ParticipantListSortDelegate.java       |  19 ++
 32 files changed, 533 insertions(+), 66 deletions(-)
 create mode 100644 src/main/java/org/olat/course/nodes/cl/ui/CheckListExcelExport.java

diff --git a/src/main/java/org/olat/core/logging/activity/ActionObject.java b/src/main/java/org/olat/core/logging/activity/ActionObject.java
index d1850b0ba63..08d3d67f336 100644
--- a/src/main/java/org/olat/core/logging/activity/ActionObject.java
+++ b/src/main/java/org/olat/core/logging/activity/ActionObject.java
@@ -98,6 +98,7 @@ public enum ActionObject {
 	waitingperson,
 	bulkassessment,
 	lectures,
-	lecturesRollcall;
+	lecturesRollcall,
+	assessmentdocument;
 
 }
diff --git a/src/main/java/org/olat/core/logging/activity/StringResourceableType.java b/src/main/java/org/olat/core/logging/activity/StringResourceableType.java
index f34f873e0c9..2f282583c65 100644
--- a/src/main/java/org/olat/core/logging/activity/StringResourceableType.java
+++ b/src/main/java/org/olat/core/logging/activity/StringResourceableType.java
@@ -108,6 +108,9 @@ public enum StringResourceableType implements ILoggingResourceableType {
 
 	/** the param part of the URI during a QTI test - equivalent to what was passed to IQDisplayController.logAudit before **/
 	qtiParams,
+	
+	/** assessment document **/
+	assessmentDocument,
 
 	/** Context sensitive help **/
 	csHelp,
@@ -129,4 +132,5 @@ public enum StringResourceableType implements ILoggingResourceableType {
 
 	/** Edubase book section */
 	bookSection;
+	
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/CourseFactory.java b/src/main/java/org/olat/course/CourseFactory.java
index 8be9355f24c..8f186134620 100644
--- a/src/main/java/org/olat/course/CourseFactory.java
+++ b/src/main/java/org/olat/course/CourseFactory.java
@@ -716,9 +716,10 @@ public class CourseFactory {
 		List<Identity> users = ScoreAccountingHelper.loadUsers(course.getCourseEnvironment());
 		List<AssessableCourseNode> nodes = ScoreAccountingHelper.loadAssessableNodes(course.getCourseEnvironment());
 		
-		String fileName = ExportUtil.createFileNameWithTimeStamp(course.getCourseTitle(), "xlsx");
-		try(OutputStream out = new FileOutputStream(new File(exportDirectory, fileName))) {
-			ScoreAccountingHelper.createCourseResultsOverviewXMLTable(users, nodes, course, locale, out);
+		String fileName = ExportUtil.createFileNameWithTimeStamp(course.getCourseTitle(), "zip");
+		try(OutputStream out = new FileOutputStream(new File(exportDirectory, fileName));
+				ZipOutputStream zout = new ZipOutputStream(out)) {
+			ScoreAccountingHelper.createCourseResultsOverview(users, nodes, course, locale, zout);
 		} catch(IOException e) {
 			log.error("", e);
 		}
@@ -742,6 +743,7 @@ public class CourseFactory {
 		// rework when backgroundjob infrastructure exists
 		DBFactory.getInstance().intermediateCommit();
 		AsyncExportManager.getInstance().asyncArchiveCourseLogFiles(archiveOnBehalfOf, new Runnable() {
+			@Override
 			public void run() {
 				// that's fine, I dont need to do anything here
 			};
diff --git a/src/main/java/org/olat/course/archiver/ScoreAccountingArchiveController.java b/src/main/java/org/olat/course/archiver/ScoreAccountingArchiveController.java
index 86af1721840..7a38f0f02d9 100644
--- a/src/main/java/org/olat/course/archiver/ScoreAccountingArchiveController.java
+++ b/src/main/java/org/olat/course/archiver/ScoreAccountingArchiveController.java
@@ -30,6 +30,7 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.List;
+import java.util.zip.ZipOutputStream;
 
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
@@ -80,10 +81,7 @@ public class ScoreAccountingArchiveController extends BasicController {
 		myPanel.setContent(myContent);
 	}
 
-	/**
-	 * @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest,
-	 *      org.olat.core.gui.components.Component, org.olat.core.gui.control.Event)
-	 */
+	@Override
 	public void event(UserRequest ureq, Component source, Event event) {
 		if (source == startButton) {
 			doStartExport();
@@ -102,12 +100,13 @@ public class ScoreAccountingArchiveController extends BasicController {
 		List<AssessableCourseNode> nodes = ScoreAccountingHelper.loadAssessableNodes(course.getCourseEnvironment());
 		
 		String courseTitle = course.getCourseTitle();
-		String fileName = ExportUtil.createFileNameWithTimeStamp(courseTitle, "xlsx");
+		String fileName = ExportUtil.createFileNameWithTimeStamp(courseTitle, "zip");
 		// location for data export
 		File exportDirectory = CourseFactory.getOrCreateDataExportDirectory(getIdentity(), courseTitle);
 		File downloadFile = new File(exportDirectory, fileName);
-		try(OutputStream out=new FileOutputStream(downloadFile)) {
-			ScoreAccountingHelper.createCourseResultsOverviewXMLTable(users, nodes, course, getLocale(), out);
+		try(OutputStream fOut = new FileOutputStream(downloadFile);
+				ZipOutputStream zout = new ZipOutputStream(fOut)) {
+			ScoreAccountingHelper.createCourseResultsOverview(users, nodes, course, getLocale(), zout);
 		} catch(IOException e) {
 			logError("", e);
 		}
@@ -119,11 +118,8 @@ public class ScoreAccountingArchiveController extends BasicController {
 		myPanel.setContent(vcFeedback);
 	}
 
-	/**
-	 * @see org.olat.core.gui.control.DefaultController#doDispose(boolean)
-	 */
+	@Override
 	protected void doDispose() {
 		// nothing to dispose
 	}
-
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/archiver/ScoreAccountingHelper.java b/src/main/java/org/olat/course/archiver/ScoreAccountingHelper.java
index 70ba2d73896..9a5b432084b 100644
--- a/src/main/java/org/olat/course/archiver/ScoreAccountingHelper.java
+++ b/src/main/java/org/olat/course/archiver/ScoreAccountingHelper.java
@@ -25,6 +25,8 @@
 
 package org.olat.course.archiver;
 
+import java.io.File;
+import java.io.IOException;
 import java.io.OutputStream;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
@@ -36,6 +38,8 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 
 import org.apache.commons.io.IOUtils;
 import org.olat.basesecurity.GroupRoles;
@@ -45,8 +49,12 @@ import org.olat.core.id.Identity;
 import org.olat.core.id.IdentityEnvironment;
 import org.olat.core.id.context.BusinessControlFactory;
 import org.olat.core.id.context.ContextEntry;
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.Util;
+import org.olat.core.util.ZipUtil;
+import org.olat.core.util.io.ShieldOutputStream;
 import org.olat.core.util.openxml.OpenXMLWorkbook;
 import org.olat.core.util.openxml.OpenXMLWorksheet;
 import org.olat.core.util.openxml.OpenXMLWorksheet.Row;
@@ -58,6 +66,8 @@ import org.olat.course.groupsandrights.CourseGroupManager;
 import org.olat.course.nodes.ArchiveOptions;
 import org.olat.course.nodes.AssessableCourseNode;
 import org.olat.course.nodes.CourseNode;
+import org.olat.course.nodes.IQTESTCourseNode;
+import org.olat.course.nodes.MSCourseNode;
 import org.olat.course.nodes.STCourseNode;
 import org.olat.course.run.environment.CourseEnvironment;
 import org.olat.course.run.scoring.ScoreAccounting;
@@ -78,6 +88,39 @@ import org.olat.user.propertyhandlers.UserPropertyHandler;
  */
 public class ScoreAccountingHelper {
 	
+	private static final OLog log = Tracing.createLoggerFor(ScoreAccountingHelper.class);
+	
+	public static void createCourseResultsOverview(List<Identity> identities, List<AssessableCourseNode> nodes, ICourse course, Locale locale, ZipOutputStream zout) {
+		try(OutputStream out = new ShieldOutputStream(zout)) {
+			zout.putNextEntry(new ZipEntry("Course_results.xlsx"));
+			createCourseResultsOverviewXMLTable(identities, nodes, course, locale, out);
+			zout.closeEntry();
+		} catch(IOException e) {
+			log.error("", e);
+		}
+
+		for(AssessableCourseNode node:nodes) {
+			String dir = "Assessment_documents/" + StringHelper.transformDisplayNameToFileSystemName(node.getShortName());
+			if(node instanceof IQTESTCourseNode
+					|| node.getModuleConfiguration().getBooleanSafe(MSCourseNode.CONFIG_KEY_HAS_INDIVIDUAL_ASSESSMENT_DOCS, false)) {
+				for(Identity assessedIdentity:identities) {
+					List<File> assessmentDocuments = course.getCourseEnvironment()
+							.getAssessmentManager().getIndividualAssessmentDocuments(node, assessedIdentity);
+					if(assessmentDocuments != null && !assessmentDocuments.isEmpty()) {
+						String name = assessedIdentity.getUser().getLastName()
+								+ "_" + assessedIdentity.getUser().getFirstName()
+								+ "_" + assessedIdentity.getName();
+						String userDirName = dir + "/" + StringHelper.transformDisplayNameToFileSystemName(name);
+						for(File document:assessmentDocuments) {
+							String path = userDirName + "/" + document.getName(); 
+							ZipUtil.addFileToZip(path, document, zout);
+						}
+					}
+				}
+			}
+		}
+	}
+	
 	/**
 	 * The results from assessable nodes are written to one row per user into an excel-sheet. An
      * assessable node will only appear if it is producing at least one of the
diff --git a/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_de.properties
index ca82bf41db3..c7b3ac5e504 100644
--- a/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_de.properties
@@ -92,6 +92,7 @@ nodechoose.intro.projectbroker=W\u00E4hlen Sie einen Kursbaustein aus, um dessen
 nodechoose.intro.scorm=W\u00E4hlen Sie im folgenden Dialog den SCORM Baustein aus, der ausgewertet werden soll
 nodechoose.intro.ta=W\u00E4hlen Sie einen Kursbaustein aus, um dessen Aufgaben/L\u00F6sungen zu archivieren.
 nodechoose.intro.wiki=W\u00E4hlen Sie einen Kursbaustein aus, um dessen Wikis zu archivieren.
+nodechoose.intro.checklist=W\u00E4hlen Sie einen Kursbaustein aus, um dessen Checkliste zu archivieren.
 overview.nonodes.checklist=Dieser Kurs enth\u00E4lt keine Checkliste.
 overview.nonodes.iqtest=Dieser Kurs enth\u00E4lt keine Tests.
 overview.nonodes.iqsurv=Dieser Kurs enth\u00E4lt keine Umfragen.
diff --git a/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_en.properties
index 4f2f1db14f1..4f67233c035 100644
--- a/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/course/archiver/_i18n/LocalStrings_en.properties
@@ -92,6 +92,7 @@ nodechoose.intro.projectbroker=Please select a course element to archive its top
 nodechoose.intro.scorm=In the following dialog please select the SCORM element to be assessed.
 nodechoose.intro.ta=Select a course element in order to archive its tasks/solutions.
 nodechoose.intro.wiki=Select a course element to archive its Wiki.
+nodechoose.intro.checklist=Select a course element to archive its checklists.
 overview.nonodes.iqtest=This course does not contain any tests.
 overview.nonodes.iqsurv=This course does not contain any surveys.
 overview.nonodes.checklist=This course does not contain any checklists.
diff --git a/src/main/java/org/olat/course/assessment/AssessmentLoggingAction.java b/src/main/java/org/olat/course/assessment/AssessmentLoggingAction.java
index 2c0c8d171af..686e6ec3473 100644
--- a/src/main/java/org/olat/course/assessment/AssessmentLoggingAction.java
+++ b/src/main/java/org/olat/course/assessment/AssessmentLoggingAction.java
@@ -80,6 +80,16 @@ public class AssessmentLoggingAction extends BaseLoggingAction {
 	public static final ILoggingAction ASSESSMENT_COACHCOMMENT_UPDATED = 
 		new AssessmentLoggingAction(ActionType.admin, CrudAction.update, ActionVerb.edit, ActionObject.testcomment).setTypeList(
 				new ResourceableTypeList().addMandatory(OlatResourceableType.course, OlatResourceableType.node, StringResourceableType.qtiCoachComment).addOptional(StringResourceableType.targetIdentity));
+	
+	public static final ILoggingAction ASSESSMENT_DOCUMENT_ADDED = 
+			new AssessmentLoggingAction(ActionType.admin, CrudAction.update, ActionVerb.add, ActionObject.assessmentdocument).setTypeList(
+					new ResourceableTypeList().addMandatory(OlatResourceableType.course, OlatResourceableType.node, StringResourceableType.assessmentDocument)
+						.addOptional(StringResourceableType.targetIdentity));
+	public static final ILoggingAction ASSESSMENT_DOCUMENT_REMOVED = 
+			new AssessmentLoggingAction(ActionType.admin, CrudAction.update, ActionVerb.remove, ActionObject.assessmentdocument).setTypeList(
+					new ResourceableTypeList().addMandatory(OlatResourceableType.course, OlatResourceableType.node, StringResourceableType.assessmentDocument)
+						.addOptional(StringResourceableType.targetIdentity));
+
 	public static final ILoggingAction ASSESSMENT_BULK = 
 			new AssessmentLoggingAction(ActionType.admin, CrudAction.update, ActionVerb.edit, ActionObject.bulkassessment).setTypeList(
 					new ResourceableTypeList().addMandatory(OlatResourceableType.course, OlatResourceableType.node));
diff --git a/src/main/java/org/olat/course/assessment/manager/CourseAssessmentManagerImpl.java b/src/main/java/org/olat/course/assessment/manager/CourseAssessmentManagerImpl.java
index 1bc14f280c1..571328ff38d 100644
--- a/src/main/java/org/olat/course/assessment/manager/CourseAssessmentManagerImpl.java
+++ b/src/main/java/org/olat/course/assessment/manager/CourseAssessmentManagerImpl.java
@@ -237,6 +237,17 @@ public class CourseAssessmentManagerImpl implements AssessmentManager {
 			int numOfDocs = docs == null ? 0 : docs.length;
 			nodeAssessment.setNumberOfAssessmentDocuments(numOfDocs);
 			assessmentService.updateAssessmentEntry(nodeAssessment);
+			
+			// node log
+			ICourse course = CourseFactory.loadCourse(cgm.getCourseEntry());
+			UserNodeAuditManager am = course.getCourseEnvironment().getAuditManager();
+			am.appendToUserNodeLog(courseNode, identity, assessedIdentity, "assessment document added: " + filename);
+			
+			// user activity logging
+			ThreadLocalUserActivityLogger.log(AssessmentLoggingAction.ASSESSMENT_DOCUMENT_ADDED, 
+					getClass(), 
+					LoggingResourceable.wrap(assessedIdentity), 
+					LoggingResourceable.wrapNonOlatResource(StringResourceableType.assessmentDocument, "", StringHelper.stripLineBreaks(filename)));
 		} catch (IOException e) {
 			log.error("", e);
 		}
@@ -254,6 +265,17 @@ public class CourseAssessmentManagerImpl implements AssessmentManager {
 			int numOfDocs = docs == null ? 0 : docs.length;
 			nodeAssessment.setNumberOfAssessmentDocuments(numOfDocs);
 			assessmentService.updateAssessmentEntry(nodeAssessment);
+
+			// node log
+			ICourse course = CourseFactory.loadCourse(cgm.getCourseEntry());
+			UserNodeAuditManager am = course.getCourseEnvironment().getAuditManager();
+			am.appendToUserNodeLog(courseNode, identity, assessedIdentity, "assessment document removed: " + document.getName());
+			
+			// user activity logging
+			ThreadLocalUserActivityLogger.log(AssessmentLoggingAction.ASSESSMENT_DOCUMENT_REMOVED, 
+					getClass(), 
+					LoggingResourceable.wrap(assessedIdentity), 
+					LoggingResourceable.wrapNonOlatResource(StringResourceableType.assessmentDocument, "", StringHelper.stripLineBreaks(document.getName())));
 		}
 	}
 	
diff --git a/src/main/java/org/olat/course/nodes/CheckListCourseNode.java b/src/main/java/org/olat/course/nodes/CheckListCourseNode.java
index 5b8ac41f6e0..802d7c569cd 100644
--- a/src/main/java/org/olat/course/nodes/CheckListCourseNode.java
+++ b/src/main/java/org/olat/course/nodes/CheckListCourseNode.java
@@ -69,6 +69,7 @@ import org.olat.course.nodes.cl.model.Checkbox;
 import org.olat.course.nodes.cl.model.CheckboxList;
 import org.olat.course.nodes.cl.ui.AssessedIdentityCheckListController;
 import org.olat.course.nodes.cl.ui.CheckListEditController;
+import org.olat.course.nodes.cl.ui.CheckListExcelExport;
 import org.olat.course.nodes.cl.ui.CheckListRunController;
 import org.olat.course.nodes.cl.ui.CheckListRunForCoachController;
 import org.olat.course.properties.CoursePropertyManager;
@@ -203,6 +204,7 @@ public class CheckListCourseNode extends AbstractAccessableCourseNode implements
 	/**
 	 * @see org.olat.course.nodes.CourseNode#isConfigValid()
 	 */
+	@Override
 	public StatusDescription isConfigValid() {
 		if (oneClickStatusCache != null) {
 			return oneClickStatusCache[0];
@@ -545,6 +547,9 @@ public class CheckListCourseNode extends AbstractAccessableCourseNode implements
 		return true;
 	}
 
+	/**
+	 * Make an archive of all datas.
+	 */
 	@Override
 	public boolean archiveNodeData(Locale locale, ICourse course, ArchiveOptions options,
 			ZipOutputStream exportStream, String charset) {
@@ -557,7 +562,7 @@ public class CheckListCourseNode extends AbstractAccessableCourseNode implements
 		CheckboxList list = (CheckboxList)config.get(CONFIG_KEY_CHECKBOX);
 		CheckboxManager checkboxManager = CoreSpringFactory.getImpl(CheckboxManager.class);
 		if(list != null && list.getList() != null) {
-			Set<String> usedNames = new HashSet<String>();
+			Set<String> usedNames = new HashSet<>();
 			
 			for(Checkbox checkbox:list.getList()) {
 				VFSContainer dir = checkboxManager.getFileContainer(course.getCourseEnvironment(), this);
@@ -574,6 +579,34 @@ public class CheckListCourseNode extends AbstractAccessableCourseNode implements
 					}
 				}
 			}
+
+			String filename = dirName + "/" + StringHelper.transformDisplayNameToFileSystemName(getShortName());
+			new CheckListExcelExport(this, course, locale).exportAll(filename, exportStream);
+		}
+		
+		//assessment documents
+		if(config.getBooleanSafe(MSCourseNode.CONFIG_KEY_HAS_INDIVIDUAL_ASSESSMENT_DOCS, false)) {
+			List<AssessmentEntry> assessmentEntries = course.getCourseEnvironment()
+					.getAssessmentManager().getAssessmentEntries(this);
+			if(assessmentEntries != null && !assessmentEntries.isEmpty()) {
+				String assessmentDirName = dirName + "/Assessment_documents";
+				for(AssessmentEntry assessmentEntry:assessmentEntries) {
+					Identity assessedIdentity = assessmentEntry.getIdentity();
+					List<File> assessmentDocuments = course.getCourseEnvironment()
+							.getAssessmentManager().getIndividualAssessmentDocuments(this, assessedIdentity);
+					
+					String name = assessedIdentity.getUser().getLastName()
+							+ "_" + assessedIdentity.getUser().getFirstName()
+							+ "_" + assessedIdentity.getName();
+					String userDirName = assessmentDirName + "/" + StringHelper.transformDisplayNameToFileSystemName(name);
+					if(assessmentDocuments != null && !assessmentDocuments.isEmpty()) {
+						for(File document:assessmentDocuments) {
+							String path = userDirName + "/" + document.getName(); 
+							ZipUtil.addFileToZip(path, document, exportStream);
+						}
+					}
+				}
+			}
 		}
 
 		return super.archiveNodeData(locale, course, options, exportStream, charset);
diff --git a/src/main/java/org/olat/course/nodes/GTACourseNode.java b/src/main/java/org/olat/course/nodes/GTACourseNode.java
index 8f54bc4352e..1f21fa68a2f 100644
--- a/src/main/java/org/olat/course/nodes/GTACourseNode.java
+++ b/src/main/java/org/olat/course/nodes/GTACourseNode.java
@@ -20,6 +20,7 @@
 package org.olat.course.nodes;
 
 import java.io.File;
+import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
@@ -30,6 +31,7 @@ import java.util.Set;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
+import org.olat.basesecurity.GroupRoles;
 import org.olat.basesecurity.IdentityRef;
 import org.olat.core.CoreSpringFactory;
 import org.olat.core.commons.services.notifications.NotificationsManager;
@@ -86,6 +88,7 @@ import org.olat.course.run.scoring.ScoreEvaluation;
 import org.olat.course.run.userview.NodeEvaluation;
 import org.olat.course.run.userview.UserCourseEnvironment;
 import org.olat.group.BusinessGroup;
+import org.olat.group.BusinessGroupService;
 import org.olat.modules.ModuleConfiguration;
 import org.olat.modules.assessment.AssessmentEntry;
 import org.olat.modules.assessment.Role;
@@ -540,9 +543,9 @@ public class GTACourseNode extends AbstractAccessableCourseNode implements Persi
 			String courseTitle = course.getCourseTitle();
 			String fileName = ExportUtil.createFileNameWithTimeStamp(courseTitle, "xlsx");
 			List<AssessableCourseNode> nodes = Collections.<AssessableCourseNode>singletonList(this);
-			try {
+			try(OutputStream out = new ShieldOutputStream(exportStream)) {
 				exportStream.putNextEntry(new ZipEntry(dirName + "/" + fileName));
-				ScoreAccountingHelper.createCourseResultsOverviewXMLTable(users, nodes, course, locale, new ShieldOutputStream(exportStream));
+				ScoreAccountingHelper.createCourseResultsOverviewXMLTable(users, nodes, course, locale, out);
 				exportStream.closeEntry();
 			} catch (Exception e) {
 				log.error("", e);
@@ -633,6 +636,18 @@ public class GTACourseNode extends AbstractAccessableCourseNode implements Persi
 				ZipUtil.addDirectoryToZip(correctionDirectory.toPath(), correctionDirName, exportStream);
 			}
 		}
+		
+		//assessment documents
+		if(config.getBooleanSafe(MSCourseNode.CONFIG_KEY_HAS_INDIVIDUAL_ASSESSMENT_DOCS, false)) {
+			List<File> assessmentDocuments = course.getCourseEnvironment()
+					.getAssessmentManager().getIndividualAssessmentDocuments(this, assessedIdentity);
+			if(assessmentDocuments != null && !assessmentDocuments.isEmpty()) {
+				for(File document:assessmentDocuments) {
+					String path = userDirName + "/"  + (++flow) + "_assessment/" + document.getName(); 
+					ZipUtil.addFileToZip(path, document, exportStream);
+				}
+			}
+		}
 	}
 	
 	public void archiveNodeData(ICourse course, BusinessGroup businessGroup, TaskList taskList, String dirName, ZipOutputStream exportStream) {
@@ -678,6 +693,27 @@ public class GTACourseNode extends AbstractAccessableCourseNode implements Persi
 				ZipUtil.addDirectoryToZip(correctionDirectory.toPath(), correctionDirName, exportStream);
 			}
 		}
+		
+		//assessment documents for all participants of the group
+		if(config.getBooleanSafe(MSCourseNode.CONFIG_KEY_HAS_INDIVIDUAL_ASSESSMENT_DOCS, false)) {
+			List<Identity> assessedIdentities = CoreSpringFactory.getImpl(BusinessGroupService.class)
+					.getMembers(businessGroup, GroupRoles.participant.name());
+			String assessmentDirName = groupDirName + "/"  + (++flow) + "_assessment";
+			for(Identity assessedIdentity:assessedIdentities) {
+				List<File> assessmentDocuments = course.getCourseEnvironment()
+						.getAssessmentManager().getIndividualAssessmentDocuments(this, assessedIdentity);
+				if(assessmentDocuments != null && !assessmentDocuments.isEmpty()) {
+					String name = assessedIdentity.getUser().getLastName()
+							+ "_" + assessedIdentity.getUser().getFirstName()
+							+ "_" + assessedIdentity.getName();
+					String userDirName = assessmentDirName + "/" + StringHelper.transformDisplayNameToFileSystemName(name);
+					for(File document:assessmentDocuments) {
+						String path = userDirName + "/" + document.getName(); 
+						ZipUtil.addFileToZip(path, document, exportStream);
+					}
+				}
+			}
+		}
 	}
 	
 	@Override
diff --git a/src/main/java/org/olat/course/nodes/MSCourseNode.java b/src/main/java/org/olat/course/nodes/MSCourseNode.java
index 629641a2ba2..1848e93bd1d 100644
--- a/src/main/java/org/olat/course/nodes/MSCourseNode.java
+++ b/src/main/java/org/olat/course/nodes/MSCourseNode.java
@@ -167,6 +167,7 @@ public class MSCourseNode extends AbstractAccessableCourseNode implements Persis
 	/**
 	 * @see org.olat.course.nodes.CourseNode#getReferencedRepositoryEntry()
 	 */
+	@Override
 	public RepositoryEntry getReferencedRepositoryEntry() {
 		return null;
 	}
@@ -174,6 +175,7 @@ public class MSCourseNode extends AbstractAccessableCourseNode implements Persis
 	/**
 	 * @see org.olat.course.nodes.CourseNode#needsReferenceToARepositoryEntry()
 	 */
+	@Override
 	public boolean needsReferenceToARepositoryEntry() {
 		return false;
 	}
@@ -181,6 +183,7 @@ public class MSCourseNode extends AbstractAccessableCourseNode implements Persis
 	/**
 	 * @see org.olat.course.nodes.CourseNode#isConfigValid()
 	 */
+	@Override
 	public StatusDescription isConfigValid() {
 		/*
 		 * first check the one click cache
@@ -402,9 +405,9 @@ public class MSCourseNode extends AbstractAccessableCourseNode implements Persis
 	 */
 	@Override
 	public void updateUserCoachComment(String coachComment, UserCourseEnvironment userCourseEnvironment) {
-		AssessmentManager am = userCourseEnvironment.getCourseEnvironment().getAssessmentManager();
-		Identity mySelf = userCourseEnvironment.getIdentityEnvironment().getIdentity();
 		if (coachComment != null) {
+			AssessmentManager am = userCourseEnvironment.getCourseEnvironment().getAssessmentManager();
+			Identity mySelf = userCourseEnvironment.getIdentityEnvironment().getIdentity();
 			am.saveNodeCoachComment(this, mySelf, coachComment);
 		}
 	}
diff --git a/src/main/java/org/olat/course/nodes/TACourseNode.java b/src/main/java/org/olat/course/nodes/TACourseNode.java
index d047e2acc08..7b1b79fc9ef 100644
--- a/src/main/java/org/olat/course/nodes/TACourseNode.java
+++ b/src/main/java/org/olat/course/nodes/TACourseNode.java
@@ -27,6 +27,7 @@ package org.olat.course.nodes;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
@@ -904,10 +905,9 @@ public class TACourseNode extends GenericCourseNode implements PersistentAssessa
 			String fileName = ExportUtil.createFileNameWithTimeStamp(courseTitle, "xlsx");
 			List<AssessableCourseNode> nodes = Collections.<AssessableCourseNode>singletonList(this);
 			// write course results overview table to filesystem
-			try {
+			try(OutputStream out = new ShieldOutputStream(exportStream)) {
 				exportStream.putNextEntry(new ZipEntry(dirName + "/" + fileName));
-				ScoreAccountingHelper.createCourseResultsOverviewXMLTable(users, nodes, course, locale,
-						new ShieldOutputStream(exportStream));
+				ScoreAccountingHelper.createCourseResultsOverviewXMLTable(users, nodes, course, locale, out);
 				exportStream.closeEntry();
 			} catch (IOException e) {
 				log.error("", e);
@@ -960,6 +960,20 @@ public class TACourseNode extends GenericCourseNode implements PersistentAssessa
 					}
 				}
 			}
+			
+			//assessment documents
+			if(getModuleConfiguration().getBooleanSafe(MSCourseNode.CONFIG_KEY_HAS_INDIVIDUAL_ASSESSMENT_DOCS, false)) {
+				for(Identity assessedIdentity:users) {
+					List<File> assessmentDocuments = course.getCourseEnvironment()
+							.getAssessmentManager().getIndividualAssessmentDocuments(this, assessedIdentity);
+					if(assessmentDocuments != null && !assessmentDocuments.isEmpty()) {
+						for(File document:assessmentDocuments) {
+							String path = dirName + "/assessment_documents/"  + assessedIdentity.getName() + "/" + document.getName(); 
+							ZipUtil.addFileToZip(path, document, exportStream);
+						}
+					}
+				}
+			}
 		}	
 		return dataFound;
 	}
diff --git a/src/main/java/org/olat/course/nodes/cl/ui/CheckListAssessmentController.java b/src/main/java/org/olat/course/nodes/cl/ui/CheckListAssessmentController.java
index c2a03d10908..b51cf3aeae4 100644
--- a/src/main/java/org/olat/course/nodes/cl/ui/CheckListAssessmentController.java
+++ b/src/main/java/org/olat/course/nodes/cl/ui/CheckListAssessmentController.java
@@ -364,7 +364,7 @@ public class CheckListAssessmentController extends FormBasicController implement
 		List<CheckListAssessmentRow> dataViews = new ArrayList<>();
 		
 		int numOfcheckbox = checkbox.size();
-		Map<String,Integer> indexed = new HashMap<String,Integer>();
+		Map<String,Integer> indexed = new HashMap<>();
 		for(int i=numOfcheckbox; i-->0; ) {
 			indexed.put(checkbox.get(i).getCheckboxId(), new Integer(i));
 		}
diff --git a/src/main/java/org/olat/course/nodes/cl/ui/CheckListExcelExport.java b/src/main/java/org/olat/course/nodes/cl/ui/CheckListExcelExport.java
new file mode 100644
index 00000000000..7730d6ee15f
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/cl/ui/CheckListExcelExport.java
@@ -0,0 +1,231 @@
+/**
+ * <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.course.nodes.cl.ui;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.olat.core.CoreSpringFactory;
+import org.olat.core.gui.translator.Translator;
+import org.olat.core.id.Identity;
+import org.olat.core.id.User;
+import org.olat.core.id.context.BusinessControlFactory;
+import org.olat.core.id.context.ContextEntry;
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.Util;
+import org.olat.core.util.io.ShieldOutputStream;
+import org.olat.core.util.openxml.OpenXMLWorkbook;
+import org.olat.core.util.openxml.OpenXMLWorksheet;
+import org.olat.core.util.openxml.OpenXMLWorksheet.Row;
+import org.olat.core.util.openxml.workbookstyle.CellStyle;
+import org.olat.course.ICourse;
+import org.olat.course.nodes.CheckListCourseNode;
+import org.olat.course.nodes.cl.CheckboxManager;
+import org.olat.course.nodes.cl.model.AssessmentData;
+import org.olat.course.nodes.cl.model.Checkbox;
+import org.olat.course.nodes.cl.model.CheckboxList;
+import org.olat.course.nodes.cl.model.DBCheck;
+import org.olat.modules.ModuleConfiguration;
+import org.olat.modules.assessment.AssessmentEntry;
+import org.olat.user.UserManager;
+import org.olat.user.propertyhandlers.UserPropertyHandler;
+
+/**
+ * 
+ * This export all the data and it doesn't filter the datas
+ * based on their roles.
+ * 
+ * Initial date: 19 janv. 2018<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class CheckListExcelExport {
+	
+	private static final OLog log = Tracing.createLoggerFor(CheckListExcelExport.class);
+
+	private Translator translator;
+	private final ICourse course;
+	private final CheckListCourseNode courseNode;
+	private final List<UserPropertyHandler> userPropertyHandlers;
+	
+	private final UserManager userManager;
+	private final CheckboxManager checkboxManager;
+	
+	public CheckListExcelExport(CheckListCourseNode courseNode, ICourse course, Locale locale) {
+		this.courseNode = courseNode;
+		this.course = course;
+		
+		userManager = CoreSpringFactory.getImpl(UserManager.class);
+		checkboxManager = CoreSpringFactory.getImpl(CheckboxManager.class);
+		userPropertyHandlers = userManager.getUserPropertyHandlersFor(CheckListAssessmentController.USER_PROPS_ID, true);
+		
+		translator = Util.createPackageTranslator(CheckListAssessmentController.class, locale);
+		translator = userManager.getPropertyHandlerTranslator(translator);
+	}
+	
+	public void exportAll(String filename, ZipOutputStream exportStream) {
+		List<AssessmentData> dataList = checkboxManager.getAssessmentDatas(course, courseNode.getIdent(), null, null);
+		try(OutputStream out = new ShieldOutputStream(exportStream)) {
+			exportStream.putNextEntry(new ZipEntry(filename + ".xlsx"));
+			exportWorkbook(dataList, out);
+			exportStream.closeEntry();
+		} catch (IOException e) {
+			log.error("", e);
+		}
+	}
+	
+	public void exportWorkbook(List<AssessmentData> dataList, OutputStream exportStream) {
+		try(OpenXMLWorkbook workbook = new OpenXMLWorkbook(exportStream, 1)) {
+			//headers
+			OpenXMLWorksheet exportSheet = workbook.nextWorksheet();
+			exportSheet.setHeaderRows(1);
+			writeHeaders(exportSheet, workbook);
+			writeData(dataList, exportSheet);
+		} catch(Exception e) {
+			log.error("", e);
+		}
+	}
+
+	private void writeHeaders(OpenXMLWorksheet exportSheet, OpenXMLWorkbook workbook) {
+		CellStyle headerStyle = workbook.getStyles().getHeaderStyle();
+		//second header
+		int col = 0;//reset column counter
+		Row header2Row = exportSheet.newRow();
+		String sequentialNumber = translator.translate("column.header.seqnum");
+		header2Row.addCell(col++, sequentialNumber, headerStyle);
+
+		for (UserPropertyHandler userPropertyHandler : userPropertyHandlers) {
+			if (userPropertyHandler == null) {
+				continue;
+			}
+			String header = translator.translate(userPropertyHandler.i18nFormElementLabelKey());
+			header2Row.addCell(col++, header, headerStyle);			
+		}
+		
+		// add other user and session information
+		header2Row.addCell(col++, translator.translate("column.header.homepage"), headerStyle);
+
+		// course node points and passed
+		if(courseNode.hasScoreConfigured()) {
+			header2Row.addCell(col++, translator.translate("column.header.node.points"), headerStyle);
+		}
+		if(courseNode.hasPassedConfigured()) {
+			header2Row.addCell(col++, translator.translate("column.header.node.passed"), headerStyle);
+		}
+
+		ModuleConfiguration config = courseNode.getModuleConfiguration();
+		CheckboxList list = (CheckboxList)config.get(CheckListCourseNode.CONFIG_KEY_CHECKBOX);
+		if(list != null && list.getList() != null) {
+			List<Checkbox> checkboxList = list.getList();
+			for(Checkbox checkbox:checkboxList) {
+				String boxTitle = checkbox.getTitle();
+				header2Row.addCell(col++, boxTitle, headerStyle);
+				if(courseNode.hasScoreConfigured() && checkbox.getPoints() != null) {
+					header2Row.addCell(col++, translator.translate("column.header.points"), headerStyle);
+				}	
+			}
+		}
+	}
+
+	private void writeData(List<AssessmentData> dataList, OpenXMLWorksheet exportSheet) {
+		List<AssessmentEntry> entries = course.getCourseEnvironment().getAssessmentManager().getAssessmentEntries(courseNode);
+		Map<Identity,AssessmentEntry> entryMap = new HashMap<>();
+		for(AssessmentEntry entry:entries) {
+			entryMap.put(entry.getIdentity(), entry);
+		}
+
+		int num = 1;
+		for(AssessmentData data:dataList) {
+			AssessmentEntry entry = entryMap.get(data.getIdentity());
+			writeDataRow(data, entry, num++, exportSheet);
+		}
+	}
+	
+	private void writeDataRow(AssessmentData data, AssessmentEntry entry, int num, OpenXMLWorksheet exportSheet) {
+		int col = 0;
+		Identity assessedIdentity = data.getIdentity();
+		User assessedUser = assessedIdentity.getUser();
+		
+		Row dataRow = exportSheet.newRow();
+		dataRow.addCell(col++, num, null);//sequence number
+		
+		for (UserPropertyHandler userPropertyHandler : userPropertyHandlers) {
+			if (userPropertyHandler != null) {
+				String property = userPropertyHandler.getUserProperty(assessedUser, translator.getLocale());
+				dataRow.addCell(col++, property, null);
+			}
+		}
+		
+		//homepage
+		ContextEntry ce = BusinessControlFactory.getInstance().createContextEntry(assessedIdentity);
+		String homepage = BusinessControlFactory.getInstance().getAsURIString(Collections.singletonList(ce), false);
+		dataRow.addCell(col++, homepage, null);
+		
+		// course node points and passed
+		if(courseNode.hasScoreConfigured()) {
+			if(entry != null && entry.getScore() != null) {
+				dataRow.addCell(col++, entry.getScore(), null);
+			} else {
+				col++;
+			}
+		}
+		if(courseNode.hasPassedConfigured()) {
+			if(entry != null && entry.getPassed() != null) {
+				dataRow.addCell(col++, entry.getPassed().toString(), null);
+			} else {
+				col++;
+			}
+		}
+		
+		ModuleConfiguration config = courseNode.getModuleConfiguration();
+		CheckboxList list = (CheckboxList)config.get(CheckListCourseNode.CONFIG_KEY_CHECKBOX);
+		if(list != null && list.getList() != null && data.getChecks() != null) {
+			Map<String,DBCheck> checkMap = data.getChecks()
+					.stream().collect(Collectors.toMap(c -> c.getCheckbox().getCheckboxId(), c -> c));
+			List<Checkbox> checkboxList = list.getList();
+			for(Checkbox checkbox:checkboxList) {
+				String boxId = checkbox.getCheckboxId();
+				DBCheck check = checkMap.get(boxId);
+				if(check != null && check.getChecked() != null && check.getChecked().booleanValue()) {
+					dataRow.addCell(col++, "x", null);
+				} else {
+					col++;
+				}
+				
+				if(courseNode.hasScoreConfigured() && checkbox.getPoints() != null) {
+					if(check != null && check.getScore() != null ) {
+						dataRow.addCell(col++, check.getScore(), null);
+					} else {
+						col++;
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/src/main/java/org/olat/course/nodes/cl/ui/CheckboxEditController.java b/src/main/java/org/olat/course/nodes/cl/ui/CheckboxEditController.java
index 598f08e17d3..1da0894938d 100644
--- a/src/main/java/org/olat/course/nodes/cl/ui/CheckboxEditController.java
+++ b/src/main/java/org/olat/course/nodes/cl/ui/CheckboxEditController.java
@@ -21,7 +21,7 @@ package org.olat.course.nodes.cl.ui;
 
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.io.InputStream;
 
 import org.olat.core.CoreSpringFactory;
@@ -231,12 +231,11 @@ public class CheckboxEditController extends FormBasicController {
 			String filename = fileEl.getUploadFileName();
 			checkbox.setFilename(filename);
 			
-			try {
-				VFSContainer container = getFileContainer();
-				VFSLeaf leaf = container.createChildLeaf(filename);
-				InputStream inStream = new FileInputStream(uploadedFile);
+			VFSContainer container = getFileContainer();
+			VFSLeaf leaf = container.createChildLeaf(filename);
+			try(InputStream inStream = new FileInputStream(uploadedFile)) {
 				VFSManager.copyContent(inStream, leaf);
-			} catch (FileNotFoundException e) {
+			} catch (IOException e) {
 				logError("", e);
 			}
 		}
diff --git a/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_de.properties
index 63531f6e32d..7e1c9be78b5 100644
--- a/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_de.properties
@@ -15,6 +15,11 @@ checklist.update.assessment=Aktualisierung der Bewertungsinformationen aller Ben
 checklist.update.efficiencystatements=Aktualisierung der Leistungsnachweise aller Benutzer
 coach.desc=In der unten stehenden Listen finden Sie die von Ihnen betreuten Teilnehmer dieses Kurses. W\u00E4hlen Sie "$\:table.header.edit.checkbox" um die Checkbox-Auswahl oder Punkte eines Teilnehmers zu ver\u00E4ndern.
 coach.due.date.desc=Diese Checkliste ist mit einem Abgabedatum versehen. Als Betreuer sollten Sie \u00C4nderungen erst nach Ablauf des Abgabedatums vornehmen.
+column.header.seqnum=Laufnummer
+column.header.homepage=Homepage
+column.header.node.passed=Kursbaustein bestanden
+column.header.node.points=Kursbaustein Punkte
+column.header.points=$\:box.points
 comment.nocomment=$org.olat.course.nodes.ms\:comment.nocomment
 comment.title=$org.olat.course.nodes.ms\:comment.title
 condition.accessibility.title=Zugang
diff --git a/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_en.properties
index b6027b44d7e..b4e906e64aa 100644
--- a/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_en.properties
@@ -15,6 +15,10 @@ checklist.update.assessment=Updating the assessment information of all course pa
 checklist.update.efficiencystatements=Updating of certificates of all users.
 coach.desc=In the list below you will find all participants of this course coached by you. Select "$\:table.header.edit.checkbox" in order to change check marks or the score of a participant.
 coach.due.date.desc=Please notice that this checklist has a deadline. As a coach, you should make changes only after the deadline expiration.
+column.header.homepage=Home page
+column.header.node.passed=Passed course element
+column.header.node.points=Score course element
+column.header.points=$\:box.points
 comment.nocomment=$org.olat.course.nodes.ms\:comment.nocomment
 comment.title=$org.olat.course.nodes.ms\:comment.title
 condition.accessibility.title=Access
diff --git a/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_fr.properties b/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_fr.properties
index 060217e5752..ba3aeee3911 100644
--- a/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_fr.properties
+++ b/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_fr.properties
@@ -15,6 +15,10 @@ checklist.update.assessment=Mettre \u00E0 jour les \u00E9valuations de tous les
 checklist.update.efficiencystatements=Mettre \u00E0 jour les certificats de tous les utilisateurs.
 coach.desc=Dans la liste ci-dessous vous trouverez tous les participants du cours que vous supervisez. S\u00E9lectionnez "$\:table.header.edit.checkbox" pour changer les cases \u00E0 cocher ou le score d'un participant.
 coach.due.date.desc=S'il vous pla\u00EEt noter que cette liste de contr\u00F4le fixe un d\u00E9lai. En tant que coach, vous ne devez effectuer des changements qu'apr\u00E8s la date limite d'expiration.
+column.header.homepage=Page d'accueil
+column.header.node.passed=Element de cours r\u00E9ussi
+column.header.node.points=Points \u00E9l\u00E9ment de cours
+column.header.points=$\:box.points
 comment.nocomment=$org.olat.course.nodes.ms\:comment.nocomment
 comment.title=$org.olat.course.nodes.ms\:comment.title
 condition.accessibility.title=Acc\u00E8s
diff --git a/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_it.properties b/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_it.properties
index 33b6ea4a5b5..210c111a50f 100644
--- a/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_it.properties
+++ b/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_it.properties
@@ -14,6 +14,8 @@ checklist.update.assessment=Aggiornare la valutazione per tutti i partecipanti a
 checklist.update.efficiencystatements=Aggiornare i certificati di tutti gli utenti.
 coach.desc=Nell'elenco sottostante troverai tutti i partecipanti al corso tutorati da te. Selezionare "$\:table.header.edit.checkbox" per modificare le checkbox o il punteggio di un partecipante.
 coach.due.date.desc=Prego notare che questa checklist ha una scadenza. Come tutore, dovresti effettuare modifiche solo dopo la scadenza.
+column.header.score=$\:box.points
+column.header.homepage=Home page
 comment.nocomment=$org.olat.course.nodes.ms\:comment.nocomment
 comment.title=$org.olat.course.nodes.ms\:comment.title
 condition.accessibility.title=Accesso
diff --git a/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_pt_BR.properties b/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_pt_BR.properties
index 1a7b369881d..c82ca1f961b 100644
--- a/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_pt_BR.properties
+++ b/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_pt_BR.properties
@@ -15,6 +15,10 @@ checklist.update.assessment=Atualizando as informa\u00E7\u00F5es de avalia\u00E7
 checklist.update.efficiencystatements=Atualizando os certificados de todos os usu\u00E1rios.
 coach.desc=Na lista abaixo voc\u00EA vai encontrar todos os participantes deste curso treinados por voc\u00EA. Selecione "$\:table.header.edit.checkbox", a fim de alterar as marcas de sele\u00E7\u00E3o ou a pontua\u00E7\u00E3o de um participante.
 coach.due.date.desc=Por favor note que este Checklist tem um prazo. Como treinador, voc\u00EA deve fazer altera\u00E7\u00F5es somente ap\u00F3s a expira\u00E7\u00E3o do prazo.
+column.header.homepage=Home page
+column.header.node.passed=Elemento de curso aprovado
+column.header.node.points=Pontua\u00E7\u00E3o do elemento do curso
+column.header.points=$\:box.points
 comment.nocomment=$org.olat.course.nodes.ms\:comment.nocomment
 comment.title=$org.olat.course.nodes.ms\:comment.title
 condition.accessibility.title=Acesso
diff --git a/src/main/java/org/olat/course/nodes/gta/ui/GroupBulkDownloadResource.java b/src/main/java/org/olat/course/nodes/gta/ui/GroupBulkDownloadResource.java
index 4e42c89e406..cfc3efea9e7 100644
--- a/src/main/java/org/olat/course/nodes/gta/ui/GroupBulkDownloadResource.java
+++ b/src/main/java/org/olat/course/nodes/gta/ui/GroupBulkDownloadResource.java
@@ -20,6 +20,7 @@
 package org.olat.course.nodes.gta.ui;
 
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
@@ -29,7 +30,6 @@ import java.util.zip.ZipOutputStream;
 
 import javax.servlet.http.HttpServletResponse;
 
-import org.apache.commons.io.IOUtils;
 import org.olat.basesecurity.GroupRoles;
 import org.olat.core.CoreSpringFactory;
 import org.olat.core.gui.media.MediaResource;
@@ -113,10 +113,8 @@ public class GroupBulkDownloadResource implements MediaResource {
 		String urlEncodedLabel = StringHelper.urlEncodeUTF8(label);
 		hres.setHeader("Content-Disposition","attachment; filename*=UTF-8''" + urlEncodedLabel);			
 		hres.setHeader("Content-Description", urlEncodedLabel);
-		
-		ZipOutputStream zout = null;
-		try {
-			zout = new ZipOutputStream(hres.getOutputStream());
+
+		try(ZipOutputStream zout = new ZipOutputStream(hres.getOutputStream())) {
 			zout.setLevel(9);
 			ICourse course = CourseFactory.loadCourse(courseOres);
 			GTAManager gtaManager = CoreSpringFactory.getImpl(GTAManager.class);
@@ -127,9 +125,9 @@ public class GroupBulkDownloadResource implements MediaResource {
 				String courseTitle = course.getCourseTitle();
 				String fileName = ExportUtil.createFileNameWithTimeStamp(courseTitle, "xlsx");
 				List<AssessableCourseNode> nodes = Collections.<AssessableCourseNode>singletonList(courseNode);
-				try {
+				try(OutputStream out = new ShieldOutputStream(zout)) {
 					zout.putNextEntry(new ZipEntry(fileName));
-					ScoreAccountingHelper.createCourseResultsOverviewXMLTable(assessableIdentities, nodes, course, locale, new ShieldOutputStream(zout));
+					ScoreAccountingHelper.createCourseResultsOverviewXMLTable(assessableIdentities, nodes, course, locale, out);
 					zout.closeEntry();
 				} catch (Exception e) {
 					log.error("", e);
@@ -142,8 +140,6 @@ public class GroupBulkDownloadResource implements MediaResource {
 			}
 		} catch (Exception e) {
 			log.error("", e);
-		} finally {
-			IOUtils.closeQuietly(zout);
 		}
 	}
 
diff --git a/src/main/java/org/olat/course/nodes/ms/MSEditFormController.java b/src/main/java/org/olat/course/nodes/ms/MSEditFormController.java
index a9df554dd31..c49d01242ab 100644
--- a/src/main/java/org/olat/course/nodes/ms/MSEditFormController.java
+++ b/src/main/java/org/olat/course/nodes/ms/MSEditFormController.java
@@ -49,7 +49,7 @@ import org.olat.modules.ModuleConfiguration;
 public class MSEditFormController extends FormBasicController {
 
 	/** Configuration this controller will modify. */
-	private ModuleConfiguration modConfig;
+	private final ModuleConfiguration modConfig;
 
 	/** whether score will be awarded or not. */
 	private MultipleSelectionElement scoreGranted;
@@ -100,9 +100,8 @@ public class MSEditFormController extends FormBasicController {
 	public MSEditFormController(UserRequest ureq, WindowControl wControl, ModuleConfiguration modConfig) {
 		super(ureq, wControl, FormBasicController.LAYOUT_DEFAULT);
 		this.modConfig = modConfig;
-		this.trueFalseKeys = new String[] { Boolean.TRUE.toString(), Boolean.FALSE.toString() };
-
-		this.passedTypeValues = new String[] { translate("form.passedtype.cutval"), translate("form.passedtype.manual") };
+		trueFalseKeys = new String[] { Boolean.TRUE.toString(), Boolean.FALSE.toString() };
+		passedTypeValues = new String[] { translate("form.passedtype.cutval"), translate("form.passedtype.manual") };
 		initForm(ureq);
 	}
 
diff --git a/src/main/java/org/olat/course/statistic/AsyncExportManager.java b/src/main/java/org/olat/course/statistic/AsyncExportManager.java
index 9e910db52ea..dc97d933cbb 100644
--- a/src/main/java/org/olat/course/statistic/AsyncExportManager.java
+++ b/src/main/java/org/olat/course/statistic/AsyncExportManager.java
@@ -33,7 +33,6 @@ import org.olat.core.commons.services.taskexecutor.TaskExecutorManager;
 import org.olat.core.id.Identity;
 import org.olat.core.logging.OLog;
 import org.olat.core.logging.Tracing;
-import org.olat.core.manager.BasicManager;
 
 /**
  * Handles asynchronous aspects of log export - including limiting the
@@ -42,7 +41,7 @@ import org.olat.core.manager.BasicManager;
  * Initial Date:  13.01.2010 <br>
  * @author Stefan
  */
-public class AsyncExportManager extends BasicManager {
+public class AsyncExportManager {
 
 	/** the logging object used in this class **/
 	private static final OLog log_ = Tracing.createLoggerFor(AsyncExportManager.class);
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 672ac0ddbe5..ea3f5c7ce2a 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
@@ -230,9 +230,9 @@ public class QTI21ArchiveFormat {
 	}
 	
 	public void export(String filename,  ZipOutputStream exportStream) {
-		try {
+		try(OutputStream out = new ShieldOutputStream(exportStream)) {
 			exportStream.putNextEntry(new ZipEntry(filename));
-			exportWorkbook(new ShieldOutputStream(exportStream));
+			exportWorkbook(out);
 			exportStream.closeEntry();
 		} catch (IOException e) {
 			log.error("", e);
diff --git a/src/main/java/org/olat/ims/qti21/resultexport/QTI21ResultsExportMediaResource.java b/src/main/java/org/olat/ims/qti21/resultexport/QTI21ResultsExportMediaResource.java
index ffd36428540..5fc79e9972d 100644
--- a/src/main/java/org/olat/ims/qti21/resultexport/QTI21ResultsExportMediaResource.java
+++ b/src/main/java/org/olat/ims/qti21/resultexport/QTI21ResultsExportMediaResource.java
@@ -73,6 +73,7 @@ import org.olat.core.util.UserSession;
 import org.olat.core.util.Util;
 import org.olat.core.util.WebappHelper;
 import org.olat.core.util.ZipUtil;
+import org.olat.course.assessment.AssessmentManager;
 import org.olat.course.nodes.ArchiveOptions;
 import org.olat.course.nodes.QTICourseNode;
 import org.olat.course.run.environment.CourseEnvironment;
@@ -266,7 +267,8 @@ public class QTI21ResultsExportMediaResource implements MediaResource {
 	}
 	
 	private List<AssessedMember> createAssessedMembersDetail(ZipOutputStream zout) throws IOException {
-		List<AssessmentEntry> assessmentEntries = courseEnv.getAssessmentManager().getAssessmentEntries(courseNode);
+		AssessmentManager assessmentManager = courseEnv.getAssessmentManager();
+		List<AssessmentEntry> assessmentEntries = assessmentManager.getAssessmentEntries(courseNode);
 		Map<Identity,AssessmentEntry> assessmentEntryMap = new HashMap<>();
 		for(AssessmentEntry assessmentEntry:assessmentEntries) {
 			assessmentEntryMap.put(assessmentEntry.getIdentity(), assessmentEntry);
@@ -280,6 +282,8 @@ public class QTI21ResultsExportMediaResource implements MediaResource {
 			
 			//content of single assessed member		
 			List<ResultDetail> assessments = createResultDetail(identity, zout, idDir);
+			List<File> assessmentDocuments = assessmentManager.getIndividualAssessmentDocuments(courseNode, identity);
+			
 			Boolean passed = null;
 			BigDecimal score = null;
 			if(assessmentEntryMap.containsKey(identity)) {
@@ -294,9 +298,16 @@ public class QTI21ResultsExportMediaResource implements MediaResource {
 					identity.getUser().getLastName(), identity.getUser().getFirstName(), memberEmail,
 					assessments.size(), passed, score, linkToUser);
 			
-			String singleUserInfoHTML = createResultListingHTML(assessments, member);
-			convertToZipEntry(zout, exportFolderName + "/" + DATA + identity.getName() + "/index.html", singleUserInfoHTML);
+			String userDataDir = exportFolderName + "/" + DATA + identity.getName();
+			String singleUserInfoHTML = createResultListingHTML(assessments, assessmentDocuments, member);
+			convertToZipEntry(zout, userDataDir + "/index.html", singleUserInfoHTML);
 			assessedMembers.add(member);	
+			
+			//assessment documents
+			for(File document:assessmentDocuments) {
+				String assessmentDocDir = userDataDir + "/Assessment_documents/" + document.getName();
+				ZipUtil.addFileToZip(assessmentDocDir, document, zout);
+			}
 		}
 		return assessedMembers;
 	}
@@ -316,7 +327,6 @@ public class QTI21ResultsExportMediaResource implements MediaResource {
 	}
 	
 	private String createResultHTML(Component results) {
-		StringOutput sb = new StringOutput(32000);
 		String pagePath = Util.getPackageVelocityRoot(this.getClass()) + "/qti21results.html";
 		URLBuilder ubu = new URLBuilder("auth", "1", "0");
 		//generate VelocityContainer and put Component
@@ -326,13 +336,19 @@ public class QTI21ResultsExportMediaResource implements MediaResource {
 		
 		//render VelocityContainer to StringOutPut
 		Renderer renderer = Renderer.getInstance(mainVC, translator, ubu, new RenderResult(), new EmptyGlobalSettings());
-		VelocityRenderDecorator vrdec = new VelocityRenderDecorator(renderer, mainVC, sb);
-		mainVC.contextPut("r", vrdec);
-		renderer.render(sb, mainVC, null);
-		return sb.toString();
+		try(StringOutput sb = new StringOutput(32000);
+				VelocityRenderDecorator vrdec = new VelocityRenderDecorator(renderer, mainVC, sb)) {
+			mainVC.contextPut("r", vrdec);
+			renderer.render(sb, mainVC, null);
+			vrdec.close();
+			return sb.toString();
+		} catch(Exception e) {
+			log.error("", e);
+			return "";
+		}
 	}
 	
-	private String createResultListingHTML(List<ResultDetail> assessments, AssessedMember assessedMember) {
+	private String createResultListingHTML(List<ResultDetail> assessments, List<File> assessmentDocs, AssessedMember assessedMember) {
 		// now put values to velocityContext
 		VelocityContext ctx = new VelocityContext();
 		ctx.put("t", translator);
@@ -340,8 +356,10 @@ public class QTI21ResultsExportMediaResource implements MediaResource {
 		ctx.put("return", translator.translate("button.return"));
 		ctx.put("assessments", assessments);
 		ctx.put("assessedMember", assessedMember);
-		if (assessments.size() > 0) ctx.put("hasResults", Boolean.TRUE);
-		
+		ctx.put("assessmentDocs", assessmentDocs);
+		ctx.put("hasResults", Boolean.valueOf(!assessments.isEmpty()));
+		ctx.put("hasDocuments", Boolean.valueOf(!assessmentDocs.isEmpty()));
+
 		String template = FileUtils.load(QTI21ResultsExportMediaResource.class
 				.getResourceAsStream("_content/qtiListing.html"), "utf-8");
 
diff --git a/src/main/java/org/olat/ims/qti21/resultexport/_content/qtiListing.html b/src/main/java/org/olat/ims/qti21/resultexport/_content/qtiListing.html
index 3d8dee15a90..c18ced758e0 100644
--- a/src/main/java/org/olat/ims/qti21/resultexport/_content/qtiListing.html
+++ b/src/main/java/org/olat/ims/qti21/resultexport/_content/qtiListing.html
@@ -6,13 +6,11 @@
 	<title>$title</title>
 	<script type="text/javascript" src="../../css/offline/qti/jquery-2.1.3.min.js" ></script>
 	<script >
-    $.noConflict();
-    jQuery(document).ready(function(){
-
-            jQuery("#results tbody tr").click(function (){
-   			jQuery(this).addClass("success");
-    		jQuery(this).siblings().removeClass("success");
-
+	$.noConflict();
+	jQuery(document).ready(function(){
+		jQuery("#results tbody tr").click(function (){
+		jQuery(this).addClass("success");
+		jQuery(this).siblings().removeClass("success");
     });});
 	</script>
 </head>
@@ -80,5 +78,24 @@
 		</table>
 	</div>
 	#end
+
+	#if($hasDocuments)
+	<div>
+		<table id='assessmentDocuments' class='table table-striped table-hover'>
+			<thead>
+				<tr>
+					<th>$t.translate("assessment.docs")</th>
+				</tr>
+			</thead>
+			<tbody>
+				#foreach($assessmentDoc in $assessmentDocs)
+				<tr>
+					<td><a href='Assessment_documents/${assessmentDoc.getName()}' target='_blank'>${assessmentDoc.getName()}</td>							
+				</tr>
+				#end
+			</tbody>
+		</table>
+	</div>
+	#end
 </body>
 </html>
diff --git a/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_de.properties
index a8f71b2ceed..86e7f44be1f 100644
--- a/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_de.properties
@@ -25,4 +25,5 @@ table.user.manualScore=Manuell Punkte
 table.user.score=Punkte
 table.user.trial=Versuch
 table.test.sessions=Test Versuche
-export.folder.name=Resultate
\ No newline at end of file
+export.folder.name=Resultate
+assessment.docs=Bewertungsdokumente
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_en.properties
index 2f89c6200fd..85e5f0a0706 100644
--- a/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_en.properties
@@ -26,3 +26,4 @@ table.user.id=ID
 table.user.manualScore=Manual score
 table.user.score=Score
 table.user.trial=Attempt
+assessment.docs=Assessment documents
diff --git a/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_fr.properties b/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_fr.properties
index 69e44960f76..13e7fab78b0 100644
--- a/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_fr.properties
+++ b/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_fr.properties
@@ -26,3 +26,4 @@ table.user.id=ID
 table.user.manualScore=Points manuels
 table.user.score=Points
 table.user.trial=Essai
+assessment.docs=Documents d'\u00E9valuation
diff --git a/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_pt_BR.properties b/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_pt_BR.properties
index 0e08f7742fb..d62b7b809ed 100644
--- a/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_pt_BR.properties
+++ b/src/main/java/org/olat/ims/qti21/resultexport/_i18n/LocalStrings_pt_BR.properties
@@ -26,3 +26,4 @@ table.user.id=ID
 table.user.manualScore=Pontua\u00E7\u00E3o manual
 table.user.score=Pontua\u00E7\u00E3o
 table.user.trial=Tentativa
+assessment.docs=Documentos de avalia\u00E7\u00E3o
diff --git a/src/main/java/org/olat/modules/lecture/ui/ParticipantListSortDelegate.java b/src/main/java/org/olat/modules/lecture/ui/ParticipantListSortDelegate.java
index f40df7ce02e..fe9bc385401 100644
--- a/src/main/java/org/olat/modules/lecture/ui/ParticipantListSortDelegate.java
+++ b/src/main/java/org/olat/modules/lecture/ui/ParticipantListSortDelegate.java
@@ -1,3 +1,22 @@
+/**
+ * <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.modules.lecture.ui;
 
 import java.util.Locale;
-- 
GitLab