Merge branch 'main' into docs

pull/73/head
Christian Merten 1 year ago
commit ee84866da0
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

@ -1,5 +1,3 @@
version: "3.9"
x-kompass: x-kompass:
&kompass &kompass
image: kompass:production image: kompass:production

@ -6,6 +6,7 @@ server {
listen 80; listen 80;
server_name 127.0.0.1; server_name 127.0.0.1;
charset utf-8; charset utf-8;
error_page 502 /downtime/502.html;
location /static { location /static {
alias /var/www/jdav_web/static; alias /var/www/jdav_web/static;
@ -15,6 +16,10 @@ server {
alias /var/www/jdav_web/media; alias /var/www/jdav_web/media;
} }
location /downtime {
alias /var/www/jdav_web/static/downtime;
}
location / { location / {
uwsgi_pass uwsgi; uwsgi_pass uwsgi;
include /etc/nginx/uwsgi_params; include /etc/nginx/uwsgi_params;

@ -22,7 +22,7 @@ class FieldPermissionsAdminMixin:
for fd in field_desc: for fd in field_desc:
if fd not in self.field_view_permissions: if fd not in self.field_view_permissions:
continue 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 False
return True return True
@ -43,7 +43,7 @@ class FieldPermissionsAdminMixin:
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
readonly_fields = super(FieldPermissionsAdminMixin, self).get_readonly_fields(request, obj) readonly_fields = super(FieldPermissionsAdminMixin, self).get_readonly_fields(request, obj)
return list(readonly_fields) +\ 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: class ChangeViewAdminMixin:

@ -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, "")

@ -9,6 +9,7 @@ from django.shortcuts import render
from django.conf import settings from django.conf import settings
from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin
from utils import get_member
from rules.contrib.admin import ObjectPermissionsModelAdmin from rules.contrib.admin import ObjectPermissionsModelAdmin
@ -118,10 +119,17 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
inlines = [BillOnSubmittedStatementInline, TransactionOnSubmittedStatementInline] inlines = [BillOnSubmittedStatementInline, TransactionOnSubmittedStatementInline]
def has_add_permission(self, request, obj=None): 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 return False
def has_change_permission(self, request, obj=None): 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): def get_readonly_fields(self, request, obj=None):
readonly_fields = ['submitted'] readonly_fields = ['submitted']
@ -218,23 +226,7 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
opts=self.opts, opts=self.opts,
statement=statement, statement=statement,
transaction_issues=statement.transaction_issues, transaction_issues=statement.transaction_issues,
total_bills=statement.total_bills, **statement.template_context())
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)
return render(request, 'admin/overview_submitted_statement.html', context=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 # To preserve integrity, no one is allowed to change confirmed statements
return False 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): def get_urls(self):
urls = super().get_urls() urls = super().get_urls()
@ -308,6 +304,9 @@ class StatementConfirmedAdmin(admin.ModelAdmin):
@admin.register(Transaction) @admin.register(Transaction)
class TransactionAdmin(admin.ModelAdmin): 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', list_display = ['member', 'ledger', 'amount', 'reference', 'statement', 'confirmed',
'confirmed_date', 'confirmed_by'] 'confirmed_date', 'confirmed_by']
list_filter = ('ledger', 'member', 'statement', 'confirmed') list_filter = ('ledger', 'member', 'statement', 'confirmed')
@ -319,16 +318,21 @@ class TransactionAdmin(admin.ModelAdmin):
return self.fields return self.fields
return super(TransactionAdmin, self).get_readonly_fields(request, obj) 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) @admin.register(Bill)
class BillAdmin(admin.ModelAdmin): 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') list_filter = ('statement', 'paid_by', 'refunded')
search_fields = ('reference', 'statement') search_fields = ('reference', 'statement')
def get_member(request):
if not hasattr(request.user, 'member'):
return None
else:
return request.user.member

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,12 +18,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: finance/admin.py:75 #: finance/admin.py:76
#, python-format #, python-format
msgid "%(name)s is already submitted." msgid "%(name)s is already submitted."
msgstr "%(name)s ist bereits eingereicht." msgstr "%(name)s ist bereits eingereicht."
#: finance/admin.py:81 #: finance/admin.py:82
#, python-format #, python-format
msgid "" msgid ""
"Successfully submited %(name)s. The finance department will notify the " "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 " "Rechnung %(name)s erfolgreich eingereicht. Das Finanzreferat wird auf dich "
"sobald wie möglich zukommen." "sobald wie möglich zukommen."
#: finance/admin.py:84 #: finance/admin.py:85
msgid "Submit statement" msgid "Submit statement"
msgstr "Rechnung einreichen" msgstr "Rechnung einreichen"
#: finance/admin.py:161 #: finance/admin.py:162
#, python-format #, python-format
msgid "%(name)s is not yet submitted." msgid "%(name)s is not yet submitted."
msgstr "%(name)s ist noch nicht eingereicht." msgstr "%(name)s ist noch nicht eingereicht."
#: finance/admin.py:168 #: finance/admin.py:169
#, python-format #, python-format
msgid "An error occured while trying to confirm %(name)s. Please try again." msgid "An error occured while trying to confirm %(name)s. Please try again."
msgstr "" msgstr ""
"Beim Abwickeln von %(name)s ist ein Fehler aufgetreten. Bitte versuche es " "Beim Abwickeln von %(name)s ist ein Fehler aufgetreten. Bitte versuche es "
"erneut." "erneut."
#: finance/admin.py:172 #: finance/admin.py:173
#, python-format #, python-format
msgid "" msgid ""
"Successfully confirmed %(name)s. I hope you executed the associated " "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 " "Erfolgreich %(name)s abgewickelt. Ich hoffe du hast die zugehörigen "
"Überweisungen ausgeführt, ich werde dich nicht nochmal erinnern." "Überweisungen ausgeführt, ich werde dich nicht nochmal erinnern."
#: finance/admin.py:179 #: finance/admin.py:180
msgid "Statement confirmed" msgid "Statement confirmed"
msgstr "Abrechnung abgewickelt" msgstr "Abrechnung abgewickelt"
#: finance/admin.py:185 #: finance/admin.py:186
msgid "" msgid ""
"Transactions do not match the covered expenses. Please correct the mistakes " "Transactions do not match the covered expenses. Please correct the mistakes "
"listed below." "listed below."
@ -69,19 +69,19 @@ msgstr ""
"Überweisungen stimmen nicht mit den übernommenen Kosten überein. Bitte " "Überweisungen stimmen nicht mit den übernommenen Kosten überein. Bitte "
"korrigiere die unten aufgeführten Fehler." "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." msgid "Some transactions have no ledger configured. Please fill in the gaps."
msgstr "" msgstr ""
"Manche Überweisungen haben kein Geldtopf eingestellt. Bitte trage das nach." "Manche Überweisungen haben kein Geldtopf eingestellt. Bitte trage das nach."
#: finance/admin.py:199 #: finance/admin.py:200
#, python-format #, python-format
msgid "Successfully rejected %(name)s. The requestor can reapply, when needed." msgid "Successfully rejected %(name)s. The requestor can reapply, when needed."
msgstr "" msgstr ""
"Die Rechnung %(name)s wurde abgelehnt. Die Person kann die Rechnung erneut " "Die Rechnung %(name)s wurde abgelehnt. Die Person kann die Rechnung erneut "
"einstellen, wenn es benötigt wird." "einstellen, wenn es benötigt wird."
#: finance/admin.py:206 #: finance/admin.py:207
#, python-format #, python-format
msgid "" msgid ""
"%(name)s already has transactions. Please delete them first, if you want to " "%(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 " "%(name)s hat bereits Überweisungen. Bitte lösche diese zunächst, bevor du "
"neue generierst." "neue generierst."
#: finance/admin.py:211 #: finance/admin.py:212
#, python-format #, python-format
msgid "Successfully generated transactions for %(name)s" msgid "Successfully generated transactions for %(name)s"
msgstr "Automatisch Überweisungsträger für %(name)s generiert." msgstr "Automatisch Überweisungsträger für %(name)s generiert."
#: finance/admin.py:214 #: finance/admin.py:215
#, python-format #, python-format
msgid "" msgid ""
"Error while generating transactions for %(name)s. Do all bills have a payer?" "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 " "Fehler beim Erzeugen der Überweisungsträger für %(name)s. Sind für alle "
"Quittungen eine bezahlende Person eingestellt? " "Quittungen eine bezahlende Person eingestellt? "
#: finance/admin.py:217 #: finance/admin.py:218
msgid "View submitted statement" msgid "View submitted statement"
msgstr "Eingereichte Abrechnung einsehen" msgstr "Eingereichte Abrechnung einsehen"
#: finance/admin.py:245 #: finance/admin.py:230
#, python-format #, python-format
msgid "Successfully reduced transactions for %(name)s." msgid "Successfully reduced transactions for %(name)s."
msgstr "Überweisungsträger für %(name)s minimiert." msgstr "Überweisungsträger für %(name)s minimiert."
#: finance/admin.py:289 #: finance/admin.py:274
#, python-format #, python-format
msgid "%(name)s is not yet confirmed." msgid "%(name)s is not yet confirmed."
msgstr "%(name)s ist noch nicht bestätigt." msgstr "%(name)s ist noch nicht bestätigt."
#: finance/admin.py:298 #: finance/admin.py:283
#, python-format #, python-format
msgid "Successfully unconfirmed %(name)s. I hope you know what you are doing." msgid "Successfully unconfirmed %(name)s. I hope you know what you are doing."
msgstr "" msgstr ""
"Erfolgreich die Bestätigung von %(name)s zurückgenommen. Ich hoffe du weißt " "Erfolgreich die Bestätigung von %(name)s zurückgenommen. Ich hoffe du weißt "
"was du machst." "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" msgid "Unconfirm statement"
msgstr "Bestätigung zurücknehmen" msgstr "Bestätigung zurücknehmen"
@ -132,165 +132,165 @@ msgstr "Bestätigung zurücknehmen"
msgid "Finance" msgid "Finance"
msgstr "Finanzen" msgstr "Finanzen"
#: finance/models.py:19 #: finance/models.py:21
msgid "Name" msgid "Name"
msgstr "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/confirmed_statement.html:38
#: finance/templates/admin/overview_submitted_statement.html:100 #: finance/templates/admin/overview_submitted_statement.html:100
msgid "Ledger" msgid "Ledger"
msgstr "Geldtopf" msgstr "Geldtopf"
#: finance/models.py:26 #: finance/models.py:28
msgid "Ledgers" msgid "Ledgers"
msgstr "Geldtöpfe" 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" msgid "Short description"
msgstr "Kurzbeschreibung" msgstr "Kurzbeschreibung"
#: finance/models.py:49 finance/models.py:385 #: finance/models.py:51 finance/models.py:416
msgid "Explanation" msgid "Explanation"
msgstr "Erklärung" msgstr "Erklärung"
#: finance/models.py:51 #: finance/models.py:53
msgid "Associated excursion" msgid "Associated excursion"
msgstr "Zugehörige Ausfahrt" msgstr "Zugehörige Ausfahrt"
#: finance/models.py:56 #: finance/models.py:58
msgid "Price per night" msgid "Price per night"
msgstr "Preis pro Nacht" msgstr "Preis pro Nacht"
#: finance/models.py:58 #: finance/models.py:60
msgid "Submitted" msgid "Submitted"
msgstr "Eingericht" msgstr "Eingericht"
#: finance/models.py:59 #: finance/models.py:61
msgid "Submitted on" msgid "Submitted on"
msgstr "Eingereicht am" msgstr "Eingereicht am"
#: finance/models.py:60 #: finance/models.py:62
msgid "Confirmed" msgid "Confirmed"
msgstr "Abgewickelt" msgstr "Abgewickelt"
#: finance/models.py:61 finance/models.py:448 #: finance/models.py:63 finance/models.py:479
msgid "Paid on" msgid "Paid on"
msgstr "Bezahlt am" msgstr "Bezahlt am"
#: finance/models.py:63 #: finance/models.py:65
msgid "Created by" msgid "Created by"
msgstr "Erstellt von" msgstr "Erstellt von"
#: finance/models.py:68 #: finance/models.py:70
msgid "Submitted by" 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" msgid "Authorized by"
msgstr "Autorisiert von" 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" msgid "Statement"
msgstr "Abrechnung" msgstr "Abrechnung"
#: finance/models.py:81 #: finance/models.py:83
msgid "Statements" msgid "Statements"
msgstr "Abrechnungen" msgstr "Abrechnungen"
#: finance/models.py:96 #: finance/models.py:98
#, python-format #, python-format
msgid "Statement: %(excursion)s" msgid "Statement: %(excursion)s"
msgstr "Abrechnung: %(excursion)s" msgstr "Abrechnung: %(excursion)s"
#: finance/models.py:148 #: finance/models.py:150
msgid "Ready to confirm" msgid "Ready to confirm"
msgstr "Bereit zur Abwicklung" msgstr "Bereit zur Abwicklung"
#: finance/models.py:192 #: finance/models.py:194
#, python-format #, python-format
msgid "Compensation for %(excu)s" msgid "Compensation for %(excu)s"
msgstr "Entschädigung für %(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 #: finance/templates/admin/overview_submitted_statement.html:78
msgid "Total" msgid "Total"
msgstr "Gesamtbetrag" msgstr "Gesamtbetrag"
#: finance/models.py:338 #: finance/models.py:369
msgid "Statement in preparation" msgid "Statement in preparation"
msgstr "Abrechnung in Vorbereitung" msgstr "Abrechnung in Vorbereitung"
#: finance/models.py:339 #: finance/models.py:370
msgid "Statements in preparation" msgid "Statements in preparation"
msgstr "Abrechnungen in Vorbereitung" msgstr "Abrechnungen in Vorbereitung"
#: finance/models.py:358 #: finance/models.py:389
msgid "Submitted statement" msgid "Submitted statement"
msgstr "Eingereichte Abrechnung" msgstr "Eingereichte Abrechnung"
#: finance/models.py:359 #: finance/models.py:390
msgid "Submitted statements" msgid "Submitted statements"
msgstr "Eingereichte Abrechnungen" msgstr "Eingereichte Abrechnungen"
#: finance/models.py:375 #: finance/models.py:406
msgid "Paid statement" msgid "Paid statement"
msgstr "Bezahlte Abrechnung" msgstr "Bezahlte Abrechnung"
#: finance/models.py:376 #: finance/models.py:407
msgid "Paid statements" msgid "Paid statements"
msgstr "Bezahlte Abrechnungen" 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" msgid "Paid by"
msgstr "Bezahlt von" msgstr "Bezahlt von"
#: finance/models.py:390 #: finance/models.py:421
msgid "Covered" msgid "Covered"
msgstr "Übernommen" msgstr "Übernommen"
#: finance/models.py:391 #: finance/models.py:422
msgid "Refunded" msgid "Refunded"
msgstr "Ausgezahlt" msgstr "Ausgezahlt"
#: finance/models.py:393 #: finance/models.py:424
msgid "Proof" msgid "Proof"
msgstr "Beleg" msgstr "Beleg"
#: finance/models.py:401 finance/models.py:438 #: finance/models.py:435 finance/models.py:442 finance/models.py:455
#: 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
msgid "Bill" 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 #: finance/templates/admin/overview_submitted_statement.html:26
msgid "Bills" 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 #: finance/templates/admin/overview_submitted_statement.html:99
msgid "Reference" msgid "Reference"
msgstr "Verwendungszweck" msgstr "Verwendungszweck"
#: finance/models.py:439 #: finance/models.py:470
msgid "Recipient" msgid "Recipient"
msgstr "Empfänger" msgstr "Empfänger"
#: finance/models.py:447 #: finance/models.py:478
msgid "Paid" msgid "Paid"
msgstr "Bezahlt" msgstr "Bezahlt"
#: finance/models.py:459 #: finance/models.py:490
msgid "Transaction" msgid "Transaction"
msgstr "Überweisung" msgstr "Überweisung"
#: finance/models.py:460 #: finance/models.py:491
#: finance/templates/admin/overview_submitted_statement.html:84 #: finance/templates/admin/overview_submitted_statement.html:84
msgid "Transactions" msgid "Transactions"
msgstr "Überweisungen" msgstr "Überweisungen"
@ -351,7 +351,7 @@ msgstr "Ausfahrt"
#, python-format #, python-format
msgid "This excursion featured %(staff_count)s youth leader(s), each costing" msgid "This excursion featured %(staff_count)s youth leader(s), each costing"
msgstr "" 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:" "entfallen die folgenden Kosten:"
#: finance/templates/admin/overview_submitted_statement.html:62 #: finance/templates/admin/overview_submitted_statement.html:62

