From bdf7e76f2212a52ea52ec8368060073de2af9b54 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Sun, 2 Feb 2025 18:15:02 +0100 Subject: [PATCH] progress --- docker/development/Dockerfile | 4 +- jdav_web/locale/de/LC_MESSAGES/django.po | 6 +- jdav_web/members/admin.py | 116 +++++--- jdav_web/members/excel.py | 18 +- .../members/locale/de/LC_MESSAGES/django.po | 259 ++++++++++++------ .../migrations/0035_ljpproposal_redo.py | 53 ++++ jdav_web/members/models.py | 41 ++- jdav_web/members/pdf.py | 23 +- .../admin/generate_seminar_report.html | 67 +++-- .../templates/members/seminar_report_docx.tex | 71 +++++ jdav_web/members/templatetags/tex_extras.py | 10 + .../freizeit/change_form_object_tools.html | 7 - requirements.txt | 1 + 13 files changed, 486 insertions(+), 190 deletions(-) create mode 100644 jdav_web/members/migrations/0035_ljpproposal_redo.py create mode 100644 jdav_web/members/templates/members/seminar_report_docx.tex diff --git a/docker/development/Dockerfile b/docker/development/Dockerfile index ca0cf04..da56878 100644 --- a/docker/development/Dockerfile +++ b/docker/development/Dockerfile @@ -1,7 +1,7 @@ -FROM python:3.9-bullseye +FROM python:3.9-bookworm # install additional dependencies -RUN apt-get update && apt-get install -y gettext texlive texlive-fonts-extra +RUN apt-get update && apt-get install -y gettext texlive texlive-fonts-extra pandoc WORKDIR /app diff --git a/jdav_web/locale/de/LC_MESSAGES/django.po b/jdav_web/locale/de/LC_MESSAGES/django.po index 4f805db..4005714 100644 --- a/jdav_web/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-02-02 00:45+0100\n" +"POT-Creation-Date: 2025-02-02 18:06+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -260,10 +260,6 @@ msgstr "SJR Antrag erstellen" msgid "Generate seminar report" msgstr "Landesjugendplan Antrag erstellen" -#: templates/admin/members/freizeit/change_form_object_tools.html -msgid "Generate LJP V-BK form" -msgstr "Erzeuge LJP V-BK Formular" - #: templates/admin/members/freizeit/change_form_object_tools.html msgid "Generate overview" msgstr "Hinweise für Jugendleiter*innen erstellen" diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index 548bd5f..975f2a0 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -28,8 +28,8 @@ from django.db.models import TextField, ManyToManyField, ForeignKey, Count,\ from django.forms import Textarea, RadioSelect, TypedChoiceField, CheckboxInput from django.shortcuts import render from django.core.exceptions import PermissionDenied, ValidationError -from .pdf import render_tex, fill_pdf_form, merge_pdfs, serve_pdf -from .excel import generate_group_overview, VBK_3_1, VBK_3_2, generate_ljp_vbk +from .pdf import render_tex, fill_pdf_form, merge_pdfs, serve_pdf, render_docx +from .excel import generate_group_overview, generate_ljp_vbk from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin @@ -1037,30 +1037,32 @@ class MemberNoteListAdmin(admin.ModelAdmin): summary.short_description = _('Generate PDF summary') -class GenerateSeminarReportForm(forms.Form): - modes = (('full', _('Full report')), - ('basic', _('Costs and participants only'))) - mode = forms.ChoiceField(choices=modes, label=_('Mode')) - prepend_v32 = forms.BooleanField(label=_('Prepend V32'), initial=True, - widget=CheckboxInput(attrs={'style': 'display: inherit'}), - required=False) - - -class GenerateVBKForm(forms.Form): - categories = ((VBK_3_1, _('Staff training')), - (VBK_3_2, _('Educational programme'))) - category = forms.ChoiceField(choices=categories, label=_('Category')) - - class GenerateSjrForm(forms.Form): - + def __init__(self, *args, **kwargs): self.attachments = kwargs.pop('attachments') - + super(GenerateSjrForm,self).__init__(*args,**kwargs) self.fields['invoice'] = forms.ChoiceField(choices=self.attachments, label=_('Invoice')) +def decorate_download(fun): + def aux(self, request, object_id): + try: + memberlist = Freizeit.objects.get(pk=object_id) + except Freizeit.DoesNotExist: + messages.error(request, _('Excursion not found.')) + return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) + if not self.may_view_excursion(request, memberlist): + return self.not_allowed_view(request, memberlist) + if not hasattr(memberlist, 'ljpproposal'): + messages.error(request, _('This excursion does not have a LJP proposal. Please add one and try again.')) + return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), + args=(memberlist.pk,))) + return fun(self, request, memberlist) + return aux + + class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): #inlines = [MemberOnListInline, LJPOnListInline, StatementOnListInline] form = FreizeitAdminForm @@ -1131,30 +1133,39 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): object=memberlist) return render(request, 'admin/generate_seminar_vbk.html', context=context) - def seminar_vbk(self, request, memberlist): + @decorate_download + def download_seminar_vbk(self, request, memberlist): + fp = generate_ljp_vbk(memberlist) + return serve_media(fp, 'application/xlsx') + + @decorate_download + def download_seminar_report_docx(self, request, memberlist): + title = memberlist.ljpproposal.title + context = dict(memberlist=memberlist, settings=settings) + return render_docx(title + '_Seminarbericht', 'members/seminar_report_docx.tex', context) + + @decorate_download + def download_seminar_report_costs_and_participants(self, request, memberlist): + title = memberlist.ljpproposal.title + context = dict(memberlist=memberlist, settings=settings) + return render_tex(title + '_Seminarbericht', 'members/seminar_report.tex', context) + + def seminar_report(self, request, memberlist): if not self.may_view_excursion(request, memberlist): return self.not_allowed_view(request, memberlist) - if "apply" in request.POST: - form = GenerateVBKForm(request.POST) - if not form.is_valid(): - messages.error(request, _('Please select a category.')) - return self.render_seminar_vbk_options(request, memberlist, form) - category = int(form.cleaned_data['category']) - title = memberlist.ljpproposal.title if hasattr(memberlist, 'ljpproposal') else memberlist.name - fp = generate_ljp_vbk(memberlist, category) - return serve_media(fp, 'application/xlsx') - return self.render_seminar_vbk_options(request, memberlist, GenerateVBKForm()) - - def render_seminar_report_options(self, request, memberlist, form): + if not hasattr(memberlist, 'ljpproposal'): + messages.error(request, _('This excursion does not have a LJP proposal. Please add one and try again.')) + return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), + args=(memberlist.pk,))) context = dict(self.admin_site.each_context(request), title=_('Generate seminar report'), opts=self.opts, memberlist=memberlist, - form=form, object=memberlist) return render(request, 'admin/generate_seminar_report.html', context=context) + seminar_report.short_description = _('Generate seminar report') - def seminar_report(self, request, memberlist): + def seminar_report_old(self, request, memberlist): if not self.may_view_excursion(request, memberlist): return self.not_allowed_view(request, memberlist) if "apply" in request.POST: @@ -1164,20 +1175,24 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): return self.render_seminar_report_options(request, memberlist, form) mode = form.cleaned_data['mode'] prepend_v32 = form.cleaned_data['prepend_v32'] + fmt = form.cleaned_data['fmt'] + title = memberlist.ljpproposal.title if hasattr(memberlist, 'ljpproposal') else memberlist.name if mode == 'full' and not hasattr(memberlist, 'ljpproposal'): messages.error(request, _('Full mode is only available, if the seminar report section is filled out.')) return self.render_seminar_report_options(request, memberlist, form) - title = memberlist.ljpproposal.title if hasattr(memberlist, 'ljpproposal') else memberlist.name context = dict(memberlist=memberlist, settings=settings, mode=mode) - fp = render_tex(title + '_Seminarbericht', 'members/seminar_report.tex', context, save_only=True) - if prepend_v32: - context = memberlist.v32_fields() - v32_fp = fill_pdf_form(title + "_LJP_V32", - 'members/V32-1_Themenorientierte_Bildungsmassnahmen.pdf', - context, - save_only=True) - return merge_pdfs(title + '_LJP_Antrag', [v32_fp, fp]) - return serve_pdf(fp) + if fmt == 'pdf': + fp = render_tex(title + '_Seminarbericht', 'members/seminar_report.tex', context, save_only=True) + if prepend_v32: + context = memberlist.v32_fields() + v32_fp = fill_pdf_form(title + "_LJP_V32", + 'members/V32-1_Themenorientierte_Bildungsmassnahmen.pdf', + context, + save_only=True) + return merge_pdfs(title + '_LJP_Antrag', [v32_fp, fp]) + return serve_pdf(fp) + else: + return render_docx(title + '_Seminarbericht', 'members/seminar_report_docx.tex', context) return self.render_seminar_report_options(request, memberlist, GenerateSeminarReportForm()) seminar_report.short_description = _('Generate seminar report') @@ -1256,6 +1271,19 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): wrap(self.action_view), name="%s_%s_action" % (self.opts.app_label, self.opts.model_name), ), + path( + "/download/ljp_vbk", + wrap(self.download_seminar_vbk), + name="%s_%s_download_ljp_vbk" % (self.opts.app_label, self.opts.model_name), + ), + path("/download/ljp_report_docx", + wrap(self.download_seminar_report_docx), + name="%s_%s_download_ljp_report_docx" % (self.opts.app_label, self.opts.model_name), + ), + path("/download/ljp_report_costs_and_participants", + wrap(self.download_seminar_report_costs_and_participants), + name="%s_%s_download_ljp_costs_participants" % (self.opts.app_label, self.opts.model_name), + ), ] return custom_urls + urls diff --git a/jdav_web/members/excel.py b/jdav_web/members/excel.py index 4b9e83d..0780652 100644 --- a/jdav_web/members/excel.py +++ b/jdav_web/members/excel.py @@ -4,7 +4,7 @@ import xlsxwriter import openpyxl from django.conf import settings from contrib.media import media_path, find_template -from .models import WEEKDAYS +from .models import WEEKDAYS, LJPProposal def generate_group_overview(all_groups, limit_to_public = True): """ @@ -70,21 +70,19 @@ def generate_group_overview(all_groups, limit_to_public = True): return filename -VBK_3_1, VBK_3_2 = 1, 2 VBK_TEMPLATES = { - VBK_3_1: 'members/LJP_VBK_3-1.xlsx', - VBK_3_2: 'members/LJP_VBK_3-2.xlsx', + LJPProposal.LJP_STAFF_TRAINING: 'members/LJP_VBK_3-1.xlsx', + LJPProposal.LJP_EDUCATIONAL: 'members/LJP_VBK_3-2.xlsx', } -def generate_ljp_vbk(excursion, mode): +def generate_ljp_vbk(excursion): """ Generate the VBK forms for LJP given an excursion. Returns the filename to the filled excel file. """ - print(mode, VBK_TEMPLATES, mode in VBK_TEMPLATES) - if not mode in VBK_TEMPLATES: - raise ValueError(f"Invalid mode {mode}.") - template_path = VBK_TEMPLATES[mode] + if not hasattr(excursion, 'ljpproposal'): + raise ValueError(f"Excursion has no LJP proposal.") + template_path = VBK_TEMPLATES[excursion.ljpproposal.category] path = find_template(template_path) workbook = openpyxl.load_workbook(path) @@ -111,6 +109,6 @@ def generate_ljp_vbk(excursion, mode): if hasattr(excursion, 'statement'): sheet['Q19'] = f"{excursion.statement.total_theoretic}" - filename = f"LJP_V-BK_3.{mode}_{title}.xlsx" + filename = f"LJP_V-BK_3.{excursion.ljpproposal.category}_{title}.xlsx" workbook.save(media_path(filename)) return filename diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po index cefb895..f65e3de 100644 --- a/jdav_web/members/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/members/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-02-02 00:45+0100\n" +"POT-Creation-Date: 2025-02-02 18:06+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -336,36 +336,19 @@ msgid "Generate PDF summary" msgstr "Übersicht erstellen" #: members/admin.py -msgid "Full report" -msgstr "Vollständiger Seminarbericht" - -#: members/admin.py -msgid "Costs and participants only" -msgstr "Nur Kosten und Teilnehmende" - -#: members/admin.py -msgid "Mode" -msgstr "Modus" - -#: members/admin.py -msgid "Prepend V32" -msgstr "V32 Formblatt einfügen" +msgid "Invoice" +msgstr "Beleg" #: members/admin.py -msgid "Staff training" -msgstr "Jugendleiter*innenweiterbildung" +msgid "Excursion not found." +msgstr "Ausfahrt nicht gefunden." #: members/admin.py -msgid "Educational programme" -msgstr "Themenorientierte Bildungsmaßnahme" - -#: members/admin.py members/models.py -msgid "Category" -msgstr "Kategorie" - -#: members/admin.py -msgid "Invoice" -msgstr "Beleg" +msgid "" +"This excursion does not have a LJP proposal. Please add one and try again." +msgstr "" +"Diese Ausfahrt hat keinen Seminarbericht. Bitte füge einen hinzu und " +"versuche es erneut." #: members/admin.py msgid "" @@ -395,10 +378,6 @@ msgstr "Hinweise für Jugendleiter erstellen" msgid "Generate LJP V-BK form" msgstr "Erzeuge LJP V-BK Formular" -#: members/admin.py -msgid "Please select a category." -msgstr "Bitte wähle eine Kategorie aus." - #: members/admin.py members/templates/admin/generate_seminar_report.html msgid "Generate seminar report" msgstr "Landesjugendplan Antrag erstellen" @@ -970,24 +949,64 @@ msgid "registration passwords" msgstr "Registrierungspasswörter" #: members/models.py -msgid "Alpinistic goals" -msgstr "Alpintechnische Ziele" +msgid "" +"Official title of your seminar, this can differ from the informal title. Use " +"e.g. sports climbing course instead of climbing weekend for fun." +msgstr "" +"Offizieller Titel des Seminars, dieser weicht in der Regel vom informellen " +"Titel ab. Verwende zum Beispiel Sportkletterkurs statt Kletterfreizeit." + +#: members/models.py +msgid "Educational programme" +msgstr "Themenorientierte Bildungsmaßnahme" + +#: members/models.py +msgid "Staff training" +msgstr "Jugendleiter*innenweiterbildung" + +#: members/models.py +msgid "Category" +msgstr "Kategorie" #: members/models.py -msgid "Pedagogic goals" -msgstr "Pädagogische Ziele" +msgid "Type of seminar. Usually the correct choice is educational programme." +msgstr "Kurstyp. In der Regel Themenorientierte Bildungsmaßnahme." #: members/models.py -msgid "Content and methods" -msgstr "Inhalte und Methoden" +msgid "Qualification" +msgstr "Qualifizierung" #: members/models.py -msgid "Evaluation" -msgstr "Wertung" +msgid "Participation" +msgstr "Partizipation" #: members/models.py -msgid "Experiences and possible improvements" -msgstr "Erfahrungen und Verbesserungsvorschläge" +msgid "Personality development" +msgstr "Persönlichkeitsentwicklung" + +#: members/models.py +msgid "Environment" +msgstr "Umwelt" + +#: members/models.py +msgid "Learning goal" +msgstr "Bildungsziel" + +#: members/models.py +msgid "Official learning goal according to LJP regulations." +msgstr "Offizielles Bildungsziel gemäß LJP Richtlinien." + +#: members/models.py +msgid "Strategy" +msgstr "Zielverfolgung- und Erreichung" + +#: members/models.py +msgid "" +"How do you want to reach the learning goal? Has the goal been reached? If " +"not, why not? If yes, what helped you to reach the goal?" +msgstr "" +"Wie wolltet ihr das Bildungsziel erreichen? Ist das Ziel so erreicht worden? " +"Wenn nicht, warum nicht? Wenn ja, was hat geholfen, das Ziel zu erreichen?" #: members/models.py msgid "LJP Proposal" @@ -1113,7 +1132,6 @@ msgstr "Zurück auf die Warteliste setzen" #: members/templates/admin/demote_to_waiter.html #: members/templates/admin/freizeit_finance_overview.html -#: members/templates/admin/generate_seminar_report.html #: members/templates/admin/generate_seminar_vbk.html #: members/templates/admin/generate_sjr_application.html #: members/templates/admin/invite_as_user.html @@ -1352,64 +1370,60 @@ msgstr "" "schnellstmöglich auf dich zurück." #: members/templates/admin/freizeit_finance_overview.html +#: members/templates/admin/generate_seminar_report.html #: members/templates/admin/invite_for_group_text.html msgid "Back" msgstr "Zurück" #: members/templates/admin/generate_seminar_report.html -msgid "" -"Here you can generate a seminar report suitable for the LJP. A report\n" -"always contains a head page with the basic information on the seminar." -msgstr "" -"Hier kannst du einen Zuschussantrag für den Landesjugendplan (LJP) " -"erstellen. Ein solcher Antrag besteht immer aus zwei Teilen: Einem Formblatt " -"und einem Seminarbericht. Ein Bericht enthält immer einen Kopf mit den " -"Stammdaten des Seminars. Darüber hinaus muss der Seminarbericht eine " -"Teilnehemendenliste, eine Kostenübersicht und eine detaillierte didaktische " -"Planung enthalten. " +msgid "LJP application for" +msgstr "Landesjugendplan Antrag für" #: members/templates/admin/generate_seminar_report.html msgid "" -"Expenses with same short description are automatically summed up and shown " -"as one expense in the\n" -"expense overview." +"For applying for contributions by the LJP, a seminar report is required. " +"From the information\n" +"that you entered in the excursion, you can automatically generate such a " +"report here." msgstr "" -"In der Kostenübersicht werden Ausgaben mit der gleichen Kurzbeschreibung " -"automatisch aufsummiert und zu einer Ausgabe zusammengefasst." +"Um Zuschüsse vom Landesjugendplan (LJP) zu beantragen, ist ein Antrag " +"notwendig. Aus den Informationen, die du in der Ausfahrt angegeben hast, " +"kann automatisch ein solcher Antrag erstellt werden." #: members/templates/admin/generate_seminar_report.html -msgid "" -"Full report: Include learning goals and a detailed, tabularized time " -"schedule. This requires\n" -"the seminar report section to be filled out." -msgstr "" -"Vollständiger Bericht: Stelle Lernziele und einen detaillierten, " -"tabellierten Zeitplan dar. Dies benötigt, dass der Seminarbericht in der " -"Ausfahrt ausgefüllt ist." +msgid "A seminar report consists of multiple components:" +msgstr "Ein LJP Antrag besteht aus verschiedenen Komponenten:" #: members/templates/admin/generate_seminar_report.html msgid "" -"Costs and participants only: Only show a list of participants and costs. In " -"this case you\n" -"have to add learning goals and a time schedule manually." +"An excel sheet containing the basic data of the seminar. This is also called " +"the V-BK form." msgstr "" -"Nur Kosten und Teilnehmende: Zeige nur eine Liste von Teilnehmenden und " -"Kosten an. In diesem Fall musst du Lernziele und einen Zeitplan manuell " -"hinzufügen." +"Eine Excel Tabelle mit den Basisdaten des Kurses. Fachbegriff: V-BK Formular." -#: members/templates/admin/generate_seminar_report.html members/tests.py -msgid "You may also choose to include the V32 attachment." +#: members/templates/admin/generate_seminar_report.html +msgid "Download" +msgstr "Herunterladen" + +#: members/templates/admin/generate_seminar_report.html +msgid "" +"A pedagocial report on the strategy and outcome of the seminar with respect " +"to achieving the\n" +"learning goal.\n" +"This also includes a detailed, tabularized time schedule and is produced as " +"an editable Microsoft Word document." msgstr "" -"Ein LJP Antrag benötigt immer ein Formblatt (in unserem Fall V32-1 " -"Themenorientierte Bildungsmaßnahmen). Dieses kannst du automatisch " -"vorausfüllen lassen und dem Antrag hinzufügen. Bitte fülle die verbleibenden " -"Felder im Formblatt selbst aus und unterschreibe das PDF." +"Ein pädagogischer Bericht zur Planung, zum Ablauf und zur Auswertung des " +"Kurses. Dies beinhaltet auch einen detaillierten, tabellarischen, zeitlichen " +"Ablauf und wird als veränderbares Word Dokument erstellt." #: members/templates/admin/generate_seminar_report.html -#: members/templates/admin/generate_seminar_vbk.html -#: members/templates/admin/generate_sjr_application.html -msgid "Generate" -msgstr "Erstellen" +msgid "" +"A cost and participants overview. This is not required for the actual " +"application, but is provided for convience as a PDF document." +msgstr "" +"Eine Kosten- und Teilnehmendenübersicht. Dies ist nicht notwendig für den " +"eigentlichen Bericht, muss aber langfristig aufbewahrt werden." #: members/templates/admin/generate_seminar_vbk.html msgid "" @@ -1433,8 +1447,12 @@ msgstr "" #: members/templates/admin/generate_seminar_vbk.html msgid "" "Depending on the type of seminar, please select one of the two options below." -msgstr "" -"Bitte wähle aus, um welche Art von Seminar es sich handelt." +msgstr "Bitte wähle aus, um welche Art von Seminar es sich handelt." + +#: members/templates/admin/generate_seminar_vbk.html +#: members/templates/admin/generate_sjr_application.html +msgid "Generate" +msgstr "Erstellen" #: members/templates/admin/generate_sjr_application.html members/tests.py msgid "Here you can generate an allowance application for the SJR." @@ -1975,6 +1993,14 @@ msgstr "" "Danke %(prename)s für dein Interesse auf der Warteliste zu bleiben.\n" "Dein Platz wurde bestätigt." +#: members/tests.py +msgid "You may also choose to include the V32 attachment." +msgstr "" +"Ein LJP Antrag benötigt immer ein Formblatt (in unserem Fall V32-1 " +"Themenorientierte Bildungsmaßnahmen). Dieses kannst du automatisch " +"vorausfüllen lassen und dem Antrag hinzufügen. Bitte fülle die verbleibenden " +"Felder im Formblatt selbst aus und unterschreibe das PDF." + #: members/tests.py msgid "This field is required." msgstr "" @@ -2017,6 +2043,73 @@ msgstr "Optionale zusätzliche E-Mailadresse" msgid "Invalid emergency contacts" msgstr "Ungültige Notfallkontakte" +#~ msgid "Full report" +#~ msgstr "Vollständiger Seminarbericht" + +#~ msgid "Costs and participants only" +#~ msgstr "Nur Kosten und Teilnehmende" + +#~ msgid "Mode" +#~ msgstr "Modus" + +#~ msgid "Prepend V32" +#~ msgstr "V32 Formblatt einfügen" + +#~ msgid "Please select a category." +#~ msgstr "Bitte wähle eine Kategorie aus." + +#~ msgid "Alpinistic goals" +#~ msgstr "Alpintechnische Ziele" + +#~ msgid "Pedagogic goals" +#~ msgstr "Pädagogische Ziele" + +#~ msgid "Content and methods" +#~ msgstr "Inhalte und Methoden" + +#~ msgid "Evaluation" +#~ msgstr "Wertung" + +#~ msgid "Experiences and possible improvements" +#~ msgstr "Erfahrungen und Verbesserungsvorschläge" + +#~ msgid "" +#~ "Here you can generate a seminar report suitable for the LJP. A report\n" +#~ "always contains a head page with the basic information on the seminar." +#~ msgstr "" +#~ "Hier kannst du einen Zuschussantrag für den Landesjugendplan (LJP) " +#~ "erstellen. Ein solcher Antrag besteht immer aus zwei Teilen: Einem " +#~ "Formblatt und einem Seminarbericht. Ein Bericht enthält immer einen Kopf " +#~ "mit den Stammdaten des Seminars. Darüber hinaus muss der Seminarbericht " +#~ "eine Teilnehemendenliste, eine Kostenübersicht und eine detaillierte " +#~ "didaktische Planung enthalten. " + +#~ msgid "" +#~ "Expenses with same short description are automatically summed up and " +#~ "shown as one expense in the\n" +#~ "expense overview." +#~ msgstr "" +#~ "In der Kostenübersicht werden Ausgaben mit der gleichen Kurzbeschreibung " +#~ "automatisch aufsummiert und zu einer Ausgabe zusammengefasst." + +#~ msgid "" +#~ "Full report: Include learning goals and a detailed, tabularized time " +#~ "schedule. This requires\n" +#~ "the seminar report section to be filled out." +#~ msgstr "" +#~ "Vollständiger Bericht: Stelle Lernziele und einen detaillierten, " +#~ "tabellierten Zeitplan dar. Dies benötigt, dass der Seminarbericht in der " +#~ "Ausfahrt ausgefüllt ist." + +#~ msgid "" +#~ "Costs and participants only: Only show a list of participants and costs. " +#~ "In this case you\n" +#~ "have to add learning goals and a time schedule manually." +#~ msgstr "" +#~ "Nur Kosten und Teilnehmende: Zeige nur eine Liste von Teilnehmenden und " +#~ "Kosten an. In diesem Fall musst du Lernziele und einen Zeitplan manuell " +#~ "hinzufügen." + #, python-format #~ msgid "" #~ "This excursion has %(approved_count)s approved youth leaders, but you " diff --git a/jdav_web/members/migrations/0035_ljpproposal_redo.py b/jdav_web/members/migrations/0035_ljpproposal_redo.py new file mode 100644 index 0000000..ffb6e2e --- /dev/null +++ b/jdav_web/members/migrations/0035_ljpproposal_redo.py @@ -0,0 +1,53 @@ +# Generated by Django 4.0.1 on 2025-02-02 17:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0034_activitycategory_ljp_category'), + ] + + operations = [ + migrations.RemoveField( + model_name='ljpproposal', + name='evaluation', + ), + migrations.RemoveField( + model_name='ljpproposal', + name='experiences', + ), + migrations.RemoveField( + model_name='ljpproposal', + name='goals_alpinistic', + ), + migrations.RemoveField( + model_name='ljpproposal', + name='goals_pedagogic', + ), + migrations.RemoveField( + model_name='ljpproposal', + name='methods', + ), + migrations.AddField( + model_name='ljpproposal', + name='goal', + field=models.IntegerField(choices=[(1, 'Qualification'), (2, 'Participation'), (3, 'Personality development'), (4, 'Environment')], default=1, help_text='Official learning goal according to LJP regulations.', verbose_name='Learning goal'), + ), + migrations.AddField( + model_name='ljpproposal', + name='goal_strategy', + field=models.TextField(blank=True, default='', help_text='How do you want to reach the learning goal? Has the goal been reached? If not, why not? If yes, what helped you to reach the goal?', verbose_name='Strategy'), + ), + migrations.AlterField( + model_name='ljpproposal', + name='title', + field=models.CharField(blank=True, default='', help_text='Official title of your seminar, this can differ from the informal title. Use e.g. sports climbing course instead of climbing weekend for fun.', max_length=30, verbose_name='Title'), + ), + migrations.AddField( + model_name='ljpproposal', + name='category', + field=models.IntegerField(choices=[(2, 'Educational programme'), (1, 'Staff training')], default=2, help_text='Type of seminar. Usually the correct choice is educational programme.', verbose_name='Category'), + ), + ] diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index 4a9c504..a7dd713 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -1146,6 +1146,13 @@ class Freizeit(CommonModel): return full_days + extra_days + @property + def total_intervention_hours(self): + if hasattr(self, 'ljpproposal'): + return sum([i.duration for i in self.ljpproposal.intervention_set.all()]) + else: + return 0 + @property def staff_count(self): return self.jugendleiter.count() @@ -1471,13 +1478,33 @@ class RegistrationPassword(models.Model): class LJPProposal(CommonModel): """A proposal for LJP""" - title = models.CharField(verbose_name=_('Title'), max_length=30) - - goals_alpinistic = models.TextField(verbose_name=_('Alpinistic goals')) - goals_pedagogic = models.TextField(verbose_name=_('Pedagogic goals')) - methods = models.TextField(verbose_name=_('Content and methods')) - evaluation = models.TextField(verbose_name=_('Evaluation')) - experiences = models.TextField(verbose_name=_('Experiences and possible improvements')) + title = models.CharField(verbose_name=_('Title'), max_length=30, + blank=True, default='', + help_text=_('Official title of your seminar, this can differ from the informal title. Use e.g. sports climbing course instead of climbing weekend for fun.')) + + LJP_STAFF_TRAINING, LJP_EDUCATIONAL = 1, 2 + LJP_CATEGORIES = [ + (LJP_EDUCATIONAL, _('Educational programme')), + (LJP_STAFF_TRAINING, _('Staff training')) + ] + category = models.IntegerField(verbose_name=_('Category'), + choices=LJP_CATEGORIES, + default=2, + help_text=_('Type of seminar. Usually the correct choice is educational programme.')) + LJP_QUALIFICATION, LJP_PARTICIPATION, LJP_DEVELOPMENT, LJP_ENVIRONMENT = 1, 2, 3, 4 + LJP_GOALS = [ + (LJP_QUALIFICATION, _('Qualification')), + (LJP_PARTICIPATION, _('Participation')), + (LJP_DEVELOPMENT, _('Personality development')), + (LJP_ENVIRONMENT, _('Environment')), + ] + goal = models.IntegerField(verbose_name=_('Learning goal'), + choices=LJP_GOALS, + default=1, + help_text=_('Official learning goal according to LJP regulations.')) + goal_strategy = models.TextField(verbose_name=_('Strategy'), + help_text=_('How do you want to reach the learning goal? Has the goal been reached? If not, why not? If yes, what helped you to reach the goal?'), + blank=True, default='') excursion = models.OneToOneField(Freizeit, verbose_name=_('Excursion'), diff --git a/jdav_web/members/pdf.py b/jdav_web/members/pdf.py index 9ffb88a..fa12007 100644 --- a/jdav_web/members/pdf.py +++ b/jdav_web/members/pdf.py @@ -19,13 +19,12 @@ def serve_pdf(filename_pdf): return serve_media(filename_pdf, 'application/pdf') -def render_tex(name, template_path, context, save_only=False): +def generate_tex(name, template_path, context): filename = name + "_" + datetime.today().strftime("%d_%m_%Y") filename = filename.replace(' ', '_').replace('&', '').replace('/', '_') # drop umlauts, accents etc. filename = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore').decode() filename_tex = filename + '.tex' - filename_pdf = filename + '.pdf' tmpl = get_template(template_path) res = tmpl.render(dict(context, creation_date=datetime.today().strftime('%d.%m.%Y'))) @@ -34,7 +33,27 @@ def render_tex(name, template_path, context, save_only=False): with open(media_path(filename_tex), 'w', encoding='utf-8') as f: f.write(res) + return filename + + +def render_docx(name, template_path, context, save_only=False): + filename = generate_tex(name, template_path, context) + filename_tex = filename + '.tex' + filename_docx = filename + '.docx' + oldwd = os.getcwd() + os.chdir(media_dir()) + subprocess.call(['pandoc', filename_tex, '-o', filename_docx]) + time.sleep(1) + os.chdir(oldwd) + if save_only: + return filename_docx + return serve_media(filename_docx, 'application/docx') + +def render_tex(name, template_path, context, save_only=False): + filename = generate_tex(name, template_path, context) + filename_tex = filename + '.tex' + filename_pdf = filename + '.pdf' # compile using pdflatex oldwd = os.getcwd() os.chdir(media_dir()) diff --git a/jdav_web/members/templates/admin/generate_seminar_report.html b/jdav_web/members/templates/admin/generate_seminar_report.html index 6a52699..f52546e 100644 --- a/jdav_web/members/templates/admin/generate_seminar_report.html +++ b/jdav_web/members/templates/admin/generate_seminar_report.html @@ -23,41 +23,48 @@ {% endblock %} {% block content %} +

