Compare commits

..

1 Commits

4
.gitignore vendored

@ -124,7 +124,3 @@ docker/test/db
docker/development/media
docker/production/media
docker/test/media
*.csv
jdav_web/static/docs

@ -1,3 +1,5 @@
version: "3.9"
x-kompass:
&kompass
image: kompass:development
@ -23,7 +25,6 @@ services:
tty: true
volumes:
- ./../../jdav_web:/app/jdav_web
- ./../../docs:/app/docs
- ./media:/app/media
ports:
- "8000:8000"

@ -33,10 +33,6 @@ cd /app
if ! [ -f /tmp/completed_initial_run ]; then
echo 'Initialising kompass master container'
cd docs
make html
cd /app
python jdav_web/manage.py compilemessages --locale de
# python jdav_web/manage.py makemigrations

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

@ -7,11 +7,6 @@ cd /app
if ! [ -f completed_initial_run ]; then
echo 'Initialising kompass master container'
cd docs
make html
cp -r build/html /app/jdav_web/static/docs
cd /app
python jdav_web/manage.py collectstatic --noinput
python jdav_web/manage.py compilemessages --locale de

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

@ -26,9 +26,6 @@ sendfile on;
keepalive_timeout 65;
# max upload size
client_max_body_size 15M;
#gzip on;
#include /etc/nginx/conf.d/*.conf;

@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

@ -1,28 +0,0 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'Kompass'
copyright = '2024, Christian Merten'
author = 'Christian Merten'
release = '2.0'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = []
templates_path = ['_templates']
exclude_patterns = []
language = 'de'
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'alabaster'
html_static_path = ['_static']

@ -1,53 +0,0 @@
.. _excursions:
Ausfahrten
==========
Neben der :ref:`Teilnehmer\*innenverwaltung <members>` ist das Abwickeln von Ausfahrten
die zweite wichtige Aufgabe des Kompass. Eine Ausfahrt für die eigene Jugendgruppe
anbieten ist neben der ganzen inhaltlichen Arbeit auch jede Menge bürokratischer Aufwand. Der Kompass
versucht dir von diesem Aufwand so viel wie möglich abzunehmen.
Konkret hilft dir der Kompass dabei
- Kriseninterventionslisten zu generieren
- Stadtjugendring oder Landesjugendplan Anträge zu erstellen
- Abrechnungen anzufertigen
.. warning::
Diese Seite ist noch im Aufbau.
Stammdaten
----------
Sobald du mit deinen Co-Jugendleiter\*innen eine Ausfahrt angedacht hast, kannst du diese im Kompass `anlegen`_.
Die bekannten Informationen trägst du schon ein, die noch unbekannten lässt du leer oder trägst
vorläufige Daten ein.
Wenn du weißt wer mitkommt, trägst du im Tab *Teilnehmer\*innen* alle ein, die zur Ausfahrt kommen.
.. _crisis-intervention-list:
Kriseninterventionsliste
------------------------
Bevor die Ausfahrt stattfindet, lässt du dir eine Kriseninterventionsliste mit allen Notfallkontakten der
Mitfahrer\*innen erstellen und schickst sie an die Geschäftsstelle.
Landesjugendplanantrag
----------------------
Möchtest du einen Landesjugendplan- oder SJR Antrag stellen? Dann trage alle Informationen für den
Seminarbericht direkt ein und lass dir den Papierkram vom Kompass erledigen.
SJR Antrag
----------
Abrechnung
----------
Im Nachhinein trägst du deine Ausgaben ein, lädst Belege hoch und reichst deine Abrechnung per Knopfdruck ein.
.. _anlegen: https://jdav-hd.de/kompassmembers/freizeit/add/
.. _Teilnehmer\*innen: https://jdav-hd.de/kompassmembers/member/

@ -1,9 +0,0 @@
Finanzen
========
Auf dieser Seite wird das Einreichen, Bearbeiten und Abwickeln von Abrechnungen
erklärt. Diese Seite ist für Finanzbeauftragte der Sektion gedacht und daher
für die meisten Benutzer\*innen des Kompass unwichtig.
.. warning::
Diese Seite ist noch im Aufbau.

@ -1,77 +0,0 @@
.. _first-steps:
Erste Schritte
==============
Wenn du zum ersten Mal den Kompass deiner Sektion benutzt ist diese
Seite der richtige Einstieg. Wir verfolgen in dieser Anleitung den Jugendleiter
Fritz Walter bei seinen ersten Schritten mit seinem Kompass. Fritz Walter leitet
die Gruppe *Kletterfüchse*.
Wie finde ich die Teilnehmer\*innen meiner Jugendgruppe?
--------------------------------------------------------
Auf der `Startseite`_ siehst du eine Auflistung der von dir geleiteten Jugendgruppen.
Klickst du auf eine der Gruppen landest du in der `Teilnehmer\*innenanzeige`_.
.. image:: images/members_changelist_filters.png
Fritz hat die Gruppe *Kletterfüchse* ausgewählt, wie du oben rechts sehen kannst.
Versuche einmal dort bei dir eine andere Gruppe auszuwählen. Falls dir keine Teilnehmer\*innen
angezeigt werden liegt das daran, dass deine *Zugriffsrechte* nicht ausreichen.
Wie ändere ich eine\*n Teilnehmer\*in meiner Jugendgruppe?
----------------------------------------------------------
Fritz möchte das eingetragene Geburtsdatum von *Lisa Lotte* ändern. Dazu klickt
er auf den entsprechenden Eintrag, ändert das Geburtsdatum und klickt auf *Speichern*.
.. note::
Nicht alle Einträge in der `Teilnehmer\*innenanzeige`_ sind klickbar. Das liegt daran,
dass du manche Teilnehmer\*innen zwar sehen, aber nicht ihre Details einsehen kannst.
Manche Einträge wiederum kannst du einsehen, aber nicht bearbeiten.
Probier doch einmal aus deinen eigenen Eintrag zu ändern. Sicherlich gibt es einige
Felder, die nicht ausgefüllt oder nicht mehr aktuell sind.
Wie schicke ich eine E-Mail an meine Gruppe?
--------------------------------------------
Nachdem Fritz die Daten seiner Gruppe auf den neusten Stand gebracht hat, möchte er nun
eine E-Mail über die bevorstehende Hallenübernachtung an seine Gruppe schreiben. Dazu
geht er zurück auf die `Startseite`_ und wählt `Nachricht verfassen`_ aus.
Als Empfänger wählt er im Feld *An Gruppe* die *Kletterfüchse* aus. Damit seine
Co-Jugendleiterin Julia auch die Antworten erhält, wählt er im Feld
*Antwort an Teilnehmer* sowohl sich selbst, als auch Julia aus. Schließlich
klickt er auf *Speichern und Email senden*, um die Nachricht zu verschicken.
.. note::
Es kann sein, dass über den Kompass verschickte E-Mails nur verzögert ankommen. Das
liegt daran, dass pro Minute stets nur 10 E-Mails verschickt werden um Stau
zu verhindern.
Probier doch mal aus dir selbst eine Nachricht zu schicken. Wähle einfach im Feld
*An Teilnehmer* dich selbst aus.
Wie organisiere ich eine Ausfahrt?
----------------------------------
Nun da Fritz seine Gruppe zur Hallenübernachtung eingeladen hat, möchte er die
Ausfahrt auch im Kompass anlegen. Dazu navigiert er zurück zur `Startseite`_ und wählt
`Ausfahrten`_ aus.
Dort wählt er oben rechts *Ausfahrt hinzufügen* aus und füllt die verschiedenen Felder
aus. Im Reiter *Teilnehmer* trägt er bereits Julia und sich selbst ein, die stehen ja
schließlich schon fest. Schließlich speichert er die Ausfahrt mit *Sichern*.
Wie geht es weiter?
-------------------
Nun hat Fritz den Bürokratiekram für heute erledigt. Du willst noch mehr wissen? Dann
geh zurück zum :ref:`index`.
.. _Startseite: https://jdav-hd.de/kompass
.. _Teilnehmer\*innenanzeige: https://jdav-hd.de/kompassmembers/member/
.. _Nachricht verfassen: https://jdav-hd.de/kompassmailer/message/add/
.. _Ausfahrten: https://jdav-hd.de/kompassmembers/freizeit/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

@ -1,43 +0,0 @@
.. Kompass documentation master file, created by
sphinx-quickstart on Sun Nov 24 18:37:20 2024.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
=======
Kompass
=======
Der Kompass ist dein Kompass in der Jugendarbeit in deiner JDAV Sektion. Wenn du das
erste mal hier bist, schau doch mal :ref:`first-steps` an.
Was ist der Kompass?
--------------------
Der Kompass ist eine Verwaltungsplattform für die tägliche Jugendarbeit in der JDAV.
Die wichtigsten Funktionen sind
- Verwaltung von Teilnehmer\*innen von Jugendgruppen
- Organisation von Ausfahrten
- Abwicklung von Abrechnungen
- Senden von E-Mails
Neben diesen Funktionen für die tägliche Arbeit, automatisiert der Kompass die
Aufnahme von neuen Mitgliedern und die Pflege der Daten durch
- Wartelistenverwaltung
- Registrierung neuer Mitglieder
- Rückmeldeverfahren
.. _index:
Inhaltsverzeichnis
------------------
.. toctree::
:maxdepth: 2
getstarted
members
excursions
waitinglist
finance

@ -1,170 +0,0 @@
.. _members:
Jugendgruppenverwaltung
=======================
Das wichtigste Objekt im Kompass ist ein\*e Teilnehmer\*in. Hier meint ein\*e Teilnehmer\*in ein im
Kompass hinterlegtes Mitglied der JDAV deiner Sektion, das heißt ob 5-jähriges Jugendgruppenkind,
langgediente\*r Jugendleiter\*in oder frischgebackene\*r Jugendreferent\*in, alle haben
einen Eintrag als Teilnehmer\*in im Kompass. Insbesondere heißt das, dass auch du selbst hier einen
Eintrag hast.
Der Startpunkt der Teilnehmer\*innenverwaltung ist der Abschnitt `Meine Jugendgruppe`_. Hier siehst du
in der Regel zwei Menüpunkte:
- Teilnehmer\*innen
- Ausfahrten
In diesem Abschnitt geht es nur um den ersten Menüpunkt. Falls du etwas über den zweiten Menüpunkt
lernen möchtest, kannst du zu :ref:`excursions` springen.
.. note::
Falls du ein Amt in deiner Sektion ausübst und zum Beispiel für Jugendgruppenkoordination
oder die Verwaltung der Warteliste zuständig ist, siehst du hier noch mehr Punkte. Mehr
Informationen dazu findest du unter :ref:`waitinglist`.
Teilnehmer\*innen Übersicht
---------------------------
Um eine Übersicht über alle Teilnehmer\*innen zu bekommen, klicke auf `Teilnehmer\*innen`_. Hier siehst du
nun alle Mitglieder, für die du die einfachen Anzeigeberechtigungen hast, das heißt deren Namen du sehen darfst.
Typischerweise sind das die Gruppenkinder deiner Jugendgruppe, aber vielleicht noch zusätzlich alle Mitglieder
des Jugendausschuss.
Wie sehe ich meine Gruppenkinder?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Oberhalb der großen Auflistung mit allen Teilnehmer\*innen siehst du verschiedene Auswahlfelder.
Eines davon heißt *Nach Gruppe*. Wenn du dort drauf klickst, kannst du die Ansicht nach einer Gruppe
filtern.
.. image:: images/members_changelist_group_filter.png
In der selben Zeile siehst du noch weitere Filtermöglichkeiten.
Ich möchte nach Alter sortieren, wie geht das?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Standardmäßig ist die Teilnehmer\*innenanzeige nach Nachname sortiert, wie du im folgenden Bild an dem
kleinen Pfeil erkennen kannst:
.. image:: images/members_changelist_sorting.png
Um zum Beispiel nach Geburtsdatum zu sortieren, klicke auf die Spalte *Geburtsdatum*. Wenn du die Reihenfolge
(das heißt von jung nach alt oder von alt nach jung), klicke auf den kleinen Pfeil im *Geburtsdatum* Reiter.
Wieso sehe ich nicht alle meine Gruppenkinder?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Hast du deine Gruppe ausgewählt und siehst trotzdem nicht alle deine Gruppenkinder auf einer Seite?
Dann liegt das vermutlich daran, dass deine Gruppe mehr als 25 Teilnehmer\*innen hat. Chapeau!
In diesem Fall kannst du unten Rechts auf der Seite zwischen den verschiedenen Seiten auswählen oder
alle auf einmal anzeigen lassen:
.. image:: images/members_changelist_pages.png
.. _Meine Jugendgruppe: https://jdav-hd.de/kompassmembers
.. _Teilnehmer\*innen: https://jdav-hd.de/kompassmembers/member/
Teilnehmer\*in Detailansicht
----------------------------
Möchtest du eine\*n Teilnehmer\*in im Detail ansehen, um zum Beispiel Personendaten, wie die Anschrift
nachzuschauen oder eine Änderung an den Daten machen, klicke auf den entsprechenden Eintrag in der Liste.
Die nun folgende Seite kann auf den ersten Blick ein wenig erschlagen, daher dröseln wir hier die wichtigsten
Punkte auf. Zunächst ist die Seite in mehrere Reiter unterteilt:
.. image:: images/members_change_tabs.png
Diese sind
- Allgemein: wichtigste Informationen wie Name und E-Mail Adresse
- Kontaktinformationen: Anschrift, Kontodaten (für Jugendleiter\*innen beim Abwickeln von Ausfahrten)
- Fähigkeiten: z.B. alpine Erfahrungen
- Sonstiges: z.B. medizinische Daten
- Notfallkontakte: Liste mit Namen und mindestens Telefonnummern. Mehr Informationen
unter :ref:`emergency-contacts`.
- Fortbildungen: eine Liste von besuchten Fortbildungen.
.. note::
Der Reiter *Fortbildungen* wird nur auf deiner Seite angezeigt, das heißt falls du eines deiner
Gruppenkinder ausgewählt hast, ist dieser Reiter nicht vorhanden.
Wieso kann ich nicht alle Felder ändern?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Manche Felder werden dir nur angezeigt, sind aber nicht änderbar. Das sind entweder
- geschützte Felder, für die du besondere Berechtigungen benötigst um sie zu ändern
(z.B. das *Gruppe* Feld). Um diese Felder zu ändern, wende dich an deine\*n Jugendreferent\*in
für Jugendkoordination. Oder,
- automatisch berechnete Felder wie zum Beispiel das *Rückgemeldet* Feld.
Wieso haben manche Einträge in der Teilnehmer\*innenübersicht keinen Link?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Die Teilnehmer\*innen die dir in der Übersicht angezeigt werden sind diejenigen für die du
einfache Ansichtberechtigungen hast. Um die Personendetails eines\*einer Teilnehmer\*in einzusehen,
benötigst du normale Ansichtberechtigungen. Falls du diese nicht hast, wird anstatt des Links
in der Übersicht nur der Name angezeigt.
Falls du denkst, dass du eine\*n Teilnehmer\*in einsehen können solltest, aber es nicht kannst, melde
dich gerne bei deine\*r Jugendreferent\*in für Jugendkoordination.
.. _echo:
Rückmeldung
-----------
Damit die Teilnehmer\*innendaten im Kompass aktuell bleiben, kannst du jederzeit deine Gruppenkinder
zu einer Rückmeldung auffordern. Dazu wählst du in der Teilnehmer\*innenübersicht alle
Teilnehmer\*innen aus, die du zur Rückmeldung auffordern möchtest,
.. image:: images/members_changelist_action.png
und wählst dann im Menü unten links *Rückmeldungsaufforderungen an ausgewählte Teilnehmer\*innen verschicken*
aus. Um die Aufforderungen zu verschicken, musst du dann nur noch auf *Ausführen* klicken.
Was passiert nach der Aufforderung zur Rückmeldung?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Der\*die ausgewählte Teilnehmer\*in erhält eine E-Mail mit einem Link. Dieser Link führt auf eine
Seite auf der die Person ihr Geburtsdatum eingeben muss.
.. note::
Das Geburtsdatumsformat ist TT.MM.JJJJ, also wenn Peter am
1.4.1999 geboren ist, müsste er *01.04.1999* eingeben.
Nach erfolgreich eingegebenem Geburtsdatum, wird die Person auf ein Formular mit ihren Daten weitergeleitet.
Dann einfach prüfen, gegebenenfalls aktualisieren und schließlich speichern. Der Link ist
immer 30 Tage lang gültig und kann in dieser Zeit auch beliebig oft benutzt werden.
Klingt alles noch abstrakt? Dann fordere doch mal dich selbst zur Rückmeldung auf und probiere es aus.
.. _emergency-contacts:
Notfallkontakte
---------------
Im Notfall helfen uns die Anschrift oder Telefonnummer einer\*eines Teilnehmer\*in nicht weiter. Stattdessen
benötigen wir Kontaktdaten von Personen, die wir im Notfall kontaktieren können. Diese können
im Reiter *Notfallkontakte* gepflegt werden. Bei der initialen Registrierung muss jede\*r Teilnehmer\*in
mindestens einen Notfallkontakt angeben.
.. note::
Bei vielen Teilnehmer\*innen sind keine Notfallkontakte eingetragen. Das liegt dann vermutlich daran,
dass die aus einem anderen System migriert wurden und daher nicht verfügbar sind.
Bei der regelmäßigen :ref:`echo` werden die Notfallkontakte ebenfalls abgefragt. Falls
du bei einem deiner Gruppenkinder feststellst, dass die Notfallkontakte fehlen
oder nicht mehr aktuell sind, trage das so schnell wie möglich nach oder benutze die :ref:`echo`.
Was bringen mir die Notfallkontakte im Kompass?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Passiert ein Notfall auf einer Ausfahrt, wirst du natürlich nicht immer die Möglichkeit
haben im Kompass die Notfallkontakte herauszusuchen. Daher kannst du dir zu jeder Ausfahrt
eine :ref:`crisis-intervention-list` generieren lassen, die zu allen Teilnehmer\*innen deiner Ausfahrt
auch alle Notfallkontakte auflistet.

