From 42c539ce1c239f918bdf06b17ebd9bd0b0e59a18 Mon Sep 17 00:00:00 2001 From: srosse <stephane.rosse@frentix.com> Date: Fri, 11 Sep 2020 07:27:07 +0200 Subject: [PATCH] OO-4893: upload assessment docs to items in correction workflow --- .../java/org/olat/ims/qti21/QTI21Service.java | 13 ++ .../ims/qti21/manager/QTI21ServiceImpl.java | 12 ++ .../QTI21ResultsExportMediaResource.java | 8 +- .../qti21/ui/AssessmentResultController.java | 56 ++++++- .../olat/ims/qti21/ui/ResourcesMapper.java | 111 +++++++++---- .../qti21/ui/_content/assessment_results.html | 11 +- .../qti21/ui/_i18n/LocalStrings_de.properties | 4 + .../qti21/ui/_i18n/LocalStrings_en.properties | 4 + .../qti21/ui/_i18n/LocalStrings_fr.properties | 2 + ...rectionIdentityInteractionsController.java | 148 +++++++++++++++++- .../_content/item_assessment_docs.html | 7 + 11 files changed, 335 insertions(+), 41 deletions(-) create mode 100644 src/main/java/org/olat/ims/qti21/ui/assessment/_content/item_assessment_docs.html diff --git a/src/main/java/org/olat/ims/qti21/QTI21Service.java b/src/main/java/org/olat/ims/qti21/QTI21Service.java index a37be98ddba..45d74e2bc04 100644 --- a/src/main/java/org/olat/ims/qti21/QTI21Service.java +++ b/src/main/java/org/olat/ims/qti21/QTI21Service.java @@ -520,6 +520,19 @@ public interface QTI21Service { public File importFileSubmission(AssessmentTestSession candidateSession, String filename, byte[] data); + + public File getAssessmentDocumentsDirectory(AssessmentTestSession candidateSession); + + /** + * The method allow to prevent creation of a lot of empty directories. + * + * @param candidateSession The assessment test session + * @param itemSession The assessment item session + * @param createDirectory Create a directory, if false and the directory doesn't exist, return null + * @return The directory or null if @createDirectory is false and the directory doesn't exist + */ + public File getAssessmentDocumentsDirectory(AssessmentTestSession candidateSession, AssessmentItemSession itemSession); + /** * Returns the sum of the correction time set in metadata of the test. Only * the items proposed to the assessed user are counted (with or without response diff --git a/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java index 89b6d444854..c0504c3613c 100644 --- a/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java +++ b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java @@ -1516,6 +1516,18 @@ public class QTI21ServiceImpl implements QTI21Service, UserDataDeletable, Initia auditLogger.logCandidateOutcomes(candidateSession, outcomes); } } + + @Override + public File getAssessmentDocumentsDirectory(AssessmentTestSession candidateSession) { + File myStore = testSessionDao.getSessionStorage(candidateSession); + return new File(myStore, "assessmentdocs"); + } + + @Override + public File getAssessmentDocumentsDirectory(AssessmentTestSession candidateSession, AssessmentItemSession itemSession) { + File assessmentDocsDir = getAssessmentDocumentsDirectory(candidateSession); + return new File(assessmentDocsDir, itemSession.getKey().toString()); + } @Override public File getSubmissionDirectory(AssessmentTestSession candidateSession) { 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 7bd4e0b8a51..c704335c285 100644 --- a/src/main/java/org/olat/ims/qti21/resultexport/QTI21ResultsExportMediaResource.java +++ b/src/main/java/org/olat/ims/qti21/resultexport/QTI21ResultsExportMediaResource.java @@ -223,7 +223,7 @@ public class QTI21ResultsExportMediaResource implements MediaResource { qaf.exportCourseElement(exportFolderName + "/" + label, zout); } - private List<ResultDetail> createResultDetail (Identity identity, ZipOutputStream zout, String idDir) throws IOException { + private List<ResultDetail> createResultDetail(Identity identity, ZipOutputStream zout, String idDir) throws IOException { List<ResultDetail> assessments = new ArrayList<>(); List<AssessmentTestSession> sessions = qtiService.getAssessmentTestSessions(entry, courseNode.getIdent(), identity, true); for (AssessmentTestSession session : sessions) { @@ -267,6 +267,12 @@ public class QTI21ResultsExportMediaResource implements MediaResource { File submissionDir = qtiService.getSubmissionDirectory(session); String baseDir = idPath + "submissions"; ZipUtil.addDirectoryToZip(submissionDir.toPath(), baseDir, zout); + + File assessmentDocsDir = qtiService.getAssessmentDocumentsDirectory(session); + if(assessmentDocsDir.exists()) { + String assessmentDocsBaseDir = idPath + "assessmentdocs"; + ZipUtil.addDirectoryToZip(assessmentDocsDir.toPath(), assessmentDocsBaseDir, zout); + } } return assessments; } diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentResultController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentResultController.java index d407bc46737..797b286bc6b 100644 --- a/src/main/java/org/olat/ims/qti21/ui/AssessmentResultController.java +++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentResultController.java @@ -51,6 +51,7 @@ import org.olat.core.gui.media.NotFoundMediaResource; import org.olat.core.id.Identity; import org.olat.core.util.CodeHelper; import org.olat.core.util.StringHelper; +import org.olat.core.util.io.SystemFileFilter; import org.olat.course.CourseFactory; import org.olat.course.ICourse; import org.olat.course.assessment.AssessmentHelper; @@ -215,6 +216,9 @@ public class AssessmentResultController extends FormBasicController { if(formLayout instanceof FormLayoutContainer) { FormLayoutContainer layoutCont = (FormLayoutContainer)formLayout; layoutCont.contextPut("options", options); + layoutCont.contextPut("mapperUri", mapperUri); + // mapperUri is the default + layoutCont.contextPut("submissionMapperUri", submissionMapperUri == null ? mapperUri : submissionMapperUri); if(assessedIdentity != null) { layoutCont.contextPut("userDisplayName", userMgr.getUserDisplayName(assessedIdentity.getKey())); } else { @@ -234,6 +238,7 @@ public class AssessmentResultController extends FormBasicController { } else { if (candidateSession != null) { + layoutCont.contextPut("candidateSessionKey", candidateSession.getKey()); // Add some meta information about the context of this assessment RepositoryEntry contextRE = candidateSession.getRepositoryEntry(); RepositoryEntry testRE = candidateSession.getTestEntry(); @@ -249,9 +254,8 @@ public class AssessmentResultController extends FormBasicController { if (testRE != null) { layoutCont.contextPut("testTitle", testRE.getDisplayname()); - layoutCont.contextPut("testId", testRE.getResourceableId()); - layoutCont.contextPut("testExternalRef", testRE.getExternalRef()); - + layoutCont.contextPut("testId", testRE.getResourceableId()); + layoutCont.contextPut("testExternalRef", testRE.getExternalRef()); } } @@ -418,6 +422,18 @@ public class AssessmentResultController extends FormBasicController { r.setManualScore(itemSession.getManualScore()); } r.setComment(itemSession.getCoachComment()); + r.setItemSessionKey(itemSession.getKey()); + + File assessmentDocsDir = qtiService.getAssessmentDocumentsDirectory(candidateSession, itemSession); + if(assessmentDocsDir != null && assessmentDocsDir.exists()) { + File[] assessmentDocs = assessmentDocsDir.listFiles(SystemFileFilter.FILES_ONLY); + if(assessmentDocs != null) { + for(File assessmentDoc:assessmentDocs) { + DocumentWrapper dw = new DocumentWrapper(assessmentDoc.getName()); + r.getAssessmentDocuments().add(dw); + } + } + } } //update max score of section @@ -561,7 +577,7 @@ public class AssessmentResultController extends FormBasicController { } private void doPrint(UserRequest ureq) { - ControllerCreator creator = getResultControllerCtreator(); + ControllerCreator creator = getResultControllerCreator(); openInNewBrowserWindow(ureq, creator); } @@ -607,7 +623,7 @@ public class AssessmentResultController extends FormBasicController { return filename; } - private ControllerCreator getResultControllerCtreator() { + private ControllerCreator getResultControllerCreator() { return (uureq, wwControl) -> { AssessmentResultController printViewCtrl = new AssessmentResultController(uureq, wwControl, assessedIdentity, anonym, candidateSession, fUnzippedDirRoot, mapperUri, submissionMapperUri, options, false, true, false); @@ -658,6 +674,8 @@ public class AssessmentResultController extends FormBasicController { private SessionStatus sessionStatus; + private Long itemSessionKey; + private int numberOfSections = 0; private int numberOfQuestions = 0; private int numberOfAnsweredQuestions = 0; @@ -669,6 +687,7 @@ public class AssessmentResultController extends FormBasicController { private final List<FlowFormItem> rubrics; private InteractionResults interactionResults; private final List<Results> subResults = new ArrayList<>(); + private final List<DocumentWrapper> assessmentDocuments = new ArrayList<>(1); public Results(boolean deleted, String title) { this.deleted = deleted; @@ -700,7 +719,15 @@ public class AssessmentResultController extends FormBasicController { } public String getItemIdentifier() { - return this.itemIdentifier; + return itemIdentifier; + } + + public Long getItemSessionKey() { + return itemSessionKey; + } + + public void setItemSessionKey(Long itemSessionKey) { + this.itemSessionKey = itemSessionKey; } public void setSessionState(ControlObjectSessionState sessionState) { @@ -980,6 +1007,23 @@ public class AssessmentResultController extends FormBasicController { public void setInteractionResults(InteractionResults interactionResults) { this.interactionResults = interactionResults; } + + public List<DocumentWrapper> getAssessmentDocuments() { + return assessmentDocuments; + } + } + + public class DocumentWrapper { + + private final String filename; + + public DocumentWrapper(String filename) { + this.filename = filename; + } + + public String getFilename() { + return filename; + } } public class SignatureMapper implements Mapper { diff --git a/src/main/java/org/olat/ims/qti21/ui/ResourcesMapper.java b/src/main/java/org/olat/ims/qti21/ui/ResourcesMapper.java index 2a6a8a2c5ee..274c4c97f31 100644 --- a/src/main/java/org/olat/ims/qti21/ui/ResourcesMapper.java +++ b/src/main/java/org/olat/ims/qti21/ui/ResourcesMapper.java @@ -45,6 +45,7 @@ public class ResourcesMapper implements Mapper { private static final Logger log = Tracing.createLoggerFor(ResourcesMapper.class); private static final String SUBMISSION_SUBPATH = "submissions/"; + private static final String ASSESSMENTDOCS_SUBPATH = "assessmentdocs/"; private final URI assessmentObjectUri; private final File submissionDirectory; @@ -97,38 +98,10 @@ public class ResourcesMapper implements Mapper { String realPath = request.getServletContext().getRealPath("/static/images/transparent.gif"); resource = new FileMediaResource(new File(realPath), true); } else { - String submissionName = null; - File storage = null; if(filename != null && filename.contains(SUBMISSION_SUBPATH)) { - int submissionIndex = filename.indexOf(SUBMISSION_SUBPATH) + SUBMISSION_SUBPATH.length(); - String submission = filename.substring(submissionIndex); - int candidateSessionIndex = submission.indexOf('/'); - if(candidateSessionIndex > 0) { - submissionName = submission.substring(candidateSessionIndex + 1); - if(submissionDirectory != null) { - storage = submissionDirectory; - } else if(submissionDirectoryMaps != null) { - String sessionKey = submission.substring(0, candidateSessionIndex); - if(StringHelper.isLong(sessionKey)) { - try { - storage = submissionDirectoryMaps.get(Long.valueOf(sessionKey)); - } catch (Exception e) { - log.error("", e); - } - } - } - } - } - - if(storage != null && StringHelper.containsNonWhitespace(submissionName)) { - File submissionFile = new File(storage, submissionName); - if(submissionFile.exists()) { - resource = new FileMediaResource(submissionFile, true); - } else { - resource = new NotFoundMediaResource(); - } - } else { - resource = new NotFoundMediaResource(); + resource = submission(filename); + } else if(filename != null && filename.contains(ASSESSMENTDOCS_SUBPATH)) { + resource = assessmentDocuments(filename); } } } catch (Exception e) { @@ -137,4 +110,80 @@ public class ResourcesMapper implements Mapper { } return resource; } + + private MediaResource submission(String filename) { + int submissionIndex = filename.indexOf(SUBMISSION_SUBPATH) + SUBMISSION_SUBPATH.length(); + String submission = filename.substring(submissionIndex); + int candidateSessionIndex = submission.indexOf('/'); + + File storage = null; + String submissionName = null; + if(candidateSessionIndex > 0) { + submissionName = submission.substring(candidateSessionIndex + 1); + if(submissionDirectory != null) { + storage = submissionDirectory; + } else if(submissionDirectoryMaps != null) { + String sessionKey = submission.substring(0, candidateSessionIndex); + storage = getStorageInMap(sessionKey); + } + } + return toResource(storage, submissionName); + } + + private MediaResource assessmentDocuments(String filename) { + int assessmentIndex = filename.indexOf(ASSESSMENTDOCS_SUBPATH) + ASSESSMENTDOCS_SUBPATH.length(); + String assessment = filename.substring(assessmentIndex); + int testSessionIndex = assessment.indexOf('/'); + if(testSessionIndex < 0) { + return new NotFoundMediaResource(); + } + int itemSessionIndex = assessment.indexOf('/', testSessionIndex + 1); + if(itemSessionIndex < 0 || itemSessionIndex <= testSessionIndex) { + return new NotFoundMediaResource(); + } + + File storage = null; + String submissionName = null; + if(testSessionIndex > 0) { + File submissionDir = null; + if(submissionDirectory != null) { + submissionDir = submissionDirectory; + } else if(submissionDirectoryMaps != null) { + String testSession = assessment.substring(0, testSessionIndex); + submissionDir = getStorageInMap(testSession); + } + + if(submissionDir != null) { + submissionName = assessment.substring(itemSessionIndex + 1); + String itemSession = assessment.substring(testSessionIndex + 1, itemSessionIndex); + storage = new File(submissionDir.getParentFile(), "assessmentdocs"); + storage = new File(storage, itemSession); + } + } + + return toResource(storage, submissionName); + } + + private MediaResource toResource(File storage, String filename) { + if(storage != null && StringHelper.containsNonWhitespace(filename)) { + File submissionFile = new File(storage, filename); + if(submissionFile.exists()) { + return new FileMediaResource(submissionFile, true); + } + } + return new NotFoundMediaResource(); + } + + private File getStorageInMap(String sessionKey) { + File storage = null; + if(StringHelper.isLong(sessionKey)) { + try { + storage = submissionDirectoryMaps.get(Long.valueOf(sessionKey)); + } catch (Exception e) { + log.error("", e); + + } + } + return storage; + } } \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/ui/_content/assessment_results.html b/src/main/java/org/olat/ims/qti21/ui/_content/assessment_results.html index eb2979d8f7c..eb5c2973fdb 100644 --- a/src/main/java/org/olat/ims/qti21/ui/_content/assessment_results.html +++ b/src/main/java/org/olat/ims/qti21/ui/_content/assessment_results.html @@ -467,7 +467,7 @@ <i class="o_icon o_icon_passed"></i> $r.translate("passed.yes") #elseif(!${itemResult.getPass().booleanValue()}) - <i class="o_icon o_icon_failed"></i> + <i class="o_icon o_icon_failed"> </i> $r.translate("passed.no") #else $r.translateWithPackage("org.olat.course.nodes.st", "passed.noinfo") @@ -475,6 +475,15 @@ </td> </tr> #end + #if($r.isNotEmpty(${itemResult.getAssessmentDocuments()})) + <tr> + <th scope="row">$r.translate("assessment.item.docs")</th> + <td><ul class="list-unstyled">#foreach($doc in ${itemResult.getAssessmentDocuments()}) + <li><a href="$submissionMapperUri/assessmentdocs/${itemResult.itemSessionKey}/${doc.filename}?href=assessmentdocs/${candidateSessionKey}/${itemResult.itemSessionKey}/${doc.filename}" target="_blank"><i class="o_icon o_icon-fw $r.getFiletypeIconCss($doc.filename)"> </i> $r.escapeHtml($doc.filename)</a></li> + #end</ul> + </td> + </tr> + #end </tbody></table> #end ## END #if($itemResult.metadataVisible) diff --git a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_de.properties index e0831e8678f..be8fb77984c 100644 --- a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_de.properties @@ -19,6 +19,8 @@ archive.table.header.section=Sektion "{0}" archive.table.header.test=Test assessment.comment.legend=Pers\u00F6nliche Notizen assessment.comment.legend.help=Nutzen Sie die Textbox f\u00FCr pers\u00F6nliche Notizen und Kommentare. Diese Notizen sind nur f\u00FCr Sie sichtbar und werden nicht in der Pr\u00FCfung ber\u00FCcksichtigt. +assessment.item.docs=Bewertungsdokument +assessment.item.docs.upload=Bewertungsdokument assessment.item.mark=F\u00FCgen Sie eine private Markierung hinzu als Erinnerung um diese Frage sp\u00E4ter noch einmal anzuschauen assessment.item.point={0} Punkt assessment.item.points={0} Punkte @@ -295,6 +297,8 @@ upload.explanation=Datei auf lokalem Computer f\u00FCr \u00DCbertragung w\u00E4h validate.xml.signature=Testquittung validieren validate.xml.signature.file=XML Datei validate.xml.signature.ok=Testquittung und Datei konnte erfolgreich validiert werden. +warning.assessment.docs.delete.title=Bewertungsdokument l\u00F6schen +warning.assessment.docs.delete.text=Wollen Sie dieses Bewertungsdokument "{0}" wirklich l\u00F6schen? warning.assignment.done=Die Korrektur dieses Tests ist bereits abgeschlossen. Wird diese Test-Session als ung\u00FCltig markiert (annulliert), so gehen alle bestehenden Korrekturen verloren. warning.assignment.inProcess=Dieser Test befindet sich bereits in Korrektur. Wird diese Test-Session als ung\u00FCltig markiert (annulliert), so gehen alle Korrekturen verloren. warning.download.log=Es gibt leider kein Logdatei f\u00FCr diesen Test. diff --git a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_en.properties index a0113150961..96315cb7c7a 100644 --- a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_en.properties @@ -19,6 +19,8 @@ archive.table.header.section=Section "{0}" archive.table.header.test=Test assessment.comment.legend=Personal notes assessment.comment.legend.help=Please use the following text box for any personal notes during this test. These notes are only visible to you and will not be taken in account for the exam. +assessment.item.docs=Assessment documents +assessment.item.docs.upload=Assessment documents assessment.item.mark=Add personal marking as a reminder to review this question assessment.item.point={0} point assessment.item.points={0} points @@ -295,6 +297,8 @@ upload.explanation=Select a file from your computer to upload it validate.xml.signature=Validate test receipt validate.xml.signature.file=XML file validate.xml.signature.ok=Test receipt and results was successfully validated. +warning.assessment.docs.delete.text=Do you really want to delete this assessment document "{0}"? +warning.assessment.docs.delete.title=Delete an assessment document warning.assignment.done=The grading of this test has already been completed. If this test session is marked as invalid, all existing corrections will be lost. warning.assignment.inProcess=The grading of the test has already started. If this test session is marked as invalid, all existing corrections will be lost. warning.download.log=There is not a log file for this test. diff --git a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_fr.properties b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_fr.properties index c0f5931c698..eec9868de6f 100644 --- a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_fr.properties +++ b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_fr.properties @@ -296,6 +296,8 @@ upload.explanation=Choisir un fichier depuis votre ordinateur pour le t\u00E9l\u validate.xml.signature=Valider la signature validate.xml.signature.file=Signature XML validate.xml.signature.ok=La signature et les r\u00E9sultats ont \u00E9t\u00E9 valid\u00E9 avec succ\u00E8s. +warning.assessment.docs.delete.text=Voulez-vous r\u00E9ellement effacer ce document "{0}"? +warning.assessment.docs.delete.title=Effacer le document warning.assignment.done=La correction de ce test est termin\u00E9e. Si cette session de test est marqu\u00E9e comme invalide, les corrections existantes seront perdues. warning.assignment.inProcess=La correction de ce test a d\u00E9j\u00E0 commenc\u00E9. Si cette session de test est marqu\u00E9e comme invalide, toutes les corrections seront perdues. warning.download.log=Il n'y a malheureusement pas de fichier journal pour ce test. diff --git a/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionIdentityInteractionsController.java b/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionIdentityInteractionsController.java index 013f21af421..461902017c3 100644 --- a/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionIdentityInteractionsController.java +++ b/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionIdentityInteractionsController.java @@ -20,8 +20,11 @@ package org.olat.ims.qti21.ui.assessment; import java.io.File; +import java.io.IOException; import java.math.BigDecimal; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -30,6 +33,7 @@ import java.util.Map; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.form.flexible.FormItem; import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.FileElement; import org.olat.core.gui.components.form.flexible.elements.FormLink; import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement; import org.olat.core.gui.components.form.flexible.elements.RichTextElement; @@ -48,10 +52,15 @@ 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.generic.closablewrapper.CloseableCalloutWindowController; +import org.olat.core.gui.control.generic.modal.DialogBoxController; +import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory; import org.olat.core.gui.control.winmgr.JSCommand; import org.olat.core.util.CodeHelper; +import org.olat.core.util.FileUtils; +import org.olat.core.util.Formatter; import org.olat.core.util.StringHelper; import org.olat.core.util.Util; +import org.olat.core.util.io.SystemFileFilter; import org.olat.course.assessment.AssessmentHelper; import org.olat.fileresource.FileResourceManager; import org.olat.fileresource.types.ImsQTI21Resource; @@ -113,11 +122,15 @@ public class CorrectionIdentityInteractionsController extends FormBasicControlle private FormLink viewSolutionButton; private FormLink overrideScoreButton; private FormLink viewCorrectSolutionButton; + private FileElement uploadDocsEl; private ItemBodyResultFormItem solutionItem; private FeedbackResultFormItem correctSolutionItem; private MultipleSelectionElement toReviewEl; private FormLayoutContainer overrideScoreCont; - + private FormLayoutContainer docsLayoutCont; + private FormLayoutContainer scoreCont; + + private DialogBoxController confirmDeleteDocCtrl; private OverrideScoreController overrideScoreCtrl; private CloseableCalloutWindowController overrideScoreCalloutCtrl; @@ -171,6 +184,7 @@ public class CorrectionIdentityInteractionsController extends FormBasicControlle assessmentObjectUri = qtiService.createAssessmentTestUri(fUnzippedDirRoot); initForm(ureq); + reloadAssessmentDocs(); } @Override @@ -225,7 +239,7 @@ public class CorrectionIdentityInteractionsController extends FormBasicControlle coachComment = itemSession.getCoachComment(); } - FormLayoutContainer scoreCont = FormLayoutContainer.createDefaultFormLayout("score.container", getTranslator()); + scoreCont = FormLayoutContainer.createDefaultFormLayout("score.container", getTranslator()); formLayout.add("score.container", scoreCont); statusEl = uifactory.addStaticTextElement("status", "status", "", scoreCont); @@ -268,6 +282,16 @@ public class CorrectionIdentityInteractionsController extends FormBasicControlle IdentityAssessmentItemWrapper wrapper = new IdentityAssessmentItemWrapper(fullname, assessmentItem, correction, responseItems, scoreEl, commentEl, statusEl); + String page = velocity_root + "/item_assessment_docs.html"; + docsLayoutCont = FormLayoutContainer.createCustomFormLayout("assessment.item.docs", getTranslator(), page); + docsLayoutCont.setLabel("assessment.item.docs", null); + docsLayoutCont.contextPut("mapperUri", mapperUri); + scoreCont.add(docsLayoutCont); + + uploadDocsEl = uifactory.addFileElement(getWindowControl(), "assessment.item.docs.upload", "assessment.item.docs.upload", scoreCont); + uploadDocsEl.addActionListener(FormEvent.ONCHANGE); + uploadDocsEl.setVisible(!readOnly); + toReviewEl = uifactory.addCheckboxesHorizontal("to.review", "to.review", scoreCont, onKeys, new String[] { "" }); toReviewEl.setEnabled(!readOnly); if(itemSession != null && itemSession.isToReview()) { @@ -515,6 +539,19 @@ public class CorrectionIdentityInteractionsController extends FormBasicControlle doToggleSolution(); } else if(viewCorrectSolutionButton == source) { doToggleCorrectSolution(); + } else if(uploadDocsEl == source) { + if(uploadDocsEl.getUploadFile() != null && StringHelper.containsNonWhitespace(uploadDocsEl.getUploadFileName())) { + doUploadAssessmentDocument(uploadDocsEl.getUploadFile(), uploadDocsEl.getUploadFileName()); + reloadAssessmentDocs(); + uploadDocsEl.reset(); + } + } else if(source instanceof FormLink) { + FormLink link = (FormLink)source; + Object uobject = link.getUserObject(); + if(link.getCmd() != null && link.getCmd().startsWith("delete_doc_") && uobject instanceof DocumentWrapper) { + DocumentWrapper wrapper = (DocumentWrapper)uobject; + doConfirmDeleteAssessmentDocument(ureq, wrapper.getDocument()); + } } super.formInnerEvent(ureq, source, event); } @@ -530,6 +567,12 @@ public class CorrectionIdentityInteractionsController extends FormBasicControlle } else if(overrideScoreCalloutCtrl == source) { overrideScoreCalloutCtrl.deactivate(); cleanUp(); + } else if(source == confirmDeleteDocCtrl) { + if(DialogBoxUIFactory.isOkEvent(event) || DialogBoxUIFactory.isYesEvent(event)) { + File documentToDelete = (File)confirmDeleteDocCtrl.getUserObject(); + doDeleteAssessmentDocument(documentToDelete); + reloadAssessmentDocs(); + } } super.event(ureq, source, event); } @@ -602,6 +645,76 @@ public class CorrectionIdentityInteractionsController extends FormBasicControlle return Double.parseDouble(scoreStr); } + private void doUploadAssessmentDocument(File uploadedFile, String filename) { + File directory = qtiService.getAssessmentDocumentsDirectory(correction.getTestSession(), correction.getItemSession()); + if(directory != null) { + if(!directory.exists()) { + directory.mkdirs(); + } + + File targetFile = new File(directory, filename); + if(targetFile.exists()) { + String newName = FileUtils.rename(targetFile); + targetFile = new File(directory, newName); + } + try { + Files.copy(uploadedFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + logError("", e); + } + } + } + + private void reloadAssessmentDocs() { + if(docsLayoutCont == null) return; + + List<DocumentWrapper> wrappers = new ArrayList<>(); + File directory = qtiService.getAssessmentDocumentsDirectory(correction.getTestSession(), correction.getItemSession()); + if(directory != null && directory.exists()) { + File[] documents = directory.listFiles(SystemFileFilter.FILES_ONLY); + if(documents != null) { + for (File document : documents) { + DocumentWrapper wrapper = new DocumentWrapper(document); + wrappers.add(wrapper); + + if(!readOnly) { + FormLink deleteButton = uifactory.addFormLink("delete_doc_" + (++count), "delete", null, docsLayoutCont, Link.BUTTON_XSMALL); + deleteButton.setEnabled(true); + deleteButton.setVisible(true); + wrapper.setDeleteButton(deleteButton); + } + } + } + } + + docsLayoutCont.contextPut("documents", wrappers); + docsLayoutCont.contextPut("itemSessionKey", correction.getItemSession().getKey()); + docsLayoutCont.contextPut("testSessionKey", correction.getTestSession().getKey()); + docsLayoutCont.contextPut("documents", wrappers); + docsLayoutCont.setVisible(!wrappers.isEmpty()); + + if(uploadDocsEl != null && uploadDocsEl.isVisible()) { + if(wrappers.isEmpty()) { + uploadDocsEl.setLabel("assessment.item.docs.upload", null); + scoreCont.setDirty(true); + } else { + uploadDocsEl.setLabel(null, null); + } + } + } + + private void doConfirmDeleteAssessmentDocument(UserRequest ureq, File document) { + String title = translate("warning.assessment.docs.delete.title"); + String text = translate("warning.assessment.docs.delete.text", + new String[] { StringHelper.escapeHtml(document.getName()) }); + confirmDeleteDocCtrl = activateOkCancelDialog(ureq, title, text, confirmDeleteDocCtrl); + confirmDeleteDocCtrl.setUserObject(document); + } + + private void doDeleteAssessmentDocument(File document) { + FileUtils.deleteFile(document); + } + private void doToggleSolution() { if(solutionItem.isVisible()) { viewSolutionButton.setIconLeftCSS("o_icon o_icon_open_togglebox"); @@ -703,4 +816,35 @@ public class CorrectionIdentityInteractionsController extends FormBasicControlle fireEvent(ureq, Event.CANCELLED_EVENT); } } + + public static class DocumentWrapper { + + private final File document; + private FormLink deleteButton; + + public DocumentWrapper(File document) { + this.document = document; + } + + public String getFilename() { + return document.getName(); + } + + public String getLabel() { + return document.getName() + " (" + Formatter.formatBytes(document.length()) + ")"; + } + + public File getDocument() { + return document; + } + + public FormLink getDeleteButton() { + return deleteButton; + } + + public void setDeleteButton(FormLink deleteButton) { + this.deleteButton = deleteButton; + deleteButton.setUserObject(this); + } + } } diff --git a/src/main/java/org/olat/ims/qti21/ui/assessment/_content/item_assessment_docs.html b/src/main/java/org/olat/ims/qti21/ui/assessment/_content/item_assessment_docs.html new file mode 100644 index 00000000000..03f05754be0 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/assessment/_content/item_assessment_docs.html @@ -0,0 +1,7 @@ +#if($r.isNotEmpty($documents)) +<ul class="form-static list-unstyled o_assessment_docs""> + #foreach($document in $documents) + <li><a href="$mapperUri/assessmentdocs/$r.encodeUrl($document.filename)?href=assessmentdocs/$testSessionKey/$itemSessionKey/$r.encodeUrl($document.filename)" target="_blank"><i class="o_icon o_icon-fw $r.getFiletypeIconCss($document.filename)"> </i> $r.escapeHtml($document.label)</a> #if($r.visible($document.deleteButton))$r.render($document.deleteButton)#end</li> + #end +</ul> +#end \ No newline at end of file -- GitLab