diff --git a/src/main/java/org/olat/core/util/pdf/PdfDocument.java b/src/main/java/org/olat/core/util/pdf/PdfDocument.java index fa55ae6dbb0725979b61a5278c4d4f6f740a5906..359f57084f961b0b9d914aa24bbada924a839943 100644 --- a/src/main/java/org/olat/core/util/pdf/PdfDocument.java +++ b/src/main/java/org/olat/core/util/pdf/PdfDocument.java @@ -112,12 +112,12 @@ public class PdfDocument { currentY -= leading; } - public void addParagraph(String text, float fontSize, float width) + public void addParagraph(String text, float fontSize, float paragraphWidth) throws IOException { - addParagraph(text, fontSize, false, width); + addParagraph(text, fontSize, false, paragraphWidth); } - public void addParagraph(String text, float fontSize, boolean bold, float width) + public void addParagraph(String text, float fontSize, boolean bold, float paragraphWidth) throws IOException { float leading = lineHeightFactory * fontSize; @@ -133,7 +133,7 @@ public class PdfDocument { } else { String subString = text.substring(0, spaceIndex); float size = fontSize * textFont.getStringWidth(subString) / 1000; - if (size > width) { + if (size > paragraphWidth) { if (lastSpace < 0) // So we have a word longer than the line... draw it anyways lastSpace = spaceIndex; subString = text.substring(0, lastSpace); @@ -162,9 +162,9 @@ public class PdfDocument { return fontSize * font.getStringWidth(string) / 1000; } - public void drawLine(float xStart, float yStart, float xEnd, float yEnd, float width) + public void drawLine(float xStart, float yStart, float xEnd, float yEnd, float lineWidth) throws IOException { - currentContentStream.setLineWidth(width); + currentContentStream.setLineWidth(lineWidth); currentContentStream.drawLine(xStart, yStart, xEnd, yEnd); } @@ -248,4 +248,78 @@ public class PdfDocument { contentStream.close(); } } + + /** + * The number 6 was chosen after some trial and errors. It's a good compromise as + * the width of the letter is not fixed. Don't replace ... with ellipsis, it break + * the PDF. + * + * @param text + * @param maxWidth + * @param fontSize + * @return + * @throws IOException + */ + protected String[] splitText(String text, float maxWidth, float fontSize) throws IOException { + float textWidth = getStringWidth(text, fontSize); + if(maxWidth < textWidth) { + + float letterWidth = textWidth / text.length(); + int maxNumOfLetter = Math.round(maxWidth / letterWidth) - 1; + //use space and comma as separator to gentle split the text + + int indexBefore = findBreakBefore(text, maxNumOfLetter); + if(indexBefore < (maxNumOfLetter / 2)) { + indexBefore = -1;//use more place + } + + String one, two; + if(indexBefore <= 0) { + //one word + indexBefore = Math.min(text.length(), maxNumOfLetter - 6); + one = text.substring(0, indexBefore) + "..."; + + int indexAfter = findBreakAfter(text, maxNumOfLetter); + if(indexAfter <= 0) { + two = text.substring(indexBefore); + } else { + two = text.substring(indexAfter); + } + } else { + one = text.substring(0, indexBefore + 1); + two = text.substring(indexBefore + 1); + } + + if(two.length() > maxNumOfLetter) { + two = two.substring(0, maxNumOfLetter - 6) + "..."; + } + return new String[] { one.trim(), two.trim() }; + } + return new String[]{ text }; + } + + public static int findBreakBefore(String line, int start) { + start = Math.min(line.length(), start); + for (int i = start; i >= 0; --i) { + char c = line.charAt(i); + if (Character.isWhitespace(c) || c == '-' || c == ',') { + return i; + } + } + return -1; + } + + public static int findBreakAfter(String line, int start) { + int len = line.length(); + for (int i = start; i < len; ++i) { + char c = line.charAt(i); + if (Character.isWhitespace(c) || c == ',') { + if(i + 1 < line.length()) { + return i + 1; + } + return i; + } + } + return -1; + } } 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 index c2571dc38b45154e497913146ad1a2a5f929b5ae..63b5658025581df9bf8e089dbc6bde2700098260 100644 --- a/src/main/java/org/olat/course/nodes/cl/ui/CheckboxPDFExport.java +++ b/src/main/java/org/olat/course/nodes/cl/ui/CheckboxPDFExport.java @@ -395,78 +395,4 @@ public class CheckboxPDFExport extends PdfDocument implements MediaResource { } return rows; } - - /** - * The number 6 was chosen after some trial and errors. It's a good compromise as - * the width of the letter is not fixed. Don't replace ... with ellipsis, it break - * the PDF. - * - * @param text - * @param maxWidth - * @param fontSize - * @return - * @throws IOException - */ - private String[] splitText(String text, float maxWidth, float fontSize) throws IOException { - float textWidth = getStringWidth(text, fontSize); - if(maxWidth < textWidth) { - - float letterWidth = textWidth / text.length(); - int maxNumOfLetter = Math.round(maxWidth / letterWidth) - 1; - //use space and comma as separator to gentle split the text - - int indexBefore = findBreakBefore(text, maxNumOfLetter); - if(indexBefore < (maxNumOfLetter / 2)) { - indexBefore = -1;//use more place - } - - String one, two; - if(indexBefore <= 0) { - //one word - indexBefore = Math.min(text.length(), maxNumOfLetter - 6); - one = text.substring(0, indexBefore) + "..."; - - int indexAfter = findBreakAfter(text, maxNumOfLetter); - if(indexAfter <= 0) { - two = text.substring(indexBefore); - } else { - two = text.substring(indexAfter); - } - } else { - one = text.substring(0, indexBefore + 1); - two = text.substring(indexBefore + 1); - } - - if(two.length() > maxNumOfLetter) { - two = two.substring(0, maxNumOfLetter - 6) + "..."; - } - return new String[] { one.trim(), two.trim() }; - } - return new String[]{ text }; - } - - public static int findBreakBefore(String line, int start) { - start = Math.min(line.length(), start); - for (int i = start; i >= 0; --i) { - char c = line.charAt(i); - if (Character.isWhitespace(c) || c == '-' || c == ',') { - return i; - } - } - return -1; - } - - public static int findBreakAfter(String line, int start) { - int len = line.length(); - for (int i = start; i < len; ++i) { - char c = line.charAt(i); - if (Character.isWhitespace(c) || c == ',') { - if(i + 1 < line.length()) { - return i + 1; - } - return i; - } - } - return -1; - } } \ No newline at end of file diff --git a/src/main/java/org/olat/modules/lecture/ui/LecturesBlockPDFExport.java b/src/main/java/org/olat/modules/lecture/ui/LecturesBlockPDFExport.java new file mode 100644 index 0000000000000000000000000000000000000000..5d42c338a68939d4e0bd0b6033beeacd710a6025 --- /dev/null +++ b/src/main/java/org/olat/modules/lecture/ui/LecturesBlockPDFExport.java @@ -0,0 +1,299 @@ +/** + * <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.modules.lecture.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.Identity; +import org.olat.core.id.User; +import org.olat.core.id.UserConstants; +import org.olat.core.logging.OLog; +import org.olat.core.logging.Tracing; +import org.olat.core.util.Formatter; +import org.olat.core.util.StringHelper; +import org.olat.core.util.pdf.PdfDocument; +import org.olat.modules.lecture.LectureBlock; + +/** + * + * Initial date: 22 juin 2017<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class LecturesBlockPDFExport extends PdfDocument implements MediaResource { + + private static final OLog log = Tracing.createLoggerFor(LecturesBlockPDFExport.class); + + private String resourceTitle; + private String teacher; + private final Translator translator; + private final LectureBlock lectureBlock; + + public LecturesBlockPDFExport(LectureBlock lectureBlock, Translator translator) + throws IOException { + super(translator.getLocale()); + + marginTopBottom = 62.0f; + marginLeftRight = 62.0f; + this.translator = translator; + this.lectureBlock = lectureBlock; + } + + public String getTeacher() { + return teacher; + } + + public void setTeacher(String teacher) { + this.teacher = teacher; + } + + public String getResourceTitle() { + return resourceTitle; + } + + public void setResourceTitle(String resourceTitle) { + this.resourceTitle = resourceTitle; + } + + @Override + public boolean acceptRanges() { + return false; + } + + @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 { + Formatter formatter = Formatter.getInstance(translator.getLocale()); + String filename = lectureBlock.getTitle() + + "_" + formatter.formatDate(lectureBlock.getStartDate()) + + "_" + formatter.formatTime(lectureBlock.getStartDate()) + + "-" + formatter.formatTime(lectureBlock.getEndDate()) + + ".pdf"; + 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(List<Identity> rows) + throws IOException, COSVisitorException, TransformerException { + addPage(); + String lectureBlockTitle = lectureBlock.getTitle(); + addMetadata(lectureBlockTitle, resourceTitle, teacher); + + if(StringHelper.containsNonWhitespace(lectureBlockTitle)) { + addParagraph(lectureBlockTitle, 16, true, width); + } + + float cellMargin = 5.0f; + float fontSize = 10.0f; + + String[] content = getRows(rows); + + int numOfRows = content.length; + for(int offset=0; offset<numOfRows; ) { + offset += drawTable(content, offset, fontSize, cellMargin); + closePage(); + if(offset<numOfRows) { + addPage(); + } + } + + addPageNumbers(); + } + + private String[] getRows(List<Identity> rows) { + int numOfRows = rows.size(); + + String[] content = new String[numOfRows]; + for(int i=0; i<numOfRows; i++) { + Identity row = rows.get(i); + content[i] = getName(row); + } + + return content; + } + + private String getName(Identity identity) { + StringBuilder sb = new StringBuilder(); + User user = identity.getUser(); + if(StringHelper.containsNonWhitespace(user.getLastName())) { + sb.append(user.getLastName()); + } + if(StringHelper.containsNonWhitespace(user.getFirstName())) { + if(sb.length() > 0) sb.append(", "); + sb.append(user.getFirstName()); + } + + String institutionalIdentifier = user.getProperty(UserConstants.INSTITUTIONALUSERIDENTIFIER, translator.getLocale()); + if(StringHelper.containsNonWhitespace(institutionalIdentifier)) { + if(sb.length() > 0) sb.append(", "); + sb.append(institutionalIdentifier); + } + return sb.toString(); + } + + public int drawTable(String[] content, int offset, float fontSize, float cellMargin) + throws IOException { + + float tableWidth = width; + float rowHeight = (lineHeightFactory * fontSize) + (2 * cellMargin); + float signatureColWidth = 150f; + + float nameMaxSizeWithMargin = tableWidth - signatureColWidth; + float nameMaxSize = nameMaxSizeWithMargin - (2 * cellMargin); + + float availableHeight = currentY - marginTopBottom - rowHeight; + + float[] rowHeights = new float[content.length]; + float usedHeight = 0.0f; + int possibleRows = 0; + for(int i = offset; i < content.length; i++) { + String name = content[i]; + float nameWidth = getStringWidth(name, fontSize); + float nameHeight; + if(nameWidth > nameMaxSize) { + nameHeight = rowHeight + (lineHeightFactory * fontSize); + } else { + nameHeight = rowHeight; + } + + if((usedHeight + nameHeight) > availableHeight) { + break; + } + usedHeight += nameHeight; + rowHeights[i] = nameHeight; + possibleRows++; + } + + int end = Math.min(offset + possibleRows, content.length); + int rows = end - offset; + + float tableHeight = usedHeight + rowHeight; + + // draw the horizontal line of the rows + float y = currentY; + float nexty = currentY; + drawLine(marginLeftRight, nexty, marginLeftRight + tableWidth, nexty, 0.5f); + nexty -= rowHeight; + for (int i =offset; i < end; i++) { + drawLine(marginLeftRight, nexty, marginLeftRight + tableWidth, nexty, 0.5f); + nexty -= rowHeights[i]; + } + drawLine(marginLeftRight, nexty, marginLeftRight + tableWidth, nexty, 0.5f); + + // draw the vertical line of the columns + float nextx = marginLeftRight; + drawLine(nextx, y, nextx, y - tableHeight, 0.5f); + nextx += nameMaxSizeWithMargin; + drawLine(nextx, y, nextx, y - tableHeight, 0.5f); + nextx += signatureColWidth; + drawLine(nextx, y, nextx, y - tableHeight, 0.5f); + + // now add the text + // draw the headers + final float textx = marginLeftRight + cellMargin; + float texty = currentY; + { + currentContentStream.beginText(); + currentContentStream.setFont(fontBold, fontSize); + currentContentStream.moveTextPositionByAmount(textx, texty - rowHeight + (2 * cellMargin)); + currentContentStream.drawString(translator.translate("pdf.table.header.participants")); + currentContentStream.endText(); + + currentContentStream.beginText(); + currentContentStream.setFont(fontBold, fontSize); + currentContentStream.moveTextPositionByAmount(textx + nameMaxSizeWithMargin, texty - rowHeight + (2 * cellMargin)); + currentContentStream.drawString(translator.translate("pdf.table.header.signature")); + currentContentStream.endText(); + } + + currentY -= rowHeight; + + //draw the content + texty = currentY - 15; + for (int i=offset; i<end; i++) { + String text = content[i]; + if(text == null) continue; + + if(rowHeights[i] > rowHeight + 1) { + //can do 2 lines + String[] texts = splitText(text, nameMaxSize, fontSize); + float lineTexty = texty; + for(int k=0; k<2 && k<texts.length; k++) { + String textLine = texts[k]; + currentContentStream.beginText(); + currentContentStream.setFont(font, fontSize); + currentContentStream.moveTextPositionByAmount(textx, lineTexty); + currentContentStream.drawString(textLine); + currentContentStream.endText(); + lineTexty -= (lineHeightFactory * fontSize); + } + } else { + currentContentStream.beginText(); + currentContentStream.setFont(font, fontSize); + currentContentStream.moveTextPositionByAmount(textx, texty); + currentContentStream.drawString(text); + currentContentStream.endText(); + } + texty -= rowHeights[i]; + } + return rows; + } +} \ No newline at end of file diff --git a/src/main/java/org/olat/modules/lecture/ui/TeacherLecturesTableController.java b/src/main/java/org/olat/modules/lecture/ui/TeacherLecturesTableController.java index 9aec2d4b9859ed6f546bb354ad4008af0ba7af0e..2436c21d608e59d37ae978a5f736456c3118a4b4 100644 --- a/src/main/java/org/olat/modules/lecture/ui/TeacherLecturesTableController.java +++ b/src/main/java/org/olat/modules/lecture/ui/TeacherLecturesTableController.java @@ -19,8 +19,12 @@ */ package org.olat.modules.lecture.ui; +import java.io.IOException; import java.util.List; +import javax.xml.transform.TransformerException; + +import org.apache.pdfbox.exceptions.COSVisitorException; import org.olat.NewControllerFactory; import org.olat.core.commons.persistence.SortKey; import org.olat.core.gui.UserRequest; @@ -59,6 +63,7 @@ import org.olat.modules.lecture.model.LectureBlockRow; import org.olat.modules.lecture.model.RollCallSecurityCallbackImpl; import org.olat.modules.lecture.ui.TeacherOverviewDataModel.TeachCols; import org.olat.modules.lecture.ui.component.LectureBlockStatusCellRenderer; +import org.olat.user.UserManager; import org.springframework.beans.factory.annotation.Autowired; /** @@ -82,6 +87,8 @@ public class TeacherLecturesTableController extends FormBasicController implemen private final String emptyI18nKey; private final boolean withRepositoryEntry, withTeachers; + @Autowired + private UserManager userManager; @Autowired private LectureModule lectureModule; @Autowired @@ -223,6 +230,20 @@ public class TeacherLecturesTableController extends FormBasicController implemen ureq.getDispatchResult().setResultingMediaResource(export); } + private void doExportAttendanceList(UserRequest ureq, LectureBlock row) { + LectureBlock lectureBlock = lectureService.getLectureBlock(row); + List<Identity> participants = lectureService.getParticipants(lectureBlock); + try { + LecturesBlockPDFExport export = new LecturesBlockPDFExport(lectureBlock, getTranslator()); + export.setTeacher(userManager.getUserDisplayName(getIdentity())); + export.setResourceTitle(lectureBlock.getEntry().getDisplayname()); + export.create(participants); + ureq.getDispatchResult().setResultingMediaResource(export); + } catch (COSVisitorException | IOException | TransformerException e) { + e.printStackTrace(); + } + } + private void doSelectLectureBlock(UserRequest ureq, LectureBlock block) { LectureBlock reloadedBlock = lectureService.getLectureBlock(block); List<Identity> participants = lectureService.startLectureBlock(getIdentity(), reloadedBlock); @@ -294,7 +315,8 @@ public class TeacherLecturesTableController extends FormBasicController implemen LectureBlock block = lectureService.getLectureBlock(row); doExportLectureBlock(ureq, block); } else if("attendance.list".equals(cmd)) { - getWindowControl().setWarning("Not implemented"); + LectureBlock block = lectureService.getLectureBlock(row); + doExportAttendanceList(ureq, block); } } } diff --git a/src/main/java/org/olat/modules/lecture/ui/TeacherRollCallController.java b/src/main/java/org/olat/modules/lecture/ui/TeacherRollCallController.java index d63b1062150196bce476dc4765a1cd403b4cd806..836a31ab9ebcd6a044f8dac7477d31f903f49f76 100644 --- a/src/main/java/org/olat/modules/lecture/ui/TeacherRollCallController.java +++ b/src/main/java/org/olat/modules/lecture/ui/TeacherRollCallController.java @@ -19,12 +19,16 @@ */ package org.olat.modules.lecture.ui; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.xml.transform.TransformerException; + +import org.apache.pdfbox.exceptions.COSVisitorException; import org.olat.basesecurity.BaseSecurityModule; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.form.flexible.FormItem; @@ -86,7 +90,8 @@ public class TeacherRollCallController extends FormBasicController { private FlexiTableElement tableEl; private TeacherRollCallDataModel tableModel; private FormSubmit quickSaveButton; - private FormLink reopenButton, cancelLectureBlockButton, closeLectureBlocksButton; + private FormLink exportAttendanceListButton, reopenButton, + cancelLectureBlockButton, closeLectureBlocksButton; private ReasonController reasonCtrl; private CloseableModalController cmc; @@ -212,6 +217,9 @@ public class TeacherRollCallController extends FormBasicController { tableEl.setCustomizeColumns(true); //buttons + exportAttendanceListButton = uifactory.addFormLink("attendance.list", formLayout, Link.BUTTON); + exportAttendanceListButton.setIconLeftCSS("o_icon o_icon_download"); + uifactory.addFormCancelButton("cancel", formLayout, ureq, getWindowControl()); quickSaveButton = uifactory.addFormSubmitButton("save", "save.temporary", formLayout); closeLectureBlocksButton = uifactory.addFormLink("close.lecture.blocks", formLayout, Link.BUTTON); @@ -439,6 +447,8 @@ public class TeacherRollCallController extends FormBasicController { } else if(cancelLectureBlockButton == source) { saveLectureBlocks(); doConfirmCancelLectureBlock(ureq); + } else if(this.exportAttendanceListButton == source) { + doExportAttendanceList(ureq); } else if(source instanceof FormLink) { FormLink link = (FormLink)source; String cmd = link.getCmd(); @@ -592,4 +602,16 @@ public class TeacherRollCallController extends FormBasicController { updateUI(); fireEvent(ureq, Event.CHANGED_EVENT); } + + private void doExportAttendanceList(UserRequest ureq) { + try { + LecturesBlockPDFExport export = new LecturesBlockPDFExport(lectureBlock, getTranslator()); + export.setTeacher(userManager.getUserDisplayName(getIdentity())); + export.setResourceTitle(lectureBlock.getEntry().getDisplayname()); + export.create(participants); + ureq.getDispatchResult().setResultingMediaResource(export); + } catch (COSVisitorException | IOException | TransformerException e) { + logError("", e); + } + } } diff --git a/src/main/java/org/olat/modules/lecture/ui/_content/rollcall.html b/src/main/java/org/olat/modules/lecture/ui/_content/rollcall.html index abe9aa64c8076dd332b9c39face0b5228dbdfff4..53fb1a81bdcf75d8eb0839796c3f7ca0c1bdba28 100644 --- a/src/main/java/org/olat/modules/lecture/ui/_content/rollcall.html +++ b/src/main/java/org/olat/modules/lecture/ui/_content/rollcall.html @@ -3,15 +3,19 @@ #if($lectureBlockOptional) <div class="o_warning">$r.translate("info.lecture.block.optional")</div> #end + #if($r.available("attendance.list")) + <div class="o_button_group o_button_group_right"> + $r.render("attendance.list") + </div> + #end <div class="o_desc">$off_desc</div> - $r.render("table") <div class="o_button_group"> $r.render("cancel") #if($r.available("save")) $r.render("save") #end - #if($r.available("cancel.lecture.blocks")) + #if($r.available("cancel.lecture.blocks")) $r.render("cancel.lecture.blocks") #end #if($r.available("close.lecture.blocks")) diff --git a/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_de.properties index 66c298170b6ca701cc3eb1de5ef2e4ece176dbb2..b5ec3b10c30044d76fb8cb45b6ed862bb2976bf7 100644 --- a/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_de.properties @@ -140,6 +140,8 @@ open=Offen open.course=Kurs öffnen partiallydone=Teilweise erledigt participant.rate=Schwellwert +pdf.table.header.participants=Teilnehmer +pdf.table.header.signature=Unterschrift planned.lectures=Geplante Lektionen previous.participant=Zur\u00FCck zum letzten Teilnehmer attendance.list=Anwesenheitsliste diff --git a/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_en.properties index 9e860458684a3ee3ffb9677ea55d494bf4671975..f810ed89b3eaec402f23b12f1a91854a3c1026e0 100644 --- a/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/modules/lecture/ui/_i18n/LocalStrings_en.properties @@ -140,6 +140,8 @@ open=Open open.course=Open course partiallydone=Partially done participant.rate=Rate +pdf.table.header.participants=Participants +pdf.table.header.signature=Signature planned.lectures=Planned lectures previous.participant=Back to previous participant reason=Reason