@ -1,10 +0,0 @@
.. _waitinglist:
Warteliste und neue Mitglieder
==============================
Hier wird die Warteliste erklärt und verschiedene Möglichkeiten erläutert, wie
neue Teilnehmer\*innen angelegt werden können.
.. warning::
Diese Seite ist noch im Aufbau.

@ -13,37 +13,17 @@ from rules.permissions import perm_exists
class FieldPermissionsAdminMixin:
field_change_permissions = {}
field_view_permissions = {}
def may_view_field(self, field_desc, request, obj=None):
if not type(field_desc) is tuple:
field_desc = (field_desc,)
for fd in field_desc:
if fd not in self.field_view_permissions:
continue
if not request.user.has_perm(self.field_view_permissions[fd]):
return False
return True
def get_fieldsets(self, request, obj=None):
fieldsets = super(FieldPermissionsAdminMixin, self).get_fieldsets(request, obj)
d = []
for title, attrs in fieldsets:
allowed = [f for f in attrs['fields'] if self.may_view_field(f, request, obj)]
if len(allowed) == 0:
continue
d.append((title, dict(attrs, **{'fields': allowed})))
return d
field_permissions = {}
def get_fields(self, request, obj=None):
fields = super(FieldPermissionsAdminMixin, self).get_fields(request, obj)
return [fd for fd in fields if self.may_view_field(fd, request, obj)]
def get_readonly_fields(self, request, obj=None):
readonly_fields = super(FieldPermissionsAdminMixin, self).get_readonly_fields(request, obj)
return list(readonly_fields) +\
[fd for fd, perm in self.field_change_permissions.items() if not request.user.has_perm(perm)]
def may_field(field):
if field not in self.field_permissions:
return True
return request.user.has_perm(self.field_permissions[field], obj)
return list(filter(may_field, fields))
class ChangeViewAdminMixin:

@ -1,9 +0,0 @@
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,7 +9,6 @@ from django.shortcuts import render
from django.conf import settings
from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin
from utils import get_member
from rules.contrib.admin import ObjectPermissionsModelAdmin
@ -119,16 +118,6 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
inlines = [BillOnSubmittedStatementInline, TransactionOnSubmittedStatementInline]
def has_add_permission(self, request, obj=None):
# Submitted statements should not be added directly, but instead be created
# as unsubmitted statements and then submitted.
return False
def has_change_permission(self, request, obj=None):
return 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):
@ -213,20 +202,32 @@ class StatementSubmittedAdmin(admin.ModelAdmin):
messages.error(request,
_("%(name)s already has transactions. Please delete them first, if you want to generate new ones") % {'name': str(statement)})
else:
success = statement.generate_transactions()
if success:
messages.success(request,
_("Successfully generated transactions for %(name)s") % {'name': str(statement)})
else:
messages.error(request,
_("Error while generating transactions for %(name)s. Do all bills have a payer?") % {'name': str(statement)})
statement.generate_transactions()
messages.success(request,
_("Successfully generated transactions for %(name)s") % {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
context = dict(self.admin_site.each_context(request),
title=_('View submitted statement'),
opts=self.opts,
statement=statement,
transaction_issues=statement.transaction_issues,
**statement.template_context())
total_bills=statement.total_bills,
total=statement.total)
if statement.excursion is not None:
context = dict(context,
nights=statement.excursion.night_count,
price_per_night=statement.real_night_cost,
duration=statement.excursion.duration,
staff_count=statement.real_staff_count,
kilometers_traveled=statement.excursion.kilometers_traveled,
means_of_transport=statement.excursion.get_tour_approach(),
euro_per_km=statement.euro_per_km,
allowance_per_day=settings.ALLOWANCE_PER_DAY,
nights_per_yl=statement.nights_per_yl,
allowance_per_yl=statement.allowance_per_yl,
transportation_per_yl=statement.transportation_per_yl,
total_per_yl=statement.total_per_yl,
total_staff=statement.total_staff)
return render(request, 'admin/overview_submitted_statement.html', context=context)
@ -245,68 +246,13 @@ class StatementConfirmedAdmin(admin.ModelAdmin):
#readonly_fields = fields
list_display = ['__str__', 'total_pretty', 'confirmed_date', 'confirmed_by']
ordering = ('-confirmed_date',)
inlines = [BillOnSubmittedStatementInline, TransactionOnSubmittedStatementInline]
def has_add_permission(self, request, obj=None):
# To preserve integrity, no one is allowed to add confirmed statements
return False
def has_change_permission(self, request, obj=None):
# To preserve integrity, no one is allowed to change confirmed statements
return False
def has_delete_permission(self, request, obj=None):
# To preserve integrity, no one is allowed to delete confirmed statements
return False
def get_urls(self):
urls = super().get_urls()
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>/unconfirm/",
wrap(self.unconfirm_view),
name="%s_%s_unconfirm" % (self.opts.app_label, self.opts.model_name),
),
]
return custom_urls + urls
def unconfirm_view(self, request, object_id):
statement = StatementConfirmed.objects.get(pk=object_id)
if not statement.confirmed:
messages.error(request,
_("%(name)s is not yet confirmed.") % {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,)))
if "unconfirm" in request.POST:
statement.confirmed = False
statement.confirmed_date = None
statement.confired_by = None
statement.save()
messages.success(request,
_("Successfully unconfirmed %(name)s. I hope you know what you are doing.")
% {'name': str(statement)})
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
context = dict(self.admin_site.each_context(request),
title=_('Unconfirm statement'),
opts=self.opts,
statement=statement)
return render(request, 'admin/unconfirm_statement.html', context=context)
@admin.register(Transaction)
class TransactionAdmin(admin.ModelAdmin):
"""The transaction admin site. This is only used to display transactions. All editing
is disabled on this site. All transactions should be changed on the respective statement
at the correct stage of the approval chain."""
list_display = ['member', 'ledger', 'amount', 'reference', 'statement', 'confirmed',
'confirmed_date', 'confirmed_by']
list_filter = ('ledger', 'member', 'statement', 'confirmed')
@ -318,21 +264,16 @@ class TransactionAdmin(admin.ModelAdmin):
return self.fields
return super(TransactionAdmin, self).get_readonly_fields(request, obj)
def has_add_permission(self, request, obj=None):
# To preserve integrity, no one is allowed to add transactions
return False
def has_change_permission(self, request, obj=None):
# To preserve integrity, no one is allowed to change transactions
return False
def has_delete_permission(self, request, obj=None):
# To preserve integrity, no one is allowed to delete transactions
return False
@admin.register(Bill)
class BillAdmin(admin.ModelAdmin):
list_display = ['__str__', 'statement', 'explanation', 'pretty_amount', 'paid_by', 'refunded']
list_display = ['__str__', 'statement', 'short_description', 'pretty_amount', 'paid_by', 'refunded']
list_filter = ('statement', 'paid_by', 'refunded')
search_fields = ('reference', 'statement')
def get_member(request):
if not hasattr(request.user, 'member'):
return None
else:
return request.user.member

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-01 16:23+0100\n"
"POT-Creation-Date: 2023-03-20 18:48+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,12 +18,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: finance/admin.py:76
#: finance/admin.py:69
#, python-format
msgid "%(name)s is already submitted."
msgstr "%(name)s ist bereits eingereicht."
#: finance/admin.py:82
#: finance/admin.py:75
#, python-format
msgid ""
"Successfully submited %(name)s. The finance department will notify the "
@ -32,23 +32,23 @@ msgstr ""
"Rechnung %(name)s erfolgreich eingereicht. Das Finanzreferat wird auf dich "
"sobald wie möglich zukommen."
#: finance/admin.py:85
#: finance/admin.py:78
msgid "Submit statement"
msgstr "Rechnung einreichen"
#: finance/admin.py:162
#: finance/admin.py:152
#, python-format
msgid "%(name)s is not yet submitted."
msgstr "%(name)s ist noch nicht eingereicht."
#: finance/admin.py:169
#: finance/admin.py:159
#, python-format
msgid "An error occured while trying to confirm %(name)s. Please try again."
msgstr ""
"Beim Abwickeln von %(name)s ist ein Fehler aufgetreten. Bitte versuche es "
"erneut."
#: finance/admin.py:173
#: finance/admin.py:163
#, python-format
msgid ""
"Successfully confirmed %(name)s. I hope you executed the associated "
@ -57,11 +57,11 @@ msgstr ""
"Erfolgreich %(name)s abgewickelt. Ich hoffe du hast die zugehörigen "
"Überweisungen ausgeführt, ich werde dich nicht nochmal erinnern."
#: finance/admin.py:180
#: finance/admin.py:170
msgid "Statement confirmed"
msgstr "Abrechnung abgewickelt"
#: finance/admin.py:186
#: finance/admin.py:176
msgid ""
"Transactions do not match the covered expenses. Please correct the mistakes "
"listed below."
@ -69,19 +69,19 @@ msgstr ""
"Überweisungen stimmen nicht mit den übernommenen Kosten überein. Bitte "
"korrigiere die unten aufgeführten Fehler."
#: finance/admin.py:191
#: finance/admin.py:181
msgid "Some transactions have no ledger configured. Please fill in the gaps."
msgstr ""
"Manche Überweisungen haben kein Geldtopf eingestellt. Bitte trage das nach."
#: finance/admin.py:200
#: finance/admin.py:190
#, python-format
msgid "Successfully rejected %(name)s. The requestor can reapply, when needed."
msgstr ""
"Die Rechnung %(name)s wurde abgelehnt. Die Person kann die Rechnung erneut "
"einstellen, wenn es benötigt wird."
#: finance/admin.py:207
#: finance/admin.py:197
#, python-format
msgid ""
"%(name)s already has transactions. Please delete them first, if you want to "
@ -90,20 +90,12 @@ msgstr ""
"%(name)s hat bereits Überweisungen. Bitte lösche diese zunächst, bevor du "
"neue generierst."
#: finance/admin.py:212
#: finance/admin.py:201
#, python-format
msgid "Successfully generated transactions for %(name)s"
msgstr "Automatisch Überweisungsträger für %(name)s generiert."
#: finance/admin.py:215
#, python-format
msgid ""
"Error while generating transactions for %(name)s. Do all bills have a payer?"
msgstr ""
"Fehler beim Erzeugen der Überweisungsträger für %(name)s. Sind für alle "
"Quittungen eine bezahlende Person eingestellt? "
#: finance/admin.py:218
#: finance/admin.py:204
msgid "View submitted statement"
msgstr "Eingereichte Abrechnung einsehen"
@ -112,185 +104,165 @@ msgstr "Eingereichte Abrechnung einsehen"
msgid "Successfully reduced transactions for %(name)s."
msgstr "Überweisungsträger für %(name)s minimiert."
#: finance/admin.py:274
#, python-format
msgid "%(name)s is not yet confirmed."
msgstr "%(name)s ist noch nicht bestätigt."
#: finance/admin.py:283
#, python-format
msgid "Successfully unconfirmed %(name)s. I hope you know what you are doing."
msgstr ""
"Erfolgreich die Bestätigung von %(name)s zurückgenommen. Ich hoffe du weißt "
"was du machst."
#: finance/admin.py:288 finance/templates/admin/unconfirm_statement.html:26
msgid "Unconfirm statement"
msgstr "Bestätigung zurücknehmen"
#: finance/apps.py:8
msgid "Finance"
msgstr "Finanzen"
#: finance/models.py:21
#: finance/models.py:13
msgid "Name"
msgstr "Name"
#: finance/models.py:27 finance/models.py:472 finance/models.py:496
#: finance/models.py:19 finance/models.py:372 finance/models.py:396
#: finance/templates/admin/confirmed_statement.html:38
#: finance/templates/admin/overview_submitted_statement.html:100
msgid "Ledger"
msgstr "Geldtopf"
#: finance/models.py:28
#: finance/models.py:20
msgid "Ledgers"
msgstr "Geldtöpfe"
#: finance/models.py:48 finance/models.py:415 finance/models.py:495
#: finance/models.py:42 finance/models.py:343 finance/models.py:395
msgid "Short description"
msgstr "Kurzbeschreibung"
#: finance/models.py:51 finance/models.py:416
#: finance/models.py:45 finance/models.py:344
msgid "Explanation"
msgstr "Erklärung"
#: finance/models.py:53
#: finance/models.py:47
msgid "Associated excursion"
msgstr "Zugehörige Ausfahrt"
msgstr "Zugehörige Freizeit"
#: finance/models.py:58
#: finance/models.py:52
msgid "Price per night"
msgstr "Preis pro Nacht"
#: finance/models.py:60
#: finance/models.py:54
msgid "Submitted"
msgstr "Eingericht"
#: finance/models.py:61
#: finance/models.py:55
msgid "Submitted on"
msgstr "Eingereicht am"
#: finance/models.py:62
#: finance/models.py:56
msgid "Confirmed"
msgstr "Abgewickelt"
#: finance/models.py:63 finance/models.py:479
#: finance/models.py:57 finance/models.py:379
msgid "Paid on"
msgstr "Bezahlt am"
#: finance/models.py:65
msgid "Created by"
msgstr "Erstellt von"
#: finance/models.py:70
#: finance/models.py:59
msgid "Submitted by"
msgstr "Eingereicht von"
msgstr "Eingereicht bei"
#: finance/models.py:75 finance/models.py:480
#: finance/models.py:64 finance/models.py:380
msgid "Authorized by"
msgstr "Autorisiert von"
#: finance/models.py:82 finance/models.py:414 finance/models.py:475
#: finance/models.py:71 finance/models.py:342 finance/models.py:375
msgid "Statement"
msgstr "Abrechnung"
#: finance/models.py:83
#: finance/models.py:72
msgid "Statements"
msgstr "Abrechnungen"
#: finance/models.py:98
#: finance/models.py:77
#, python-format
msgid "Statement: %(excursion)s"
msgstr "Abrechnung: %(excursion)s"
#: finance/models.py:150
#: finance/models.py:123
msgid "Ready to confirm"
msgstr "Bereit zur Abwicklung"
#: finance/models.py:194
#: finance/models.py:162
#, python-format
msgid "Compensation for %(excu)s"
msgstr "Entschädigung für %(excu)s"
#: finance/models.py:327
#: finance/models.py:294
#: finance/templates/admin/overview_submitted_statement.html:78
msgid "Total"
msgstr "Gesamtbetrag"
#: finance/models.py:369
#: finance/models.py:307
msgid "Statement in preparation"
msgstr "Abrechnung in Vorbereitung"
#: finance/models.py:370
#: finance/models.py:308
msgid "Statements in preparation"
msgstr "Abrechnungen in Vorbereitung"
#: finance/models.py:389
#: finance/models.py:321
msgid "Submitted statement"
msgstr "Eingereichte Abrechnung"
#: finance/models.py:390
#: finance/models.py:322
msgid "Submitted statements"
msgstr "Eingereichte Abrechnungen"
#: finance/models.py:406
#: finance/models.py:336
msgid "Paid statement"
msgstr "Bezahlte Abrechnung"
#: finance/models.py:407
#: finance/models.py:337
msgid "Paid statements"
msgstr "Bezahlte Abrechnungen"
#: 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
#: finance/models.py:347
msgid "Paid by"
msgstr "Bezahlt von"
#: finance/models.py:421
#: finance/models.py:349
msgid "Covered"
msgstr "Übernommen"
#: finance/models.py:422
#: finance/models.py:350
msgid "Refunded"
msgstr "Ausgezahlt"
#: finance/models.py:424
#: finance/models.py:352
msgid "Proof"
msgstr "Beleg"
#: finance/models.py:435 finance/models.py:442 finance/models.py:455
#: finance/models.py:360 finance/models.py:369
#: 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:363
msgid "Bill"
msgstr "Ausgabe"
msgstr "Quittung"
#: finance/models.py:436 finance/models.py:443 finance/models.py:456
#: finance/models.py:364
#: finance/templates/admin/overview_submitted_statement.html:26
msgid "Bills"
msgstr "Ausgaben"
msgstr "Quittungen"
#: finance/models.py:468 finance/templates/admin/confirmed_statement.html:37
#: finance/models.py:368 finance/templates/admin/confirmed_statement.html:37
#: finance/templates/admin/overview_submitted_statement.html:99
msgid "Reference"
msgstr "Verwendungszweck"
#: finance/models.py:470
#: finance/models.py:370
msgid "Recipient"
msgstr "Empfänger"
#: finance/models.py:478
#: finance/models.py:378
msgid "Paid"
msgstr "Bezahlt"
#: finance/models.py:490
#: finance/models.py:390
msgid "Transaction"
msgstr "Überweisung"
#: finance/models.py:491
#: finance/models.py:391
#: finance/templates/admin/overview_submitted_statement.html:84
msgid "Transactions"
msgstr "Überweisungen"
@ -298,7 +270,6 @@ msgstr "Überweisungen"
#: finance/templates/admin/confirmed_statement.html:17
#: finance/templates/admin/overview_submitted_statement.html:17
#: finance/templates/admin/submit_statement.html:17
#: finance/templates/admin/unconfirm_statement.html:17
msgid "Home"
msgstr "Start"
@ -345,13 +316,13 @@ msgstr "Der Gesamtbetrag beträgt %(total_bills)s €."
#: finance/templates/admin/overview_submitted_statement.html:54
msgid "Excursion"
msgstr "Ausfahrt"
msgstr "Freizeit"
#: finance/templates/admin/overview_submitted_statement.html:57
#, python-format
msgid "This excursion featured %(staff_count)s youth leader(s), each costing"
msgstr ""
"Diese Ausfahrt hatte %(staff_count)s Jugendleiter*innen. Auf jede*n "
"Diese Freizeit hatte %(staff_count)s Jugendleiter:innen. Auf jede:n "
"entfallen die folgenden Kosten:"
#: finance/templates/admin/overview_submitted_statement.html:62
@ -445,7 +416,6 @@ msgstr "Ablehnen"
#: finance/templates/admin/overview_submitted_statement.html:178
#: finance/templates/admin/submit_statement.html:35
#: finance/templates/admin/unconfirm_statement.html:39
msgid "Cancel"
msgstr "Abbruch"
@ -465,24 +435,3 @@ msgid ""
msgstr ""
"Möchtest du die Abrechnung beim Finanzreferat einreichen? Wenn du "
"fortschreitest, sind keine weiteren Änderungen an der Abrechnung möglich."
#: finance/templates/admin/unconfirm_statement.html:21
#: finance/templates/admin/unconfirm_statement.html:38
msgid "Unconfirm"
msgstr "Bestätigung zurücknehmen"
#: finance/templates/admin/unconfirm_statement.html:29
msgid ""
"You are entering risk zone! Do you really want to manually set this "
"statement back to unconfirmed?"
msgstr ""
"Du bewegst dich in einer Risiko Zone! Möchtest du wirklich manuell die "
"Bestätigung dieser Abrechnung zurücknehmen?"
#: finance/templates/admin/unconfirm_statement.html:36
msgid ""
"I am aware that this is not a standard procedure and this might cause data "
"integrity issues."
msgstr ""
"Mir ist bewusst, dass das keine Standardprozedur ist und das dies die "
"Integrität der Daten zerstören kann."

