diff --git a/src/main/java/org/olat/core/gui/render/StringOutput.java b/src/main/java/org/olat/core/gui/render/StringOutput.java index 3eeaa4a2f77d3d31840aecc00bcba41fab0dc738..b2f2ec1fbc17de63801a8721238126dbd7303f1b 100644 --- a/src/main/java/org/olat/core/gui/render/StringOutput.java +++ b/src/main/java/org/olat/core/gui/render/StringOutput.java @@ -190,6 +190,11 @@ public class StringOutput extends Writer { return this; } + public StringOutput insert(int offset, String str) { + sb.insert(offset, str); + return this; + } + /** * Generate the following html code: onclick="call" onkeyup="if(event.which == 13 || event.keyCode){ call }" * @param call The JavaScript method to envelop @@ -223,12 +228,20 @@ public class StringOutput extends Writer { return sb.indexOf(str) >= 0; } + public int indexOf(String str) { + return sb.indexOf(str); + } + + public StringBuilder getBuffer() { + return sb; + } + public Reader getReader() { return new StringOutputReader(); } @Override - public void flush() throws IOException { + public void flush() { // } @@ -237,9 +250,6 @@ public class StringOutput extends Writer { // } - /** - * @see java.lang.Object#toString() - */ @Override public String toString() { return sb.toString(); @@ -247,109 +257,118 @@ public class StringOutput extends Writer { private class StringOutputReader extends Reader { - private int length; - private int next = 0; - private int mark = 0; - /** - * Creates a new string reader. - * - * @param s String providing the character stream. - */ - public StringOutputReader() { - this.length = sb.length(); - } + private int length; + private int next = 0; + private int mark = 0; + + /** + * Creates a new string reader. + * + * @param s String providing the character stream. + */ + public StringOutputReader() { + this.length = sb.length(); + } - /** - * Reads a single character. - * - * @return The character read, or -1 if the end of the stream has been - * reached - * - * @exception IOException If an I/O error occurs - */ - public int read() throws IOException { - synchronized (lock) { - if (next >= length) - return -1; - - char[] dst = new char[1]; - sb.getChars(next++, next, dst, 0); - return dst[0]; - } - } + /** + * Reads a single character. + * + * @return The character read, or -1 if the end of the stream has been + * reached + * + * @exception IOException If an I/O error occurs + */ + @Override + public int read() throws IOException { + synchronized (lock) { + if (next >= length) + return -1; + + char[] dst = new char[1]; + sb.getChars(next++, next, dst, 0); + return dst[0]; + } + } - public int read(char cbuf[], int off, int len) throws IOException { - synchronized (lock) { - if ((off < 0) || (off > cbuf.length) || (len < 0) || - ((off + len) > cbuf.length) || ((off + len) < 0)) { - throw new IndexOutOfBoundsException(); - } else if (len == 0) { - return 0; - } - if (next >= length) return -1; - - int n = Math.min(length - next, len); - sb.getChars(next, next + n, cbuf, off); - next += n; - return n; - } - } + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + synchronized (lock) { + if ((off < 0) || (off > cbuf.length) || (len < 0) || + ((off + len) > cbuf.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + if (next >= length) return -1; + + int n = Math.min(length - next, len); + sb.getChars(next, next + n, cbuf, off); + next += n; + return n; + } + } - /** - * Skips the specified number of characters in the stream. Returns - * the number of characters that were skipped. - * - * <p>The <code>ns</code> parameter may be negative, even though the - * <code>skip</code> method of the {@link Reader} superclass throws - * an exception in this case. Negative values of <code>ns</code> cause the - * stream to skip backwards. Negative return values indicate a skip - * backwards. It is not possible to skip backwards past the beginning of - * the string. - * - * <p>If the entire string has been read or skipped, then this method has - * no effect and always returns 0. - * - * @exception IOException If an I/O error occurs - */ - public long skip(long ns) throws IOException { - synchronized (lock) { - if (next >= length) - return 0; - // Bound skip by beginning and end of the source - long n = Math.min(length - next, ns); - n = Math.max(-next, n); - next += n; - return n; - } - } + /** + * Skips the specified number of characters in the stream. Returns + * the number of characters that were skipped. + * + * <p>The <code>ns</code> parameter may be negative, even though the + * <code>skip</code> method of the {@link Reader} superclass throws + * an exception in this case. Negative values of <code>ns</code> cause the + * stream to skip backwards. Negative return values indicate a skip + * backwards. It is not possible to skip backwards past the beginning of + * the string. + * + * <p>If the entire string has been read or skipped, then this method has + * no effect and always returns 0. + * + * @exception IOException If an I/O error occurs + */ + @Override + public long skip(long ns) throws IOException { + synchronized (lock) { + if (next >= length) + return 0; + // Bound skip by beginning and end of the source + long n = Math.min(length - next, ns); + n = Math.max(-next, n); + next += n; + return n; + } + } - public boolean ready() throws IOException { - synchronized (lock) { - return true; - } - } + @Override + public boolean ready() throws IOException { + synchronized (lock) { + return true; + } + } - public boolean markSupported() { - return true; - } + @Override + public boolean markSupported() { + return true; + } - public void mark(int readAheadLimit) throws IOException { - if (readAheadLimit < 0) { - throw new IllegalArgumentException("Read-ahead limit < 0"); - } - synchronized (lock) { - mark = next; - } - } + @Override + public void mark(int readAheadLimit) throws IOException { + if (readAheadLimit < 0) { + throw new IllegalArgumentException("Read-ahead limit < 0"); + } + synchronized (lock) { + mark = next; + } + } - public void reset() throws IOException { - synchronized (lock) { - next = mark; - } - } + @Override + public void reset() throws IOException { + synchronized (lock) { + next = mark; + } + } - public void close() { - // - } + @Override + public void close() throws IOException { + // + } } } \ No newline at end of file diff --git a/src/main/java/org/olat/course/certificate/manager/CertificatePdfServiceWorker.java b/src/main/java/org/olat/course/certificate/manager/CertificatePdfServiceWorker.java index 031d6644583a04932d115fa9d8f270ae84cc041f..a7a46e7e05049f8fdb5fa1843cf5f13332122d39 100644 --- a/src/main/java/org/olat/course/certificate/manager/CertificatePdfServiceWorker.java +++ b/src/main/java/org/olat/course/certificate/manager/CertificatePdfServiceWorker.java @@ -23,11 +23,13 @@ import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; import java.nio.charset.Charset; import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.Date; import java.util.List; import java.util.Locale; @@ -35,6 +37,7 @@ import java.util.UUID; import org.apache.velocity.VelocityContext; import org.olat.core.commons.services.pdf.PdfService; +import org.olat.core.gui.render.StringOutput; import org.olat.core.id.Identity; import org.olat.core.id.User; import org.olat.core.id.UserConstants; @@ -45,6 +48,7 @@ import org.olat.core.util.StringHelper; import org.olat.core.util.i18n.I18nManager; import org.olat.course.assessment.AssessmentHelper; import org.olat.course.certificate.CertificateTemplate; +import org.olat.course.certificate.CertificatesManager; import org.olat.repository.RepositoryEntry; import org.olat.user.UserManager; import org.olat.user.propertyhandlers.UserPropertyHandler; @@ -105,6 +109,14 @@ public class CertificatePdfServiceWorker { File certificateFile = new File(destinationDir, filename); File templateFile = certificatesManager.getTemplateFile(template); File htmlCertificateFile = copyAndEnrichTemplate(templateFile); + File qrCodeScriptFile = new File(htmlCertificateFile.getParent(), "qrcode.min.js"); + if(!qrCodeScriptFile.exists()) { + try(InputStream inQRCodeLib = CertificatesManager.class.getResourceAsStream("qrcode.min.js")) { + Files.copy(inQRCodeLib, qrCodeScriptFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch(Exception e) { + log.error("Can not read qrcode.min.js for QR Code PDF generation", e); + } + } try(OutputStream out = new FileOutputStream(certificateFile)) { pdfService.convert(htmlCertificateFile.getParentFile(), htmlCertificateFile.getName(), out); @@ -122,19 +134,67 @@ public class CertificatePdfServiceWorker { } private File copyAndEnrichTemplate(File templateFile) { - VelocityContext context = getContext(); boolean result = false; File htmlCertificate = new File(templateFile.getParent(), "c" + UUID.randomUUID() + ".html"); + try(Reader in = Files.newBufferedReader(templateFile.toPath(), Charset.forName("UTF-8")); - Writer output = new FileWriter(htmlCertificate)) { - result = certificatesManager.getVelocityEngine().evaluate(context, output, "mailTemplate", in); + StringOutput content = new StringOutput(32000); + Writer output = new FileWriter(htmlCertificate)) { + VelocityContext context = getContext(); + result = certificatesManager.getVelocityEngine().evaluate(context, content, "mailTemplate", in); + content.flush(); + + if(hasQRCode(content)) { + injectQRCodeScript(content); + } + + output.write(content.toString()); output.flush(); + result = true; } catch(Exception e) { log.error("", e); } return result ? htmlCertificate : null; } + private boolean hasQRCode(StringOutput content) { + return content.contains("o_qrcode"); + } + + private void injectQRCodeScript(StringOutput content) { + int injectionIndex = injectionPoint(content); + + StringBuilder qr = new StringBuilder(512); + qr.append("<script src='qrcode.min.js'></script>\n") + .append("<script>\n") + .append("/* <![CDATA[ */ \n") + .append("document.addEventListener('load', new function() {\n") + .append(" var qrcodes = document.querySelectorAll('.o_qrcode');\n") + .append(" for (var i=0; i<qrcodes.length; i++) {\n") + .append(" var qrcode = qrcodes[i];\n") + .append(" var val = qrcode.textContent;\n") + .append(" while (qrcode.firstChild) {\n") + .append(" qrcode.removeChild(qrcode.firstChild);\n") + .append(" }\n") + .append(" new QRCode(qrcode, val);\n") + .append(" }\n") + .append("});\n") + .append("/* ]]> */\n") + .append("</script>"); + content.insert(injectionIndex, qr.toString()); + } + + private int injectionPoint(StringOutput content) { + String[] anchors = new String[] { "</body", "</ body", "</BODY", "</ BODY", "</html", "</HTML" }; + for(String anchor:anchors) { + int bodyIndex = content.indexOf(anchor); + if(bodyIndex > 0) { + return bodyIndex; + } + } + return content.length();// last hope + } + private VelocityContext getContext() { VelocityContext context = new VelocityContext(); fillUserProperties(context); diff --git a/src/main/java/org/olat/course/config/ui/CourseOptionsController.java b/src/main/java/org/olat/course/config/ui/CourseOptionsController.java index 088808af7330092925ec15699bd68a1f1ecfa72c..49ad41aab18e35789a4b6aa5c91bd1494e12e474 100644 --- a/src/main/java/org/olat/course/config/ui/CourseOptionsController.java +++ b/src/main/java/org/olat/course/config/ui/CourseOptionsController.java @@ -210,7 +210,8 @@ public class CourseOptionsController extends FormBasicController { glossaryNameEl = uifactory.addStaticTextElement("glossaryName", "glossary.isconfigured", translate("glossary.no.glossary"), glossaryCont); - + glossaryNameEl.setExampleKey("chkbx.glossary.inverse.explain", null); + boolean managedGlossary = RepositoryEntryManagedFlag.isManaged(entry, RepositoryEntryManagedFlag.glossary); FormLayoutContainer buttonsCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator()); glossaryCont.add(buttonsCont); diff --git a/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_de.properties index 5d030748a22033c592d91e0130a50a52f9370a41..7dd27afe7ea0d2af0216305be13ad61c6a539a56 100644 --- a/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_de.properties @@ -2,7 +2,8 @@ chkbx.calendar.onoff=Kurskalender chkbx.chat.onoff=Kurs-Chat chkbx.efficency.onoff=Leistungsnachweis verwenden -chkbx.glossary.explain=Das Glossary muss unter "Optionen" konfiguriert werden. +chkbx.glossary.explain=Das Glossar muss unter "Optionen" konfiguriert werden. +chkbx.glossary.inverse.explain=Glossar Menu in Toolbar muss unter "Toolbar" konfiguriert werden. chkbx.glossary.onoff=Glossar chkbx.search.onoff=Kurssuche chkbx.menu.onoff=Menu sichtbar f\u00FCr Teilnehmer und Betreuer diff --git a/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_en.properties index 4c11406b6a14aa9c799e049bed2e164e129f288e..f8f93a81c3576a204c245789ad5468abd66777fc 100644 --- a/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_en.properties @@ -4,6 +4,7 @@ chkbx.chat.onoff=Use course chat chkbx.search.onoff=Enable course search chkbx.efficency.onoff=Use evidence of achievement chkbx.glossary.explain=The glossary need to be configured under "Options". +chkbx.glossary.inverse.explain=Glossary menu in toolbar is configured under "Toolbar". chkbx.glossary.onoff=Glossary chkbx.menu.onoff=Menu visible for participants and coaches chkbx.toolbar.explain=Activate tools in toolbar: diff --git a/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_fr.properties b/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_fr.properties index 4ab45c70a80b3602a8f9c78cbb31df0f69660a80..a813f3d55800f766cbfb10581d0db2abb8be7332 100644 --- a/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_fr.properties +++ b/src/main/java/org/olat/course/config/ui/_i18n/LocalStrings_fr.properties @@ -4,6 +4,7 @@ chkbx.calendar.onoff=Calendrier du cours chkbx.chat.onoff=Chat du cours chkbx.efficency.onoff=Utiliser l'attestation de performance chkbx.glossary.explain=Le glossaire doit \u00EAtre configurer sous "Options". +chkbx.glossary.inverse.explain=Le menu du glossaire dans la barre d'outils est à configurer sous "Barre d'outils". chkbx.glossary.onoff=Glossaire chkbx.menu.onoff=Menu visible pour les participants et les coaches chkbx.search.onoff=Recherche au sein du cours diff --git a/src/main/java/org/olat/course/run/CourseRuntimeController.java b/src/main/java/org/olat/course/run/CourseRuntimeController.java index 42900b3b909ab7a7e26e91551c6363fde959359e..5c2e0d613820293594159584daab6adbf625516c 100644 --- a/src/main/java/org/olat/course/run/CourseRuntimeController.java +++ b/src/main/java/org/olat/course/run/CourseRuntimeController.java @@ -1771,7 +1771,7 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im if(glossary != null) { ICourse course = CourseFactory.loadCourse(getRepositoryEntry()); CourseConfig cc = course.getCourseEnvironment().getCourseConfig(); - glossary.setVisible(cc.hasGlossary()); + glossary.setVisible(cc.hasGlossary() && cc.isGlossaryEnabled()); toolbarPanel.setDirty(true); } break;