diff --git a/src/main/java/org/olat/core/util/pdf/PdfDocument.java b/src/main/java/org/olat/core/util/pdf/PdfDocument.java new file mode 100644 index 0000000000000000000000000000000000000000..16ff65731457179975e7ed70db7783836b914ce6 --- /dev/null +++ b/src/main/java/org/olat/core/util/pdf/PdfDocument.java @@ -0,0 +1,216 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.core.util.pdf; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import javax.xml.transform.TransformerException; + +import org.apache.jempbox.xmp.XMPMetadata; +import org.apache.jempbox.xmp.XMPSchemaBasic; +import org.apache.jempbox.xmp.XMPSchemaDublinCore; +import org.apache.jempbox.xmp.XMPSchemaPDF; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.PDDocumentInformation; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDMetadata; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.edit.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType1Font; + +/** + * + * Initial date: 12.02.2014<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class PdfDocument { + + protected PDFont font = PDType1Font.HELVETICA; + protected float marginTopBottom = 72.0f; + protected float marginLeftRight = 72.0f; + protected float lineHeightFactory = 1.5f; + protected float width; + + protected PDDocument document; + protected PDPage currentPage; + protected PDPageContentStream currentContentStream; + + protected float currentY; + + public PdfDocument() throws IOException { + document = new PDDocument(); + } + + public void close() throws IOException { + document.close(); + } + + public PDPage addPage() throws IOException { + if(currentContentStream != null) { + currentContentStream.close(); + } + + PDPage page = new PDPage(); + document.addPage(page); + currentPage = page; + currentContentStream = new PDPageContentStream(document, currentPage); + + PDRectangle mediabox = currentPage.findMediaBox(); + width = mediabox.getWidth() - 2 * marginLeftRight; + currentY = mediabox.getUpperRightY() - marginTopBottom; + return page; + } + + public void closePage() throws IOException { + if(currentContentStream != null) { + currentContentStream.close(); + } + } + + public void addText(String text, float fontSize) + throws IOException { + currentContentStream.beginText(); + currentContentStream.setFont(font, fontSize); + currentContentStream.moveTextPositionByAmount(marginLeftRight, currentY); + currentContentStream.drawString(text); + currentContentStream.endText(); + + float leading = lineHeightFactory * fontSize; + currentY -= leading; + } + + public void addParagraph(String text, float fontSize, float width) + throws IOException { + + float leading = lineHeightFactory * fontSize; + + List<String> lines = new ArrayList<String>(); + int lastSpace = -1; + while (text.length() > 0) { + int spaceIndex = text.indexOf(' ', lastSpace + 1); + if (spaceIndex < 0) { + lines.add(text); + text = ""; + } else { + String subString = text.substring(0, spaceIndex); + float size = fontSize * font.getStringWidth(subString) / 1000; + if (size > width) { + if (lastSpace < 0) // So we have a word longer than the line... draw it anyways + lastSpace = spaceIndex; + subString = text.substring(0, lastSpace); + lines.add(subString); + text = text.substring(lastSpace).trim(); + lastSpace = -1; + } else { + lastSpace = spaceIndex; + } + } + } + + currentContentStream.beginText(); + currentContentStream.setFont(font, fontSize); + currentContentStream.moveTextPositionByAmount(marginLeftRight, currentY); + for (String line: lines) { + currentContentStream.drawString(line); + currentContentStream.moveTextPositionByAmount(0, -leading); + currentY -= leading; + } + currentContentStream.endText(); + } + + public float getStringWidth(String string, float fontSize) + throws IOException { + return fontSize * font.getStringWidth(string) / 1000; + } + + public void drawLine(float xStart, float yStart, float xEnd, float yEnd, float width) + throws IOException { + currentContentStream.setLineWidth(width); + currentContentStream.drawLine(xStart, yStart, xEnd, yEnd); + } + + public void addMetadata(String title, String subject, String author) + throws IOException, TransformerException { + PDDocumentCatalog catalog = document.getDocumentCatalog(); + PDDocumentInformation info = document.getDocumentInformation(); + Calendar date = Calendar.getInstance(); + + info.setAuthor(author); + info.setCreator(author); + info.setCreationDate(date); + info.setModificationDate(date); + info.setTitle(title); + info.setSubject(subject); + + XMPMetadata metadata = new XMPMetadata(); + XMPSchemaPDF pdfSchema = metadata.addPDFSchema(); + pdfSchema.setProducer("OpenOLAT"); + + XMPSchemaBasic basicSchema = metadata.addBasicSchema(); + basicSchema.setModifyDate(date); + basicSchema.setCreateDate(date); + basicSchema.setCreatorTool("OpenOLAT"); + basicSchema.setMetadataDate(date); + + XMPSchemaDublinCore dcSchema = metadata.addDublinCoreSchema(); + dcSchema.setTitle(title); + dcSchema.addCreator(author); + dcSchema.setDescription(subject); + + PDMetadata metadataStream = new PDMetadata(document); + metadataStream.importXMPMetadata(metadata); + catalog.setMetadata(metadataStream); + } + + public void addPageNumbers() throws IOException { + float footerFontSize = 10.0f; + + @SuppressWarnings("unchecked") + List<PDPage> allPages = document.getDocumentCatalog().getAllPages(); + int numOfPages = allPages.size(); + for( int i=0; i<allPages.size(); i++ ) { + PDPage page = allPages.get( i ); + PDRectangle pageSize = page.findMediaBox(); + + String text = (i+1) + " / " + numOfPages; + float stringWidth = getStringWidth(text, footerFontSize); + // calculate to center of the page + float pageWidth = pageSize.getWidth(); + double x = (pageWidth - stringWidth) / 2.0f; + double y = (marginTopBottom / 2.0f); + + // append the content to the existing stream + PDPageContentStream contentStream = new PDPageContentStream(document, page, true, true,true); + contentStream.beginText(); + // set font and font size + contentStream.setFont( font, footerFontSize ); + contentStream.setTextTranslation(x, y); + contentStream.drawString(text); + contentStream.endText(); + contentStream.close(); + } + } +} diff --git a/src/main/java/org/olat/course/nodes/cl/ui/CheckListAssessmentController.java b/src/main/java/org/olat/course/nodes/cl/ui/CheckListAssessmentController.java index 9af5f9f38a6e9a08640d334a69aab5663745860b..49a4f69f10dd8a07047b95af9eb1fa384bcbca78 100644 --- a/src/main/java/org/olat/course/nodes/cl/ui/CheckListAssessmentController.java +++ b/src/main/java/org/olat/course/nodes/cl/ui/CheckListAssessmentController.java @@ -19,6 +19,7 @@ */ package org.olat.course.nodes.cl.ui; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -28,6 +29,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.xml.transform.TransformerException; + +import org.apache.pdfbox.exceptions.COSVisitorException; import org.olat.NewControllerFactory; import org.olat.basesecurity.BaseSecurity; import org.olat.basesecurity.BaseSecurityModule; @@ -38,6 +42,7 @@ import org.olat.core.gui.components.Component; 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.FlexiTableElement; +import org.olat.core.gui.components.form.flexible.elements.FormLink; import org.olat.core.gui.components.form.flexible.impl.FormBasicController; import org.olat.core.gui.components.form.flexible.impl.FormEvent; import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; @@ -49,6 +54,7 @@ import org.olat.core.gui.components.form.flexible.impl.elements.table.SelectionE import org.olat.core.gui.components.form.flexible.impl.elements.table.StaticFlexiCellRenderer; import org.olat.core.gui.components.form.flexible.impl.elements.table.StaticFlexiColumnModel; import org.olat.core.gui.components.form.flexible.impl.elements.table.TextFlexiCellRenderer; +import org.olat.core.gui.components.link.Link; import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.ControllerEventListener; import org.olat.core.gui.control.Event; @@ -94,9 +100,11 @@ public class CheckListAssessmentController extends FormBasicController implement private final UserCourseEnvironment userCourseEnv; private final boolean isAdministrativeUser; + private FormLink pdfExport; private CheckboxAssessmentDataModel model; private FlexiTableElement table; private final List<UserPropertyHandler> userPropertyHandlers; + private final CheckboxList checkboxList; private CloseableModalController cmc; private AssessedIdentityOverviewController editCtrl; @@ -130,6 +138,7 @@ public class CheckListAssessmentController extends FormBasicController implement this.courseNode = courseNode; this.userCourseEnv = userCourseEnv; config = courseNode.getModuleConfiguration(); + checkboxList = (CheckboxList)config.get(CheckListCourseNode.CONFIG_KEY_CHECKBOX); Roles roles = ureq.getUserSession().getRoles(); isAdministrativeUser = securityModule.isUserAllowedAdminProps(roles); userPropertyHandlers = userManager.getUserPropertyHandlersFor(USER_PROPS_ID, isAdministrativeUser); @@ -179,10 +188,9 @@ public class CheckListAssessmentController extends FormBasicController implement } } - CheckboxList list = (CheckboxList)config.get(CheckListCourseNode.CONFIG_KEY_CHECKBOX); - List<Checkbox> checkboxList = list.getList(); + List<Checkbox> boxList = checkboxList.getList(); int j = 0; - for(Checkbox box:checkboxList) { + for(Checkbox box:boxList) { int colIndex = CheckboxAssessmentDataModel.CHECKBOX_OFFSET + j++; String colName = "checkbox_" + colIndex; DefaultFlexiColumnModel column = new DefaultFlexiColumnModel(true, colName, colIndex, true, colName); @@ -215,6 +223,8 @@ public class CheckListAssessmentController extends FormBasicController implement table = uifactory.addTableElement(ureq, getWindowControl(), "checkbox-list", model, getTranslator(), formLayout); table.setFilterKeysAndValues("participants", keys, values); table.setExportEnabled(true); + + pdfExport = uifactory.addFormLink("pdf.export", formLayout, Link.BUTTON); } private List<AssessmentDataView> loadDatas() { @@ -303,6 +313,8 @@ public class CheckListAssessmentController extends FormBasicController implement doOpenIdentity(ureq, row); } } + } else if(pdfExport == source) { + doExportPDF(ureq); } super.formInnerEvent(ureq, source, event); } @@ -330,6 +342,21 @@ public class CheckListAssessmentController extends FormBasicController implement // } + private void doExportPDF(UserRequest ureq) { + try { + String name = courseNode.getShortTitle(); + CheckboxPDFExport pdfExport = new CheckboxPDFExport(name, getTranslator(), userPropertyHandlers); + pdfExport.setAuthor(userManager.getUserDisplayName(getIdentity())); + pdfExport.setTitle(courseNode.getShortTitle()); + pdfExport.setSubject(courseNode.getLongTitle()); + pdfExport.setObjectives(courseNode.getLearningObjectives()); + pdfExport.create(checkboxList, model); + ureq.getDispatchResult().setResultingMediaResource(pdfExport); + } catch (IOException | COSVisitorException | TransformerException e) { + logError("", e); + } + } + private void doOpenIdentity(UserRequest ureq, AssessmentDataView row) { String businessPath = "[Identity:" + row.getIdentityKey() + "]"; NewControllerFactory.getInstance().launch(businessPath, ureq, getWindowControl()); diff --git a/src/main/java/org/olat/course/nodes/cl/ui/CheckboxAssessmentDataModel.java b/src/main/java/org/olat/course/nodes/cl/ui/CheckboxAssessmentDataModel.java index 7dc3722dc8faf8fbf56e8e7bedc1f0d84678d5f2..b48c7e17a68d890e391f03a04360dfde53b9a35d 100644 --- a/src/main/java/org/olat/course/nodes/cl/ui/CheckboxAssessmentDataModel.java +++ b/src/main/java/org/olat/course/nodes/cl/ui/CheckboxAssessmentDataModel.java @@ -55,6 +55,13 @@ public class CheckboxAssessmentDataModel extends DefaultFlexiTableDataModel<Asse super(datas, columnModel); backupRows = datas; } + + /** + * @return The list of rows, not filtered + */ + public List<AssessmentDataView> getBackedUpRows() { + return backupRows; + } @Override public DefaultFlexiTableDataModel<AssessmentDataView> createCopyWithEmptyList() { diff --git a/src/main/java/org/olat/course/nodes/cl/ui/CheckboxPDFExport.java b/src/main/java/org/olat/course/nodes/cl/ui/CheckboxPDFExport.java new file mode 100644 index 0000000000000000000000000000000000000000..b69471fe365c3005232b74da1d01ddf631c7b81b --- /dev/null +++ b/src/main/java/org/olat/course/nodes/cl/ui/CheckboxPDFExport.java @@ -0,0 +1,333 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.course.nodes.cl.ui; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import javax.servlet.http.HttpServletResponse; +import javax.xml.transform.TransformerException; + +import org.apache.pdfbox.exceptions.COSVisitorException; +import org.olat.core.gui.media.MediaResource; +import org.olat.core.gui.translator.Translator; +import org.olat.core.id.UserConstants; +import org.olat.core.logging.OLog; +import org.olat.core.logging.Tracing; +import org.olat.core.util.StringHelper; +import org.olat.core.util.pdf.PdfDocument; +import org.olat.course.nodes.cl.model.AssessmentDataView; +import org.olat.course.nodes.cl.model.Checkbox; +import org.olat.course.nodes.cl.model.CheckboxList; +import org.olat.user.propertyhandlers.UserPropertyHandler; + +/** + * + * Initial date: 12.02.2014<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class CheckboxPDFExport extends PdfDocument implements MediaResource { + + private static final OLog log = Tracing.createLoggerFor(CheckboxPDFExport.class); + + private final String filename; + private String title; + private String subject; + private String objectives; + private String author; + private final Translator translator; + private int firstNameIndex; + private int lastNameIndex; + + public CheckboxPDFExport(String filename, Translator translator, List<UserPropertyHandler> userPropertyHandlers) + throws IOException { + super(); + this.filename = filename; + this.translator = translator; + + int i=0; + for(UserPropertyHandler userPropertyHandler:userPropertyHandlers) { + if(UserConstants.LASTNAME.equals(userPropertyHandler.getName())) { + lastNameIndex = i; + } + i++; + } + + int j=0; + for(UserPropertyHandler userPropertyHandler:userPropertyHandlers) { + if(UserConstants.FIRSTNAME.equals(userPropertyHandler.getName())) { + firstNameIndex = j; + } + j++; + } + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getObjectives() { + return objectives; + } + + public void setObjectives(String objectives) { + this.objectives = objectives; + } + + @Override + public String getContentType() { + return "application/pdf"; + } + + @Override + public Long getSize() { + return null; + } + + @Override + public InputStream getInputStream() { + return null; + } + + @Override + public Long getLastModified() { + return null; + } + + @Override + public void prepare(HttpServletResponse hres) { + try { + hres.setHeader("Content-Disposition","attachment; filename*=UTF-8''" + StringHelper.urlEncodeUTF8(filename)); + hres.setHeader("Content-Description",StringHelper.urlEncodeUTF8(filename)); + document.save(hres.getOutputStream()); + } catch (COSVisitorException | IOException e) { + log.error("", e); + } + } + + @Override + public void release() { + try { + close(); + } catch (IOException e) { + log.error("", e); + } + } + + public void create(CheckboxList checkboxList, CheckboxAssessmentDataModel dataModel) + throws IOException, COSVisitorException, TransformerException { + addPage(); + addMetadata(title, subject, author); + if(StringHelper.containsNonWhitespace(objectives)) { + addParagraph(objectives, 10, width); + } + + float maxSize = 0.0f; + float fontSize = 10.0f; + for(Checkbox box:checkboxList.getList()) { + maxSize = Math.max(maxSize, getStringWidth(box.getTitle(), fontSize)); + } + + String[] headers = getHeaders(checkboxList); + String[][] content = getRows(checkboxList, dataModel); + int numOfRows = content.length; + for(int offset=0; offset<numOfRows; ) { + offset += drawTable(headers, content, offset, maxSize, fontSize, 5); + closePage(); + if(offset<numOfRows) { + addPage(); + } + } + + addPageNumbers(); + } + + private String[][] getRows(CheckboxList checkboxList, CheckboxAssessmentDataModel dataModel) { + List<AssessmentDataView> rows = dataModel.getBackedUpRows(); + int numOfRows = rows.size(); + List<Checkbox> boxList = checkboxList.getList(); + int numOfCheckbox = boxList.size(); + + String[][] content = new String[numOfRows][]; + for(int i=0; i<numOfRows; i++) { + AssessmentDataView row = rows.get(i); + content[i] = new String[numOfCheckbox + 2]; + content[i][0] = getName(row); + for(int j=0; j<numOfCheckbox; j++) { + Boolean[] checked = row.getChecked(); + if(checked != null && j >= 0 && j < checked.length) { + Boolean check = checked[j]; + if(check != null && check.booleanValue()) { + content[i][j+1] = "x"; + } + } + + } + } + + return content; + } + + private String getName(AssessmentDataView view) { + StringBuilder sb = new StringBuilder(); + sb.append(view.getIdentityProp(lastNameIndex)) + .append(", ") + .append(view.getIdentityProp(firstNameIndex)); + return sb.toString(); + } + + private String[] getHeaders(CheckboxList checkboxList) { + int numOfCheckbox = checkboxList.getList().size(); + String[] headers = new String[numOfCheckbox + 2]; + headers[0] = translator.translate("participants"); + int pos = 1; + for(Checkbox box:checkboxList.getList()) { + headers[pos++] = box.getTitle(); + } + headers[numOfCheckbox + 1] = translator.translate("signature"); + return headers; + } + + public int drawTable(String[] headers, String[][] content, int offset, float maxHeaderSize, float fontSize, float margin) + throws IOException { + + float tableWidth = width; + int cols = content[0].length; + + + float headerHeight = maxHeaderSize + (2*margin); + + float rowHeight = (lineHeightFactory * fontSize) + (2 * margin); + + float availableHeight = currentY - marginTopBottom - headerHeight; + float numOfAvailableRows = availableHeight / rowHeight; + int possibleRows = Math.round(numOfAvailableRows); + int end = Math.min(offset + possibleRows, content.length); + int rows = end - offset; + + float tableHeight = (rowHeight * rows) + headerHeight; + float colWidth = (tableWidth - 200) / (float) (cols - 2.0f); + float cellMargin = 5f; + + // draw the rows + float y = currentY; + float nexty = currentY; + drawLine(marginLeftRight, nexty, marginLeftRight + tableWidth, nexty, 0.5f); + nexty -= headerHeight; + for (int i = 0; i <= rows; i++) { + drawLine(marginLeftRight, nexty, marginLeftRight + tableWidth, nexty, 0.5f); + nexty -= rowHeight; + } + + // draw the columns + float nextx = marginLeftRight; + drawLine(nextx, y, nextx, y - tableHeight, 0.5f); + nextx += 100; + for (int i=1; i<=cols-2; i++) { + drawLine(nextx, y, nextx, y - tableHeight, 0.5f); + nextx += colWidth; + } + drawLine(nextx, y, nextx, y - tableHeight, 0.5f); + nextx += 100; + drawLine(nextx, y, nextx, y - tableHeight, 0.5f); + + + // now add the text + currentContentStream.setFont(font, fontSize); + + // draw the headers + float textx = marginLeftRight + cellMargin; + float texty = currentY; + for (int h=0; h<cols; h++) { + String text = headers[h]; + if(text == null) { + text = ""; + } + currentContentStream.beginText(); + if (h == 0 || h == (cols-1)) { + currentContentStream.moveTextPositionByAmount(textx, texty - headerHeight + margin); + currentContentStream.drawString(text); + textx += 100; + } else { + currentContentStream.setTextRotation(3 * (Math.PI / 2), textx + margin, texty - margin); + currentContentStream.drawString(text); + textx += colWidth; + } + currentContentStream.endText(); + + } + + currentY -= headerHeight; + + + textx = marginLeftRight + cellMargin; + texty = currentY - 15; + for (int i=offset; i<end; i++) { + if(i==200) { + System.out.println(); + } + + String[] rowContent = content[i]; + if(rowContent == null) continue; + + for (int j = 0; j < cols; j++) { + String text = rowContent[j]; + if(text != null) { + if("x".equals(text)) { + text = "x"; + } + currentContentStream.beginText(); + currentContentStream.moveTextPositionByAmount(textx, texty); + currentContentStream.drawString(text); + currentContentStream.endText(); + } + textx += (j==0 ? 100 : colWidth); + } + texty -= rowHeight; + textx = marginLeftRight + cellMargin; + } + + return rows; + } + + +} diff --git a/src/main/java/org/olat/course/nodes/cl/ui/_content/assessment_list.html b/src/main/java/org/olat/course/nodes/cl/ui/_content/assessment_list.html index 55a8a5fcd35c389f81665f99fd307b80e2b6e502..5231a88f656c2f813830eac1a2243093cfe8b32d 100644 --- a/src/main/java/org/olat/course/nodes/cl/ui/_content/assessment_list.html +++ b/src/main/java/org/olat/course/nodes/cl/ui/_content/assessment_list.html @@ -6,4 +6,10 @@ <p>$r.translate("run.due.date",$r.formatDateAndTime($dueDate))</p> #end $r.render("checkbox-list") + + <div class="b_clearfix o_qpool_button_bar_box"> + <div class="o_qpool_button_bar"> + $r.render("pdf.export") + </div> + </div> </div> \ No newline at end of file diff --git a/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_de.properties index d0b1090d22b54fe7b2781171fc8dc7d1face3e57..d8d6755e8cd54a51cc31a0818cd2911e4d0d95ab 100644 --- a/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/course/nodes/cl/ui/_i18n/LocalStrings_de.properties @@ -49,10 +49,12 @@ label.presented=Vorgef label.controlled=Kontrolliert label.present=Anwesend points=Punkte +pdf.export=PDF generieren award.point.on=Punkte vergeben bei Auswahl description=Beschreibung participants=Mitglieder file=Datei +signature=Unterschrift run.run=Persönliche Checkliste run.coach=Checklisten Verwaltung run.mark=Markierung