@ -1,18 +0,0 @@
# 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,14 +6,12 @@ from .rules import is_creator, not_submitted, leads_excursion
from members.rules import is_leader, statement_not_submitted
from django.db import models
from django.db.models import Sum
from django.utils.translation import gettext_lazy as _
from members.models import Member, Freizeit, OEFFENTLICHE_ANREISE, MUSKELKRAFT_ANREISE
from django.conf import settings
import rules
from contrib.models import CommonModel
from contrib.rules import has_global_perm
from utils import cvt_to_decimal
# Create your models here.
@ -30,7 +28,7 @@ class Ledger(models.Model):
class TransactionIssue:
def __init__(self, member, current, target):
self.member, self.current, self.target = member, current, target
self.member, self.current, self. target = member, current, target
@property
def difference(self):
@ -107,7 +105,7 @@ class Statement(CommonModel):
@property
def transaction_issues(self):
needed_paiments = [(b.paid_by, b.amount) for b in self.bill_set.all() if b.costs_covered and b.paid_by]
needed_paiments = [(b.paid_by, b.amount) for b in self.bill_set.all() if b.costs_covered]
if self.excursion is not None:
needed_paiments.extend([(yl, self.real_per_yl) for yl in self.excursion.jugendleiter.all()])
@ -181,19 +179,16 @@ class Statement(CommonModel):
for bill in self.bill_set.all():
if not bill.costs_covered:
continue
if not bill.paid_by:
return False
ref = "{}: {}".format(str(self), bill.short_description)
Transaction(statement=self, member=bill.paid_by, amount=bill.amount, confirmed=False, reference=ref).save()
# excursion specific
if self.excursion is None:
return True
return
for yl in self.excursion.jugendleiter.all():
ref = _("Compensation for %(excu)s") % {'excu': self.excursion.name}
Transaction(statement=self, member=yl, amount=self.real_per_yl, confirmed=False, reference=ref).save()
return True
def reduce_transactions(self):
# to minimize the number of needed bank transactions, we bundle transactions from same ledger to
@ -285,7 +280,7 @@ class Statement(CommonModel):
if self.excursion is None:
return 0
return cvt_to_decimal(self.total_staff / self.excursion.staff_count)
return self.total_staff / self.excursion.staff_count
@property
def total_staff(self):
@ -326,37 +321,6 @@ class Statement(CommonModel):
return "{}".format(self.total)
total_pretty.short_description = _('Total')
def template_context(self):
context = {
'total_bills': self.total_bills,
'total_bills_theoretic': self.total_bills_theoretic,
'total': self.total,
}
if self.excursion:
excursion_context = {
'nights': self.excursion.night_count,
'price_per_night': self.real_night_cost,
'duration': self.excursion.duration,
'staff_count': self.real_staff_count,
'kilometers_traveled': self.excursion.kilometers_traveled,
'means_of_transport': self.excursion.get_tour_approach(),
'euro_per_km': self.euro_per_km,
'allowance_per_day': settings.ALLOWANCE_PER_DAY,
'nights_per_yl': self.nights_per_yl,
'allowance_per_yl': self.allowance_per_yl,
'transportation_per_yl': self.transportation_per_yl,
'total_per_yl': self.total_per_yl,
'total_staff': self.total_staff,
}
return dict(context, **excursion_context)
else:
return context
def grouped_bills(self):
return self.bill_set.values('short_description')\
.order_by('short_description')\
.annotate(amount=Sum('amount'))
class StatementUnSubmittedManager(models.Manager):
def get_queryset(self):
@ -417,7 +381,7 @@ class Bill(CommonModel):
short_description = models.CharField(verbose_name=_('Short description'), max_length=30)
explanation = models.TextField(verbose_name=_('Explanation'), blank=True)
amount = models.DecimalField(verbose_name=_('Amount'), max_digits=6, decimal_places=2, default=0)
amount = models.DecimalField(max_digits=6, decimal_places=2, default=0)
paid_by = models.ForeignKey(Member, verbose_name=_('Paid by'), null=True,
on_delete=models.SET_NULL)
costs_covered = models.BooleanField(verbose_name=_('Covered'), default=False)
@ -499,3 +463,7 @@ class Receipt(models.Model):
on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=6, decimal_places=2)
comments = models.TextField()
def cvt_to_decimal(f):
return Decimal(f).quantize(Decimal('.01'), rounding=ROUND_HALF_DOWN)

