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 %}
+
+
+
+
+{% 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 %}
+
+
+
+ {% trans "Explanation" %}
+ {% trans "Amount" %}
+
+ {% for bill in memberlist.statement.bill_set.all %}
+
+
+ {{bill.short_description}}
+
+
+ {{bill.explanation}}
+
+
+ {{ bill.amount }}€
+
+
+ {% endfor %}
+
+
+
+{% 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 %}
+
+
+
+{% 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 %}
@@ -47,7 +47,9 @@
function addRequired(element) {
var inputs = element.getElementsByTagName('input');
for (var i = 0; i < inputs.length; i++) {
- inputs[i].setAttribute('required', 'required');
+ if (inputs[i].getAttribute('type') != 'email') {
+ inputs[i].setAttribute('required', 'required');
+ }
}
}
function removeRequired(element) {
diff --git a/jdav_web/members/templates/members/notes_list.tex b/jdav_web/members/templates/members/notes_list.tex
index f07cb48..fbfa23e 100644
--- a/jdav_web/members/templates/members/notes_list.tex
+++ b/jdav_web/members/templates/members/notes_list.tex
@@ -1,6 +1,6 @@
{% load tex_extras %}
-\documentclass{article}
+\documentclass[a4paper]{article}
\usepackage[utf8]{inputenc}
\usepackage{booktabs}
@@ -20,7 +20,7 @@
\begin{document}
% HEADLINE
-{\noindent\LARGE{Teilnehmer:innenübersicht}}\\[1mm]
+{\noindent\LARGE{Teilnehmer*innenübersicht}}\\[1mm]
\textit{Erstellt: {{ creation_date }} }\\
% DESCRIPTION
diff --git a/jdav_web/members/templates/members/register_failed.html b/jdav_web/members/templates/members/register_failed.html
index 49af016..c1371c8 100644
--- a/jdav_web/members/templates/members/register_failed.html
+++ b/jdav_web/members/templates/members/register_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,7 @@
{% trans "Something went wrong while processing your registration." %}
-
{% 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/register_waiting_list.html b/jdav_web/members/templates/members/register_waiting_list.html
index cf7f62f..4e53bfd 100644
--- a/jdav_web/members/templates/members/register_waiting_list.html
+++ b/jdav_web/members/templates/members/register_waiting_list.html
@@ -18,7 +18,7 @@ Da wir durch das Engagment unserer ehrenamtlichen Jugendleitern, aber auch die K
Die Anmeldung für die Gruppen JuMa ,
-Klettertreff und
+Klettertreff und
Mountainbike laufen separat.
diff --git a/jdav_web/members/templates/members/seminar_report.tex b/jdav_web/members/templates/members/seminar_report.tex
index ea0160b..d73cb67 100644
--- a/jdav_web/members/templates/members/seminar_report.tex
+++ b/jdav_web/members/templates/members/seminar_report.tex
@@ -1,6 +1,6 @@
{% load tex_extras %}
-\documentclass{article}
+\documentclass[a4paper]{article}
\usepackage[utf8]{inputenc}
\usepackage{booktabs}
@@ -119,7 +119,7 @@
{% endif %}
{% endif %}
-\section{Teilnehmer:innenliste}
+\section{Teilnehmer*innenliste}
\begin{table}[H]
\begin{tabularx}{1\linewidth}{@{\extracolsep{\fill}}lLl|c|c|c}
@@ -148,7 +148,7 @@
Aufwandsentschädigung & {{ memberlist.statement.total_allowance }} € \\
Fahrtkosten & {{ memberlist.statement.total_transportation }} € \\
Übernachtungskosten & {{ memberlist.statement.total_nights }} € \\
- {% for bill in memberlist.statement.bill_set.all %}
+ {% for bill in memberlist.statement.grouped_bills %}
{{ bill.short_description|esc_all }} & {{ bill.amount }} € \\
{% endfor %}
\bottomrule
diff --git a/jdav_web/members/views.py b/jdav_web/members/views.py
index b96e130..4824c15 100644
--- a/jdav_web/members/views.py
+++ b/jdav_web/members/views.py
@@ -13,8 +13,8 @@ from django.conf import settings
class MemberForm(ModelForm):
class Meta:
model = Member
- fields = ['prename', 'lastname', 'street', 'plz', 'town', 'address_extra',
- 'phone_number', 'dav_badge_no']
+ fields = ['prename', 'lastname', 'gender', 'street', 'plz', 'town',
+ 'address_extra', 'phone_number', 'dav_badge_no']
class MemberRegistrationForm(ModelForm):
def __init__(self, *args, **kwargs):
@@ -29,7 +29,7 @@ class MemberRegistrationForm(ModelForm):
'phone_number', 'birth_date', 'gender', 'email', 'alternative_email',
'registration_form']
widgets = {
- 'birth_date': DateInput(format='%d.%m.%Y', attrs={'type': 'date'}),
+ 'birth_date': DateInput(format='%Y-%m-%d', attrs={'type': 'date'}),
'registration_form': FileInput(attrs={'accept': 'application/pdf,image/jpeg,image/png'}),
}
help_texts = {
@@ -53,7 +53,7 @@ class MemberRegistrationWaitingListForm(ModelForm):
model = MemberWaitingList
fields = ['prename', 'lastname', 'birth_date', 'gender', 'email', 'application_text']
widgets = {
- 'birth_date': DateInput(format='%d.%m.%Y', attrs={'type': 'date'})
+ 'birth_date': DateInput(format='%Y-%m-%d', attrs={'type': 'date'})
}
help_texts = {
'prename': _('Prename of the member.'),
@@ -72,7 +72,7 @@ class EmergencyContactForm(ModelForm):
class Meta:
model = EmergencyContact
fields = ['prename', 'lastname', 'email', 'phone_number']
- required = ['prename', 'lastname', 'email', 'phone_number']
+ required = ['prename', 'lastname', 'phone_number']
class BaseEmergencyContactsFormSet(BaseInlineFormSet):
@@ -124,6 +124,13 @@ def echo(request):
return HttpResponseRedirect(reverse('startpage:index'))
if request.method == 'GET':
+ key = request.GET['key']
+ # try to get a member from the supplied echo key
+ try:
+ member = Member.objects.get(echo_key=key)
+ except Member.DoesNotExist:
+ return render_echo_failed(request, _("invalid"))
+
# show password
return render_echo_password(request, request.GET['key'])
@@ -151,7 +158,9 @@ def echo(request):
raise ValueError(_("Invalid emergency contacts"))
form.save()
emergency_contacts_formset.save()
- member.echo_key, member.echo_expire = "", timezone.now()
+ # We don't invalidate the echo key, so the user
+ # can echo again if wanted.
+ # member.echo_key, member.echo_expire = "", timezone.now()
member.echoed = True
member.save()
return render_echo_success(request, member.prename)
diff --git a/jdav_web/startpage/locale/de/LC_MESSAGES/django.po b/jdav_web/startpage/locale/de/LC_MESSAGES/django.po
index 3d7f153..a02680c 100644
--- a/jdav_web/startpage/locale/de/LC_MESSAGES/django.po
+++ b/jdav_web/startpage/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-23 19:12+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/startpage/templates/startpage/base.html b/jdav_web/startpage/templates/startpage/base.html
index 05414ba..ae69f54 100644
--- a/jdav_web/startpage/templates/startpage/base.html
+++ b/jdav_web/startpage/templates/startpage/base.html
@@ -7,6 +7,7 @@
+
diff --git a/jdav_web/startpage/templates/startpage/gruppen/detail.html b/jdav_web/startpage/templates/startpage/gruppen/detail.html
index fdd63d2..e34205d 100644
--- a/jdav_web/startpage/templates/startpage/gruppen/detail.html
+++ b/jdav_web/startpage/templates/startpage/gruppen/detail.html
@@ -1,10 +1,11 @@
{% extends "startpage/base_subsite.html" %}
+{% load markdown_extras markdownify %}
{% block content %}
{{ group.name }}
-{{ group.description }}
+{{ group.description|markdownify }}
Gruppenleitung
diff --git a/jdav_web/startpage/templates/startpage/gruppen/faq.html b/jdav_web/startpage/templates/startpage/gruppen/faq.html
index 1dc29cc..d6de1f3 100644
--- a/jdav_web/startpage/templates/startpage/gruppen/faq.html
+++ b/jdav_web/startpage/templates/startpage/gruppen/faq.html
@@ -10,7 +10,7 @@
Wie lange sind die Wartezeiten?
-Leider haben wir eine sehr große Nachfrage, der wir mit unseren Jugendleiter:innen und unseren Gruppenplätzen nicht nachkommen können. Die Wartezeiten sind sehr unterscheidlich und hängen vom Alter und den verfügbaren Gruppenplätzen ab. Die Wartezeiten kann zwischen einigen Wochen und mehr als einem Jahr liegen.
+Leider haben wir eine sehr große Nachfrage, der wir mit unseren Jugendleiter*innen und unseren Gruppenplätzen nicht nachkommen können. Die Wartezeiten sind sehr unterscheidlich und hängen vom Alter und den verfügbaren Gruppenplätzen ab. Die Wartezeiten kann zwischen einigen Wochen und mehr als einem Jahr liegen.
Muss mein Kind DAV Mitglied sein?
diff --git a/jdav_web/startpage/templates/startpage/impressum.html b/jdav_web/startpage/templates/startpage/impressum.html
index eb31dc1..78371b6 100644
--- a/jdav_web/startpage/templates/startpage/impressum.html
+++ b/jdav_web/startpage/templates/startpage/impressum.html
@@ -38,7 +38,7 @@ Der Deutsche Alpenverein Sektion Heidelberg 1869 e.V. ist im Vereinsregister des
Jugendreferat
-Leitung: Eva Engelmann und Robert Scheffler (kommissarisch)
+Leitung: Eva Engelmann und Robert Scheffler
E-Mail: jugend@alpenverein-heidelberg.de
URL: www.jdav-hd.de ; www.jdav-heidelberg.de
@@ -59,12 +59,13 @@ Diese Datenschutz-Information gilt für die Datenverarbeitung durch:
Verantwortlicher: Deutscher Alpenverein Sektion Heidelberg 1869 e.V.
Harbigweg 20, 69124 Heidelberg
-Email: alpenverein-heidelberg@t-online.de
+Email: geschaeftsstelle@alpenverein-heidelberg.de
Telefon: +49 6221 484076
-Zur Auftragsverarbeitung für die jdav Heidelberg ist die Firma reeweb AG Wettsteinplatz 74058 Basel Schweiz beauftragt. Die Datenverarbeitung erfolgt in Rechenzentren in der Schweiz oder Staaten des Europäischen Wirtschaftsraums (EWR).
+Zur Auftragsverarbeitung für die jdav Heidelberg ist die Firma Contabo GmbH Aschauer Str. 32a 81549 München beauftragt.
+Die Datenverarbeitung erfolgt in Rechenzentren in der Schweiz oder Staaten des Europäischen Wirtschaftsraums (EWR).
@@ -72,7 +73,7 @@ Zur Auftragsverarbeitung für die jdav Heidelberg ist die Firma reeweb AG Wettst
-Beim Aufrufen unserer Website jdav-hd.merten.dev werden durch den auf Ihrem Endgerät zum Einsatz kommenden Browser automatisch Informationen an den Server unserer Website gesendet. Diese Informationen werden temporär in einem sog. Logfile gespeichert. Folgende Informationen werden dabei ohne Ihr Zutun erfasst und bis zur automatischen Löschung gespeichert:
+Beim Aufrufen unserer Website jdav-hd.de werden durch den auf Ihrem Endgerät zum Einsatz kommenden Browser automatisch Informationen an den Server unserer Website gesendet. Diese Informationen werden temporär in einem sog. Logfile gespeichert. Folgende Informationen werden dabei ohne Ihr Zutun erfasst und bis zur automatischen Löschung gespeichert:
@@ -131,7 +132,8 @@ Zur Weitergabe der Daten im Rahmen einer Mitgliedschaft im Alpenverein Heidelber
-Die jdav Heidelberg hat die Firma reeweb AG Wettsteinplatz 74058 Basel Schweiz beauftragt die Daten ihrer Mitglieder zu verwalten. Diese Datenverarbeitung erfolgt in Rechenzentren in der Schweiz oder Staaten des Europäischen Wirtschaftsraums (EWR).
+Die jdav Heidelberg hat die Firma Contabo GmbH Aschauer Str. 32a 81549 München beauftragt die Daten ihrer Mitglieder zu
+verwalten. Diese Datenverarbeitung erfolgt in Rechenzentren in der Schweiz oder Staaten des Europäischen Wirtschaftsraums (EWR).
@@ -191,7 +193,10 @@ Die meisten Browser akzeptieren Cookies automatisch. Sie können Ihren Browser j
-Auf unserer Internetseite sind Kontaktformulare vorhanden, welche für die elektronische Kontaktaufnahme genutzt werden können. Nimmt ein Nutzer diese Möglichkeit wahr, so werden die in der Eingabemaske eingegeben Daten an uns übermittelt und gespeichert. Diese Daten sind:
+Auf unserer Internetseite sind Kontaktformulare vorhanden, welche für die elektronische Kontaktaufnahme genutzt werden können.
+Nimmt ein Nutzer diese Möglichkeit wahr, so werden die in der Eingabemaske eingegeben Daten an uns übermittelt und gespeichert.
+Für diese Datenverarbeitung ist die Firma Contabo GmbH Aschauer Str. 32a 81549 München beauftragt.
+Diese Daten sind:
@@ -258,7 +263,7 @@ Die während des Absendevorgangs zusätzlich erhobenen personenbezogenen Daten w
6. Das geschieht, wenn Sie Online-Anmeldeformulare nutzen:
-Auf unserer Internetseite sind Anmeldeformulare vorhanden, welches für die elektronische Anmeldung zu Jugendgruppen, Kursen und Veranstaltungen unseres Vereins genutzt werden kann. Nimmt ein Nutzer diese Möglichkeit wahr, so werden die in der Eingabemaske eingegeben Daten an uns übermittelt und gespeichert. Für diese Datenverarbeitung ist die Firma reeweb AG Wettsteinplatz 74058 Basel Schweiz beauftragt. Diese Daten sind:
+Auf unserer Internetseite sind Anmeldeformulare vorhanden, welches für die elektronische Anmeldung zu Jugendgruppen, Kursen und Veranstaltungen unseres Vereins genutzt werden kann. Nimmt ein Nutzer diese Möglichkeit wahr, so werden die in der Eingabemaske eingegeben Daten an uns übermittelt und gespeichert.
@@ -346,7 +351,7 @@ Sofern Ihre personenbezogenen Daten auf Grundlage von berechtigten Interessen ge
Möchten Sie von Ihrem Widerrufs- oder Widerspruchsrecht Gebrauch machen, genügt eine Email an
jugend@alpenverein-heidelberg.de oder
-alpenverein-heidelberg@t-online.de .
+geschaeftsstelle@alpenverein-heidelberg.de .
10. Die Sicherheit Ihrer Daten:
@@ -358,7 +363,7 @@ Wir bedienen uns geeigneter technischer und organisatorischer Sicherheitsmaßnah
11. Aktualität und Änderung dieser Datenschutzerklärung
-Diese Datenschutzerklärung ist aktuell gültig und hat den Stand April 2020.
+Diese Datenschutzerklärung ist aktuell gültig und hat den Stand November 2024.
diff --git a/jdav_web/startpage/templates/startpage/navigation.html b/jdav_web/startpage/templates/startpage/navigation.html
index 1d15166..74dc605 100644
--- a/jdav_web/startpage/templates/startpage/navigation.html
+++ b/jdav_web/startpage/templates/startpage/navigation.html
@@ -51,7 +51,7 @@ window.onclick = function(event) {