diff --git a/src/main/java/org/olat/core/gui/util/SyntheticUserRequest.java b/src/main/java/org/olat/core/gui/util/SyntheticUserRequest.java index 43b931a02834c66f551c6e266474182624d013c7..f5ba39992824304366f14be996df4ccc1a3cf6b0 100644 --- a/src/main/java/org/olat/core/gui/util/SyntheticUserRequest.java +++ b/src/main/java/org/olat/core/gui/util/SyntheticUserRequest.java @@ -43,6 +43,7 @@ public class SyntheticUserRequest implements UserRequest { private final Locale locale; private final Identity identity; private final Date requestTimestamp; + private UserSession userSession = null; public SyntheticUserRequest(Identity identity, Locale locale) { this.identity = identity; @@ -50,6 +51,13 @@ public class SyntheticUserRequest implements UserRequest { requestTimestamp = new Date(); } + public SyntheticUserRequest(Identity identity, Locale locale, UserSession userSession) { + this.identity = identity; + this.locale = locale; + this.userSession = userSession; + this.requestTimestamp = new Date(); + } + @Override public String getUuid() { return null; @@ -82,7 +90,7 @@ public class SyntheticUserRequest implements UserRequest { @Override public UserSession getUserSession() { - return null; + return userSession; } @Override diff --git a/src/main/java/org/olat/course/archiver/ScoreAccountingArchiveController.java b/src/main/java/org/olat/course/archiver/ScoreAccountingArchiveController.java index 688c73894a7c26fb7fe85e18c3a79977bf41f048..89dbb46493a7f02b5c26f9b9d4a9ff922ef1860d 100644 --- a/src/main/java/org/olat/course/archiver/ScoreAccountingArchiveController.java +++ b/src/main/java/org/olat/course/archiver/ScoreAccountingArchiveController.java @@ -45,6 +45,7 @@ import org.olat.core.util.ExportUtil; import org.olat.course.CourseFactory; import org.olat.course.ICourse; import org.olat.course.nodes.AssessableCourseNode; +import org.olat.course.nodes.CourseNode; import org.olat.user.UserManager; /** @@ -60,19 +61,24 @@ public class ScoreAccountingArchiveController extends BasicController { private VelocityContainer myContent; private VelocityContainer vcFeedback; private Link startButton, downloadButton; + + private GenericArchiveController genericArchiveController; /** * Constructor for the score accounting archive controller * @param ureq * @param course */ - public ScoreAccountingArchiveController(UserRequest ureq, WindowControl wControl, OLATResourceable ores) { + public ScoreAccountingArchiveController(UserRequest ureq, WindowControl wControl, + OLATResourceable ores, CourseNode... nodeTypes) { super(ureq, wControl); this.ores = ores; myPanel = putInitialPanel(myPanel); myContent = createVelocityContainer("start"); startButton = LinkFactory.createButtonSmall("cmd.start", myContent, this); + genericArchiveController = new GenericArchiveController(ureq, wControl, ores, nodeTypes); + myContent.put("genericarchive", genericArchiveController.getInitialComponent()); myPanel.setContent(myContent); } @@ -114,6 +120,7 @@ public class ScoreAccountingArchiveController extends BasicController { vcFeedback.contextPut("body", translate("course.res.feedback", new String[] { downloadFile.getName() })); downloadButton = LinkFactory.createButtonSmall("cmd.download", vcFeedback, this); downloadButton.setUserObject(downloadFile); + vcFeedback.put("genericarchive", genericArchiveController.getInitialComponent()); myPanel.setContent(vcFeedback); } diff --git a/src/main/java/org/olat/course/archiver/_content/feedback.html b/src/main/java/org/olat/course/archiver/_content/feedback.html index b18e2be1519c192fffe332a3a3335536730911ad..48f7962f3ac580d5260e50528eed2bbc0ace607a 100644 --- a/src/main/java/org/olat/course/archiver/_content/feedback.html +++ b/src/main/java/org/olat/course/archiver/_content/feedback.html @@ -3,5 +3,7 @@ $body </p> <p>$r.render("cmd.download")</p> - +<div class="o_block_bottom"> + $r.render("genericarchive") +</div> diff --git a/src/main/java/org/olat/course/archiver/_content/start.html b/src/main/java/org/olat/course/archiver/_content/start.html index 2795af9092a9bd6584ecf6729f782f7b2d60216d..d0afe420424aa583a4566687ba797da588291a82 100644 --- a/src/main/java/org/olat/course/archiver/_content/start.html +++ b/src/main/java/org/olat/course/archiver/_content/start.html @@ -5,3 +5,6 @@ <p> $r.render("cmd.start") </p> +<div class="o_block_bottom"> + $r.render("genericarchive") +</div> \ No newline at end of file diff --git a/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java b/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java index e1f3f1128b92c42ee8c47ad06e6b35d959159a10..6aebf90f17849ba79ce4656827fc13b4db3abc74 100644 --- a/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java +++ b/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java @@ -28,10 +28,14 @@ package org.olat.course.nodes; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; import java.util.zip.ZipOutputStream; +import org.olat.basesecurity.GroupRoles; import org.olat.core.CoreSpringFactory; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.stack.BreadcrumbPanel; @@ -40,7 +44,9 @@ import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.generic.messages.MessageUIFactory; import org.olat.core.gui.control.generic.tabbable.TabbableController; +import org.olat.core.gui.media.MediaResource; import org.olat.core.gui.translator.Translator; +import org.olat.core.gui.util.SyntheticUserRequest; import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.id.Roles; @@ -48,6 +54,7 @@ import org.olat.core.logging.DBRuntimeException; import org.olat.core.logging.KnownIssueException; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; +import org.olat.core.util.UserSession; import org.olat.core.util.Util; import org.olat.core.util.coordinate.CoordinatorManager; import org.olat.core.util.resource.OresHelper; @@ -72,6 +79,8 @@ import org.olat.course.run.userview.UserCourseEnvironment; import org.olat.course.statistic.StatisticResourceOption; import org.olat.course.statistic.StatisticResourceResult; import org.olat.fileresource.types.ImsQTI21Resource; +import org.olat.group.BusinessGroup; +import org.olat.group.BusinessGroupService; import org.olat.ims.qti.QTI12ResultDetailsController; import org.olat.ims.qti.QTIResultManager; import org.olat.ims.qti.QTIResultSet; @@ -82,7 +91,9 @@ import org.olat.ims.qti.fileresource.TestFileResource; import org.olat.ims.qti.process.AssessmentInstance; import org.olat.ims.qti.process.FilePersister; import org.olat.ims.qti.resultexport.QTI12ExportResultsReportController; +import org.olat.ims.qti.resultexport.QTI12ResultsExportMediaResource; import org.olat.ims.qti.resultexport.QTI21ExportResultsReportController; +import org.olat.ims.qti.resultexport.QTI21ResultsExportMediaResource; import org.olat.ims.qti.statistics.QTIStatisticResourceResult; import org.olat.ims.qti.statistics.QTIStatisticSearchParams; import org.olat.ims.qti.statistics.QTIType; @@ -629,6 +640,21 @@ public class IQTESTCourseNode extends AbstractAccessableCourseNode implements Pe String repositorySoftKey = (String)getModuleConfiguration().get(IQEditController.CONFIG_KEY_REPOSITORY_SOFTKEY); Long courseResourceableId = course.getResourceableId(); + // 1) prepare result export + List<BusinessGroup> groups = course.getCourseEnvironment().getCourseGroupManager().getAllBusinessGroups(); + Set<Identity> ids = new HashSet<>(); + BusinessGroupService groupService = CoreSpringFactory.getImpl(BusinessGroupService.class); + for (BusinessGroup businessGroup : groups) { + ids.addAll(groupService.getMembers(businessGroup, GroupRoles.participant.toString())); + } + List<Identity> identities = ids.stream().collect(Collectors.toList()); + //create SyntheticUserRequest with UserSession to avoid Nullpointer in AssessmentResultController + UserRequest ureq = new SyntheticUserRequest(identities.get(0), locale, new UserSession()); + Roles roles = new Roles(false, false, false, false, false, false, false); + ureq.getUserSession().setRoles(roles); + CourseEnvironment courseEnv = course.getCourseEnvironment(); + MediaResource resource; + try { RepositoryEntry re = RepositoryManager.getInstance().lookupRepositoryEntryBySoftkey(repositorySoftKey, true); boolean onyx = OnyxModule.isOnyxTest(re.getOlatResource()); @@ -640,11 +666,19 @@ public class IQTESTCourseNode extends AbstractAccessableCourseNode implements Pe } return true; } else if(ImsQTI21Resource.TYPE_NAME.equals(re.getOlatResource().getResourceableTypeName())) { + // 2a) create export resource + QTI21Service qtiService = CoreSpringFactory.getImpl(QTI21Service.class); + resource = new QTI21ResultsExportMediaResource(courseEnv, identities, this, + qtiService, ureq, exportStream, locale); + // excel results QTI21ArchiveFormat qaf = new QTI21ArchiveFormat(locale, true, true, true); RepositoryEntry courseEntry = course.getCourseEnvironment().getCourseGroupManager().getCourseEntry(); qaf.export(courseEntry, getIdent(), re, exportStream); return true; } else { + // 2b) create export resource + resource = new QTI12ResultsExportMediaResource(courseEnv, ureq, identities, this, exportStream); + // excel results String shortTitle = getShortTitle(); QTIExportManager qem = QTIExportManager.getInstance(); QTIExportFormatter qef = new QTIExportFormatterCSVType1(locale, "\t", "\"", "\r\n", false); diff --git a/src/main/java/org/olat/ims/qti/resultexport/QTI12ResultsExportMediaResource.java b/src/main/java/org/olat/ims/qti/resultexport/QTI12ResultsExportMediaResource.java index 02da1d5bf1f7f731440559f399f36598fe60c0e8..5a158fbdf6e74b9078cea23eb2470a7e0d7a3065 100644 --- a/src/main/java/org/olat/ims/qti/resultexport/QTI12ResultsExportMediaResource.java +++ b/src/main/java/org/olat/ims/qti/resultexport/QTI12ResultsExportMediaResource.java @@ -67,7 +67,7 @@ public class QTI12ResultsExportMediaResource implements MediaResource { private static final OLog log = Tracing.createLoggerFor(QTI12ExportResultsReportController.class); - private static final String DATA = "export/userdata/"; + private static final String DATA = "userdata/"; private static final String SEP = File.separator; private static final SimpleDateFormat assessmentDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static final SimpleDateFormat displayDateFormat = new SimpleDateFormat("HH:mm:ss"); @@ -82,6 +82,7 @@ public class QTI12ResultsExportMediaResource implements MediaResource { private QTICourseNode courseNode; private CourseEnvironment courseEnv; private UserRequest ureq; + private ZipOutputStream exportStream; private String title; private Translator translator; @@ -100,6 +101,25 @@ public class QTI12ResultsExportMediaResource implements MediaResource { qtiResultManager = QTIResultManager.getInstance(); } + + public QTI12ResultsExportMediaResource(CourseEnvironment courseEnv, UserRequest ureq, List<Identity> identities, + QTICourseNode courseNode, ZipOutputStream exportStream) { + this.courseNode = courseNode; + this.courseEnv = courseEnv; + this.ureq = ureq; + this.title = "qti12export"; + this.identities = identities; + this.velocityHelper = VelocityHelper.getInstance(); + + + this.exportStream = exportStream; + + translator = new PackageTranslator(QTI12ResultsExportMediaResource.class.getPackage().getName(), ureq.getLocale()); + + qtiResultManager = QTIResultManager.getInstance(); + + prepare(null); + } @Override public boolean acceptRanges() { @@ -128,25 +148,28 @@ public class QTI12ResultsExportMediaResource implements MediaResource { @Override public void prepare(HttpServletResponse hres) { + String exportFolderName = translator.translate("export.folder.name"); - String label = StringHelper.transformDisplayNameToFileSystemName(title); - if (label != null && !label.toLowerCase().endsWith(".zip")) { - label += ".zip"; - } - - String urlEncodedLabel = StringHelper.urlEncodeUTF8(label); - hres.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + urlEncodedLabel); - hres.setHeader("Content-Description", urlEncodedLabel); + if (hres != null) { + String label = StringHelper.transformDisplayNameToFileSystemName(title); + if (label != null && !label.toLowerCase().endsWith(".zip")) { + label += ".zip"; + } - try (ZipOutputStream zout = new ZipOutputStream(hres.getOutputStream())) { + String urlEncodedLabel = StringHelper.urlEncodeUTF8(label); + hres.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + urlEncodedLabel); + hres.setHeader("Content-Description", urlEncodedLabel); + } + try { + ZipOutputStream zout = hres == null ? exportStream : new ZipOutputStream(hres.getOutputStream()); zout.setLevel(9); List<AssessedMember> assessedMembers = new ArrayList<AssessedMember>(); for (Identity identity : identities) { - String idDir = DATA + identity.getName(); + String idDir = exportFolderName + "/" + DATA + identity.getName(); idDir = idDir.endsWith(SEP) ? idDir : idDir + SEP; createZipDirectory(zout, idDir); @@ -171,7 +194,7 @@ public class QTI12ResultsExportMediaResource implements MediaResource { String idPath = idDir + translator.translate("table.user.attempt") + (resultSets.indexOf(qtiResultSet)+1) + SEP; createZipDirectory(zout, idPath); - String linkToHTML = createHTMLfromQTIResultSet(idPath, idDir, zout, ureq, identity, qtiResultSet); + String linkToHTML = createHTMLfromQTIResultSet(idPath, idDir, zout, identity, qtiResultSet); // content of result table ResultDetail resultDetail = new ResultDetail(createLink(String.valueOf(assessmentID), linkToHTML, true), @@ -184,9 +207,9 @@ public class QTI12ResultsExportMediaResource implements MediaResource { } String oneUserHTML = createResultListingHTML(assessments, assessedMember); - convertToZipEntry(zout, DATA + identity.getName() + "/index.html", oneUserHTML); + convertToZipEntry(zout, exportFolderName + "/" + DATA + identity.getName() + "/index.html", oneUserHTML); - String linkToUser = idDir.replace("export/", "") + "index.html"; + String linkToUser = idDir.replace(exportFolderName + "/", "") + "index.html"; //content of assessed members table AssessedMember member = new AssessedMember(); member.setUsername(createLink(identity.getName(), linkToUser, false)); @@ -199,16 +222,18 @@ public class QTI12ResultsExportMediaResource implements MediaResource { //convert velocity template to zip entry String usersHTML = createMemberListingHTML(assessedMembers); - convertToZipEntry(zout, "export/index.html", usersHTML); + convertToZipEntry(zout, exportFolderName + "/index.html", usersHTML); //Copy resource files or file trees to export file tree File sasstheme = new File(WebappHelper.getContextRealPath("/static/offline/qti")); - fsToZip(zout, sasstheme.toPath(), "export/css/offline/qti/"); + fsToZip(zout, sasstheme.toPath(), exportFolderName + "/css/offline/qti/"); File fontawesome = new File(WebappHelper.getContextRealPath("/static/font-awesome")); - fsToZip(zout, fontawesome.toPath(), "export/css/font-awesome/"); + fsToZip(zout, fontawesome.toPath(), exportFolderName + "/css/font-awesome/"); - zout.close(); + if (hres != null) { + zout.close(); + } } catch (Exception e) { log.error("Unknown error while assessment result resource export", e); @@ -283,7 +308,7 @@ public class QTI12ResultsExportMediaResource implements MediaResource { return fDoc; } - private String createHTMLfromQTIResultSet(String idPath, String idDir, ZipOutputStream zout, UserRequest ureq, + private String createHTMLfromQTIResultSet(String idPath, String idDir, ZipOutputStream zout, Identity assessedIdentity, QTIResultSet resultSet) throws IOException { Document doc = FilePersister.retreiveResultsReporting(assessedIdentity, diff --git a/src/main/java/org/olat/ims/qti/resultexport/QTI21ResultsExportMediaResource.java b/src/main/java/org/olat/ims/qti/resultexport/QTI21ResultsExportMediaResource.java index 26a28525ee33d8c5bd3e962175f7c2b1a13e5984..658cf8a4fa0c413ab229e3e2177fd02653078c51 100644 --- a/src/main/java/org/olat/ims/qti/resultexport/QTI21ResultsExportMediaResource.java +++ b/src/main/java/org/olat/ims/qti/resultexport/QTI21ResultsExportMediaResource.java @@ -35,6 +35,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.TimeZone; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -77,7 +78,7 @@ public class QTI21ResultsExportMediaResource implements MediaResource { private static final OLog log = Tracing.createLoggerFor(QTI12ExportResultsReportController.class); - private static final String DATA = "export/userdata/"; + private static final String DATA = "userdata/"; private static final String SEP = File.separator; private static final SimpleDateFormat assessmentDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static final SimpleDateFormat displayDateFormat = new SimpleDateFormat("HH:mm:ss"); @@ -95,6 +96,8 @@ public class QTI21ResultsExportMediaResource implements MediaResource { private RepositoryEntry entry; private UserRequest ureq; + private ZipOutputStream exportStream; + private Locale locale; public QTI21ResultsExportMediaResource(CourseEnvironment courseEnv, List<Identity> identities, @@ -107,6 +110,21 @@ public class QTI21ResultsExportMediaResource implements MediaResource { this.ureq = ureq; this.entry = courseEnv.getCourseGroupManager().getCourseEntry(); } + + public QTI21ResultsExportMediaResource(CourseEnvironment courseEnv, List<Identity> identities, + QTICourseNode courseNode, QTI21Service qtiService, UserRequest ureq, ZipOutputStream exportStream, Locale locale) { + this.title = "qti21export"; + this.courseNode = courseNode; + this.identities = identities; + this.velocityHelper = VelocityHelper.getInstance(); + this.qtiService = qtiService; + this.ureq = ureq; + this.entry = courseEnv.getCourseGroupManager().getCourseEntry(); + this.exportStream = exportStream; + this.locale = locale; + + prepare(null); + } @Override public boolean acceptRanges() { @@ -136,26 +154,28 @@ public class QTI21ResultsExportMediaResource implements MediaResource { @Override public void prepare(HttpServletResponse hres) { //init package translator - translator = Util.createPackageTranslator(QTI21ResultsExportMediaResource.class, hres.getLocale()); + translator = Util.createPackageTranslator(QTI21ResultsExportMediaResource.class, hres == null ? locale: hres.getLocale()); + String exportFolderName = translator.translate("export.folder.name"); + if (hres != null) { + String label = StringHelper.transformDisplayNameToFileSystemName(title); + if (label != null && !label.toLowerCase().endsWith(".zip")) { + label += ".zip"; + } - String label = StringHelper.transformDisplayNameToFileSystemName(title); - if (label != null && !label.toLowerCase().endsWith(".zip")) { - label += ".zip"; + String urlEncodedLabel = StringHelper.urlEncodeUTF8(label); + hres.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + urlEncodedLabel); + hres.setHeader("Content-Description", urlEncodedLabel); } - String urlEncodedLabel = StringHelper.urlEncodeUTF8(label); - hres.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + urlEncodedLabel); - hres.setHeader("Content-Description", urlEncodedLabel); - - try (ZipOutputStream zout = new ZipOutputStream(hres.getOutputStream())) { - + try { + ZipOutputStream zout = hres == null ? exportStream : new ZipOutputStream(hres.getOutputStream()); zout.setLevel(9); List<AssessedMember> assessedMembers = new ArrayList<AssessedMember>(); for (Identity identity : identities) { - String idDir = DATA + identity.getName(); + String idDir = exportFolderName + "/" + DATA + identity.getName(); idDir = idDir.endsWith(SEP) ? idDir : idDir + SEP; createZipDirectory(zout, idDir); @@ -204,9 +224,9 @@ public class QTI21ResultsExportMediaResource implements MediaResource { } String singleUserInfoHTML = createResultListingHTML(assessments, assessedMember); - convertToZipEntry(zout, DATA + identity.getName() + "/index.html", singleUserInfoHTML); + convertToZipEntry(zout, exportFolderName + "/" + DATA + identity.getName() + "/index.html", singleUserInfoHTML); - String linkToUser = idDir.replace("export/", "") + "index.html"; + String linkToUser = idDir.replace(exportFolderName + "/", "") + "index.html"; //content of assessed members table AssessedMember member = new AssessedMember(); member.setUsername(createLink(identity.getName(), linkToUser, false)); @@ -219,16 +239,18 @@ public class QTI21ResultsExportMediaResource implements MediaResource { //convert velocity template to zip entry String membersHTML = createMemberListingHTML(assessedMembers); - convertToZipEntry(zout, "export/index.html", membersHTML); + convertToZipEntry(zout, exportFolderName + "/index.html", membersHTML); //Copy resource files or file trees to export file tree File sasstheme = new File(WebappHelper.getContextRealPath("/static/offline/qti")); - fsToZip(zout, sasstheme.toPath(), "export/css/offline/qti/"); + fsToZip(zout, sasstheme.toPath(), exportFolderName + "/css/offline/qti/"); File fontawesome = new File(WebappHelper.getContextRealPath("/static/font-awesome")); - fsToZip(zout, fontawesome.toPath(), "export/css/font-awesome/"); + fsToZip(zout, fontawesome.toPath(), exportFolderName + "/css/font-awesome/"); - zout.close(); + if (hres != null) { + zout.close(); + } } catch (Exception e) { log.error("Unknown error while assessment result resource export", e); diff --git a/src/main/java/org/olat/ims/qti/resultexport/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti/resultexport/_i18n/LocalStrings_de.properties index 06369e733f66828646d0dd8ba88ddfe754bd44dc..c75a1a2247f6f0675f347dbaef4ebdab3bfcb8fe 100644 --- a/src/main/java/org/olat/ims/qti/resultexport/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/ims/qti/resultexport/_i18n/LocalStrings_de.properties @@ -19,3 +19,4 @@ table.user.email=Email table.user.id=ID table.user.score=Punkte table.user.trial=Versuch +export.folder.name=Resultate \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti/resultexport/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti/resultexport/_i18n/LocalStrings_en.properties index e8eddaed2973b5a539e0672fd8e3eba9fcf4ec27..d65cf956a65603bc39c5116473c2bb29145a2493 100644 --- a/src/main/java/org/olat/ims/qti/resultexport/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/ims/qti/resultexport/_i18n/LocalStrings_en.properties @@ -17,4 +17,5 @@ table.all.lastname=Lastname table.all.tries=Attempts table.user.attempt=Attempt_ table.all.passed=Passed -error.no.assessed.users=No Assessments available \ No newline at end of file +error.no.assessed.users=No Assessments available +export.folder.name=Results \ No newline at end of file