@ -1,41 +0,0 @@
{% 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 }} admin-view
{% 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' statement.pk|admin_urlquote %}">{{ statement|truncatewords:"18" }}</a>
&rsaquo; {% translate 'Unconfirm' %}
</div>
{% endblock %}
{% block content %}
<h2>{% translate "Unconfirm statement" %}</h2>
<p>
{% blocktrans %}You are entering risk zone! Do you really want to manually set this statement back to unconfirmed?{% endblocktrans %}
</p>
<form action="" method="post">
{% csrf_token %}
<p>
<input type="checkbox" required>
{% blocktrans %}I am aware that this is not a standard procedure and this might cause data integrity issues.{% endblocktrans %}
</p>
<input class="default danger" type="submit" name="unconfirm" value="{% translate 'Unconfirm' %}">
<a class="button cancel-link" href="{% add_preserved_filters change_url %}">{% trans 'Cancel' %}</a>
</form>
{% endblock %}

@ -0,0 +1,40 @@
from django.conf import settings
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
class MyOIDCAB(OIDCAuthenticationBackend):
def filter_users_by_claims(self, claims):
username = claims.get(settings.OIDC_CLAIM_USERNAME)
if not username:
return self.UserModel.objects.none()
return self.UserModel.objects.filter(username=username)
def get_username(self, claims):
username = claims.get(settings.OIDC_CLAIM_USERNAME, '')
if not username:
return super(MyOIDCAB, self).get_username(claims)
return username
def get_userinfo(self, access_token, id_token, payload):
return super(MyOIDCAB, self).get_userinfo(access_token, id_token, payload)
def create_user(self, claims):
user = super(MyOIDCAB, self).create_user(claims)
return self.update_user(user, claims)
def update_user(self, user, claims):
user.first_name = claims.get(settings.OIDC_CLAIM_FIRST_NAME, '')
user.last_name = claims.get(settings.OIDC_CLAIM_LAST_NAME, '')
groups = claims.get('groups', [])
if settings.OIDC_GROUP_STAFF in groups:
user.is_staff = True
if settings.OIDC_GROUP_SUPERUSER in groups:
user.is_superuser = True
user.save()
return user

@ -16,6 +16,7 @@ import os
base_settings = [
'local.py',
'components/base.py',
'components/authentication.py',
'components/database.py',
'components/cache.py',
'components/jet.py',

@ -0,0 +1,51 @@
# Authentication
AUTHENTICATION_BACKENDS = (
'jdav_web.oidc.MyOIDCAB',
'django.contrib.auth.backends.ModelBackend',
'rules.permissions.ObjectPermissionBackend',
)
# Use Open ID Connect if possible
OIDC_ENABLED = '1' == os.environ.get('OIDC_ENABLED', '0')
# OIDC configuration
OIDC_RP_CLIENT_ID = os.environ.get('OIDC_RP_CLIENT_ID', '')
OIDC_RP_CLIENT_SECRET = os.environ.get('OIDC_RP_CLIENT_SECRET', '')
OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ.get('OIDC_OP_AUTHORIZATION_ENDPOINT', '')
OIDC_OP_TOKEN_ENDPOINT = os.environ.get('OIDC_OP_TOKEN_ENDPOINT', '')
OIDC_OP_USER_ENDPOINT = os.environ.get('OIDC_OP_USER_ENDPOINT', '')
OIDC_OP_JWKS_ENDPOINT = os.environ.get('OIDC_OP_JWKS_ENDPOINT', '')
OIDC_RP_SIGN_ALGO = os.environ.get('OIDC_RP_SIGN_ALGO', 'RS256')
OIDC_RP_SCOPES = os.environ.get('ODIC_RP_SCOPES', 'openid email profile')
OIDC_CLAIM_USERNAME = os.environ.get('OIDC_CLAIM_USERNAME', 'username')
OIDC_CLAIM_FIRST_NAME = os.environ.get('OIDC_CLAIM_FIRST_NAME', 'given_name')
OIDC_CLAIM_LAST_NAME = os.environ.get('OIDC_CLAIM_LAST_NAME', 'last_name')
OIDC_GROUP_STAFF = os.environ.get('OIDC_GROUP_STAFF', 'staff')
OIDC_GROUP_SUPERUSER = os.environ.get('OIDC_GROUP_STAFF', 'superuser')
LOGIN_REDIRECT_URL = "/kompass"
LOGOUT_REDIRECT_URL = "/"
# default login URL, is not used if OIDC is not enabled
LOGIN_URL = "/oidc/authenticate/"
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]

@ -38,7 +38,6 @@ USE_X_FORWARDED_HOST = True
# Application definition
INSTALLED_APPS = [
'logindata.apps.LoginDataConfig',
'contrib.apps.ContribConfig',
'startpage.apps.StartpageConfig',
'material.apps.MaterialConfig',
@ -56,6 +55,7 @@ INSTALLED_APPS = [
'jet',
'django.contrib.admin',
'django.contrib.auth',
'mozilla_django_oidc',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
@ -74,10 +74,9 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware',
'mozilla_django_oidc.middleware.SessionRefresh',
]
X_FRAME_OPTIONS = 'SAMEORIGIN'
ROOT_URLCONF = 'jdav_web.urls'
TEMPLATES = [
@ -98,29 +97,6 @@ TEMPLATES = [
WSGI_APPLICATION = 'jdav_web.wsgi.application'
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'rules.permissions.ObjectPermissionBackend',
)
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/

@ -12,6 +12,6 @@ CACHES = {
}
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 1
CACHE_MIDDLEWARE_SECONDS = 120
CACHE_MIDDLEWARE_KEY_PREFIX = ''

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

@ -5,10 +5,9 @@ JET_DEFAULT_THEME = 'jdav-green'
JET_CHANGE_FORM_SIBLING_LINKS = False
JET_SIDE_MENU_ITEMS = [
{'app_label': 'logindata', 'permissions': ['auth'], 'items': [
{'name': 'authgroup', 'permissions': ['auth.group'] },
{'name': 'logindatum', 'permissions': ['auth.user']},
{'name': 'registrationpassword', 'permissions': ['auth.user']},
{'app_label': 'auth', 'permissions': ['auth'], 'items': [
{'name': 'group', 'permissions': ['auth.group'] },
{'name': 'user', 'permissions': ['auth.user']},
]},
{'app_label': 'django_celery_beat', 'permissions': ['django_celery_beat'], 'items': [
{'name': 'crontabschedule'},
@ -34,16 +33,12 @@ JET_SIDE_MENU_ITEMS = [
]},
{'app_label': 'members', 'items': [
{'name': 'member', 'permissions': ['members.view_member']},
{'name': 'group', 'permissions': ['members.view_group']},
{'name': 'membernotelist', 'permissions': ['members.view_membernotelist']},
{'name': 'freizeit', 'permissions': ['members.view_freizeit']},
{'name': 'klettertreff', 'permissions': ['members.view_klettertreff']},
]},
{'label': 'Gruppenverwaltung', 'app_label': 'members', 'permissions': ['members.view_group'], 'items': [
{'name': 'group', 'permissions': ['members.view_group']},
{'name': 'activitycategory', 'permissions': ['members.view_activitycategory']},
{'name': 'trainingcategory', 'permissions': ['members.view_trainingcategory']},
]},
{'label': 'Neue Mitglieder', 'app_label': 'members', 'permissions': ['members.view_memberunconfirmedproxy'], 'items': [
{'name': 'memberunconfirmedproxy', 'permissions': ['members.view_memberunconfirmedproxy']},
{'name': 'memberwaitinglist', 'permissions': ['members.view_memberwaitinglist']},
]},
@ -51,14 +46,7 @@ JET_SIDE_MENU_ITEMS = [
{'name': 'materialcategory', 'permissions': ['material.view_materialcategory']},
{'name': 'materialpart', 'permissions': ['material.view_materialpart']},
]},
{'app_label': 'startpage', 'permissions': ['startpage'], 'items': [
{'name': 'section', 'permissions': ['startpage.view_section']},
{'name': 'post', 'permissions': ['startpage.view_post']},
]},
{'label': 'Externe Links', 'items' : [
{ 'label': 'Nextcloud', 'url': CLOUD_LINK },
{ 'label': 'DAV 360', 'url': DAV_360_LINK },
{ 'label': 'Julei-Wiki', 'url': WIKI_LINK },
{ 'label': 'Kompass Dokumentation', 'url': DOCS_LINK },
{ 'label': 'Packlisten und Co.', 'url': CLOUD_LINK }
]},
]

@ -22,46 +22,19 @@ der Registrierung kommst du hier:
Viele Grüße
Dein KOMPASS"""
GROUP_TIME_AVAILABLE_TEXT = """Die Gruppenstunde findet jeden {weekday} von {start_time} bis {end_time} Uhr statt."""
GROUP_TIME_UNAVAILABLE_TEXT = """Bitte erfrage die Gruppenzeiten bei der Gruppenleitung ({contact_email})."""
INVITE_TEXT = """Hallo {name},
wir haben gute Neuigkeiten für dich. Es ist ein Platz in der Jugendgruppe {group_name} {group_link}freigeworden.
{group_time}
Bitte kontaktiere die Gruppenleitung ({contact_email}) für alle weiteren Absprachen.
Wenn du nach der Schnupperstunde beschließt der Gruppe beizutreten, benötigen wir noch ein paar
Informationen und deine Anmeldebestätigung von dir. Die lädst du herunter
(siehe %(REGISTRATION_FORM_DOWNLOAD_LINK)s), lässt sie von deinen Eltern ausfüllen, unterschreiben
und lädst ein Foto davon in unserem Anmeldeformular hoch. Das kannst du alles über folgenden Link erledigen:
wir haben gute Neuigkeiten für dich. Es ist ein Platz in der Jugendgruppe freigeworden. Wir brauchen
jetzt noch ein paar Informationen von dir und deine Anmeldebestätigung. Das kannst du alles über folgenden
Link erledigen:
{link}
Du siehst dort auch die Daten, die du bei deiner Eintragung auf die Warteliste angegeben hast. Bitte
überprüfe, ob die Daten noch stimmen und ändere sie bei Bedarf ab.
Falls du zu dem obigen Termin keine Zeit hast oder dich ganz von der Warteliste abmelden möchtest,
lehne bitte diese Einladung unter folgendem Link ab:
{invitation_reject_link}
Bei Fragen, wende dich gerne an %(RESPONSIBLE_MAIL)s.
Viele Grüße
Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION, 'RESPONSIBLE_MAIL': RESPONSIBLE_MAIL,
'REGISTRATION_FORM_DOWNLOAD_LINK': REGISTRATION_FORM_DOWNLOAD_LINK }
LEAVE_WAITINGLIST_TEXT = """Hallo {name},
du hast dich erfolgreich von der Warteliste abgemeldet. Falls du zu einem späteren
Zeitpunkt wieder der Warteliste beitreten möchtest, kannst du das über unsere Webseite machen.
Falls du dich nicht selbst abgemeldet hast, wende dich bitte umgehend an %(RESPONSIBLE_MAIL)s.
Viele Grüße
Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION, 'RESPONSIBLE_MAIL': RESPONSIBLE_MAIL }
@ -77,8 +50,7 @@ Wenn du weiterhin auf der Warteliste bleiben möchtest, klicke auf den folgenden
{link}
Das ist Erinnerung Nummer {reminder} von {max_reminder_count}. Nach Erinnerung Nummer {max_reminder_count} wirst
du automatisch entfernt.
Falls du nicht mehr auf der Warteliste bleiben möchtest, musst du nichts machen. Du wirst automatisch entfernt.
Viele Grüße
Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION }
@ -104,19 +76,14 @@ Dein:e Jugendreferent:in""" % { 'SEKTION': SEKTION }
ECHO_TEXT = """Hallo {name},
um unsere Daten auf dem aktuellen Stand zu halten und sicherzugehen, dass du
weiterhin ein Teil unserer Jugendarbeit bleiben möchtest, brauchen wir eine
um unsere Daten auf dem aktuellen Stand zu halten, brauchen wir eine
kurze Bestätigung von dir. Dafür besuche einfach diesen Link:
{link}
Dort kannst du deine Daten nach Eingabe eines Passworts überprüfen und ggf. ändern. Dein
Passwort ist dein Geburtsdatum. Wäre dein Geburtsdatum zum Beispiel der 4. Januar 1942,
so wäre dein Passwort: 04.01.1942
Falls du nicht innerhalb von 30 Tagen deine Daten bestätigst, gehen wir davon aus, dass du nicht mehr Teil
unserer Jugendarbeit sein möchtest. Dein Platz wird dann weitervergeben, deine Daten aus unserer Datenbank
gelöscht und du erhälst in Zukunft keine Mails mehr von uns.
Dort kannst du deine Daten überprüfen und ändern. Falls du nicht innerhalb von
30 Tagen deine Daten bestätigst, wirst du aus unserer Datenbank gelöscht und
erhälst in Zukunft keine Mails mehr von uns.
Bei Fragen, wende dich gerne an %(RESPONSIBLE_MAIL)s.
@ -147,23 +114,3 @@ verschickt. Wenn Du in Zukunft keine Emails mehr erhalten möchtest,
kannst Du hier den Newsletter deabonnieren:
{link}""" % { 'SEKTION': SEKTION }
INVITE_AS_USER_TEXT = """Hallo {name},
du bist Jugendleiter*in in der Sektion %(SEKTION)s. Die Verwaltung unserer Jugendgruppen,
Ausfahrten und Finanzen erfolgt in unserer Online Plattform Kompass. Deine Stammdaten sind
dort bereits hinterlegt. Damit du dich auch anmelden kannst, folge bitte dem folgenden Link
und wähle ein Passwort.
{link}
Bei Fragen, wende dich gerne an %(RESPONSIBLE_MAIL)s.
Viele Grüße
Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION, 'RESPONSIBLE_MAIL': RESPONSIBLE_MAIL }
ADDRESS = """JDAV %(SEKTION)s
%(STREET)s
%(PLACE)s""" % { 'SEKTION': SEKTION, 'STREET': SEKTION_STREET, 'PLACE': SEKTION_TOWN }

@ -7,48 +7,19 @@ SEKTION_TELEPHONE = "06221 284076"
SEKTION_TELEFAX = "06221 437338"
SEKTION_CONTACT_MAIL = "geschaeftsstelle@alpenverein-heidelberg.de"
SEKTION_BOARD_MAIL = "vorstand@alpenverein-heidelberg.de"
SEKTION_CRISIS_INTERVENTION_MAIL = "krisenmanagement@alpenverein-heidelberg.de"
SEKTION_IBAN = "DE22 6729 0000 0000 1019 40"
SEKTION_ACCOUNT_HOLDER = "Deutscher Alpenverein Sektion Heidelberg 1869"
RESPONSIBLE_MAIL = "jugendreferat@jdav-hd.de"
DIGITAL_MAIL = "digitales@jdav-hd.de"
# LJP
V32_HEAD_ORGANISATION = """JDAV Baden-Württemberg
Rotebühlstraße 59A
70178 Stuttgart
info@jdav-bw.de
0711 - 49 09 46 00"""
LJP_CONTRIBUTION_PER_DAY = 25
# echo
ECHO_PASSWORD_BIRTHDATE_FORMAT = '%d.%m.%Y'
ECHO_GRACE_PERIOD = 30
# misc
CONGRATULATE_MEMBERS_MAX = 10
MAX_AGE_GOOD_CONDUCT_CERTIFICATE_MONTHS = 24
ALLOWED_EMAIL_DOMAINS_FOR_INVITE_AS_USER = ('alpenverein-heidelberg.de', )
# mail mode
SEND_FROM_ASSOCIATION_EMAIL = os.environ.get('SEND_FROM_ASSOCIATION_EMAIL', '0') == '1'
# finance
ALLOWANCE_PER_DAY = 22
ALLOWANCE_PER_DAY = 10
MAX_NIGHT_COST = 11
CLOUD_LINK = 'https://nc.cloud-jdav-hd.de'
DAV_360_LINK = 'https://dav360.de'
WIKI_LINK = 'https://davbgs.sharepoint.com/sites/S-114-O-JDAV-Jugendreferat'
DOCS_LINK = os.environ.get('DOCS_LINK', 'https://jdav-hd.de/static/docs/')
CLOUD_LINK = 'https://cloud.jdav-ludwigsburg.de/index.php/s/qxQCTR8JqYSXXCQ'
# Admin setup
@ -58,18 +29,13 @@ ADMINS = (('admin', 'christian@merten-moser.de'),)
GRACE_PERIOD_WAITING_CONFIRMATION = 30
WAITING_CONFIRMATION_FREQUENCY = 90
CONFIRMATION_REMINDER_FREQUENCY = 30
MAX_REMINDER_COUNT = 3
# testing
TEST_MAIL = "post@flavigny.de"
REGISTRATION_FORM_DOWNLOAD_LINK = 'https://nc.cloud-jdav-hd.de'
REGISTRATION_FORM_DOWNLOAD_LINK = 'https://cloud.jdav-ludwigsburg.de/index.php/s/NQfRqA9MTKfPBkC'
DOMAIN = os.environ.get('DOMAIN', 'example.com')
DOMAIN = 'jdav-hd.merten.dev'
STARTPAGE_REDIRECT_URL = 'https://jdav-hd.de'
ROOT_SECTION = os.environ.get('ROOT_SECTION', 'wir')
RECENT_SECTION = 'aktuelles'
REPORTS_SECTION = 'berichte'

@ -15,6 +15,7 @@ Including another URLconf
"""
from django.urls import re_path, include
from django.contrib import admin
from django.contrib.admin.views.decorators import staff_member_required
from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns
from django.conf import settings
@ -26,13 +27,20 @@ urlpatterns = static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
admin.site.index_title = _('Startpage')
admin.site.site_header = 'Kompass'
if settings.OIDC_ENABLED:
admin.site.login = staff_member_required(
admin.site.login, login_url=settings.LOGIN_URL
)
urlpatterns += i18n_patterns(
re_path(r'^oidc/', include('mozilla_django_oidc.urls')),
)
urlpatterns += i18n_patterns(
re_path(r'^kompass/?', admin.site.urls, name='kompass'),
re_path(r'^kompass/?', admin.site.urls),
re_path(r'^jet/', include('jet.urls', 'jet')), # Django JET URLS
re_path(r'^admin/?', RedirectView.as_view(url='/kompass')),
re_path(r'^admin/', RedirectView.as_view(url='/kompass')),
re_path(r'^newsletter/', include('mailer.urls', namespace="mailer")),
re_path(r'^members/', include('members.urls', namespace="members")),
re_path(r'^login/', include('logindata.urls', namespace="logindata")),
re_path(r'^LBAlpin/Programm(/)?(20)?[0-9]{0,2}', include('ludwigsburgalpin.urls',
namespace="ludwigsburgalpin")),
re_path(r'^_nested_admin/', include('nested_admin.urls')),

@ -1 +1 @@
Subproject commit 69133f184f0f9b53a3b0a39f91e8eba99698cabc
Subproject commit 67ab498ef24dd815a194abb0e6d714f6b02dc1de

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-01 16:23+0100\n"
"POT-Creation-Date: 2023-03-20 18:48+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,220 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: contrib/admin.py:59
#, python-format
msgid "You are not allowed to view %(name)s."
msgstr "Du hast nicht die notwendigen Berechtigungen um %(name)s zu sehen."
#: jdav_web/urls.py:26
msgid "Startpage"
msgstr "Startseite"
#: logindata/admin.py:25
msgid "Permissions"
msgstr "Berechtigungen"
#: logindata/admin.py:36
msgid "Important dates"
msgstr "Wichtigen Daten"
#: logindata/apps.py:8
msgid "Authentication"
msgstr "Authentifizierung"
#: logindata/models.py:10
msgid "Permission group"
msgstr "Berechtigungsgruppe"
#: logindata/models.py:11
msgid "Permission groups"
msgstr "Berechtigungsgruppen"
#: logindata/models.py:17
msgid "Login Datum"
msgstr "Zugangsdaten"
#: logindata/models.py:18
msgid "Login Data"
msgstr "Zugangsdaten"
#: logindata/models.py:25
msgid "Password"
msgstr "Passwort"
#: logindata/models.py:31
msgid "Active registration password"
msgstr "Aktives Registrierungspasswort"
#: logindata/models.py:32
msgid "Active registration passwords"
msgstr "Aktive Registrierungspasswörter"
#: logindata/templates/logindata/register_failed.html:5
msgid "Registration"
msgstr "Registrierung"
#: logindata/templates/logindata/register_failed.html:10
#: logindata/templates/logindata/register_form.html:13
#: logindata/templates/logindata/register_password.html:11
#: logindata/templates/logindata/register_success.html:10
msgid "Set login data"
msgstr "Zugangsdaten wählen"
#: logindata/templates/logindata/register_failed.html:12
msgid "Something went wrong. The registration key is invalid or has expired."
msgstr ""
"Etwas ist schief gegangen. Der Registrierungscode ist ungültig oder ist "
"abgelaufen."
#: logindata/templates/logindata/register_failed.html:14
msgid "If you think this is a mistake, please"
msgstr "Falls du denkst, dass das ein Fehler ist, bitte"
#: logindata/templates/logindata/register_failed.html:14
msgid "contact us."
msgstr "kontaktiere uns."
#: logindata/templates/logindata/register_form.html:6
#: logindata/templates/logindata/register_password.html:6
msgid "Register"
msgstr "Registrieren"
#: logindata/templates/logindata/register_form.html:15
#: logindata/templates/logindata/register_password.html:13
msgid "Welcome, "
msgstr "Willkommen, "
#: logindata/templates/logindata/register_form.html:16
msgid ""
"To set your personal login data, please enter the password that you received."
msgstr ""
"Um deine persönlichen Zugansdaten festzulegen, gib bitte das Passwort ein, "
"das du erhalten hast."
#: logindata/templates/logindata/register_form.html:30
#: logindata/templates/logindata/register_password.html:23
msgid "submit"
msgstr "Einreichen"
#: logindata/templates/logindata/register_password.html:13
msgid ""
"To set your personal login data for Kompass, please enter the password that "
"you received."
msgstr ""
"Um deine persönlichen Zugangsdaten festzulegen, gib bitte das Passwort ein, "
"das du erhalten hast."
#: logindata/templates/logindata/register_success.html:5
msgid "Registration successful"
msgstr "Zugangsdaten erfolgreich festgelegt"
#: logindata/templates/logindata/register_success.html:12
msgid "You successfully set your login data. You can now proceed to"
msgstr ""
"Du hast deine Zugangsdaten erfolgreich festgelegt. Du kannst nun weiter zum"
#: logindata/views.py:59
msgid "You entered a wrong password."
msgstr "Das eingegebene Passwort ist falsch."
#: templates/admin/delete_confirmation.html:7
#, python-format
msgid ""
"Deleting the %(object_name)s '%(escaped_object)s' would result in deleting "
"related objects, but your account doesn't have permission to delete the "
"following types of objects:"
msgstr ""
"Löschen von %(object_name)s '%(escaped_object)s' würde zur Löschung der "
"folgenden verknüpften Objekte führen, aber du hast nicht die Berechtigung "
"die folgenden Typen von Objekten zu löschen:"
#: templates/admin/delete_confirmation.html:12
#, python-format
msgid ""
"Deleting the %(object_name)s '%(escaped_object)s' would require deleting the "
"following protected related objects:"
msgstr ""
"Löschen von %(object_name)s '%(escaped_object)s' würde zur Löschung der "
"folgenden geschützten verknüpften Objekte führen:"
#: templates/admin/delete_confirmation.html:17
#, python-format
msgid ""
"Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"?"
msgstr ""
"Bist du sicher, dass du %(object_name)s \"%(escaped_object)s\" und alle "
"davon abhängigen Objekte löschen möchtest? "
#: templates/admin/delete_confirmation.html:29
#: templates/admin/delete_selected_confirmation.html:34
msgid "Yes, Im sure"
msgstr "Ja, ich bin sicher"
#: templates/admin/delete_confirmation.html:30
#: templates/admin/delete_selected_confirmation.html:35
msgid "No, take me back"
msgstr "Nein, bitte abbrechen"
#: templates/admin/delete_selected_confirmation.html:6
#, python-format
msgid ""
"Deleting the selected %(objects_name)s would result in deleting related "
"objects, but your account doesn't have permission to delete the following "
"types of objects:"
msgstr ""
"Löschen der ausgewählten %(objects_name)s würde zur Löschung der folgenden "
"verknüpften Objekte führen, aber du hast nicht die Berechtigung die "
"folgenden Typen von Objekten zu löschen:"
#: templates/admin/delete_selected_confirmation.html:9
#, python-format
msgid ""
"Deleting the selected %(objects_name)s would require deleting the following "
"protected related objects:"
msgstr ""
"Löschen der ausgewählten %(objects_name)s würde zur Löschung der folgenden "
"geschützten verknüpften Objekte führen:"
#: templates/admin/delete_selected_confirmation.html:12
#, python-format
msgid ""
"Are you sure you want to delete the selected %(objects_name)s? All of the "
"following objects and their related items will be deleted:"
msgstr ""
"Bist du sicher, dass du die ausgewählten %(objects_name)s löschen möchtest? "
"Alle folgenden Objekte und alle davon abhängigen Objekte werden gelöscht:"
#: templates/admin/delete_selected_confirmation.html:14
msgid "Summary"
msgstr "Zusammenfassung"
#: templates/admin/delete_selected_confirmation.html:18
msgid "Objects"
msgstr "Objekte"
#: templates/admin/edit_inline/stacked.html:20
#: templates/admin/edit_inline/tabular.html:47
#: templates/nesting/admin/inlines/stacked.html:42
msgid "Change"
msgstr "Ändern"
#: templates/admin/edit_inline/stacked.html:20
#: templates/admin/edit_inline/tabular.html:47
#: templates/nesting/admin/inlines/stacked.html:42
msgid "View"
msgstr "Anzeigen"
#: templates/admin/edit_inline/stacked.html:22
#: templates/admin/edit_inline/tabular.html:49
#: templates/nesting/admin/inlines/stacked.html:44
msgid "View on site"
msgstr "Auf der Website anzeigen"
#: templates/admin/edit_inline/tabular.html:33
msgid "Delete?"
msgstr "Löschen?"
#: templates/admin/finance/statementconfirmed/change_form_object_tools.html:8
msgid "Unconfirm"
msgstr "Bestätigung zurücknehmen"
@ -253,50 +43,26 @@ msgid "Generate crisis intervention list"
msgstr "Kriseninterventionsliste erstellen"
#: templates/admin/members/freizeit/change_form_object_tools.html:16
msgid "Generate SJR application"
msgstr "SJR Antrag erstellen"
msgid "Generate overview"
msgstr "Übersicht erstellen"
#: templates/admin/members/freizeit/change_form_object_tools.html:23
msgid "Generate seminar report"
msgstr "Landesjugendplan Antrag erstellen"
#: templates/admin/members/freizeit/change_form_object_tools.html:30
msgid "Generate overview"
msgstr "Hinweise für Jugendleiter*innen erstellen"
#: templates/admin/members/freizeit/change_form_object_tools.html:38
msgid "Finance overview"
msgstr "Kostenübersicht"
msgstr "Seminarbericht erstellen"
#: templates/admin/members/member/change_form_object_tools.html:8
msgid "Invite as user"
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/freizeit/change_form_object_tools.html:29
msgid "Submit statement"
msgstr "Abrechnung einreichen"
#: templates/admin/members/memberwaitinglist/change_form_object_tools.html:8
#: templates/admin/members/memberwaitinglist/submit_line.html:9
msgid "Invite to group"
msgstr "Zu Gruppe einladen"
#: templates/nesting/admin/inlines/stacked.html:87
#, python-format
msgid "Add another %(verbose_name)s"
msgstr "Weiteren %(verbose_name)s hinzufügen"
#: utils.py:15
msgid "Please keep filesize under {} MiB. Current filesize: {:10.2f} MiB."
msgstr "Maximale Dateigröße {} MiB. Aktuelle Dateigröße: {:10.2f} MiB."
#: utils.py:43
#: utils.py:26
msgid "Filetype not supported."
msgstr "Dateityp nicht unterstützt."
#: utils.py:45
#: utils.py:28
msgid "Please keep filesize under {}. Current filesize: {}"
msgstr "Maximale Dateigröße {}. Aktuelle Dateigröße: {}."
#~ msgid "Submit statement"
#~ msgstr "Abrechnung einreichen"

@ -1,52 +0,0 @@
from django.utils.translation import gettext_lazy as _
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin, GroupAdmin as BaseAuthGroupAdmin
from django.contrib.auth.models import User as BaseUser, Group as BaseAuthGroup
from .models import AuthGroup, LoginDatum, RegistrationPassword
from members.models import Member
# Register your models here.
class AuthGroupAdmin(BaseAuthGroupAdmin):
pass
class UserInline(admin.StackedInline):
model = Member
can_delete = False
verbose_name_plural = "member"
class LoginDatumAdmin(BaseUserAdmin):
list_display = ('username', 'is_superuser')
#inlines = [UserInline]
fieldsets = (
(None, {"fields": ("username", "password")}),
(
_("Permissions"),
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
),
},
),
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": ("username", "password1", "password2"),
},
),
)
admin.site.unregister(BaseUser)
admin.site.unregister(BaseAuthGroup)
admin.site.register(LoginDatum, LoginDatumAdmin)
admin.site.register(AuthGroup, AuthGroupAdmin)
admin.site.register(RegistrationPassword)

@ -1,8 +0,0 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class LoginDataConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'logindata'
verbose_name = _('Authentication')

@ -1,55 +0,0 @@
# Generated by Django 4.0.1 on 2024-11-23 21:15
import django.contrib.auth.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='RegistrationPassword',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=100, verbose_name='Password')),
],
),
migrations.CreateModel(
name='AuthGroup',
fields=[
],
options={
'verbose_name': 'Permission group',
'verbose_name_plural': 'Permission groups',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('auth.group',),
managers=[
('objects', django.contrib.auth.models.GroupManager()),
],
),
migrations.CreateModel(
name='LoginDatum',
fields=[
],
options={
'verbose_name': 'Login Datum',
'verbose_name_plural': 'Login Data',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('auth.user',),
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -1,17 +0,0 @@
# Generated by Django 4.0.1 on 2024-11-24 00:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('logindata', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='registrationpassword',
options={'verbose_name': 'Active registration password', 'verbose_name_plural': 'Active registration passwords'},
),
]

@ -1,46 +0,0 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin, GroupAdmin as BaseAuthGroupAdmin
from django.contrib.auth.models import User as BaseUser, Group as BaseAuthGroup
class AuthGroup(BaseAuthGroup):
class Meta:
proxy = True
verbose_name = _('Permission group')
verbose_name_plural = _('Permission groups')
class LoginDatum(BaseUser):
class Meta:
proxy = True
verbose_name = _('Login Datum')
verbose_name_plural = _('Login Data')
class RegistrationPassword(models.Model):
"""
A password that can be used to register after inviting a member.
"""
password = models.CharField(max_length=100, verbose_name=_('Password'))
def __str__(self):
return self.password
class Meta:
verbose_name = _('Active registration password')
verbose_name_plural = _('Active registration passwords')
def initial_user_setup(user, member):
try:
standard_group = AuthGroup.objects.get(name='Standard')
except AuthGroup.DoesNotExist:
return False
user.is_staff = True
user.save()
user.groups.add(standard_group)
member.user = user
member.invite_as_user_key = ''
member.save()
return True

@ -1,16 +0,0 @@
{% extends "members/base.html" %}
{% load i18n static common %}
{% block title %}
{% trans "Registration" %}
{% endblock %}
{% block content %}
<h1>{% trans "Set login data" %}</h1>
<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:{% settings_value 'RESPONSIBLE_MAIL' %}">{% trans "contact us." %}</a></p>
{% endblock %}

@ -1,33 +0,0 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Register" %}
{% endblock %}
{% block content %}
<link rel="stylesheet" href="{% static "ludwigsburgalpin/termine.css" static %}">
<h1>{% trans "Set login data" %}</h1>
<p>{% trans "Welcome, " %} {{ member.prename }}.
{% blocktrans %}To set your personal login data, please enter the password that you received.{% endblocktrans %}</p>
{% if error_message %}
<p><b>{{ error_message }}</b></p>
{% endif %}
<form action="" method="post" enctype="multipart/form-data">
<table class="termine">
{% csrf_token %}
{{form}}
</table>
<input name="key" type="hidden" value="{{key}}">
<input name="password" type="hidden" value="{{password}}">
<input name="save" type="hidden">
<input type="submit" value="{% trans "submit" %}"/>
</form>
{% endblock %}

@ -1,26 +0,0 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Register" %}
{% endblock %}
{% block content %}
<h1>{% trans "Set login data" %}</h1>
<p>{% trans "Welcome, " %} {{ member.prename }}. {% blocktrans %}To set your personal login data for Kompass, please enter the password that you received.{% endblocktrans%}</p>
{% if error_message %}
<p class="errorlist">{{ error_message }}</p>
{% endif %}
<form action="" method="post">
{% csrf_token %}
<input type="password" name="password" required>
<input type="hidden" name="key" value="{{key}}">
<p><input type="submit" value="{% trans "submit" %}"/></p>
</form>
{% endblock %}

@ -1,15 +0,0 @@
{% extends "members/base.html" %}
{% load i18n %}
{% block title %}
{% trans "Registration successful" %}
{% endblock %}
{% block content %}
<h1>{% trans "Set login data" %}</h1>
<p>{% blocktrans %}You successfully set your login data. You can now proceed to{% endblocktrans%}
<a href="/kompass">login</a>.</p>
{% endblock %}

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

@ -1,8 +0,0 @@
from django.urls import re_path
from . import views
app_name = "logindata"
urlpatterns = [
re_path(r'^register', views.register , name='register'),
]

@ -1,75 +0,0 @@
from django import forms
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from django.contrib.auth.forms import UserCreationForm
from members.models import Member
from .models import initial_user_setup, RegistrationPassword
def render_register_password(request, key, member, error_message=''):
return render(request, 'logindata/register_password.html',
context={'key': key,
'member': member,
'error_message': error_message})
def render_register_failed(request):
return render(request, 'logindata/register_failed.html')
def render_register_form(request, key, password, member, form):
return render(request, 'logindata/register_form.html',
context={'key': key,
'password': password,
'member': member,
'form': form})
def render_register_success(request):
return render(request, 'logindata/register_success.html')
# Create your views here.
def register(request):
if request.method == 'GET' and 'key' not in request.GET:
return HttpResponseRedirect(reverse('startpage:index'))
if request.method == 'POST' and 'key' not in request.POST:
return HttpResponseRedirect(reverse('startpage:index'))
key = request.GET['key'] if request.method == 'GET' else request.POST['key']
if not key:
return render_register_failed(request)
try:
member = Member.objects.get(invite_as_user_key=key)
except (Member.DoesNotExist, Member.MultipleObjectsReturned):
return render_register_failed(request)
if request.method == 'GET':
return render_register_password(request, request.GET['key'], member)
if 'password' not in request.POST:
return render_register_failed(request)
password = request.POST['password']
# check if the entered password is one of the active registration passwords
if RegistrationPassword.objects.filter(password=password).count() == 0:
return render_register_password(request, key, member, error_message=_('You entered a wrong password.'))
if "save" in request.POST:
form = UserCreationForm(request.POST)
if not form.is_valid():
# form is invalid, reprint form with (automatic) error messages
return render_register_form(request, key, password, member, form)
user = form.save(commit=False)
success = initial_user_setup(user, member)
if success:
return render_register_success(request)
else:
return render_register_failed(request)
else:
prefill = {'username': member.suggested_username()}
form = UserCreationForm(initial=prefill)
return render_register_form(request, key, password, member, form)

@ -12,7 +12,6 @@ from rules.contrib.admin import ObjectPermissionsModelAdmin
from .models import Message, Attachment, MessageForm, EmailAddress, EmailAddressForm
from .mailutils import NOT_SENT, PARTLY_SENT
from members.models import Member
from members.admin import FilteredMemberFieldMixin
from contrib.admin import CommonAdminMixin, CommonAdminInlineMixin
@ -21,9 +20,8 @@ class AttachmentInline(CommonAdminInlineMixin, admin.TabularInline):
extra = 0
class EmailAddressAdmin(FilteredMemberFieldMixin, admin.ModelAdmin):
list_display = ('email', 'internal_only')
fields = ('name', 'to_members', 'to_groups', 'internal_only')
class EmailAddressAdmin(admin.ModelAdmin):
list_display = ('email', )
#formfield_overrides = {
# models.ManyToManyField: {'widget': forms.CheckboxSelectMultiple},
# models.ForeignKey: {'widget': apply_select2(forms.Select)}
@ -32,12 +30,11 @@ class EmailAddressAdmin(FilteredMemberFieldMixin, admin.ModelAdmin):
form = EmailAddressForm
class MessageAdmin(FilteredMemberFieldMixin, CommonAdminMixin, ObjectPermissionsModelAdmin):
class MessageAdmin(CommonAdminMixin, ObjectPermissionsModelAdmin):
"""Message creation view"""
exclude = ('created_by', 'to_notelist')
exclude = ('created_by',)
list_display = ('subject', 'get_recipients', 'sent')
search_fields = ('subject',)
list_filter = ('sent',)
change_form_template = "mailer/change_form.html"
readonly_fields = ('sent',)
#formfield_overrides = {
@ -91,15 +88,7 @@ class MessageAdmin(FilteredMemberFieldMixin, CommonAdminMixin, ObjectPermissions
def submit_message(msg, request):
sender = None
if not hasattr(request.user, 'member'):
messages.error(request, _("Your account is not connected to a member. Please contact your system administrator."))
return
sender = request.user.member
if not sender.has_internal_email():
messages.error(request, _("Your email address is not an internal email address. Please change your email address and try again."))
return
success = msg.submit(sender)
success = msg.submit()
if success == NOT_SENT:
messages.error(request, _("Failed to send message"))
elif success == PARTLY_SENT:

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-02 22:50+0100\n"
"POT-Creation-Date: 2023-03-20 18:48+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,35 +18,19 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: mailer/admin.py:69
#: mailer/admin.py:56
msgid "Send message"
msgstr "Nachricht verschicken"
#: 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
#: mailer/admin.py:86
msgid "Failed to send message"
msgstr "Fehler beim Senden der Email"
#: mailer/admin.py:106
#: mailer/admin.py:88
msgid "Failed to send some messages"
msgstr "Fehler beim Senden der Emails"
#: mailer/admin.py:108
#: mailer/admin.py:90
msgid "Successfully sent message"
msgstr "Email wurde erfolgreich verschickt"
@ -54,143 +38,125 @@ msgstr "Email wurde erfolgreich verschickt"
msgid "mailer"
msgstr "Verteiler"
#: mailer/management/commands/notify_active.py:36
#: mailer/management/commands/notify_active.py:38
#, python-format
msgid "Congratulation %(name)s"
msgstr "Herzlichen Glückwunsch %(name)s"
#: mailer/models.py:20
msgid "Only alphanumeric characters, ., - and _ are allowed"
msgstr "Nur Buchstaben, Zahlen, ., . und _ sind erlaubt"
#: mailer/models.py:18
msgid "Only alphanumeric characters are allowed"
msgstr "Nur Buchstaben und Zahlen erlaubt"
#: mailer/models.py:25
#: mailer/models.py:23
msgid "name"
msgstr "Name"
#: mailer/models.py:27
#: mailer/models.py:25
msgid "Forward to participants"
msgstr "Weiterleitung an Teilnehmer*innen"
msgstr "Weiterleitung an Teilnehmer"
#: mailer/models.py:30
#: mailer/models.py:28
msgid "Forward to group"
msgstr "Weiterleitung an Gruppe"
#: 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
#: mailer/models.py:45
msgid "email address"
msgstr "Email-Adresse"
#: mailer/models.py:56
#: mailer/models.py:46
msgid "email addresses"
msgstr "Email-Adressen"
#: mailer/models.py:69
#: mailer/models.py:59
msgid "Either a group or at least one member is required as forward recipient."
msgstr ""
"Es muss entweder eine Gruppe oder mindestens ein*e Teilnehmer*in als "
"Empfänger*in ausgewählt werden."
"Es muss entweder eine Gruppe oder mindestens ein Teilnehmer als Empfänger "
"ausgewählt werden."
#: mailer/models.py:77
#: mailer/models.py:67
msgid "subject"
msgstr "Betreff"
#: mailer/models.py:78
#: mailer/models.py:68
msgid "content"
msgstr "Inhalt"
#: mailer/models.py:80
#: mailer/models.py:70
msgid "to group"
msgstr "An Gruppe"
#: mailer/models.py:83
#: mailer/models.py:73
msgid "to freizeit"
msgstr "An Ausfahrt"
msgstr "An Freizeit"
#: mailer/models.py:88
#: mailer/models.py:78
msgid "to notes list"
msgstr "An Notizliste"
#: mailer/models.py:93
#: mailer/models.py:83
msgid "to member"
msgstr "An Teilnehmer*innen"
msgstr "An Teilnehmer"
#: mailer/models.py:96
#: mailer/models.py:86
msgid "reply to participant"
msgstr "Antwort an Teilnehmer*innen"
msgstr "Antwort an Teilnehmer"
#: mailer/models.py:100
#: mailer/models.py:90
msgid "reply to custom email address"
msgstr "Antwort an Email-Adresse"
#: mailer/models.py:103
#: mailer/models.py:93
msgid "sent"
msgstr "Gesendet"
#: mailer/models.py:104
msgid "Created by"
msgstr "Erstellt von"
#: mailer/models.py:122
#: mailer/models.py:107
msgid "Some other members"
msgstr "Andere Teilnehmer*innen"
msgstr "Andere Teilnehmer"
#: mailer/models.py:124
#: mailer/models.py:109
msgid "recipients"
msgstr "Empfänger"
#: mailer/models.py:196
#: mailer/models.py:182
msgid "message"
msgstr "Nachricht"
#: mailer/models.py:197
#: mailer/models.py:183
msgid "messages"
msgstr "Nachrichten"
#: mailer/models.py:199
#: mailer/models.py:185
msgid "Can submit mails"
msgstr "Kann Mails verschicken"
#: mailer/models.py:220
#: mailer/models.py:201
msgid ""
"Either a group, a memberlist or at least one member is required as recipient"
msgstr ""
"Es muss entweder eine Gruppe, eine Teilnehmer*innenliste oder mindestens "
"ein*e Teilnehmer*in als Empfänger*in ausgewählt werden."
"Es muss entweder eine Gruppe, eine Teilnehmerliste oder mindestens ein "
"Teilnehmer als Empfänger ausgewählt werden."
#: mailer/models.py:206
msgid ""
"At least one reply-to recipient is required. Use the info mail if you really "
"want no reply-to recipient."
msgstr ""
"Es muss mindestens ein Antwort-An Empfänger angegeben werden. Nutze die info "
"Email-Adresse falls du wirklich keinen Antwort-An Empfänger haben möchtest."
#: mailer/models.py:227
#: mailer/models.py:213
msgid "file"
msgstr "Datei"
#: mailer/models.py:232
#: mailer/models.py:219
msgid "Empty"
msgstr "Leer"
#: mailer/models.py:235
#: mailer/models.py:222
msgid "attachment"
msgstr "Anhang"
#: mailer/models.py:236
#: mailer/models.py:223
msgid "attachments"
msgstr "Anhänge"
@ -271,7 +237,7 @@ msgstr "Vorname"
msgid "Lastname"
msgstr "Nachname"
#: mailer/templates/mailer/subscribe.html:26 mailer/views.py:60
#: mailer/templates/mailer/subscribe.html:26 mailer/views.py:59
msgid "Birthdate"
msgstr "Geburtsdatum"
@ -296,30 +262,22 @@ msgstr "Hier kannst du dich vom Newsletter abmelden"
msgid "Successfully unsubscribed from the newsletter for "
msgstr "Newsletter erfolgreich abbestellt für "
#: mailer/views.py:36
#: mailer/views.py:35
msgid "Can't verify this link. Try again!"
msgstr "Ungültiger Link. Bitte nochmal versuchen!"
#: mailer/views.py:48
#: mailer/views.py:47
msgid "Please fill in every field"
msgstr "Bitte jedes Feld ausfüllen!"
#: mailer/views.py:50
#: mailer/views.py:49
msgid "Unsubscription confirmation"
msgstr "Abmeldebestätigung"
#: mailer/views.py:83
#: mailer/views.py:82
msgid "Please fill in every field!"
msgstr "Bitte jedes Feld ausfüllen!"
#: mailer/views.py:90
#: mailer/views.py:89
msgid "Member already exists"
msgstr "Mitglied schon vorhanden"
#~ msgid ""
#~ "At least one reply-to recipient is required. Use the info mail if you "
#~ "really want no reply-to recipient."
#~ msgstr ""
#~ "Es muss mindestens ein Antwort-An Empfänger angegeben werden. Nutze die "
#~ "info Email-Adresse falls du wirklich keinen Antwort-An Empfänger haben "
#~ "möchtest."

@ -7,20 +7,14 @@ import os
NOT_SENT, SENT, PARTLY_SENT = 0, 1, 2
def send(subject, content, sender, recipients, message_id=None, reply_to=None,
attachments=None, cc=None):
attachments=None):
failed, succeeded = False, False
if type(recipients) != list:
recipients = [recipients]
if not cc:
cc = []
elif type(cc) != list:
cc = [cc]
if reply_to is not None:
kwargs = {"reply_to": reply_to}
else:
kwargs = {}
if sender == settings.DEFAULT_SENDING_MAIL:
sender = addr_with_name(settings.DEFAULT_SENDING_MAIL, settings.DEFAULT_SENDING_NAME)
url = prepend_base_url("/newsletter/unsubscribe")
headers = {'List-Unsubscribe': '<{unsubscribe_url}>'.format(unsubscribe_url=url)}
if message_id is not None:
@ -29,7 +23,7 @@ def send(subject, content, sender, recipients, message_id=None, reply_to=None,
# construct mails
mails = []
for recipient in set(recipients):
email = EmailMessage(subject, content, sender, [recipient], cc=cc,
email = EmailMessage(subject, content, sender, [recipient],
headers=headers, **kwargs)
if attachments is not None:
for attach in attachments:
@ -53,8 +47,10 @@ def send(subject, content, sender, recipients, message_id=None, reply_to=None,
def get_content(content, registration_complete=True):
url = prepend_base_url("/newsletter/unsubscribe")
prepend = settings.PREPEND_INCOMPLETE_REGISTRATION_TEXT
text = "{prepend}{content}".format(prepend="" if registration_complete else prepend,
content=content)
footer = settings.MAIL_FOOTER.format(link=url)
text = "{prepend}{content}{footer}".format(prepend="" if registration_complete else prepend,
content=content,
footer=footer)
return text
@ -68,14 +64,11 @@ def get_echo_link(member):
return prepend_base_url("/members/echo?key={}".format(key))
def get_registration_link(key):
def get_registration_link(waiter):
key = waiter.generate_registration_key()
return prepend_base_url("/members/registration?key={}".format(key))
def get_invitation_reject_link(key):
return prepend_base_url("/members/waitinglist/invitation/reject?key={}".format(key))
def get_wait_confirmation_link(waiter):
key = waiter.generate_wait_confirmation_key()
return prepend_base_url("/members/waitinglist/confirm?key={}".format(key))
@ -85,13 +78,5 @@ def get_mail_confirmation_link(key):
return prepend_base_url("/members/mail/confirm?key={}".format(key))
def get_invite_as_user_key(key):
return prepend_base_url("/login/register?key={}".format(key))
def prepend_base_url(absolutelink):
return "{protocol}://{base}{link}".format(protocol=settings.PROTOCOL, base=settings.BASE_URL, link=absolutelink)
def addr_with_name(addr, name):
return "{name} <{addr}>".format(name=name, addr=addr)

@ -1,19 +0,0 @@
# Generated by Django 4.0.1 on 2024-11-17 23:31
from django.db import migrations
import utils
class Migration(migrations.Migration):
dependencies = [
('mailer', '0003_alter_message_options'),
]
operations = [
migrations.AlterField(
model_name='attachment',
name='f',
field=utils.RestrictedFileField(upload_to='attachments', verbose_name='file'),
),
]

@ -1,19 +0,0 @@
# Generated by Django 4.0.1 on 2024-11-23 14:03
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mailer', '0004_alter_attachment_f'),
]
operations = [
migrations.AlterField(
model_name='emailaddress',
name='name',
field=models.CharField(max_length=50, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z._-]*$', 'Only alphanumeric characters, ., - and _ are allowed')], verbose_name='name'),
),
]

@ -1,19 +0,0 @@
# 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'),
),
]

@ -1,18 +0,0 @@
# 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,8 +3,7 @@ from django.core.exceptions import ValidationError
from django import forms
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext
from .mailutils import send, get_content, NOT_SENT, SENT, PARTLY_SENT,\
addr_with_name
from .mailutils import send, get_content, NOT_SENT, SENT, PARTLY_SENT
from utils import RestrictedFileField
from jdav_web.celery import app
from django.core.validators import RegexValidator
@ -16,8 +15,7 @@ from .rules import is_creator
import os
alphanumeric = RegexValidator(r'^[0-9a-zA-Z._-]*$',
_('Only alphanumeric characters, ., - and _ are allowed'))
alphanumeric = RegexValidator(r'^[0-9a-zA-Z]*$', _('Only alphanumeric characters are allowed'))
class EmailAddress(models.Model):
@ -29,14 +27,6 @@ class EmailAddress(models.Model):
to_groups = models.ManyToManyField('members.Group',
verbose_name=_('Forward to group'),
blank=True)
internal_only = models.BooleanField(verbose_name=_('Restrict to internal email addresses'),
help_text=_('Only allow forwarding to this e-mail address from the internal domain.'),
default=False)
allowed_senders = models.ManyToManyField('members.Group',
verbose_name=_('Allowed sender'),
help_text=_('Only forward e-mails of members of selected groups. Leave empty to allow all senders.'),
blank=True,
related_name='allowed_sender_on_emailaddresses')
@property
def email(self):
@ -123,7 +113,7 @@ class Message(CommonModel):
return ", ".join(recipients)
get_recipients.short_description = _('recipients')
def submit(self, sender=None):
def submit(self):
"""Sends the mail to the specified group of members"""
# recipients
members = set()
@ -146,39 +136,40 @@ class Message(CommonModel):
attach = [a.f.path for a in Attachment.objects.filter(msg__id=self.pk)
if a.f.name]
emails = [member.email for member in filtered]
emails.extend([member.alternative_email for member in filtered if member.alternative_email])
recipients_with_reminder = [m for m in filtered if not m.registered]
recipients_without_reminder = [m for m in filtered if m.registered]
emails_rem = [member.email for member in recipients_with_reminder]
emails_rem.extend([member.email_parents for member in recipients_with_reminder
if member.email_parents and member.cc_email_parents])
emails_no_rem = [member.email for member in recipients_without_reminder]
emails_no_rem.extend([member.email_parents for member in recipients_without_reminder
if member.email_parents and member.cc_email_parents])
# remove any underscores from subject to prevent Arne from using
# terrible looking underscores in subjects
self.subject = self.subject.replace('_', ' ')
# generate message id
message_id = "<{pk}@{domain}>".format(pk=self.pk, domain=settings.DOMAIN)
# reply to addresses
reply_to = [jl.association_email for jl in self.reply_to.all()]
reply_to.extend([ml.email for ml in self.reply_to_email_address.all()])
# set correct from address
# if the sender is none or if sending from association emails has been
# disabled, use the default sending mail
if sender is None:
from_addr = addr_with_name(settings.DEFAULT_SENDING_MAIL, settings.DEFAULT_SENDING_NAME)
elif sender and settings.SEND_FROM_ASSOCIATION_EMAIL:
from_addr = addr_with_name(sender.association_email, sender.name)
else:
from_addr = 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))
reply_to_unfiltered = [jl.association_email for jl in self.reply_to.all()]
reply_to_unfiltered.extend([ml.email for ml in self.reply_to_email_address.all()])
# remove sending address from reply-to field (probably unnecessary since it's removed by
# the mail provider anyways)
reply_to = [mail for mail in reply_to_unfiltered if mail != settings.DEFAULT_SENDING_MAIL ]
try:
success = send(self.subject, get_content(self.content, registration_complete=True),
from_addr,
emails,
message_id=message_id,
attachments=attach,
reply_to=reply_to)
if success == SENT or success == PARTLY_SENT:
success1 = send(self.subject, get_content(self.content, registration_complete=False),
settings.DEFAULT_SENDING_MAIL,
emails_rem,
message_id=message_id,
attachments=attach,
reply_to=reply_to)
success2 = send(self.subject, get_content(self.content, registration_complete=True),
settings.DEFAULT_SENDING_MAIL,
emails_no_rem,
message_id=message_id,
attachments=attach,
reply_to=reply_to)
if (success1 == SENT or success1 == PARTLY_SENT) and (success2 == SENT or success2 == PARTLY_SENT):
self.sent = True
for a in Attachment.objects.filter(msg__id=self.pk):
if a.f.name:
@ -219,6 +210,11 @@ class MessageForm(forms.ModelForm):
if not group and freizeit is None and not members and notelist is None:
raise ValidationError(_('Either a group, a memberlist or at least'
' one member is required as recipient'))
reply_to = self.cleaned_data.get('reply_to')
reply_to_email_address = self.cleaned_data.get('reply_to_email_address')
if not reply_to and not reply_to_email_address:
raise ValidationError(_('At least one reply-to recipient is required. '
'Use the info mail if you really want no reply-to recipient.'))
class Attachment(CommonModel):
"""Represents an attachment to an email"""
@ -226,7 +222,8 @@ class Attachment(CommonModel):
# file (not naming it file because of builtin)
f = RestrictedFileField(_('file'),
upload_to='attachments',
max_upload_size=10)
blank=True,
max_upload_size=10485760)
def __str__(self):
return os.path.basename(self.f.name) if self.f.name else _("Empty")

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-01 16:23+0100\n"
"POT-Creation-Date: 2023-03-20 18:48+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\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.db.models import TextField, ManyToManyField, ForeignKey, Count,\
Sum, Case, Q, F, When, Value, IntegerField, Subquery, OuterRef
from django.forms import Textarea, RadioSelect, TypedChoiceField, CheckboxInput
from django.forms import Textarea, RadioSelect, TypedChoiceField
from django.shortcuts import render
from django.core.exceptions import PermissionDenied
from .pdf import render_tex, fill_pdf_form, merge_pdfs, serve_pdf
from .pdf import render_tex
from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin
@ -35,13 +35,11 @@ import nested_admin
from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, Klettertreff,
MemberWaitingList, LJPProposal, Intervention, PermissionMember,
PermissionGroup, MemberTraining, TrainingCategory,
KlettertreffAttendee, ActivityCategory, EmergencyContact,
annotate_activity_score, RegistrationPassword, MemberUnconfirmedProxy,
InvitationToGroup)
KlettertreffAttendee, ActivityCategory,
annotate_activity_score, RegistrationPassword, MemberUnconfirmedProxy)
from finance.models import Statement, BillOnExcursionProxy
from mailer.mailutils import send as send_mail, get_echo_link
from django.conf import settings
from utils import get_member
#from easy_select2 import apply_select2
@ -83,10 +81,6 @@ class FilteredMemberFieldMixin:
return field
class InviteAsUserForm(forms.Form):
_selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
class PermissionOnGroupInline(admin.StackedInline):
model = PermissionGroup
extra = 1
@ -108,15 +102,6 @@ class TrainingOnMemberInline(CommonAdminInlineMixin, admin.TabularInline):
extra = 0
class EmergencyContactInline(CommonAdminInlineMixin, admin.TabularInline):
model = EmergencyContact
formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
}
fields = ['prename', 'lastname', 'email', 'phone_number']
extra = 0
class TrainingCategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'permission_needed')
ordering = ('name', )
@ -124,7 +109,7 @@ class TrainingCategoryAdmin(admin.ModelAdmin):
class RegistrationFilter(admin.SimpleListFilter):
title = _('Registration complete')
parameter_name = 'registration_complete'
parameter_name = 'registered'
default_value = ('All', None)
def lookups(self, request, model_admin):
@ -136,14 +121,14 @@ class RegistrationFilter(admin.SimpleListFilter):
def queryset(self, request, queryset):
if self.value() == 'True':
return queryset.filter(registration_complete=True)
return queryset.filter(registered=True)
elif self.value() == 'False':
return queryset.filter(registration_complete=False)
return queryset.filter(registered=False)
elif self.value() is None:
if self.default_value[1] is None:
return queryset
else:
return queryset.filter(registration_complete=self.default_value[1])
return queryset.filter(registered=self.default_value[1])
elif self.value() == 'All':
return queryset
@ -163,100 +148,44 @@ class RegistrationFilter(admin.SimpleListFilter):
# Register your models here.
class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
fieldsets = [
(None,
{
'fields': [('prename', 'lastname'),
('email', 'alternative_email'),
'phone_number',
'birth_date',
'gender',
'group', 'registration_form', 'image',
('join_date', 'leave_date'),
'comments',
'legal_guardians',
'dav_badge_no',
'active', 'echoed', 'gets_newsletter',
'user',
]
}
),
(_("Contact information"),
{
'fields': ['street', 'plz', 'town', 'address_extra', 'country', 'iban']
}
),
(_("Skills"),
{
'fields': ['swimming_badge', 'climbing_badge', 'alpine_experience']
}
),
(_("Others"),
{
'fields': ['allergies', 'tetanus_vaccination', 'medication', 'photos_may_be_taken',
'may_cancel_appointment_independently']
}
),
(_("Organizational"),
{
'fields': [
('good_conduct_certificate_presented_date',
'good_conduct_certificate_valid'),
'has_key', 'has_free_ticket_gym']
}
),
]
fields = ['prename', 'lastname', 'email',
('email_parents', 'cc_email_parents'),
'street', 'plz', 'town', 'address_extra', 'country', 'nationality',
'phone_number_private', 'phone_number_mobile',
'phone_number_parents', 'birth_date', 'gender', 'civil_status',
'dav_badge_no',
'group',
'swimming_badge', 'climbing_badge', 'rock_experience', 'allergies',
'medication', 'tetanus_vaccination', 'photos_may_be_taken', 'legal_guardians',
('good_conduct_certificate_presented_date', 'good_conduct_certificate_presentation_needed'),
'iban', 'has_key', 'has_free_ticket_gym', 'gets_newsletter', 'registered', 'registration_form',
'image',
'active', 'echoed',
('join_date', 'leave_date'),
'comments', 'technical_comments',
'user']
list_display = ('name_text_or_link', 'birth_date', 'age', 'get_group', 'gets_newsletter',
'registration_complete', 'active', 'echoed', 'comments', 'activity_score')
'registered', 'active', 'echoed', 'comments', 'activity_score')
search_fields = ('prename', 'lastname', 'email')
list_filter = ('group', 'gets_newsletter', RegistrationFilter, 'active')
list_display_links = None
readonly_fields = ['echoed', 'good_conduct_certificate_valid']
inlines = [EmergencyContactInline, TrainingOnMemberInline, PermissionOnMemberInline]
inlines = [TrainingOnMemberInline, PermissionOnMemberInline]
#formfield_overrides = {
# ManyToManyField: {'widget': forms.CheckboxSelectMultiple},
# ForeignKey: {'widget': apply_select2(forms.Select)}
#}
change_form_template = "members/change_member.html"
ordering = ('lastname',)
actions = ['request_echo', 'invite_as_user_action']
actions = ['send_mail_to', 'request_echo']
list_per_page = 25
sensitive_fields = ['iban', 'registration_form', 'comments']
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 = {
field_permissions = {
'user': 'members.may_set_auth_user',
'group': 'members.may_change_member_group',
'good_conduct_certificate_presented_date': 'members.may_change_organizationals',
'has_key': 'members.may_change_organizationals',
'has_free_ticket_gym': 'members.may_change_organizationals',
'group': 'members.may_change_group'
}
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>/inviteasuser/", wrap(self.invite_as_user_view),
name="%s_%s_inviteasuser" % (self.opts.app_label, self.opts.model_name),
),
]
return custom_urls + urls
def get_queryset(self, request):
queryset = super().get_queryset(request)
return annotate_activity_score(queryset.prefetch_related('group'))
@ -284,80 +213,14 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
for member in queryset:
if not member.gets_newsletter:
continue
member.send_mail(_("Echo required"),
settings.ECHO_TEXT.format(name=member.prename, link=get_echo_link(member)))
messages.success(request, _("Successfully requested echo from selected members."))
send_mail(_("Echo required"),
settings.ECHO_TEXT.format(name=member.prename, link=get_echo_link(member)),
settings.DEFAULT_SENDING_MAIL,
[member.email, member.email_parents] if member.email_parents and member.cc_email_parents
else member.email)
messages.success(request, _("Successfully requested echo from selected members."))
request_echo.short_description = _('Request echo from selected members')
def invite_as_user(self, request, queryset):
failures = []
for member in queryset:
success = member.invite_as_user()
if not success:
failures.append(member)
messages.error(request,
_('%(name)s does not have a DAV360 email address or is already registered.') % {'name': member.name})
if queryset.count() == 1 and len(failures) == 0:
messages.success(request, _('Successfully invited %(name)s as user.') % {'name': queryset[0].name})
elif len(failures) == 0:
messages.success(request, _('Successfully invited selected members to join as users.'))
else:
messages.warning(request, _('Some members have been invited, others could not be invited.'))
def has_may_invite_as_user_permission(self, request):
return request.user.has_perm('%s.%s' % (self.opts.app_label, 'may_invite_as_user'))
def invite_as_user_action(self, request, queryset):
if not request.user.has_perm('members.may_invite_as_user'):
messages.error(request, _('Permission denied.'))
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
if "apply" in request.POST:
self.invite_as_user(request, queryset)
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
context = dict(self.admin_site.each_context(request),
title=_('Invite as user'),
opts=self.opts,
members=queryset,
form=InviteAsUserForm(initial={'_selected_action': queryset.values_list('id', flat=True)}))
return render(request, 'admin/invite_selected_as_user.html', context=context)
invite_as_user_action.short_description = _('Invite selected members to join Kompass as users.')
invite_as_user_action.allowed_permissions = ('may_invite_as_user',)
def invite_as_user_view(self, request, object_id):
if not request.user.has_perm('members.may_invite_as_user'):
messages.error(request, _('Permission denied.'))
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name),
args=(object_id,)))
try:
m = Member.objects.get(pk=object_id)
except Member.DoesNotExist:
messages.error(request, _("Member not found."))
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
if m.user:
messages.error(request,
_("%(name)s already has login data.") % {'name': str(m)})
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name),
args=(object_id,)))
if not m.has_internal_email():
messages.error(request,
_("The configured email address for %(name)s is not an internal one.") % {'name': str(m)})
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name),
args=(object_id,)))
if "apply" in request.POST:
self.invite_as_user(request, Member.objects.filter(pk=object_id))
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name),
args=(object_id,)))
context = dict(self.admin_site.each_context(request),
title=_('Invite as user'),
opts=self.opts,
member=m,
object=m)
if m.invite_as_user_key:
messages.warning(request, _('%(name)s already has a pending invitation as user.' % {'name': str(m)}))
return render(request, 'admin/invite_as_user.html', context=context)
def activity_score(self, obj):
score = obj._activity_score
# show 1 to 5 climbers based on activity in last year
@ -387,76 +250,16 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
name_text_or_link.admin_order_field = 'lastname'
class DemoteToWaiterForm(forms.Form):
_selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
class MemberUnconfirmedAdmin(CommonAdminMixin, admin.ModelAdmin):
fieldsets = [
(None,
{
'fields': [('prename', 'lastname'),
('email', 'alternative_email'),
'phone_number',
'birth_date',
'gender',
'group', 'registration_form', 'image',
('join_date', 'leave_date'),
'comments',
'legal_guardians',
'dav_badge_no',
'active', 'echoed', 'gets_newsletter',
'user',
]
}
),
(_("Contact information"),
{
'fields': ['street', 'plz', 'town', 'address_extra', 'country', 'iban']
}
),
(_("Skills"),
{
'fields': ['swimming_badge', 'climbing_badge', 'alpine_experience']
}
),
(_("Others"),
{
'fields': ['allergies', 'tetanus_vaccination', 'medication', 'photos_may_be_taken']
}
),
(_("Organizational"),
{
'fields': [
('good_conduct_certificate_presented_date',
'good_conduct_certificate_valid'),
'has_key', 'has_free_ticket_gym']
}
),
]
list_display = ('name', 'birth_date', 'age', 'get_group', 'confirmed_mail', 'confirmed_alternative_mail')
class MemberUnconfirmedAdmin(admin.ModelAdmin):
fields = ['prename', 'lastname', 'email', 'email_parents', 'cc_email_parents', 'street', 'plz',
'town', 'phone_number_mobile', 'phone_number_private','phone_number_parents', 'birth_date', 'group',
'registered', 'registration_form', 'active', 'comments']
list_display = ('name', 'birth_date', 'age', 'get_group', 'confirmed_mail', 'confirmed_mail_parents')
search_fields = ('prename', 'lastname', 'email')
list_filter = ('group', 'confirmed_mail', 'confirmed_alternative_mail')
readonly_fields = ['confirmed_mail', 'confirmed_alternative_mail',
'good_conduct_certificate_valid']
actions = ['request_mail_confirmation', 'confirm', 'demote_to_waiter_action']
inlines = [EmergencyContactInline]
list_filter = ('group', 'confirmed_mail', 'confirmed_mail_parents')
actions = ['request_mail_confirmation', 'confirm', 'demote_to_waiter']
change_form_template = "members/change_member_unconfirmed.html"
field_view_permissions = {
'user': 'members.may_set_auth_user',
'good_conduct_certificate_presented_date': 'members.may_change_organizationals',
'has_key': 'members.may_change_organizationals',
'has_free_ticket_gym': 'members.may_change_organizationals',
}
field_change_permissions = {
'user': 'members.may_set_auth_user',
'good_conduct_certificate_presented_date': 'members.may_change_organizationals',
'has_key': 'members.may_change_organizationals',
'has_free_ticket_gym': 'members.may_change_organizationals',
}
def has_add_permission(self, request, obj=None):
return False
@ -495,62 +298,26 @@ class MemberUnconfirmedAdmin(CommonAdminMixin, admin.ModelAdmin):
messages.error(request, _("Failed to confirm some registrations because of unconfirmed email addresses."))
confirm.short_description = _('Confirm selected registrations')
def get_urls(self):
urls = super().get_urls()
def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
wrapper.model_admin = self
return update_wrapper(wrapper, view)
custom_urls = [
path(
"<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):
for member in queryset:
#mem_as_dict = member.__dict__
#del mem_as_dict['_state']
#del mem_as_dict['id']
waiter = MemberWaitingList(prename=member.prename,
lastname=member.lastname,
email=member.email,
email_parents=member.email_parents,
cc_email_parents=member.cc_email_parents,
birth_date=member.birth_date,
gender=member.gender,
comments=member.comments,
confirmed_mail=member.confirmed_mail,
confirm_mail_key=member.confirm_mail_key)
confirmed_mail_parents=member.confirmed_mail_parents,
confirm_mail_key=member.confirm_mail_key,
confirm_mail_parents_key=member.confirm_mail_parents_key)
waiter.save()
member.delete()
messages.success(request, _("Successfully demoted %(name)s to waiter.") % {'name': waiter.name})
demote_to_waiter.short_description = _('Demote selected registrations to waiters.')
def response_change(self, request, member):
if "_confirm" in request.POST:
@ -568,28 +335,14 @@ class WaiterInviteForm(forms.Form):
label=_('Group'))
class InvitationToGroupAdmin(admin.TabularInline):
model = InvitationToGroup
fields = ['group', 'date', 'status']
readonly_fields = ['group', 'date', 'status']
extra = 0
can_delete = False
def has_add_permission(self, request, obj=None):
return False
class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin):
fields = ['prename', 'lastname', 'email', 'birth_date', 'gender', 'application_text',
'application_date', 'comments',
'sent_reminders']
list_display = ('name', 'birth_date', 'age', 'application_date', 'confirmed_mail',
'waiting_confirmed', 'sent_reminders')
fields = ['prename', 'lastname', 'email', 'email_parents', 'birth_date', 'application_text', 'application_date', 'comments', 'invited_for_group']
list_display = ('name', 'birth_date', 'age', 'application_date', 'confirmed_mail', 'confirmed_mail_parents',
'waiting_confirmed')
search_fields = ('prename', 'lastname', 'email')
list_filter = ('confirmed_mail',)
list_filter = ('confirmed_mail', 'confirmed_mail_parents')
actions = ['ask_for_registration', 'ask_for_wait_confirmation']
inlines = [InvitationToGroupAdmin]
readonly_fields= ['application_date', 'sent_reminders']
readonly_fields= ('invited_for_group',)
def has_add_permission(self, request, obj=None):
return False
@ -611,15 +364,11 @@ class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin):
messages.error(request,
_("An error occurred while trying to invite said members. Please try again."))
return HttpResponseRedirect(request.get_full_path())
if not group.contact_email:
messages.error(request,
_('The selected group does not have a contact email. Please first set a contact email and then try again.'))
return HttpResponseRedirect(request.get_full_path())
for waiter in queryset:
waiter.invited_for_group = group
waiter.save()
waiter.invite_to_group(group)
waiter.invite_to_group()
messages.success(request,
_("Successfully invited %(name)s to %(group)s.") % {'name': waiter.name, 'group': waiter.invited_for_group.name})
@ -672,19 +421,13 @@ class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin):
_("An error occurred while trying to invite said members. Please try again."))
return HttpResponseRedirect(request.get_full_path())
if not group.contact_email:
messages.error(request,
_('The selected group does not have a contact email. Please first set a contact email and then try again.'))
return HttpResponseRedirect(request.get_full_path())
waiter.invited_for_group = group
waiter.save()
waiter.invite_to_group(group)
waiter.invite_to_group()
messages.success(request,
_("Successfully invited %(name)s to %(group)s.") % {'name': waiter.name, 'group': waiter.invited_for_group.name})
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (waiter._meta.app_label, waiter._meta.model_name),
args=(object_id,)))
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (waiter._meta.app_label, waiter._meta.model_name)))
context = dict(self.admin_site.each_context(request),
title=_('Select group for invitation'),
@ -696,7 +439,6 @@ class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin):
'admin/invite_for_group.html',
context=context)
class RegistrationPasswordInline(admin.TabularInline):
model = RegistrationPassword
extra = 0
@ -715,12 +457,10 @@ class GroupAdminForm(forms.ModelForm):
class GroupAdmin(CommonAdminMixin, admin.ModelAdmin):
fields = ['name', 'description', 'year_from', 'year_to', 'leiters', 'contact_email', 'show_website',
'weekday', ('start_time', 'end_time')]
fields = ['name', 'description', 'year_from', 'year_to', 'leiters', 'show_website']
form = GroupAdminForm
list_display = ('name', 'year_from', 'year_to')
inlines = [RegistrationPasswordInline, PermissionOnGroupInline]
search_fields = ('name',)
class ActivityCategoryAdmin(admin.ModelAdmin):
@ -749,7 +489,7 @@ class FreizeitAdminForm(forms.ModelForm):
self.fields['jugendleiter'].queryset = q.filter(group__name='Jugendleiter')
class BillOnExcursionInline(CommonAdminInlineMixin, admin.TabularInline):
class BillOnExcursionInline(FilteredMemberFieldMixin, CommonAdminInlineMixin, admin.TabularInline):
model = BillOnExcursionProxy
extra = 0
sortable_options = []
@ -762,7 +502,6 @@ class BillOnExcursionInline(CommonAdminInlineMixin, admin.TabularInline):
class StatementOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline):
model = Statement
extra = 1
description = _('Please list here all expenses in relation with this excursion and upload relevant bills. These have to be permanently stored for the application of LJP contributions. The short descriptions are used in the seminar report cost overview (possible descriptions are e.g. food, material, etc.).')
sortable_options = []
fields = ['night_cost']
inlines = [BillOnExcursionInline]
@ -780,15 +519,13 @@ class InterventionOnLJPInline(CommonAdminInlineMixin, admin.TabularInline):
class LJPOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline):
model = LJPProposal
extra = 1
description = _('Here you can work on a seminar report for applying for financial contributions from Landesjugendplan (LJP). More information on creating a seminar report can be found in the wiki. The seminar report or only a participant list and cost overview can be consequently downloaded.')
sortable_options = []
inlines = [InterventionOnLJPInline]
class MemberOnListInline(CommonAdminInlineMixin, GenericTabularInline):
class MemberOnListInline(FilteredMemberFieldMixin, CommonAdminInlineMixin, GenericTabularInline):
model = NewMemberOnList
extra = 0
description = _('Please list all participants (also youth leaders) of this excursion. Here you can still make changes just before departure and hence generate the latest participant list for crisis intervention at all times.')
formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
}
@ -847,29 +584,14 @@ class MemberNoteListAdmin(admin.ModelAdmin):
summary.short_description = _('Generate PDF summary')
class GenerateSeminarReportForm(forms.Form):
modes = (('full', _('Full report')),
('basic', _('Costs and participants only')))
mode = forms.ChoiceField(choices=modes, label=_('Mode'))
prepend_v32 = forms.BooleanField(label=_('Prepend V32'), initial=True,
widget=CheckboxInput(attrs={'style': 'display: inherit'}),
required=False)
class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
class FreizeitAdmin(FilteredMemberFieldMixin, CommonAdminMixin, nested_admin.NestedModelAdmin):
#inlines = [MemberOnListInline, LJPOnListInline, StatementOnListInline]
form = FreizeitAdminForm
list_display = ['__str__', 'date']
search_fields = ('name',)
ordering = ('-date',)
actions = ['crisis_intervention_list', 'notes_list', 'seminar_report']
view_on_site = False
fieldsets = (
(None, {
'fields': ('name', 'place', 'destination', 'date', 'end', 'description', 'groups', 'jugendleiter',
'tour_type', 'tour_approach', 'kilometers_traveled', 'activity', 'difficulty'),
'description': _('General information on your excursion. These are partly relevant for the amount of financial compensation (means of transport, travel distance, etc.).')
}),
)
#formfield_overrides = {
# ManyToManyField: {'widget': forms.CheckboxSelectMultiple},
# ForeignKey: {'widget': apply_select2(forms.Select)}
@ -900,14 +622,18 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
_("You are not allowed to view all members on excursion %(name)s.") % {'name': memberlist.name})
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
def crisis_intervention_list(self, request, memberlist):
def crisis_intervention_list(self, request, queryset):
# this ensures legacy compatibilty
memberlist = queryset[0]
if not self.may_view_excursion(request, memberlist):
return self.not_allowed_view(request, memberlist)
context = dict(memberlist=memberlist, settings=settings)
return render_tex(memberlist.name + "_Krisenliste", 'members/crisis_intervention_list.tex', context)
crisis_intervention_list.short_description = _('Generate crisis intervention list')
def notes_list(self, request, memberlist):
def notes_list(self, request, queryset):
# this ensures legacy compatibilty
memberlist = queryset[0]
if not self.may_view_excursion(request, memberlist):
return self.not_allowed_view(request, memberlist)
people, skills = memberlist.skill_summary
@ -915,72 +641,15 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
return render_tex(memberlist.name + "_Notizen", 'members/notes_list.tex', context)
notes_list.short_description = _('Generate overview')
def render_seminar_report_options(self, request, memberlist, form):
context = dict(self.admin_site.each_context(request),
title=_('Generate seminar report'),
opts=self.opts,
memberlist=memberlist,
form=form,
object=memberlist)
return render(request, 'admin/generate_seminar_report.html', context=context)
def seminar_report(self, request, memberlist):
if not self.may_view_excursion(request, memberlist):
return self.not_allowed_view(request, memberlist)
if "apply" in request.POST:
form = GenerateSeminarReportForm(request.POST)
if not form.is_valid():
messages.error(request, _('Please select a mode.'))
return self.render_seminar_report_options(request, memberlist, form)
mode = form.cleaned_data['mode']
prepend_v32 = form.cleaned_data['prepend_v32']
if mode == 'full' and not hasattr(memberlist, 'ljpproposal'):
messages.error(request, _('Full mode is only available, if the seminar report section is filled out.'))
return self.render_seminar_report_options(request, memberlist, form)
title = memberlist.ljpproposal.title if hasattr(memberlist, 'ljpproposal') else memberlist.name
context = dict(memberlist=memberlist, settings=settings, mode=mode)
fp = render_tex(title + '_Seminarbericht', 'members/seminar_report.tex', context, save_only=True)
if prepend_v32:
context = memberlist.v32_fields()
v32_fp = fill_pdf_form(title + "_LJP_V32",
'members/V32-1_Themenorientierte_Bildungsmassnahmen.pdf',
context,
save_only=True)
return merge_pdfs(title + 'LJP_Antrag', [v32_fp, fp])
return serve_pdf(fp)
return self.render_seminar_report_options(request, memberlist, GenerateSeminarReportForm())
seminar_report.short_description = _('Generate seminar report')
def sjr_application(self, request, memberlist):
def seminar_report(self, request, queryset):
# this ensures legacy compatibilty
memberlist = queryset[0]
if not self.may_view_excursion(request, memberlist):
return self.not_allowed_view(request, memberlist)
context = memberlist.sjr_application_fields()
if hasattr(memberlist, 'statement'):
attachments = [b.proof.path for b in memberlist.statement.bill_set.all() if b.proof]
else:
attachments = []
context = dict(memberlist=memberlist, settings=settings)
title = memberlist.ljpproposal.title if hasattr(memberlist, 'ljpproposal') else memberlist.name
return fill_pdf_form(title + "_SJR_Antrag", 'members/sjr_template.pdf', context, attachments)
sjr_application.short_description = _('Generate SJR application')
def finance_overview(self, request, memberlist):
if not memberlist.statement:
messages.error(request, _("No statement found. Please add a statement and then retry."))
if "apply" in request.POST:
memberlist.statement.submit(get_member(request))
messages.success(request,
_("Successfully submited statement. The finance department will notify you as soon as possible."))
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(memberlist.pk,)))
context = dict(self.admin_site.each_context(request),
title=_('Finance overview'),
opts=self.opts,
memberlist=memberlist,
object=memberlist,
participant_count=memberlist.participant_count,
ljp_contributions=memberlist.potential_ljp_contributions,
total_relative_costs=memberlist.total_relative_costs,
**memberlist.statement.template_context())
return render(request, 'admin/freizeit_finance_overview.html', context=context)
return render_tex(title + "_Seminarbericht", 'members/seminar_report.tex', context)
seminar_report.short_description = _('Generate seminar report')
def get_urls(self):
urls = super().get_urls()
@ -1002,16 +671,12 @@ class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
return custom_urls + urls
def action_view(self, request, object_id):
if "sjr_application" in request.POST:
return self.sjr_application(request, Freizeit.objects.get(pk=object_id))
if "seminar_report" in request.POST:
return self.seminar_report(request, Freizeit.objects.get(pk=object_id))
return self.seminar_report(request, [Freizeit.objects.get(pk=object_id)])
if "notes_list" in request.POST:
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:
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 self.crisis_intervention_list(request, [Freizeit.objects.get(pk=object_id)])
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name),
args=(object_id,)))

