From 8b03deda5bea524153704f18a58d27a82ef04ab9 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Fri, 7 Feb 2025 22:06:14 +0100 Subject: [PATCH] members/admin: adapt LJP application to new format (#121) Implements the new LJP application format as outlined in #116. In particular: - The V-32 PDF is replaced by an `.xlsx` sheet. - The LaTeX generated PDF for the seminar report is replaced by a `.docx` file, generated by `pandoc` from a modified `.tex` file. - The cost and participants overview from the old PDF can still be generated separately, but is no longer required. Also adds many fields to `LJPProposal` that are required to generate the full application. --- docker/development/Dockerfile | 4 +- docker/production/Dockerfile | 4 +- docker/test/Dockerfile | 4 +- jdav_web/contrib/media.py | 11 + jdav_web/members/admin.py | 102 +++--- jdav_web/members/excel.py | 66 +++- .../members/locale/de/LC_MESSAGES/django.po | 321 +++++++++++++----- .../migrations/0035_ljp_application_rework.py | 68 ++++ jdav_web/members/models.py | 56 ++- jdav_web/members/pdf.py | 34 +- .../admin/generate_seminar_report.html | 67 ++-- .../templates/members/LJP_VBK_3-1.xlsx | Bin 0 -> 17651 bytes .../templates/members/LJP_VBK_3-2.xlsx | Bin 0 -> 17648 bytes .../templates/members/seminar_report.tex | 2 +- .../templates/members/seminar_report_docx.tex | 71 ++++ jdav_web/members/templatetags/tex_extras.py | 10 + jdav_web/members/tests.py | 61 +++- requirements.txt | 1 + 18 files changed, 688 insertions(+), 194 deletions(-) create mode 100644 jdav_web/members/migrations/0035_ljp_application_rework.py create mode 100644 jdav_web/members/templates/members/LJP_VBK_3-1.xlsx create mode 100644 jdav_web/members/templates/members/LJP_VBK_3-2.xlsx 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/docker/production/Dockerfile b/docker/production/Dockerfile index bf51753..47f31d4 100644 --- a/docker/production/Dockerfile +++ b/docker/production/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 # create user RUN groupadd -g 501 app && useradd -g 501 -u 501 -m -d /app app diff --git a/docker/test/Dockerfile b/docker/test/Dockerfile index a4de64d..3005167 100644 --- a/docker/test/Dockerfile +++ b/docker/test/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 # create user RUN groupadd -g 501 app && useradd -g 501 -u 501 -m -d /app app diff --git a/jdav_web/contrib/media.py b/jdav_web/contrib/media.py index 4ff27c9..d5e873b 100644 --- a/jdav_web/contrib/media.py +++ b/jdav_web/contrib/media.py @@ -1,9 +1,20 @@ import os from django.conf import settings from django.http import HttpResponse +from django import template +from django.template.loader import get_template from wsgiref.util import FileWrapper +def find_template(template_name): + for engine in template.engines.all(): + for loader in engine.engine.template_loaders: + for origin in loader.get_template_sources(template_name): + if os.path.exists(origin.name): + return origin.name + raise template.TemplateDoesNotExist(f"Could not find template: {template_name}") + + def media_path(fp): return os.path.join(os.path.join(settings.MEDIA_ROOT, "memberlists"), fp) diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index 3c761f9..3dead73 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 +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 @@ -844,7 +844,7 @@ class GroupAdmin(CommonAdminMixin, admin.ModelAdmin): class ActivityCategoryAdmin(admin.ModelAdmin): - fields = ['name', 'description'] + fields = ['name', 'ljp_category', 'description'] class FreizeitAdminForm(forms.ModelForm): @@ -1037,14 +1037,6 @@ 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 GenerateSjrForm(forms.Form): def __init__(self, *args, **kwargs): @@ -1054,6 +1046,23 @@ class GenerateSjrForm(forms.Form): 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 @@ -1064,8 +1073,8 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): view_on_site = False fieldsets = ( (None, { - 'fields': ('name', 'place', 'destination', 'date', 'end', 'description', 'groups', 'jugendleiter', - 'approved_extra_youth_leader_count', + 'fields': ('name', 'place', 'postcode', 'destination', 'date', 'end', 'description', 'groups', + 'jugendleiter', 'approved_extra_youth_leader_count', 'tour_type', 'tour_approach', 'kilometers_traveled', 'activity', 'difficulty'), 'description': _('General information on your excursion. These are partly relevant for the amount of financial compensation (means of transport, travel distance, etc.).') }), @@ -1115,40 +1124,36 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): return render_tex(memberlist.name + "_Notizen", 'members/notes_list.tex', context) notes_list.short_description = _('Generate overview') - def render_seminar_report_options(self, request, memberlist, form): + @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 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) - - 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 = GenerateSeminarReportForm(request.POST) - if not form.is_valid(): - messages.error(request, _('Please select a mode.')) - return self.render_seminar_report_options(request, memberlist, form) - mode = form.cleaned_data['mode'] - prepend_v32 = form.cleaned_data['prepend_v32'] - 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) - return self.render_seminar_report_options(request, memberlist, GenerateSeminarReportForm()) seminar_report.short_description = _('Generate seminar report') def render_sjr_options(self, request, memberlist, form): @@ -1226,12 +1231,27 @@ 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 def action_view(self, request, object_id): if "sjr_application" in request.POST: return self.sjr_application(request, Freizeit.objects.get(pk=object_id)) + if "seminar_vbk" in request.POST: + return self.seminar_vbk(request, Freizeit.objects.get(pk=object_id)) if "seminar_report" in request.POST: return self.seminar_report(request, Freizeit.objects.get(pk=object_id)) if "notes_list" in request.POST: diff --git a/jdav_web/members/excel.py b/jdav_web/members/excel.py index 6fa8ff7..3f4c154 100644 --- a/jdav_web/members/excel.py +++ b/jdav_web/members/excel.py @@ -1,9 +1,10 @@ from datetime import datetime import os import xlsxwriter +import openpyxl from django.conf import settings -from contrib.media import media_path -from .models import WEEKDAYS +from contrib.media import media_path, find_template +from .models import WEEKDAYS, LJPProposal def generate_group_overview(all_groups, limit_to_public = True): """ @@ -67,3 +68,64 @@ def generate_group_overview(all_groups, limit_to_public = True): workbook.close() return filename + + +VBK_TEMPLATES = { + LJPProposal.LJP_STAFF_TRAINING: 'members/LJP_VBK_3-1.xlsx', + LJPProposal.LJP_EDUCATIONAL: 'members/LJP_VBK_3-2.xlsx', +} + +NOT_BW_REASONS = { + LJPProposal.NOT_BW_CONTENT: 'aufgrund der Lehrgangsinhalte', + LJPProposal.NOT_BW_ROOMS: 'trägereigene Räumlichkeiten', + LJPProposal.NOT_BW_CLOSE_BORDER: 'Grenznähe', + LJPProposal.NOT_BW_ECONOMIC: 'wirtschaftliche Sparsamkeit', +} + +LJP_GOALS = { + LJPProposal.LJP_QUALIFICATION: 'Qualifizierung', + LJPProposal.LJP_PARTICIPATION: 'Partizipation', + LJPProposal.LJP_DEVELOPMENT: 'Persönlichkeitsentwicklung', + LJPProposal.LJP_ENVIRONMENT: 'Umwelt', +} + + +def generate_ljp_vbk(excursion): + """ + Generate the VBK forms for LJP given an excursion. Returns the filename to the filled excel file. + """ + 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) + + sheet = workbook.active + title = excursion.ljpproposal.title + + sheet['I6'] = settings.SEKTION_IBAN + sheet['I8'] = settings.SEKTION_ACCOUNT_HOLDER + sheet['P3'] = excursion.end.year + sheet['B4'] = f"Sektion {settings.SEKTION}" + sheet['B5'] = settings.SEKTION_STREET + sheet['B6'] = settings.SEKTION_TOWN + sheet['B7'] = settings.RESPONSIBLE_MAIL + sheet['B36'] = f"{settings.SEKTION}, {datetime.today():%d.%m.%Y}" + sheet['F19'] = f"B {excursion.date:%y}-{excursion.pk}" + sheet['C19'] = LJP_GOALS[excursion.ljpproposal.goal] if excursion.ljpproposal.goal in LJP_GOALS else "" + sheet['D19'] = settings.SEKTION + sheet['G19'] = title + sheet['I19'] = f"von {excursion.date:%d.%m.%y} bis {excursion.end:%d.%m.%y}" + sheet['J19'] = excursion.duration + sheet['L19'] = f"{excursion.ljp_participant_count}" + sheet['H19'] = excursion.get_ljp_activity_category() + sheet['M19'] = f"{excursion.postcode}, {excursion.place}" + sheet['N19'] = f"{NOT_BW_REASONS[excursion.ljpproposal.not_bw_reason]}"\ + if not excursion.ljpproposal.not_bw_reason is None else "" + + if hasattr(excursion, 'statement'): + sheet['Q19'] = f"{excursion.statement.total_theoretic}" + + 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 fc719e6..c0adc32 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:32+0100\n" +"POT-Creation-Date: 2025-02-06 00:32+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -336,24 +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" +msgid "Invoice" +msgstr "Beleg" #: members/admin.py -msgid "Prepend V32" -msgstr "V32 Formblatt einfügen" +msgid "Excursion not found." +msgstr "Ausfahrt nicht gefunden." #: 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 "" @@ -383,17 +378,6 @@ msgstr "Hinweise für Jugendleiter erstellen" msgid "Generate seminar report" msgstr "Landesjugendplan Antrag erstellen" -#: members/admin.py members/tests.py -msgid "Please select a mode." -msgstr "Bitte wähle einen Modus aus." - -#: members/admin.py members/tests.py -msgid "" -"Full mode is only available, if the seminar report section is filled out." -msgstr "" -"Der vollständiger Modus ist nur verfügbar, wenn der Seminarbericht " -"ausgefüllt ist. " - #: members/admin.py members/templates/admin/generate_sjr_application.html msgid "Generate SJR application" msgstr "SJR Antrag erstellen" @@ -491,8 +475,7 @@ msgstr "LJP Spielart" #: members/models.py msgid "" "The official category for LJP applications associated with this activity." -msgstr "" -"Die offizielle Spielart für LJP Anträge mit dieser Aktivität." +msgstr "Die offizielle Spielart für LJP Anträge mit dieser Aktivität." #: members/models.py msgid "Description" @@ -951,24 +934,92 @@ 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 "Type of seminar. Usually the correct choice is educational programme." +msgstr "Kurstyp. In der Regel Themenorientierte Bildungsmaßnahme." + +#: members/models.py +msgid "Qualification" +msgstr "Qualifizierung" + +#: members/models.py +msgid "Participation" +msgstr "Partizipation" + +#: members/models.py +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 "Course content" +msgstr "aufgrund der Lehrgangsinhalte" #: members/models.py -msgid "Pedagogic goals" -msgstr "Pädagogische Ziele" +msgid "Available rooms" +msgstr "trägereigene Räumlichkeiten" #: members/models.py -msgid "Content and methods" -msgstr "Inhalte und Methoden" +msgid "Close to the border" +msgstr "Grenznähe" #: members/models.py -msgid "Evaluation" -msgstr "Wertung" +msgid "Economic reasons" +msgstr "wirtschaftliche Sparsamkeit" #: members/models.py -msgid "Experiences and possible improvements" -msgstr "Erfahrungen und Verbesserungsvorschläge" +msgid "Explanation if excursion not in Baden-Württemberg" +msgstr "Begründung, falls Kursort nicht in Baden-Württemberg" + +#: members/models.py +msgid "" +"If the excursion takes place outside of Baden-Württemberg, please explain. " +"Otherwise, leave this empty." +msgstr "" +"Falls die Ausfahrt außerhalb von Baden-Württemberg stattfindet, gib bitte " +"eine Begründung an. Sonst lass dieses Feld frei." #: members/models.py msgid "LJP Proposal" @@ -1046,10 +1097,6 @@ msgstr "Fortbildungstyp" msgid "Training categories" msgstr "Fortbildungstypen" -#: members/models.py -msgid "Category" -msgstr "Kategorien" - #: members/models.py msgid "Comments" msgstr "Kommentar" @@ -1097,7 +1144,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_sjr_application.html #: members/templates/admin/invite_as_user.html #: members/templates/admin/invite_for_group.html @@ -1335,63 +1381,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_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_sjr_application.html members/tests.py msgid "Here you can generate an allowance application for the SJR." @@ -1410,6 +1453,10 @@ msgid "" "Please send this application form to the jdav finance officer via email." msgstr "Bitte sende diesen Antrag an den/die JDAV-Finanzwart*in per E-Mail." +#: members/templates/admin/generate_sjr_application.html +msgid "Generate" +msgstr "Erstellen" + #: members/templates/admin/invite_as_user.html #, python-format msgid "" @@ -1932,6 +1979,25 @@ 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 "Please select a mode." +msgstr "Bitte wähle einen Modus aus." + +#: members/tests.py +msgid "" +"Full mode is only available, if the seminar report section is filled out." +msgstr "" +"Der vollständiger Modus ist nur verfügbar, wenn der Seminarbericht " +"ausgefüllt ist. " + #: members/tests.py msgid "This field is required." msgstr "" @@ -1974,6 +2040,99 @@ msgstr "Optionale zusätzliche E-Mailadresse" msgid "Invalid emergency contacts" msgstr "Ungültige Notfallkontakte" +#~ msgid "Generate LJP V-BK form" +#~ msgstr "Erzeuge LJP V-BK Formular" + +#~ msgid "" +#~ "Every LJP application needs a V-BK form containing the most important " +#~ "facts about the seminar.\n" +#~ "Here you can automatically generate such a form in Excel format." +#~ msgstr "" +#~ "Jeder LJP Antrag benötigt ein V-BK Formular, das die wichtigsten " +#~ "Randdaten des Seminars enthält. Hier kannst du automatisch ein solches " +#~ "Formular im Excel Format erstellen." + +#~ msgid "" +#~ "Your excursion currently has no cost-plan attached, hence the total costs " +#~ "can't be automatically\n" +#~ "calculated and added to the form." +#~ msgstr "" +#~ "Deine Ausfahrt hat zur Zeit keinen Kostenplan. Daher können die " +#~ "Gesamtkosten nicht automatisch berechnet und dem Formular hinzugefügt " +#~ "werden." + +#~ 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." + +#~ 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_ljp_application_rework.py b/jdav_web/members/migrations/0035_ljp_application_rework.py new file mode 100644 index 0000000..7a6037c --- /dev/null +++ b/jdav_web/members/migrations/0035_ljp_application_rework.py @@ -0,0 +1,68 @@ +# Generated by Django 4.0.1 on 2025-02-06 21:17 + +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'), + ), + migrations.AddField( + model_name='ljpproposal', + name='not_bw_reason', + field=models.IntegerField(blank=True, choices=[(1, 'Course content'), (2, 'Available rooms'), (3, 'Close to the border'), (4, 'Economic reasons')], default=None, help_text='If the excursion takes place outside of Baden-Württemberg, please explain. Otherwise, leave this empty.', null=True, verbose_name='Explanation if excursion not in Baden-Württemberg'), + ), + 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=100, verbose_name='Title'), + ), + migrations.AddField( + model_name='freizeit', + name='postcode', + field=models.CharField(default='', max_length=30, verbose_name='Postcode'), + ), + ] diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index 4a9c504..0d1f99c 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -1058,6 +1058,7 @@ class Freizeit(CommonModel): name = models.CharField(verbose_name=_('Activity'), default='', max_length=50) place = models.CharField(verbose_name=_('Place'), default='', max_length=50) + postcode = models.CharField(verbose_name=_('Postcode'), default='', max_length=30) destination = models.CharField(verbose_name=_('Destination (optional)'), default='', max_length=50, blank=True, help_text=_('e.g. a peak')) @@ -1146,6 +1147,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 +1479,47 @@ 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=100, + 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='') + + NOT_BW_CONTENT, NOT_BW_ROOMS, NOT_BW_CLOSE_BORDER, NOT_BW_ECONOMIC = 1, 2, 3, 4 + NOT_BW_REASONS = [ + (NOT_BW_CONTENT, _('Course content')), + (NOT_BW_ROOMS, _('Available rooms')), + (NOT_BW_CLOSE_BORDER, _('Close to the border')), + (NOT_BW_ECONOMIC, _('Economic reasons')), + ] + not_bw_reason = models.IntegerField(verbose_name=_('Explanation if excursion not in Baden-Württemberg'), + choices=NOT_BW_REASONS, + default=None, + blank=True, + null=True, + help_text=_('If the excursion takes place outside of Baden-Württemberg, please explain. Otherwise, leave this empty.')) excursion = models.OneToOneField(Freizeit, verbose_name=_('Excursion'), diff --git a/jdav_web/members/pdf.py b/jdav_web/members/pdf.py index ff43d94..fa12007 100644 --- a/jdav_web/members/pdf.py +++ b/jdav_web/members/pdf.py @@ -11,30 +11,20 @@ from django.template.loader import get_template from django.conf import settings from django.http import HttpResponse, HttpResponseRedirect from wsgiref.util import FileWrapper -from contrib.media import media_path, media_dir, serve_media, ensure_media_dir +from contrib.media import media_path, media_dir, serve_media, ensure_media_dir, find_template from PIL import Image -def find_template(template_name): - for engine in template.engines.all(): - for loader in engine.engine.template_loaders: - for origin in loader.get_template_sources(template_name): - if os.path.exists(origin.name): - return origin.name - raise template.TemplateDoesNotExist(f"Could not find template: {template_name}") - - 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'))) @@ -43,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/LJP_VBK_3-1.xlsx b/jdav_web/members/templates/members/LJP_VBK_3-1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a02224a22f6f3c4de9f0de9f27b46851840656bf GIT binary patch literal 17651 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 Q+Cv8N0)({sg1@!?9|@?4wg3PC literal 0 HcmV?d00001 diff --git a/jdav_web/members/templates/members/LJP_VBK_3-2.xlsx b/jdav_web/members/templates/members/LJP_VBK_3-2.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1b21d3d986d922702d20540d68237edfc7fa789e GIT binary patch literal 17648 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