@ -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'),
),
]

@ -6,12 +6,14 @@ from .rules import is_creator, not_submitted, leads_excursion
from members.rules import is_leader, statement_not_submitted from members.rules import is_leader, statement_not_submitted
from django.db import models from django.db import models
from django.db.models import Sum
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from members.models import Member, Freizeit, OEFFENTLICHE_ANREISE, MUSKELKRAFT_ANREISE from members.models import Member, Freizeit, OEFFENTLICHE_ANREISE, MUSKELKRAFT_ANREISE
from django.conf import settings from django.conf import settings
import rules import rules
from contrib.models import CommonModel from contrib.models import CommonModel
from contrib.rules import has_global_perm from contrib.rules import has_global_perm
from utils import cvt_to_decimal
# Create your models here. # Create your models here.
@ -186,7 +188,7 @@ class Statement(CommonModel):
# excursion specific # excursion specific
if self.excursion is None: if self.excursion is None:
return return True
for yl in self.excursion.jugendleiter.all(): for yl in self.excursion.jugendleiter.all():
ref = _("Compensation for %(excu)s") % {'excu': self.excursion.name} ref = _("Compensation for %(excu)s") % {'excu': self.excursion.name}
@ -283,7 +285,7 @@ class Statement(CommonModel):
if self.excursion is None: if self.excursion is None:
return 0 return 0
return self.total_staff / self.excursion.staff_count return cvt_to_decimal(self.total_staff / self.excursion.staff_count)
@property @property
def total_staff(self): def total_staff(self):
@ -324,6 +326,37 @@ class Statement(CommonModel):
return "{}".format(self.total) return "{}".format(self.total)
total_pretty.short_description = _('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): class StatementUnSubmittedManager(models.Manager):
def get_queryset(self): def get_queryset(self):
@ -384,7 +417,7 @@ class Bill(CommonModel):
short_description = models.CharField(verbose_name=_('Short description'), max_length=30) short_description = models.CharField(verbose_name=_('Short description'), max_length=30)
explanation = models.TextField(verbose_name=_('Explanation'), blank=True) 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, paid_by = models.ForeignKey(Member, verbose_name=_('Paid by'), null=True,
on_delete=models.SET_NULL) on_delete=models.SET_NULL)
costs_covered = models.BooleanField(verbose_name=_('Covered'), default=False) costs_covered = models.BooleanField(verbose_name=_('Covered'), default=False)
@ -466,7 +499,3 @@ class Receipt(models.Model):
on_delete=models.CASCADE) on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=6, decimal_places=2) amount = models.DecimalField(max_digits=6, decimal_places=2)
comments = models.TextField() comments = models.TextField()
def cvt_to_decimal(f):
return Decimal(f).quantize(Decimal('.01'), rounding=ROUND_HALF_DOWN)

@ -14,3 +14,4 @@ CELERY_EMAIL_TASK_CONFIG = {
} }
DEFAULT_SENDING_MAIL = os.environ.get('EMAIL_SENDING_ADDRESS', 'django@localhost') DEFAULT_SENDING_MAIL = os.environ.get('EMAIL_SENDING_ADDRESS', 'django@localhost')
DEFAULT_SENDING_NAME = os.environ.get('EMAIL_SENDING_NAME', 'Kompass')

@ -22,12 +22,19 @@ der Registrierung kommst du hier:
Viele Grüße Viele Grüße
Dein KOMPASS""" 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}, INVITE_TEXT = """Hallo {name},
wir haben gute Neuigkeiten für dich. Es ist ein Platz in der Jugendgruppe {group_name} {group_link}freigeworden. 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 (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: 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}, 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 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 dort bereits hinterlegt. Damit du dich auch anmelden kannst, folge bitte dem folgenden Link
und wähle ein Passwort. und wähle ein Passwort.
@ -155,3 +162,8 @@ Bei Fragen, wende dich gerne an %(RESPONSIBLE_MAIL)s.
Viele Grüße Viele Grüße
Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION, 'RESPONSIBLE_MAIL': RESPONSIBLE_MAIL } 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 }

@ -8,21 +8,41 @@ SEKTION_TELEFAX = "06221 437338"
SEKTION_CONTACT_MAIL = "geschaeftsstelle@alpenverein-heidelberg.de" SEKTION_CONTACT_MAIL = "geschaeftsstelle@alpenverein-heidelberg.de"
SEKTION_BOARD_MAIL = "vorstand@alpenverein-heidelberg.de" SEKTION_BOARD_MAIL = "vorstand@alpenverein-heidelberg.de"
SEKTION_CRISIS_INTERVENTION_MAIL = "krisenmanagement@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" 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
ECHO_PASSWORD_BIRTHDATE_FORMAT = '%d.%m.%Y' ECHO_PASSWORD_BIRTHDATE_FORMAT = '%d.%m.%Y'
ECHO_GRACE_PERIOD = 30
# misc # misc
CONGRATULATE_MEMBERS_MAX = 10 CONGRATULATE_MEMBERS_MAX = 10
MAX_AGE_GOOD_CONDUCT_CERTIFICATE_MONTHS = 24 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 # finance
ALLOWANCE_PER_DAY = 10 ALLOWANCE_PER_DAY = 22
MAX_NIGHT_COST = 11 MAX_NIGHT_COST = 11
CLOUD_LINK = 'https://nc.cloud-jdav-hd.de' 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' 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' STARTPAGE_REDIRECT_URL = 'https://jdav-hd.de'
ROOT_SECTION = 'jdav_heidelberg' ROOT_SECTION = os.environ.get('ROOT_SECTION', 'wir')
RECENT_SECTION = 'aktuelles' RECENT_SECTION = 'aktuelles'
REPORTS_SECTION = 'berichte' REPORTS_SECTION = 'berichte'

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -67,28 +67,28 @@ msgstr "Aktives Registrierungspasswort"
msgid "Active registration passwords" msgid "Active registration passwords"
msgstr "Aktive Registrierungspasswörter" msgstr "Aktive Registrierungspasswörter"
#: logindata/templates/logindata/register_failed.html:6 #: logindata/templates/logindata/register_failed.html:5
msgid "Registration" msgid "Registration"
msgstr "Registrierung" 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_form.html:13
#: logindata/templates/logindata/register_password.html:11 #: logindata/templates/logindata/register_password.html:11
#: logindata/templates/logindata/register_success.html:10 #: logindata/templates/logindata/register_success.html:10
msgid "Set login data" msgid "Set login data"
msgstr "Zugangsdaten wählen" 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." msgid "Something went wrong. The registration key is invalid or has expired."
msgstr "" msgstr ""
"Etwas ist schief gegangen. Der Registrierungscode ist ungültig oder ist " "Etwas ist schief gegangen. Der Registrierungscode ist ungültig oder ist "
"abgelaufen." "abgelaufen."
#: logindata/templates/logindata/register_failed.html:15 #: logindata/templates/logindata/register_failed.html:14
msgid "If you think this is a mistake, please" msgid "If you think this is a mistake, please"
msgstr "Falls du denkst, dass das ein Fehler ist, bitte" 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." msgid "contact us."
msgstr "kontaktiere uns." msgstr "kontaktiere uns."
@ -142,9 +142,9 @@ msgid ""
"related objects, but your account doesn't have permission to delete the " "related objects, but your account doesn't have permission to delete the "
"following types of objects:" "following types of objects:"
msgstr "" msgstr ""
"Löschen von %(object_name)s '%(escaped_object)s' würde zur Löschung der folgenden " "Löschen von %(object_name)s '%(escaped_object)s' würde zur Löschung der "
"verknüpften Objekte führen, aber du hast nicht die Berechtigung die folgenden Typen " "folgenden verknüpften Objekte führen, aber du hast nicht die Berechtigung "
"von Objekten zu löschen:" "die folgenden Typen von Objekten zu löschen:"
#: templates/admin/delete_confirmation.html:12 #: templates/admin/delete_confirmation.html:12
#, python-format #, python-format
@ -152,16 +152,16 @@ msgid ""
"Deleting the %(object_name)s '%(escaped_object)s' would require deleting the " "Deleting the %(object_name)s '%(escaped_object)s' would require deleting the "
"following protected related objects:" "following protected related objects:"
msgstr "" msgstr ""
"Löschen von %(object_name)s '%(escaped_object)s' würde zur Löschung der folgenden " "Löschen von %(object_name)s '%(escaped_object)s' würde zur Löschung der "
"geschützten verknüpften Objekte führen:" "folgenden geschützten verknüpften Objekte führen:"
#: templates/admin/delete_confirmation.html:17 #: templates/admin/delete_confirmation.html:17
#, python-format #, python-format
msgid "" msgid ""
"Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"?" "Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"?"
msgstr "" msgstr ""
"Bist du sicher, dass du %(object_name)s \"%(escaped_object)s\" und alle davon abhängigen " "Bist du sicher, dass du %(object_name)s \"%(escaped_object)s\" und alle "
"Objekte löschen möchtest? " "davon abhängigen Objekte löschen möchtest? "
#: templates/admin/delete_confirmation.html:29 #: templates/admin/delete_confirmation.html:29
#: templates/admin/delete_selected_confirmation.html:34 #: templates/admin/delete_selected_confirmation.html:34
@ -181,8 +181,8 @@ msgid ""
"types of objects:" "types of objects:"
msgstr "" msgstr ""
"Löschen der ausgewählten %(objects_name)s würde zur Löschung der folgenden " "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 " "verknüpften Objekte führen, aber du hast nicht die Berechtigung die "
"von Objekten zu löschen:" "folgenden Typen von Objekten zu löschen:"
#: templates/admin/delete_selected_confirmation.html:9 #: templates/admin/delete_selected_confirmation.html:9
#, python-format #, python-format
@ -210,6 +210,28 @@ msgstr "Zusammenfassung"
msgid "Objects" msgid "Objects"
msgstr "Objekte" 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 #: templates/admin/finance/statementconfirmed/change_form_object_tools.html:8
msgid "Unconfirm" msgid "Unconfirm"
msgstr "Bestätigung zurücknehmen" msgstr "Bestätigung zurücknehmen"
@ -235,34 +257,46 @@ msgid "Generate SJR application"
msgstr "SJR Antrag erstellen" msgstr "SJR Antrag erstellen"
#: templates/admin/members/freizeit/change_form_object_tools.html:23 #: templates/admin/members/freizeit/change_form_object_tools.html:23
msgid "Generate overview" msgid "Generate seminar report"
msgstr "Übersicht erstellen" msgstr "Landesjugendplan Antrag erstellen"
#: templates/admin/members/freizeit/change_form_object_tools.html:30 #: templates/admin/members/freizeit/change_form_object_tools.html:30
msgid "Generate seminar report" msgid "Generate overview"
msgstr "Seminarbericht erstellen" msgstr "Hinweise für Jugendleiter*innen erstellen"
#: templates/admin/members/freizeit/change_form_object_tools.html:36 #: templates/admin/members/freizeit/change_form_object_tools.html:38
msgid "Submit statement" msgid "Finance overview"
msgstr "Abrechnung einreichen" msgstr "Kostenübersicht"
#: templates/admin/members/member/change_form_object_tools.html:8 #: templates/admin/members/member/change_form_object_tools.html:8
msgid "Invite as user" 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/change_form_object_tools.html:8
#: templates/admin/members/memberwaitinglist/submit_line.html:9 #: templates/admin/members/memberwaitinglist/submit_line.html:9
msgid "Invite to group" msgid "Invite to group"
msgstr "Zu Gruppe einladen" 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." msgid "Please keep filesize under {} MiB. Current filesize: {:10.2f} MiB."
msgstr "Maximale Dateigröße {} MiB. Aktuelle Dateigröße: {:10.2f} MiB." msgstr "Maximale Dateigröße {} MiB. Aktuelle Dateigröße: {:10.2f} MiB."
#: utils.py:42 #: utils.py:43
msgid "Filetype not supported." msgid "Filetype not supported."
msgstr "Dateityp nicht unterstützt." msgstr "Dateityp nicht unterstützt."
#: utils.py:44 #: utils.py:45
msgid "Please keep filesize under {}. Current filesize: {}" msgid "Please keep filesize under {}. Current filesize: {}"
msgstr "Maximale Dateigröße {}. Aktuelle Dateigröße: {}." msgstr "Maximale Dateigröße {}. Aktuelle Dateigröße: {}."
#~ msgid "Submit statement"
#~ msgstr "Abrechnung einreichen"