@ -4,4 +4,4 @@ from django.utils.translation import gettext_lazy as _
class MembersConfig(AppConfig):
name = 'members'
verbose_name = _('member administration')
verbose_name = _('members')

File diff suppressed because it is too large Load Diff

@ -1,156 +0,0 @@
# Generated by Django 4.0.1 on 2024-10-13 19:02
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import rules.contrib.models
class Migration(migrations.Migration):
dependencies = [
('members', '0013_memberwaitinglist_add_application_text_and_date'),
]
operations = [
migrations.RemoveField(
model_name='member',
name='civil_status',
),
migrations.RemoveField(
model_name='member',
name='nationality',
),
migrations.RemoveField(
model_name='member',
name='registered',
),
migrations.RemoveField(
model_name='member',
name='rock_experience',
),
migrations.RemoveField(
model_name='member',
name='technical_comments',
),
migrations.AddField(
model_name='member',
name='alpine_experience',
field=models.TextField(blank=True, default='', verbose_name='Alpine experience'),
),
migrations.RemoveField(
model_name='member',
name='gender',
),
migrations.AddField(
model_name='member',
name='gender',
field=models.IntegerField(choices=[(0, 'Männlich'), (1, 'Weiblich'), (2, 'Divers')], default=2, verbose_name='Gender'),
),
migrations.RemoveField(
model_name='member',
name='swimming_badge',
),
migrations.AddField(
model_name='member',
name='swimming_badge',
field=models.BooleanField(default=False, verbose_name='Knows how to swim'),
),
migrations.RenameField(
model_name='member',
old_name='confirm_mail_parents_key',
new_name='confirm_alternative_mail_key',
),
migrations.RemoveField(
model_name='member',
name='cc_email_parents',
),
migrations.RemoveField(
model_name='member',
name='confirmed_mail_parents',
),
migrations.RemoveField(
model_name='member',
name='email_parents',
),
migrations.RemoveField(
model_name='member',
name='phone_number_mobile',
),
migrations.RemoveField(
model_name='member',
name='phone_number_parents',
),
migrations.RemoveField(
model_name='member',
name='phone_number_private',
),
migrations.RemoveField(
model_name='memberwaitinglist',
name='cc_email_parents',
),
migrations.RemoveField(
model_name='memberwaitinglist',
name='confirm_mail_parents_key',
),
migrations.RemoveField(
model_name='memberwaitinglist',
name='confirmed_mail_parents',
),
migrations.RemoveField(
model_name='memberwaitinglist',
name='email_parents',
),
migrations.AddField(
model_name='member',
name='alternative_email',
field=models.EmailField(blank=True, default=None, max_length=100),
),
migrations.AddField(
model_name='member',
name='confirmed_alternative_mail',
field=models.BooleanField(default=True, verbose_name='Alternative email confirmed'),
),
migrations.AddField(
model_name='member',
name='phone_number',
field=models.CharField(default='', max_length=100, verbose_name='phone number'),
preserve_default=False,
),
migrations.CreateModel(
name='EmergencyContact',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.member', verbose_name='Member')),
('confirm_mail_key', models.CharField(default='', max_length=32)),
('confirmed_mail', models.BooleanField(default=True, verbose_name='Email confirmed')),
('email', models.EmailField(default='', max_length=100)),
('lastname', models.CharField(default='', max_length=20, verbose_name='last name')),
('phone_number', models.CharField(default='', max_length=100, verbose_name='phone number')),
('prename', models.CharField(default='', max_length=20, verbose_name='prename')),
],
options={
'verbose_name': 'Emergency contact',
'verbose_name_plural': 'Emergency contacts',
'abstract': False,
'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'),
},
bases=(models.Model, rules.contrib.models.RulesModelMixin),
),
migrations.AlterField(
model_name='memberwaitinglist',
name='application_text',
field=models.TextField(blank=True, default='', verbose_name='Do you want to tell us something else?'),
),
migrations.AddField(
model_name='memberwaitinglist',
name='gender',
field=models.IntegerField(choices=[(0, 'Männlich'), (1, 'Weiblich'), (2, 'Divers')], default=2, verbose_name='Gender'),
),
migrations.AlterField(
model_name='memberwaitinglist',
name='application_date',
field=models.DateTimeField(auto_now=True, default=django.utils.timezone.now, verbose_name='application date'),
preserve_default=False,
),
]

