diff --git a/docker/production/docker-compose.yaml b/docker/production/docker-compose.yaml index feabdbe..35d8daa 100644 --- a/docker/production/docker-compose.yaml +++ b/docker/production/docker-compose.yaml @@ -1,5 +1,3 @@ -version: "3.9" - x-kompass: &kompass image: kompass:production diff --git a/docker/production/nginx/kompass.nginx.conf b/docker/production/nginx/kompass.nginx.conf index 5b63859..c1b1103 100644 --- a/docker/production/nginx/kompass.nginx.conf +++ b/docker/production/nginx/kompass.nginx.conf @@ -6,6 +6,7 @@ server { listen 80; server_name 127.0.0.1; charset utf-8; + error_page 502 /downtime/502.html; location /static { alias /var/www/jdav_web/static; @@ -15,6 +16,10 @@ server { alias /var/www/jdav_web/media; } + location /downtime { + alias /var/www/jdav_web/static/downtime; + } + location / { uwsgi_pass uwsgi; include /etc/nginx/uwsgi_params; diff --git a/jdav_web/contrib/admin.py b/jdav_web/contrib/admin.py index 36119d0..4346029 100644 --- a/jdav_web/contrib/admin.py +++ b/jdav_web/contrib/admin.py @@ -22,7 +22,7 @@ class FieldPermissionsAdminMixin: for fd in field_desc: if fd not in self.field_view_permissions: continue - if not request.user.has_perm(self.field_view_permissions[fd], obj): + if not request.user.has_perm(self.field_view_permissions[fd]): return False return True @@ -43,7 +43,7 @@ class FieldPermissionsAdminMixin: def get_readonly_fields(self, request, obj=None): readonly_fields = super(FieldPermissionsAdminMixin, self).get_readonly_fields(request, obj) return list(readonly_fields) +\ - [fd for fd, perm in self.field_change_permissions.items() if not request.user.has_perm(perm, obj)] + [fd for fd, perm in self.field_change_permissions.items() if not request.user.has_perm(perm)] class ChangeViewAdminMixin: diff --git a/jdav_web/contrib/templatetags/common.py b/jdav_web/contrib/templatetags/common.py new file mode 100644 index 0000000..4e2b350 --- /dev/null +++ b/jdav_web/contrib/templatetags/common.py @@ -0,0 +1,9 @@ +from django import template +from django.conf import settings + +register = template.Library() + +# settings value +@register.simple_tag +def settings_value(name): + return getattr(settings, name, "") diff --git a/jdav_web/finance/admin.py b/jdav_web/finance/admin.py index 442b0cc..e7f0ccd 100644 --- a/jdav_web/finance/admin.py +++ b/jdav_web/finance/admin.py @@ -9,6 +9,7 @@ from django.shortcuts import render from django.conf import settings from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin +from utils import get_member from rules.contrib.admin import ObjectPermissionsModelAdmin @@ -118,10 +119,17 @@ class StatementSubmittedAdmin(admin.ModelAdmin): inlines = [BillOnSubmittedStatementInline, TransactionOnSubmittedStatementInline] def has_add_permission(self, request, obj=None): + # Submitted statements should not be added directly, but instead be created + # as unsubmitted statements and then submitted. return False def has_change_permission(self, request, obj=None): - return True + return request.user.has_perm('finance.process_statementsubmitted') + + def has_delete_permission(self, request, obj=None): + # Submitted statements should not be deleted. Instead they can be rejected + # and then deleted as unsubmitted statements. + return False def get_readonly_fields(self, request, obj=None): readonly_fields = ['submitted'] @@ -218,23 +226,7 @@ class StatementSubmittedAdmin(admin.ModelAdmin): opts=self.opts, statement=statement, transaction_issues=statement.transaction_issues, - total_bills=statement.total_bills, - total=statement.total) - if statement.excursion is not None: - context = dict(context, - nights=statement.excursion.night_count, - price_per_night=statement.real_night_cost, - duration=statement.excursion.duration, - staff_count=statement.real_staff_count, - kilometers_traveled=statement.excursion.kilometers_traveled, - means_of_transport=statement.excursion.get_tour_approach(), - euro_per_km=statement.euro_per_km, - allowance_per_day=settings.ALLOWANCE_PER_DAY, - nights_per_yl=statement.nights_per_yl, - allowance_per_yl=statement.allowance_per_yl, - transportation_per_yl=statement.transportation_per_yl, - total_per_yl=statement.total_per_yl, - total_staff=statement.total_staff) + **statement.template_context()) return render(request, 'admin/overview_submitted_statement.html', context=context) @@ -263,6 +255,10 @@ class StatementConfirmedAdmin(admin.ModelAdmin): # To preserve integrity, no one is allowed to change confirmed statements return False + def has_delete_permission(self, request, obj=None): + # To preserve integrity, no one is allowed to delete confirmed statements + return False + def get_urls(self): urls = super().get_urls() @@ -308,6 +304,9 @@ class StatementConfirmedAdmin(admin.ModelAdmin): @admin.register(Transaction) class TransactionAdmin(admin.ModelAdmin): + """The transaction admin site. This is only used to display transactions. All editing + is disabled on this site. All transactions should be changed on the respective statement + at the correct stage of the approval chain.""" list_display = ['member', 'ledger', 'amount', 'reference', 'statement', 'confirmed', 'confirmed_date', 'confirmed_by'] list_filter = ('ledger', 'member', 'statement', 'confirmed') @@ -319,16 +318,21 @@ class TransactionAdmin(admin.ModelAdmin): return self.fields return super(TransactionAdmin, self).get_readonly_fields(request, obj) + def has_add_permission(self, request, obj=None): + # To preserve integrity, no one is allowed to add transactions + return False + + def has_change_permission(self, request, obj=None): + # To preserve integrity, no one is allowed to change transactions + return False + + def has_delete_permission(self, request, obj=None): + # To preserve integrity, no one is allowed to delete transactions + return False + @admin.register(Bill) class BillAdmin(admin.ModelAdmin): - list_display = ['__str__', 'statement', 'short_description', 'pretty_amount', 'paid_by', 'refunded'] + list_display = ['__str__', 'statement', 'explanation', 'pretty_amount', 'paid_by', 'refunded'] list_filter = ('statement', 'paid_by', 'refunded') search_fields = ('reference', 'statement') - - -def get_member(request): - if not hasattr(request.user, 'member'): - return None - else: - return request.user.member diff --git a/jdav_web/finance/locale/de/LC_MESSAGES/django.po b/jdav_web/finance/locale/de/LC_MESSAGES/django.po index cbb5e02..cc5404f 100644 --- a/jdav_web/finance/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/finance/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: 2024-11-24 01:34+0100\n" +"POT-Creation-Date: 2024-12-01 16:23+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,12 +18,12 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: finance/admin.py:75 +#: finance/admin.py:76 #, python-format msgid "%(name)s is already submitted." msgstr "%(name)s ist bereits eingereicht." -#: finance/admin.py:81 +#: finance/admin.py:82 #, python-format msgid "" "Successfully submited %(name)s. The finance department will notify the " @@ -32,23 +32,23 @@ msgstr "" "Rechnung %(name)s erfolgreich eingereicht. Das Finanzreferat wird auf dich " "sobald wie möglich zukommen." -#: finance/admin.py:84 +#: finance/admin.py:85 msgid "Submit statement" msgstr "Rechnung einreichen" -#: finance/admin.py:161 +#: finance/admin.py:162 #, python-format msgid "%(name)s is not yet submitted." msgstr "%(name)s ist noch nicht eingereicht." -#: finance/admin.py:168 +#: finance/admin.py:169 #, python-format msgid "An error occured while trying to confirm %(name)s. Please try again." msgstr "" "Beim Abwickeln von %(name)s ist ein Fehler aufgetreten. Bitte versuche es " "erneut." -#: finance/admin.py:172 +#: finance/admin.py:173 #, python-format msgid "" "Successfully confirmed %(name)s. I hope you executed the associated " @@ -57,11 +57,11 @@ msgstr "" "Erfolgreich %(name)s abgewickelt. Ich hoffe du hast die zugehörigen " "Überweisungen ausgeführt, ich werde dich nicht nochmal erinnern." -#: finance/admin.py:179 +#: finance/admin.py:180 msgid "Statement confirmed" msgstr "Abrechnung abgewickelt" -#: finance/admin.py:185 +#: finance/admin.py:186 msgid "" "Transactions do not match the covered expenses. Please correct the mistakes " "listed below." @@ -69,19 +69,19 @@ msgstr "" "Überweisungen stimmen nicht mit den übernommenen Kosten überein. Bitte " "korrigiere die unten aufgeführten Fehler." -#: finance/admin.py:190 +#: finance/admin.py:191 msgid "Some transactions have no ledger configured. Please fill in the gaps." msgstr "" "Manche Überweisungen haben kein Geldtopf eingestellt. Bitte trage das nach." -#: finance/admin.py:199 +#: finance/admin.py:200 #, python-format msgid "Successfully rejected %(name)s. The requestor can reapply, when needed." msgstr "" "Die Rechnung %(name)s wurde abgelehnt. Die Person kann die Rechnung erneut " "einstellen, wenn es benötigt wird." -#: finance/admin.py:206 +#: finance/admin.py:207 #, python-format msgid "" "%(name)s already has transactions. Please delete them first, if you want to " @@ -90,12 +90,12 @@ msgstr "" "%(name)s hat bereits Überweisungen. Bitte lösche diese zunächst, bevor du " "neue generierst." -#: finance/admin.py:211 +#: finance/admin.py:212 #, python-format msgid "Successfully generated transactions for %(name)s" msgstr "Automatisch Überweisungsträger für %(name)s generiert." -#: finance/admin.py:214 +#: finance/admin.py:215 #, python-format msgid "" "Error while generating transactions for %(name)s. Do all bills have a payer?" @@ -103,28 +103,28 @@ msgstr "" "Fehler beim Erzeugen der Überweisungsträger für %(name)s. Sind für alle " "Quittungen eine bezahlende Person eingestellt? " -#: finance/admin.py:217 +#: finance/admin.py:218 msgid "View submitted statement" msgstr "Eingereichte Abrechnung einsehen" -#: finance/admin.py:245 +#: finance/admin.py:230 #, python-format msgid "Successfully reduced transactions for %(name)s." msgstr "Überweisungsträger für %(name)s minimiert." -#: finance/admin.py:289 +#: finance/admin.py:274 #, python-format msgid "%(name)s is not yet confirmed." msgstr "%(name)s ist noch nicht bestätigt." -#: finance/admin.py:298 +#: finance/admin.py:283 #, python-format msgid "Successfully unconfirmed %(name)s. I hope you know what you are doing." msgstr "" "Erfolgreich die Bestätigung von %(name)s zurückgenommen. Ich hoffe du weißt " "was du machst." -#: finance/admin.py:303 finance/templates/admin/unconfirm_statement.html:26 +#: finance/admin.py:288 finance/templates/admin/unconfirm_statement.html:26 msgid "Unconfirm statement" msgstr "Bestätigung zurücknehmen" @@ -132,165 +132,165 @@ msgstr "Bestätigung zurücknehmen" msgid "Finance" msgstr "Finanzen" -#: finance/models.py:19 +#: finance/models.py:21 msgid "Name" msgstr "Name" -#: finance/models.py:25 finance/models.py:441 finance/models.py:465 +#: finance/models.py:27 finance/models.py:472 finance/models.py:496 #: finance/templates/admin/confirmed_statement.html:38 #: finance/templates/admin/overview_submitted_statement.html:100 msgid "Ledger" msgstr "Geldtopf" -#: finance/models.py:26 +#: finance/models.py:28 msgid "Ledgers" msgstr "Geldtöpfe" -#: finance/models.py:46 finance/models.py:384 finance/models.py:464 +#: finance/models.py:48 finance/models.py:415 finance/models.py:495 msgid "Short description" msgstr "Kurzbeschreibung" -#: finance/models.py:49 finance/models.py:385 +#: finance/models.py:51 finance/models.py:416 msgid "Explanation" msgstr "Erklärung" -#: finance/models.py:51 +#: finance/models.py:53 msgid "Associated excursion" msgstr "Zugehörige Ausfahrt" -#: finance/models.py:56 +#: finance/models.py:58 msgid "Price per night" msgstr "Preis pro Nacht" -#: finance/models.py:58 +#: finance/models.py:60 msgid "Submitted" msgstr "Eingericht" -#: finance/models.py:59 +#: finance/models.py:61 msgid "Submitted on" msgstr "Eingereicht am" -#: finance/models.py:60 +#: finance/models.py:62 msgid "Confirmed" msgstr "Abgewickelt" -#: finance/models.py:61 finance/models.py:448 +#: finance/models.py:63 finance/models.py:479 msgid "Paid on" msgstr "Bezahlt am" -#: finance/models.py:63 +#: finance/models.py:65 msgid "Created by" msgstr "Erstellt von" -#: finance/models.py:68 +#: finance/models.py:70 msgid "Submitted by" -msgstr "Eingereicht bei" +msgstr "Eingereicht von" -#: finance/models.py:73 finance/models.py:449 +#: finance/models.py:75 finance/models.py:480 msgid "Authorized by" msgstr "Autorisiert von" -#: finance/models.py:80 finance/models.py:383 finance/models.py:444 +#: finance/models.py:82 finance/models.py:414 finance/models.py:475 msgid "Statement" msgstr "Abrechnung" -#: finance/models.py:81 +#: finance/models.py:83 msgid "Statements" msgstr "Abrechnungen" -#: finance/models.py:96 +#: finance/models.py:98 #, python-format msgid "Statement: %(excursion)s" msgstr "Abrechnung: %(excursion)s" -#: finance/models.py:148 +#: finance/models.py:150 msgid "Ready to confirm" msgstr "Bereit zur Abwicklung" -#: finance/models.py:192 +#: finance/models.py:194 #, python-format msgid "Compensation for %(excu)s" msgstr "Entschädigung für %(excu)s" -#: finance/models.py:325 +#: finance/models.py:327 #: finance/templates/admin/overview_submitted_statement.html:78 msgid "Total" msgstr "Gesamtbetrag" -#: finance/models.py:338 +#: finance/models.py:369 msgid "Statement in preparation" msgstr "Abrechnung in Vorbereitung" -#: finance/models.py:339 +#: finance/models.py:370 msgid "Statements in preparation" msgstr "Abrechnungen in Vorbereitung" -#: finance/models.py:358 +#: finance/models.py:389 msgid "Submitted statement" msgstr "Eingereichte Abrechnung" -#: finance/models.py:359 +#: finance/models.py:390 msgid "Submitted statements" msgstr "Eingereichte Abrechnungen" -#: finance/models.py:375 +#: finance/models.py:406 msgid "Paid statement" msgstr "Bezahlte Abrechnung" -#: finance/models.py:376 +#: finance/models.py:407 msgid "Paid statements" msgstr "Bezahlte Abrechnungen" -#: finance/models.py:388 +#: finance/models.py:418 finance/models.py:432 finance/models.py:469 +#: finance/templates/admin/confirmed_statement.html:36 +#: finance/templates/admin/overview_submitted_statement.html:31 +#: finance/templates/admin/overview_submitted_statement.html:98 +msgid "Amount" +msgstr "Betrag" + +#: finance/models.py:419 msgid "Paid by" msgstr "Bezahlt von" -#: finance/models.py:390 +#: finance/models.py:421 msgid "Covered" msgstr "Übernommen" -#: finance/models.py:391 +#: finance/models.py:422 msgid "Refunded" msgstr "Ausgezahlt" -#: finance/models.py:393 +#: finance/models.py:424 msgid "Proof" msgstr "Beleg" -#: finance/models.py:401 finance/models.py:438 -#: finance/templates/admin/confirmed_statement.html:36 -#: finance/templates/admin/overview_submitted_statement.html:31 -#: finance/templates/admin/overview_submitted_statement.html:98 -msgid "Amount" -msgstr "Betrag" - -#: finance/models.py:404 finance/models.py:411 finance/models.py:424 +#: finance/models.py:435 finance/models.py:442 finance/models.py:455 msgid "Bill" -msgstr "Quittung" +msgstr "Ausgabe" -#: finance/models.py:405 finance/models.py:412 finance/models.py:425 +#: finance/models.py:436 finance/models.py:443 finance/models.py:456 #: finance/templates/admin/overview_submitted_statement.html:26 msgid "Bills" -msgstr "Quittungen" +msgstr "Ausgaben" -#: finance/models.py:437 finance/templates/admin/confirmed_statement.html:37 +#: finance/models.py:468 finance/templates/admin/confirmed_statement.html:37 #: finance/templates/admin/overview_submitted_statement.html:99 msgid "Reference" msgstr "Verwendungszweck" -#: finance/models.py:439 +#: finance/models.py:470 msgid "Recipient" msgstr "Empfänger" -#: finance/models.py:447 +#: finance/models.py:478 msgid "Paid" msgstr "Bezahlt" -#: finance/models.py:459 +#: finance/models.py:490 msgid "Transaction" msgstr "Überweisung" -#: finance/models.py:460 +#: finance/models.py:491 #: finance/templates/admin/overview_submitted_statement.html:84 msgid "Transactions" msgstr "Überweisungen" @@ -351,7 +351,7 @@ msgstr "Ausfahrt" #, python-format msgid "This excursion featured %(staff_count)s youth leader(s), each costing" msgstr "" -"Diese Ausfahrt hatte %(staff_count)s Jugendleiter:innen. Auf jede:n " +"Diese Ausfahrt hatte %(staff_count)s Jugendleiter*innen. Auf jede*n " "entfallen die folgenden Kosten:" #: finance/templates/admin/overview_submitted_statement.html:62 diff --git a/jdav_web/finance/migrations/0004_alter_bill_amount.py b/jdav_web/finance/migrations/0004_alter_bill_amount.py new file mode 100644 index 0000000..127b3ee --- /dev/null +++ b/jdav_web/finance/migrations/0004_alter_bill_amount.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.1 on 2024-12-02 00:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('finance', '0003_alter_bill_options_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='bill', + name='amount', + field=models.DecimalField(decimal_places=2, default=0, max_digits=6, verbose_name='Amount'), + ), + ] diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py index 91f0a6c..4ad3196 100644 --- a/jdav_web/finance/models.py +++ b/jdav_web/finance/models.py @@ -6,12 +6,14 @@ from .rules import is_creator, not_submitted, leads_excursion from members.rules import is_leader, statement_not_submitted from django.db import models +from django.db.models import Sum from django.utils.translation import gettext_lazy as _ from members.models import Member, Freizeit, OEFFENTLICHE_ANREISE, MUSKELKRAFT_ANREISE from django.conf import settings import rules from contrib.models import CommonModel from contrib.rules import has_global_perm +from utils import cvt_to_decimal # Create your models here. @@ -186,7 +188,7 @@ class Statement(CommonModel): # excursion specific if self.excursion is None: - return + return True for yl in self.excursion.jugendleiter.all(): ref = _("Compensation for %(excu)s") % {'excu': self.excursion.name} @@ -283,7 +285,7 @@ class Statement(CommonModel): if self.excursion is None: return 0 - return self.total_staff / self.excursion.staff_count + return cvt_to_decimal(self.total_staff / self.excursion.staff_count) @property def total_staff(self): @@ -324,6 +326,37 @@ class Statement(CommonModel): return "{}€".format(self.total) total_pretty.short_description = _('Total') + def template_context(self): + context = { + 'total_bills': self.total_bills, + 'total_bills_theoretic': self.total_bills_theoretic, + 'total': self.total, + } + if self.excursion: + excursion_context = { + 'nights': self.excursion.night_count, + 'price_per_night': self.real_night_cost, + 'duration': self.excursion.duration, + 'staff_count': self.real_staff_count, + 'kilometers_traveled': self.excursion.kilometers_traveled, + 'means_of_transport': self.excursion.get_tour_approach(), + 'euro_per_km': self.euro_per_km, + 'allowance_per_day': settings.ALLOWANCE_PER_DAY, + 'nights_per_yl': self.nights_per_yl, + 'allowance_per_yl': self.allowance_per_yl, + 'transportation_per_yl': self.transportation_per_yl, + 'total_per_yl': self.total_per_yl, + 'total_staff': self.total_staff, + } + return dict(context, **excursion_context) + else: + return context + + def grouped_bills(self): + return self.bill_set.values('short_description')\ + .order_by('short_description')\ + .annotate(amount=Sum('amount')) + class StatementUnSubmittedManager(models.Manager): def get_queryset(self): @@ -384,7 +417,7 @@ class Bill(CommonModel): short_description = models.CharField(verbose_name=_('Short description'), max_length=30) explanation = models.TextField(verbose_name=_('Explanation'), blank=True) - amount = models.DecimalField(max_digits=6, decimal_places=2, default=0) + amount = models.DecimalField(verbose_name=_('Amount'), max_digits=6, decimal_places=2, default=0) paid_by = models.ForeignKey(Member, verbose_name=_('Paid by'), null=True, on_delete=models.SET_NULL) costs_covered = models.BooleanField(verbose_name=_('Covered'), default=False) @@ -466,7 +499,3 @@ class Receipt(models.Model): on_delete=models.CASCADE) amount = models.DecimalField(max_digits=6, decimal_places=2) comments = models.TextField() - - -def cvt_to_decimal(f): - return Decimal(f).quantize(Decimal('.01'), rounding=ROUND_HALF_DOWN) diff --git a/jdav_web/jdav_web/settings/components/emails.py b/jdav_web/jdav_web/settings/components/emails.py index a1b7a92..a93ec23 100644 --- a/jdav_web/jdav_web/settings/components/emails.py +++ b/jdav_web/jdav_web/settings/components/emails.py @@ -14,3 +14,4 @@ CELERY_EMAIL_TASK_CONFIG = { } DEFAULT_SENDING_MAIL = os.environ.get('EMAIL_SENDING_ADDRESS', 'django@localhost') +DEFAULT_SENDING_NAME = os.environ.get('EMAIL_SENDING_NAME', 'Kompass') diff --git a/jdav_web/jdav_web/settings/components/texts.py b/jdav_web/jdav_web/settings/components/texts.py index 6371e57..68042f8 100644 --- a/jdav_web/jdav_web/settings/components/texts.py +++ b/jdav_web/jdav_web/settings/components/texts.py @@ -22,12 +22,19 @@ der Registrierung kommst du hier: Viele Grüße Dein KOMPASS""" +GROUP_TIME_AVAILABLE_TEXT = """Die Gruppenstunde findet jeden {weekday} von {start_time} bis {end_time} Uhr statt.""" + +GROUP_TIME_UNAVAILABLE_TEXT = """Bitte erfrage die Gruppenzeiten bei der Gruppenleitung ({contact_email}).""" + INVITE_TEXT = """Hallo {name}, wir haben gute Neuigkeiten für dich. Es ist ein Platz in der Jugendgruppe {group_name} {group_link}freigeworden. -Wir treffen uns jeden {weekday} von {start_time} bis {end_time} Uhr. +{group_time} + +Bitte kontaktiere die Gruppenleitung ({contact_email}) für alle weiteren Absprachen. -Wir brauchen jetzt noch ein paar Informationen von dir und deine Anmeldebestätigung. Die lädst du herunter +Wenn du nach der Schnupperstunde beschließt der Gruppe beizutreten, benötigen wir noch ein paar +Informationen und deine Anmeldebestätigung von dir. Die lädst du herunter (siehe %(REGISTRATION_FORM_DOWNLOAD_LINK)s), lässt sie von deinen Eltern ausfüllen, unterschreiben und lädst ein Foto davon in unserem Anmeldeformular hoch. Das kannst du alles über folgenden Link erledigen: @@ -144,7 +151,7 @@ kannst Du hier den Newsletter deabonnieren: INVITE_AS_USER_TEXT = """Hallo {name}, -du bist Jugendleiter:in in der Sektion %(SEKTION)s. Die Verwaltung unserer Jugendgruppen, +du bist Jugendleiter*in in der Sektion %(SEKTION)s. Die Verwaltung unserer Jugendgruppen, Ausfahrten und Finanzen erfolgt in unserer Online Plattform Kompass. Deine Stammdaten sind dort bereits hinterlegt. Damit du dich auch anmelden kannst, folge bitte dem folgenden Link und wähle ein Passwort. @@ -155,3 +162,8 @@ Bei Fragen, wende dich gerne an %(RESPONSIBLE_MAIL)s. Viele Grüße Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION, 'RESPONSIBLE_MAIL': RESPONSIBLE_MAIL } + + +ADDRESS = """JDAV %(SEKTION)s +%(STREET)s +%(PLACE)s""" % { 'SEKTION': SEKTION, 'STREET': SEKTION_STREET, 'PLACE': SEKTION_TOWN } diff --git a/jdav_web/jdav_web/settings/local.py b/jdav_web/jdav_web/settings/local.py index 2e68dfd..28f0c56 100644 --- a/jdav_web/jdav_web/settings/local.py +++ b/jdav_web/jdav_web/settings/local.py @@ -8,21 +8,41 @@ SEKTION_TELEFAX = "06221 437338" SEKTION_CONTACT_MAIL = "geschaeftsstelle@alpenverein-heidelberg.de" SEKTION_BOARD_MAIL = "vorstand@alpenverein-heidelberg.de" SEKTION_CRISIS_INTERVENTION_MAIL = "krisenmanagement@alpenverein-heidelberg.de" +SEKTION_IBAN = "DE22 6729 0000 0000 1019 40" +SEKTION_ACCOUNT_HOLDER = "Deutscher Alpenverein Sektion Heidelberg 1869" RESPONSIBLE_MAIL = "jugendreferat@jdav-hd.de" +DIGITAL_MAIL = "digitales@jdav-hd.de" + +# LJP + +V32_HEAD_ORGANISATION = """JDAV Baden-Württemberg +Rotebühlstraße 59A +70178 Stuttgart + +info@jdav-bw.de +0711 - 49 09 46 00""" + +LJP_CONTRIBUTION_PER_DAY = 25 # echo ECHO_PASSWORD_BIRTHDATE_FORMAT = '%d.%m.%Y' +ECHO_GRACE_PERIOD = 30 # misc CONGRATULATE_MEMBERS_MAX = 10 MAX_AGE_GOOD_CONDUCT_CERTIFICATE_MONTHS = 24 +ALLOWED_EMAIL_DOMAINS_FOR_INVITE_AS_USER = ('alpenverein-heidelberg.de', ) + +# mail mode + +SEND_FROM_ASSOCIATION_EMAIL = os.environ.get('SEND_FROM_ASSOCIATION_EMAIL', '0') == '1' # finance -ALLOWANCE_PER_DAY = 10 +ALLOWANCE_PER_DAY = 22 MAX_NIGHT_COST = 11 CLOUD_LINK = 'https://nc.cloud-jdav-hd.de' @@ -46,9 +66,9 @@ TEST_MAIL = "post@flavigny.de" REGISTRATION_FORM_DOWNLOAD_LINK = 'https://nc.cloud-jdav-hd.de' -DOMAIN = 'jdav-hd.merten.dev' +DOMAIN = os.environ.get('DOMAIN', 'example.com') STARTPAGE_REDIRECT_URL = 'https://jdav-hd.de' -ROOT_SECTION = 'jdav_heidelberg' +ROOT_SECTION = os.environ.get('ROOT_SECTION', 'wir') RECENT_SECTION = 'aktuelles' REPORTS_SECTION = 'berichte' diff --git a/jdav_web/locale/de/LC_MESSAGES/django.po b/jdav_web/locale/de/LC_MESSAGES/django.po index 27b06a5..14cfefa 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: 2024-11-24 18:18+0100\n" +"POT-Creation-Date: 2024-12-01 16:23+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -67,28 +67,28 @@ msgstr "Aktives Registrierungspasswort" msgid "Active registration passwords" msgstr "Aktive Registrierungspasswörter" -#: logindata/templates/logindata/register_failed.html:6 +#: logindata/templates/logindata/register_failed.html:5 msgid "Registration" msgstr "Registrierung" -#: logindata/templates/logindata/register_failed.html:11 +#: logindata/templates/logindata/register_failed.html:10 #: logindata/templates/logindata/register_form.html:13 #: logindata/templates/logindata/register_password.html:11 #: logindata/templates/logindata/register_success.html:10 msgid "Set login data" msgstr "Zugangsdaten wählen" -#: logindata/templates/logindata/register_failed.html:13 +#: logindata/templates/logindata/register_failed.html:12 msgid "Something went wrong. The registration key is invalid or has expired." msgstr "" "Etwas ist schief gegangen. Der Registrierungscode ist ungültig oder ist " "abgelaufen." -#: logindata/templates/logindata/register_failed.html:15 +#: logindata/templates/logindata/register_failed.html:14 msgid "If you think this is a mistake, please" msgstr "Falls du denkst, dass das ein Fehler ist, bitte" -#: logindata/templates/logindata/register_failed.html:15 +#: logindata/templates/logindata/register_failed.html:14 msgid "contact us." msgstr "kontaktiere uns." @@ -142,9 +142,9 @@ msgid "" "related objects, but your account doesn't have permission to delete the " "following types of objects:" msgstr "" -"Löschen von %(object_name)s '%(escaped_object)s' würde zur Löschung der folgenden " -"verknüpften Objekte führen, aber du hast nicht die Berechtigung die folgenden Typen " -"von Objekten zu löschen:" +"Löschen von %(object_name)s '%(escaped_object)s' würde zur Löschung der " +"folgenden verknüpften Objekte führen, aber du hast nicht die Berechtigung " +"die folgenden Typen von Objekten zu löschen:" #: templates/admin/delete_confirmation.html:12 #, python-format @@ -152,16 +152,16 @@ msgid "" "Deleting the %(object_name)s '%(escaped_object)s' would require deleting the " "following protected related objects:" msgstr "" -"Löschen von %(object_name)s '%(escaped_object)s' würde zur Löschung der folgenden " -"geschützten verknüpften Objekte führen:" +"Löschen von %(object_name)s '%(escaped_object)s' würde zur Löschung der " +"folgenden geschützten verknüpften Objekte führen:" #: templates/admin/delete_confirmation.html:17 #, python-format msgid "" "Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"?" msgstr "" -"Bist du sicher, dass du %(object_name)s \"%(escaped_object)s\" und alle davon abhängigen " -"Objekte löschen möchtest? " +"Bist du sicher, dass du %(object_name)s \"%(escaped_object)s\" und alle " +"davon abhängigen Objekte löschen möchtest? " #: templates/admin/delete_confirmation.html:29 #: templates/admin/delete_selected_confirmation.html:34 @@ -181,8 +181,8 @@ msgid "" "types of objects:" msgstr "" "Löschen der ausgewählten %(objects_name)s würde zur Löschung der folgenden " -"verknüpften Objekte führen, aber du hast nicht die Berechtigung die folgenden Typen " -"von Objekten zu löschen:" +"verknüpften Objekte führen, aber du hast nicht die Berechtigung die " +"folgenden Typen von Objekten zu löschen:" #: templates/admin/delete_selected_confirmation.html:9 #, python-format @@ -210,6 +210,28 @@ msgstr "Zusammenfassung" msgid "Objects" msgstr "Objekte" +#: templates/admin/edit_inline/stacked.html:20 +#: templates/admin/edit_inline/tabular.html:47 +#: templates/nesting/admin/inlines/stacked.html:42 +msgid "Change" +msgstr "Ändern" + +#: templates/admin/edit_inline/stacked.html:20 +#: templates/admin/edit_inline/tabular.html:47 +#: templates/nesting/admin/inlines/stacked.html:42 +msgid "View" +msgstr "Anzeigen" + +#: templates/admin/edit_inline/stacked.html:22 +#: templates/admin/edit_inline/tabular.html:49 +#: templates/nesting/admin/inlines/stacked.html:44 +msgid "View on site" +msgstr "Auf der Website anzeigen" + +#: templates/admin/edit_inline/tabular.html:33 +msgid "Delete?" +msgstr "Löschen?" + #: templates/admin/finance/statementconfirmed/change_form_object_tools.html:8 msgid "Unconfirm" msgstr "Bestätigung zurücknehmen" @@ -235,34 +257,46 @@ msgid "Generate SJR application" msgstr "SJR Antrag erstellen" #: templates/admin/members/freizeit/change_form_object_tools.html:23 -msgid "Generate overview" -msgstr "Übersicht erstellen" +msgid "Generate seminar report" +msgstr "Landesjugendplan Antrag erstellen" #: templates/admin/members/freizeit/change_form_object_tools.html:30 -msgid "Generate seminar report" -msgstr "Seminarbericht erstellen" +msgid "Generate overview" +msgstr "Hinweise für Jugendleiter*innen erstellen" -#: templates/admin/members/freizeit/change_form_object_tools.html:36 -msgid "Submit statement" -msgstr "Abrechnung einreichen" +#: templates/admin/members/freizeit/change_form_object_tools.html:38 +msgid "Finance overview" +msgstr "Kostenübersicht" #: templates/admin/members/member/change_form_object_tools.html:8 msgid "Invite as user" -msgstr "Als Kompassbenutzer:in einladen" +msgstr "Als Kompassbenutzer*in einladen" + +#: templates/admin/members/memberunconfirmedproxy/change_form_object_tools.html:8 +msgid "Demote to waiter" +msgstr "Zurück auf die Warteliste setzen" #: templates/admin/members/memberwaitinglist/change_form_object_tools.html:8 #: templates/admin/members/memberwaitinglist/submit_line.html:9 msgid "Invite to group" msgstr "Zu Gruppe einladen" -#: utils.py:14 +#: templates/nesting/admin/inlines/stacked.html:87 +#, python-format +msgid "Add another %(verbose_name)s" +msgstr "Weiteren %(verbose_name)s hinzufügen" + +#: utils.py:15 msgid "Please keep filesize under {} MiB. Current filesize: {:10.2f} MiB." msgstr "Maximale Dateigröße {} MiB. Aktuelle Dateigröße: {:10.2f} MiB." -#: utils.py:42 +#: utils.py:43 msgid "Filetype not supported." msgstr "Dateityp nicht unterstützt." -#: utils.py:44 +#: utils.py:45 msgid "Please keep filesize under {}. Current filesize: {}" msgstr "Maximale Dateigröße {}. Aktuelle Dateigröße: {}." + +#~ msgid "Submit statement" +#~ msgstr "Abrechnung einreichen" diff --git a/jdav_web/logindata/templates/logindata/register_failed.html b/jdav_web/logindata/templates/logindata/register_failed.html index 433815a..5484579 100644 --- a/jdav_web/logindata/templates/logindata/register_failed.html +++ b/jdav_web/logindata/templates/logindata/register_failed.html @@ -1,6 +1,5 @@ {% extends "members/base.html" %} -{% load i18n %} -{% load static %} +{% load i18n static common %} {% block title %} {% trans "Registration" %} @@ -12,6 +11,6 @@

{% trans "Something went wrong. The registration key is invalid or has expired." %}

-

{% trans "If you think this is a mistake, please" %} {% trans "contact us." %}

+

{% trans "If you think this is a mistake, please" %} {% trans "contact us." %}

{% endblock %} diff --git a/jdav_web/mailer/admin.py b/jdav_web/mailer/admin.py index 6fc2a3f..a889d06 100644 --- a/jdav_web/mailer/admin.py +++ b/jdav_web/mailer/admin.py @@ -22,7 +22,8 @@ class AttachmentInline(CommonAdminInlineMixin, admin.TabularInline): class EmailAddressAdmin(FilteredMemberFieldMixin, admin.ModelAdmin): - list_display = ('email', ) + list_display = ('email', 'internal_only') + fields = ('name', 'to_members', 'to_groups', 'internal_only') #formfield_overrides = { # models.ManyToManyField: {'widget': forms.CheckboxSelectMultiple}, # models.ForeignKey: {'widget': apply_select2(forms.Select)} @@ -33,9 +34,10 @@ class EmailAddressAdmin(FilteredMemberFieldMixin, admin.ModelAdmin): class MessageAdmin(FilteredMemberFieldMixin, CommonAdminMixin, ObjectPermissionsModelAdmin): """Message creation view""" - exclude = ('created_by',) + exclude = ('created_by', 'to_notelist') list_display = ('subject', 'get_recipients', 'sent') search_fields = ('subject',) + list_filter = ('sent',) change_form_template = "mailer/change_form.html" readonly_fields = ('sent',) #formfield_overrides = { @@ -90,8 +92,13 @@ class MessageAdmin(FilteredMemberFieldMixin, CommonAdminMixin, ObjectPermissions def submit_message(msg, request): sender = None - if hasattr(request.user, 'member'): - sender = request.user.member + if not hasattr(request.user, 'member'): + messages.error(request, _("Your account is not connected to a member. Please contact your system administrator.")) + return + sender = request.user.member + if not sender.has_internal_email(): + messages.error(request, _("Your email address is not an internal email address. Please change your email address and try again.")) + return success = msg.submit(sender) if success == NOT_SENT: messages.error(request, _("Failed to send message")) diff --git a/jdav_web/mailer/locale/de/LC_MESSAGES/django.po b/jdav_web/mailer/locale/de/LC_MESSAGES/django.po index a1886e1..7a7e0ec 100644 --- a/jdav_web/mailer/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/mailer/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: 2024-11-18 21:46+0100\n" +"POT-Creation-Date: 2024-12-02 22:50+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,19 +18,35 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: mailer/admin.py:67 +#: mailer/admin.py:69 msgid "Send message" msgstr "Nachricht verschicken" -#: mailer/admin.py:97 +#: mailer/admin.py:96 +msgid "" +"Your account is not connected to a member. Please contact your system " +"administrator." +msgstr "" +"Dein Account ist nicht mit eine*r Teilnehmer*in verknüpft. Bitte kontaktiere " +"deine*n Systemadministrator*in." + +#: mailer/admin.py:100 +msgid "" +"Your email address is not an internal email address. Please change your " +"email address and try again." +msgstr "" +"Deine E-Mail Adresse ist keine DAV360 E-Mail Adresse. Bitte stelle sicher, " +"dass deine E-Mail Adresse mit @alpenverein-heidelberg.de endet." + +#: mailer/admin.py:104 msgid "Failed to send message" msgstr "Fehler beim Senden der Email" -#: mailer/admin.py:99 +#: mailer/admin.py:106 msgid "Failed to send some messages" msgstr "Fehler beim Senden der Emails" -#: mailer/admin.py:101 +#: mailer/admin.py:108 msgid "Successfully sent message" msgstr "Email wurde erfolgreich verschickt" @@ -43,116 +59,138 @@ msgstr "Verteiler" msgid "Congratulation %(name)s" msgstr "Herzlichen Glückwunsch %(name)s" -#: mailer/models.py:19 +#: mailer/models.py:20 msgid "Only alphanumeric characters, ., - and _ are allowed" msgstr "Nur Buchstaben, Zahlen, ., . und _ sind erlaubt" -#: mailer/models.py:24 +#: mailer/models.py:25 msgid "name" msgstr "Name" -#: mailer/models.py:26 +#: mailer/models.py:27 msgid "Forward to participants" -msgstr "Weiterleitung an Teilnehmer" +msgstr "Weiterleitung an Teilnehmer*innen" -#: mailer/models.py:29 +#: mailer/models.py:30 msgid "Forward to group" msgstr "Weiterleitung an Gruppe" -#: mailer/models.py:46 +#: mailer/models.py:32 +msgid "Restrict to internal email addresses" +msgstr "Weiterleitung nur von internen E-Mail Adressen erlaubt" + +#: mailer/models.py:33 +msgid "Only allow forwarding to this e-mail address from the internal domain." +msgstr "" +"Leite nur E-Mails weiter, die von ...@alpenverein-heidelberg.de verschickt " +"wurden. " + +#: mailer/models.py:36 +msgid "Allowed sender" +msgstr "Erlaubte Absender:innen" + +#: mailer/models.py:37 +msgid "" +"Only forward e-mails of members of selected groups. Leave empty to allow all " +"senders." +msgstr "" +"Leite nur E-Mails von Mitgliedern dieser Gruppen weiter. Lasse dieses Feld " +"frei, um alle Absender*innen zu erlauben." + +#: mailer/models.py:55 msgid "email address" msgstr "Email-Adresse" -#: mailer/models.py:47 +#: mailer/models.py:56 msgid "email addresses" msgstr "Email-Adressen" -#: mailer/models.py:60 +#: mailer/models.py:69 msgid "Either a group or at least one member is required as forward recipient." msgstr "" -"Es muss entweder eine Gruppe oder mindestens ein Teilnehmer als Empfänger " -"ausgewählt werden." +"Es muss entweder eine Gruppe oder mindestens ein*e Teilnehmer*in als " +"Empfänger*in ausgewählt werden." -#: mailer/models.py:68 +#: mailer/models.py:77 msgid "subject" msgstr "Betreff" -#: mailer/models.py:69 +#: mailer/models.py:78 msgid "content" msgstr "Inhalt" -#: mailer/models.py:71 +#: mailer/models.py:80 msgid "to group" msgstr "An Gruppe" -#: mailer/models.py:74 +#: mailer/models.py:83 msgid "to freizeit" msgstr "An Ausfahrt" -#: mailer/models.py:79 +#: mailer/models.py:88 msgid "to notes list" msgstr "An Notizliste" -#: mailer/models.py:84 +#: mailer/models.py:93 msgid "to member" -msgstr "An Teilnehmer" +msgstr "An Teilnehmer*innen" -#: mailer/models.py:87 +#: mailer/models.py:96 msgid "reply to participant" -msgstr "Antwort an Teilnehmer" +msgstr "Antwort an Teilnehmer*innen" -#: mailer/models.py:91 +#: mailer/models.py:100 msgid "reply to custom email address" msgstr "Antwort an Email-Adresse" -#: mailer/models.py:94 +#: mailer/models.py:103 msgid "sent" msgstr "Gesendet" -#: mailer/models.py:95 +#: mailer/models.py:104 msgid "Created by" msgstr "Erstellt von" -#: mailer/models.py:113 +#: mailer/models.py:122 msgid "Some other members" -msgstr "Andere Teilnehmer" +msgstr "Andere Teilnehmer*innen" -#: mailer/models.py:115 +#: mailer/models.py:124 msgid "recipients" msgstr "Empfänger" -#: mailer/models.py:178 +#: mailer/models.py:196 msgid "message" msgstr "Nachricht" -#: mailer/models.py:179 +#: mailer/models.py:197 msgid "messages" msgstr "Nachrichten" -#: mailer/models.py:181 +#: mailer/models.py:199 msgid "Can submit mails" msgstr "Kann Mails verschicken" -#: mailer/models.py:202 +#: mailer/models.py:220 msgid "" "Either a group, a memberlist or at least one member is required as recipient" msgstr "" -"Es muss entweder eine Gruppe, eine Teilnehmerliste oder mindestens ein " -"Teilnehmer als Empfänger ausgewählt werden." +"Es muss entweder eine Gruppe, eine Teilnehmer*innenliste oder mindestens " +"ein*e Teilnehmer*in als Empfänger*in ausgewählt werden." -#: mailer/models.py:209 +#: mailer/models.py:227 msgid "file" msgstr "Datei" -#: mailer/models.py:214 +#: mailer/models.py:232 msgid "Empty" msgstr "Leer" -#: mailer/models.py:217 +#: mailer/models.py:235 msgid "attachment" msgstr "Anhang" -#: mailer/models.py:218 +#: mailer/models.py:236 msgid "attachments" msgstr "Anhänge" diff --git a/jdav_web/mailer/mailutils.py b/jdav_web/mailer/mailutils.py index 701de61..b2fe2d9 100644 --- a/jdav_web/mailer/mailutils.py +++ b/jdav_web/mailer/mailutils.py @@ -7,14 +7,20 @@ import os NOT_SENT, SENT, PARTLY_SENT = 0, 1, 2 def send(subject, content, sender, recipients, message_id=None, reply_to=None, - attachments=None): + attachments=None, cc=None): failed, succeeded = False, False if type(recipients) != list: recipients = [recipients] + if not cc: + cc = [] + elif type(cc) != list: + cc = [cc] if reply_to is not None: kwargs = {"reply_to": reply_to} else: kwargs = {} + if sender == settings.DEFAULT_SENDING_MAIL: + sender = addr_with_name(settings.DEFAULT_SENDING_MAIL, settings.DEFAULT_SENDING_NAME) url = prepend_base_url("/newsletter/unsubscribe") headers = {'List-Unsubscribe': '<{unsubscribe_url}>'.format(unsubscribe_url=url)} if message_id is not None: @@ -23,7 +29,7 @@ def send(subject, content, sender, recipients, message_id=None, reply_to=None, # construct mails mails = [] for recipient in set(recipients): - email = EmailMessage(subject, content, sender, [recipient], + email = EmailMessage(subject, content, sender, [recipient], cc=cc, headers=headers, **kwargs) if attachments is not None: for attach in attachments: @@ -47,10 +53,8 @@ def send(subject, content, sender, recipients, message_id=None, reply_to=None, def get_content(content, registration_complete=True): url = prepend_base_url("/newsletter/unsubscribe") prepend = settings.PREPEND_INCOMPLETE_REGISTRATION_TEXT - footer = settings.MAIL_FOOTER.format(link=url) - text = "{prepend}{content}{footer}".format(prepend="" if registration_complete else prepend, - content=content, - footer=footer) + text = "{prepend}{content}".format(prepend="" if registration_complete else prepend, + content=content) return text @@ -87,3 +91,7 @@ def get_invite_as_user_key(key): def prepend_base_url(absolutelink): return "{protocol}://{base}{link}".format(protocol=settings.PROTOCOL, base=settings.BASE_URL, link=absolutelink) + + +def addr_with_name(addr, name): + return "{name} <{addr}>".format(name=name, addr=addr) diff --git a/jdav_web/mailer/migrations/0006_emailaddress_allowed_senders.py b/jdav_web/mailer/migrations/0006_emailaddress_allowed_senders.py new file mode 100644 index 0000000..dd6de2f --- /dev/null +++ b/jdav_web/mailer/migrations/0006_emailaddress_allowed_senders.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.1 on 2024-12-01 15:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0029_alter_member_gender_alter_memberwaitinglist_gender'), + ('mailer', '0005_alter_emailaddress_name'), + ] + + operations = [ + migrations.AddField( + model_name='emailaddress', + name='allowed_senders', + field=models.ManyToManyField(blank=True, help_text='Only forward e-mails of members of selected groups. Leave empty to allow all senders.', related_name='allowed_sender_on_emailaddresses', to='members.Group', verbose_name='Allowed sender'), + ), + ] diff --git a/jdav_web/mailer/migrations/0007_emailaddress_internal_only.py b/jdav_web/mailer/migrations/0007_emailaddress_internal_only.py new file mode 100644 index 0000000..6bc75ce --- /dev/null +++ b/jdav_web/mailer/migrations/0007_emailaddress_internal_only.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.1 on 2024-12-01 17:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailer', '0006_emailaddress_allowed_senders'), + ] + + operations = [ + migrations.AddField( + model_name='emailaddress', + name='internal_only', + field=models.BooleanField(default=False, help_text='Only allow forwarding to this e-mail address from the internal domain.', verbose_name='Restrict to internal email addresses'), + ), + ] diff --git a/jdav_web/mailer/models.py b/jdav_web/mailer/models.py index 0e90440..bed3877 100644 --- a/jdav_web/mailer/models.py +++ b/jdav_web/mailer/models.py @@ -3,7 +3,8 @@ from django.core.exceptions import ValidationError from django import forms from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext -from .mailutils import send, get_content, NOT_SENT, SENT, PARTLY_SENT +from .mailutils import send, get_content, NOT_SENT, SENT, PARTLY_SENT,\ + addr_with_name from utils import RestrictedFileField from jdav_web.celery import app from django.core.validators import RegexValidator @@ -28,6 +29,14 @@ class EmailAddress(models.Model): to_groups = models.ManyToManyField('members.Group', verbose_name=_('Forward to group'), blank=True) + internal_only = models.BooleanField(verbose_name=_('Restrict to internal email addresses'), + help_text=_('Only allow forwarding to this e-mail address from the internal domain.'), + default=False) + allowed_senders = models.ManyToManyField('members.Group', + verbose_name=_('Allowed sender'), + help_text=_('Only forward e-mails of members of selected groups. Leave empty to allow all senders.'), + blank=True, + related_name='allowed_sender_on_emailaddresses') @property def email(self): @@ -149,10 +158,19 @@ class Message(CommonModel): reply_to = [jl.association_email for jl in self.reply_to.all()] reply_to.extend([ml.email for ml in self.reply_to_email_address.all()]) # set correct from address + # if the sender is none or if sending from association emails has been + # disabled, use the default sending mail if sender is None: - from_addr = settings.DEFAULT_SENDING_MAIL + from_addr = addr_with_name(settings.DEFAULT_SENDING_MAIL, settings.DEFAULT_SENDING_NAME) + elif sender and settings.SEND_FROM_ASSOCIATION_EMAIL: + from_addr = addr_with_name(sender.association_email, sender.name) else: - from_addr = sender.association_email + from_addr = addr_with_name(settings.DEFAULT_SENDING_MAIL, sender.name) + # if sending from the association email has been disabled, + # a sender was supplied and the reply to is empty, add the sender's + # DAV360 email as reply to + if sender and not settings.SEND_FROM_ASSOCIATION_EMAIL and sender.has_internal_email() and reply_to == []: + reply_to.append(addr_with_name(sender.email, sender.name)) try: success = send(self.subject, get_content(self.content, registration_complete=True), from_addr, diff --git a/jdav_web/material/locale/de/LC_MESSAGES/django.po b/jdav_web/material/locale/de/LC_MESSAGES/django.po index 6eb6613..12557af 100644 --- a/jdav_web/material/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/material/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: 2024-11-13 23:36+0100\n" +"POT-Creation-Date: 2024-12-01 16:23+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index eacf19f..2466faf 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -23,10 +23,10 @@ from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from django.db.models import TextField, ManyToManyField, ForeignKey, Count,\ Sum, Case, Q, F, When, Value, IntegerField, Subquery, OuterRef -from django.forms import Textarea, RadioSelect, TypedChoiceField +from django.forms import Textarea, RadioSelect, TypedChoiceField, CheckboxInput from django.shortcuts import render from django.core.exceptions import PermissionDenied -from .pdf import render_tex, fill_pdf_form +from .pdf import render_tex, fill_pdf_form, merge_pdfs, serve_pdf from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin @@ -41,6 +41,7 @@ from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, K from finance.models import Statement, BillOnExcursionProxy from mailer.mailutils import send as send_mail, get_echo_link from django.conf import settings +from utils import get_member #from easy_select2 import apply_select2 @@ -112,8 +113,7 @@ class EmergencyContactInline(CommonAdminInlineMixin, admin.TabularInline): formfield_overrides = { TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} } - fields = ['prename', 'lastname', 'email', 'phone_number', 'confirmed_mail'] - readonly_fields = ['confirmed_mail'] + fields = ['prename', 'lastname', 'email', 'phone_number'] extra = 0 @@ -170,6 +170,7 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): ('email', 'alternative_email'), 'phone_number', 'birth_date', + 'gender', 'group', 'registration_form', 'image', ('join_date', 'leave_date'), 'comments', @@ -232,7 +233,7 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): field_change_permissions = { 'user': 'members.may_set_auth_user', - 'group': 'members.may_change_group', + 'group': 'members.may_change_member_group', 'good_conduct_certificate_presented_date': 'members.may_change_organizationals', 'has_key': 'members.may_change_organizationals', 'has_free_ticket_gym': 'members.may_change_organizationals', @@ -289,12 +290,19 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): request_echo.short_description = _('Request echo from selected members') def invite_as_user(self, request, queryset): + failures = [] for member in queryset: - member.invite_as_user() - if queryset.count() == 1: + success = member.invite_as_user() + if not success: + failures.append(member) + messages.error(request, + _('%(name)s does not have a DAV360 email address or is already registered.') % {'name': member.name}) + if queryset.count() == 1 and len(failures) == 0: messages.success(request, _('Successfully invited %(name)s as user.') % {'name': queryset[0].name}) - else: + elif len(failures) == 0: messages.success(request, _('Successfully invited selected members to join as users.')) + else: + messages.warning(request, _('Some members have been invited, others could not be invited.')) def has_may_invite_as_user_permission(self, request): return request.user.has_perm('%s.%s' % (self.opts.app_label, 'may_invite_as_user')) @@ -331,6 +339,11 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): _("%(name)s already has login data.") % {'name': str(m)}) return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(object_id,))) + if not m.has_internal_email(): + messages.error(request, + _("The configured email address for %(name)s is not an internal one.") % {'name': str(m)}) + return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), + args=(object_id,))) if "apply" in request.POST: self.invite_as_user(request, Member.objects.filter(pk=object_id)) return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), @@ -374,7 +387,11 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): name_text_or_link.admin_order_field = 'lastname' -class MemberUnconfirmedAdmin(admin.ModelAdmin): +class DemoteToWaiterForm(forms.Form): + _selected_action = forms.CharField(widget=forms.MultipleHiddenInput) + + +class MemberUnconfirmedAdmin(CommonAdminMixin, admin.ModelAdmin): fieldsets = [ (None, { @@ -382,6 +399,7 @@ class MemberUnconfirmedAdmin(admin.ModelAdmin): ('email', 'alternative_email'), 'phone_number', 'birth_date', + 'gender', 'group', 'registration_form', 'image', ('join_date', 'leave_date'), 'comments', @@ -421,10 +439,24 @@ class MemberUnconfirmedAdmin(admin.ModelAdmin): list_filter = ('group', 'confirmed_mail', 'confirmed_alternative_mail') readonly_fields = ['confirmed_mail', 'confirmed_alternative_mail', 'good_conduct_certificate_valid'] - actions = ['request_mail_confirmation', 'confirm', 'demote_to_waiter'] + actions = ['request_mail_confirmation', 'confirm', 'demote_to_waiter_action'] inlines = [EmergencyContactInline] change_form_template = "members/change_member_unconfirmed.html" + field_view_permissions = { + 'user': 'members.may_set_auth_user', + 'good_conduct_certificate_presented_date': 'members.may_change_organizationals', + 'has_key': 'members.may_change_organizationals', + 'has_free_ticket_gym': 'members.may_change_organizationals', + } + + field_change_permissions = { + 'user': 'members.may_set_auth_user', + 'good_conduct_certificate_presented_date': 'members.may_change_organizationals', + 'has_key': 'members.may_change_organizationals', + 'has_free_ticket_gym': 'members.may_change_organizationals', + } + def has_add_permission(self, request, obj=None): return False @@ -463,22 +495,62 @@ class MemberUnconfirmedAdmin(admin.ModelAdmin): messages.error(request, _("Failed to confirm some registrations because of unconfirmed email addresses.")) confirm.short_description = _('Confirm selected registrations') + def get_urls(self): + urls = super().get_urls() + + def wrap(view): + def wrapper(*args, **kwargs): + return self.admin_site.admin_view(view)(*args, **kwargs) + + wrapper.model_admin = self + return update_wrapper(wrapper, view) + + custom_urls = [ + path( + "/demote/", + wrap(self.demote_to_waiter_view), + name="%s_%s_demote" % (self.opts.app_label, self.opts.model_name), + ), + ] + return custom_urls + urls + + def demote_to_waiter_action(self, request, queryset): + return self.demote_to_waiter_view(request, queryset) + demote_to_waiter_action.short_description = _('Demote selected registrations to waiters.') + + def demote_to_waiter_view(self, request, object_id): + if type(object_id) == str: + member = MemberUnconfirmedProxy.objects.get(pk=object_id) + queryset = [member] + form = None + else: + queryset = object_id + form = DemoteToWaiterForm(initial={'_selected_action': queryset.values_list('id', flat=True)}) + + if "apply" in request.POST: + self.demote_to_waiter(request, queryset) + return HttpResponseRedirect(reverse('admin:members_memberunconfirmedproxy_changelist')) + + context = dict(self.admin_site.each_context(request), + title=_('Demote member to waiter'), + opts=self.opts, + queryset=queryset, + form=form) + return render(request, 'admin/demote_to_waiter.html', context=context) + def demote_to_waiter(self, request, queryset): for member in queryset: - #mem_as_dict = member.__dict__ - #del mem_as_dict['_state'] - #del mem_as_dict['id'] waiter = MemberWaitingList(prename=member.prename, lastname=member.lastname, email=member.email, birth_date=member.birth_date, + gender=member.gender, comments=member.comments, confirmed_mail=member.confirmed_mail, confirm_mail_key=member.confirm_mail_key) waiter.save() member.delete() messages.success(request, _("Successfully demoted %(name)s to waiter.") % {'name': waiter.name}) - demote_to_waiter.short_description = _('Demote selected registrations to waiters.') def response_change(self, request, member): if "_confirm" in request.POST: @@ -496,7 +568,7 @@ class WaiterInviteForm(forms.Form): label=_('Group')) -class InvitationToGroupAdmin(CommonAdminInlineMixin, admin.TabularInline): +class InvitationToGroupAdmin(admin.TabularInline): model = InvitationToGroup fields = ['group', 'date', 'status'] readonly_fields = ['group', 'date', 'status'] @@ -539,6 +611,10 @@ class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin): messages.error(request, _("An error occurred while trying to invite said members. Please try again.")) return HttpResponseRedirect(request.get_full_path()) + if not group.contact_email: + messages.error(request, + _('The selected group does not have a contact email. Please first set a contact email and then try again.')) + return HttpResponseRedirect(request.get_full_path()) for waiter in queryset: waiter.invited_for_group = group @@ -596,6 +672,11 @@ class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin): _("An error occurred while trying to invite said members. Please try again.")) return HttpResponseRedirect(request.get_full_path()) + if not group.contact_email: + messages.error(request, + _('The selected group does not have a contact email. Please first set a contact email and then try again.')) + return HttpResponseRedirect(request.get_full_path()) + waiter.invited_for_group = group waiter.save() waiter.invite_to_group(group) @@ -634,7 +715,7 @@ class GroupAdminForm(forms.ModelForm): class GroupAdmin(CommonAdminMixin, admin.ModelAdmin): - fields = ['name', 'description', 'year_from', 'year_to', 'leiters', 'show_website', + fields = ['name', 'description', 'year_from', 'year_to', 'leiters', 'contact_email', 'show_website', 'weekday', ('start_time', 'end_time')] form = GroupAdminForm list_display = ('name', 'year_from', 'year_to') @@ -681,6 +762,7 @@ class BillOnExcursionInline(CommonAdminInlineMixin, admin.TabularInline): class StatementOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline): model = Statement extra = 1 + description = _('Please list here all expenses in relation with this excursion and upload relevant bills. These have to be permanently stored for the application of LJP contributions. The short descriptions are used in the seminar report cost overview (possible descriptions are e.g. food, material, etc.).') sortable_options = [] fields = ['night_cost'] inlines = [BillOnExcursionInline] @@ -698,6 +780,7 @@ class InterventionOnLJPInline(CommonAdminInlineMixin, admin.TabularInline): class LJPOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline): model = LJPProposal extra = 1 + description = _('Here you can work on a seminar report for applying for financial contributions from Landesjugendplan (LJP). More information on creating a seminar report can be found in the wiki. The seminar report or only a participant list and cost overview can be consequently downloaded.') sortable_options = [] inlines = [InterventionOnLJPInline] @@ -705,6 +788,7 @@ class LJPOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline): class MemberOnListInline(CommonAdminInlineMixin, GenericTabularInline): model = NewMemberOnList extra = 0 + description = _('Please list all participants (also youth leaders) of this excursion. Here you can still make changes just before departure and hence generate the latest participant list for crisis intervention at all times.') formfield_overrides = { TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} } @@ -767,6 +851,9 @@ 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 FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): @@ -776,6 +863,13 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): search_fields = ('name',) ordering = ('-date',) view_on_site = False + fieldsets = ( + (None, { + 'fields': ('name', 'place', 'destination', 'date', 'end', 'description', 'groups', 'jugendleiter', + '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.).') + }), + ) #formfield_overrides = { # ManyToManyField: {'widget': forms.CheckboxSelectMultiple}, # ForeignKey: {'widget': apply_select2(forms.Select)} @@ -839,12 +933,21 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): messages.error(request, _('Please select a mode.')) return self.render_seminar_report_options(request, memberlist, form) mode = form.cleaned_data['mode'] + prepend_v32 = form.cleaned_data['prepend_v32'] if mode == 'full' and not hasattr(memberlist, 'ljpproposal'): messages.error(request, _('Full mode is only available, if the seminar report section is filled out.')) return self.render_seminar_report_options(request, memberlist, form) - context = dict(memberlist=memberlist, settings=settings, mode=mode) title = memberlist.ljpproposal.title if hasattr(memberlist, 'ljpproposal') else memberlist.name - return render_tex(title + '_Seminarbericht', 'members/seminar_report.tex', context) + context = dict(memberlist=memberlist, settings=settings, mode=mode) + fp = render_tex(title + '_Seminarbericht', 'members/seminar_report.tex', context, save_only=True) + if prepend_v32: + context = memberlist.v32_fields() + v32_fp = fill_pdf_form(title + "_LJP_V32", + 'members/V32-1_Themenorientierte_Bildungsmassnahmen.pdf', + context, + save_only=True) + return merge_pdfs(title + 'LJP_Antrag', [v32_fp, fp]) + return serve_pdf(fp) return self.render_seminar_report_options(request, memberlist, GenerateSeminarReportForm()) seminar_report.short_description = _('Generate seminar report') @@ -860,6 +963,25 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): return fill_pdf_form(title + "_SJR_Antrag", 'members/sjr_template.pdf', context, attachments) sjr_application.short_description = _('Generate SJR application') + def finance_overview(self, request, memberlist): + if not memberlist.statement: + messages.error(request, _("No statement found. Please add a statement and then retry.")) + if "apply" in request.POST: + memberlist.statement.submit(get_member(request)) + messages.success(request, + _("Successfully submited statement. The finance department will notify you as soon as possible.")) + 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=_('Finance overview'), + opts=self.opts, + memberlist=memberlist, + object=memberlist, + participant_count=memberlist.participant_count, + ljp_contributions=memberlist.potential_ljp_contributions, + total_relative_costs=memberlist.total_relative_costs, + **memberlist.statement.template_context()) + return render(request, 'admin/freizeit_finance_overview.html', context=context) + def get_urls(self): urls = super().get_urls() @@ -888,6 +1010,8 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin): return self.notes_list(request, Freizeit.objects.get(pk=object_id)) if "crisis_intervention_list" in request.POST: return self.crisis_intervention_list(request, Freizeit.objects.get(pk=object_id)) + if "finance_overview" in request.POST: + return self.finance_overview(request, Freizeit.objects.get(pk=object_id)) return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(object_id,))) diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po index 5e81b32..62ef53b 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: 2024-11-24 02:47+0100\n" +"POT-Creation-Date: 2024-12-03 00:26+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: members/admin.py:126 members/models.py:371 +#: members/admin.py:126 members/models.py:391 msgid "Registration complete" msgstr "Anmeldung vollständig" @@ -34,803 +34,912 @@ msgstr "Nein" msgid "All" msgstr "Alle" -#: members/admin.py:183 members/admin.py:395 +#: members/admin.py:184 members/admin.py:413 msgid "Contact information" msgstr "Kontaktinformationen" -#: members/admin.py:188 members/admin.py:400 +#: members/admin.py:189 members/admin.py:418 msgid "Skills" msgstr "Fähigkeiten" -#: members/admin.py:193 members/admin.py:405 +#: members/admin.py:194 members/admin.py:423 msgid "Others" msgstr "Sonstiges" -#: members/admin.py:199 members/admin.py:410 +#: members/admin.py:200 members/admin.py:428 msgid "Organizational" msgstr "Organisatorisches" -#: members/admin.py:280 +#: members/admin.py:281 msgid "Compose new mail to selected members" -msgstr "Neue Nachricht an ausgewählte Teilnehmer verfassen" +msgstr "Neue Nachricht an ausgewählte Teilnehmer*innen verfassen" -#: members/admin.py:286 +#: members/admin.py:287 msgid "Echo required" msgstr "Rückmeldung erforderlich" -#: members/admin.py:288 +#: members/admin.py:289 msgid "Successfully requested echo from selected members." msgstr "" -"Rückmeldungsaufforderung erfolgreich an ausgewählte Teilnehmer verschickt." +"Rückmeldungsaufforderung erfolgreich an ausgewählte Teilnehmer*innen " +"verschickt." -#: members/admin.py:289 +#: members/admin.py:290 msgid "Request echo from selected members" -msgstr "Rückmeldungsaufforderung an ausgewählte Teilnehmer verschicken" +msgstr "Rückmeldungsaufforderung an ausgewählte Teilnehmer*innen verschicken" + +#: members/admin.py:299 +#, python-format +msgid "%(name)s does not have a DAV360 email address or is already registered." +msgstr "%(name)s hat keine DAV360 E-Mail Adresse oder ist bereits registriert." -#: members/admin.py:295 +#: members/admin.py:301 #, python-format msgid "Successfully invited %(name)s as user." msgstr "Erfolgreich %(name)s aufgefordert Zugangsdaten zu wählen." -#: members/admin.py:297 +#: members/admin.py:303 msgid "Successfully invited selected members to join as users." msgstr "" -"Erfolgreich ausgewählte Teilnehmer:innen aufgefordert Zugangsdaten zu wählen." +"Erfolgreich ausgewählte Teilnehmer*innen aufgefordert Zugangsdaten zu wählen." + +#: members/admin.py:305 +msgid "Some members have been invited, others could not be invited." +msgstr "" +"Manche Teilnehmer*innen wurden eingeladen, andere konnten nicht eingeladen " +"werden." -#: members/admin.py:304 members/admin.py:321 +#: members/admin.py:312 members/admin.py:329 msgid "Permission denied." msgstr "Fehlende Berechtigungen." -#: members/admin.py:311 members/admin.py:340 +#: members/admin.py:319 members/admin.py:353 #: members/templates/admin/invite_as_user.html:21 msgid "Invite as user" msgstr "Kompass Zugangsdaten wählen lassen" -#: members/admin.py:316 +#: members/admin.py:324 msgid "Invite selected members to join Kompass as users." -msgstr "Ausgewählte Teilnehmer:innen Kompass Zugangsdaten wählen lassen." +msgstr "Ausgewählte Teilnehmer*innen Kompass Zugangsdaten wählen lassen." -#: members/admin.py:327 +#: members/admin.py:335 msgid "Member not found." -msgstr "Teilnehmer:in nicht gefunden." +msgstr "Teilnehmer*in nicht gefunden." -#: members/admin.py:331 +#: members/admin.py:339 #, python-format msgid "%(name)s already has login data." msgstr "%(name)s hat schon Zugangsdaten." -#: members/admin.py:345 +#: members/admin.py:344 +#, python-format +msgid "The configured email address for %(name)s is not an internal one." +msgstr "Die für %(name)s eingestellte E-Mail Adresse ist keine DAV360 Adresse." + +#: members/admin.py:358 #, python-format msgid "%(name)s already has a pending invitation as user." msgstr "" "%(name)s hat bereits eine ausstehende Aufforderung Zugangsdaten zu wählen." -#: members/admin.py:363 +#: members/admin.py:376 msgid "activity" msgstr "Aktivität" -#: members/admin.py:373 members/models.py:53 members/models.py:1379 +#: members/admin.py:386 members/models.py:55 members/models.py:1506 msgid "Name" msgstr "Name" -#: members/admin.py:444 +#: members/admin.py:476 msgid "Successfully requested mail confirmation from selected registrations." msgstr "Aufforderung zur Bestätigung der Email Adresse versendet." -#: members/admin.py:445 +#: members/admin.py:477 msgid "Request mail confirmation from selected registrations" msgstr "Aufforderung zur Bestätigung der Email Adresse versenden" -#: members/admin.py:452 members/admin.py:486 +#: members/admin.py:484 members/admin.py:558 #, python-format msgid "Successfully confirmed %(name)s." msgstr "Registrierung von %(name)s erfolgreich bestätigt." -#: members/admin.py:456 members/admin.py:489 +#: members/admin.py:488 members/admin.py:561 #, python-format msgid "Can't confirm. %(name)s has unconfirmed email addresses." msgstr "Bestätigung nicht möglich. %(name)s hat unbestätigte Emailadressen." -#: members/admin.py:461 +#: members/admin.py:493 msgid "Successfully confirmed multiple registrations." msgstr "Erfolgreich mehrere Registrierungen bestätigt." -#: members/admin.py:463 +#: members/admin.py:495 msgid "" "Failed to confirm some registrations because of unconfirmed email addresses." msgstr "" "Einige Bestätigungen fehlgeschlagen, weil Emailadressen noch nicht bestätigt " "sind." -#: members/admin.py:464 +#: members/admin.py:496 msgid "Confirm selected registrations" msgstr "Ausgewählte Registrierungen bestätigen" -#: members/admin.py:480 +#: members/admin.py:519 +msgid "Demote selected registrations to waiters." +msgstr "Ausgewählte Registrierungen zurück auf die Warteliste setzen." + +#: members/admin.py:535 +msgid "Demote member to waiter" +msgstr "Ausgewählte Registrierung zurück auf die Warteliste setzen." + +#: members/admin.py:553 #, python-format msgid "Successfully demoted %(name)s to waiter." msgstr "%(name)s zurück auf die Warteliste gesetzt." -#: members/admin.py:481 -msgid "Demote selected registrations to waiters." -msgstr "Ausgewählte Registrierungen zurück auf die Warteliste setzen." - -#: members/admin.py:496 members/models.py:378 members/models.py:718 -#: members/models.py:1124 +#: members/admin.py:568 members/models.py:398 members/models.py:764 +#: members/models.py:1251 msgid "Group" msgstr "Gruppe" -#: members/admin.py:530 +#: members/admin.py:602 #, python-format msgid "Successfully asked %(name)s to confirm their waiting status." msgstr "Erfolgreich %(name)s aufgefordert den Wartelistenplatz zu bestätigen." -#: members/admin.py:531 +#: members/admin.py:603 msgid "Ask selected waiters to confirm their waiting status" msgstr "Wartende auffordern den Wartelistenplatz zu bestätigen" -#: members/admin.py:540 members/admin.py:596 +#: members/admin.py:612 members/admin.py:672 msgid "" "An error occurred while trying to invite said members. Please try again." msgstr "" "Beim Einladen dieser Personen ist ein Fehler aufgetreten. Bitte versuche es " "nochmal. " -#: members/admin.py:548 members/admin.py:603 +#: members/admin.py:616 members/admin.py:677 +msgid "" +"The selected group does not have a contact email. Please first set a contact " +"email and then try again." +msgstr "" +"Die ausgewählte Gruppe hat keine Kontakt E-Mail Adresse. Bitte stelle eine " +"Kontakt E-Mail Adresse ein und versuche es erneut." + +#: members/admin.py:624 members/admin.py:684 #, python-format msgid "Successfully invited %(name)s to %(group)s." msgstr "Erfolgreich %(name)s zu Gruppe %(group)s eingeladen." -#: members/admin.py:552 members/admin.py:609 +#: members/admin.py:628 members/admin.py:690 msgid "Select group for invitation" msgstr "Wähle Gruppe für Einladung aus" -#: members/admin.py:559 +#: members/admin.py:635 msgid "Offer waiter a place in a group." msgstr "Personen auf der Warteliste einen Gruppenplatz anbieten." -#: members/admin.py:651 +#: members/admin.py:733 msgid "Difficulty" msgstr "Schwierigkeit" -#: members/admin.py:654 +#: members/admin.py:736 msgid "Tour type" msgstr "Art der Tour" -#: members/admin.py:657 members/models.py:934 +#: members/admin.py:739 members/models.py:982 msgid "Means of transportation" msgstr "Verkehrsmittel" -#: members/admin.py:752 +#: members/admin.py:765 +msgid "" +"Please list here all expenses in relation with this excursion and upload " +"relevant bills. These have to be permanently stored for the application of " +"LJP contributions. The short descriptions are used in the seminar report " +"cost overview (possible descriptions are e.g. food, material, etc.)." +msgstr "" +"Gib hier bitte alle deine Ausgaben in Zusammenhang mit der Ausfahrt an und " +"lade entsprechende Belege/Quittungen hoch. Diese müssen für die Beantragung " +"von LJP-Zuschüssen langfristig aufbewahrt werden. Die Kurzbeschreibung der " +"einzelnen Posten wird dabei auf der LJP-Kostenübersicht angezeigt (sinnvoll " +"wären z.B. Anreise, Verpflegung, Material etc.)." + +#: members/admin.py:783 +msgid "" +"Here you can work on a seminar report for applying for financial " +"contributions from Landesjugendplan (LJP). More information on creating a " +"seminar report can be found in the wiki. The seminar report or only a " +"participant list and cost overview can be consequently downloaded." +msgstr "" +"Hier kannst du an einem Seminarbericht für die Beantragung von Zuschüssen " +"des Landesjugendplans (LJP) arbeiten. Weitere Informationen zur Gestaltung " +"von Seminarberichten findest du im JL-Wiki. Den Seminarbericht oder " +"wahlweise nur TN-Liste und Kostenübersicht kannst du anschließend " +"herunterladen." + +#: members/admin.py:791 +msgid "" +"Please list all participants (also youth leaders) of this excursion. Here " +"you can still make changes just before departure and hence generate the " +"latest participant list for crisis intervention at all times." +msgstr "" +"Gib hier bitte alle Personen an, die bei der Ausfahrt dabei sind (auch JL). " +"Hier kannst du auch spontan kurz vor Abfahrt noch Änderungen machen und so " +"jederzeit die aktuelle Teilnehmer*innenliste für die Krisenintervention " +"generieren." + +#: members/admin.py:837 #, python-format msgid "You are not allowed to view all members on note list %(name)s." msgstr "" -"Du hast nicht die nötigen Rechte um alle Teilnehmer:innen der Notizliste " +"Du hast nicht die nötigen Rechte um alle Teilnehmer*innen der Notizliste " "%(name)s anzusehen." -#: members/admin.py:762 +#: members/admin.py:847 msgid "Generate PDF summary" msgstr "Übersicht erstellen" -#: members/admin.py:766 +#: members/admin.py:851 msgid "Full report" msgstr "Vollständiger Seminarbericht" -#: members/admin.py:767 +#: members/admin.py:852 msgid "Costs and participants only" msgstr "Nur Kosten und Teilnehmende" -#: members/admin.py:768 +#: members/admin.py:853 msgid "Mode" msgstr "Modus" -#: members/admin.py:805 +#: members/admin.py:854 +msgid "Prepend V32" +msgstr "V32 Formblatt einfügen" + +#: members/admin.py:870 +msgid "" +"General information on your excursion. These are partly relevant for the " +"amount of financial compensation (means of transport, travel distance, etc.)." +msgstr "" +"Hier kannst du allgemein Angaben zu deiner Ausfahrt machen. Diese sind " +"teilweise relevant für die Zuschüsse aus dem Jugendetat (Verkehrsmittel, " +"Fahrstrecke in km)." + +#: members/admin.py:900 #, python-format msgid "You are not allowed to view all members on excursion %(name)s." msgstr "" -"Du hast nicht die nötigen Rechte um alle Teilnehmer:innen der Ausfahrt " +"Du hast nicht die nötigen Rechte um alle Teilnehmer*innen der Ausfahrt " "%(name)s anzusehen." -#: members/admin.py:813 +#: members/admin.py:908 msgid "Generate crisis intervention list" msgstr "Kriseninterventionsliste erstellen" -#: members/admin.py:821 +#: members/admin.py:916 msgid "Generate overview" msgstr "Hinweise für Jugendleiter erstellen" -#: members/admin.py:825 members/admin.py:848 +#: members/admin.py:920 members/admin.py:952 #: members/templates/admin/generate_seminar_report.html:21 msgid "Generate seminar report" -msgstr "Seminarbericht erstellen" +msgstr "Landesjugendplan Antrag erstellen" -#: members/admin.py:838 +#: members/admin.py:933 msgid "Please select a mode." msgstr "Bitte wähle einen Modus aus." -#: members/admin.py:842 +#: members/admin.py:938 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:860 +#: members/admin.py:964 msgid "Generate SJR application" msgstr "SJR Antrag erstellen" +#: members/admin.py:968 +msgid "No statement found. Please add a statement and then retry." +msgstr "" +"Keine Abrechnung angelegt. Bitte lege eine Abrechnung and und versuche es " +"erneut." + +#: members/admin.py:972 +msgid "" +"Successfully submited statement. The finance department will notify you as " +"soon as possible." +msgstr "" +"Abrechnung erfolgreich eingericht. Die Finanzabteilung wird sich bei dir so " +"schnell wie möglich melden." + +#: members/admin.py:975 +#: members/templates/admin/freizeit_finance_overview.html:21 +msgid "Finance overview" +msgstr "Kostenübersicht" + #: members/apps.py:7 msgid "member administration" msgstr "Meine Jugendgruppe" -#: members/models.py:39 +#: members/models.py:41 msgid "Monday" msgstr "Montag" -#: members/models.py:40 +#: members/models.py:42 msgid "Tuesday" msgstr "Dienstag" -#: members/models.py:41 +#: members/models.py:43 msgid "Wednesday" msgstr "Mittwoch" -#: members/models.py:42 +#: members/models.py:44 msgid "Thursday" msgstr "Donnerstag" -#: members/models.py:43 +#: members/models.py:45 msgid "Friday" msgstr "Freitag" -#: members/models.py:44 +#: members/models.py:46 msgid "Saturday" msgstr "Samstag" -#: members/models.py:45 +#: members/models.py:47 msgid "Sunday" msgstr "Sonntag" -#: members/models.py:54 members/models.py:920 +#: members/models.py:56 members/models.py:968 msgid "Description" msgstr "Beschreibung" -#: members/models.py:60 members/models.py:912 +#: members/models.py:62 members/models.py:960 #: members/templates/members/change_member.html:18 msgid "Activity" msgstr "Aktivität" -#: members/models.py:61 +#: members/models.py:63 msgid "Activities" msgstr "Aktivitäten" -#: members/models.py:69 +#: members/models.py:71 msgid "name" msgstr "Name" -#: members/models.py:70 +#: members/models.py:72 msgid "description" msgstr "Beschreibung" -#: members/models.py:71 +#: members/models.py:73 msgid "show on website" msgstr "Auf der Webseite anzeigen" -#: members/models.py:72 +#: members/models.py:74 msgid "lowest year" msgstr "Ab Jahrgang" -#: members/models.py:73 +#: members/models.py:75 msgid "highest year" msgstr "Bis Jahrgang" -#: members/models.py:74 +#: members/models.py:76 msgid "youth leaders" msgstr "Jugendleiter" -#: members/models.py:77 members/models.py:1206 +#: members/models.py:78 +msgid "week day" +msgstr "Wochentag" + +#: members/models.py:79 members/models.py:1333 msgid "Starting time" msgstr "Zeitpunkt" -#: members/models.py:78 +#: members/models.py:80 msgid "Ending time" msgstr "Endzeitpunkt" -#: members/models.py:85 members/models.py:247 +#: members/models.py:82 +msgid "Contact email" +msgstr "Kontakt Email" + +#: members/models.py:92 members/models.py:259 msgid "group" msgstr "Gruppe" -#: members/models.py:86 +#: members/models.py:93 msgid "groups" msgstr "Gruppen" -#: members/models.py:98 +#: members/models.py:109 msgid "prename" msgstr "Vorname" -#: members/models.py:99 +#: members/models.py:110 msgid "last name" msgstr "Nachname" -#: members/models.py:102 +#: members/models.py:113 msgid "Email confirmed" msgstr "Emailadresse bestätigt" -#: members/models.py:137 +#: members/models.py:150 msgid "Email confirmation needed" msgstr "Email Bestätigung erforderlich" -#: members/models.py:177 members/models.py:221 +#: members/models.py:190 members/models.py:233 msgid "phone number" msgstr "Telefonnummer (mobil)" -#: members/models.py:187 +#: members/models.py:200 msgid "birth date" msgstr "Geburtsdatum" -#: members/models.py:193 +#: members/models.py:205 msgid "Gender" msgstr "Gender" -#: members/models.py:194 +#: members/models.py:206 msgid "comments" msgstr "Kommentare" -#: members/models.py:218 +#: members/models.py:230 msgid "Alternative email confirmed" msgstr "Alternative E-Mail Adresse bestätigt" -#: members/models.py:222 +#: members/models.py:234 msgid "street and house number" msgstr "Straße und Hausnummer" -#: members/models.py:223 +#: members/models.py:235 msgid "Postcode" msgstr "PLZ" -#: members/models.py:225 +#: members/models.py:237 msgid "town" msgstr "Stadt" -#: members/models.py:226 +#: members/models.py:238 msgid "Address extra" msgstr "Adress-Zusatz" -#: members/models.py:227 +#: members/models.py:239 msgid "Country" msgstr "Land" -#: members/models.py:229 +#: members/models.py:241 msgid "Good conduct certificate presented on" msgstr "Führungszeugnis vorgelegt am" -#: members/models.py:230 +#: members/models.py:242 msgid "Joined on" msgstr "Eintritt" -#: members/models.py:231 +#: members/models.py:243 msgid "Left on" msgstr "Austritt" -#: members/models.py:232 +#: members/models.py:244 msgid "Has key" msgstr "Hat Jugendraumschlüssel" -#: members/models.py:233 +#: members/models.py:245 msgid "Has a free ticket for the climbing gym" msgstr "Hat Freikarte für Kletterhalle" -#: members/models.py:234 +#: members/models.py:246 msgid "DAV badge number" msgstr "DAV Mitgliedsnummer" -#: members/models.py:235 +#: members/models.py:247 msgid "Knows how to swim" msgstr "Kann schwimmen" -#: members/models.py:236 +#: members/models.py:248 msgid "Climbing badge" msgstr "Kletterschein" -#: members/models.py:237 +#: members/models.py:249 msgid "Alpine experience" msgstr "Alpine Erfahrung" -#: members/models.py:238 +#: members/models.py:250 msgid "Allergies" msgstr "Allergieen" -#: members/models.py:239 +#: members/models.py:251 msgid "Medication" msgstr "Medikamente" -#: members/models.py:240 +#: members/models.py:252 msgid "Tetanus vaccination" msgstr "Tetanusimpfung" -#: members/models.py:241 +#: members/models.py:253 msgid "Photos may be taken" msgstr "Fotoerlaubnis" -#: members/models.py:242 +#: members/models.py:254 msgid "Legal guardians" msgstr "Erziehungsberechtigte" -#: members/models.py:244 +#: members/models.py:256 msgid "May cancel a group appointment independently" msgstr "Darf sich allein von der Gruppenstunde abmelden" -#: members/models.py:251 +#: members/models.py:263 msgid "receives newsletter" msgstr "Erhält den Newsletter" -#: members/models.py:255 +#: members/models.py:267 msgid "created" msgstr "erstellt" -#: members/models.py:256 +#: members/models.py:268 msgid "Active" msgstr "Aktiv" -#: members/models.py:257 +#: members/models.py:269 msgid "registration form" msgstr "Anmeldeformular" -#: members/models.py:265 +#: members/models.py:277 msgid "image" msgstr "Bild" -#: members/models.py:274 +#: members/models.py:286 msgid "Echoed" msgstr "Rückgemeldet" -#: members/models.py:275 +#: members/models.py:287 msgid "Confirmed" msgstr "Bestätigt" -#: members/models.py:277 +#: members/models.py:289 msgid "Login data" msgstr "Zugangsdaten" -#: members/models.py:307 +#: members/models.py:319 msgid "Good conduct certificate valid" msgstr "Führungszeugnis gültig" -#: members/models.py:381 +#: members/models.py:401 msgid "member" -msgstr "Teilnehmer" +msgstr "Teilnehmer*in" -#: members/models.py:382 +#: members/models.py:402 msgid "members" -msgstr "Teilnehmer" +msgstr "Teilnehmer*innen" -#: members/models.py:455 +#: members/models.py:471 #, python-format msgid "New unconfirmed registration for group %(group)s" msgstr "Neue unbestätigte Registrierung für Gruppe %(group)s" -#: members/models.py:666 +#: members/models.py:697 msgid "Set login data for Kompass" msgstr "Zugangsdaten für Kompass wählen" -#: members/models.py:675 members/models.py:868 members/models.py:879 -#: members/models.py:1155 members/models.py:1162 +#: members/models.py:715 members/models.py:916 members/models.py:927 +#: members/models.py:1282 members/models.py:1289 msgid "Member" -msgstr "Teilnehmer" +msgstr "Teilnehmer*in" -#: members/models.py:681 +#: members/models.py:722 msgid "Emergency contact" msgstr "Notfallkontakt" -#: members/models.py:682 +#: members/models.py:723 msgid "Emergency contacts" msgstr "Notfallkontakte" -#: members/models.py:702 +#: members/models.py:743 msgid "Unconfirmed registration" msgstr "Unbestätigte Registrierung" -#: members/models.py:703 +#: members/models.py:744 msgid "Unconfirmed registrations" msgstr "Unbestätigte Registrierungen" -#: members/models.py:717 members/models.py:762 +#: members/models.py:763 members/models.py:808 msgid "Waiter" msgstr "Wartende Person" -#: members/models.py:719 +#: members/models.py:765 msgid "Invitation date" msgstr "Einladungsdatum" -#: members/models.py:720 members/templates/members/reject_success.html:6 +#: members/models.py:766 members/templates/members/reject_success.html:6 #: members/templates/members/reject_success.html:11 msgid "Invitation rejected" msgstr "Einladung abgelehnt" -#: members/models.py:724 +#: members/models.py:770 msgid "Invitation to group" msgstr "Gruppeneinladung" -#: members/models.py:725 +#: members/models.py:771 msgid "Invitations to groups" msgstr "Gruppeneinladungen" -#: members/models.py:732 +#: members/models.py:778 msgid "Rejected" msgstr "Abgelehnt" -#: members/models.py:734 +#: members/models.py:780 msgid "Expired" msgstr "Abgelaufen" -#: members/models.py:736 +#: members/models.py:782 msgid "Undecided" msgstr "Ausstehend" -#: members/models.py:737 +#: members/models.py:783 msgid "Status" msgstr "Status" -#: members/models.py:748 +#: members/models.py:794 msgid "Do you want to tell us something else?" msgstr "Möchtest du uns noch etwas mitteilen?" -#: members/models.py:749 +#: members/models.py:795 msgid "application date" msgstr "Bewerbungsdatum" -#: members/models.py:751 +#: members/models.py:797 msgid "Last wait confirmation" msgstr "Letzte Wartebestätigung" -#: members/models.py:755 +#: members/models.py:801 msgid "Last reminder" msgstr "Letzte Erinnerung" -#: members/models.py:756 +#: members/models.py:802 msgid "Missed reminders" msgstr "Verpasste Erinnerungen" -#: members/models.py:763 +#: members/models.py:809 msgid "Waiters" msgstr "Warteliste" -#: members/models.py:787 +#: members/models.py:833 msgid "Waiting status confirmed" msgstr "Wartelistenplatz bestätigt" -#: members/models.py:794 +#: members/models.py:840 msgid "Waiting confirmation needed" msgstr "Wartelistenplatzbestätigung erforderlich" -#: members/models.py:847 +#: members/models.py:895 msgid "Invitation to trial group meeting" msgstr "Einladung zu Schnupperstunde" -#: members/models.py:859 +#: members/models.py:907 msgid "Unregistered from waiting list" msgstr "Von der Warteliste abgemeldet" -#: members/models.py:873 +#: members/models.py:921 msgid "Comment" msgstr "Kommentar" -#: members/models.py:880 members/models.py:1163 +#: members/models.py:928 members/models.py:1290 msgid "Members" -msgstr "Teilnehmer" +msgstr "Teilnehmer*innen" -#: members/models.py:914 +#: members/models.py:962 msgid "Place" msgstr "Stützpunkt / Ort" -#: members/models.py:915 +#: members/models.py:963 msgid "Destination (optional)" msgstr "ggf. Ziel" -#: members/models.py:917 +#: members/models.py:965 msgid "e.g. a peak" msgstr "z.B. ein Gipfel" -#: members/models.py:918 +#: members/models.py:966 msgid "Begin" msgstr "Anfang" -#: members/models.py:919 +#: members/models.py:967 msgid "End (optional)" msgstr "Ende" -#: members/models.py:922 +#: members/models.py:970 msgid "Groups" msgstr "Gruppen" -#: members/models.py:935 +#: members/models.py:983 msgid "Kilometers traveled" msgstr "Fahrstrecke in Kilometer" -#: members/models.py:938 +#: members/models.py:986 msgid "Categories" msgstr "Kategorien" -#: members/models.py:939 +#: members/models.py:987 msgid "easy" msgstr "leicht" -#: members/models.py:939 +#: members/models.py:987 msgid "medium" msgstr "mittel" -#: members/models.py:939 +#: members/models.py:987 msgid "hard" msgstr "schwer" -#: members/models.py:949 members/models.py:1186 +#: members/models.py:997 members/models.py:1313 +#: members/templates/admin/freizeit_finance_overview.html:26 msgid "Excursion" msgstr "Ausfahrt" -#: members/models.py:950 +#: members/models.py:998 msgid "Excursions" msgstr "Ausfahrten" -#: members/models.py:1101 members/models.py:1177 members/models.py:1393 +#: members/models.py:1228 members/models.py:1304 members/models.py:1520 msgid "Title" msgstr "Titel" -#: members/models.py:1102 members/models.py:1120 members/models.py:1394 +#: members/models.py:1229 members/models.py:1247 members/models.py:1521 msgid "Date" msgstr "Datum" -#: members/models.py:1121 +#: members/models.py:1248 msgid "Location" msgstr "Ort" -#: members/models.py:1122 +#: members/models.py:1249 msgid "Topic" msgstr "Thema" -#: members/models.py:1146 +#: members/models.py:1273 msgid "Jugendleiter" msgstr "Jugendleiter" -#: members/models.py:1149 +#: members/models.py:1276 msgid "Klettertreff" msgstr "Klettertreff" -#: members/models.py:1150 +#: members/models.py:1277 msgid "Klettertreffs" msgstr "Klettertreffs" -#: members/models.py:1168 +#: members/models.py:1295 msgid "Password" msgstr "Passwort" -#: members/models.py:1171 +#: members/models.py:1298 msgid "registration password" msgstr "Registrierungspassort" -#: members/models.py:1172 +#: members/models.py:1299 msgid "registration passwords" msgstr "Registrierungspasswörter" -#: members/models.py:1179 +#: members/models.py:1306 msgid "Alpinistic goals" msgstr "Alpintechnische Ziele" -#: members/models.py:1180 +#: members/models.py:1307 msgid "Pedagogic goals" msgstr "Pädagogische Ziele" -#: members/models.py:1181 +#: members/models.py:1308 msgid "Content and methods" msgstr "Inhalte und Methoden" -#: members/models.py:1182 +#: members/models.py:1309 msgid "Evaluation" msgstr "Wertung" -#: members/models.py:1183 +#: members/models.py:1310 msgid "Experiences and possible improvements" msgstr "Erfahrungen und Verbesserungsvorschläge" -#: members/models.py:1192 members/models.py:1213 +#: members/models.py:1319 members/models.py:1340 msgid "LJP Proposal" msgstr "Seminarbericht" -#: members/models.py:1193 +#: members/models.py:1320 msgid "LJP Proposals" msgstr "Seminarberichte" -#: members/models.py:1207 +#: members/models.py:1334 msgid "Duration in hours" msgstr "Dauer in Stunden" -#: members/models.py:1210 +#: members/models.py:1337 msgid "Activity and method" msgstr "Art der Aktion inkl. Methode" -#: members/models.py:1218 +#: members/models.py:1345 msgid "Intervention" msgstr "Aktion" -#: members/models.py:1219 +#: members/models.py:1346 msgid "Interventions" msgstr "Aktionen" -#: members/models.py:1321 members/models.py:1351 +#: members/models.py:1448 members/models.py:1478 msgid "May list members" -msgstr "Darf folgende Teilnehmer:innen listen" +msgstr "Darf folgende Teilnehmer*innen listen" -#: members/models.py:1323 members/models.py:1353 +#: members/models.py:1450 members/models.py:1480 msgid "May view members" -msgstr "Darf folgende Teilnehmer:innen anzeigen" +msgstr "Darf folgende Teilnehmer*innen anzeigen" -#: members/models.py:1325 members/models.py:1355 +#: members/models.py:1452 members/models.py:1482 msgid "May change members" -msgstr "Darf folgende Teilnehmer:innen ändern" +msgstr "Darf folgende Teilnehmer*innen ändern" -#: members/models.py:1327 members/models.py:1357 +#: members/models.py:1454 members/models.py:1484 msgid "May delete members" -msgstr "Darf folgende Teilnehmer:innen löschen" +msgstr "Darf folgende Teilnehmer*innen löschen" -#: members/models.py:1331 members/models.py:1361 +#: members/models.py:1458 members/models.py:1488 msgid "May list members of groups" -msgstr "Darf Teilnehmer:innen folgender Gruppen listen" +msgstr "Darf Teilnehmer*innen folgender Gruppen listen" -#: members/models.py:1333 members/models.py:1363 +#: members/models.py:1460 members/models.py:1490 msgid "May view members of groups" -msgstr "Darf Teilnehmer:innen folgender Gruppen anzeigen" +msgstr "Darf Teilnehmer*innen folgender Gruppen anzeigen" -#: members/models.py:1335 members/models.py:1365 +#: members/models.py:1462 members/models.py:1492 msgid "May change members of groups" -msgstr "Darf Teilnehmer:innen folgender Gruppen ändern" +msgstr "Darf Teilnehmer*innen folgender Gruppen ändern" -#: members/models.py:1337 members/models.py:1367 +#: members/models.py:1464 members/models.py:1494 msgid "May delete members of groups" -msgstr "Darf Teilnehmer:innen folgender Gruppen löschen" +msgstr "Darf Teilnehmer*innen folgender Gruppen löschen" -#: members/models.py:1340 members/models.py:1341 members/models.py:1344 +#: members/models.py:1467 members/models.py:1468 members/models.py:1471 msgid "Permissions" msgstr "Berechtigungen" -#: members/models.py:1370 members/models.py:1371 members/models.py:1374 +#: members/models.py:1497 members/models.py:1498 members/models.py:1501 msgid "Group permissions" msgstr "Gruppenberechtigungen" -#: members/models.py:1380 +#: members/models.py:1507 msgid "Permission needed" msgstr "Freigabe erforderlich" -#: members/models.py:1383 +#: members/models.py:1510 msgid "Training category" msgstr "Fortbildungstyp" -#: members/models.py:1384 +#: members/models.py:1511 msgid "Training categories" msgstr "Fortbildungstypen" -#: members/models.py:1395 +#: members/models.py:1522 msgid "Category" msgstr "Kategorien" -#: members/models.py:1396 +#: members/models.py:1523 msgid "Comments" msgstr "Kommentar" -#: members/models.py:1397 +#: members/models.py:1524 msgid "Participated" msgstr "Teilgenommmen" -#: members/models.py:1398 +#: members/models.py:1525 msgid "Passed" msgstr "Bestanden" -#: members/models.py:1401 +#: members/models.py:1528 msgid "Training" msgstr "Fortbildung" -#: members/models.py:1402 +#: members/models.py:1529 msgid "Trainings" msgstr "Fortbildungen" +#: members/templates/admin/demote_to_waiter.html:17 +#: members/templates/admin/freizeit_finance_overview.html:17 #: members/templates/admin/generate_seminar_report.html:17 #: members/templates/admin/invite_as_user.html:17 #: members/templates/admin/invite_for_group.html:17 @@ -839,25 +948,242 @@ msgstr "Fortbildungen" msgid "Home" msgstr "Start" +#: members/templates/admin/demote_to_waiter.html:20 +#: members/templates/admin/demote_to_waiter.html:25 +msgid "Demote to waiter" +msgstr "Zurück auf die Warteliste setzen" + +#: members/templates/admin/demote_to_waiter.html:27 +msgid "" +"Do you want to demote the following unconfirmed registrations to waiters?" +msgstr "Möchtest du die folgenden Personen zurück auf die Warteliste setzen?" + +#: members/templates/admin/demote_to_waiter.html:45 +msgid "Demote" +msgstr "Zurück auf die Warteliste setzen" + +#: members/templates/admin/demote_to_waiter.html:46 +#: members/templates/admin/freizeit_finance_overview.html:154 +#: members/templates/admin/generate_seminar_report.html:60 +#: members/templates/admin/invite_as_user.html:37 +#: members/templates/admin/invite_for_group.html:52 +#: members/templates/admin/invite_selected_as_user.html:49 +#: members/templates/admin/invite_selected_for_group.html:53 +msgid "Cancel" +msgstr "Abbrechen" + +#: members/templates/admin/freizeit_finance_overview.html:29 +msgid "" +"\n" +"Here you see an estimate on the expected costs and contributions by the " +"association. This is not a guaranteed\n" +"cost plan!\n" +msgstr "" +"\n" +"Hier siehst du eine Schätzung der erwarteten Kosten und Zuschüsse. Dies ist " +"kein garantierter Kostenplan.\n" + +#: members/templates/admin/freizeit_finance_overview.html:34 +#: members/templates/admin/freizeit_finance_overview.html:100 +msgid "Expenses" +msgstr "Ausgaben" + +#: members/templates/admin/freizeit_finance_overview.html:35 +msgid "You listed the following expenses:" +msgstr "Du hast die folgenden Ausgaben angegeben:" + +#: members/templates/admin/freizeit_finance_overview.html:39 +msgid "Explanation" +msgstr "Erklärung" + +#: members/templates/admin/freizeit_finance_overview.html:40 +msgid "Amount" +msgstr "Betrag" + +#: members/templates/admin/freizeit_finance_overview.html:58 +#, python-format +msgid "The total expected expenses are %(total_bills_theoretic)s €." +msgstr "" +"Insgesamt belaufen sich die geschätzten Ausgaben auf " +"%(total_bills_theoretic)s €." + +#: members/templates/admin/freizeit_finance_overview.html:60 +#: members/templates/admin/freizeit_finance_overview.html:108 +msgid "Contributions by the association" +msgstr "Sektionszuschüsse" + +#: members/templates/admin/freizeit_finance_overview.html:63 +#, python-format +msgid "" +"According to the contribution guidelines,\n" +"%(staff_count)s youth leader(s) receive contributions. Each of them receives" +msgstr "" +"Gemäß den Zuschussrichtlinien erhalten %(staff_count)s Jugendleiter*innen " +"Zuschüsse. Jeweils sind das" + +#: members/templates/admin/freizeit_finance_overview.html:69 +#, python-format +msgid "" +"%(nights)s nights for %(price_per_night)s€ per night making a total of " +"%(nights_per_yl)s€." +msgstr "" +"%(nights)s Nächte zum Preis von %(price_per_night)s€ pro Nacht. Das ergibt " +"eine Gesamtsumme von %(nights_per_yl)s€." + +#: members/templates/admin/freizeit_finance_overview.html:72 +#, python-format +msgid "" +"%(duration)s days for %(allowance_per_day)s€ per day making a total of " +"%(allowance_per_yl)s€." +msgstr "" +"%(duration)s Tage für %(allowance_per_day)s€ pro Tag. Das ergibt eine " +"Gesamtsumme von %(allowance_per_yl)s€." + +#: members/templates/admin/freizeit_finance_overview.html:75 +#, python-format +msgid "" +"%(kilometers_traveled)s km by %(means_of_transport)s (%(euro_per_km)s € / " +"km) making a total of %(transportation_per_yl)s€." +msgstr "" +"%(kilometers_traveled)s km mit %(means_of_transport)s (%(euro_per_km)s€ / " +"km). Das ergibt eine Gesamtsumme von %(transportation_per_yl)s€." + +#: members/templates/admin/freizeit_finance_overview.html:80 +#, python-format +msgid "" +"In total these are contributions of %(total_per_yl)s€ times %(staff_count)s, " +"giving %(total_staff)s€." +msgstr "" +"Insgesamt sind das Kosten von %(total_per_yl)s€ mal %(staff_count)s, " +"insgesamt also %(total_staff)s€." + +#: members/templates/admin/freizeit_finance_overview.html:83 +msgid "LJP contributions" +msgstr "LJP Zuschüsse" + +#: members/templates/admin/freizeit_finance_overview.html:86 +#, python-format +msgid "" +"By submitting a seminar report, you may apply for LJP contributions. In this " +"case,\n" +"you may obtain up to 25€ times %(duration)s days for %(participant_count)s " +"participants but only up to\n" +"90%% of the total costs. This results in a total of %(ljp_contributions)s€." +msgstr "" +"Indem du einen Seminarbericht anfertigst, kannst du Landesjugendplan (LJP) " +"Zuschüsse beantragen. In diesem Fall kannst du bis zu 25€ mal %(duration)s " +"Tage für %(participant_count)s Teilnehmende, aber nicht mehr als 90%% der " +"Gesamtausgaben erhalten. Das resultiert in einem Gesamtzuschuss von " +"%(ljp_contributions)s€." + +#: members/templates/admin/freizeit_finance_overview.html:91 +msgid "Summary" +msgstr "Zusammenfassung" + +#: members/templates/admin/freizeit_finance_overview.html:94 +msgid "This is the estimated cost and contribution summary:" +msgstr "Das ist die geschätzte Kosten- und Zuschussübersicht." + +#: members/templates/admin/freizeit_finance_overview.html:116 +msgid "Potential LJP contributions" +msgstr "Mögliche LJP Zuschüsse" + +#: members/templates/admin/freizeit_finance_overview.html:124 +msgid "Remaining costs" +msgstr "Verbleibende Kosten" + +#: members/templates/admin/freizeit_finance_overview.html:133 +msgid "" +"Positive remaining costs indicate that the estimated costs exceed the " +"estimated contributions, while negative\n" +"remaining costs indicate that the estimated contributions exceed the " +"estimated costs." +msgstr "" +"Positive verbleibende Kosten bedeuten, dass die geschätzten Kosten die " +"geschätzten Zuschüsse übersteigen, während negative Kosten\n" +" bedeuten, dass die geschätzten Zuschüsse die geschätzten Kosten übersteigen." + +#: members/templates/admin/freizeit_finance_overview.html:137 +msgid "" +"Note that this cost calculation expects you to apply for LJP contributions. " +"On the\n" +"excursions main page, you can generate a template for a seminar report." +msgstr "" +"Beachte dass diese Kostenkalkulation davon ausgeht, dass du LJP Zuschüsse " +"beantragst. Auf der Hauptseite dieser Ausfahrt kannst du dir eine Vorlage " +"und alle Formblätter für einen solchen Antrag erstellen lassen." + +#: members/templates/admin/freizeit_finance_overview.html:142 +msgid "Submit statement" +msgstr "Abrechnung einreichen" + +#: members/templates/admin/freizeit_finance_overview.html:144 +msgid "" +"Did you already complete this excursion? If yes, please check if all listed " +"expenses are correct\n" +"and then submit the statement for processing by the finance department. If " +"you proceed,\n" +"no further changes to the statement are possible." +msgstr "" +"Hat die Ausfahrt bereits stattgefunden? Wenn ja, prüfe bitte ob alle " +"aufgelisteten Kosten korrekt sind und reiche deine Abrechnung dann beim " +"Finanzreferat ein. Wenn du fortschreitest sind keine weiteren Änderungen an " +"der Abrechnung mehr möglich." + +#: members/templates/admin/freizeit_finance_overview.html:153 +msgid "Submit" +msgstr "Einreichen" + +#: members/templates/admin/freizeit_finance_overview.html:158 +msgid "Statement submitted" +msgstr "Abrechnung eingereicht" + +#: members/templates/admin/freizeit_finance_overview.html:160 +msgid "" +"The statement for this excursion was already submitted. The finance " +"department is currently processing your\n" +"data and you will receive a response shortly." +msgstr "" +"Die Abrechnung für diese Ausfahrt wurde bereits eingereicht. Das " +"Finanzreferat bearbeitet deine Abrechnung zur Zeit und kommt " +"schnellstmöglich auf dich zurück." + +#: members/templates/admin/freizeit_finance_overview.html:163 +msgid "Back" +msgstr "Zurück" + #: members/templates/admin/generate_seminar_report.html:27 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 Seminarbericht für den Landesjugendplan erstellen. " -"Ein Bericht enthält immer einen Kopf mit den Stammdaten des Seminars." +"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. " + +#: members/templates/admin/generate_seminar_report.html:31 +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." -#: members/templates/admin/generate_seminar_report.html:32 +#: members/templates/admin/generate_seminar_report.html:36 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 detaillierte, " -"tabellierten Zeitplan dar. Dies benötigt, dass der Seminarbericht " -"in der Ausfahrt ausgefüllt ist." +"Vollständiger Bericht: Stelle Lernziele und einen detaillierten, " +"tabellierten Zeitplan dar. Dies benötigt, dass der Seminarbericht in der " +"Ausfahrt ausgefüllt ist." -#: members/templates/admin/generate_seminar_report.html:36 +#: members/templates/admin/generate_seminar_report.html:40 msgid "" "Costs and participants only: Only show a list of participants and costs. In " "this case you\n" @@ -867,22 +1193,18 @@ msgstr "" "Kosten an. In diesem Fall musst du Lernziele und einen Zeitplan manuell " "hinzufügen." -#: members/templates/admin/generate_seminar_report.html:42 -msgid "Please choose one of the listed modes." -msgstr "Bitte wähle einen der obigen Modi." +#: members/templates/admin/generate_seminar_report.html:46 +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/templates/admin/generate_seminar_report.html:53 +#: members/templates/admin/generate_seminar_report.html:59 msgid "Generate" msgstr "Erstellen" -#: members/templates/admin/generate_seminar_report.html:54 -#: members/templates/admin/invite_as_user.html:37 -#: members/templates/admin/invite_for_group.html:52 -#: members/templates/admin/invite_selected_as_user.html:49 -#: members/templates/admin/invite_selected_for_group.html:53 -msgid "Cancel" -msgstr "Abbrechen" - #: members/templates/admin/invite_as_user.html:27 #, python-format msgid "" @@ -923,11 +1245,11 @@ msgstr "Bitte wähle die Gruppe aus zu der du %(waiter)s einladen möchtest." #: members/templates/admin/invite_selected_as_user.html:20 msgid "Invite multiple members as users" -msgstr "Mehrere Teilnehmer:innen Zugangsdaten wählen lassen" +msgstr "Mehrere Teilnehmer*innen Zugangsdaten wählen lassen" #: members/templates/admin/invite_selected_as_user.html:26 msgid "You selected the following members:" -msgstr "Du hast die folgenden Teilnehmer:innen ausgewählt:" +msgstr "Du hast die folgenden Teilnehmer*innen ausgewählt:" #: members/templates/admin/invite_selected_as_user.html:38 msgid "" @@ -937,7 +1259,7 @@ msgid "" "entering one of the current\n" "active registration passwords." msgstr "" -"Möchtest du diese Teilnehmer:innen auffordern Zugangsdaten für den Kompass " +"Möchtest du diese Teilnehmer*innen auffordern Zugangsdaten für den Kompass " "zu wählen?Sie werden eine E-Mail mit einem Link erhalten, um, nach Eingabe " "eines der aktiven Registrierungspasswörter, Benutzername und Passwort zu " "setzen." @@ -976,7 +1298,7 @@ msgid "Save and confirm registration" msgstr "Speichern und Registrierung bestätigen" #: members/templates/members/echo.html:6 members/templates/members/echo.html:13 -#: members/templates/members/echo_failed.html:11 +#: members/templates/members/echo_failed.html:10 #: members/templates/members/echo_password.html:6 #: members/templates/members/echo_password.html:11 #: members/templates/members/echo_success.html:10 @@ -991,23 +1313,23 @@ msgstr "" "Vielen Dank, dass du dich rückmeldest. Hier siehst du deine aktuellen Daten. " "Falls sich etwas geändert hat, trage das bitte hier ein." -#: members/templates/members/echo_failed.html:6 +#: members/templates/members/echo_failed.html:5 msgid "Echo failed" msgstr "Rückmeldung fehlgeschlagen" -#: members/templates/members/echo_failed.html:13 -#: members/templates/members/invited_registration_failed.html:13 +#: members/templates/members/echo_failed.html:12 +#: members/templates/members/invited_registration_failed.html:12 msgid "Something went wrong. The key you supplied is" msgstr "Etwas ist schief gegangen. Der verwendete Code ist" -#: members/templates/members/echo_failed.html:15 -#: members/templates/members/invited_registration_failed.html:15 -#: members/templates/members/register_failed.html:15 +#: members/templates/members/echo_failed.html:14 +#: members/templates/members/invited_registration_failed.html:14 +#: members/templates/members/register_failed.html:14 msgid "If you think this is a mistake, please" msgstr "Wenn du denkst, dass das ein Fehler ist, " #: members/templates/members/echo_failed.html:15 -#: members/templates/members/invited_registration_failed.html:15 +#: members/templates/members/invited_registration_failed.html:14 #: members/templates/members/register_failed.html:15 msgid "contact us." msgstr "kontaktiere uns." @@ -1017,11 +1339,10 @@ msgid "" "Thanks for echoing back. Please enter the password, which you can find in " "the email we sent you.\n" msgstr "" -"Bitte gib dein Passwort ein. Weitere Informationenzur Rückmeldung findest du " -"in der E-Mail.\n" +"Bitte gib dein Passwort ein. Weitere Informationen zur Rückmeldung findest " +"du in der E-Mail.\n" #: members/templates/members/echo_password.html:24 -#: members/templates/members/member_form.html:37 #: members/templates/members/register_password.html:22 #: members/templates/members/register_waiting_list.html:39 msgid "submit" @@ -1045,14 +1366,14 @@ msgstr "" "Du hast zu oft ein falsches Passwort eingegeben. Bitte frage deinen " "Jugendleiter nach einem korrekten Passwort." -#: members/templates/members/invited_registration_failed.html:6 -#: members/templates/members/register_failed.html:6 +#: members/templates/members/invited_registration_failed.html:5 +#: members/templates/members/register_failed.html:5 msgid "Registration failed" msgstr "Registrierung fehlgeschlagen" -#: members/templates/members/invited_registration_failed.html:11 +#: members/templates/members/invited_registration_failed.html:10 #: members/templates/members/register.html:6 -#: members/templates/members/register_failed.html:11 +#: members/templates/members/register_failed.html:10 #: members/templates/members/register_password.html:6 #: members/templates/members/register_success.html:6 #: members/templates/members/register_wrong_password.html:6 @@ -1122,7 +1443,11 @@ msgstr "" "Ich bin einverstanden, dass meine Daten auf dem Server der JDAV %(sektion)s " "gespeichert und verarbeitet werden." -#: members/templates/members/member_form.html:101 +#: members/templates/members/member_form.html:37 +msgid "Save" +msgstr "Speichern" + +#: members/templates/members/member_form.html:103 msgid "This file is bigger than the maximal allowed file size of 5 MiB." msgstr "Diese Datei ist größer als die maximal erlaubte Dateigröße von 5 MiB." @@ -1137,7 +1462,7 @@ msgstr "Registrieren" msgid "Here you can register for group" msgstr "Hier kannst du dich registrieren für die Gruppe" -#: members/templates/members/register_failed.html:13 +#: members/templates/members/register_failed.html:12 msgid "Something went wrong while processing your registration." msgstr "Etwas ist schief gelaufen, bei der Verarbeitung deiner Registrierung." @@ -1325,22 +1650,26 @@ msgstr "" msgid "optional additional email address" msgstr "Optionale zusätzliche E-Mailadresse" -#: members/views.py:97 members/views.py:177 +#: members/views.py:97 members/views.py:186 msgid "The entered password is wrong." msgstr "Das eingegebene Passwort ist falsch." -#: members/views.py:131 members/views.py:139 members/views.py:334 +#: members/views.py:132 members/views.py:138 members/views.py:146 +#: members/views.py:343 msgid "invalid" msgstr "ungültig" -#: members/views.py:142 members/views.py:336 +#: members/views.py:149 members/views.py:345 msgid "expired" msgstr "abgelaufen" -#: members/views.py:151 +#: members/views.py:158 msgid "Invalid emergency contacts" msgstr "Ungültige Notfallkontakte" +#~ msgid "Please choose one of the listed modes." +#~ msgstr "Bitte wähle einen der obigen Modi." + #~ msgid "Good conduct certificate presentation needed" #~ msgstr "Vorlage Führungszeugnis notwendig" diff --git a/jdav_web/members/migrations/0026_alter_emergencycontact_email.py b/jdav_web/members/migrations/0026_alter_emergencycontact_email.py new file mode 100644 index 0000000..3adfa23 --- /dev/null +++ b/jdav_web/members/migrations/0026_alter_emergencycontact_email.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.1 on 2024-11-24 19:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0025_alter_member_options'), + ] + + operations = [ + migrations.AlterField( + model_name='emergencycontact', + name='email', + field=models.EmailField(blank=True, default='', max_length=100), + ), + ] diff --git a/jdav_web/members/migrations/0027_alter_group_weekday.py b/jdav_web/members/migrations/0027_alter_group_weekday.py new file mode 100644 index 0000000..1713448 --- /dev/null +++ b/jdav_web/members/migrations/0027_alter_group_weekday.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.1 on 2024-11-27 22:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0026_alter_emergencycontact_email'), + ] + + operations = [ + migrations.AlterField( + model_name='group', + name='weekday', + field=models.IntegerField(blank=True, choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')], null=True, verbose_name='week day'), + ), + ] diff --git a/jdav_web/members/migrations/0028_group_contact_email.py b/jdav_web/members/migrations/0028_group_contact_email.py new file mode 100644 index 0000000..3f8d948 --- /dev/null +++ b/jdav_web/members/migrations/0028_group_contact_email.py @@ -0,0 +1,20 @@ +# Generated by Django 4.0.1 on 2024-11-27 22:40 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailer', '0005_alter_emailaddress_name'), + ('members', '0027_alter_group_weekday'), + ] + + operations = [ + migrations.AddField( + model_name='group', + name='contact_email', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='mailer.emailaddress', verbose_name='Contact email'), + ), + ] diff --git a/jdav_web/members/migrations/0029_alter_member_gender_alter_memberwaitinglist_gender.py b/jdav_web/members/migrations/0029_alter_member_gender_alter_memberwaitinglist_gender.py new file mode 100644 index 0000000..3cee52d --- /dev/null +++ b/jdav_web/members/migrations/0029_alter_member_gender_alter_memberwaitinglist_gender.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.1 on 2024-11-28 00:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0028_group_contact_email'), + ] + + operations = [ + migrations.AlterField( + model_name='member', + name='gender', + field=models.IntegerField(choices=[(0, 'Männlich'), (1, 'Weiblich'), (2, 'Divers')], verbose_name='Gender'), + ), + migrations.AlterField( + model_name='memberwaitinglist', + name='gender', + field=models.IntegerField(choices=[(0, 'Männlich'), (1, 'Weiblich'), (2, 'Divers')], verbose_name='Gender'), + ), + ] diff --git a/jdav_web/members/migrations/0030_alter_member_options.py b/jdav_web/members/migrations/0030_alter_member_options.py new file mode 100644 index 0000000..7a65370 --- /dev/null +++ b/jdav_web/members/migrations/0030_alter_member_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.0.1 on 2024-12-02 00:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0029_alter_member_gender_alter_memberwaitinglist_gender'), + ] + + operations = [ + migrations.AlterModelOptions( + name='member', + options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'permissions': (('may_see_qualities', 'Is allowed to see the quality overview'), ('may_set_auth_user', 'Is allowed to set auth user member connections.'), ('may_change_member_group', 'Can change the group field'), ('may_invite_as_user', 'Is allowed to invite a member to set login data.'), ('may_change_organizationals', 'Is allowed to set organizational settings on members.')), 'verbose_name': 'member', 'verbose_name_plural': 'members'}, + ), + ] diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index 92cf9fb..11314f5 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta import uuid import pytz +import unicodedata import re import csv from django.db import models @@ -24,6 +25,7 @@ from .rules import may_view, may_change, may_delete, is_own_training, is_oneself import rules from contrib.models import CommonModel from contrib.rules import memberize_user, has_global_perm +from utils import cvt_to_decimal from dateutil.relativedelta import relativedelta @@ -73,9 +75,14 @@ class Group(models.Model): year_to = models.IntegerField(verbose_name=_('highest year'), default=2011) leiters = models.ManyToManyField('members.Member', verbose_name=_('youth leaders'), related_name='leited_groups', blank=True) - weekday = models.IntegerField(choices=WEEKDAYS, null=True, blank=True) + weekday = models.IntegerField(verbose_name=_('week day'), choices=WEEKDAYS, null=True, blank=True) start_time = models.TimeField(verbose_name=_('Starting time'), null=True, blank=True) end_time = models.TimeField(verbose_name=_('Ending time'), null=True, blank=True) + contact_email = models.ForeignKey('mailer.EmailAddress', + verbose_name=_('Contact email'), + null=True, + blank=True, + on_delete=models.SET_NULL) def __str__(self): """String representation""" @@ -85,6 +92,10 @@ class Group(models.Model): verbose_name = _('group') verbose_name_plural = _('groups') + def has_time_info(self): + # return if the group has all relevant time slot information filled + return self.weekday and self.start_time and self.end_time + class MemberManager(models.Manager): def get_queryset(self): @@ -130,6 +141,8 @@ class Contact(CommonModel): for email_fd, confirmed_email_fd, confirm_mail_key_fd in self.email_fields: if getattr(self, confirmed_email_fd) and not rerequest: continue + if not getattr(self, email_fd): + continue requested_confirmation = True setattr(self, confirmed_email_fd, False) confirm_mail_key = uuid.uuid4().hex @@ -152,9 +165,9 @@ class Contact(CommonModel): return getattr(self, email_fd) return None - def send_mail(self, subject, content): + def send_mail(self, subject, content, cc=None): send_mail(subject, content, settings.DEFAULT_SENDING_MAIL, - [getattr(self, email_fd) for email_fd, _, _ in self.email_fields]) + [getattr(self, email_fd) for email_fd, _, _ in self.email_fields], cc=cc) def confirm_mail_by_key(key): @@ -189,7 +202,6 @@ class Person(Contact): (FEMALE, 'Weiblich'), (DIVERSE, 'Divers')) gender = models.IntegerField(choices=gender_choices, - default=DIVERSE, verbose_name=_('Gender')) comments = models.TextField(_('comments'), default='', blank=True) @@ -314,7 +326,7 @@ class Member(Person): def generate_echo_key(self): self.echo_key = uuid.uuid4().hex - self.echo_expire = timezone.now() + timezone.timedelta(days=30) + self.echo_expire = timezone.now() + timezone.timedelta(days=settings.ECHO_GRACE_PERIOD) self.echoed = False self.save() return self.echo_key @@ -357,11 +369,19 @@ class Member(Person): """A synonym for the email field.""" return self.email + @property + def username(self): + """Return the username. Either this the name of the linked user, or + it is the suggested username.""" + if not self.user: + return self.suggested_username() + else: + return self.user.username + @property def association_email(self): """Returning the association email of the member""" - raw = "{0}.{1}@{2}".format(self.prename.lower(), self.lastname.lower(), settings.DOMAIN) - return raw.replace('ö', 'oe').replace('ä', 'ae').replace('ü', 'ue') + return "{username}@{domain}".format(username=self.username, domain=settings.DOMAIN) def registration_complete(self): """Check if all necessary fields are set.""" @@ -383,8 +403,9 @@ class Member(Person): permissions = ( ('may_see_qualities', 'Is allowed to see the quality overview'), ('may_set_auth_user', 'Is allowed to set auth user member connections.'), - ('change_member_group', 'Can change the group field'), + ('may_change_member_group', 'Can change the group field'), ('may_invite_as_user', 'Is allowed to invite a member to set login data.'), + ('may_change_organizationals', 'Is allowed to set organizational settings on members.'), ) rules_permissions = { 'members': rules.always_allow, @@ -434,11 +455,6 @@ class Member(Person): return not self.confirmed and self.confirmed_alternative_mail and self.confirmed_mail and\ all([emc.confirmed_mail for emc in self.emergencycontact_set.all()]) - def request_mail_confirmation(self, rerequest=False): - ret = super().request_mail_confirmation(rerequest) - rets = [emc.request_mail_confirmation(rerequest) for emc in self.emergencycontact_set.all()] - return ret or any(rets) - def confirm_mail(self, key): ret = super().confirm_mail(key) if self.registration_ready(): @@ -492,6 +508,8 @@ class Member(Person): return queryset elif name == "EmergencyContact": return queryset + elif name == "MemberUnconfirmedProxy": + return queryset else: raise ValueError(name) @@ -657,15 +675,29 @@ class Member(Person): def suggested_username(self): """Returns a suggested username given by {prename}.{lastname}.""" raw = "{0}.{1}".format(self.prename.lower(), self.lastname.lower()) - return raw.replace('ö', 'oe').replace('ä', 'ae').replace('ü', 'ue') + return normalize_name(raw) + + def has_internal_email(self): + """Returns if the configured e-mail address is a DAV360 email address.""" + match = re.match('(^[^@]*)@(.*)$', self.email) + if not match: + return False + return match.group(2) in settings.ALLOWED_EMAIL_DOMAINS_FOR_INVITE_AS_USER def invite_as_user(self): """Invites the member to join Kompass as a user.""" + if not self.has_internal_email(): + # dont invite if the email address is not an internal one + return False + if self.user: + # don't reinvite if there is already userdata attached + return False self.invite_as_user_key = uuid.uuid4().hex self.save() self.send_mail(_('Set login data for Kompass'), settings.INVITE_AS_USER_TEXT.format(name=self.prename, link=get_invite_as_user_key(self.invite_as_user_key))) + return True def led_groups(self): """Returns a queryset of groups that this member is a youth leader of.""" @@ -681,6 +713,7 @@ class EmergencyContact(ContactWithPhoneNumber): Emergency contact of a member """ member = models.ForeignKey(Member, verbose_name=_('Member'), on_delete=models.CASCADE) + email = models.EmailField(max_length=100, default='', blank=True) def __str__(self): return str(self.member) @@ -710,6 +743,11 @@ class MemberUnconfirmedProxy(Member): verbose_name = _('Unconfirmed registration') verbose_name_plural = _('Unconfirmed registrations') permissions = (('may_manage_all_registrations', 'Can view and manage all unconfirmed registrations.'),) + rules_permissions = { + 'view_obj': may_view | has_global_perm('members.may_manage_all_registrations'), + 'change_obj': may_change | has_global_perm('members.may_manage_all_registrations'), + 'delete_obj': may_delete | has_global_perm('members.may_manage_all_registrations'), + } def __str__(self): """String representation""" @@ -846,21 +884,23 @@ class MemberWaitingList(Person): group_link = '({url}) '.format(url=prepend_base_url(reverse('startpage:gruppe_detail', args=[group.name]))) else: group_link = '' - # TODO: inform the user that the group has no configured weekday, start_time or end_time - weekday = WEEKDAYS[group.weekday][1] if group.weekday != None else WEEKDAYS[0][1] - start_time = group.start_time.strftime('%H:%M') if group.start_time != None else "14:00" - end_time = group.end_time.strftime('%H:%M') if group.end_time != None else "16:00" + if group.has_time_info(): + group_time = settings.GROUP_TIME_AVAILABLE_TEXT.format(weekday=WEEKDAYS[group.weekday][1], + start_time=group.start_time.strftime('%H:%M'), + end_time=group.end_time.strftime('%H:%M')) + else: + group_time = settings.GROUP_TIME_UNAVAILABLE_TEXT.format(contact_email=group.contact_email) invitation = InvitationToGroup(group=group, waiter=self) invitation.save() self.send_mail(_("Invitation to trial group meeting"), settings.INVITE_TEXT.format(name=self.prename, - weekday=weekday, - start_time=start_time, - end_time=end_time, - group_name=group.name, - group_link=group_link, - link=get_registration_link(invitation.key), - invitation_reject_link=get_invitation_reject_link(invitation.key))) + group_time=group_time, + group_name=group.name, + group_link=group_link, + contact_email=group.contact_email, + link=get_registration_link(invitation.key), + invitation_reject_link=get_invitation_reject_link(invitation.key)), + cc=group.contact_email.email) def unregister(self): """Delete the waiter and inform them about the deletion via email.""" @@ -1015,6 +1055,31 @@ class Freizeit(CommonModel): jls = set(self.jugendleiter.distinct()) return len(ps - jls) + @property + def ljp_participant_count(self): + ps = set(map(lambda x: x.member, self.membersonlist.distinct())) + jls = set(self.jugendleiter.distinct()) + count = len(ps.union(jls)) + return count + #return count if count >= 5 else 0 + + @property + def maximal_ljp_contributions(self): + return cvt_to_decimal(settings.LJP_CONTRIBUTION_PER_DAY * self.ljp_participant_count * self.duration) + + @property + def potential_ljp_contributions(self): + return cvt_to_decimal(min(self.maximal_ljp_contributions, + 0.9 * float(self.statement.total_bills_theoretic) + float(self.statement.total_staff))) + + @property + def total_relative_costs(self): + if not self.statement: + return 0 + total_costs = self.statement.total_bills_theoretic + total_contributions = self.statement.total_staff + self.potential_ljp_contributions + return total_costs - total_contributions + @property def time_period_str(self): time_period = self.date.strftime('%d.%m.%Y') @@ -1091,6 +1156,60 @@ class Freizeit(CommonModel): base['Status' + suffix] = str(2) return base + def v32_fields(self): + title = self.ljpproposal.title if hasattr(self, 'ljpproposal') else self.name + base = { + # AntragstellerIn + 'Textfeld 2': settings.ADDRESS, + # Dachorganisation + 'Textfeld 3': settings.V32_HEAD_ORGANISATION, + # Datum der Maßnahme am/vom + 'Textfeld 20': self.date.strftime('%d.%m.%Y'), + # bis + 'Textfeld 28': self.end.strftime('%d.%m.%Y'), + # Thema der Maßnahme + 'Textfeld 22': title, + # IBAN + 'Textfeld 36': settings.SEKTION_IBAN, + # Kontoinhaber + 'Textfeld 37': settings.SEKTION_ACCOUNT_HOLDER, + # Zahl der zuwendungsfähigen Teilnehemr + 'Textfeld 43': str(self.ljp_participant_count), + # Teilnahmetage + 'Textfeld 46': str(round(self.duration * self.ljp_participant_count, 1)).replace('.', ','), + # Euro Tagessatz + 'Textfeld 48': str(settings.LJP_CONTRIBUTION_PER_DAY), + # Erbetener Zuschuss + 'Textfeld 50': str(self.maximal_ljp_contributions).replace('.', ','), + # Stunden Bildungsprogramm + 'Textfeld 52': '??', + # Tage + 'Textfeld 53': str(round(self.duration, 1)).replace('.', ','), + # Haushaltsjahr + 'Textfeld 54': str(datetime.now().year), + # nicht anrechenbare Teilnahmetage + 'Textfeld 55': '0', + # Gesamt-Teilnahmetage + 'Textfeld 56': str(round(self.duration * self.ljp_participant_count, 1)).replace('.', ','), + # Ort, Datum + 'DatumOrt 2': 'Heidelberg, ' + datetime.now().strftime('%d.%m.%Y') + } + if hasattr(self, 'statement'): + possible_contributions = self.maximal_ljp_contributions + total_contributions = min(self.statement.total_theoretic, possible_contributions) + self_participation = max(cvt_to_decimal(0), self.statement.total_theoretic - possible_contributions) + # Gesamtkosten von + base['Textfeld 62'] = str(self.statement.total_theoretic).replace('.', ',') + # Eigenmittel und Teilnahmebeiträge + base['Textfeld 59'] = str(self_participation).replace('.', ',') + # Drittmittel + base['Textfeld 60'] = '0,00' + # Erbetener Zuschuss + base['Textfeld 61'] = str(total_contributions).replace('.', ',') + # Ergibt wieder + base['Textfeld 58'] = base['Textfeld 62'] + return base + @staticmethod def filter_queryset_by_permissions(member, queryset=None): if queryset is None: @@ -1524,7 +1643,7 @@ def parse_date(value): def parse_datetime(value): tz = pytz.timezone('Europe/Berlin') if value == '': - return None + return timezone.now() return tz.localize(datetime.strptime(value, '%d.%m.%Y %H:%M:%S')) @@ -1625,7 +1744,7 @@ def import_from_csv_waitinglist(path): kwargs = dict([ transform_field(k, v) for k, v in row.items() if k in CLUBDESK_TO_KOMPASS ]) kwargs_filtered = { k : v for k, v in kwargs.items() if k in ['prename', 'lastname', 'email', 'birth_date', 'application_text', 'application_date'] } - mem = MemberWaitingList(**kwargs_filtered) + mem = MemberWaitingList(gender=DIVERSE, **kwargs_filtered) mem.save() if kwargs['contacted_by']: @@ -1639,3 +1758,8 @@ def import_from_csv_waitinglist(path): for row in rows: transform_row(row) + + +def normalize_name(raw): + noumlaut = raw.replace('ö', 'oe').replace('ä', 'ae').replace('ü', 'ue').replace(' ', '_') + return unicodedata.normalize('NFKD', noumlaut).encode('ascii', 'ignore').decode('ascii') diff --git a/jdav_web/members/pdf.py b/jdav_web/members/pdf.py index 00a9978..a273b95 100644 --- a/jdav_web/members/pdf.py +++ b/jdav_web/members/pdf.py @@ -31,7 +31,17 @@ def media_dir(): return os.path.join(settings.MEDIA_MEMBERLISTS, "memberlists") -def render_tex(name, template_path, context): +def serve_pdf(filename_pdf): + # provide the user with the resulting pdf file + with open(media_path(filename_pdf), 'rb') as pdf: + response = HttpResponse(FileWrapper(pdf))#, content='application/pdf') + response['Content-Type'] = 'application/pdf' + response['Content-Disposition'] = 'attachment; filename='+filename_pdf + + return response + + +def render_tex(name, template_path, context, save_only=False): filename = name + "_" + datetime.today().strftime("%d_%m_%Y") filename = filename.replace(' ', '_').replace('&', '').replace('/', '_') # drop umlauts, accents etc. @@ -64,16 +74,12 @@ def render_tex(name, template_path, context): os.chdir(oldwd) - # provide the user with the resulting pdf file - with open(media_path(filename_pdf), 'rb') as pdf: - response = HttpResponse(FileWrapper(pdf))#, content='application/pdf') - response['Content-Type'] = 'application/pdf' - response['Content-Disposition'] = 'attachment; filename='+filename_pdf - - return response + if save_only: + return filename_pdf + return serve_pdf(filename_pdf) -def fill_pdf_form(name, template_path, fields, attachments=[]): +def fill_pdf_form(name, template_path, fields, attachments=[], save_only=False): filename = name + "_" + datetime.today().strftime("%d_%m_%Y") filename = filename.replace(' ', '_').replace('&', '').replace('/', '_') # drop umlauts, accents etc. @@ -104,10 +110,22 @@ def fill_pdf_form(name, template_path, fields, attachments=[]): with open(media_path(filename_pdf), 'wb') as output_stream: writer.write(output_stream) - # provide the user with the resulting pdf file - with open(media_path(filename_pdf), 'rb') as pdf: - response = HttpResponse(FileWrapper(pdf))#, content='application/pdf') - response['Content-Type'] = 'application/pdf' - response['Content-Disposition'] = 'attachment; filename='+filename_pdf + if save_only: + return filename_pdf + return serve_pdf(filename_pdf) - return response + +def merge_pdfs(name, filenames, save_only=False): + merger = PdfWriter() + + for pdf in filenames: + merger.append(media_path(pdf)) + + filename = name + "_" + datetime.today().strftime("%d_%m_%Y") + filename_pdf = filename + ".pdf" + merger.write(media_path(filename_pdf)) + merger.close() + + if save_only: + return filename_pdf + return serve_pdf(filename_pdf) diff --git a/jdav_web/members/templates/admin/demote_to_waiter.html b/jdav_web/members/templates/admin/demote_to_waiter.html new file mode 100644 index 0000000..661ddef --- /dev/null +++ b/jdav_web/members/templates/admin/demote_to_waiter.html @@ -0,0 +1,48 @@ +{% 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 %} +

{% translate "Demote to waiter" %}

+

+{% trans "Do you want to demote the following unconfirmed registrations to waiters?" %} +

+

+

    + {% for member in queryset %} +
  • + {{ member }} +
  • + {% endfor %} +
+

+ +
+ {% csrf_token %} + {% if form %} + {{form}} + {% endif %} + + + {% translate "Cancel" %} +
+{% endblock %} diff --git a/jdav_web/members/templates/admin/freizeit_finance_overview.html b/jdav_web/members/templates/admin/freizeit_finance_overview.html new file mode 100644 index 0000000..ffba21f --- /dev/null +++ b/jdav_web/members/templates/admin/freizeit_finance_overview.html @@ -0,0 +1,167 @@ +{% 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 %} +

{% trans 'Excursion' %}: {{ memberlist.name }}

+ +

+{% blocktrans %} +Here you see an estimate on the expected costs and contributions by the association. This is not a guaranteed +cost plan! +{% endblocktrans %} +

+

{% translate "Expenses" %}

+{% blocktrans %}You listed the following expenses:{% endblocktrans %} +

+ + + + + {% for bill in memberlist.statement.bill_set.all %} + + + + + + {% endfor %} +
+ {% trans "Explanation" %}{% trans "Amount" %}
+ {{bill.short_description}} + + {{bill.explanation}} + + {{ bill.amount }}€ +
+

+ +

{% blocktrans %}The total expected expenses are {{ total_bills_theoretic }} €.{% endblocktrans %}

+ +

{% trans "Contributions by the association" %}

+ +

+{% blocktrans %}According to the contribution guidelines, +{{ staff_count }} youth leader(s) receive contributions. Each of them receives{% endblocktrans %} +

+

+

    +
  • + {% blocktrans %}{{ nights }} nights for {{ price_per_night }}€ per night making a total of {{ nights_per_yl }}€.{% endblocktrans %} +
  • +
  • + {% blocktrans %}{{ duration }} days for {{ allowance_per_day }}€ per day making a total of {{ allowance_per_yl }}€.{% endblocktrans %} +
  • +
  • + {% blocktrans %}{{ kilometers_traveled }} km by {{ means_of_transport }} ({{euro_per_km}} € / km) making a total of {{ transportation_per_yl }}€.{% endblocktrans %} +
  • +
+

+

+{% blocktrans %}In total these are contributions of {{ total_per_yl }}€ times {{ staff_count }}, giving {{ total_staff }}€.{% endblocktrans %} +

+ +

{% trans "LJP contributions" %}

+ +

+{% blocktrans %}By submitting a seminar report, you may apply for LJP contributions. In this case, +you may obtain up to 25€ times {{ duration }} days for {{ participant_count }} participants but only up to +90% of the total costs. This results in a total of {{ ljp_contributions }}€.{% endblocktrans %} +

+ +

{% trans "Summary" %}

+ +

+{% blocktrans %}This is the estimated cost and contribution summary:{% endblocktrans %} +

+ + + + + + + + + + + + + + + + + + +
+ {% trans "Expenses" %} + + {{ total_bills_theoretic }}€ +
+ {% trans "Contributions by the association" %} + + -{{ total_staff }}€ +
+ {% trans "Potential LJP contributions" %} + + -{{ ljp_contributions }}€ +
+ {% trans "Remaining costs" %} + + {{ total_relative_costs }}€ +
+
+

+{% blocktrans %}Positive remaining costs indicate that the estimated costs exceed the estimated contributions, while negative +remaining costs indicate that the estimated contributions exceed the estimated costs.{% endblocktrans %} +

+

+{% blocktrans %}Note that this cost calculation expects you to apply for LJP contributions. On the +excursions main page, you can generate a template for a seminar report.{% endblocktrans %} +

+ +{% if not memberlist.statement.submitted %} +

{% trans "Submit statement" %}

+

+{% blocktrans %}Did you already complete this excursion? If yes, please check if all listed expenses are correct +and then submit the statement for processing by the finance department. If you proceed, +no further changes to the statement are possible.{% endblocktrans %} +

+ +
+ {% csrf_token %} + + + + {% translate "Cancel" %} +
+{% else %} +
+

{% trans "Statement submitted" %}

+

+{% blocktrans %}The statement for this excursion was already submitted. The finance department is currently processing your +data and you will receive a response shortly.{% endblocktrans %} +

+{% translate "Back" %} + +{% endif %} + +{% endblock %} diff --git a/jdav_web/members/templates/admin/generate_seminar_report.html b/jdav_web/members/templates/admin/generate_seminar_report.html index 8b4cfa0..6a52699 100644 --- a/jdav_web/members/templates/admin/generate_seminar_report.html +++ b/jdav_web/members/templates/admin/generate_seminar_report.html @@ -27,6 +27,10 @@ {% 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 %}Expenses with same short description are automatically summed up and shown as one expense in the +expense overview.{% endblocktrans %} +

  • {% blocktrans %}Full report: Include learning goals and a detailed, tabularized time schedule. This requires @@ -39,12 +43,14 @@ have to add learning goals and a time schedule manually.{% endblocktrans %}

-

{% blocktrans %}Please choose one of the listed modes.{% endblocktrans %}

+

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

{% csrf_token %}

- {{form}} + + {{ form }} +


diff --git a/jdav_web/members/templates/members/V32-1_Themenorientierte_Bildungsmassnahmen.pdf b/jdav_web/members/templates/members/V32-1_Themenorientierte_Bildungsmassnahmen.pdf new file mode 100644 index 0000000..46a427a Binary files /dev/null and b/jdav_web/members/templates/members/V32-1_Themenorientierte_Bildungsmassnahmen.pdf differ diff --git a/jdav_web/members/templates/members/crisis_intervention_list.tex b/jdav_web/members/templates/members/crisis_intervention_list.tex index 2481209..b615d1b 100644 --- a/jdav_web/members/templates/members/crisis_intervention_list.tex +++ b/jdav_web/members/templates/members/crisis_intervention_list.tex @@ -1,6 +1,6 @@ {% load tex_extras %} -\documentclass{article} +\documentclass[a4paper]{article} \usepackage[utf8]{inputenc} \usepackage{booktabs} @@ -58,7 +58,7 @@ \end{textblock*} % HEADLINE -{\noindent\LARGE{Teilnehmer:innenliste\\[2mm]Sektionsveranstaltung}}\\[1mm] +{\noindent\LARGE{Teilnehmer*innenliste\\[2mm]Sektionsveranstaltung}}\\[1mm] \textit{Erstellt: {{ creation_date }} }\\ % DESCRIPTION TABLE @@ -89,8 +89,7 @@ {{ m.member.contact_phone_number|esc_all }} & {% for c in m.member.emergencycontact_set.all %} {{ c.name }} \newline - Tel.: {{ c.phone_number }} \newline - Email: \href{mailto:{{ c.email }}}{ {{c.email}}} + Tel.: {{ c.phone_number }} {% endfor %} \\ {% endfor %} \bottomrule diff --git a/jdav_web/members/templates/members/echo_failed.html b/jdav_web/members/templates/members/echo_failed.html index 8e7ee67..be66343 100644 --- a/jdav_web/members/templates/members/echo_failed.html +++ b/jdav_web/members/templates/members/echo_failed.html @@ -1,6 +1,5 @@ {% extends "members/base.html" %} -{% load i18n %} -{% load static %} +{% load i18n static common %} {% block title %} {% trans "Echo failed" %} @@ -12,6 +11,7 @@

{% trans "Something went wrong. The key you supplied is" %} {{ reason }}.

-

{% trans "If you think this is a mistake, please" %} {% trans "contact us." %}

+

{% trans "If you think this is a mistake, please" %} +{% trans "contact us." %}

{% endblock %} diff --git a/jdav_web/members/templates/members/invited_registration_failed.html b/jdav_web/members/templates/members/invited_registration_failed.html index 619f534..393307f 100644 --- a/jdav_web/members/templates/members/invited_registration_failed.html +++ b/jdav_web/members/templates/members/invited_registration_failed.html @@ -1,6 +1,5 @@ {% extends "members/base.html" %} -{% load i18n %} -{% load static %} +{% load i18n static common %} {% block title %} {% trans "Registration failed" %} @@ -12,6 +11,6 @@

{% trans "Something went wrong. The key you supplied is" %} {{ reason }}.

-

{% trans "If you think this is a mistake, please" %} {% trans "contact us." %}

+

{% trans "If you think this is a mistake, please" %} {% trans "contact us." %}

{% endblock %} diff --git a/jdav_web/members/templates/members/member_form.html b/jdav_web/members/templates/members/member_form.html index f0664d3..e357cfa 100644 --- a/jdav_web/members/templates/members/member_form.html +++ b/jdav_web/members/templates/members/member_form.html @@ -34,7 +34,7 @@ -

+