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 0000000..a02224a Binary files /dev/null and b/jdav_web/members/templates/members/LJP_VBK_3-1.xlsx differ 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 0000000..1b21d3d Binary files /dev/null and b/jdav_web/members/templates/members/LJP_VBK_3-2.xlsx differ diff --git a/jdav_web/members/templates/members/seminar_report.tex b/jdav_web/members/templates/members/seminar_report.tex index f7ffa2c..9098150 100644 --- a/jdav_web/members/templates/members/seminar_report.tex +++ b/jdav_web/members/templates/members/seminar_report.tex @@ -59,7 +59,7 @@ {{ settings.SEKTION_TOWN }} \\ Tel.: {{ settings.SEKTION_TELEPHONE }} \\ Fax: {{ settings.SEKTION_TELEFAX }} \\ - {{ settings.SEKTION_CONTACT_MAIL }} \\ + {{ settings.RESPONSIBLE_MAIL }} \\ \end{flushright} \end{textblock*} 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..ac72022 --- /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.goal_strategy|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/members/tests.py b/jdav_web/members/tests.py index 8966cb5..7e4b3b3 100644 --- a/jdav_web/members/tests.py +++ b/jdav_web/members/tests.py @@ -17,7 +17,7 @@ from unittest import skip, mock from .models import Member, Group, PermissionMember, PermissionGroup, Freizeit, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE,\ MemberNoteList, NewMemberOnList, confirm_mail_by_key, EmergencyContact, MemberWaitingList,\ RegistrationPassword, MemberUnconfirmedProxy, InvitationToGroup, DIVERSE, MALE, FEMALE,\ - Klettertreff, KlettertreffAttendee + Klettertreff, KlettertreffAttendee, LJPProposal from .admin import MemberWaitingListAdmin, MemberAdmin, FreizeitAdmin, MemberNoteListAdmin,\ MemberUnconfirmedAdmin, RegistrationFilter, FilteredMemberFieldMixin,\ MemberAdminForm, StatementOnListForm, KlettertreffAdmin, GroupAdmin @@ -671,6 +671,16 @@ class FreizeitAdminTestCase(AdminTestCase, PDFActionMixin): self.bill = Bill.objects.create(statement=self.st, short_description='bla', explanation='bli', amount=42.69, costs_covered=True, paid_by=fr, proof=file) + self.ex2 = Freizeit.objects.create(name='Wild trip 2', kilometers_traveled=0, + tour_type=GEMEINSCHAFTS_TOUR, + tour_approach=MUSKELKRAFT_ANREISE, + difficulty=1) + self.ljpproposal = LJPProposal.objects.create(title='My seminar', + category=LJPProposal.LJP_STAFF_TRAINING, + goal=LJPProposal.LJP_ENVIRONMENT, + goal_strategy='my strategy', + not_bw_reason=LJPProposal.NOT_BW_ROOMS, + excursion=self.ex2) def test_changelist(self): c = self._login('superuser') @@ -746,27 +756,50 @@ class FreizeitAdminTestCase(AdminTestCase, PDFActionMixin): c = self._login('superuser') url = reverse('admin:members_freizeit_action', args=(self.ex.pk,)) - response = c.post(url, data={'seminar_report': ''}) + response = c.post(url, data={'seminar_report': ''}, follow=True) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('You may also choose to include the V32 attachment.')) + self.assertContains(response, + _('This excursion does not have a LJP proposal. Please add one and try again.')) + url = reverse('admin:members_freizeit_action', args=(self.ex2.pk,)) response = c.post(url, data={'seminar_report': '', 'apply': ''}) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Please select a mode.')) + self.assertContains(response, _('A seminar report consists of multiple components:')) + + def test_invalid_download(self): + url = reverse('admin:members_freizeit_download_ljp_vbk', args=(self.ex.pk,)) + c = self._login('standard') + response = c.get(url, follow=True) + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertContains(response, _("You are not allowed to view all members on excursion %(name)s.") % {'name': self.ex.name}) - response = c.post(url, data={'seminar_report': '', - 'apply': '', - 'mode': 'full', - 'prepend_v32': 'true'}) + c = self._login('superuser') + response = c.get(url, follow=True) + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertContains(response, _('This excursion does not have a LJP proposal. Please add one and try again.')) + + url = reverse('admin:members_freizeit_download_ljp_vbk', args=(123456789,)) + response = c.get(url, follow=True) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Full mode is only available, if the seminar report section is filled out.')) + self.assertContains(response, _('Excursion not found.')) - response = c.post(url, data={'seminar_report': '', - 'apply': '', - 'mode': 'basic', - 'prepend_v32': 'true'}) + def test_download_seminar_vbk(self): + url = reverse('admin:members_freizeit_download_ljp_vbk', args=(self.ex2.pk,)) + c = self._login('superuser') + response = c.get(url) + self.assertEqual(response.status_code, HTTPStatus.OK) + + def test_download_seminar_report_docx(self): + url = reverse('admin:members_freizeit_download_ljp_report_docx', args=(self.ex2.pk,)) + c = self._login('superuser') + response = c.get(url) + self.assertEqual(response.status_code, HTTPStatus.OK) + + def test_download_seminar_report_costs_and_participants(self): + url = reverse('admin:members_freizeit_download_ljp_costs_participants', args=(self.ex2.pk,)) + c = self._login('superuser') + response = c.get(url) self.assertEqual(response.status_code, HTTPStatus.OK) - print(mocked_fun.call_count) @mock.patch('members.pdf.fill_pdf_form') def test_sjr_application_post(self, mocked_fun): 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