@ -1,28 +0,0 @@
# Generated by Django 4.0.1 on 2024-10-13 19:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0014_remove_fields_alternative_email'),
]
operations = [
migrations.AlterField(
model_name='emergencycontact',
name='lastname',
field=models.CharField(max_length=20, verbose_name='last name'),
),
migrations.AlterField(
model_name='emergencycontact',
name='phone_number',
field=models.CharField(max_length=100, verbose_name='phone number'),
),
migrations.AlterField(
model_name='emergencycontact',
name='prename',
field=models.CharField(max_length=20, verbose_name='prename'),
),
]

@ -1,28 +0,0 @@
# Generated by Django 4.0.1 on 2024-10-15 21:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0015_alter_emergencycontact_lastname_and_more'),
]
operations = [
migrations.AlterField(
model_name='emergencycontact',
name='confirmed_mail',
field=models.BooleanField(default=False, verbose_name='Email confirmed'),
),
migrations.AlterField(
model_name='member',
name='confirmed_mail',
field=models.BooleanField(default=False, verbose_name='Email confirmed'),
),
migrations.AlterField(
model_name='memberwaitinglist',
name='confirmed_mail',
field=models.BooleanField(default=False, verbose_name='Email confirmed'),
),
]

@ -1,18 +0,0 @@
# Generated by Django 4.0.1 on 2024-10-15 21:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0016_alter_emergencycontact_confirmed_mail_and_more'),
]
operations = [
migrations.AlterField(
model_name='member',
name='alternative_email',
field=models.EmailField(blank=True, default=None, max_length=100, null=True),
),
]