@ -1,6 +1,5 @@
{% extends "members/base.html" %} {% extends "members/base.html" %}
{% load i18n %} {% load i18n static common %}
{% load static %}
{% block title %} {% block title %}
{% trans "Registration" %} {% trans "Registration" %}
@ -12,6 +11,6 @@
<p>{% trans "Something went wrong. The registration key is invalid or has expired." %}</p> <p>{% trans "Something went wrong. The registration key is invalid or has expired." %}</p>
<p>{% trans "If you think this is a mistake, please" %} <a href="mailto:jugendreferent@jdav-ludwigsburg.de">{% trans "contact us." %}</a></p> <p>{% trans "If you think this is a mistake, please" %} <a href="mailto:{% settings_value 'RESPONSIBLE_MAIL' %}">{% trans "contact us." %}</a></p>
{% endblock %} {% endblock %}

@ -22,7 +22,8 @@ class AttachmentInline(CommonAdminInlineMixin, admin.TabularInline):
class EmailAddressAdmin(FilteredMemberFieldMixin, admin.ModelAdmin): class EmailAddressAdmin(FilteredMemberFieldMixin, admin.ModelAdmin):
list_display = ('email', ) list_display = ('email', 'internal_only')
fields = ('name', 'to_members', 'to_groups', 'internal_only')
#formfield_overrides = { #formfield_overrides = {
# models.ManyToManyField: {'widget': forms.CheckboxSelectMultiple}, # models.ManyToManyField: {'widget': forms.CheckboxSelectMultiple},
# models.ForeignKey: {'widget': apply_select2(forms.Select)} # models.ForeignKey: {'widget': apply_select2(forms.Select)}
@ -33,9 +34,10 @@ class EmailAddressAdmin(FilteredMemberFieldMixin, admin.ModelAdmin):
class MessageAdmin(FilteredMemberFieldMixin, CommonAdminMixin, ObjectPermissionsModelAdmin): class MessageAdmin(FilteredMemberFieldMixin, CommonAdminMixin, ObjectPermissionsModelAdmin):
"""Message creation view""" """Message creation view"""
exclude = ('created_by',) exclude = ('created_by', 'to_notelist')
list_display = ('subject', 'get_recipients', 'sent') list_display = ('subject', 'get_recipients', 'sent')
search_fields = ('subject',) search_fields = ('subject',)
list_filter = ('sent',)
change_form_template = "mailer/change_form.html" change_form_template = "mailer/change_form.html"
readonly_fields = ('sent',) readonly_fields = ('sent',)
#formfield_overrides = { #formfield_overrides = {
@ -90,8 +92,13 @@ class MessageAdmin(FilteredMemberFieldMixin, CommonAdminMixin, ObjectPermissions
def submit_message(msg, request): def submit_message(msg, request):
sender = None sender = None
if hasattr(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 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) success = msg.submit(sender)
if success == NOT_SENT: if success == NOT_SENT:
messages.error(request, _("Failed to send message")) messages.error(request, _("Failed to send message"))

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,19 +18,35 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: mailer/admin.py:67 #: mailer/admin.py:69
msgid "Send message" msgid "Send message"
msgstr "Nachricht verschicken" 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" msgid "Failed to send message"
msgstr "Fehler beim Senden der Email" msgstr "Fehler beim Senden der Email"
#: mailer/admin.py:99 #: mailer/admin.py:106
msgid "Failed to send some messages" msgid "Failed to send some messages"
msgstr "Fehler beim Senden der Emails" msgstr "Fehler beim Senden der Emails"
#: mailer/admin.py:101 #: mailer/admin.py:108
msgid "Successfully sent message" msgid "Successfully sent message"
msgstr "Email wurde erfolgreich verschickt" msgstr "Email wurde erfolgreich verschickt"
@ -43,116 +59,138 @@ msgstr "Verteiler"
msgid "Congratulation %(name)s" msgid "Congratulation %(name)s"
msgstr "Herzlichen Glückwunsch %(name)s" msgstr "Herzlichen Glückwunsch %(name)s"
#: mailer/models.py:19 #: mailer/models.py:20
msgid "Only alphanumeric characters, ., - and _ are allowed" msgid "Only alphanumeric characters, ., - and _ are allowed"
msgstr "Nur Buchstaben, Zahlen, ., . und _ sind erlaubt" msgstr "Nur Buchstaben, Zahlen, ., . und _ sind erlaubt"
#: mailer/models.py:24 #: mailer/models.py:25
msgid "name" msgid "name"
msgstr "Name" msgstr "Name"
#: mailer/models.py:26 #: mailer/models.py:27
msgid "Forward to participants" 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" msgid "Forward to group"
msgstr "Weiterleitung an Gruppe" 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" msgid "email address"
msgstr "Email-Adresse" msgstr "Email-Adresse"
#: mailer/models.py:47 #: mailer/models.py:56
msgid "email addresses" msgid "email addresses"
msgstr "Email-Adressen" 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." msgid "Either a group or at least one member is required as forward recipient."
msgstr "" msgstr ""
"Es muss entweder eine Gruppe oder mindestens ein Teilnehmer als Empfänger " "Es muss entweder eine Gruppe oder mindestens ein*e Teilnehmer*in als "
"ausgewählt werden." "Empfänger*in ausgewählt werden."
#: mailer/models.py:68 #: mailer/models.py:77
msgid "subject" msgid "subject"
msgstr "Betreff" msgstr "Betreff"
#: mailer/models.py:69 #: mailer/models.py:78
msgid "content" msgid "content"
msgstr "Inhalt" msgstr "Inhalt"
#: mailer/models.py:71 #: mailer/models.py:80
msgid "to group" msgid "to group"
msgstr "An Gruppe" msgstr "An Gruppe"
#: mailer/models.py:74 #: mailer/models.py:83
msgid "to freizeit" msgid "to freizeit"
msgstr "An Ausfahrt" msgstr "An Ausfahrt"
#: mailer/models.py:79 #: mailer/models.py:88
msgid "to notes list" msgid "to notes list"
msgstr "An Notizliste" msgstr "An Notizliste"
#: mailer/models.py:84 #: mailer/models.py:93
msgid "to member" msgid "to member"
msgstr "An Teilnehmer" msgstr "An Teilnehmer*innen"
#: mailer/models.py:87 #: mailer/models.py:96
msgid "reply to participant" 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" msgid "reply to custom email address"
msgstr "Antwort an Email-Adresse" msgstr "Antwort an Email-Adresse"
#: mailer/models.py:94 #: mailer/models.py:103
msgid "sent" msgid "sent"
msgstr "Gesendet" msgstr "Gesendet"
#: mailer/models.py:95 #: mailer/models.py:104
msgid "Created by" msgid "Created by"
msgstr "Erstellt von" msgstr "Erstellt von"
#: mailer/models.py:113 #: mailer/models.py:122
msgid "Some other members" msgid "Some other members"
msgstr "Andere Teilnehmer" msgstr "Andere Teilnehmer*innen"
#: mailer/models.py:115 #: mailer/models.py:124
msgid "recipients" msgid "recipients"
msgstr "Empfänger" msgstr "Empfänger"
#: mailer/models.py:178 #: mailer/models.py:196
msgid "message" msgid "message"
msgstr "Nachricht" msgstr "Nachricht"
#: mailer/models.py:179 #: mailer/models.py:197
msgid "messages" msgid "messages"
msgstr "Nachrichten" msgstr "Nachrichten"
#: mailer/models.py:181 #: mailer/models.py:199
msgid "Can submit mails" msgid "Can submit mails"
msgstr "Kann Mails verschicken" msgstr "Kann Mails verschicken"
#: mailer/models.py:202 #: mailer/models.py:220
msgid "" msgid ""
"Either a group, a memberlist or at least one member is required as recipient" "Either a group, a memberlist or at least one member is required as recipient"
msgstr "" msgstr ""
"Es muss entweder eine Gruppe, eine Teilnehmerliste oder mindestens ein " "Es muss entweder eine Gruppe, eine Teilnehmer*innenliste oder mindestens "
"Teilnehmer als Empfänger ausgewählt werden." "ein*e Teilnehmer*in als Empfänger*in ausgewählt werden."
#: mailer/models.py:209 #: mailer/models.py:227
msgid "file" msgid "file"
msgstr "Datei" msgstr "Datei"
#: mailer/models.py:214 #: mailer/models.py:232
msgid "Empty" msgid "Empty"
msgstr "Leer" msgstr "Leer"
#: mailer/models.py:217 #: mailer/models.py:235
msgid "attachment" msgid "attachment"
msgstr "Anhang" msgstr "Anhang"
#: mailer/models.py:218 #: mailer/models.py:236
msgid "attachments" msgid "attachments"
msgstr "Anhänge" msgstr "Anhänge"

@ -7,14 +7,20 @@ import os
NOT_SENT, SENT, PARTLY_SENT = 0, 1, 2 NOT_SENT, SENT, PARTLY_SENT = 0, 1, 2
def send(subject, content, sender, recipients, message_id=None, reply_to=None, def send(subject, content, sender, recipients, message_id=None, reply_to=None,
attachments=None): attachments=None, cc=None):
failed, succeeded = False, False failed, succeeded = False, False
if type(recipients) != list: if type(recipients) != list:
recipients = [recipients] recipients = [recipients]
if not cc:
cc = []
elif type(cc) != list:
cc = [cc]
if reply_to is not None: if reply_to is not None:
kwargs = {"reply_to": reply_to} kwargs = {"reply_to": reply_to}
else: else:
kwargs = {} 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") url = prepend_base_url("/newsletter/unsubscribe")
headers = {'List-Unsubscribe': '<{unsubscribe_url}>'.format(unsubscribe_url=url)} headers = {'List-Unsubscribe': '<{unsubscribe_url}>'.format(unsubscribe_url=url)}
if message_id is not None: if message_id is not None:
@ -23,7 +29,7 @@ def send(subject, content, sender, recipients, message_id=None, reply_to=None,
# construct mails # construct mails
mails = [] mails = []
for recipient in set(recipients): for recipient in set(recipients):
email = EmailMessage(subject, content, sender, [recipient], email = EmailMessage(subject, content, sender, [recipient], cc=cc,
headers=headers, **kwargs) headers=headers, **kwargs)
if attachments is not None: if attachments is not None:
for attach in attachments: 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): def get_content(content, registration_complete=True):
url = prepend_base_url("/newsletter/unsubscribe") url = prepend_base_url("/newsletter/unsubscribe")
prepend = settings.PREPEND_INCOMPLETE_REGISTRATION_TEXT prepend = settings.PREPEND_INCOMPLETE_REGISTRATION_TEXT
footer = settings.MAIL_FOOTER.format(link=url) text = "{prepend}{content}".format(prepend="" if registration_complete else prepend,
text = "{prepend}{content}{footer}".format(prepend="" if registration_complete else prepend, content=content)
content=content,
footer=footer)
return text return text
@ -87,3 +91,7 @@ def get_invite_as_user_key(key):
def prepend_base_url(absolutelink): def prepend_base_url(absolutelink):
return "{protocol}://{base}{link}".format(protocol=settings.PROTOCOL, base=settings.BASE_URL, link=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)

@ -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'),
),
]

@ -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'),
),
]

