From f59a97578c125f84d32ca3e07dd3cbc1d786daff Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Sun, 2 Feb 2025 00:54:02 +0100 Subject: [PATCH 01/12] ljp vbk generation --- jdav_web/contrib/media.py | 11 ++++ jdav_web/locale/de/LC_MESSAGES/django.po | 6 +- jdav_web/members/admin.py | 37 +++++++++++- jdav_web/members/excel.py | 49 ++++++++++++++- .../members/locale/de/LC_MESSAGES/django.po | 57 +++++++++++++++--- jdav_web/members/pdf.py | 11 +--- .../templates/admin/generate_seminar_vbk.html | 56 +++++++++++++++++ .../templates/members/LJP_VBK_3-1.xlsx | Bin 0 -> 17651 bytes .../templates/members/LJP_VBK_3-2.xlsx | Bin 0 -> 17648 bytes .../freizeit/change_form_object_tools.html | 7 +++ 10 files changed, 212 insertions(+), 22 deletions(-) create mode 100644 jdav_web/members/templates/admin/generate_seminar_vbk.html 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 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/locale/de/LC_MESSAGES/django.po b/jdav_web/locale/de/LC_MESSAGES/django.po index cafea14..4f805db 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-01 14:54+0100\n" +"POT-Creation-Date: 2025-02-02 00:45+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -260,6 +260,10 @@ 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 2ca8b50..548bd5f 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -29,7 +29,7 @@ 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 .excel import generate_group_overview, VBK_3_1, VBK_3_2, 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): @@ -1045,6 +1045,13 @@ class GenerateSeminarReportForm(forms.Form): 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): @@ -1052,7 +1059,6 @@ class GenerateSjrForm(forms.Form): super(GenerateSjrForm,self).__init__(*args,**kwargs) self.fields['invoice'] = forms.ChoiceField(choices=self.attachments, label=_('Invoice')) - class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): @@ -1116,6 +1122,29 @@ 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_vbk_options(self, request, memberlist, form): + context = dict(self.admin_site.each_context(request), + title=_('Generate LJP V-BK form'), + opts=self.opts, + memberlist=memberlist, + form=form, + object=memberlist) + return render(request, 'admin/generate_seminar_vbk.html', context=context) + + def seminar_vbk(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): context = dict(self.admin_site.each_context(request), title=_('Generate seminar report'), @@ -1233,6 +1262,8 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): 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..4b9e83d 100644 --- a/jdav_web/members/excel.py +++ b/jdav_web/members/excel.py @@ -1,8 +1,9 @@ from datetime import datetime import os import xlsxwriter +import openpyxl from django.conf import settings -from contrib.media import media_path +from contrib.media import media_path, find_template from .models import WEEKDAYS def generate_group_overview(all_groups, limit_to_public = True): @@ -67,3 +68,49 @@ def generate_group_overview(all_groups, limit_to_public = True): workbook.close() 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', +} + + +def generate_ljp_vbk(excursion, mode): + """ + 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] + path = find_template(template_path) + workbook = openpyxl.load_workbook(path) + + sheet = workbook.active + title = excursion.ljpproposal.title if hasattr(excursion, 'ljpproposal') else excursion.name + + 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['D19'] = settings.SEKTION + sheet['G19'] = title + sheet['I19'] = f"von {excursion.date:%d.%m.%y} bis {excursion.end:%d.%m.%y}" + sheet['J19'] = f"{excursion.duration}" + sheet['L19'] = f"{excursion.ljp_participant_count}" + sheet['H19'] = excursion.get_ljp_activity_category() + sheet['M19'] = excursion.place + + if hasattr(excursion, 'statement'): + sheet['Q19'] = f"{excursion.statement.total_theoretic}" + + filename = f"LJP_V-BK_3.{mode}_{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..cefb895 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-02 00:45+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -351,6 +351,18 @@ msgstr "Modus" msgid "Prepend V32" msgstr "V32 Formblatt einfügen" +#: members/admin.py +msgid "Staff training" +msgstr "Jugendleiter*innenweiterbildung" + +#: 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" @@ -379,6 +391,14 @@ msgstr "Kriseninterventionsliste erstellen" msgid "Generate overview" msgstr "Hinweise für Jugendleiter erstellen" +#: members/admin.py members/templates/admin/generate_seminar_vbk.html +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" @@ -491,8 +511,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" @@ -1046,10 +1065,6 @@ msgstr "Fortbildungstyp" msgid "Training categories" msgstr "Fortbildungstypen" -#: members/models.py -msgid "Category" -msgstr "Kategorien" - #: members/models.py msgid "Comments" msgstr "Kommentar" @@ -1073,6 +1088,7 @@ msgstr "Fortbildungen" #: 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 #: members/templates/admin/invite_for_group.html @@ -1098,6 +1114,7 @@ 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 #: members/templates/admin/invite_for_group.html @@ -1389,10 +1406,36 @@ msgstr "" "Felder im Formblatt selbst aus und unterschreibe das PDF." #: 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" +#: members/templates/admin/generate_seminar_vbk.html +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." + +#: members/templates/admin/generate_seminar_vbk.html +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." + +#: 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." + #: members/templates/admin/generate_sjr_application.html members/tests.py msgid "Here you can generate an allowance application for the SJR." msgstr "Hier kannst du einen SJR-Zuschussantrag erstellen." diff --git a/jdav_web/members/pdf.py b/jdav_web/members/pdf.py index ff43d94..9ffb88a 100644 --- a/jdav_web/members/pdf.py +++ b/jdav_web/members/pdf.py @@ -11,19 +11,10 @@ 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') diff --git a/jdav_web/members/templates/admin/generate_seminar_vbk.html b/jdav_web/members/templates/admin/generate_seminar_vbk.html new file mode 100644 index 0000000..f028989 --- /dev/null +++ b/jdav_web/members/templates/admin/generate_seminar_vbk.html @@ -0,0 +1,56 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls static %} + +{% block extrahead %} + {{ block.super }} + {{ media }} + + + +{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} invite-waiter +{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} + +