@ -1,28 +0,0 @@
# Generated by Django 4.0.1 on 2024-10-20 23:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0017_alter_member_alternative_email'),
]
operations = [
migrations.AddField(
model_name='group',
name='end_time',
field=models.TimeField(blank=True, null=True, verbose_name='Ending time'),
),
migrations.AddField(
model_name='group',
name='start_time',
field=models.TimeField(blank=True, null=True, verbose_name='Starting time'),
),
migrations.AddField(
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),
),
]

@ -1,23 +0,0 @@
# Generated by Django 4.0.1 on 2024-10-27 19:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0018_group_add_times'),
]
operations = [
migrations.AddField(
model_name='memberwaitinglist',
name='last_reminder',
field=models.DateTimeField(auto_now=True, verbose_name='Last reminder'),
),
migrations.AddField(
model_name='memberwaitinglist',
name='sent_reminders',
field=models.IntegerField(default=0, verbose_name='Missed reminders'),
),
]

@ -1,27 +0,0 @@
# Generated by Django 4.0.1 on 2024-11-17 15:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0019_memberwaitinglist_last_reminder_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='freizeit',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'verbose_name': 'Excursion', 'verbose_name_plural': 'Excursions'},
),
migrations.AddField(
model_name='freizeit',
name='description',
field=models.TextField(blank=True, default='', verbose_name='Description'),
),
migrations.AlterField(
model_name='freizeit',
name='destination',
field=models.CharField(blank=True, default='', help_text='e.g. a peak', max_length=50, verbose_name='Destination (optional)'),
),
]