@ -3,7 +3,8 @@ from django.core.exceptions import ValidationError
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext 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 utils import RestrictedFileField
from jdav_web.celery import app from jdav_web.celery import app
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
@ -28,6 +29,14 @@ class EmailAddress(models.Model):
to_groups = models.ManyToManyField('members.Group', to_groups = models.ManyToManyField('members.Group',
verbose_name=_('Forward to group'), verbose_name=_('Forward to group'),
blank=True) 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 @property
def email(self): def email(self):
@ -149,10 +158,19 @@ class Message(CommonModel):
reply_to = [jl.association_email for jl in self.reply_to.all()] 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()]) reply_to.extend([ml.email for ml in self.reply_to_email_address.all()])
# set correct from address # 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: 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: 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: try:
success = send(self.subject, get_content(self.content, registration_complete=True), success = send(self.subject, get_content(self.content, registration_complete=True),
from_addr, from_addr,

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"

@ -23,10 +23,10 @@ from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.db.models import TextField, ManyToManyField, ForeignKey, Count,\ from django.db.models import TextField, ManyToManyField, ForeignKey, Count,\
Sum, Case, Q, F, When, Value, IntegerField, Subquery, OuterRef 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.shortcuts import render
from django.core.exceptions import PermissionDenied 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 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 finance.models import Statement, BillOnExcursionProxy
from mailer.mailutils import send as send_mail, get_echo_link from mailer.mailutils import send as send_mail, get_echo_link
from django.conf import settings from django.conf import settings
from utils import get_member
#from easy_select2 import apply_select2 #from easy_select2 import apply_select2
@ -112,8 +113,7 @@ class EmergencyContactInline(CommonAdminInlineMixin, admin.TabularInline):
formfield_overrides = { formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
} }
fields = ['prename', 'lastname', 'email', 'phone_number', 'confirmed_mail'] fields = ['prename', 'lastname', 'email', 'phone_number']
readonly_fields = ['confirmed_mail']
extra = 0 extra = 0
@ -170,6 +170,7 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
('email', 'alternative_email'), ('email', 'alternative_email'),
'phone_number', 'phone_number',
'birth_date', 'birth_date',
'gender',
'group', 'registration_form', 'image', 'group', 'registration_form', 'image',
('join_date', 'leave_date'), ('join_date', 'leave_date'),
'comments', 'comments',
@ -232,7 +233,7 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
field_change_permissions = { field_change_permissions = {
'user': 'members.may_set_auth_user', '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', 'good_conduct_certificate_presented_date': 'members.may_change_organizationals',
'has_key': 'members.may_change_organizationals', 'has_key': 'members.may_change_organizationals',
'has_free_ticket_gym': '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') request_echo.short_description = _('Request echo from selected members')
def invite_as_user(self, request, queryset): def invite_as_user(self, request, queryset):
failures = []
for member in queryset: for member in queryset:
member.invite_as_user() success = member.invite_as_user()
if queryset.count() == 1: 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}) 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.')) 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): def has_may_invite_as_user_permission(self, request):
return request.user.has_perm('%s.%s' % (self.opts.app_label, 'may_invite_as_user')) 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)}) _("%(name)s already has login data.") % {'name': str(m)})
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name),
args=(object_id,))) 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: if "apply" in request.POST:
self.invite_as_user(request, Member.objects.filter(pk=object_id)) 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), 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' 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 = [ fieldsets = [
(None, (None,
{ {
@ -382,6 +399,7 @@ class MemberUnconfirmedAdmin(admin.ModelAdmin):
('email', 'alternative_email'), ('email', 'alternative_email'),
'phone_number', 'phone_number',
'birth_date', 'birth_date',
'gender',
'group', 'registration_form', 'image', 'group', 'registration_form', 'image',
('join_date', 'leave_date'), ('join_date', 'leave_date'),
'comments', 'comments',
@ -421,10 +439,24 @@ class MemberUnconfirmedAdmin(admin.ModelAdmin):
list_filter = ('group', 'confirmed_mail', 'confirmed_alternative_mail') list_filter = ('group', 'confirmed_mail', 'confirmed_alternative_mail')
readonly_fields = ['confirmed_mail', 'confirmed_alternative_mail', readonly_fields = ['confirmed_mail', 'confirmed_alternative_mail',
'good_conduct_certificate_valid'] 'good_conduct_certificate_valid']
actions = ['request_mail_confirmation', 'confirm', 'demote_to_waiter'] actions = ['request_mail_confirmation', 'confirm', 'demote_to_waiter_action']
inlines = [EmergencyContactInline] inlines = [EmergencyContactInline]
change_form_template = "members/change_member_unconfirmed.html" 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): def has_add_permission(self, request, obj=None):
return False return False
@ -463,22 +495,62 @@ class MemberUnconfirmedAdmin(admin.ModelAdmin):
messages.error(request, _("Failed to confirm some registrations because of unconfirmed email addresses.")) messages.error(request, _("Failed to confirm some registrations because of unconfirmed email addresses."))
confirm.short_description = _('Confirm selected registrations') 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(
"<path:object_id>/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): def demote_to_waiter(self, request, queryset):
for member in 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, waiter = MemberWaitingList(prename=member.prename,
lastname=member.lastname, lastname=member.lastname,
email=member.email, email=member.email,
birth_date=member.birth_date, birth_date=member.birth_date,
gender=member.gender,
comments=member.comments, comments=member.comments,
confirmed_mail=member.confirmed_mail, confirmed_mail=member.confirmed_mail,
confirm_mail_key=member.confirm_mail_key) confirm_mail_key=member.confirm_mail_key)
waiter.save() waiter.save()
member.delete() member.delete()
messages.success(request, _("Successfully demoted %(name)s to waiter.") % {'name': waiter.name}) 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): def response_change(self, request, member):
if "_confirm" in request.POST: if "_confirm" in request.POST:
@ -496,7 +568,7 @@ class WaiterInviteForm(forms.Form):
label=_('Group')) label=_('Group'))
class InvitationToGroupAdmin(CommonAdminInlineMixin, admin.TabularInline): class InvitationToGroupAdmin(admin.TabularInline):
model = InvitationToGroup model = InvitationToGroup
fields = ['group', 'date', 'status'] fields = ['group', 'date', 'status']
readonly_fields = ['group', 'date', 'status'] readonly_fields = ['group', 'date', 'status']
@ -539,6 +611,10 @@ class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin):
messages.error(request, messages.error(request,
_("An error occurred while trying to invite said members. Please try again.")) _("An error occurred while trying to invite said members. Please try again."))
return HttpResponseRedirect(request.get_full_path()) 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: for waiter in queryset:
waiter.invited_for_group = group 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.")) _("An error occurred while trying to invite said members. Please try again."))
return HttpResponseRedirect(request.get_full_path()) 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.invited_for_group = group
waiter.save() waiter.save()
waiter.invite_to_group(group) waiter.invite_to_group(group)
@ -634,7 +715,7 @@ class GroupAdminForm(forms.ModelForm):
class GroupAdmin(CommonAdminMixin, admin.ModelAdmin): 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')] 'weekday', ('start_time', 'end_time')]
form = GroupAdminForm form = GroupAdminForm
list_display = ('name', 'year_from', 'year_to') list_display = ('name', 'year_from', 'year_to')
@ -681,6 +762,7 @@ class BillOnExcursionInline(CommonAdminInlineMixin, admin.TabularInline):
class StatementOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline): class StatementOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline):
model = Statement model = Statement
extra = 1 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 = [] sortable_options = []
fields = ['night_cost'] fields = ['night_cost']
inlines = [BillOnExcursionInline] inlines = [BillOnExcursionInline]
@ -698,6 +780,7 @@ class InterventionOnLJPInline(CommonAdminInlineMixin, admin.TabularInline):
class LJPOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline): class LJPOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline):
model = LJPProposal model = LJPProposal
extra = 1 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 = [] sortable_options = []
inlines = [InterventionOnLJPInline] inlines = [InterventionOnLJPInline]
@ -705,6 +788,7 @@ class LJPOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline):
class MemberOnListInline(CommonAdminInlineMixin, GenericTabularInline): class MemberOnListInline(CommonAdminInlineMixin, GenericTabularInline):
model = NewMemberOnList model = NewMemberOnList
extra = 0 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 = { formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
} }
@ -767,6 +851,9 @@ class GenerateSeminarReportForm(forms.Form):
modes = (('full', _('Full report')), modes = (('full', _('Full report')),
('basic', _('Costs and participants only'))) ('basic', _('Costs and participants only')))
mode = forms.ChoiceField(choices=modes, label=_('Mode')) 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): class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
@ -776,6 +863,13 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
search_fields = ('name',) search_fields = ('name',)
ordering = ('-date',) ordering = ('-date',)
view_on_site = False 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 = { #formfield_overrides = {
# ManyToManyField: {'widget': forms.CheckboxSelectMultiple}, # ManyToManyField: {'widget': forms.CheckboxSelectMultiple},
# ForeignKey: {'widget': apply_select2(forms.Select)} # ForeignKey: {'widget': apply_select2(forms.Select)}
@ -839,12 +933,21 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
messages.error(request, _('Please select a mode.')) messages.error(request, _('Please select a mode.'))
return self.render_seminar_report_options(request, memberlist, form) return self.render_seminar_report_options(request, memberlist, form)
mode = form.cleaned_data['mode'] mode = form.cleaned_data['mode']
prepend_v32 = form.cleaned_data['prepend_v32']
if mode == 'full' and not hasattr(memberlist, 'ljpproposal'): if mode == 'full' and not hasattr(memberlist, 'ljpproposal'):
messages.error(request, _('Full mode is only available, if the seminar report section is filled out.')) 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) 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 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()) return self.render_seminar_report_options(request, memberlist, GenerateSeminarReportForm())
seminar_report.short_description = _('Generate seminar report') 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) return fill_pdf_form(title + "_SJR_Antrag", 'members/sjr_template.pdf', context, attachments)
sjr_application.short_description = _('Generate SJR application') 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): def get_urls(self):
urls = super().get_urls() 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)) return self.notes_list(request, Freizeit.objects.get(pk=object_id))
if "crisis_intervention_list" in request.POST: if "crisis_intervention_list" in request.POST:
return self.crisis_intervention_list(request, Freizeit.objects.get(pk=object_id)) 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), return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name),
args=(object_id,))) args=(object_id,)))

File diff suppressed because it is too large Load Diff

@ -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),
),
]

@ -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'),
),
]

@ -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'),
),
]

@ -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'),
),
]

@ -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'},
),
]