{% trans 'LJP application for' %}: {{ memberlist.ljpproposal.title }} ({{ memberlist.name }})

-{% blocktrans %}Here you can generate a seminar report suitable for the LJP. A report -always contains a head page with the basic information on the seminar.{% endblocktrans %} +{% blocktrans %}For applying for contributions by the LJP, a seminar report is required. From the information +that you entered in the excursion, you can automatically generate such a report here.{% endblocktrans %}

-{% blocktrans %}Expenses with same short description are automatically summed up and shown as one expense in the -expense overview.{% endblocktrans %} +{% blocktrans %}A seminar report consists of multiple components:{% endblocktrans %}

-
    -
  • -{% blocktrans %}Full report: Include learning goals and a detailed, tabularized time schedule. This requires -the seminar report section to be filled out.{% endblocktrans %} -
  • -
  • -{% blocktrans %}Costs and participants only: Only show a list of participants and costs. In this case you -have to add learning goals and a time schedule manually.{% endblocktrans %} -
  • -
-
-

{% blocktrans %}You may also choose to include the V32 attachment.{% endblocktrans %}

+

+ + + + + + + + + + + + + +
+{% blocktrans %}An excel sheet containing the basic data of the seminar. This is also called the V-BK form.{% endblocktrans %} + +{% translate "Download" %} +
+{% blocktrans %}A pedagocial report on the strategy and outcome of the seminar with respect to achieving the +learning goal. +This also includes a detailed, tabularized time schedule and is produced as an editable Microsoft Word document.{% endblocktrans %} + +{% translate "Download" %} +
+{% blocktrans %}A cost and participants overview. This is not required for the actual application, but is provided for convience as a PDF document.{% endblocktrans %} + +{% translate "Download" %} +
+

