From 948fbca19778ff4001ad400ae12a5bc140f8b919 Mon Sep 17 00:00:00 2001 From: srosse <none@none> Date: Fri, 1 Jul 2016 08:46:30 +0200 Subject: [PATCH] OO-1593: start work on word export, fix image of alien item in preview --- .../util/openxml/HTMLToOpenXMLHandler.java | 116 ++-- .../core/util/openxml/OpenXMLDocument.java | 47 +- .../util/openxml/OpenXMLDocumentWriter.java | 144 +++-- .../olat/core/util/openxml/OpenXMLUtils.java | 2 +- .../qti21/manager/CorrectResponsesUtil.java | 7 +- .../manager/openxml/QTI21WordExport.java | 531 ++++++++++++++++++ .../model/xml/AssessmentItemFactory.java | 16 + .../model/xml/AssessmentTestBuilder.java | 4 +- .../FIBAssessmentItemBuilder.java | 2 +- .../ui/AssessmentItemDisplayController.java | 5 +- .../AssessmentItemEditorController.java | 4 +- .../AssessmentItemPreviewController.java | 5 +- .../AssessmentTestComposerController.java | 19 +- .../editor/_i18n/LocalStrings_de.properties | 1 + .../editor/_i18n/LocalStrings_en.properties | 1 + .../editor/_i18n/LocalStrings_fr.properties | 1 + 16 files changed, 786 insertions(+), 119 deletions(-) create mode 100644 src/main/java/org/olat/ims/qti21/manager/openxml/QTI21WordExport.java diff --git a/src/main/java/org/olat/core/util/openxml/HTMLToOpenXMLHandler.java b/src/main/java/org/olat/core/util/openxml/HTMLToOpenXMLHandler.java index 47782ba9b0e..e9a9316c7d7 100644 --- a/src/main/java/org/olat/core/util/openxml/HTMLToOpenXMLHandler.java +++ b/src/main/java/org/olat/core/util/openxml/HTMLToOpenXMLHandler.java @@ -19,6 +19,7 @@ */ package org.olat.core.util.openxml; +import java.io.File; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; @@ -54,33 +55,41 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { private StringBuilder textBuffer; private Spacing startSpacing; - private final OpenXMLDocument factory; + protected final OpenXMLDocument factory; - private List<Node> content = new ArrayList<Node>(); - private Deque<StyleStatus> styleStack = new ArrayDeque<StyleStatus>(); + protected List<Node> content = new ArrayList<Node>(); + protected Deque<StyleStatus> styleStack = new ArrayDeque<StyleStatus>(); - private Table currentTable; - private Element currentParagraph; - private ListParagraph currentListParagraph; - private boolean pNeedNewParagraph = true; + protected Table currentTable; + protected Element currentParagraph; + protected ListParagraph currentListParagraph; + protected boolean pNeedNewParagraph = true; - public HTMLToOpenXMLHandler(OpenXMLDocument document, Element paragraph) { + public HTMLToOpenXMLHandler(OpenXMLDocument document) { this.factory = document; + } + + public HTMLToOpenXMLHandler(OpenXMLDocument document, Element paragraph) { + this(document); this.currentParagraph = paragraph; } public HTMLToOpenXMLHandler(OpenXMLDocument document, Spacing spacing) { - this.factory = document; + this(document); this.startSpacing = spacing; } + public void setInitialParagraph(Element paragraph) { + this.currentParagraph = paragraph; + } + /** * Flush the text if a new paragraph is created. Trailing text is flushed * in the previous paragraph. * @param create * @return */ - private Element getCurrentParagraph(boolean create) { + protected Element getCurrentParagraph(boolean create) { if(create || currentParagraph == null) { //flush the text if(textBuffer != null) { @@ -97,7 +106,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { return currentParagraph; } - private Element appendParagraph(Spacing spacing) { + protected Element appendParagraph(Spacing spacing) { //flush the text if(textBuffer != null) { flushText(); @@ -110,7 +119,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { return currentParagraph; } - private Element getCurrentListParagraph(boolean create) { + protected Element getCurrentListParagraph(boolean create) { if(create || currentParagraph == null) { //flush the text if(textBuffer != null) { @@ -122,14 +131,14 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { return currentParagraph; } - private void closeParagraph() { + protected void closeParagraph() { flushText(); currentParagraph = addContent(currentParagraph); textBuffer = null; latex = false; } - private Element addContent(Node element) { + protected Element addContent(Node element) { if(element == null) return null; if(currentTable != null) { @@ -140,7 +149,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { return null; } - private void flushText() { + protected void flushText() { if(textBuffer == null) return; if(latex) { @@ -171,7 +180,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { * Get or create a run on the current paragraph * @return */ - private Element getCurrentRun() { + protected Element getCurrentRun() { Element paragraphEl; if(currentParagraph == null) { Indent indent = getCurrentIndent(); @@ -191,7 +200,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { return (Element)paragraphEl.appendChild(factory.createRunEl(null, runStyle)); } - private Style[] setTextPreferences(String cssStyles) { + protected Style[] setTextPreferences(String cssStyles) { if(cssStyles == null) { return setTextPreferences(); } else { @@ -207,19 +216,19 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { /** * Create a new run with preferences */ - private Style[] setTextPreferences(Style... styles) { + protected Style[] setTextPreferences(Style... styles) { Node runPrefs = getRunForTextPreferences(); factory.createRunPrefsEl(runPrefs, styles); return styles; } - private Style[] unsetTextPreferences(Style... styles) { + protected Style[] unsetTextPreferences(Style... styles) { Node runPrefs = getRunForTextPreferences(); factory.createRunReversePrefsEl(runPrefs, styles); return styles; } - private Node getRunForTextPreferences() { + protected Node getRunForTextPreferences() { Element paragraphEl = getCurrentParagraph(false); Node runPrefs = null; @@ -305,7 +314,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { return null; } - private void setImage(String path) { + protected void setImage(String path) { Element imgEl = factory.createImageEl(path); if(imgEl != null) { PredefinedStyle style = getCurrentPredefinedStyle(); @@ -314,6 +323,42 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { paragrapheEl.appendChild(runEl); } } + + protected void setImage(File file) { + Element imgEl = factory.createImageEl(file); + if(imgEl != null) { + PredefinedStyle style = getCurrentPredefinedStyle(); + Element runEl = factory.createRunEl(Collections.singletonList(imgEl), style); + Element paragrapheEl = getCurrentParagraph(false); + paragrapheEl.appendChild(runEl); + } + } + + protected void startTable() { + closeParagraph(); + currentTable = new Table(); + } + + protected void startCurrentTableRow() { + currentTable.addRowEl(); + } + + protected void closeCurrentTableRow() { + if(currentTable != null) { + currentTable.closeRow(); + } + textBuffer = null; + latex = false; + currentParagraph = null; + } + + protected void endTable() { + if(currentTable != null) { + content.add(currentTable.getTableEl()); + } + currentTable = null; + currentParagraph = null; + } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { @@ -346,10 +391,9 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { String path = attributes.getValue("src"); setImage(path); } else if("table".equalsIgnoreCase(tag)) { - closeParagraph(); - currentTable = new Table(); + startTable(); } else if("tr".equals(tag)) { - currentTable.addRowEl(); + startCurrentTableRow(); } else if("td".equals(tag) || "th".equals(tag)) { int colspan = OpenXMLUtils.getSpanAttribute("colspan", attributes); int rowspan = OpenXMLUtils.getSpanAttribute("rowspan", attributes); @@ -407,21 +451,12 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { unsetTextPreferences(Style.bold); popStyle(tag); } else if("table".equals(tag)) { - if(currentTable != null) { - content.add(currentTable.getTableEl()); - } - currentTable = null; - currentParagraph = null; + endTable(); } else if("td".equals(tag) || "th".equals(tag)) { flushText(); currentParagraph = addContent(currentParagraph); } else if("tr".equals(tag)) { - if(currentTable != null) { - currentTable.closeRow(); - } - textBuffer = null; - latex = false; - currentParagraph = null; + closeCurrentTableRow(); } else if("ul".equals(tag) || "ol".equals(tag)) { closeParagraph(); currentListParagraph = null; @@ -444,7 +479,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { } } - private static class StyleStatus { + public static class StyleStatus { private final String tag; private final Style[] styles; private final boolean quote; @@ -472,7 +507,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { } } - private class Table { + public class Table { private final Element tableEl; private int nextCol; @@ -539,6 +574,13 @@ public class HTMLToOpenXMLHandler extends DefaultHandler { return currentRowEl.appendChild(currentCellEl); } + public Node addCellEl(Element cellEl, int colSpan) { + nextCol += closeCell(nextCol); + currentCellEl = cellEl; + nextCol += (colSpan <= 1 ? 1 : colSpan); + return currentRowEl.appendChild(currentCellEl); + } + public int closeCell(int lastIndex) { for(int i=lastIndex+1; i-->0; ) { Span span = rowSpans[i]; diff --git a/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java b/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java index 4c0e44d0b0e..4bb7244b524 100644 --- a/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java +++ b/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java @@ -285,6 +285,17 @@ public class OpenXMLDocument { getCursor().appendChild(paragraphEl); } + public Element createFillInBlanck(int length) { + Element runEl = createRunEl(null); + runEl.appendChild(createRunPrefsEl(Style.underline)); + + int tabLength = length / 5; + for(int i=tabLength; i-->0; ) { + runEl.appendChild(document.createElement("w:tab")); + } + return runEl; + } + /* <w:p w:rsidR="00F528BA" w:rsidRPr="00245F75" w:rsidRDefault="00F528BA" w:rsidP="00245F75"> <w:pPr> @@ -296,18 +307,23 @@ public class OpenXMLDocument { */ public void appendFillInBlanckWholeLine(int rows) { for(int i=rows+1; i-->0; ) { - Element paragraphEl = createParagraphEl(); - Node pargraphPrefs = paragraphEl.appendChild(document.createElement("w:pPr")); - Node pargraphBottomPrefs = pargraphPrefs.appendChild(document.createElement("w:pBdr")); - Element bottomEl = (Element)pargraphBottomPrefs.appendChild(document.createElement("w:between")); - bottomEl.setAttribute("w:val", "single"); - bottomEl.setAttribute("w:sz", "4"); - bottomEl.setAttribute("w:space", "1"); - bottomEl.setAttribute("w:color", "auto"); + Element paragraphEl = createFillInBlanckWholeLine(); getCursor().appendChild(paragraphEl); } } + public Element createFillInBlanckWholeLine() { + Element paragraphEl = createParagraphEl(); + Node pargraphPrefs = paragraphEl.appendChild(document.createElement("w:pPr")); + Node pargraphBottomPrefs = pargraphPrefs.appendChild(document.createElement("w:pBdr")); + Element bottomEl = (Element)pargraphBottomPrefs.appendChild(document.createElement("w:between")); + bottomEl.setAttribute("w:val", "single"); + bottomEl.setAttribute("w:sz", "4"); + bottomEl.setAttribute("w:space", "1"); + bottomEl.setAttribute("w:color", "auto"); + return paragraphEl; + } + public void appendText(String text, boolean newParagraph, Style... textStyles) { if(!StringHelper.containsNonWhitespace(text)) return; @@ -412,6 +428,21 @@ public class OpenXMLDocument { } } + public void appendHtmlText(String html, boolean newParagraph, HTMLToOpenXMLHandler handler) { + if(!StringHelper.containsNonWhitespace(html)) return; + try { + SAXParser parser = new SAXParser(); + Element paragraphEl = getParagraphToAppendTo(newParagraph); + handler.setInitialParagraph(paragraphEl); + parser.setContentHandler(handler); + parser.parse(new InputSource(new StringReader(html))); + } catch (SAXException e) { + log.error("", e); + } catch (IOException e) { + log.error("", e); + } + } + public Node appendTable(Integer... width) { Element tableEl = createTable(width); return getCursor().appendChild(tableEl); diff --git a/src/main/java/org/olat/core/util/openxml/OpenXMLDocumentWriter.java b/src/main/java/org/olat/core/util/openxml/OpenXMLDocumentWriter.java index 89a3e00e710..49e1a5efc66 100644 --- a/src/main/java/org/olat/core/util/openxml/OpenXMLDocumentWriter.java +++ b/src/main/java/org/olat/core/util/openxml/OpenXMLDocumentWriter.java @@ -28,6 +28,9 @@ import java.util.Collection; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + import org.apache.commons.io.IOUtils; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; @@ -72,32 +75,27 @@ public class OpenXMLDocumentWriter { document.appendPageSettings(); //_rels - ZipEntry rels = new ZipEntry("_rels/.rels"); - out.putNextEntry(rels); + out.putNextEntry(new ZipEntry("_rels/.rels")); createShadowDocumentRelationships(out); out.closeEntry(); //[Content_Types].xml - ZipEntry contentType = new ZipEntry("[Content_Types].xml"); - out.putNextEntry(contentType); + out.putNextEntry(new ZipEntry("[Content_Types].xml")); createContentTypes(document, out); out.closeEntry(); //docProps/app.xml - ZipEntry app = new ZipEntry("docProps/app.xml"); - out.putNextEntry(app); + out.putNextEntry(new ZipEntry("docProps/app.xml")); createDocPropsApp(out); out.closeEntry(); //docProps/core.xml - ZipEntry core = new ZipEntry("docProps/core.xml"); - out.putNextEntry(core); + out.putNextEntry(new ZipEntry("docProps/core.xml")); createDocPropsCore(out); out.closeEntry(); //word/_rels/document.xml.rels - ZipEntry docRels = new ZipEntry("word/_rels/document.xml.rels"); - out.putNextEntry(docRels); + out.putNextEntry(new ZipEntry("word/_rels/document.xml.rels")); createDocumentRelationships(out, document); out.closeEntry(); @@ -199,31 +197,39 @@ public class OpenXMLDocumentWriter { <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml" /> </Relationships> */ - protected void createDocumentRelationships(OutputStream out, OpenXMLDocument document) { + protected void createDocumentRelationships(ZipOutputStream out, OpenXMLDocument document) { try { + XMLStreamWriter writer = OpenXMLUtils.createStreamWriter(out); + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("Relationships"); + writer.writeNamespace("", SCHEMA_RELATIONSHIPS); + Document doc = OpenXMLUtils.createDocument(); Element relationshipsEl = (Element)doc.appendChild(doc.createElement("Relationships")); relationshipsEl.setAttribute("xmlns", SCHEMA_RELATIONSHIPS); addRelationship("rId1", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles", - "styles.xml", relationshipsEl, doc); + "styles.xml", writer); addRelationship("rId2", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering", - "numbering.xml", relationshipsEl, doc); + "numbering.xml", writer); if(document != null) { for(DocReference docRef:document.getImages()) { addRelationship(docRef.getId(), "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - "media/" + docRef.getFilename(), relationshipsEl, doc); + "media/" + docRef.getFilename(), writer); } for(HeaderReference headerRef:document.getHeaders()) { addRelationship(headerRef.getId(), "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header", - headerRef.getFilename(), relationshipsEl, doc); + headerRef.getFilename(), writer); } } - OpenXMLUtils.writeTo(doc, out, false); - } catch (DOMException e) { + writer.writeEndElement();// end Relationships + writer.writeEndDocument(); + writer.flush(); + writer.close(); + } catch (XMLStreamException e) { log.error("", e); } } @@ -236,30 +242,36 @@ public class OpenXMLDocumentWriter { <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/> </Relationships> */ - protected void createShadowDocumentRelationships(OutputStream out) { + protected void createShadowDocumentRelationships(ZipOutputStream zout) { try { - Document doc = OpenXMLUtils.createDocument(); - Element relationshipsEl = (Element)doc.appendChild(doc.createElement("Relationships")); - relationshipsEl.setAttribute("xmlns", SCHEMA_RELATIONSHIPS); + XMLStreamWriter writer = OpenXMLUtils.createStreamWriter(zout); + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("Relationships"); + writer.writeNamespace("", SCHEMA_RELATIONSHIPS); addRelationship("rId1", "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties", - "docProps/core.xml", relationshipsEl, doc); + "docProps/core.xml", writer); addRelationship("rId2", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties", - "docProps/app.xml", relationshipsEl, doc); + "docProps/app.xml", writer); addRelationship("rId3", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", - "word/document.xml", relationshipsEl, doc); + "word/document.xml", writer); - OpenXMLUtils.writeTo(doc, out, false); - } catch (DOMException e) { + writer.writeEndElement();// end Relationships + writer.writeEndDocument(); + writer.flush(); + writer.close(); + } catch (XMLStreamException e) { log.error("", e); } } - private final void addRelationship(String id, String type, String target, Element propertiesEl, Document doc) { - Element relEl = (Element)propertiesEl.appendChild(doc.createElement("Relationship")); - relEl.setAttribute("Id", id); - relEl.setAttribute("Type", type); - relEl.setAttribute("Target", target); + private final void addRelationship(String id, String type, String target, XMLStreamWriter writer) + throws XMLStreamException { + writer.writeStartElement("Relationship"); + writer.writeAttribute("Id", id); + writer.writeAttribute("Type", type); + writer.writeAttribute("Target", target); + writer.writeEndElement(); } /* @@ -333,41 +345,51 @@ public class OpenXMLDocumentWriter { <Override ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml" PartName="/word/styles.xml" /> </Types> */ - protected void createContentTypes(OpenXMLDocument document, OutputStream out) { - Document doc = OpenXMLUtils.createDocument(); - Element typesEl = (Element)doc.appendChild(doc.createElement("Types")); - typesEl.setAttribute("xmlns", SCHEMA_CONTENT_TYPES); - //Default - createContentTypesDefault("rels", CT_RELATIONSHIP, typesEl, doc); - createContentTypesDefault("xml", "application/xml", typesEl, doc); - createContentTypesDefault("jpeg", "image/jpeg", typesEl, doc); - createContentTypesDefault("jpg", "image/jpeg", typesEl, doc); - createContentTypesDefault("png", "image/png", typesEl, doc); - createContentTypesDefault("gif", "image/gif", typesEl, doc); - //Override - createContentTypesOverride("/docProps/app.xml", CT_EXT_PROPERTIES, typesEl, doc); - createContentTypesOverride("/docProps/core.xml", CT_CORE_PROPERTIES, typesEl, doc); - createContentTypesOverride("/word/document.xml", CT_WORD_DOCUMENT, typesEl, doc); - createContentTypesOverride("/word/styles.xml", CT_STYLES, typesEl, doc); - createContentTypesOverride("/word/numbering.xml", CT_NUMBERING, typesEl, doc); - - for(HeaderReference headerRef:document.getHeaders()) { - createContentTypesOverride("/word/" + headerRef.getFilename(), CT_HEADER, typesEl, doc); + protected void createContentTypes(OpenXMLDocument document, ZipOutputStream out) { + try { + XMLStreamWriter writer = OpenXMLUtils.createStreamWriter(out); + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("Types"); + writer.writeNamespace("", SCHEMA_CONTENT_TYPES); + + //Default + createContentTypesDefault("rels", CT_RELATIONSHIP, writer); + createContentTypesDefault("xml", "application/xml", writer); + createContentTypesDefault("jpeg", "image/jpeg", writer); + createContentTypesDefault("jpg", "image/jpeg", writer); + createContentTypesDefault("png", "image/png", writer); + createContentTypesDefault("gif", "image/gif", writer); + //Override + createContentTypesOverride("/docProps/app.xml", CT_EXT_PROPERTIES, writer); + createContentTypesOverride("/docProps/core.xml", CT_CORE_PROPERTIES, writer); + createContentTypesOverride("/word/document.xml", CT_WORD_DOCUMENT, writer); + createContentTypesOverride("/word/styles.xml", CT_STYLES, writer); + createContentTypesOverride("/word/numbering.xml", CT_NUMBERING, writer); + + for(HeaderReference headerRef:document.getHeaders()) { + createContentTypesOverride("/word/" + headerRef.getFilename(), CT_HEADER, writer); + } + + writer.writeEndElement();// end Types + writer.flush(); + writer.close(); + } catch (XMLStreamException e) { + log.error("", e); } - OpenXMLUtils.writeTo(doc, out, false); } - private final void createContentTypesDefault(String extension, String type, Element typesEl, Document doc) { - Element defaultEl = (Element)typesEl.appendChild(doc.createElement("Default")); - defaultEl.setAttribute("Extension", extension); - defaultEl.setAttribute("ContentType", type); + private final void createContentTypesDefault(String extension, String type, XMLStreamWriter writer) throws XMLStreamException { + writer.writeStartElement("Default"); + writer.writeAttribute("Extension", extension); + writer.writeAttribute("ContentType", type); + writer.writeEndElement(); } - private final void createContentTypesOverride(String partName, String type, Element typesEl, Document doc) { - Element overrideEl = (Element)typesEl.appendChild(doc.createElement("Override")); - overrideEl.setAttribute("PartName", partName); - overrideEl.setAttribute("ContentType", type); + private final void createContentTypesOverride(String partName, String type, XMLStreamWriter writer) throws XMLStreamException { + writer.writeStartElement("Override"); + writer.writeAttribute("PartName", partName); + writer.writeAttribute("ContentType", type); + writer.writeEndElement(); } - } diff --git a/src/main/java/org/olat/core/util/openxml/OpenXMLUtils.java b/src/main/java/org/olat/core/util/openxml/OpenXMLUtils.java index 6971c752705..75b96833c4f 100644 --- a/src/main/java/org/olat/core/util/openxml/OpenXMLUtils.java +++ b/src/main/java/org/olat/core/util/openxml/OpenXMLUtils.java @@ -136,7 +136,7 @@ public class OpenXMLUtils { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // Turn on validation, and turn on namespaces factory.setValidating(true); - factory.setNamespaceAware(true); + factory.setNamespaceAware(false); DocumentBuilder builder = factory.newDocumentBuilder(); Document doc = builder.newDocument(); return doc; diff --git a/src/main/java/org/olat/ims/qti21/manager/CorrectResponsesUtil.java b/src/main/java/org/olat/ims/qti21/manager/CorrectResponsesUtil.java index e1b690b6ebd..1c8fead8fa1 100644 --- a/src/main/java/org/olat/ims/qti21/manager/CorrectResponsesUtil.java +++ b/src/main/java/org/olat/ims/qti21/manager/CorrectResponsesUtil.java @@ -164,9 +164,12 @@ public class CorrectResponsesUtil { * @return */ public static final List<Identifier> getCorrectIdentifierResponses(AssessmentItem assessmentItem, Interaction interaction) { + return getCorrectIdentifierResponses(assessmentItem, interaction.getResponseIdentifier()); + } + + public static final List<Identifier> getCorrectIdentifierResponses(AssessmentItem assessmentItem, Identifier responseIdentifier) { List<Identifier> correctAnswers = new ArrayList<>(5); - - ResponseDeclaration responseDeclaration = assessmentItem.getResponseDeclaration(interaction.getResponseIdentifier()); + ResponseDeclaration responseDeclaration = assessmentItem.getResponseDeclaration(responseIdentifier); if(responseDeclaration != null && responseDeclaration.getCorrectResponse() != null) { CorrectResponse correctResponse = responseDeclaration.getCorrectResponse(); if(correctResponse.getCardinality().isOneOf(Cardinality.SINGLE)) { diff --git a/src/main/java/org/olat/ims/qti21/manager/openxml/QTI21WordExport.java b/src/main/java/org/olat/ims/qti21/manager/openxml/QTI21WordExport.java new file mode 100644 index 00000000000..a3009c386ca --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/manager/openxml/QTI21WordExport.java @@ -0,0 +1,531 @@ +package org.olat.ims.qti21.manager.openxml; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.DoubleAdder; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.olat.core.gui.media.MediaResource; +import org.olat.core.gui.translator.Translator; +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.openxml.HTMLToOpenXMLHandler; +import org.olat.core.util.openxml.OpenXMLDocument; +import org.olat.core.util.openxml.OpenXMLDocument.Style; +import org.olat.core.util.openxml.OpenXMLDocument.Unit; +import org.olat.core.util.openxml.OpenXMLDocumentWriter; +import org.olat.core.util.vfs.VFSContainer; +import org.olat.course.assessment.AssessmentHelper; +import org.olat.ims.qti.editor.beecom.objects.Item; +import org.olat.ims.qti.export.QTIWordExport; +import org.olat.ims.qti21.QTI21Constants; +import org.olat.ims.qti21.manager.CorrectResponsesUtil; +import org.olat.ims.qti21.model.QTI21QuestionType; +import org.olat.ims.qti21.model.xml.AssessmentHtmlBuilder; +import org.olat.ims.qti21.model.xml.AssessmentItemFactory; +import org.olat.ims.qti21.model.xml.AssessmentTestBuilder; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.NumericalEntry; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.TextEntry; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.TextEntryAlternative; +import org.olat.ims.qti21.ui.AssessmentTestDisplayController; +import org.olat.ims.qti21.ui.editor.AssessmentTestComposerController; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.Attributes; + +import uk.ac.ed.ph.jqtiplus.node.content.basic.Block; +import uk.ac.ed.ph.jqtiplus.node.content.variable.RubricBlock; +import uk.ac.ed.ph.jqtiplus.node.content.xhtml.object.Object; +import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.DrawingInteraction; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.GraphicAssociateInteraction; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.GraphicOrderInteraction; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.HotspotInteraction; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.PositionObjectInteraction; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.SelectPointInteraction; +import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration; +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef; +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection; +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest; +import uk.ac.ed.ph.jqtiplus.node.test.SectionPart; +import uk.ac.ed.ph.jqtiplus.node.test.TestPart; +import uk.ac.ed.ph.jqtiplus.node.test.outcome.processing.OutcomeCondition; +import uk.ac.ed.ph.jqtiplus.node.test.outcome.processing.OutcomeRule; +import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; +import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentTest; +import uk.ac.ed.ph.jqtiplus.types.Identifier; +import uk.ac.ed.ph.jqtiplus.value.BaseType; +import uk.ac.ed.ph.jqtiplus.value.Cardinality; + +public class QTI21WordExport implements MediaResource { + + private final static OLog log = Tracing.createLoggerFor(QTIWordExport.class); + + private String encoding; + private ResolvedAssessmentTest resolvedAssessmentTest; + private VFSContainer mediaContainer; + private Locale locale; + private final CountDownLatch latch; + private final AssessmentHtmlBuilder htmlBuilder; + + public QTI21WordExport(ResolvedAssessmentTest resolvedAssessmentTest, VFSContainer mediaContainer, + Locale locale, String encoding, CountDownLatch latch) { + this.encoding = encoding; + this.locale = locale; + this.resolvedAssessmentTest = resolvedAssessmentTest; + this.latch = latch; + this.mediaContainer = mediaContainer; + htmlBuilder = new AssessmentHtmlBuilder(); + } + + @Override + public boolean acceptRanges() { + return false; + } + + @Override + public String getContentType() { + return "application/zip"; + } + + @Override + public Long getSize() { + return null; + } + + @Override + public InputStream getInputStream() { + return null; + } + + @Override + public Long getLastModified() { + return null; + } + + @Override + public void release() { + // + } + + @Override + public void prepare(HttpServletResponse hres) { + try { + hres.setCharacterEncoding(encoding); + } catch (Exception e) { + log.error("", e); + } + + ZipOutputStream zout = null; + try { + AssessmentTest assessmentTest = resolvedAssessmentTest.getRootNodeLookup().extractIfSuccessful(); + + String label = assessmentTest.getTitle(); + String secureLabel = StringHelper.transformDisplayNameToFileSystemName(label); + + String file = secureLabel + ".zip"; + hres.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + StringHelper.urlEncodeUTF8(file)); + hres.setHeader("Content-Description", StringHelper.urlEncodeUTF8(label)); + + zout = new ZipOutputStream(hres.getOutputStream()); + zout.setLevel(9); + + ZipEntry test = new ZipEntry(secureLabel + ".docx"); + zout.putNextEntry(test); + exportTest(assessmentTest, label, zout, false); + zout.closeEntry(); + + ZipEntry responses = new ZipEntry(secureLabel + "_responses.docx"); + zout.putNextEntry(responses); + exportTest(assessmentTest, label, zout, true); + zout.closeEntry(); + } catch (Exception e) { + log.error("", e); + } finally { + latch.countDown(); + IOUtils.closeQuietly(zout); + } + } + + private void exportTest(AssessmentTest assessmentTest, String header, OutputStream out, boolean withResponses) { + ZipOutputStream zout = null; + try { + OpenXMLDocument document = new OpenXMLDocument(); + document.setMediaContainer(mediaContainer); + document.setDocumentHeader(header); + + Translator translator = Util.createPackageTranslator(AssessmentTestDisplayController.class, locale, + Util.createPackageTranslator(AssessmentTestComposerController.class, locale)); + + renderAssessmentTest(assessmentTest, document, translator); + + for(TestPart testPart:assessmentTest.getChildAbstractParts()) { + List<AssessmentSection> assessmentSections = testPart.getAssessmentSections(); + for(AssessmentSection assessmentSection:assessmentSections) { + renderAssessmentSection(assessmentSection, document, withResponses, translator); + } + } + + zout = new ZipOutputStream(out); + zout.setLevel(9); + + OpenXMLDocumentWriter writer = new OpenXMLDocumentWriter(); + writer.createDocument(zout, document); + } catch (Exception e) { + log.error("", e); + } finally { + if(zout != null) { + try { + zout.finish(); + } catch (IOException e) { + log.error("", e); + } + } + } + } + + public static void renderAlienItem(Item item, OpenXMLDocument document, Translator translator) { + String title = item.getTitle(); + if(!StringHelper.containsNonWhitespace(title)) { + title = item.getLabel(); + } + document.appendHeading1(title, null); + String notSupported = translator.translate("info.alienitem"); + document.appendText(notSupported, true, Style.bold); + } + + public void renderAssessmentSection(AssessmentSection assessmentSection, OpenXMLDocument document, boolean withResponses, Translator translator) { + String title = assessmentSection.getTitle(); + document.appendHeading1(title, null); + List<RubricBlock> rubricBlocks = assessmentSection.getRubricBlocks(); + for(RubricBlock rubricBlock:rubricBlocks) { + String htmlRubric = htmlBuilder.blocksString(rubricBlock.getBlocks()); + document.appendHtmlText(htmlRubric, true); + } + + for(SectionPart sectionPart:assessmentSection.getChildAbstractParts()) { + if(sectionPart instanceof AssessmentSection) { + renderAssessmentSection((AssessmentSection)sectionPart, document, withResponses, translator); + } else if(sectionPart instanceof AssessmentItemRef) { + AssessmentItemRef itemRef = (AssessmentItemRef)sectionPart; + ResolvedAssessmentItem resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef); + AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful(); + renderAssessmentItem(assessmentItem, itemRef, document, withResponses, translator); + document.appendPageBreak(); + } + } + } + + public void renderAssessmentTest(AssessmentTest assessmentTest, OpenXMLDocument document, Translator translator) { + String title = assessmentTest.getTitle(); + document.appendTitle(title); + + if(assessmentTest.getOutcomeProcessing() != null) { + List<OutcomeRule> outcomeRules = assessmentTest.getOutcomeProcessing().getOutcomeRules(); + for(OutcomeRule outcomeRule:outcomeRules) { + // pass rule + if(outcomeRule instanceof OutcomeCondition) { + OutcomeCondition outcomeCondition = (OutcomeCondition)outcomeRule; + boolean findIf = AssessmentTestBuilder.findSetOutcomeValue(outcomeCondition.getOutcomeIf(), QTI21Constants.PASS_IDENTIFIER); + boolean findElse = AssessmentTestBuilder.findSetOutcomeValue(outcomeCondition.getOutcomeElse(), QTI21Constants.PASS_IDENTIFIER); + if(findIf && findElse) { + Double cutValue = AssessmentTestBuilder.extractCutValue(outcomeCondition.getOutcomeIf()); + String cutValueLabel = translator.translate("cut.value"); + document.appendText(cutValueLabel + ": " + AssessmentHelper.getRoundedScore(cutValue), true); + } + } + } + } + } + + public void renderAssessmentItem(AssessmentItem item, AssessmentItemRef itemRef, OpenXMLDocument document, boolean withResponses, Translator translator) { + StringBuilder addText = new StringBuilder(); + + QTI21QuestionType type = QTI21QuestionType.getType(item); + String typeDescription = ""; + switch(type) { + case sc: typeDescription = translator.translate("form.choice"); break; + case mc: typeDescription = translator.translate("form.choice"); break; + case fib: typeDescription = translator.translate("form.fib"); break; + case numerical: typeDescription = translator.translate("form.fib"); break; + case kprim: typeDescription = translator.translate("form.kprim"); break; + case hotspot: typeDescription = translator.translate("form.hotspot"); break; + case essay: typeDescription = translator.translate("form.essay"); break; + default: typeDescription = null; break; + } + + Double maxScore = AssessmentItemFactory.extractMaxScore(item); + + if(StringHelper.containsNonWhitespace(typeDescription) || maxScore != null) { + if(StringHelper.containsNonWhitespace(typeDescription)) { + addText.append("(").append(typeDescription).append(")"); + } + if(maxScore != null) { + addText.append(" - ").append(AssessmentHelper.getRoundedScore(maxScore)); + } + } + + String title = item.getTitle(); + document.appendHeading1(title, addText.toString()); + + URI itemUri = resolvedAssessmentTest.getSystemIdByItemRefMap().get(itemRef); + File itemFile = new File(itemUri); + + List<Block> itemBodyBlocks = item.getItemBody().getBlocks(); + String html = htmlBuilder.blocksString(itemBodyBlocks); + //System.out.println(html); + document.appendHtmlText(html, true, new QTI21AndHTMLToOpenXMLHandler(document, item, itemFile, withResponses)); + } + + private static class QTI21AndHTMLToOpenXMLHandler extends HTMLToOpenXMLHandler { + + private final File itemFile; + private final AssessmentItem assessmentItem; + private final boolean withResponses; + + private String simpleChoiceIdentifier; + private String responseIdentifier; + + public QTI21AndHTMLToOpenXMLHandler(OpenXMLDocument document, AssessmentItem assessmentItem, File itemFile, boolean withResponses) { + super(document); + this.itemFile = itemFile; + this.withResponses = withResponses; + this.assessmentItem = assessmentItem; + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) { + super.startElement(uri, localName, qName, attributes); + String tag = localName.toLowerCase(); + if("choiceinteraction".equals(tag)) { + responseIdentifier = attributes.getValue("responseidentifier"); + } else if("simplechoice".equals(tag)) { + if(currentTable == null) { + startTable(); + } + currentTable.addRowEl(); + currentTable.addCellEl(factory.createTableCell("E9EAF2", 4560, Unit.pct), 1); + simpleChoiceIdentifier = attributes.getValue("identifier"); + } else if("textentryinteraction".equals(tag)) { + startTextEntryInteraction(tag, attributes); + } else if("extendedtextinteraction".equals(tag)) { + startExtendedTextInteraction(attributes); + } else if("hotspotinteraction".equals(tag)) { + startHotspotInteraction(attributes); + } else if("inlinechoiceinteraction".equals(tag)) { + + } else if("hottextinteraction".equals(tag)) { + + } else if("hottext".equals(tag)) { + + } else if("matchinteraction".equals(tag)) { + // + } else if("gapmatchinteraction".equals(tag)) { + + } else if("selectpointinteraction".equals(tag)) { + startSelectPointInteraction(attributes); + } else if("graphicassociateinteraction".equals(tag)) { + startGraphicAssociateInteraction(attributes); + } else if("graphicorderinteraction".equals(tag)) { + startGraphicOrderInteraction(attributes); + } else if("graphicgapmatchinteraction".equals(tag)) { + // + } else if("associateinteraction".equals(tag)) { + // + } else if("uploadinteraction".equals(tag)) { + // + } else if("positionobjectinteraction".equals(tag)) { + startPositionObjectInteraction(attributes); + } else if("sliderinteraction".equals(tag)) { + // + } else if("drawinginteraction".equals(tag)) { + startDrawingInteraction(attributes); + } + } + + @Override + public void endElement(String uri, String localName, String qName) { + String tag = localName.toLowerCase(); + if("choiceinteraction".equals(tag)) { + endTable(); + } else if("simplechoice".equals(tag)) { + Element checkboxCell = factory.createTableCell(null, 369, Unit.pct); + Node checkboxNode = currentTable.addCellEl(checkboxCell, 1); + + boolean checked = false; + if(withResponses) { + Identifier identifier = Identifier.assumedLegal(simpleChoiceIdentifier); + List<Identifier> correctAnswers = CorrectResponsesUtil + .getCorrectIdentifierResponses(assessmentItem, Identifier.assumedLegal(responseIdentifier)); + checked = correctAnswers.contains(identifier); + } + + Node responseEl = factory.createCheckbox(checked); + Node wrapEl = factory.wrapInParagraph(responseEl); + checkboxNode.appendChild(wrapEl); + closeCurrentTableRow(); + } else if("textentryinteraction".equals(tag)) { + //auto closing tag + } else if("extendedtextinteraction".equals(tag)) { + //auto closing tag + } else if("hotspotinteraction".equals(tag)) { + //all work done during start + } + super.endElement(uri, localName, qName); + } + + private void startDrawingInteraction(Attributes attributes) { + Interaction interaction = getInteractionByResponseIdentifier(attributes); + if(interaction instanceof DrawingInteraction) { + DrawingInteraction drawingInteraction = (DrawingInteraction)interaction; + setObject(drawingInteraction.getObject()); + } + } + + private void startSelectPointInteraction(Attributes attributes) { + Interaction interaction = getInteractionByResponseIdentifier(attributes); + if(interaction instanceof SelectPointInteraction) { + SelectPointInteraction selectPointInteraction = (SelectPointInteraction)interaction; + setObject(selectPointInteraction.getObject()); + } + } + + private void startGraphicAssociateInteraction(Attributes attributes) { + Interaction interaction = getInteractionByResponseIdentifier(attributes); + if(interaction instanceof GraphicAssociateInteraction) { + GraphicAssociateInteraction associateInteraction = (GraphicAssociateInteraction)interaction; + setObject(associateInteraction.getObject()); + } + } + + private void startGraphicOrderInteraction(Attributes attributes) { + Interaction interaction = getInteractionByResponseIdentifier(attributes); + if(interaction instanceof GraphicOrderInteraction) { + GraphicOrderInteraction orderInteraction = (GraphicOrderInteraction)interaction; + setObject(orderInteraction.getObject()); + } + } + + private void startPositionObjectInteraction(Attributes attributes) { + Interaction interaction = getInteractionByResponseIdentifier(attributes); + if(interaction instanceof PositionObjectInteraction) { + PositionObjectInteraction positionObject = (PositionObjectInteraction)interaction; + setObject(positionObject.getObject()); + } + } + + private void startHotspotInteraction(Attributes attributes) { + Interaction interaction = getInteractionByResponseIdentifier(attributes); + if(interaction instanceof HotspotInteraction) { + HotspotInteraction hotspotInteraction = (HotspotInteraction)interaction; + setObject(hotspotInteraction.getObject()); + } + } + + private void setObject(Object object) { + if(object != null && StringHelper.containsNonWhitespace(object.getData())) { + setImage(new File(itemFile.getParentFile(), object.getData())); + } + } + + private Interaction getInteractionByResponseIdentifier(Attributes attributes) { + String identifier = attributes.getValue("responseidentifier"); + if(StringHelper.containsNonWhitespace(identifier)) { + Identifier rIdentifier = Identifier.assumedLegal(identifier); + List<Interaction> interactions = assessmentItem.getItemBody().findInteractions(); + for(Interaction interaction:interactions) { + if(rIdentifier.equals(interaction.getResponseIdentifier())) { + return interaction; + } + } + } + return null; + } + + private void startExtendedTextInteraction(Attributes attributes) { + closeParagraph(); + + int expectedLines = 5; + String length = attributes.getValue("expectedlines"); + try { + expectedLines = Integer.parseInt(length) + 1; + } catch (NumberFormatException e) { + // + } + + for(int i=expectedLines; i-->0; ) { + Element paragraphEl = factory.createFillInBlanckWholeLine(); + currentParagraph = addContent(paragraphEl); + } + } + + private void startTextEntryInteraction(String tag, Attributes attributes) { + flushText(); + + Style[] styles = setTextPreferences(Style.italic); + styleStack.add(new StyleStatus(tag, true, styles)); + pNeedNewParagraph = false; + + if(withResponses) { + String response = ""; + Identifier responseId = Identifier.assumedLegal(attributes.getValue("responseidentifier")); + ResponseDeclaration responseDeclaration = assessmentItem.getResponseDeclaration(responseId); + if(responseDeclaration != null) { + if(responseDeclaration.hasBaseType(BaseType.STRING) && responseDeclaration.hasCardinality(Cardinality.SINGLE)) { + TextEntry textEntry = new TextEntry(responseId); + FIBAssessmentItemBuilder.extractTextEntrySettingsFromResponseDeclaration(textEntry, responseDeclaration, new AtomicInteger(), new DoubleAdder()); + + StringBuilder sb = new StringBuilder(); + if(StringHelper.containsNonWhitespace(textEntry.getSolution())) { + sb.append(textEntry.getSolution()); + } + if(textEntry.getAlternatives() != null) { + for(TextEntryAlternative alt:textEntry.getAlternatives()) { + if(StringHelper.containsNonWhitespace(alt.getAlternative())) { + if(sb.length() > 0) sb.append(", "); + sb.append(alt.getAlternative()); + } + } + } + response = sb.toString(); + } else if(responseDeclaration.hasBaseType(BaseType.FLOAT) && responseDeclaration.hasCardinality(Cardinality.SINGLE)) { + NumericalEntry numericalEntry = new NumericalEntry(responseId); + FIBAssessmentItemBuilder.extractNumericalEntrySettings(assessmentItem, numericalEntry, responseDeclaration, new AtomicInteger(), new DoubleAdder()); + if(numericalEntry.getSolution() != null) { + response = numericalEntry.getSolution().toString(); + } + } + } + characters(response.toCharArray(), 0, response.length()); + } else { + int expectedLength = 20; + String length = attributes.getValue("expectedlength"); + try { + expectedLength = Integer.parseInt(length); + } catch (NumberFormatException e) { + // + } + Element blanckEl = factory.createFillInBlanck(expectedLength); + getCurrentParagraph(false).appendChild(blanckEl); + } + + flushText(); + popStyle(tag); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java index afcd40ea1af..ad76301de33 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java @@ -20,6 +20,7 @@ package org.olat.ims.qti21.model.xml; import static org.olat.ims.qti21.QTI21Constants.MAXSCORE_CLX_IDENTIFIER; +import static org.olat.ims.qti21.QTI21Constants.MAXSCORE_IDENTIFIER; import static org.olat.ims.qti21.QTI21Constants.MINSCORE_CLX_IDENTIFIER; import static org.olat.ims.qti21.QTI21Constants.SCORE_CLX_IDENTIFIER; import static org.olat.ims.qti21.QTI21Constants.SCORE_IDENTIFIER; @@ -1045,4 +1046,19 @@ public class AssessmentItemFactory { } } } + + public static Double extractMaxScore(AssessmentItem assessmentItem) { + Double maxScore = null; + OutcomeDeclaration outcomeDeclaration = assessmentItem.getOutcomeDeclaration(MAXSCORE_IDENTIFIER); + if(outcomeDeclaration != null) { + DefaultValue defaultValue = outcomeDeclaration.getDefaultValue(); + if(defaultValue != null) { + Value maxScoreValue = defaultValue.evaluate(); + if(maxScoreValue instanceof FloatValue) { + maxScore = new Double(((FloatValue)maxScoreValue).doubleValue()); + } + } + } + return maxScore; + } } diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentTestBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentTestBuilder.java index 848d2f6a4a6..dd52049f4d4 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentTestBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentTestBuilder.java @@ -102,7 +102,7 @@ public class AssessmentTestBuilder { } } - private Double extractCutValue(OutcomeIf outcomeIf) { + public static Double extractCutValue(OutcomeIf outcomeIf) { if(outcomeIf != null && outcomeIf.getExpressions().size() > 0) { Expression gte = outcomeIf.getExpressions().get(0); if(gte.getExpressions().size() > 1) { @@ -118,7 +118,7 @@ public class AssessmentTestBuilder { return null; } - private boolean findSetOutcomeValue(OutcomeConditionChild outcomeConditionChild, Identifier identifier) { + public static boolean findSetOutcomeValue(OutcomeConditionChild outcomeConditionChild, Identifier identifier) { if(outcomeConditionChild == null || outcomeConditionChild.getOutcomeRules() == null || outcomeConditionChild.getOutcomeRules().isEmpty()) return false; diff --git a/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java index 595a5b43bd6..def9642a2a4 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java @@ -323,7 +323,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { * @param countAlternatives * @param mappedScore */ - private void extractTextEntrySettingsFromResponseDeclaration(TextEntry textEntry, ResponseDeclaration responseDeclaration, + public static void extractTextEntrySettingsFromResponseDeclaration(TextEntry textEntry, ResponseDeclaration responseDeclaration, AtomicInteger countAlternatives, DoubleAdder mappedScore) { String solution = null; diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentItemDisplayController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentItemDisplayController.java index 86931dfaf11..56c2a025cd5 100644 --- a/src/main/java/org/olat/ims/qti21/ui/AssessmentItemDisplayController.java +++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentItemDisplayController.java @@ -167,11 +167,12 @@ public class AssessmentItemDisplayController extends BasicController implements public AssessmentItemDisplayController(UserRequest ureq, WindowControl wControl, RepositoryEntry testEntry, AssessmentEntry assessmentEntry, boolean authorMode, - ResolvedAssessmentItem resolvedAssessmentItem, AssessmentItemRef itemRef, File fUnzippedDirRoot, + ResolvedAssessmentItem resolvedAssessmentItem, AssessmentItemRef itemRef, + File fUnzippedDirRoot, File itemFile, AssessmentSessionAuditLogger candidateAuditLogger) { super(ureq, wControl); - this.itemFileRef = new File(fUnzippedDirRoot, itemRef.getHref().toString()); + this.itemFileRef = itemFile; this.fUnzippedDirRoot = fUnzippedDirRoot; this.resolvedAssessmentItem = resolvedAssessmentItem; this.candidateAuditLogger = candidateAuditLogger; diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java index d44188e3064..9d0e81792e9 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java @@ -143,7 +143,7 @@ public class AssessmentItemEditorController extends BasicController { AssessmentEntry assessmentEntry = assessmentService.getOrCreateAssessmentEntry(getIdentity(), testEntry, null, testEntry); displayCtrl = new AssessmentItemPreviewController(ureq, getWindowControl(), - resolvedAssessmentItem, itemRef, testEntry, assessmentEntry, rootDirectory); + resolvedAssessmentItem, itemRef, testEntry, assessmentEntry, rootDirectory, itemFile); listenTo(displayCtrl); displayTabPosition = tabbedPane.addTab(translate("preview"), displayCtrl); @@ -293,7 +293,7 @@ public class AssessmentItemEditorController extends BasicController { if(testEntry != null) { AssessmentEntry assessmentEntry = assessmentService.getOrCreateAssessmentEntry(getIdentity(), testEntry, null, testEntry); displayCtrl = new AssessmentItemPreviewController(ureq, getWindowControl(), - resolvedAssessmentItem, itemRef, testEntry, assessmentEntry, rootDirectory); + resolvedAssessmentItem, itemRef, testEntry, assessmentEntry, rootDirectory, itemFile); } else { displayCtrl = new AssessmentItemPreviewController(ureq, getWindowControl(), resolvedAssessmentItem, rootDirectory, itemFile); } diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemPreviewController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemPreviewController.java index fbc32992600..008bbf0de12 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemPreviewController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemPreviewController.java @@ -84,11 +84,12 @@ public class AssessmentItemPreviewController extends BasicController { public AssessmentItemPreviewController(UserRequest ureq, WindowControl wControl, ResolvedAssessmentItem resolvedAssessmentItem, AssessmentItemRef itemRef, RepositoryEntry testEntry, AssessmentEntry assessmentEntry, - File rootDirectory) { + File rootDirectory, File itemFile) { this(ureq, wControl); displayCtrl = new AssessmentItemDisplayController(ureq, getWindowControl(), - testEntry, assessmentEntry, true, resolvedAssessmentItem, itemRef, rootDirectory, candidateAuditLogger); + testEntry, assessmentEntry, true, resolvedAssessmentItem, itemRef, + rootDirectory, itemFile, candidateAuditLogger); listenTo(displayCtrl); mainVC.put("display", displayCtrl.getInitialComponent()); } 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 78dbedfe53c..6da3e57b7b8 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 @@ -27,6 +27,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Date; import java.util.List; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import org.olat.core.commons.fullWebApp.LayoutMain3ColsController; @@ -58,6 +59,7 @@ import org.olat.core.gui.control.generic.wizard.Step; import org.olat.core.gui.control.generic.wizard.StepRunnerCallback; import org.olat.core.gui.control.generic.wizard.StepsMainRunController; import org.olat.core.gui.control.generic.wizard.StepsRunContext; +import org.olat.core.gui.media.MediaResource; import org.olat.core.helpers.Settings; import org.olat.core.util.Formatter; import org.olat.core.util.Util; @@ -67,6 +69,7 @@ import org.olat.core.util.vfs.VFSContainer; import org.olat.fileresource.FileResourceManager; import org.olat.ims.qti21.QTI21Constants; import org.olat.ims.qti21.QTI21Service; +import org.olat.ims.qti21.manager.openxml.QTI21WordExport; import org.olat.ims.qti21.model.IdentifierGenerator; import org.olat.ims.qti21.model.QTI21QuestionType; import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; @@ -131,7 +134,7 @@ public class AssessmentTestComposerController extends MainLayoutBasicController private Dropdown exportItemTools, addItemTools, changeItemTools; private Link newTestPartLink, newSectionLink, newSingleChoiceLink, newMultipleChoiceLink, newKPrimLink, newFIBLink, newNumericalLink, newHotspotLink, newEssayLink; - private Link importFromPoolLink, importFromTableLink, exportToPoolLink; + private Link importFromPoolLink, importFromTableLink, exportToPoolLink, exportToDocxLink; private Link deleteLink, copyLink; private final TooledStackedPanel toolbar; private VelocityContainer mainVC; @@ -157,6 +160,7 @@ public class AssessmentTestComposerController extends MainLayoutBasicController private LockResult lockEntry; private LockResult activeSessionLock; + private CountDownLatch exportLatch; @Autowired private QTI21Service qtiService; @@ -265,6 +269,11 @@ public class AssessmentTestComposerController extends MainLayoutBasicController exportToPoolLink.setIconLeftCSS("o_icon o_icon_table o_icon-fw"); exportToPoolLink.setDomReplacementWrapperRequired(false); exportItemTools.addComponent(exportToPoolLink); + + exportToDocxLink = LinkFactory.createToolLink("export.pool", translate("tools.export.docx"), this, "o_mi_docx_export"); + exportToDocxLink.setIconLeftCSS("o_icon o_icon_download o_icon-fw"); + exportToDocxLink.setDomReplacementWrapperRequired(false); + exportItemTools.addComponent(exportToDocxLink); //changes changeItemTools = new Dropdown("changeTools", "change.elements", false, getTranslator()); @@ -430,6 +439,8 @@ public class AssessmentTestComposerController extends MainLayoutBasicController doImportTable(ureq); } else if(exportToPoolLink == source) { doExportPool(); + } else if(exportToDocxLink == source) { + doExportDocx(ureq); } else if(deleteLink == source) { doConfirmDelete(ureq); } else if(copyLink == source) { @@ -590,6 +601,12 @@ public class AssessmentTestComposerController extends MainLayoutBasicController .importAssessmentItemRef(getIdentity(), itemRef, assessmentItem, metadata, unzippedDirRoot, getLocale()); } + private void doExportDocx(UserRequest ureq) { + exportLatch = new CountDownLatch(1); + MediaResource mr = new QTI21WordExport(resolvedAssessmentTest, unzippedContRoot, getLocale(), "UTF-8", exportLatch); + ureq.getDispatchResult().setResultingMediaResource(mr); + } + private void doImportTable(UserRequest ureq) { removeAsListenerAndDispose(importTableWizard); diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties index 7dd6dbf0f38..4e29676eaf4 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties @@ -107,6 +107,7 @@ time.limit.max=Zeitbeschr\u00E4nkung (Minute) title.add=$org.olat.ims.qti.editor\:title.add tools.change.copy=$org.olat.ims.qti.editor\:tools.change.copy tools.change.delete=$org.olat.ims.qti.editor\:tools.change.delete +tools.export.docx=$org.olat.ims.qti.editor\:tools.export.docx tools.export.header=$org.olat.ims.qti.editor\:tools.export.header tools.export.qpool=$org.olat.ims.qti.editor\:tools.export.qpool tools.import.qpool=$org.olat.ims.qti.editor\:tools.import.qpool diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties index b7b5ba8efe9..9df64c3456e 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties @@ -107,6 +107,7 @@ time.limit.max=Time limit (minute) title.add=$org.olat.ims.qti.editor\:title.add tools.change.copy=$org.olat.ims.qti.editor\:tools.change.copy tools.change.delete=$org.olat.ims.qti.editor\:tools.change.delete +tools.export.docx=$org.olat.ims.qti.editor\:tools.export.docx tools.export.header=$org.olat.ims.qti.editor\:tools.export.header tools.export.qpool=$org.olat.ims.qti.editor\:tools.export.qpool tools.import.qpool=$org.olat.ims.qti.editor\:tools.import.qpool diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_fr.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_fr.properties index 27a73370ecc..0348ea71246 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_fr.properties +++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_fr.properties @@ -26,6 +26,7 @@ form.score.answer.points=Points title.add=$org.olat.ims.qti.editor\:title.add tools.change.copy=$org.olat.ims.qti.editor\:tools.change.copy tools.change.delete=$org.olat.ims.qti.editor\:tools.change.delete +tools.export.docx=$org.olat.ims.qti.editor\:tools.export.docx tools.export.header=$org.olat.ims.qti.editor\:tools.export.header tools.export.qpool=$org.olat.ims.qti.editor\:tools.export.qpool tools.import.qpool=$org.olat.ims.qti.editor\:tools.import.qpool -- GitLab