@ -1,6 +1,7 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import uuid import uuid
import pytz import pytz
import unicodedata
import re import re
import csv import csv
from django.db import models 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 import rules
from contrib.models import CommonModel from contrib.models import CommonModel
from contrib.rules import memberize_user, has_global_perm from contrib.rules import memberize_user, has_global_perm
from utils import cvt_to_decimal
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
@ -73,9 +75,14 @@ class Group(models.Model):
year_to = models.IntegerField(verbose_name=_('highest year'), default=2011) year_to = models.IntegerField(verbose_name=_('highest year'), default=2011)
leiters = models.ManyToManyField('members.Member', verbose_name=_('youth leaders'), leiters = models.ManyToManyField('members.Member', verbose_name=_('youth leaders'),
related_name='leited_groups', blank=True) 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) start_time = models.TimeField(verbose_name=_('Starting time'), null=True, blank=True)
end_time = models.TimeField(verbose_name=_('Ending 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): def __str__(self):
"""String representation""" """String representation"""
@ -85,6 +92,10 @@ class Group(models.Model):
verbose_name = _('group') verbose_name = _('group')
verbose_name_plural = _('groups') 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): class MemberManager(models.Manager):
def get_queryset(self): 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: for email_fd, confirmed_email_fd, confirm_mail_key_fd in self.email_fields:
if getattr(self, confirmed_email_fd) and not rerequest: if getattr(self, confirmed_email_fd) and not rerequest:
continue continue
if not getattr(self, email_fd):
continue
requested_confirmation = True requested_confirmation = True
setattr(self, confirmed_email_fd, False) setattr(self, confirmed_email_fd, False)
confirm_mail_key = uuid.uuid4().hex confirm_mail_key = uuid.uuid4().hex
@ -152,9 +165,9 @@ class Contact(CommonModel):
return getattr(self, email_fd) return getattr(self, email_fd)
return None return None
def send_mail(self, subject, content): def send_mail(self, subject, content, cc=None):
send_mail(subject, content, settings.DEFAULT_SENDING_MAIL, 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): def confirm_mail_by_key(key):
@ -189,7 +202,6 @@ class Person(Contact):
(FEMALE, 'Weiblich'), (FEMALE, 'Weiblich'),
(DIVERSE, 'Divers')) (DIVERSE, 'Divers'))
gender = models.IntegerField(choices=gender_choices, gender = models.IntegerField(choices=gender_choices,
default=DIVERSE,
verbose_name=_('Gender')) verbose_name=_('Gender'))
comments = models.TextField(_('comments'), default='', blank=True) comments = models.TextField(_('comments'), default='', blank=True)
@ -314,7 +326,7 @@ class Member(Person):
def generate_echo_key(self): def generate_echo_key(self):
self.echo_key = uuid.uuid4().hex 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.echoed = False
self.save() self.save()
return self.echo_key return self.echo_key
@ -357,11 +369,19 @@ class Member(Person):
"""A synonym for the email field.""" """A synonym for the email field."""
return self.email 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 @property
def association_email(self): def association_email(self):
"""Returning the association email of the member""" """Returning the association email of the member"""
raw = "{0}.{1}@{2}".format(self.prename.lower(), self.lastname.lower(), settings.DOMAIN) return "{username}@{domain}".format(username=self.username, domain=settings.DOMAIN)
return raw.replace('ö', 'oe').replace('ä', 'ae').replace('ü', 'ue')
def registration_complete(self): def registration_complete(self):
"""Check if all necessary fields are set.""" """Check if all necessary fields are set."""
@ -383,8 +403,9 @@ class Member(Person):
permissions = ( permissions = (
('may_see_qualities', 'Is allowed to see the quality overview'), ('may_see_qualities', 'Is allowed to see the quality overview'),
('may_set_auth_user', 'Is allowed to set auth user member connections.'), ('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_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 = { rules_permissions = {
'members': rules.always_allow, '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\ 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()]) 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): def confirm_mail(self, key):
ret = super().confirm_mail(key) ret = super().confirm_mail(key)
if self.registration_ready(): if self.registration_ready():
@ -492,6 +508,8 @@ class Member(Person):
return queryset return queryset
elif name == "EmergencyContact": elif name == "EmergencyContact":
return queryset return queryset
elif name == "MemberUnconfirmedProxy":
return queryset
else: else:
raise ValueError(name) raise ValueError(name)
@ -657,15 +675,29 @@ class Member(Person):
def suggested_username(self): def suggested_username(self):
"""Returns a suggested username given by {prename}.{lastname}.""" """Returns a suggested username given by {prename}.{lastname}."""
raw = "{0}.{1}".format(self.prename.lower(), self.lastname.lower()) 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): def invite_as_user(self):
"""Invites the member to join Kompass as a user.""" """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.invite_as_user_key = uuid.uuid4().hex
self.save() self.save()
self.send_mail(_('Set login data for Kompass'), self.send_mail(_('Set login data for Kompass'),
settings.INVITE_AS_USER_TEXT.format(name=self.prename, settings.INVITE_AS_USER_TEXT.format(name=self.prename,
link=get_invite_as_user_key(self.invite_as_user_key))) link=get_invite_as_user_key(self.invite_as_user_key)))
return True
def led_groups(self): def led_groups(self):
"""Returns a queryset of groups that this member is a youth leader of.""" """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 Emergency contact of a member
""" """
member = models.ForeignKey(Member, verbose_name=_('Member'), on_delete=models.CASCADE) member = models.ForeignKey(Member, verbose_name=_('Member'), on_delete=models.CASCADE)
email = models.EmailField(max_length=100, default='', blank=True)
def __str__(self): def __str__(self):
return str(self.member) return str(self.member)
@ -710,6 +743,11 @@ class MemberUnconfirmedProxy(Member):
verbose_name = _('Unconfirmed registration') verbose_name = _('Unconfirmed registration')
verbose_name_plural = _('Unconfirmed registrations') verbose_name_plural = _('Unconfirmed registrations')
permissions = (('may_manage_all_registrations', 'Can view and manage all 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): def __str__(self):
"""String representation""" """String representation"""
@ -846,21 +884,23 @@ class MemberWaitingList(Person):
group_link = '({url}) '.format(url=prepend_base_url(reverse('startpage:gruppe_detail', args=[group.name]))) group_link = '({url}) '.format(url=prepend_base_url(reverse('startpage:gruppe_detail', args=[group.name])))
else: else:
group_link = '' group_link = ''
# TODO: inform the user that the group has no configured weekday, start_time or end_time if group.has_time_info():
weekday = WEEKDAYS[group.weekday][1] if group.weekday != None else WEEKDAYS[0][1] group_time = settings.GROUP_TIME_AVAILABLE_TEXT.format(weekday=WEEKDAYS[group.weekday][1],
start_time = group.start_time.strftime('%H:%M') if group.start_time != None else "14:00" start_time=group.start_time.strftime('%H:%M'),
end_time = group.end_time.strftime('%H:%M') if group.end_time != None else "16:00" 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 = InvitationToGroup(group=group, waiter=self)
invitation.save() invitation.save()
self.send_mail(_("Invitation to trial group meeting"), self.send_mail(_("Invitation to trial group meeting"),
settings.INVITE_TEXT.format(name=self.prename, settings.INVITE_TEXT.format(name=self.prename,
weekday=weekday, group_time=group_time,
start_time=start_time,
end_time=end_time,
group_name=group.name, group_name=group.name,
group_link=group_link, group_link=group_link,
contact_email=group.contact_email,
link=get_registration_link(invitation.key), link=get_registration_link(invitation.key),
invitation_reject_link=get_invitation_reject_link(invitation.key))) invitation_reject_link=get_invitation_reject_link(invitation.key)),
cc=group.contact_email.email)
def unregister(self): def unregister(self):
"""Delete the waiter and inform them about the deletion via email.""" """Delete the waiter and inform them about the deletion via email."""
@ -1015,6 +1055,31 @@ class Freizeit(CommonModel):
jls = set(self.jugendleiter.distinct()) jls = set(self.jugendleiter.distinct())
return len(ps - jls) 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 @property
def time_period_str(self): def time_period_str(self):
time_period = self.date.strftime('%d.%m.%Y') time_period = self.date.strftime('%d.%m.%Y')
@ -1091,6 +1156,60 @@ class Freizeit(CommonModel):
base['Status' + suffix] = str(2) base['Status' + suffix] = str(2)
return base 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 @staticmethod
def filter_queryset_by_permissions(member, queryset=None): def filter_queryset_by_permissions(member, queryset=None):
if queryset is None: if queryset is None:
@ -1524,7 +1643,7 @@ def parse_date(value):
def parse_datetime(value): def parse_datetime(value):
tz = pytz.timezone('Europe/Berlin') tz = pytz.timezone('Europe/Berlin')
if value == '': if value == '':
return None return timezone.now()
return tz.localize(datetime.strptime(value, '%d.%m.%Y %H:%M:%S')) 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 = 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'] } 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() mem.save()
if kwargs['contacted_by']: if kwargs['contacted_by']:
@ -1639,3 +1758,8 @@ def import_from_csv_waitinglist(path):
for row in rows: for row in rows:
transform_row(row) 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')

@ -31,7 +31,17 @@ def media_dir():
return os.path.join(settings.MEDIA_MEMBERLISTS, "memberlists") 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 = name + "_" + datetime.today().strftime("%d_%m_%Y")
filename = filename.replace(' ', '_').replace('&', '').replace('/', '_') filename = filename.replace(' ', '_').replace('&', '').replace('/', '_')
# drop umlauts, accents etc. # drop umlauts, accents etc.
@ -64,16 +74,12 @@ def render_tex(name, template_path, context):
os.chdir(oldwd) os.chdir(oldwd)
# provide the user with the resulting pdf file if save_only:
with open(media_path(filename_pdf), 'rb') as pdf: return filename_pdf
response = HttpResponse(FileWrapper(pdf))#, content='application/pdf') return serve_pdf(filename_pdf)
response['Content-Type'] = 'application/pdf'
response['Content-Disposition'] = 'attachment; filename='+filename_pdf
return response
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 = name + "_" + datetime.today().strftime("%d_%m_%Y")
filename = filename.replace(' ', '_').replace('&', '').replace('/', '_') filename = filename.replace(' ', '_').replace('&', '').replace('/', '_')
# drop umlauts, accents etc. # 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: with open(media_path(filename_pdf), 'wb') as output_stream:
writer.write(output_stream) writer.write(output_stream)
# provide the user with the resulting pdf file if save_only:
with open(media_path(filename_pdf), 'rb') as pdf: return filename_pdf
response = HttpResponse(FileWrapper(pdf))#, content='application/pdf') return serve_pdf(filename_pdf)
response['Content-Type'] = 'application/pdf'
response['Content-Disposition'] = 'attachment; filename='+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)

@ -0,0 +1,48 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}
{% block extrahead %}
{{ block.super }}
{{ media }}
<script src="{% static 'admin/js/cancel.js' %}" async></script>
<script type="text/javascript" src="{% static "admin/js/vendor/jquery/jquery.js" %}"></script>
<script type="text/javascript" src="{% static "admin/js/jquery.init.js" %}"></script>
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} invite-waiter
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; {% translate 'Demote to waiter' %}
</div>
{% endblock %}
{% block content %}
<h2>{% translate "Demote to waiter" %}</h2>
<p>
{% trans "Do you want to demote the following unconfirmed registrations to waiters?" %}
</p>
<p>
<ul>
{% for member in queryset %}
<li>
<a href="{% url 'admin:members_memberunconfirmedproxy_change' 3 %}">{{ member }}</a>
</li>
{% endfor %}
</ul>
</p>
<form action="" method="post">
{% csrf_token %}
{% if form %}
{{form}}
{% endif %}
<input type="hidden" name="action" value="demote_to_waiter_action">
<input class="default" style="color: $default-link-color" type="submit" name="apply" value="{% translate 'Demote' %}">
<a href="#" class="button cancel-link">{% translate "Cancel" %}</a>
</form>
{% endblock %}

@ -0,0 +1,167 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}
{% block extrahead %}
{{ block.super }}
{{ media }}
<script src="{% static 'admin/js/cancel.js' %}" async></script>
<script type="text/javascript" src="{% static "admin/js/vendor/jquery/jquery.js" %}"></script>
<script type="text/javascript" src="{% static "admin/js/jquery.init.js" %}"></script>
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} invite-waiter
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a>
&rsaquo; {% translate 'Finance overview' %}
</div>
{% endblock %}
{% block content %}
<h2>{% trans 'Excursion' %}: {{ memberlist.name }}</h2>
<p>
{% blocktrans %}
Here you see an estimate on the expected costs and contributions by the association. This is not a guaranteed
cost plan!
{% endblocktrans %}
</p>
<h3>{% translate "Expenses" %}</h3>
{% blocktrans %}You listed the following expenses:{% endblocktrans %}
<p>
<table>
<th>
<td>{% trans "Explanation" %}</td>
<td>{% trans "Amount" %}</td>
</th>
{% for bill in memberlist.statement.bill_set.all %}
<tr>
<td>
{{bill.short_description}}
</td>
<td>
{{bill.explanation}}
</td>
<td>
{{ bill.amount }}€
</td>
</tr>
{% endfor %}
</table>
</p>
<p>{% blocktrans %}The total expected expenses are {{ total_bills_theoretic }} €.{% endblocktrans %}</p>
<h3>{% trans "Contributions by the association" %}</h3>
<p>
{% blocktrans %}According to the contribution guidelines,
{{ staff_count }} youth leader(s) receive contributions. Each of them receives{% endblocktrans %}
</p>
<p>
<ul>
<li>
{% blocktrans %}{{ nights }} nights for {{ price_per_night }}€ per night making a total of {{ nights_per_yl }}€.{% endblocktrans %}
</li>
<li>
{% blocktrans %}{{ duration }} days for {{ allowance_per_day }}€ per day making a total of {{ allowance_per_yl }}€.{% endblocktrans %}
</li>
<li>
{% blocktrans %}{{ kilometers_traveled }} km by {{ means_of_transport }} ({{euro_per_km}} € / km) making a total of {{ transportation_per_yl }}€.{% endblocktrans %}
</li>
</ul>
</p>
<p>
{% blocktrans %}In total these are contributions of {{ total_per_yl }}€ times {{ staff_count }}, giving {{ total_staff }}€.{% endblocktrans %}
</p>
<h3>{% trans "LJP contributions" %}</h3>
<p>
{% 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 %}
</p>
<h3>{% trans "Summary" %}</h3>
<p>
{% blocktrans %}This is the estimated cost and contribution summary:{% endblocktrans %}
</p>
<table>
<tr>
<td>
{% trans "Expenses" %}
</td>
<td>
{{ total_bills_theoretic }}€
</td>
</tr>
<tr>
<td>
{% trans "Contributions by the association" %}
</td>
<td>
-{{ total_staff }}€
</td>
</tr>
<tr>
<td>
{% trans "Potential LJP contributions" %}
</td>
<td>
-{{ ljp_contributions }}€
</td>
</tr>
<tr>
<td>
{% trans "Remaining costs" %}
</td>
<td>
{{ total_relative_costs }}€
</td>
</tr>
</table>
<br>
<p>
{% 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 %}
</p>
<p>
{% 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 %}
</p>
{% if not memberlist.statement.submitted %}
<h3>{% trans "Submit statement" %}</h3>
<p>
{% 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 %}
</p>
<form action="" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="finance_overview">
<input type="hidden" name="finance_overview">
<input class="default" style="color: $default-link-color" type="submit" name="apply" value="{% translate 'Submit' %}">
<a href="#" class="button cancel-link">{% translate "Cancel" %}</a>
</form>
{% else %}
<br>
<h3>{% trans "Statement submitted" %}</h3>
<p>
{% 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 %}
</p>
<a href="#" class="button cancel-link">{% translate "Back" %}</a>
{% endif %}
{% endblock %}

@ -27,6 +27,10 @@
{% blocktrans %}Here you can generate a seminar report suitable for the LJP. A report {% 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 %} always contains a head page with the basic information on the seminar.{% endblocktrans %}
</p> </p>
<p>
{% blocktrans %}Expenses with same short description are automatically summed up and shown as one expense in the
expense overview.{% endblocktrans %}
</p>
<ul> <ul>
<li> <li>
{% blocktrans %}Full report: Include learning goals and a detailed, tabularized time schedule. This requires {% 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 %}
</ul> </ul>
<br> <br>
<p>{% blocktrans %}Please choose one of the listed modes.{% endblocktrans %}</p> <p>{% blocktrans %}You may also choose to include the V32 attachment.{% endblocktrans %}</p>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
<p> <p>
<table>
{{ form }} {{ form }}
</table>
</p> </p>
<br> <br>
<input type="hidden" name="action" value="seminar_report"> <input type="hidden" name="action" value="seminar_report">

