From 327c8756c2de6312a989a0def4697bb5fe9670d6 Mon Sep 17 00:00:00 2001 From: srosse <none@none> Date: Mon, 31 Jul 2017 11:27:38 +0200 Subject: [PATCH] OO-2931: allow images and videos in the rubric of a QTI 2.1 section --- .../model/xml/AssessmentHtmlBuilder.java | 63 ++++++++++++++++++- .../ui/AssessmentTestDisplayController.java | 4 +- .../AssessmentTestComponentRenderer.java | 3 +- .../AssessmentSectionEditorController.java | 15 ++++- ...essmentSectionOptionsEditorController.java | 22 +++++-- .../AssessmentTestComposerController.java | 4 +- 6 files changed, 97 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentHtmlBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentHtmlBuilder.java index 9b19ebdd7da..6f54181f45b 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentHtmlBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentHtmlBuilder.java @@ -21,6 +21,7 @@ package org.olat.ims.qti21.model.xml; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.StringReader; import java.util.ArrayList; import java.util.List; @@ -33,7 +34,7 @@ import org.olat.core.gui.render.StringOutput; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; -import org.olat.core.util.filter.FilterFactory; +import org.olat.core.util.filter.impl.NekoHTMLFilter; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -41,6 +42,7 @@ import org.w3c.dom.Node; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; import uk.ac.ed.ph.jqtiplus.JqtiExtensionManager; import uk.ac.ed.ph.jqtiplus.exception.QtiModelException; @@ -76,7 +78,24 @@ public class AssessmentHtmlBuilder { } public boolean containsSomething(String html) { - return StringHelper.containsNonWhitespace(FilterFactory.getHtmlTagsFilter().filter(html)); + if(!StringHelper.containsNonWhitespace(html)) return false; + + try { + SAXParser parser = new SAXParser(); + ContentDetectionHandler contentHandler = new ContentDetectionHandler(); + parser.setContentHandler(contentHandler); + parser.parse(new InputSource(new StringReader(html))); + return contentHandler.isContentAvailable(); + } catch (SAXException e) { + log.error("", e); + return false; + } catch (IOException e) { + log.error("", e); + return false; + } catch (Exception e) { + log.error("", e); + return false; + } } public String flowStaticString(List<? extends FlowStatic> statics) { @@ -497,4 +516,44 @@ public class AssessmentHtmlBuilder { // } } + + private static class ContentDetectionHandler extends DefaultHandler { + + private boolean collect = false; + private boolean content = false; + + public boolean isContentAvailable() { + return content; + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) { + String elem = localName.toLowerCase(); + if("script".equals(elem)) { + collect = false; + } else if(!NekoHTMLFilter.blockTags.contains(localName)) { + content = true; + } + } + + @Override + public void characters(char[] chars, int offset, int length) { + if(!content && collect && offset >= 0 && length > 0) { + String text = new String(chars, offset, length); + if(text.trim().length() > 0) { + content = true; + } + } + } + + @Override + public void endElement(String uri, String localName, String qName) { + String elem = localName.toLowerCase(); + if("script".equals(elem)) { + collect = true; + } else if(!NekoHTMLFilter.blockTags.contains(localName)) { + content = true; + } + } + } } diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java index 4c57f68c40a..af2d32f20c8 100644 --- a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java +++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java @@ -367,8 +367,8 @@ public class AssessmentTestDisplayController extends BasicController implements private void initQtiWorks(UserRequest ureq) { qtiWorksCtrl = new QtiWorksController(ureq, getWindowControl()); - listenTo(qtiWorksCtrl); - mainVC.put("qtirun", qtiWorksCtrl.getInitialComponent()); + listenTo(qtiWorksCtrl); + mainVC.put("qtirun", qtiWorksCtrl.getInitialComponent()); } @Override diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java index 94438a7f5f9..323a255b914 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java @@ -297,7 +297,7 @@ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRe } if(writeRubrics) { - sb.append("<div class='o_info o_assessmentsection_rubrics'>"); + sb.append("<div class='o_info o_assessmentsection_rubrics clearfix'>"); //write the titles first if(writeTitles) { sb.append("<h4>"); @@ -316,7 +316,6 @@ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRe sb.append("</h4>"); } - for(int i=sectionParentLine.size(); i-->0; ) { AssessmentSection selectedSection = sectionParentLine.get(i); for(RubricBlock rubricBlock:selectedSection.getRubricBlocks()) { diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionEditorController.java index 3289b05ecbb..ddf06f62955 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionEditorController.java @@ -19,6 +19,8 @@ */ package org.olat.ims.qti21.ui.editor; +import java.io.File; + import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.tabbedpane.TabbedPane; @@ -28,6 +30,7 @@ import org.olat.core.gui.control.Event; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.controller.BasicController; import org.olat.core.util.Util; +import org.olat.core.util.vfs.VFSContainer; import org.olat.ims.qti21.ui.AssessmentTestDisplayController; import org.olat.ims.qti21.ui.editor.events.AssessmentSectionEvent; @@ -44,6 +47,10 @@ public class AssessmentSectionEditorController extends BasicController { private final TabbedPane tabbedPane; private final VelocityContainer mainVC; + private final File testFile; + private final File rootDirectory; + private final VFSContainer rootContainer; + private final AssessmentSection section; private final boolean editable; @@ -53,10 +60,14 @@ public class AssessmentSectionEditorController extends BasicController { private AssessmentSectionExpertOptionsEditorController expertOptionsCtrl; public AssessmentSectionEditorController(UserRequest ureq, WindowControl wControl, - AssessmentSection section, boolean restrictedEdit, boolean editable) { + AssessmentSection section, File rootDirectory, VFSContainer rootContainer, File testFile, + boolean restrictedEdit, boolean editable) { super(ureq, wControl, Util.createPackageTranslator(AssessmentTestDisplayController.class, ureq.getLocale())); this.section = section; this.editable = editable; + this.testFile = testFile; + this.rootDirectory = rootDirectory; + this.rootContainer = rootContainer; this.restrictedEdit = restrictedEdit; mainVC = createVelocityContainer("assessment_test_editor"); @@ -71,7 +82,7 @@ public class AssessmentSectionEditorController extends BasicController { } private void initSectionEditor(UserRequest ureq) { - optionsCtrl = new AssessmentSectionOptionsEditorController(ureq, getWindowControl(), section, restrictedEdit, editable); + optionsCtrl = new AssessmentSectionOptionsEditorController(ureq, getWindowControl(), section, rootDirectory, rootContainer, testFile, restrictedEdit, editable); listenTo(optionsCtrl); expertOptionsCtrl = new AssessmentSectionExpertOptionsEditorController(ureq, getWindowControl(), section, restrictedEdit, editable); listenTo(expertOptionsCtrl); diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionOptionsEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionOptionsEditorController.java index a38369ed5fb..9b67a4ecb88 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionOptionsEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentSectionOptionsEditorController.java @@ -19,6 +19,7 @@ */ package org.olat.ims.qti21.ui.editor; +import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -35,6 +36,7 @@ import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.WindowControl; import org.olat.core.util.StringHelper; import org.olat.core.util.Util; +import org.olat.core.util.vfs.VFSContainer; import org.olat.ims.qti21.model.xml.AssessmentHtmlBuilder; import org.olat.ims.qti21.ui.AssessmentTestDisplayController; import org.olat.ims.qti21.ui.editor.events.AssessmentSectionEvent; @@ -57,6 +59,10 @@ public class AssessmentSectionOptionsEditorController extends FormBasicControlle private SingleSelection shuffleEl, randomSelectedEl; private List<RichTextElement> rubricEls = new ArrayList<>(); + private final File testFile; + private final File rootDirectory; + private final VFSContainer rootContainer; + private final AssessmentSection section; private final AssessmentHtmlBuilder htmlBuilder; @@ -66,10 +72,14 @@ public class AssessmentSectionOptionsEditorController extends FormBasicControlle private static final String[] yesnoKeys = new String[]{ "y", "n"}; public AssessmentSectionOptionsEditorController(UserRequest ureq, WindowControl wControl, - AssessmentSection section, boolean restrictedEdit, boolean editable) { + AssessmentSection section, File rootDirectory, VFSContainer rootContainer, File testFile, + boolean restrictedEdit, boolean editable) { super(ureq, wControl, Util.createPackageTranslator(AssessmentTestDisplayController.class, ureq.getLocale())); this.section = section; this.editable = editable; + this.testFile = testFile; + this.rootDirectory = rootDirectory; + this.rootContainer = rootContainer; this.restrictedEdit = restrictedEdit; htmlBuilder = new AssessmentHtmlBuilder(); initForm(ureq); @@ -87,9 +97,11 @@ public class AssessmentSectionOptionsEditorController extends FormBasicControlle titleEl = uifactory.addTextElement("title", "form.metadata.title", 255, title, formLayout); titleEl.setEnabled(editable); titleEl.setMandatory(true); - + + String relativePath = rootDirectory.toPath().relativize(testFile.toPath().getParent()).toString(); + VFSContainer itemContainer = (VFSContainer)rootContainer.resolve(relativePath); if(section.getRubricBlocks().isEmpty()) { - RichTextElement rubricEl = uifactory.addRichTextElementForQTI21("rubric" + counter++, "form.imd.rubric", "", 8, -1, null, + RichTextElement rubricEl = uifactory.addRichTextElementForQTI21("rubric" + counter++, "form.imd.rubric", "", 12, -1, itemContainer, formLayout, ureq.getUserSession(), getWindowControl()); rubricEl.getEditorConfiguration().setFileBrowserUploadRelPath("media"); rubricEl.setEnabled(editable); @@ -97,7 +109,7 @@ public class AssessmentSectionOptionsEditorController extends FormBasicControlle } else { for(RubricBlock rubricBlock:section.getRubricBlocks()) { String rubric = htmlBuilder.blocksString(rubricBlock.getBlocks()); - RichTextElement rubricEl = uifactory.addRichTextElementForQTI21("rubric" + counter++, "form.imd.rubric", rubric, 8, -1, null, + RichTextElement rubricEl = uifactory.addRichTextElementForQTI21("rubric" + counter++, "form.imd.rubric", rubric, 12, -1, itemContainer, formLayout, ureq.getUserSession(), getWindowControl()); rubricEl.getEditorConfiguration().setFileBrowserUploadRelPath("media"); rubricEl.setEnabled(editable); @@ -191,7 +203,7 @@ public class AssessmentSectionOptionsEditorController extends FormBasicControlle //rubrics List<RubricBlock> rubricBlocks = new ArrayList<>(); for(RichTextElement rubricEl:rubricEls) { - String rubric = rubricEl.getValue(); + String rubric = rubricEl.getRawValue(); if(htmlBuilder.containsSomething(rubric)) { RubricBlock rubricBlock = (RubricBlock)rubricEl.getUserObject(); if(rubricBlock == null) { diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java index bd2159fa6e1..3ee92648531 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java @@ -1120,8 +1120,10 @@ public class AssessmentTestComposerController extends MainLayoutBasicController currentEditorCtrl = new AssessmentTestPartEditorController(ureq, getWindowControl(), (TestPart)uobject, restrictedEdit, assessmentTestBuilder.isEditable()); } else if(uobject instanceof AssessmentSection) { + URI testURI = resolvedAssessmentTest.getTestLookup().getSystemId(); + File testFile = new File(testURI); currentEditorCtrl = new AssessmentSectionEditorController(ureq, getWindowControl(), (AssessmentSection)uobject, - restrictedEdit, assessmentTestBuilder.isEditable()); + unzippedDirRoot, unzippedContRoot, testFile, restrictedEdit, assessmentTestBuilder.isEditable()); } else if(uobject instanceof AssessmentItemRef) { AssessmentItemRef itemRef = (AssessmentItemRef)uobject; ResolvedAssessmentItem item = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef); -- GitLab