@ -1,34 +0,0 @@
# Generated by Django 4.0.1 on 2024-11-17 21:32
from django.db import migrations, models
import django.db.models.deletion
import members.models
class Migration(migrations.Migration):
dependencies = [
('members', '0020_alter_freizeit_options_freizeit_description_and_more'),
]
operations = [
migrations.CreateModel(
name='InvitationToGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(auto_now=True, verbose_name='Invitation date')),
('rejected', models.BooleanField(default=False, verbose_name='Invitation rejected')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.group', verbose_name='Group')),
('waiter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.memberwaitinglist', verbose_name='Waiter')),
('key', models.CharField(default=members.models.gen_key, max_length=32)),
],
options={
'verbose_name': 'Invitation to group',
'verbose_name_plural': 'Invitations to groups',
},
),
migrations.RemoveField(
model_name='memberwaitinglist',
name='invited_for_group',
),
]

@ -1,43 +0,0 @@
# Generated by Django 4.0.1 on 2024-11-18 20:29
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('members', '0021_group_invitations'),
]
operations = [
migrations.RemoveField(
model_name='member',
name='good_conduct_certificate_presentation_needed',
),
migrations.AlterField(
model_name='member',
name='allergies',
field=models.TextField(blank=True, default='', verbose_name='Allergies'),
),
migrations.AlterField(
model_name='member',
name='medication',
field=models.TextField(blank=True, default='', verbose_name='Medication'),
),
migrations.AddField(
model_name='member',
name='may_cancel_appointment_independently',
field=models.BooleanField(blank=True, default=None, null=True, verbose_name='May cancel a group appointment independently'),
),
migrations.AlterField(
model_name='member',
name='phone_number',
field=models.CharField(blank=True, default='', max_length=100, verbose_name='phone number'),
),
migrations.AlterField(
model_name='memberwaitinglist',
name='application_date',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='application date'),
),
]

@ -1,21 +0,0 @@
# Generated by Django 4.0.1 on 2024-11-19 00:22
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('members', '0022_adapt_fields'),
]
operations = [
migrations.AlterField(
model_name='member',
name='user',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Login data'),
),
]

@ -1,18 +0,0 @@
# Generated by Django 4.0.1 on 2024-11-23 19:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0023_alter_member_user'),
]
operations = [
migrations.AddField(
model_name='member',
name='invite_as_user_key',
field=models.CharField(default='', max_length=32),
),
]

@ -1,17 +0,0 @@
# Generated by Django 4.0.1 on 2024-11-24 00:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('members', '0024_member_invite_as_user_key'),
]
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.'), ('change_member_group', 'Can change the group field'), ('may_invite_as_user', 'Is allowed to invite a member to set login data.')), 'verbose_name': 'member', 'verbose_name_plural': 'members'},
),
]

@ -1,18 +0,0 @@
# 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),
),
]

@ -1,18 +0,0 @@
# 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'),
),
]

@ -1,20 +0,0 @@
# 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'),
),
]

@ -1,23 +0,0 @@
# 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'),
),
]

@ -1,17 +0,0 @@
# 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'},
),
]

File diff suppressed because it is too large Load Diff

@ -4,23 +4,10 @@ import os
import subprocess
import time
import glob
from io import BytesIO
from pypdf import PdfReader, PdfWriter
from django import template
from django.template.loader import get_template
from django.conf import settings
from django.http import HttpResponse, HttpResponseRedirect
from wsgiref.util import FileWrapper
from PIL import Image
def find_template(template_name):
for engine in template.engines.all():
for loader in engine.engine.template_loaders:
for origin in loader.get_template_sources(template_name):
if os.path.exists(origin.name):
return origin.name
raise template.TemplateDoesNotExist(f"Could not find template: {template_name}")
def media_path(fp):
@ -31,17 +18,7 @@ def media_dir():
return os.path.join(settings.MEDIA_MEMBERLISTS, "memberlists")
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):
def render_tex(name, template_path, context):
filename = name + "_" + datetime.today().strftime("%d_%m_%Y")
filename = filename.replace(' ', '_').replace('&', '').replace('/', '_')
# drop umlauts, accents etc.
@ -74,58 +51,10 @@ def render_tex(name, template_path, context, save_only=False):
os.chdir(oldwd)
if save_only:
return filename_pdf
return serve_pdf(filename_pdf)
def fill_pdf_form(name, template_path, fields, attachments=[], save_only=False):
filename = name + "_" + datetime.today().strftime("%d_%m_%Y")
filename = filename.replace(' ', '_').replace('&', '').replace('/', '_')
# drop umlauts, accents etc.
filename = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore').decode()
filename_pdf = filename + '.pdf'
path = find_template(template_path)
if not os.path.exists(media_dir()):
os.makedirs(media_dir())
reader = PdfReader(path)
writer = PdfWriter()
writer.append(reader)
writer.update_page_form_field_values(None, fields, auto_regenerate=False)
for fp in attachments:
try:
img = Image.open(fp)
img_pdf = BytesIO()
img.save(img_pdf, "pdf")
writer.append(img_pdf)
except:
print("Could not add image", fp)
with open(media_path(filename_pdf), 'wb') as output_stream:
writer.write(output_stream)
if save_only:
return filename_pdf
return serve_pdf(filename_pdf)
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()
# provide the user with the resulting pdf file
with open(media_path(filename_pdf), 'rb') as pdf:
response = HttpResponse(FileWrapper(pdf))#, content='application/pdf')
response['Content-Type'] = 'application/pdf'
response['Content-Disposition'] = 'attachment; filename='+filename_pdf
if save_only:
return filename_pdf
return serve_pdf(filename_pdf)
return response

@ -5,16 +5,6 @@ from .models import MemberWaitingList
@shared_task
def ask_for_waiting_confirmation():
reminder_cutoff = timezone.now() - timezone.timedelta(days=settings.CONFIRMATION_REMINDER_FREQUENCY)
cutoff = timezone.now() - timezone.timedelta(days=settings.WAITING_CONFIRMATION_FREQUENCY)
no = 0
# we ask all waiters for wait confirmation whose last confirmed waiting status is at least
# settings.WAITING_CONFIRMATION_FREQUENCY days ago, who have not received a reminder
# in the last settings.CONFIRMATION_REMINDER_FREQUENCY days and
# who have yet received strictly less reminders then settings.MAX_REMINDER_COUNT.
for waiter in MemberWaitingList.objects.filter(last_wait_confirmation__lte=cutoff,
last_reminder__lte=reminder_cutoff,
sent_reminders__lt=settings.MAX_REMINDER_COUNT):
for waiter in MemberWaitingList.objects.filter(last_wait_confirmation__lte=cutoff):
waiter.ask_for_wait_confirmation()
no += 1
return no

@ -1,48 +0,0 @@
{% 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 %}

@ -1,167 +0,0 @@
{% 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 %}

@ -1,63 +0,0 @@
{% 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 'Generate seminar report' %}
</div>
{% endblock %}
{% block content %}
<p>
{% 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 %}
</p>
<p>
{% blocktrans %}Expenses with same short description are automatically summed up and shown as one expense in the
expense overview.{% endblocktrans %}
</p>
<ul>
<li>
{% blocktrans %}Full report: Include learning goals and a detailed, tabularized time schedule. This requires
the seminar report section to be filled out.{% endblocktrans %}
</li>
<li>
{% blocktrans %}Costs and participants only: Only show a list of participants and costs. In this case you
have to add learning goals and a time schedule manually.{% endblocktrans %}
</li>
</ul>
<br>
<p>{% blocktrans %}You may also choose to include the V32 attachment.{% endblocktrans %}</p>
<form action="" method="post">
{% csrf_token %}
<p>
<table>
{{ form }}
</table>
</p>
<br>
<input type="hidden" name="action" value="seminar_report">
<input type="hidden" name="seminar_report">
<input class="default" style="color: $default-link-color" type="submit" name="apply"
value="{% translate 'Generate' %}">
<a href="#" class="button cancel-link">{% translate "Cancel" %}</a>
</form>
{% endblock %}

@ -1,40 +0,0 @@
{% 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 'Invite as user' %}
</div>
{% endblock %}
{% block content %}
<p>
{% blocktrans %}Do you want to invite {{ member }} to set their login data for Kompass? They will
receive an email with a link to set their username and password after entering one of the current
active registration passwords.{% endblocktrans %}
</p>
<form action="" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="invite_as_user">
<input class="default" style="color: $default-link-color" type="submit" name="apply"
value="{% translate 'Invite' %}">
<a href="#" class="button cancel-link">{% translate "Cancel" %}</a>
</form>
{% endblock %}

@ -1,52 +0,0 @@
{% 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 'Invite multiple members as users' %}
</div>
{% endblock %}
{% block content %}
<p>
{% trans "You selected the following members:" %}
</p>
<p>
{% for member in members %}
<ul>
<li>
<a href="{% url 'admin:members_member_change' member.id %}">{{ member }}</a>
</li>
</ul>
{% endfor %}
</p>
<p>
{% blocktrans %}Do you want to invite these members to set their login data for Kompass? They will
receive an email with a link to set their username and password after entering one of the current
active registration passwords.{% endblocktrans %}
</p>
<form action="" method="post">
{% csrf_token %}
{{form}}
<input type="hidden" name="action" value="invite_as_user_action">
<input class="default" style="color: $default-link-color" type="submit" name="apply"
value="{% translate 'Invite' %}">
<a href="#" class="button cancel-link">{% translate "Cancel" %}</a>
</form>
{% endblock %}

@ -3,7 +3,6 @@
{% load static %}
{% block after_field_sets %}
<!--
<p><b>{% trans "Participations:" %}</b></p>
<ul>
{% for activity in activities %}
@ -26,6 +25,5 @@
{% endfor %}
</table>
{% endif %}
-->
{% endblock %}

@ -1,6 +1,6 @@
{% load tex_extras %}
\documentclass[a4paper]{article}
\documentclass{article}
\usepackage[utf8]{inputenc}
\usepackage{booktabs}
@ -13,7 +13,6 @@
\usepackage[margin=1in]{geometry}
\usepackage{array}
\usepackage{tabularx}
\usepackage{ltablex}
\newcommand{\picpos}[4]{
\begin{textblock*}{#1}(#2, #3)
@ -43,7 +42,7 @@
}
\begin{document}
% HEADER RIGHT
\picpos{4.5cm}{11.5cm}{0cm}{/app/jdav_web/static/general/img/dav_logo_hd.png}
\picpos{4.5cm}{11.5cm}{0cm}{/app/jdav_web/static/general/img/dav_logo.png}
\begin{textblock*}{5cm}(11.5cm, 2.3cm)
\begin{flushright}
\small
@ -58,7 +57,7 @@
\end{textblock*}
% HEADLINE
{\noindent\LARGE{Teilnehmer*innenliste\\[2mm]Sektionsveranstaltung}}\\[1mm]
{\noindent\LARGE{Teilnehmer:innenliste\\[2mm]Sektionsveranstaltung}}\\[1mm]
\textit{Erstellt: {{ creation_date }} }\\
% DESCRIPTION TABLE
@ -66,8 +65,8 @@
\begin{tabular}{ll}
\large Aktivität: & {{ memberlist.name|esc_all }} \\
\large Gruppe: & {{ memberlist.groups_str|esc_all }} \\
\large Ort / Stützpunkt: & {{ memberlist.place|esc_all }} \\
\large ggf. Ziel: & {{ memberlist.destination|esc_all }} \\
\large Ziel: & {{ memberlist.place|esc_all }} \\
\large Stützpunkt: & {{ memberlist.destination|esc_all }} \\
\large Zeitraum: & {{ memberlist.time_period_str|esc_all }} \\
\large Betreuer:innen: & {{ memberlist.staff_str|esc_all }} \\
\large Art der Tour: & {% checked_if_true 'Gemeinschaftstour' memberlist.get_tour_type %}
@ -79,25 +78,22 @@
\end{tabular}
\end{table}
\begin{tabularx}{1\linewidth}{@{\extracolsep{\fill}}lLlLL}
\toprule
\textbf{Name} & \textbf{Anschrift} & \textbf{Telefon} & \textbf{Notfallkontakte} \\
\midrule
{% for m in memberlist.membersonlist.all %}
{{ m.member.name|esc_all }} &
{{ m.member.address|esc_all }} &
{{ m.member.contact_phone_number|esc_all }} &
{% for c in m.member.emergencycontact_set.all %}
{{ c.name }} \newline
Tel.: {{ c.phone_number }}
{% endfor %} \\
{% endfor %}
\bottomrule
\end{tabularx}
\begin{table}[H]
\begin{tabularx}{1\linewidth}{@{\extracolsep{\fill}}lLlL}
\toprule
\textbf{Name} & \textbf{Anschrift} & \textbf{Telefon} & \textbf{E-Mail} \\
\midrule
{% for m in memberlist.membersonlist.all %}
{{ m.member.name|esc_all }} & {{ m.member.address|esc_all }} & {{ m.member.contact_phone_number|esc_all }} & \Email{ {{ m.member.contact_email }} } \\
{% endfor %}
\bottomrule
\end{tabularx}
\end{table}
\vspace{1cm}
\noindent Bitte die ausgefüllte Teilnehmerliste vor Antritt der Aktivität per E-Mail an
\href{mailto:{{ settings.SEKTION_CRISIS_INTERVENTION_MAIL }}}{ {{ settings.SEKTION_CRISIS_INTERVENTION_MAIL }} } senden.
\href{mailto:{{ settings.SEKTION_CONTACT_MAIL }} }{ {{ settings.SEKTION_CONTACT_MAIL }} } und
\href{mailto:{{ settings.SEKTION_BOARD_MAIL }} }{ {{ settings.SEKTION_BOARD_MAIL }} } senden.
\end{document}

@ -18,6 +18,13 @@
<p><b>{{ error_message }}</b></p>
{% endif %}
{% include "members/member_form.html" %}
<form action="" method="post">
<table class="termine">
{% csrf_token %}
{{form}}
</table>
<input type="hidden" name="key" value="{{ key }}">
<p><input type="submit" value="{% trans "submit" %}"/></p>
</form>
{% endblock %}

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

@ -1,27 +0,0 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Echo" %}
{% endblock %}
{% block content %}
<h1>{% trans "Echo" %}</h1>
<p>{% blocktrans %}Thanks for echoing back. Please enter the password, which you can find in the email we sent you.
{% endblocktrans %}</p>
{% if error_message %}
<p><b>{{ error_message }}</b></p>
{% endif %}
<form action="" method="post">
{% csrf_token %}
<input type="password" name="password" required>
<input type="hidden" name="key" value="{{key}}">
<p><input type="submit" value="{% trans "submit" %}"/></p>
</form>
{% endblock %}

@ -1,15 +0,0 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Echo" %}
{% endblock %}
{% block content %}
<h1>{% trans "Echo" %}</h1>
<p>{% trans "You entered a wrong password to often." %}</p>
{% endblock %}

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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save