@ -1,6 +1,6 @@
{% load tex_extras %} {% load tex_extras %}
\documentclass{article} \documentclass[a4paper]{article}
\usepackage[utf8]{inputenc} \usepackage[utf8]{inputenc}
\usepackage{booktabs} \usepackage{booktabs}
@ -58,7 +58,7 @@
\end{textblock*} \end{textblock*}
% HEADLINE % HEADLINE
{\noindent\LARGE{Teilnehmer:innenliste\\[2mm]Sektionsveranstaltung}}\\[1mm] {\noindent\LARGE{Teilnehmer*innenliste\\[2mm]Sektionsveranstaltung}}\\[1mm]
\textit{Erstellt: {{ creation_date }} }\\ \textit{Erstellt: {{ creation_date }} }\\
% DESCRIPTION TABLE % DESCRIPTION TABLE
@ -89,8 +89,7 @@
{{ m.member.contact_phone_number|esc_all }} & {{ m.member.contact_phone_number|esc_all }} &
{% for c in m.member.emergencycontact_set.all %} {% for c in m.member.emergencycontact_set.all %}
{{ c.name }} \newline {{ c.name }} \newline
Tel.: {{ c.phone_number }} \newline Tel.: {{ c.phone_number }}
Email: \href{mailto:{{ c.email }}}{ {{c.email}}}
{% endfor %} \\ {% endfor %} \\
{% endfor %} {% endfor %}
\bottomrule \bottomrule

@ -1,6 +1,5 @@
{% extends "members/base.html" %} {% extends "members/base.html" %}
{% load i18n %} {% load i18n static common %}
{% load static %}
{% block title %} {% block title %}
{% trans "Echo failed" %} {% trans "Echo failed" %}
@ -12,6 +11,7 @@
<p><b>{% trans "Something went wrong. The key you supplied is" %} {{ reason }}.</b></p> <p><b>{% trans "Something went wrong. The key you supplied is" %} {{ reason }}.</b></p>
<p>{% trans "If you think this is a mistake, please" %} <a href="mailto:jugendreferent@jdav-ludwigsburg.de">{% trans "contact us." %}</a></p> <p>{% trans "If you think this is a mistake, please" %}
<a href="mailto:{% settings_value 'RESPONSIBLE_MAIL' %}">{% trans "contact us." %}</a></p>
{% endblock %} {% endblock %}

@ -1,6 +1,5 @@
{% extends "members/base.html" %} {% extends "members/base.html" %}
{% load i18n %} {% load i18n static common %}
{% load static %}
{% block title %} {% block title %}
{% trans "Registration failed" %} {% trans "Registration failed" %}
@ -12,6 +11,6 @@
<p><b>{% trans "Something went wrong. The key you supplied is" %} {{ reason }}.</b></p> <p><b>{% trans "Something went wrong. The key you supplied is" %} {{ reason }}.</b></p>
<p>{% trans "If you think this is a mistake, please" %} <a href="mailto:jugendreferent@jdav-ludwigsburg.de">{% trans "contact us." %}</a></p> <p>{% trans "If you think this is a mistake, please" %} <a href="mailto:{% settings_value 'RESPONSIBLE_MAIL' %}">{% trans "contact us." %}</a></p>
{% endblock %} {% endblock %}