+{% blocktrans %}Every LJP application needs a V-BK form containing the most important facts about the seminar. +Here you can automatically generate such a form in Excel format.{% endblocktrans %} +

+{% if not memberlist.statement %} +

+{% blocktrans %}Your excursion currently has no cost-plan attached, hence the total costs can't be automatically +calculated and added to the form.{% endblocktrans %} +

+{% endif %} +

+{% blocktrans %}Depending on the type of seminar, please select one of the two options below.{% endblocktrans %} +

+ +
+ {% csrf_token %} +

+ + {{ form }} +
+

+
+ + + + {% translate "Cancel" %} +
+ +{% 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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 +
  • +
    + {% csrf_token %} + +
    +
  • +
  • {% csrf_token %} -- 2.38.4 From bdf7e76f2212a52ea52ec8368060073de2af9b54 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Sun, 2 Feb 2025 18:15:02 +0100 Subject: [PATCH 02/12] 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 -- 2.38.4 From f5ffee2f791c394a7ed59d3688ae662564f677eb Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Sun, 2 Feb 2025 18:20:15 +0100 Subject: [PATCH 03/12] cleanup --- jdav_web/members/admin.py | 43 ++------------ .../templates/admin/generate_seminar_vbk.html | 56 ------------------- 2 files changed, 6 insertions(+), 93 deletions(-) delete mode 100644 jdav_web/members/templates/admin/generate_seminar_vbk.html diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index 975f2a0..aed2fd9 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -1165,37 +1165,6 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): return render(request, 'admin/generate_seminar_report.html', context=context) seminar_report.short_description = _('Generate seminar report') - 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: - 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'] - 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) - context = dict(memberlist=memberlist, settings=settings, mode=mode) - 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') - def render_sjr_options(self, request, memberlist, form): context = dict(self.admin_site.each_context(request), title=_('Generate SJR application'), @@ -1204,7 +1173,7 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): form=form, object=memberlist) return render(request, 'admin/generate_sjr_application.html', context=context) - + def sjr_application(self, request, memberlist): if hasattr(memberlist, 'statement'): attachment_names = [f"{b.short_description}: {b.explanation} ({b.amount:.2f}€)" for b in memberlist.statement.bill_set.all() if b.proof] @@ -1213,7 +1182,7 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): attachment_names = [] attachment_paths = [] attachments = zip(attachment_paths, attachment_names) - + if not self.may_view_excursion(request, memberlist): return self.not_allowed_view(request, memberlist) if "apply" in request.POST: @@ -1221,15 +1190,15 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): if not form.is_valid(): messages.error(request, _('Please select an invoice.')) return self.render_sjr_options(request, memberlist, form) - + selected_attachments = [form.cleaned_data['invoice']] context = memberlist.sjr_application_fields() title = memberlist.ljpproposal.title if hasattr(memberlist, 'ljpproposal') else memberlist.name - + return fill_pdf_form(title + "_SJR_Antrag", 'members/sjr_template.pdf', context, selected_attachments) - + return self.render_sjr_options(request, memberlist, GenerateSjrForm(attachments=attachments)) - + sjr_application.short_description = _('Generate SJR application') def finance_overview(self, request, memberlist): diff --git a/jdav_web/members/templates/admin/generate_seminar_vbk.html b/jdav_web/members/templates/admin/generate_seminar_vbk.html deleted file mode 100644 index f028989..0000000 --- a/jdav_web/members/templates/admin/generate_seminar_vbk.html +++ /dev/null @@ -1,56 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load i18n admin_urls static %} - -{% block extrahead %} - {{ block.super }} - {{ media }} - - - -{% endblock %} - -{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} invite-waiter -{% endblock %} - -{% block breadcrumbs %} - -{% endblock %} - -{% block content %} - -

    -{% blocktrans %}Every LJP application needs a V-BK form containing the most important facts about the seminar. -Here you can automatically generate such a form in Excel format.{% endblocktrans %} -

    -{% if not memberlist.statement %} -

    -{% blocktrans %}Your excursion currently has no cost-plan attached, hence the total costs can't be automatically -calculated and added to the form.{% endblocktrans %} -

    -{% endif %} -

    -{% blocktrans %}Depending on the type of seminar, please select one of the two options below.{% endblocktrans %} -

    - - - {% csrf_token %} -

    - - {{ form }} -
    -

    -
    - - - - {% translate "Cancel" %} - - -{% endblock %} -- 2.38.4 From 6b187812bb6c3693a6bf275babade63a43b90d23 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Sun, 2 Feb 2025 18:22:10 +0100 Subject: [PATCH 04/12] more cleanup --- jdav_web/members/admin.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index aed2fd9..5abb944 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -1124,15 +1124,6 @@ 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_vbk_options(self, request, memberlist, form): - context = dict(self.admin_site.each_context(request), - title=_('Generate LJP V-BK form'), - opts=self.opts, - memberlist=memberlist, - form=form, - object=memberlist) - return render(request, 'admin/generate_seminar_vbk.html', context=context) - @decorate_download def download_seminar_vbk(self, request, memberlist): fp = generate_ljp_vbk(memberlist) -- 2.38.4 From 0edd7df8432239ba987909f04a7b3f9ebb7e769b Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Sun, 2 Feb 2025 18:37:59 +0100 Subject: [PATCH 05/12] update dockerfiles --- docker/production/Dockerfile | 4 ++-- docker/test/Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 -- 2.38.4 From 4f0e9435e991795d6b2fa6b1cad1f353fc288f60 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Wed, 5 Feb 2025 00:23:02 +0100 Subject: [PATCH 06/12] fixes --- jdav_web/members/templates/members/seminar_report.tex | 2 +- jdav_web/members/templates/members/seminar_report_docx.tex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 index d5f666b..ac72022 100644 --- a/jdav_web/members/templates/members/seminar_report_docx.tex +++ b/jdav_web/members/templates/members/seminar_report_docx.tex @@ -46,9 +46,9 @@ \end{tabular} \end{table} -\section{Zielverfolgung- und Erreichung} +\section{Zielverfolgung und -Erreichung} -{{ memberlist.ljpproposal.goals|esc_all }} +{{ memberlist.ljpproposal.goal_strategy|esc_all }} \section{Zeitlicher Ablauf} -- 2.38.4 From 582dd81465a9a5251b2b69924c84eaff6b7a46d8 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Thu, 6 Feb 2025 00:35:30 +0100 Subject: [PATCH 07/12] add postcode and not bw reason --- jdav_web/members/admin.py | 4 +- jdav_web/members/excel.py | 13 +- .../members/locale/de/LC_MESSAGES/django.po | 117 +++++++++++------- .../0036_ljpproposal_not_bw_reason.py | 18 +++ ...eit_postal_code_alter_ljpproposal_title.py | 23 ++++ ..._freizeit_postal_code_freizeit_postcode.py | 22 ++++ jdav_web/members/models.py | 17 ++- 7 files changed, 162 insertions(+), 52 deletions(-) create mode 100644 jdav_web/members/migrations/0036_ljpproposal_not_bw_reason.py create mode 100644 jdav_web/members/migrations/0037_freizeit_postal_code_alter_ljpproposal_title.py create mode 100644 jdav_web/members/migrations/0038_remove_freizeit_postal_code_freizeit_postcode.py diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index 5abb944..3dead73 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -1073,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.).') }), diff --git a/jdav_web/members/excel.py b/jdav_web/members/excel.py index 0780652..393e962 100644 --- a/jdav_web/members/excel.py +++ b/jdav_web/members/excel.py @@ -75,6 +75,13 @@ VBK_TEMPLATES = { 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', +} + def generate_ljp_vbk(excursion): """ @@ -87,7 +94,7 @@ def generate_ljp_vbk(excursion): workbook = openpyxl.load_workbook(path) sheet = workbook.active - title = excursion.ljpproposal.title if hasattr(excursion, 'ljpproposal') else excursion.name + title = excursion.ljpproposal.title sheet['I6'] = settings.SEKTION_IBAN sheet['I8'] = settings.SEKTION_ACCOUNT_HOLDER @@ -104,7 +111,9 @@ def generate_ljp_vbk(excursion): sheet['J19'] = f"{excursion.duration}" sheet['L19'] = f"{excursion.ljp_participant_count}" sheet['H19'] = excursion.get_ljp_activity_category() - sheet['M19'] = excursion.place + 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}" diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po index f65e3de..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 18:06+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" @@ -374,25 +374,10 @@ msgstr "Kriseninterventionsliste erstellen" msgid "Generate overview" msgstr "Hinweise für Jugendleiter erstellen" -#: members/admin.py members/templates/admin/generate_seminar_vbk.html -msgid "Generate LJP V-BK form" -msgstr "Erzeuge LJP V-BK Formular" - #: members/admin.py members/templates/admin/generate_seminar_report.html 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" @@ -1008,6 +993,34 @@ 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 "Available rooms" +msgstr "trägereigene Räumlichkeiten" + +#: members/models.py +msgid "Close to the border" +msgstr "Grenznähe" + +#: members/models.py +msgid "Economic reasons" +msgstr "wirtschaftliche Sparsamkeit" + +#: members/models.py +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" msgstr "Seminarbericht" @@ -1107,7 +1120,6 @@ msgstr "Fortbildungen" #: 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 #: members/templates/admin/invite_for_group.html @@ -1132,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_vbk.html #: members/templates/admin/generate_sjr_application.html #: members/templates/admin/invite_as_user.html #: members/templates/admin/invite_for_group.html @@ -1425,35 +1436,6 @@ 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 "" -"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." - -#: members/templates/admin/generate_seminar_vbk.html -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." - -#: 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." - -#: 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." msgstr "Hier kannst du einen SJR-Zuschussantrag erstellen." @@ -1471,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 "" @@ -2001,6 +1987,17 @@ msgstr "" "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 "" @@ -2043,6 +2040,32 @@ 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" diff --git a/jdav_web/members/migrations/0036_ljpproposal_not_bw_reason.py b/jdav_web/members/migrations/0036_ljpproposal_not_bw_reason.py new file mode 100644 index 0000000..e4efdfd --- /dev/null +++ b/jdav_web/members/migrations/0036_ljpproposal_not_bw_reason.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.1 on 2025-02-05 23:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0035_ljpproposal_redo'), + ] + + operations = [ + 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'), + ), + ] diff --git a/jdav_web/members/migrations/0037_freizeit_postal_code_alter_ljpproposal_title.py b/jdav_web/members/migrations/0037_freizeit_postal_code_alter_ljpproposal_title.py new file mode 100644 index 0000000..95b741f --- /dev/null +++ b/jdav_web/members/migrations/0037_freizeit_postal_code_alter_ljpproposal_title.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.1 on 2025-02-05 23:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0036_ljpproposal_not_bw_reason'), + ] + + operations = [ + migrations.AddField( + model_name='freizeit', + name='postal_code', + field=models.CharField(default='', max_length=30, verbose_name='Postal code'), + ), + 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'), + ), + ] diff --git a/jdav_web/members/migrations/0038_remove_freizeit_postal_code_freizeit_postcode.py b/jdav_web/members/migrations/0038_remove_freizeit_postal_code_freizeit_postcode.py new file mode 100644 index 0000000..e487458 --- /dev/null +++ b/jdav_web/members/migrations/0038_remove_freizeit_postal_code_freizeit_postcode.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.1 on 2025-02-05 23:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0037_freizeit_postal_code_alter_ljpproposal_title'), + ] + + operations = [ + migrations.RemoveField( + model_name='freizeit', + name='postal_code', + ), + 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 a7dd713..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')) @@ -1478,7 +1479,7 @@ class RegistrationPassword(models.Model): class LJPProposal(CommonModel): """A proposal for LJP""" - title = models.CharField(verbose_name=_('Title'), max_length=30, + 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.')) @@ -1506,6 +1507,20 @@ class LJPProposal(CommonModel): 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'), blank=True, -- 2.38.4 From 0daafdd5d7e4e02f72fbe43312de18ca5795c50f Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Thu, 6 Feb 2025 00:51:32 +0100 Subject: [PATCH 08/12] fix --- jdav_web/members/excel.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/jdav_web/members/excel.py b/jdav_web/members/excel.py index 393e962..3f4c154 100644 --- a/jdav_web/members/excel.py +++ b/jdav_web/members/excel.py @@ -82,6 +82,13 @@ NOT_BW_REASONS = { 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): """ @@ -105,10 +112,11 @@ def generate_ljp_vbk(excursion): 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'] = f"{excursion.duration}" + 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}" -- 2.38.4 From 7f3fa1c1bb7b573c64e2e7d14e59e1ac80f87fe0 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Thu, 6 Feb 2025 22:19:16 +0100 Subject: [PATCH 09/12] squash migrations --- .../migrations/0035_ljp_application_rework.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 jdav_web/members/migrations/0035_ljp_application_rework.py 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..89818d7 --- /dev/null +++ b/jdav_web/members/migrations/0035_ljp_application_rework.py @@ -0,0 +1,70 @@ +# Generated by Django 4.0.1 on 2025-02-06 21:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [('members', '0035_ljpproposal_redo'), ('members', '0036_ljpproposal_not_bw_reason'), ('members', '0037_freizeit_postal_code_alter_ljpproposal_title'), ('members', '0038_remove_freizeit_postal_code_freizeit_postcode')] + + 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'), + ), + ] -- 2.38.4 From a9bd5d59d66c47f509f550156a761bc740b2c6cc Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Thu, 6 Feb 2025 22:22:44 +0100 Subject: [PATCH 10/12] remove old migrations --- .../migrations/0035_ljp_application_rework.py | 2 - .../migrations/0035_ljpproposal_redo.py | 53 ------------------- .../0036_ljpproposal_not_bw_reason.py | 18 ------- ...eit_postal_code_alter_ljpproposal_title.py | 23 -------- ..._freizeit_postal_code_freizeit_postcode.py | 22 -------- 5 files changed, 118 deletions(-) delete mode 100644 jdav_web/members/migrations/0035_ljpproposal_redo.py delete mode 100644 jdav_web/members/migrations/0036_ljpproposal_not_bw_reason.py delete mode 100644 jdav_web/members/migrations/0037_freizeit_postal_code_alter_ljpproposal_title.py delete mode 100644 jdav_web/members/migrations/0038_remove_freizeit_postal_code_freizeit_postcode.py diff --git a/jdav_web/members/migrations/0035_ljp_application_rework.py b/jdav_web/members/migrations/0035_ljp_application_rework.py index 89818d7..7a6037c 100644 --- a/jdav_web/members/migrations/0035_ljp_application_rework.py +++ b/jdav_web/members/migrations/0035_ljp_application_rework.py @@ -5,8 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - replaces = [('members', '0035_ljpproposal_redo'), ('members', '0036_ljpproposal_not_bw_reason'), ('members', '0037_freizeit_postal_code_alter_ljpproposal_title'), ('members', '0038_remove_freizeit_postal_code_freizeit_postcode')] - dependencies = [ ('members', '0034_activitycategory_ljp_category'), ] diff --git a/jdav_web/members/migrations/0035_ljpproposal_redo.py b/jdav_web/members/migrations/0035_ljpproposal_redo.py deleted file mode 100644 index ffb6e2e..0000000 --- a/jdav_web/members/migrations/0035_ljpproposal_redo.py +++ /dev/null @@ -1,53 +0,0 @@ -# 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/migrations/0036_ljpproposal_not_bw_reason.py b/jdav_web/members/migrations/0036_ljpproposal_not_bw_reason.py deleted file mode 100644 index e4efdfd..0000000 --- a/jdav_web/members/migrations/0036_ljpproposal_not_bw_reason.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.0.1 on 2025-02-05 23:18 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('members', '0035_ljpproposal_redo'), - ] - - operations = [ - 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'), - ), - ] diff --git a/jdav_web/members/migrations/0037_freizeit_postal_code_alter_ljpproposal_title.py b/jdav_web/members/migrations/0037_freizeit_postal_code_alter_ljpproposal_title.py deleted file mode 100644 index 95b741f..0000000 --- a/jdav_web/members/migrations/0037_freizeit_postal_code_alter_ljpproposal_title.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.0.1 on 2025-02-05 23:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('members', '0036_ljpproposal_not_bw_reason'), - ] - - operations = [ - migrations.AddField( - model_name='freizeit', - name='postal_code', - field=models.CharField(default='', max_length=30, verbose_name='Postal code'), - ), - 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'), - ), - ] diff --git a/jdav_web/members/migrations/0038_remove_freizeit_postal_code_freizeit_postcode.py b/jdav_web/members/migrations/0038_remove_freizeit_postal_code_freizeit_postcode.py deleted file mode 100644 index e487458..0000000 --- a/jdav_web/members/migrations/0038_remove_freizeit_postal_code_freizeit_postcode.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 4.0.1 on 2025-02-05 23:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('members', '0037_freizeit_postal_code_alter_ljpproposal_title'), - ] - - operations = [ - migrations.RemoveField( - model_name='freizeit', - name='postal_code', - ), - migrations.AddField( - model_name='freizeit', - name='postcode', - field=models.CharField(default='', max_length=30, verbose_name='Postcode'), - ), - ] -- 2.38.4 From e0f20c9ca160b19ea7562f67866dbe33289b027f Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Thu, 6 Feb 2025 22:25:22 +0100 Subject: [PATCH 11/12] revert --- jdav_web/locale/de/LC_MESSAGES/django.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdav_web/locale/de/LC_MESSAGES/django.po b/jdav_web/locale/de/LC_MESSAGES/django.po index 4005714..cafea14 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 18:06+0100\n" +"POT-Creation-Date: 2025-02-01 14:54+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" -- 2.38.4 From 48f1bde59dfdd458f75263f11440a593e5fe079a Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Fri, 7 Feb 2025 22:00:08 +0100 Subject: [PATCH 12/12] adapt tests --- jdav_web/members/tests.py | 61 ++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 14 deletions(-) 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): -- 2.38.4