-
- {% csrf_token %} -

- - {{ form }} -
-

-
- - - - {% translate "Cancel" %} -
+

+{% translate "Back" %} +

{% endblock %} diff --git a/jdav_web/members/templates/members/seminar_report_docx.tex b/jdav_web/members/templates/members/seminar_report_docx.tex new file mode 100644 index 0000000..d5f666b --- /dev/null +++ b/jdav_web/members/templates/members/seminar_report_docx.tex @@ -0,0 +1,71 @@ +{% load tex_extras %} + +\documentclass[a4paper]{article} + +\usepackage[utf8]{inputenc} +\usepackage{booktabs} +\usepackage{amssymb} +\usepackage{cmbright} +\usepackage{graphicx} +\usepackage{textpos} +\usepackage[colorlinks, breaklinks]{hyperref} +\usepackage{float} +\usepackage[margin=1in]{geometry} +\usepackage{array} +\usepackage{ragged2e} +\usepackage{tabularx} +\usepackage{titlesec} + +\titleformat{\section} + {\Large\slshape}{\thesection\;} + {0em}{} + +\title{Seminarbericht} + +\begin{document} + +\maketitle + +% DESCRIPTION TABLE +\begin{table}[H] + \begin{tabular}{ll} + \textbf{Sektion:} & {{ settings.SEKTION }} \\ + \textbf{Titel der Maßnahme:} & {% if not memberlist.ljpproposal %}{{ memberlist.name|esc_all }}{% else %}{{ memberlist.ljpproposal.title }} {% endif %} \\ + \textbf{Anzahl der durchgeführten Lehrgangstage:} & {{ memberlist.duration }} \\ + \end{tabular} +\end{table} + +\section{Bildungsziel} + +\begin{table}[H] + \begin{tabular}{ccllllllllllll} + {% if memberlist.ljpproposal.goal == 1 %}x{% endif %}& 1 & \multicolumn{12}{l}{Ehrenamtliche qualifizieren und stärken} \\ + {% if memberlist.ljpproposal.goal == 2 %}x{% endif %}& 2 & \multicolumn{12}{l}{Erleben von demokratischen Prozessen. Entwickeln und Stärken eines Demokratieverständnisses.} \\ + {% if memberlist.ljpproposal.goal == 3 %}x{% endif %}& 3 & \multicolumn{12}{l}{Entwicklung der Persönlichkeit und Erweiterung des sozialen Handlungsrepertoires.} \\ + {% if memberlist.ljpproposal.goal == 4 %}x{% endif %}& 4 & \multicolumn{12}{l}{Bewusstsein schaffen einer Verantwortung für Natur, Umwelt und zukünftige Generationen.} \\ + \end{tabular} +\end{table} + +\section{Zielverfolgung- und Erreichung} + +{{ memberlist.ljpproposal.goals|esc_all }} + +\section{Zeitlicher Ablauf} + +\begin{table}[H] + \begin{tabular}{lllllll} + \toprule + \textbf{Datum} & \textbf{Uhrzeit} & \multicolumn{4}{l}{\textbf{Art der Aktion}} & \textbf{Dauer} \\ + \midrule + {% for intervention in memberlist.ljpproposal.intervention_set.all %} + {{ intervention.date_start|date_short }} + & {{ intervention.date_start|time_short }} + & \multicolumn{4}{l}{ {{ intervention.activity|esc_all }} } + & {{ intervention.duration }} h \\ + {% endfor %} + \bottomrule + & & \multicolumn{4}{l}{} & Summe: {{ memberlist.total_intervention_hours }} h \\ + \end{tabular} +\end{table} + +\end{document} diff --git a/jdav_web/members/templatetags/tex_extras.py b/jdav_web/members/templatetags/tex_extras.py index ac9b4ef..beb678d 100644 --- a/jdav_web/members/templatetags/tex_extras.py +++ b/jdav_web/members/templatetags/tex_extras.py @@ -18,3 +18,13 @@ def esc_all(val): @register.filter def datetime_short(date): return date.strftime('%d.%m.%Y %H:%M') + + +@register.filter +def date_short(date): + return date.strftime('%d.%m.%y') + + +@register.filter +def time_short(date): + return date.strftime('%H:%M') diff --git a/jdav_web/templates/admin/members/freizeit/change_form_object_tools.html b/jdav_web/templates/admin/members/freizeit/change_form_object_tools.html index c52ac75..f27115c 100644 --- a/jdav_web/templates/admin/members/freizeit/change_form_object_tools.html +++ b/jdav_web/templates/admin/members/freizeit/change_form_object_tools.html @@ -24,13 +24,6 @@ -
  • -
    - {% csrf_token %} - -
    -
  • -
  • {% csrf_token %} diff --git a/requirements.txt b/requirements.txt index 1ad9aa9..08b607e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,6 +33,7 @@ kombu==5.2.3 Markdown==3.4.3 MarkupSafe==3.0.2 mysqlclient==2.1.0 +openpyxl==3.1.5 packaging==24.2 Pillow==9.0.0 prompt-toolkit==3.0.24