@ -34,7 +34,7 @@
<input type="hidden" name="waiter_key" value="{{ waiter_key }}"> <input type="hidden" name="waiter_key" value="{{ waiter_key }}">
<input type="hidden" name="save"> <input type="hidden" name="save">
<input type="hidden" name="key" value="{{ key }}"> <input type="hidden" name="key" value="{{ key }}">
<p><input type="submit" value="{% trans "submit" %}"/></p> <p><input type="submit" value="{% trans "Save" %}"/></p>
</form> </form>
<div id="empty_form" class="form-row" style="display:none"> <div id="empty_form" class="form-row" style="display:none">
@ -47,9 +47,11 @@
function addRequired(element) { function addRequired(element) {
var inputs = element.getElementsByTagName('input'); var inputs = element.getElementsByTagName('input');
for (var i = 0; i < inputs.length; i++) { for (var i = 0; i < inputs.length; i++) {
if (inputs[i].getAttribute('type') != 'email') {
inputs[i].setAttribute('required', 'required'); inputs[i].setAttribute('required', 'required');
} }
} }
}
function removeRequired(element) { function removeRequired(element) {
var inputs = element.getElementsByTagName('input'); var inputs = element.getElementsByTagName('input');
for (var i = 0; i < inputs.length; i++) { for (var i = 0; i < inputs.length; i++) {

@ -1,6 +1,6 @@
{% load tex_extras %} {% load tex_extras %}
\documentclass{article} \documentclass[a4paper]{article}
\usepackage[utf8]{inputenc} \usepackage[utf8]{inputenc}
\usepackage{booktabs} \usepackage{booktabs}
@ -20,7 +20,7 @@
\begin{document} \begin{document}
% HEADLINE % HEADLINE
{\noindent\LARGE{Teilnehmer:innenübersicht}}\\[1mm] {\noindent\LARGE{Teilnehmer*innenübersicht}}\\[1mm]
\textit{Erstellt: {{ creation_date }} }\\ \textit{Erstellt: {{ creation_date }} }\\
% DESCRIPTION % DESCRIPTION

@ -1,6 +1,5 @@
{% extends "members/base.html" %} {% extends "members/base.html" %}
{% load i18n %} {% load i18n static common %}
{% load static %}
{% block title %} {% block title %}
{% trans "Registration failed" %} {% trans "Registration failed" %}
@ -12,6 +11,7 @@
<p><b>{% trans "Something went wrong while processing your registration." %}</b></p> <p><b>{% trans "Something went wrong while processing your registration." %}</b></p>
<p>{% trans "If you think this is a mistake, please" %} <a href="mailto:jugendreferent@jdav-ludwigsburg.de">{% trans "contact us." %}</a></p> <p>{% trans "If you think this is a mistake, please" %}
<a href="mailto:{% settings_value 'RESPONSIBLE_MAIL' %}">{% trans "contact us." %}</a></p>
{% endblock %} {% endblock %}

@ -18,7 +18,7 @@ Da wir durch das Engagment unserer ehrenamtlichen Jugendleitern, aber auch die K
<p> <p>
Die Anmeldung für die Gruppen <a href="{% url 'startpage:gruppe_detail' group_name='JuMa' %}">JuMa</a>, Die Anmeldung für die Gruppen <a href="{% url 'startpage:gruppe_detail' group_name='JuMa' %}">JuMa</a>,
<a href="{% url 'startpage:gruppe_detail' group_name='Kletterteff' %}">Klettertreff</a> und <a href="{% url 'startpage:gruppe_detail' group_name='Klettertreff' %}">Klettertreff</a> und
<a href="{% url 'startpage:gruppe_detail' group_name='Mountainbike' %}">Mountainbike</a> laufen separat. <a href="{% url 'startpage:gruppe_detail' group_name='Mountainbike' %}">Mountainbike</a> laufen separat.
</p> </p>

@ -1,6 +1,6 @@
{% load tex_extras %} {% load tex_extras %}
\documentclass{article} \documentclass[a4paper]{article}
\usepackage[utf8]{inputenc} \usepackage[utf8]{inputenc}
\usepackage{booktabs} \usepackage{booktabs}
@ -119,7 +119,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
\section{Teilnehmer:innenliste} \section{Teilnehmer*innenliste}
\begin{table}[H] \begin{table}[H]
\begin{tabularx}{1\linewidth}{@{\extracolsep{\fill}}lLl|c|c|c} \begin{tabularx}{1\linewidth}{@{\extracolsep{\fill}}lLl|c|c|c}
@ -148,7 +148,7 @@
Aufwandsentschädigung & {{ memberlist.statement.total_allowance }}\\ Aufwandsentschädigung & {{ memberlist.statement.total_allowance }}\\
Fahrtkosten & {{ memberlist.statement.total_transportation }}\\ Fahrtkosten & {{ memberlist.statement.total_transportation }}\\
Übernachtungskosten & {{ memberlist.statement.total_nights }}\\ Ü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 }}\\ {{ bill.short_description|esc_all }} & {{ bill.amount }}\\
{% endfor %} {% endfor %}
\bottomrule \bottomrule

@ -13,8 +13,8 @@ from django.conf import settings
class MemberForm(ModelForm): class MemberForm(ModelForm):
class Meta: class Meta:
model = Member model = Member
fields = ['prename', 'lastname', 'street', 'plz', 'town', 'address_extra', fields = ['prename', 'lastname', 'gender', 'street', 'plz', 'town',
'phone_number', 'dav_badge_no'] 'address_extra', 'phone_number', 'dav_badge_no']
class MemberRegistrationForm(ModelForm): class MemberRegistrationForm(ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -29,7 +29,7 @@ class MemberRegistrationForm(ModelForm):
'phone_number', 'birth_date', 'gender', 'email', 'alternative_email', 'phone_number', 'birth_date', 'gender', 'email', 'alternative_email',
'registration_form'] 'registration_form']
widgets = { 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'}), 'registration_form': FileInput(attrs={'accept': 'application/pdf,image/jpeg,image/png'}),
} }
help_texts = { help_texts = {
@ -53,7 +53,7 @@ class MemberRegistrationWaitingListForm(ModelForm):
model = MemberWaitingList model = MemberWaitingList
fields = ['prename', 'lastname', 'birth_date', 'gender', 'email', 'application_text'] fields = ['prename', 'lastname', 'birth_date', 'gender', 'email', 'application_text']
widgets = { widgets = {
'birth_date': DateInput(format='%d.%m.%Y', attrs={'type': 'date'}) 'birth_date': DateInput(format='%Y-%m-%d', attrs={'type': 'date'})
} }
help_texts = { help_texts = {
'prename': _('Prename of the member.'), 'prename': _('Prename of the member.'),
@ -72,7 +72,7 @@ class EmergencyContactForm(ModelForm):
class Meta: class Meta:
model = EmergencyContact model = EmergencyContact
fields = ['prename', 'lastname', 'email', 'phone_number'] fields = ['prename', 'lastname', 'email', 'phone_number']
required = ['prename', 'lastname', 'email', 'phone_number'] required = ['prename', 'lastname', 'phone_number']
class BaseEmergencyContactsFormSet(BaseInlineFormSet): class BaseEmergencyContactsFormSet(BaseInlineFormSet):
@ -124,6 +124,13 @@ def echo(request):
return HttpResponseRedirect(reverse('startpage:index')) return HttpResponseRedirect(reverse('startpage:index'))
if request.method == 'GET': 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 # show password
return render_echo_password(request, request.GET['key']) return render_echo_password(request, request.GET['key'])
@ -151,7 +158,9 @@ def echo(request):
raise ValueError(_("Invalid emergency contacts")) raise ValueError(_("Invalid emergency contacts"))
form.save() form.save()
emergency_contacts_formset.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.echoed = True
member.save() member.save()
return render_echo_success(request, member.prename) return render_echo_success(request, member.prename)

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"

@ -7,6 +7,7 @@
<link rel="stylesheet" href="{% static 'startpage/css/base.css' %}"> <link rel="stylesheet" href="{% static 'startpage/css/base.css' %}">
<link rel="stylesheet" href="{% static 'startpage/css/fonts.css' %}"> <link rel="stylesheet" href="{% static 'startpage/css/fonts.css' %}">
<link rel="stylesheet" href="{% static 'startpage/css/people_grid.css' %}"> <link rel="stylesheet" href="{% static 'startpage/css/people_grid.css' %}">
<link rel="shortcut icon" type="image/png" href="{% static 'startpage/img/favicon.png' %}"/>
</head> </head>
<body> <body>

@ -1,10 +1,11 @@
{% extends "startpage/base_subsite.html" %} {% extends "startpage/base_subsite.html" %}
{% load markdown_extras markdownify %}
{% block content %} {% block content %}
<h1>{{ group.name }}</h1> <h1>{{ group.name }}</h1>
<p> <p>
{{ group.description }} {{ group.description|markdownify }}
</p> </p>
<h2>Gruppenleitung</h2> <h2>Gruppenleitung</h2>

@ -10,7 +10,7 @@
<h3>Wie lange sind die Wartezeiten?</h3> <h3>Wie lange sind die Wartezeiten?</h3>
<p> <p>
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.
</p> </p>
<h3>Muss mein Kind DAV Mitglied sein?</h3> <h3>Muss mein Kind DAV Mitglied sein?</h3>

@ -38,7 +38,7 @@ Der Deutsche Alpenverein Sektion Heidelberg 1869 e.V. ist im Vereinsregister des
<h3>Jugendreferat</h3> <h3>Jugendreferat</h3>
<p> <p>
Leitung: Eva Engelmann und Robert Scheffler (kommissarisch)<br> Leitung: Eva Engelmann und Robert Scheffler<br>
E-Mail: jugend@alpenverein-heidelberg.de<br> E-Mail: jugend@alpenverein-heidelberg.de<br>
URL: <a href="https://www.jdav-hd.de">www.jdav-hd.de</a> ; <a href="https://www.jdav-heidelberg.de">www.jdav-heidelberg.de</a> URL: <a href="https://www.jdav-hd.de">www.jdav-hd.de</a> ; <a href="https://www.jdav-heidelberg.de">www.jdav-heidelberg.de</a>
</p> </p>
@ -59,12 +59,13 @@ Diese Datenschutz-Information gilt für die Datenverarbeitung durch:
<p> <p>
Verantwortlicher: Deutscher Alpenverein Sektion Heidelberg 1869 e.V.<br> Verantwortlicher: Deutscher Alpenverein Sektion Heidelberg 1869 e.V.<br>
Harbigweg 20, 69124 Heidelberg<br> Harbigweg 20, 69124 Heidelberg<br>
Email: <a href="mailto:alpenverein-heidelberg@t-online.de">alpenverein-heidelberg@t-online.de</a><br> Email: <a href="mailto:geschaeftsstelle@alpenverein-heidelberg.de">geschaeftsstelle@alpenverein-heidelberg.de</a><br>
Telefon: +49 6221 484076 Telefon: +49 6221 484076
</p> </p>
<p> <p>
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).
</p> </p>
<h3> <h3>
@ -72,7 +73,7 @@ Zur Auftragsverarbeitung für die jdav Heidelberg ist die Firma reeweb AG Wettst
</h3> </h3>
<p> <p>
Beim Aufrufen unserer Website <a href="https://jdav-hd.merten.dev">jdav-hd.merten.dev</a> 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 <a href="https://jdav-hd.de">jdav-hd.de</a> 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:
</p> </p>
<p> <p>
@ -131,7 +132,8 @@ Zur Weitergabe der Daten im Rahmen einer Mitgliedschaft im Alpenverein Heidelber
</p> </p>
<p> <p>
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).
</p> </p>
<p> <p>
@ -191,7 +193,10 @@ Die meisten Browser akzeptieren Cookies automatisch. Sie können Ihren Browser j
</p> </p>
<p> <p>
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:
</p> </p>
<p> <p>
@ -258,7 +263,7 @@ Die während des Absendevorgangs zusätzlich erhobenen personenbezogenen Daten w
<h3>6. Das geschieht, wenn Sie Online-Anmeldeformulare nutzen:</h3> <h3>6. Das geschieht, wenn Sie Online-Anmeldeformulare nutzen:</h3>
<p> <p>
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.
</p> </p>
<p> <p>
@ -346,7 +351,7 @@ Sofern Ihre personenbezogenen Daten auf Grundlage von berechtigten Interessen ge
<p> <p>
Möchten Sie von Ihrem Widerrufs- oder Widerspruchsrecht Gebrauch machen, genügt eine Email an Möchten Sie von Ihrem Widerrufs- oder Widerspruchsrecht Gebrauch machen, genügt eine Email an
<a href="mailto:jugend@alpenverein-heidelberg.de">jugend@alpenverein-heidelberg.de</a> oder <a href="mailto:jugend@alpenverein-heidelberg.de">jugend@alpenverein-heidelberg.de</a> oder
<a href="mailto:alpenverein-heidelberg@t-online.de">alpenverein-heidelberg@t-online.de</a>. <a href="mailto:geschaeftsstelle@alpenverein-heidelberg.de">geschaeftsstelle@alpenverein-heidelberg.de</a>.
</p> </p>
<h3>10. Die Sicherheit Ihrer Daten:</h3> <h3>10. Die Sicherheit Ihrer Daten:</h3>
@ -358,7 +363,7 @@ Wir bedienen uns geeigneter technischer und organisatorischer Sicherheitsmaßnah
<h3>11. Aktualität und Änderung dieser Datenschutzerklärung</h3> <h3>11. Aktualität und Änderung dieser Datenschutzerklärung</h3>
<p> <p>
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.
</p> </p>
<p> <p>

@ -51,7 +51,7 @@ window.onclick = function(event) {
<div class="menu-level-1"> <div class="menu-level-1">
<ul> <ul>
<li> <li>
<a href="{% url 'startpage:index' %}">jdav Heidelberg</a> <a href="{% url 'startpage:index' %}">{{ root_section.title }}</a>
<div class="menu-level-2"> <div class="menu-level-2">
<ul> <ul>
<li> <li>

@ -11,7 +11,7 @@ from .models import Post, Section
# render shortcut adding additional context variables, needed for navbar # render shortcut adding additional context variables, needed for navbar
def render(request, template_path, context={}): def render(request, template_path, context={}):
context['groups'] = Group.objects.filter(show_website=True) context['groups'] = Group.objects.filter(show_website=True).order_by('name')
context['sections'] = Section.objects.all() context['sections'] = Section.objects.all()
try: try:
context['root_section'] = Section.objects.get(urlname=settings.ROOT_SECTION) context['root_section'] = Section.objects.get(urlname=settings.ROOT_SECTION)
@ -22,8 +22,8 @@ def render(request, template_path, context={}):
def index(request): def index(request):
context = { context = {
'recent_posts': Post.objects.filter(section__urlname=settings.RECENT_SECTION), 'recent_posts': Post.objects.filter(section__urlname=settings.RECENT_SECTION).order_by('-date'),
'reports': Post.objects.filter(section__urlname=settings.REPORTS_SECTION), 'reports': Post.objects.filter(section__urlname=settings.REPORTS_SECTION).order_by('-date'),
} }
return render(request, 'startpage/index.html', context) return render(request, 'startpage/index.html', context)

@ -0,0 +1,15 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
<link rel="stylesheet" href="/downtime/base.css">
<link rel="shortcut icon" type="image/png" href="/downtime/favicon.png"/>
<title>Wartungsarbeiten</title>
</head>
<body>
<div id="main">
<h2>Wartungsarbeiten</h2>
<p>Unsere Webseite ist zur Zeit wegen Wartungsarbeiten leider nicht verf&uuml;gbar. Bitte
versuche es zu einem sp&auml;teren Zeitpunkt nochmal.</p>
</div>
</body>
</html>

@ -0,0 +1,9 @@
#main {
text-align: center;
font-size: 15pt;
width: 60%;
margin: 0 auto;
overflow: hidden;
position: relative;
padding-top: 40pt;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

@ -92,18 +92,21 @@ h6 {
} }
.content { .content {
width: 55%; max-width: 100%;
width: 1000px;
margin: 0 auto; margin: 0 auto;
padding-left: 20px;
padding-right: 20px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
padding-top: 40px; padding-top: 40px;
padding-bottom: 40px; padding-bottom: 40px;
} }
@media only screen and (min-width: 1024px) {
.column { .column {
float: left; float: left;
width: 50%; width: 50%;
padding: 0 20px;
} }
/* Clear floats after the columns */ /* Clear floats after the columns */
@ -111,6 +114,7 @@ h6 {
content: ""; content: "";
clear: both; clear: both;
} }
}
.footer a { .footer a {
text-decoration: none; text-decoration: none;

@ -1,7 +1,7 @@
.grid-container { .grid-container {
display: grid; display: grid;
grid-gap: 10px; grid-gap: 10px;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
padding: 15px; padding: 15px;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

@ -0,0 +1,38 @@
{% load i18n admin_urls %}
<div class="js-inline-admin-formset inline-group"
id="{{ inline_admin_formset.formset.prefix }}-group"
data-inline-type="stacked"
data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<fieldset class="module {{ inline_admin_formset.classes }}" aria-labelledby="{{ inline_admin_formset.formset.prefix }}-heading">
{% if inline_admin_formset.is_collapsible %}<details><summary>{% endif %}
<h2 id="{{ inline_admin_formset.formset.prefix }}-heading" class="inline-heading">
{% if inline_admin_formset.formset.max_num == 1 %}
{{ inline_admin_formset.opts.verbose_name|capfirst }}
{% else %}
{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}
{% endif %}
</h2>
{% if inline_admin_formset.is_collapsible %}</summary>{% endif %}
{{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }}
{% for inline_admin_form in inline_admin_formset %}<div class="inline-related{% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if forloop.last and inline_admin_formset.has_add_permission %}empty{% else %}{{ forloop.counter0 }}{% endif %}">
<h3><b>{{ inline_admin_formset.opts.verbose_name|capfirst }}:</b> <span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} <a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{{ inline_admin_formset.has_change_permission|yesno:'inlinechangelink,inlineviewlink' }}">{% if inline_admin_formset.has_change_permission %}{% translate "Change" %}{% else %}{% translate "View" %}{% endif %}</a>{% endif %}
{% else %}#{{ forloop.counter }}{% endif %}</span>
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% translate "View on site" %}</a>{% endif %}
{% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
</h3>
{% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %}
{% with parent_counter=forloop.counter0 %}
{% for fieldset in inline_admin_form %}
{% include "admin/includes/fieldset.html" with heading_level=4 prefix=fieldset.formset.prefix id_prefix=parent_counter id_suffix=forloop.counter0 %}
{% endfor %}
{% endwith %}
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{% if inline_admin_form.fk_field %}{{ inline_admin_form.fk_field.field }}{% endif %}
</div>{% endfor %}
{% if inline_admin_formset.is_collapsible %}</details>{% endif %}
</fieldset>
</div>

@ -0,0 +1,78 @@
{% load i18n admin_urls static admin_modify %}
<div class="js-inline-admin-formset inline-group" id="{{ inline_admin_formset.formset.prefix }}-group"
data-inline-type="tabular"
data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
{{ inline_admin_formset.formset.management_form }}
<fieldset class="module {{ inline_admin_formset.classes }}" aria-labelledby="{{ inline_admin_formset.formset.prefix }}-heading">
{% if inline_admin_formset.is_collapsible %}<details><summary>{% endif %}
<h2 id="{{ inline_admin_formset.formset.prefix }}-heading" class="inline-heading">
{% if inline_admin_formset.formset.max_num == 1 %}
{{ inline_admin_formset.opts.verbose_name|capfirst }}
{% else %}
{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}
{% endif %}
</h2>
{% if inline_admin_formset.is_collapsible %}</summary>{% endif %}
{{ inline_admin_formset.formset.non_form_errors }}
{% if inline_admin_formset.opts.description %}
<div class="description">
<p>
{{ inline_admin_formset.opts.description }}
</p>
</div>
{% endif %}
<table>
<thead><tr>
<th class="original"></th>
{% for field in inline_admin_formset.fields %}
<th class="column-{{ field.name }}{% if field.required %} required{% endif %}{% if field.widget.is_hidden %} hidden{% endif %}">{{ field.label|capfirst }}
{% if field.help_text %}<img src="{% static "admin/img/icon-unknown.svg" %}" class="help help-tooltip" width="10" height="10" alt="({{ field.help_text|striptags }})" title="{{ field.help_text|striptags }}">{% endif %}
</th>
{% endfor %}
{% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission %}<th>{% translate "Delete?" %}</th>{% endif %}
</tr></thead>
<tbody>
{% for inline_admin_form in inline_admin_formset %}
{% if inline_admin_form.form.non_field_errors %}
<tr class="row-form-errors"><td colspan="{{ inline_admin_form|cell_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
{% endif %}
<tr class="form-row {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} empty-form{% endif %}"
id="{{ inline_admin_formset.formset.prefix }}-{% if forloop.last and inline_admin_formset.has_add_permission %}empty{% else %}{{ forloop.counter0 }}{% endif %}">
<td class="original">
{% if inline_admin_form.original or inline_admin_form.show_url %}<p>
{% if inline_admin_form.original %}
{{ inline_admin_form.original }}
{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}<a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{{ inline_admin_formset.has_change_permission|yesno:'inlinechangelink,inlineviewlink' }}">{% if inline_admin_formset.has_change_permission %}{% translate "Change" %}{% else %}{% translate "View" %}{% endif %}</a>{% endif %}
{% endif %}
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% translate "View on site" %}</a>{% endif %}
</p>{% endif %}
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{% if inline_admin_form.fk_field %}{{ inline_admin_form.fk_field.field }}{% endif %}
</td>
{% for fieldset in inline_admin_form %}
{% for line in fieldset %}
{% for field in line %}
<td class="{% if field.field.name %}field-{{ field.field.name }}{% endif %}{% if field.field.is_hidden %} hidden{% endif %}">
{% if field.is_readonly %}
<p>{{ field.contents }}</p>
{% else %}
{{ field.field.errors.as_ul }}
{{ field.field }}
{% endif %}
</td>
{% endfor %}
{% endfor %}
{% endfor %}
{% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission %}
<td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% if inline_admin_formset.is_collapsible %}</details>{% endif %}
</fieldset>
</div>
</div>

@ -1,5 +1,5 @@
{% extends "admin/index.html" %} {% extends "admin/index.html" %}
{% load i18n static %} {% load i18n static common %}
{% block content %} {% block content %}
@ -19,45 +19,81 @@ auf deine
{% if user.member %} {% if user.member %}
<div> <div class="module">
<p>
<h3>Deine Jugendgruppen</h3> <h3>Deine Jugendgruppen</h3>
<p>
Hier siehst du alle von dir geleiteten Jugendgruppen. Hier siehst du alle von dir geleiteten Jugendgruppen.
</p> </p>
<p> <p>
<table> <table>
{% for group in user.member.led_groups %} {% for group in user.member.led_groups %}
<tr> <tr>
<td> <th scope="row">
<a href='{% url 'admin:members_member_changelist' %}?group__id__exact={{group.pk}}'>{{ group.name }}</a> <a href='{% url 'admin:members_member_changelist' %}?group__id__exact={{group.pk}}'>{{ group.name }}</a>
</td> </th>
<td></td>
<td></td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
</p> </p>
</div> </div>
<div> <div class="module">
<p>
<h3>Deine letzten Ausfahrten</h3> <h3>Deine letzten Ausfahrten</h3>
<p>
Hier siehst du alle von dir geleiteten Ausfahrten. Hier siehst du alle von dir geleiteten Ausfahrten.
</p> </p>
<p> <p>
<table> <table>
{% for freizeit in user.member.led_freizeiten %} {% for freizeit in user.member.led_freizeiten %}
<tr> <tr>
<td> <th scope="row">
<a href='{% url 'admin:members_freizeit_change' freizeit.pk %}'>{{freizeit.name}}</a> <a href='{% url 'admin:members_freizeit_change' freizeit.pk %}'>{{freizeit.name}}</a>
</td> </th>
<td></td>
<td></td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
</p> </p>
</div> </div>
</div>
{% endif %} {% endif %}
<div class="module current-app">
<h3>E-Mail Verteiler</h3>
<p>
Hier kannst du E-Mails an deine Gruppe oder an andere Menschen in der JDAV Heidelberg schicken.
</p>
<table>
<tr>
<th scope="row">
<a href='{% url 'admin:mailer_message_add' %}'>Verfassen</a>
</th>
<td></td>
<td></td>
</tr>
<tr>
<th scope="row">
<a href='{% url 'admin:mailer_message_changelist' %}?sent__exact=1'>Gesendet</a>
</th>
<td></td>
<td></td>
</tr>
<tr>
<th scope="row">
<a href='{% url 'admin:mailer_message_changelist' %}?sent__exact=0'>Entwürfe</a>
</th>
<td></td>
<td></td>
</tr>
</table>
</div>
</div>
{% endblock %} {% endblock %}
{% block sidebar %} {% block sidebar %}
@ -67,7 +103,7 @@ Hier siehst du alle von dir geleiteten Ausfahrten.
<table> <table>
<tr> <tr>
<td> <td>
<a href="https://nc.cloud-jdav-hd.de/">Nextcloud</a> <a href="{% settings_value 'CLOUD_LINK' %}">Nextcloud</a>
</td> </td>
<td> <td>
Hier liegen Vorlagen für Formulare und nützliche Handbücher. Hier liegen Vorlagen für Formulare und nützliche Handbücher.
@ -75,7 +111,7 @@ Hier siehst du alle von dir geleiteten Ausfahrten.
</tr> </tr>
<tr> <tr>
<td> <td>
<a href="https://dav360.de">DAV 360</a> <a href="{% settings_value 'DAV_360_LINK' %}">DAV 360</a>
</td> </td>
<td> <td>
Zugriff zu Online Office, Teams und deinem DAV Mailaccount. Zugriff zu Online Office, Teams und deinem DAV Mailaccount.
@ -83,10 +119,10 @@ Hier siehst du alle von dir geleiteten Ausfahrten.
</tr> </tr>
<tr> <tr>
<td> <td>
<a href="https://davbgs.sharepoint.com/sites/S-114-O-JDAV-Jugendreferat">Julei-Wiki</a> <a href="{% settings_value 'WIKI_LINK' %}">Julei-Wiki</a>
</td> </td>
<td> <td>
Informationen zum Jugendleiter:in-sein. Informationen zum Jugendleiter*in-sein.
</td> </td>
</tr> </tr>
</table> </table>
@ -103,7 +139,7 @@ weiterweißt oder sonst der Schuh drückt, schreibe eine E-Mail an eine der folg
Jugendreferat Jugendreferat
</td> </td>
<td> <td>
<a href="mailto:jugendreferat@jdav-hd.de">jugendreferat@jdav-hd.de</a> <a href="mailto:{% settings_value 'RESPONSIBLE_MAIL' %}">jugendreferat@jdav-hd.de</a>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -111,7 +147,7 @@ weiterweißt oder sonst der Schuh drückt, schreibe eine E-Mail an eine der folg
Fragen zum Kompass Fragen zum Kompass
</td> </td>
<td> <td>
<a href="mailto:digitales@jdav-hd.de">digitales@jdav-hd.de</a> <a href="mailto:{% settings_value 'DIGITAL_MAIL' %}">digitales@jdav-hd.de</a>
</td> </td>
</tr> </tr>
</table> </table>

@ -0,0 +1,55 @@
{% extends "admin/app_index.html" %}
{% block content %}
<div id="content-main">
<div class="app-mailer module current-app">
<h2>E-Mail Verteiler</h2>
<p>
Hier kannst du E-Mails an deine Gruppe oder an andere Menschen in der JDAV Heidelberg schicken.
</p>
<table>
<tr>
<th scope="row">
<a href='{% url 'admin:mailer_message_add' %}'>Verfassen</a>
</th>
<td></td>
<td></td>
</tr>
<tr>
<th scope="row">
<a href='{% url 'admin:mailer_message_changelist' %}?sent__exact=1'>Gesendet</a>
</th>
<td></td>
<td></td>
</tr>
<tr>
<th scope="row">
<a href='{% url 'admin:mailer_message_changelist' %}?sent__exact=0'>Entwürfe</a>
</th>
<td></td>
<td></td>
</tr>
</table>
{% if perms.mailer.change_emailaddress %}
<br>
<div class="app-mailer module current-app">
<h2>Weiterleitungen</h2>
<p>
Hier kannst du E-Mail Adressen, also Weiterleitungen, konfigurieren.
</p>
<table>
<tr>
<th scope="row">
<a href='{% url 'admin:mailer_emailaddress_changelist' %}'>E-Mail Adressen</a>
</th>
<td></td>
<td></td>
</tr>
</table>
</div>
</div>
{% endif %}
{% endblock %}

@ -5,12 +5,12 @@
<div class="app-members module current-app"> <div class="app-members module current-app">
<h2>Meine Jugendgruppen</h2> <h2>Meine Jugendgruppen</h2>
<p> <p>
Hier siehst du alle Teilnehmer:innen deiner Jugendgruppen und alle von dir geleiteten Ausfahrten. Hier siehst du alle Teilnehmer*innen deiner Jugendgruppen und alle von dir geleiteten Ausfahrten.
</p> </p>
<table> <table>
<tr> <tr>
<th scope="row"> <th scope="row">
<a href="{% url 'admin:members_member_changelist' %}">Teilnehmer:innen</a> <a href="{% url 'admin:members_member_changelist' %}">Teilnehmer*innen</a>
</th> </th>
<td></td> <td></td>
<td></td> <td></td>
@ -24,7 +24,7 @@ Hier siehst du alle Teilnehmer:innen deiner Jugendgruppen und alle von dir gelei
</tr> </tr>
</table> </table>
<table> <table>
{% if perms.members.change_waitinglist %} {% if perms.members.may_manage_waiting_list %}
<br> <br>
<div class="app-members module current-app"> <div class="app-members module current-app">
<h2>Neue Mitglieder</h2> <h2>Neue Mitglieder</h2>

@ -20,20 +20,23 @@
<li> <li>
<form method="post" action="{% url 'admin:members_freizeit_action' original.pk %}"> <form method="post" action="{% url 'admin:members_freizeit_action' original.pk %}">
{% csrf_token %} {% csrf_token %}
<input type="submit" name="notes_list" value="{% trans 'Generate overview' %}"> <input type="submit" name="seminar_report" value="{% trans 'Generate seminar report' %}">
</form> </form>
</li> </li>
<li> <li>
<form method="post" action="{% url 'admin:members_freizeit_action' original.pk %}"> <form method="post" action="{% url 'admin:members_freizeit_action' original.pk %}">
{% csrf_token %} {% csrf_token %}
<input type="submit" name="seminar_report" value="{% trans 'Generate seminar report' %}"> <input type="submit" name="notes_list" value="{% trans 'Generate overview' %}">
</form> </form>
</li> </li>
{% if original.statement and not original.statement.submitted %} {% if original.statement %}
<li> <li>
<a class="historylink" href="{% url 'admin:finance_statementunsubmitted_submit' original.statement.pk %}">{% trans 'Submit statement' %}</a> <form method="post" action="{% url 'admin:members_freizeit_action' original.pk %}">
{% csrf_token %}
<input type="submit" name="finance_overview" value="{% trans 'Finance overview' %}">
</form>
</li> </li>
{% endif %} {% endif %}
{{block.super}} {{block.super}}

@ -0,0 +1,13 @@
{% extends "admin/change_form_object_tools.html" %}
{% load i18n admin_urls %}
{% block object-tools-items %}
<li>
{% url opts|admin_urlname:'demote' original.pk|admin_urlquote as demote_url %}
<a class="historylink" href="{% add_preserved_filters demote_url %}">{% trans 'Demote to waiter' %}</a>
</li>
{{block.super}}
{% endblock %}

@ -0,0 +1,95 @@
{% load i18n nested_admin admin_urls %}
{% with inline_admin_formset.formset.is_nested as is_nested %}
{% with inline_admin_formset.opts as inline_opts %}
<div class="inline-group group djn-group djn-stacked{% if is_nested %} djn-group-nested{% else %} djn-group-root{% endif %}"
id="{{ inline_admin_formset.formset.prefix }}-group"
data-inline-type="stacked"
data-inline-formset="{{ inline_admin_formset.inline_formset_data }}"
data-inline-model="{{ inline_admin_formset.inline_model_id }}">
{% ifinlineclasses %}<fieldset class="djn-fieldset module {{ inline_admin_formset.classes }}" aria-labelledby="{{ inline_admin_formset.formset.prefix }}-heading">{% endifinlineclasses %}
{% if inline_admin_formset.is_collapsible %}<details><summary>{% endif %}
<h2 id="{{ inline_admin_formset.formset.prefix }}-heading" class="inline-heading">
{% if inline_admin_formset.opts.title %}{{ inline_admin_formset.opts.title }}{% else %}{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}{% endif %}
</h2>
{% if inline_admin_formset.is_collapsible %}</summary>{% endif %}
{{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }}
{% if inline_admin_formset.opts.description %}
<div class="description">
<p>
{{ inline_admin_formset.opts.description }}
</p>
</div>
{% endif %}
<div class="items djn-items">
{% with inline_admin_formset.opts.sortable_field_name|default:"" as sortable_field_name %}
{% for inline_admin_form in inline_admin_formset|formsetsort:sortable_field_name %}
{% if forloop.first %}
<div class="djn-item djn-no-drag"><div></div></div>
{% endif %}
<div class="{% if not forloop.last or not inline_admin_formset.has_add_permission %}djn-item{% endif %} djn-module djn-inline-form{% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} djn-empty-form empty-form last-related{% endif %} inline-related"
data-inline-model="{{ inline_admin_form.model_admin.opts.app_label }}-{{ inline_admin_form.model_admin.opts.model_name }}"
{% if inline_admin_form.pk_field.field %}
data-is-initial="{% if inline_admin_form.pk_field.field.value %}true{% else %}false{% endif %}"
{% endif %}
id="{{ inline_admin_formset.formset.prefix }}-{% if forloop.last and inline_admin_formset.has_add_permission %}empty{% else %}{{ inline_admin_form.form|form_index }}{% endif %}">
<h3 class="{% if not inline_opts.sortable_options or not inline_opts.sortable_options.disabled %} djn-drag-handler{% endif %}">
<b>{{ inline_admin_formset.opts.verbose_name|capfirst }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} <a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{% if inline_admin_formset.has_change_permission %}inlinechangelink{% else %}inlineviewlink{% endif %}">{% if inline_admin_formset.has_change_permission %}{% trans "Change" %}{% else %}{% trans "View" %}{% endif %}</a>{% endif %}
{% else %}#{{ forloop.counter }}{% endif %}</span>
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %}
{% if inline_admin_formset.formset.can_delete %}
{% if inline_admin_form.original %}
<span class="delete djn-delete-handler {{ inline_admin_formset.handler_classes|join:" " }}">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>
{% else %}
<span><a class="inline-deletelink djn-remove-handler {{ inline_admin_formset.handler_classes|join:" " }}" href="javascript:void(0)">Remove</a></span>
{% endif %}
{% endif %}
</h3>
{% if inline_admin_form.form.non_field_errors %}
<ul class="errorlist">
<li>{{ inline_admin_form.form.non_field_errors }}</li>
</ul>
{% endif %}
{% if "5.1"|django_version_gte %}
{% with parent_counter=forloop.counter0 %}
{% for fieldset in inline_admin_form %}
{% include inline_admin_formset.opts.fieldset_template with heading_level=4 id_prefix=parent_counter id_suffix=forloop.counter0 %}
{% endfor %}
{% endwith %}
{% else %}
{% for fieldset in inline_admin_form %}
{% include inline_admin_formset.opts.fieldset_template %}
{% endfor %}
{% endif %}
{% if inline_admin_form.has_auto_field or inline_admin_form.needs_explicit_pk_field %}
{{ inline_admin_form.pk_field.field }}
{% endif %}
{% if inline_admin_form.fk_field %}{{ inline_admin_form.fk_field.field }}{% endif %}
{% if inline_admin_form.form.inlines %}
{% for nested in inline_admin_form.form.inlines %}
{% include nested.opts.template with inline_admin_formset=nested %}
{% endfor %}
{% endif %}
</div>
{% endfor %}
{% endwith %}
</div>
<div class="djn-module djn-add-item add-item add-row">
<a href="javascript://" class="add-handler djn-add-handler {{ inline_admin_formset.handler_classes|join:" " }}">
{% blocktrans with inline_admin_formset.opts.verbose_name|strip_parent_name:inline_opts.verbose_name|title as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}
</a>
</div>
{% if inline_admin_formset.is_collapsible %}</details>{% endif %}
{% ifinlineclasses %}</fieldset>{% endifinlineclasses %}
</div>
{% endwith %}{# ends with inline_admin_formset.opts as inline_opts #}
{% endwith %}{# ends {% with inline_admin_formset.formset.is_nested as is_nested %} #}

@ -1,6 +1,7 @@
from django.db import models from django.db import models
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from decimal import Decimal, ROUND_HALF_DOWN
def file_size_validator(max_upload_size): def file_size_validator(max_upload_size):
@ -48,3 +49,14 @@ class RestrictedFileField(models.FileField):
except AttributeError as e: except AttributeError as e:
print(e) print(e)
return data return data
def cvt_to_decimal(f):
return Decimal(f).quantize(Decimal('.01'), rounding=ROUND_HALF_DOWN)
def get_member(request):
if not hasattr(request.user, 'member'):
return None
else:
return request.user.member

Loading…
Cancel
Save