diff --git a/README.md b/README.md index 35e9d7d..e37f645 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,46 @@ -# kompass +# jdav Kompass [![Build Status](https://jenkins.merten.dev/buildStatus/icon?job=gitea%2Fkompass%2Fmain)](https://jenkins.merten.dev/job/gitea/job/kompass/job/main/) -This repository has the purpose to develop a webapplication that can be used by -JDAV to send newsletters, manage user lists and keep material lists up to date. -As this repository is also meant to be a base for exchange during development, feel free -to contribute ideas in form of edits to this README, issues, landmarks, projects, wiki entries, ... +Kompass is an administration platform designed for local sections of the Young German Alpine Club. It provides +tools to contact and (automatically) manage members, groups, material, excursions and statements. -# Docker +For more details on the features, see the (German) [documentation](https://jdav-hd.de/static/docs/index.html). -In the `docker` subfolder, there are `docker-compose.yaml`s for development and production use. For the development -version, no further setup is needed. +# Contributing -# Production +Any form of contribution is appreciated. If you found a bug or have a feature request, please file an +[issue](https://git.jdav-hd.merten.dev/digitales/kompass/issues). If you want to help with the documentation or +want to contribute code, please open a [pull request](https://git.jdav-hd.merten.dev/digitales/kompass/pulls). -In production, the docker setup needs an external database. The exact access credentials are configured in the respective -docker.env files. +The following is a short description of where to find the documentation with more information. -# Useful stuff -## Reset database for certain app +# Documentation -The following can be useful in case that automatic migrations throw errors. +Documentation is handled by [sphinx](https://www.sphinx-doc.org/) and located in `docs/`. -1. delete everything in the migrations folder except for __init__.py. -2. drop into my MySQL console and do: DELETE FROM django_migrations WHERE app='my_app' -3. while at the MySQL console, drop all of the tables associated with my_app. -4. re-run ./manage.py makemigrations my_app - this generates a 0001_initial.py file in my migrations folder. -5. run ./manage migrate my_app - I expect this command to re-build all my tables, but instead it says: "No migrations to apply." +The sphinx documentation contains information about: +- Development Setup +- Architecture +- Testing +- Production Deployment +- End user documentation +- and much more... + +> Please add all further documentation also in the sphinx documentation. And not in the readme + +## online +Online (latest release version): https://jdav-hd.de/static/docs/ + +## local +To read the documentation build it locally and view it in your browser: +```bash +cd docs/ +make html + +# MacOS (with firefox) +open -a firefox $(pwd)/docs/build/html/index.html +# Linux (I guess?!?) +firefox ${pwd}/docs/build/html/index.html +``` diff --git a/docker/test/docker-compose.yaml b/docker/test/docker-compose.yaml index 9bc9bae..edbb892 100644 --- a/docker/test/docker-compose.yaml +++ b/docker/test/docker-compose.yaml @@ -14,8 +14,8 @@ services: entrypoint: /app/docker/test/entrypoint-master.sh volumes: - type: bind - source: ./coverage.xml - target: /app/jdav_web/coverage.xml + source: ./htmlcov/ + target: /app/jdav_web/htmlcov/ cache: restart: always diff --git a/docker/test/entrypoint-master.sh b/docker/test/entrypoint-master.sh index 8c70043..2c80809 100755 --- a/docker/test/entrypoint-master.sh +++ b/docker/test/entrypoint-master.sh @@ -38,5 +38,5 @@ fi cd jdav_web -coverage run manage.py test startpage finance members -v 2 -coverage xml +coverage run manage.py test startpage finance members contrib logindata mailer material -v 2 --noinput +coverage html diff --git a/docs/source/_static/favicon.png b/docs/source/_static/favicon.png new file mode 100644 index 0000000..c8498b9 Binary files /dev/null and b/docs/source/_static/favicon.png differ diff --git a/docs/source/_static/favicon2.png b/docs/source/_static/favicon2.png new file mode 100644 index 0000000..bac2794 Binary files /dev/null and b/docs/source/_static/favicon2.png differ diff --git a/docs/source/_static/jdav_logo.png b/docs/source/_static/jdav_logo.png new file mode 100644 index 0000000..87e1400 Binary files /dev/null and b/docs/source/_static/jdav_logo.png differ diff --git a/docs/source/_static/jdav_logo_transparent.png b/docs/source/_static/jdav_logo_transparent.png new file mode 100644 index 0000000..d3ef7b7 Binary files /dev/null and b/docs/source/_static/jdav_logo_transparent.png differ diff --git a/docs/source/about.rst b/docs/source/about.rst new file mode 100644 index 0000000..d057902 --- /dev/null +++ b/docs/source/about.rst @@ -0,0 +1,20 @@ +:orphan: true + +.. meta:: + :description: Miscellaneous information about the Kompass project + +.. vale off + +About +===== + +.. rst-class:: lead + +.. attention:: + Die Seite befindet sich noch im Aufbau. -- The page is still under construction. + + (Stand: 08.01.2025) + + +- About the kompass project +- About this documentation diff --git a/docs/source/conf.py b/docs/source/conf.py index e3ff583..c4c7f60 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -3,15 +3,19 @@ # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html -# -- Project information ----------------------------------------------------- +from dataclasses import asdict +from sphinxawesome_theme import ThemeOptions + + +# -- 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' +author = 'The Kompass Team' +copyright = f'2025, {author}' -# -- General configuration --------------------------------------------------- +# -- General configuration ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [] @@ -21,8 +25,39 @@ exclude_patterns = [] language = 'de' -# -- Options for HTML output ------------------------------------------------- +# -- Options for HTML output --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'alabaster' +html_theme = 'sphinxawesome_theme' html_static_path = ['_static'] + + +# -- Sphinxawsome-theme options ------------------------------------------------ +# https://sphinxawesome.xyz/how-to/configure/ + +html_logo = "_static/favicon2.png" +html_favicon = "_static/favicon2.png" + +html_sidebars = { + "about": ["sidebar_main_nav_links.html"], + # "changelog": ["sidebar_main_nav_links.html"], +} + +# Code blocks color scheme +pygments_style = "emacs" +pygments_style_dark = "emacs" + +# Could be directly in html_theme_options, but this way it has type hints +# from sphinxawesome_theme +theme_options = ThemeOptions( + show_prev_next=True, + show_breadcrumbs=True, + main_nav_links={ + "Docs": "index", + "About": "about", + # "Changelog": "changelog" + }, + show_scrolltop=True, +) + +html_theme_options = asdict(theme_options) diff --git a/docs/source/development_manual/architecture.rst b/docs/source/development_manual/architecture.rst new file mode 100644 index 0000000..d278f90 --- /dev/null +++ b/docs/source/development_manual/architecture.rst @@ -0,0 +1,7 @@ +.. _development_manual/architecture: + +================= +Architecture +================= + +tbd \ No newline at end of file diff --git a/docs/source/development_manual/contributing.rst b/docs/source/development_manual/contributing.rst new file mode 100644 index 0000000..dce0c53 --- /dev/null +++ b/docs/source/development_manual/contributing.rst @@ -0,0 +1,85 @@ +.. _development_manual/contributing: + +============ +Contributing +============ + +Any form of contribution is appreciated. If you found a bug or have a feature request, please file an +`issue `_. If you want to help with the documentation or +want to contribute code, please open a `pull request `_. + +.. note:: + + Please read this page carefully before contributing. + +Miscellaneous +------------- + +- version control with `git `_ +- own gitea instance at https://git.jdav-hd.merten.dev/ +- protected ``main`` branch + +Organization and branches +------------------------- + +The stable development happens on the ``main``-branch for which only maintainers have write access. Any pull request +should hence be targeted at ``main``. Regularly, the production instances are updated to the latest ``main`` version, +in particular these are considered to be stable. + +If you have standard write access to the repository, feel free to create new branches. To make organization +easier, please follow the branch naming convention: ``/``. + +The ``testing``-branch is deployed on the development instances. No development should happen there, this branch +is regularly reset to the ``main``-branch. + + +Workflow +-------- + +- request a gitea account from the project maintainers +- decide on an `issue `_ to work on or create a new one +- branch out to an own branch (naming convention: ``/``) from the ``main``-branch +- work on the issue and commit your changes +- create a pull request from your branch to the ``main``-branch + + +.. _development_manual/contributing/documentation: + +Documentation +------------- + +If you want to contribute to the documentation, please follow the steps below. + +Online (latest release version): https://jdav-hd.de/static/docs/ + +- This documentation is build `sphinx `_ and `awsome sphinx theme `_ the source code is located in ``docs/``. +- All documentation is written in `reStructuredText `_ and uses the `sphinx directives `_. + - The directives can vary due to the theme, see the `awesome sphinx theme documentation `_. +- All technical documentation is written in english, user documentation is written in german. + +To read the documentation build it locally and view it in your browser: + +.. code-block:: bash + + cd docs/ + make html + + # MacOS (with firefox) + open -a firefox $(pwd)/docs/build/html/index.html + # Linux (I guess?!?) + firefox ${pwd}/docs/build/html/index.html + +Code +---- + +If you want to contribute code, please follow the inital setup steps in the :ref:`development_manual/setup` section. And dont forget to :ref:`document ` your code properly and write tests. + + +.. note:: + + Still open / to decide: + + - linting + - (auto) formatting + - reliable tests via ci/cd pipeline + diff --git a/docs/source/development_manual/deployment.rst b/docs/source/development_manual/deployment.rst new file mode 100644 index 0000000..afc9acc --- /dev/null +++ b/docs/source/development_manual/deployment.rst @@ -0,0 +1,7 @@ +.. _development_manual/deployment: + +===================== +Production Deployment +===================== + +tbd \ No newline at end of file diff --git a/docs/source/development_manual/index.rst b/docs/source/development_manual/index.rst new file mode 100644 index 0000000..f219dc9 --- /dev/null +++ b/docs/source/development_manual/index.rst @@ -0,0 +1,42 @@ +.. _development_manual/index: + +######################### +Development Documentation +######################### + +This part of the documentation describes the development and maintenance of the Kompass project. + +.. toctree:: + :titlesonly: + + contributing + setup + architecture + testing + deployment + + +Contributing +------------ + +Any form of contribution is appreciated! + +.. seealso:: + + :ref:`Contributing ` + + + +Documentation +------------- + +Structure + +- :ref:`Nutzer Dokumentation ` auf deutsch +- :ref:`Development Documentation ` auf englisch + +.. seealso:: + + :ref:`Contributing #Documentation ` + + diff --git a/docs/source/development_manual/setup.rst b/docs/source/development_manual/setup.rst new file mode 100644 index 0000000..25a8de6 --- /dev/null +++ b/docs/source/development_manual/setup.rst @@ -0,0 +1,101 @@ +.. _development_manual/setup: + +================= +Development Setup +================= + +The project is run with ``docker`` and all related files are in the ``docker/`` subfolder. Besides the actual Kompass +application, a database (postgresql) and a broker (redis) are setup and run in the docker container. No +external services are needed for running the development container. + +Initial installation +-------------------- + +A working ``docker`` setup (with ``docker compose``) is required. For installation instructions see the +`docker manual `_. + +1. Clone the repository and change into the directory of the repository. + +2. Fetch submodules + +.. code-block:: bash + + git submodule update --init + + +.. _step-3: + +3. Prepare development environment: to allow automatic rebuilding upon changes in the source, + the owner of the ``/app/jdav_web`` directory in the Docker container must match your + user. For this, make sure that the output of ``echo UID`` and ``echo UID`` is not empty. Then run + +.. code-block:: bash + + export GID=${GID} + export UID=${UID} + +4. Start docker + +.. code-block:: bash + + cd docker/development + docker compose up + +This runs the docker in your current shell, which is useful to see any log output. If you want to run +the development server in the background instead, use ``docker compose up -d``. + +During the initial run, the container is built and all dependencies are installed which can take a few minutes. +After successful installation, the Kompass initialization runs, which in particular sets up all tables in the +database. + +If you need to rebuild the container (e.g. after changing the ``requirements.txt``), execute + +.. code-block:: bash + + docker compose up --build + +5. Setup admin user: in a separate shell, while the docker container is running, execute + +.. code-block:: bash + + cd docker/development + docker compose exec master bash -c "cd jdav_web && python3 manage.py createsuperuser" + +This creates an admin user for the administration interface. + + +Development +----------- + +If the initial installation was successful, you can start developing. Changes to files cause an automatic +reload of the development server. If you need to generate and perform database migrations or generate locale files, +use + +.. code-block:: bash + + cd docker/development + docker compose exec master bash + cd jdav_web + +This starts a shell in the container, where you can execute any django maintenance commands via +``python3 manage.py ``. For more information, see the https://docs.djangoproject.com/en/4.0/ref/django-admin. + + + + +Known Issues +------------ + +- If the ``UID`` and ``GID`` variables are not setup properly, you will encounter the following error message + after running ``docker compose up``. + +.. code-block:: bash + + => ERROR [master 6/7] RUN groupadd -g fritze && useradd -g -u -m -d /app fritze 0.2s + ------ + > [master 6/7] RUN groupadd -g fritze && useradd -g -u -m -d /app fritze: + 0.141 groupadd: invalid group ID 'fritze' + ------ + failed to solve: process "/bin/sh -c groupadd -g $GID $USER && useradd -g $GID -u $UID -m -d /app $USER" did not complete successfully: exit code: 3 + +In this case repeat :ref:`step 3 ` above. diff --git a/docs/source/development_manual/testing.rst b/docs/source/development_manual/testing.rst new file mode 100644 index 0000000..218894f --- /dev/null +++ b/docs/source/development_manual/testing.rst @@ -0,0 +1,7 @@ +.. _development_manual/testing: + +================= +Testing +================= + +To run the tests, you can use the docker setup under ``docker/test``. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 9970d37..5c4bddb 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -2,42 +2,41 @@ 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. +.. _index: -======= -Kompass -======= +############ +jdav 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. +erste mal hier bist, schau doch mal :ref:`user_manual/getstarted` an. + +.. attention:: + Die Dokumentation befindet sich noch im Aufbau. -- The documentation is still under construction. + + (Stand: 08.01.2025) + -Was ist der Kompass? +Nutzer Dokumentation -------------------- -Der Kompass ist eine Verwaltungsplattform für die tägliche Jugendarbeit in der JDAV. -Die wichtigsten Funktionen sind +- auf deutsch +- Hier findest Du als Nutzer alles was Du wissen musst um den Kompass bedienen zu können. -- Verwaltung von Teilnehmer\*innen von Jugendgruppen -- Organisation von Ausfahrten -- Abwicklung von Abrechnungen -- Senden von E-Mails +.. toctree:: + :titlesonly: -Neben diesen Funktionen für die tägliche Arbeit, automatisiert der Kompass die -Aufnahme von neuen Mitgliedern und die Pflege der Daten durch + user_manual/index -- Wartelistenverwaltung -- Registrierung neuer Mitglieder -- Rückmeldeverfahren -.. _index: +Development Documentation +------------------------- -Inhaltsverzeichnis ------------------- +- auf englisch +- Hier findest Du als Entwickler alles was Du wissen musst um den Kompass entwickeln und zu pflegen. .. toctree:: - :maxdepth: 2 + :titlesonly: + + development_manual/index - getstarted - members - excursions - waitinglist - finance diff --git a/docs/source/excursions.rst b/docs/source/user_manual/excursions.rst similarity index 92% rename from docs/source/excursions.rst rename to docs/source/user_manual/excursions.rst index eb50caf..6169a87 100644 --- a/docs/source/excursions.rst +++ b/docs/source/user_manual/excursions.rst @@ -1,9 +1,9 @@ -.. _excursions: +.. _user_manual/excursions: Ausfahrten ========== -Neben der :ref:`Teilnehmer\*innenverwaltung ` ist das Abwickeln von Ausfahrten +Neben der :ref:`Teilnehmer\*innenverwaltung ` 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. @@ -43,6 +43,8 @@ Seminarbericht direkt ein und lass dir den Papierkram vom Kompass erledigen. SJR Antrag ---------- +tbd + Abrechnung ---------- diff --git a/docs/source/finance.rst b/docs/source/user_manual/finance.rst similarity index 91% rename from docs/source/finance.rst rename to docs/source/user_manual/finance.rst index e093ee1..238aacf 100644 --- a/docs/source/finance.rst +++ b/docs/source/user_manual/finance.rst @@ -1,3 +1,5 @@ +.. _user_manual/finance: + Finanzen ======== diff --git a/docs/source/getstarted.rst b/docs/source/user_manual/getstarted.rst similarity index 97% rename from docs/source/getstarted.rst rename to docs/source/user_manual/getstarted.rst index 5010292..72e9d0c 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/user_manual/getstarted.rst @@ -1,4 +1,4 @@ -.. _first-steps: +.. _user_manual/getstarted: Erste Schritte ============== @@ -29,7 +29,7 @@ er auf den entsprechenden Eintrag, ändert das Geburtsdatum und klickt auf *Spei .. 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. Für mehr Details siehe :ref:`Teilnehmer\*innenverwaltung ` + Manche Einträge wiederum kannst du einsehen, aber nicht bearbeiten. Für mehr Details siehe :ref:`Teilnehmer\*innenverwaltung ` Probier doch einmal aus deinen eigenen Eintrag zu ändern. Sicherlich gibt es einige Felder, die nicht ausgefüllt oder nicht mehr aktuell sind. Lade z.B. ein Bild von dir hoch, @@ -70,7 +70,7 @@ 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`. +geh zurück zur :ref:`user_manual/index`. .. _Startseite: https://jdav-hd.de/kompass .. _Teilnehmer\*innenanzeige: https://jdav-hd.de/kompassmembers/member/ diff --git a/docs/source/images/members_change_tabs.png b/docs/source/user_manual/images/members_change_tabs.png similarity index 100% rename from docs/source/images/members_change_tabs.png rename to docs/source/user_manual/images/members_change_tabs.png diff --git a/docs/source/images/members_changelist_action.png b/docs/source/user_manual/images/members_changelist_action.png similarity index 100% rename from docs/source/images/members_changelist_action.png rename to docs/source/user_manual/images/members_changelist_action.png diff --git a/docs/source/images/members_changelist_filters.png b/docs/source/user_manual/images/members_changelist_filters.png similarity index 100% rename from docs/source/images/members_changelist_filters.png rename to docs/source/user_manual/images/members_changelist_filters.png diff --git a/docs/source/images/members_changelist_group_filter.png b/docs/source/user_manual/images/members_changelist_group_filter.png similarity index 100% rename from docs/source/images/members_changelist_group_filter.png rename to docs/source/user_manual/images/members_changelist_group_filter.png diff --git a/docs/source/images/members_changelist_pages.png b/docs/source/user_manual/images/members_changelist_pages.png similarity index 100% rename from docs/source/images/members_changelist_pages.png rename to docs/source/user_manual/images/members_changelist_pages.png diff --git a/docs/source/images/members_changelist_sorting.png b/docs/source/user_manual/images/members_changelist_sorting.png similarity index 100% rename from docs/source/images/members_changelist_sorting.png rename to docs/source/user_manual/images/members_changelist_sorting.png diff --git a/docs/source/images/members_registration_form.png b/docs/source/user_manual/images/members_registration_form.png similarity index 100% rename from docs/source/images/members_registration_form.png rename to docs/source/user_manual/images/members_registration_form.png diff --git a/docs/source/images/members_unconfirmed_registration_demote.png b/docs/source/user_manual/images/members_unconfirmed_registration_demote.png similarity index 100% rename from docs/source/images/members_unconfirmed_registration_demote.png rename to docs/source/user_manual/images/members_unconfirmed_registration_demote.png diff --git a/docs/source/images/members_waitinglist_change_invite_to_group.png b/docs/source/user_manual/images/members_waitinglist_change_invite_to_group.png similarity index 100% rename from docs/source/images/members_waitinglist_change_invite_to_group.png rename to docs/source/user_manual/images/members_waitinglist_change_invite_to_group.png diff --git a/docs/source/images/members_waitinglist_change_invite_to_group_button.png b/docs/source/user_manual/images/members_waitinglist_change_invite_to_group_button.png similarity index 100% rename from docs/source/images/members_waitinglist_change_invite_to_group_button.png rename to docs/source/user_manual/images/members_waitinglist_change_invite_to_group_button.png diff --git a/docs/source/images/members_waitinglist_change_invite_to_group_selection.png b/docs/source/user_manual/images/members_waitinglist_change_invite_to_group_selection.png similarity index 100% rename from docs/source/images/members_waitinglist_change_invite_to_group_selection.png rename to docs/source/user_manual/images/members_waitinglist_change_invite_to_group_selection.png diff --git a/docs/source/user_manual/index.rst b/docs/source/user_manual/index.rst new file mode 100644 index 0000000..6d802e8 --- /dev/null +++ b/docs/source/user_manual/index.rst @@ -0,0 +1,50 @@ +.. _user_manual/index: + +#################### +Nutzer Dokumentation +#################### + + +Der Kompass ist dein Kompass in der Jugendarbeit in deiner JDAV Sektion. Wenn du das +erste mal hier bist, schau doch mal :ref:`user_manual/getstarted` an. + +.. toctree:: + :titlesonly: + + getstarted + members + excursions + waitinglist + finance + + +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 + +Feedback +-------- + +Wenn Du Feedback hast, schreibe uns gerne eine E-Mail an: `digitales@jdav-hd.de `_. +Der Kompass lebt davon, dass er genau unsere Probleme löst und nicht nur ein weiteres Tool ist. + +Feedback könnte sein: + +- Fehler in der Software (bug) +- Verbesserungsvorschläge +- Wünsche für neue Funktionen +- etc. pp. diff --git a/docs/source/members.rst b/docs/source/user_manual/members.rst similarity index 94% rename from docs/source/members.rst rename to docs/source/user_manual/members.rst index 9eed0b5..5b72e86 100644 --- a/docs/source/members.rst +++ b/docs/source/user_manual/members.rst @@ -1,4 +1,4 @@ -.. _members: +.. _user_manual/members: Teilnehmer\*innenverwaltung =========================== @@ -15,12 +15,12 @@ In der Teilnehmer\*innenverwaltung siehst du in der Regel zwei Menüpunkte: - Teilnehmer\*innenverwaltung: Ausfahrten und *Alle Teilnehmer\*innen*. In diesem Abschnitt geht es nur um die Teilnehmer\*innen selbst. Wenn du etwas zum Punkt Ausfahrten -lernen möchtest, kannst du zu :ref:`excursions` springen. +lernen möchtest, kannst du zu :ref:`user_manual/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`. + Informationen dazu findest du unter :ref:`user_manual/waitinglist`. Falls du direkt zu einer von dir geleiteten Jugendgruppe gehen möchtest, findest du unter `Teilnehmer*innenverwaltung`_ oder auf der `Startseite`_ @@ -138,14 +138,14 @@ Der\*die ausgewählte Teilnehmer\*in erhält eine E-Mail mit einem Link. Dieser 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. + Das Geburtsdatumsformat ist ``TT.MM.JJJJ``, also wenn Peter am + 1.4.1999 geboren ist, muss 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 +Dann 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. +Klingt alles noch abstrakt? Dann fordere dich doch mal selbst zur Rückmeldung auf und probiere es aus. .. _emergency-contacts: @@ -159,7 +159,7 @@ 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. + dass sie 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 diff --git a/docs/source/waitinglist.rst b/docs/source/user_manual/waitinglist.rst similarity index 98% rename from docs/source/waitinglist.rst rename to docs/source/user_manual/waitinglist.rst index 09bfe7b..22f8a56 100644 --- a/docs/source/waitinglist.rst +++ b/docs/source/user_manual/waitinglist.rst @@ -1,4 +1,4 @@ -.. _waitinglist: +.. _user_manual/waitinglist: Warteliste und neue Mitglieder ============================== @@ -65,7 +65,7 @@ Neues Mitglied in euerer Gruppe Nach dem ihr ein neues Mitglied in eurer Gruppe habt seid ihr auch vorrangig für die Datenpflege zuständig. Bitte ruft die Detailansicht des\*der Teilnehmer\*in auf. Öffnet das Anmeldeformular und Übertragt die Infos in die zugehörigen Felder. Weiteres dazu findet ihr in der -:ref:`Teilnehmer\*innenverwaltung ` +:ref:`Teilnehmer\*innenverwaltung ` .. _group-registration-password: diff --git a/jdav_web/finance/locale/de/LC_MESSAGES/django.po b/jdav_web/finance/locale/de/LC_MESSAGES/django.po index cc5404f..6c85b0f 100644 --- a/jdav_web/finance/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/finance/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-01 16:23+0100\n" +"POT-Creation-Date: 2025-01-01 21:48+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,12 +18,12 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: finance/admin.py:76 +#: finance/admin.py #, python-format msgid "%(name)s is already submitted." msgstr "%(name)s ist bereits eingereicht." -#: finance/admin.py:82 +#: finance/admin.py #, 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 msgid "Submit statement" msgstr "Rechnung einreichen" -#: finance/admin.py:162 +#: finance/admin.py #, python-format msgid "%(name)s is not yet submitted." msgstr "%(name)s ist noch nicht eingereicht." -#: finance/admin.py:169 +#: finance/admin.py #, 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 #, 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 msgid "Statement confirmed" msgstr "Abrechnung abgewickelt" -#: finance/admin.py:186 +#: finance/admin.py 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 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 #, 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 #, python-format msgid "" "%(name)s already has transactions. Please delete them first, if you want to " @@ -90,12 +90,12 @@ msgstr "" "%(name)s hat bereits Überweisungen. Bitte lösche diese zunächst, bevor du " "neue generierst." -#: finance/admin.py:212 +#: finance/admin.py #, python-format msgid "Successfully generated transactions for %(name)s" msgstr "Automatisch Überweisungsträger für %(name)s generiert." -#: finance/admin.py:215 +#: finance/admin.py #, python-format msgid "" "Error while generating transactions for %(name)s. Do all bills have a payer?" @@ -103,214 +103,208 @@ 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 msgid "View submitted statement" msgstr "Eingereichte Abrechnung einsehen" -#: finance/admin.py:230 +#: finance/admin.py #, python-format msgid "Successfully reduced transactions for %(name)s." msgstr "Überweisungsträger für %(name)s minimiert." -#: finance/admin.py:274 +#: finance/admin.py #, python-format msgid "%(name)s is not yet confirmed." msgstr "%(name)s ist noch nicht bestätigt." -#: finance/admin.py:283 +#: finance/admin.py #, 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 +#: finance/admin.py finance/templates/admin/unconfirm_statement.html msgid "Unconfirm statement" msgstr "Bestätigung zurücknehmen" -#: finance/apps.py:8 +#: finance/apps.py msgid "Finance" msgstr "Finanzen" -#: finance/models.py:21 +#: finance/models.py msgid "Name" msgstr "Name" -#: finance/models.py:27 finance/models.py:472 finance/models.py:496 -#: finance/templates/admin/confirmed_statement.html:38 -#: finance/templates/admin/overview_submitted_statement.html:100 +#: finance/models.py finance/templates/admin/confirmed_statement.html +#: finance/templates/admin/overview_submitted_statement.html msgid "Ledger" msgstr "Geldtopf" -#: finance/models.py:28 +#: finance/models.py msgid "Ledgers" msgstr "Geldtöpfe" -#: finance/models.py:48 finance/models.py:415 finance/models.py:495 +#: finance/models.py msgid "Short description" msgstr "Kurzbeschreibung" -#: finance/models.py:51 finance/models.py:416 +#: finance/models.py msgid "Explanation" msgstr "Erklärung" -#: finance/models.py:53 +#: finance/models.py msgid "Associated excursion" msgstr "Zugehörige Ausfahrt" -#: finance/models.py:58 +#: finance/models.py msgid "Price per night" msgstr "Preis pro Nacht" -#: finance/models.py:60 +#: finance/models.py msgid "Submitted" msgstr "Eingericht" -#: finance/models.py:61 +#: finance/models.py msgid "Submitted on" msgstr "Eingereicht am" -#: finance/models.py:62 +#: finance/models.py msgid "Confirmed" msgstr "Abgewickelt" -#: finance/models.py:63 finance/models.py:479 +#: finance/models.py msgid "Paid on" msgstr "Bezahlt am" -#: finance/models.py:65 +#: finance/models.py msgid "Created by" msgstr "Erstellt von" -#: finance/models.py:70 +#: finance/models.py msgid "Submitted by" msgstr "Eingereicht von" -#: finance/models.py:75 finance/models.py:480 +#: finance/models.py msgid "Authorized by" msgstr "Autorisiert von" -#: finance/models.py:82 finance/models.py:414 finance/models.py:475 +#: finance/models.py msgid "Statement" msgstr "Abrechnung" -#: finance/models.py:83 +#: finance/models.py msgid "Statements" msgstr "Abrechnungen" -#: finance/models.py:98 +#: finance/models.py #, python-format msgid "Statement: %(excursion)s" msgstr "Abrechnung: %(excursion)s" -#: finance/models.py:150 +#: finance/models.py msgid "Ready to confirm" msgstr "Bereit zur Abwicklung" -#: finance/models.py:194 +#: finance/models.py #, python-format msgid "Compensation for %(excu)s" msgstr "Entschädigung für %(excu)s" -#: finance/models.py:327 -#: finance/templates/admin/overview_submitted_statement.html:78 +#: finance/models.py finance/templates/admin/overview_submitted_statement.html msgid "Total" msgstr "Gesamtbetrag" -#: finance/models.py:369 +#: finance/models.py msgid "Statement in preparation" msgstr "Abrechnung in Vorbereitung" -#: finance/models.py:370 +#: finance/models.py msgid "Statements in preparation" msgstr "Abrechnungen in Vorbereitung" -#: finance/models.py:389 +#: finance/models.py msgid "Submitted statement" msgstr "Eingereichte Abrechnung" -#: finance/models.py:390 +#: finance/models.py msgid "Submitted statements" msgstr "Eingereichte Abrechnungen" -#: finance/models.py:406 +#: finance/models.py msgid "Paid statement" msgstr "Bezahlte Abrechnung" -#: finance/models.py:407 +#: finance/models.py 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 +#: finance/models.py finance/templates/admin/confirmed_statement.html +#: finance/templates/admin/overview_submitted_statement.html msgid "Amount" msgstr "Betrag" -#: finance/models.py:419 +#: finance/models.py msgid "Paid by" msgstr "Bezahlt von" -#: finance/models.py:421 +#: finance/models.py msgid "Covered" msgstr "Übernommen" -#: finance/models.py:422 +#: finance/models.py msgid "Refunded" msgstr "Ausgezahlt" -#: finance/models.py:424 +#: finance/models.py msgid "Proof" msgstr "Beleg" -#: finance/models.py:435 finance/models.py:442 finance/models.py:455 +#: finance/models.py msgid "Bill" msgstr "Ausgabe" -#: finance/models.py:436 finance/models.py:443 finance/models.py:456 -#: finance/templates/admin/overview_submitted_statement.html:26 +#: finance/models.py finance/templates/admin/overview_submitted_statement.html msgid "Bills" msgstr "Ausgaben" -#: finance/models.py:468 finance/templates/admin/confirmed_statement.html:37 -#: finance/templates/admin/overview_submitted_statement.html:99 +#: finance/models.py finance/templates/admin/confirmed_statement.html +#: finance/templates/admin/overview_submitted_statement.html msgid "Reference" msgstr "Verwendungszweck" -#: finance/models.py:470 +#: finance/models.py msgid "Recipient" msgstr "Empfänger" -#: finance/models.py:478 +#: finance/models.py msgid "Paid" msgstr "Bezahlt" -#: finance/models.py:490 +#: finance/models.py msgid "Transaction" msgstr "Überweisung" -#: finance/models.py:491 -#: finance/templates/admin/overview_submitted_statement.html:84 +#: finance/models.py finance/templates/admin/overview_submitted_statement.html msgid "Transactions" 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 +#: finance/templates/admin/confirmed_statement.html +#: finance/templates/admin/overview_submitted_statement.html +#: finance/templates/admin/submit_statement.html +#: finance/templates/admin/unconfirm_statement.html msgid "Home" msgstr "Start" -#: finance/templates/admin/confirmed_statement.html:21 +#: finance/templates/admin/confirmed_statement.html msgid "Paiment" msgstr "Bezahlung" -#: finance/templates/admin/confirmed_statement.html:26 +#: finance/templates/admin/confirmed_statement.html msgid "Paying statement" msgstr "Rechnung bezahlen" -#: finance/templates/admin/confirmed_statement.html:29 +#: finance/templates/admin/confirmed_statement.html msgid "" "The statement is valid. Please execute the following transactions and then " "proceed by finalizing the confirmation." @@ -318,43 +312,59 @@ msgstr "" "Die Abrechnung ist gültig. Bitte führe die folgenden Überweisungen aus und " "fahre dann fort, indem du die Abwicklung bestätigst." -#: finance/templates/admin/confirmed_statement.html:35 +#: finance/templates/admin/confirmed_statement.html msgid "IBAN" msgstr "IBAN" -#: finance/templates/admin/confirmed_statement.html:66 +#: finance/templates/admin/confirmed_statement.html +msgid "QR Code" +msgstr "QR Code" + +#: finance/templates/admin/confirmed_statement.html +msgid "Show" +msgstr "Anzeigen" + +#: finance/templates/admin/confirmed_statement.html +msgid "No QR code can be displayed." +msgstr "Es kann kein QR-Code angezeigt werden." + +#: finance/templates/admin/confirmed_statement.html +msgid "Showing" +msgstr "Sichtbar" + +#: finance/templates/admin/confirmed_statement.html msgid "I did execute the listed transactions." msgstr "Ich habe die aufgeführten Überweisungen ausgeführt." -#: finance/templates/admin/confirmed_statement.html:68 +#: finance/templates/admin/confirmed_statement.html msgid "Confirm" msgstr "Bestätigen" -#: finance/templates/admin/overview_submitted_statement.html:21 +#: finance/templates/admin/overview_submitted_statement.html msgid "Overview" msgstr "Übersicht" -#: finance/templates/admin/overview_submitted_statement.html:32 +#: finance/templates/admin/overview_submitted_statement.html msgid "Covered by association" msgstr "Vom Verein übernommen" -#: finance/templates/admin/overview_submitted_statement.html:50 +#: finance/templates/admin/overview_submitted_statement.html #, python-format msgid "The total amount is %(total_bills)s €." msgstr "Der Gesamtbetrag beträgt %(total_bills)s €." -#: finance/templates/admin/overview_submitted_statement.html:54 +#: finance/templates/admin/overview_submitted_statement.html msgid "Excursion" msgstr "Ausfahrt" -#: finance/templates/admin/overview_submitted_statement.html:57 +#: finance/templates/admin/overview_submitted_statement.html #, 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 " "entfallen die folgenden Kosten:" -#: finance/templates/admin/overview_submitted_statement.html:62 +#: finance/templates/admin/overview_submitted_statement.html #, python-format msgid "" "%(nights)s nights for %(price_per_night)s€ per night making a total of " @@ -363,7 +373,7 @@ msgstr "" "%(nights)s Nächte zum Preis von %(price_per_night)s€ pro Nacht. Das ergibt " "eine Gesamtsumme von %(nights_per_yl)s€." -#: finance/templates/admin/overview_submitted_statement.html:65 +#: finance/templates/admin/overview_submitted_statement.html #, python-format msgid "" "%(duration)s days for %(allowance_per_day)s€ per day making a total of " @@ -372,7 +382,7 @@ msgstr "" "%(duration)s Tage für %(allowance_per_day)s€ pro Tag. Das ergibt eine " "Gesamtsumme von %(allowance_per_yl)s€." -#: finance/templates/admin/overview_submitted_statement.html:68 +#: finance/templates/admin/overview_submitted_statement.html #, python-format msgid "" "%(kilometers_traveled)s km by %(means_of_transport)s (%(euro_per_km)s € / " @@ -381,7 +391,7 @@ msgstr "" "%(kilometers_traveled)s km mit %(means_of_transport)s (%(euro_per_km)s€ / " "km). Das ergibt eine Gesamtsumme von %(transportation_per_yl)s€." -#: finance/templates/admin/overview_submitted_statement.html:73 +#: finance/templates/admin/overview_submitted_statement.html #, python-format msgid "" "In total this is %(total_per_yl)s€ times %(staff_count)s, giving " @@ -390,12 +400,12 @@ msgstr "" "Insgesamt sind das Kosten von %(total_per_yl)s€ mal %(staff_count)s, " "insgesamt also %(total_staff)s€." -#: finance/templates/admin/overview_submitted_statement.html:81 +#: finance/templates/admin/overview_submitted_statement.html #, python-format msgid "This results in a total amount of %(total)s€" msgstr "Das resultiert in einem Gesamtbetrag von %(total)s€" -#: finance/templates/admin/overview_submitted_statement.html:87 +#: finance/templates/admin/overview_submitted_statement.html msgid "" "Currently, no transactions are planned. You can auto generate them from the " "data, by clicking the following button." @@ -403,19 +413,19 @@ msgstr "" "Aktuell sind keine Überweisungen vorgesehen. Du kannst die erforderlichen " "durch Klicken auf den folgenden Knopf generieren." -#: finance/templates/admin/overview_submitted_statement.html:90 +#: finance/templates/admin/overview_submitted_statement.html msgid "Generate transactions" msgstr "Erzeuge Überweisungsträger" -#: finance/templates/admin/overview_submitted_statement.html:94 +#: finance/templates/admin/overview_submitted_statement.html msgid "Currently the following transactions are planned." msgstr "Aktuell sind die folgenden Überweisungen vorgesehen." -#: finance/templates/admin/overview_submitted_statement.html:137 +#: finance/templates/admin/overview_submitted_statement.html msgid "These transactions match the calculated costs." msgstr "Diese Überweisungen stimmen mit den berechneten Kosten überein." -#: finance/templates/admin/overview_submitted_statement.html:140 +#: finance/templates/admin/overview_submitted_statement.html msgid "" "The current transactions do not reflect all costs in this statement. Please " "fix the following issues:" @@ -423,42 +433,41 @@ msgstr "" "Die aktuell vorgesehenen Überweisungen stimmen nicht mit den berechneten " "Kosten überein. Bitte korrigiere die folgenden Fehler:" -#: finance/templates/admin/overview_submitted_statement.html:145 +#: finance/templates/admin/overview_submitted_statement.html msgid "Currently receiving" msgstr "Erhält aktuell" -#: finance/templates/admin/overview_submitted_statement.html:146 +#: finance/templates/admin/overview_submitted_statement.html msgid "Actual costs" msgstr "Tatsächliche Kosten" -#: finance/templates/admin/overview_submitted_statement.html:147 +#: finance/templates/admin/overview_submitted_statement.html msgid "Difference" msgstr "Differenz" -#: finance/templates/admin/overview_submitted_statement.html:174 +#: finance/templates/admin/overview_submitted_statement.html msgid "Accept" msgstr "Annehmen" -#: finance/templates/admin/overview_submitted_statement.html:175 +#: finance/templates/admin/overview_submitted_statement.html msgid "Reject" msgstr "Ablehnen" -#: finance/templates/admin/overview_submitted_statement.html:178 -#: finance/templates/admin/submit_statement.html:35 -#: finance/templates/admin/unconfirm_statement.html:39 +#: finance/templates/admin/overview_submitted_statement.html +#: finance/templates/admin/submit_statement.html +#: finance/templates/admin/unconfirm_statement.html msgid "Cancel" msgstr "Abbruch" -#: finance/templates/admin/submit_statement.html:21 -#: finance/templates/admin/submit_statement.html:34 +#: finance/templates/admin/submit_statement.html msgid "Submit" msgstr "Einreichen" -#: finance/templates/admin/submit_statement.html:26 +#: finance/templates/admin/submit_statement.html msgid "Submit to the finance department" msgstr "Beim Finanzreferat einreichen" -#: finance/templates/admin/submit_statement.html:28 +#: finance/templates/admin/submit_statement.html msgid "" "Do you want to submit the statement for further processing by the finance " "department? If you proceed, no further changes to the statement are possible." @@ -466,12 +475,11 @@ 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 +#: finance/templates/admin/unconfirm_statement.html msgid "Unconfirm" msgstr "Bestätigung zurücknehmen" -#: finance/templates/admin/unconfirm_statement.html:29 +#: finance/templates/admin/unconfirm_statement.html msgid "" "You are entering risk zone! Do you really want to manually set this " "statement back to unconfirmed?" @@ -479,7 +487,7 @@ 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 +#: finance/templates/admin/unconfirm_statement.html msgid "" "I am aware that this is not a standard procedure and this might cause data " "integrity issues." diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py index 04286e1..d27b74f 100644 --- a/jdav_web/finance/models.py +++ b/jdav_web/finance/models.py @@ -15,6 +15,9 @@ from contrib.models import CommonModel from contrib.rules import has_global_perm from utils import cvt_to_decimal, RestrictedFileField +from schwifty import IBAN +import re + # Create your models here. class Ledger(models.Model): @@ -495,6 +498,45 @@ class Transaction(models.Model): def __str__(self): return "T#{}".format(self.pk) + @staticmethod + def escape_reference(reference): + umlaut_map = { + 'ä': 'ae', 'ö': 'oe', 'ü': 'ue', + 'Ä': 'Ae', 'Ö': 'Oe', 'Ü': 'Ue', + 'ß': 'ss' + } + pattern = re.compile('|'.join(umlaut_map.keys())) + int_reference = pattern.sub(lambda x: umlaut_map[x.group()], reference) + allowed_chars = r"[^a-z0-9 /?: .,'+-]" + clean_reference = re.sub(allowed_chars, '', int_reference, flags=re.IGNORECASE) + return clean_reference + + def code(self): + + if self.amount == 0: + return "" + + iban = IBAN(self.member.iban, allow_invalid=True) + if not iban.is_valid: + return "" + bic = iban.bic + + reference = self.escape_reference(self.reference) + + # also escaping receiver as umlaute are also not allowed here + receiver = self.escape_reference(f"{self.member.prename} {self.member.lastname}") + return f"""BCD +001 +1 +SCT +{bic} +{receiver} +{iban} +EUR{self.amount} + + +{reference}""" + class Meta: verbose_name = _('Transaction') verbose_name_plural = _('Transactions') diff --git a/jdav_web/finance/templates/admin/confirmed_statement.html b/jdav_web/finance/templates/admin/confirmed_statement.html index aa2c079..3d35418 100644 --- a/jdav_web/finance/templates/admin/confirmed_statement.html +++ b/jdav_web/finance/templates/admin/confirmed_statement.html @@ -7,6 +7,8 @@ + + {% endblock %} {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} admin-view @@ -36,6 +38,7 @@ {% trans "Amount" %} {% trans "Reference" %} {% trans "Ledger" %} + {% trans "QR Code" %} {% for transaction in statement.transaction_set.all %} @@ -54,11 +57,53 @@ {{ transaction.ledger }} + + {% trans "Show" %} + {% endfor %}

+ + + + + + +
{% csrf_token %}

diff --git a/jdav_web/finance/tests.py b/jdav_web/finance/tests.py index 0014d8d..08b531a 100644 --- a/jdav_web/finance/tests.py +++ b/jdav_web/finance/tests.py @@ -3,7 +3,7 @@ from django.utils import timezone from django.conf import settings from .models import Statement, StatementUnSubmitted, StatementSubmitted, Bill, Ledger, Transaction from members.models import Member, Group, Freizeit, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE, NewMemberOnList,\ - FAHRGEMEINSCHAFT_ANREISE + FAHRGEMEINSCHAFT_ANREISE, MALE, FEMALE, DIVERSE # Create your tests here. class StatementTestCase(TestCase): @@ -11,11 +11,11 @@ class StatementTestCase(TestCase): kilometers_traveled = 512 participant_count = 10 staff_count = 5 - + def setUp(self): self.jl = Group.objects.create(name="Jugendleiter") self.fritz = Member.objects.create(prename="Fritz", lastname="Wulter", birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=MALE) self.fritz.group.add(self.jl) self.fritz.save() @@ -39,12 +39,12 @@ class StatementTestCase(TestCase): self.st3 = Statement.objects.create(night_cost=self.night_cost, excursion=ex) for i in range(self.participant_count): m = Member.objects.create(prename='Fritz {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=MALE) mol = NewMemberOnList.objects.create(member=m, memberlist=ex) ex.membersonlist.add(mol) for i in range(self.staff_count): m = Member.objects.create(prename='Fritz {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=MALE) Bill.objects.create(statement=self.st3, short_description='food', explanation='i was hungry', amount=42.69, costs_covered=True, paid_by=m) m.group.add(self.jl) @@ -57,7 +57,7 @@ class StatementTestCase(TestCase): self.st4 = Statement.objects.create(night_cost=self.night_cost, excursion=ex) for i in range(2): m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=DIVERSE) mol = NewMemberOnList.objects.create(member=m, memberlist=ex) ex.membersonlist.add(mol) @@ -66,7 +66,7 @@ class StatementTestCase(TestCase): 'Admissible staff count is not 0, although not enough participants.') for i in range(2): m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=DIVERSE) mol = NewMemberOnList.objects.create(member=m, memberlist=self.st4.excursion) self.st4.excursion.membersonlist.add(mol) self.assertEqual(self.st4.admissible_staff_count, 2, diff --git a/jdav_web/jdav_web/settings/components/texts.py b/jdav_web/jdav_web/settings/components/texts.py index d1e5eaf..9bf4021 100644 --- a/jdav_web/jdav_web/settings/components/texts.py +++ b/jdav_web/jdav_web/settings/components/texts.py @@ -26,7 +26,7 @@ GROUP_TIME_AVAILABLE_TEXT = """Die Gruppenstunde findet jeden {weekday} von {sta GROUP_TIME_UNAVAILABLE_TEXT = """Bitte erfrage die Gruppenzeiten bei der Gruppenleitung ({contact_email}).""" -INVITE_TEXT = """Hallo {name}, +INVITE_TEXT = """Hallo {{name}}, wir haben gute Neuigkeiten für dich. Es ist ein Platz in der Jugendgruppe {group_name} {group_link}freigeworden. {group_time} @@ -34,11 +34,9 @@ wir haben gute Neuigkeiten für dich. Es ist ein Platz in der Jugendgruppe {grou 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: +Informationen und eine schriftliche Anmeldebestätigung von dir. Das kannst du alles über folgenden Link erledigen: -{link} +{{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. @@ -46,7 +44,7 @@ Du siehst dort auch die Daten, die du bei deiner Eintragung auf die Warteliste a 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} +{{invitation_reject_link}} Bei Fragen, wende dich gerne an %(RESPONSIBLE_MAIL)s. diff --git a/jdav_web/locale/de/LC_MESSAGES/django.po b/jdav_web/locale/de/LC_MESSAGES/django.po index 14cfefa..c28a46f 100644 --- a/jdav_web/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-01 16:23+0100\n" +"POT-Creation-Date: 2025-01-01 21:48+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,103 +18,103 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: contrib/admin.py:59 +#: contrib/admin.py #, 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 +#: jdav_web/urls.py msgid "Startpage" msgstr "Startseite" -#: logindata/admin.py:25 +#: logindata/admin.py msgid "Permissions" msgstr "Berechtigungen" -#: logindata/admin.py:36 +#: logindata/admin.py msgid "Important dates" msgstr "Wichtigen Daten" -#: logindata/apps.py:8 +#: logindata/apps.py msgid "Authentication" msgstr "Authentifizierung" -#: logindata/models.py:10 +#: logindata/models.py msgid "Permission group" msgstr "Berechtigungsgruppe" -#: logindata/models.py:11 +#: logindata/models.py msgid "Permission groups" msgstr "Berechtigungsgruppen" -#: logindata/models.py:17 +#: logindata/models.py msgid "Login Datum" msgstr "Zugangsdaten" -#: logindata/models.py:18 +#: logindata/models.py msgid "Login Data" msgstr "Zugangsdaten" -#: logindata/models.py:25 +#: logindata/models.py msgid "Password" msgstr "Passwort" -#: logindata/models.py:31 +#: logindata/models.py msgid "Active registration password" msgstr "Aktives Registrierungspasswort" -#: logindata/models.py:32 +#: logindata/models.py msgid "Active registration passwords" msgstr "Aktive Registrierungspasswörter" -#: logindata/templates/logindata/register_failed.html:5 +#: logindata/templates/logindata/register_failed.html 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 +#: logindata/templates/logindata/register_failed.html +#: logindata/templates/logindata/register_form.html +#: logindata/templates/logindata/register_password.html +#: logindata/templates/logindata/register_success.html msgid "Set login data" msgstr "Zugangsdaten wählen" -#: logindata/templates/logindata/register_failed.html:12 +#: logindata/templates/logindata/register_failed.html 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 +#: logindata/templates/logindata/register_failed.html 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 +#: logindata/templates/logindata/register_failed.html msgid "contact us." msgstr "kontaktiere uns." -#: logindata/templates/logindata/register_form.html:6 -#: logindata/templates/logindata/register_password.html:6 +#: logindata/templates/logindata/register_form.html +#: logindata/templates/logindata/register_password.html msgid "Register" msgstr "Registrieren" -#: logindata/templates/logindata/register_form.html:15 -#: logindata/templates/logindata/register_password.html:13 +#: logindata/templates/logindata/register_form.html +#: logindata/templates/logindata/register_password.html msgid "Welcome, " msgstr "Willkommen, " -#: logindata/templates/logindata/register_form.html:16 +#: logindata/templates/logindata/register_form.html 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 +#: logindata/templates/logindata/register_form.html +#: logindata/templates/logindata/register_password.html msgid "submit" msgstr "Einreichen" -#: logindata/templates/logindata/register_password.html:13 +#: logindata/templates/logindata/register_password.html msgid "" "To set your personal login data for Kompass, please enter the password that " "you received." @@ -122,20 +122,20 @@ msgstr "" "Um deine persönlichen Zugangsdaten festzulegen, gib bitte das Passwort ein, " "das du erhalten hast." -#: logindata/templates/logindata/register_success.html:5 +#: logindata/templates/logindata/register_success.html msgid "Registration successful" msgstr "Zugangsdaten erfolgreich festgelegt" -#: logindata/templates/logindata/register_success.html:12 +#: logindata/templates/logindata/register_success.html 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 +#: logindata/views.py msgid "You entered a wrong password." msgstr "Das eingegebene Passwort ist falsch." -#: templates/admin/delete_confirmation.html:7 +#: templates/admin/delete_confirmation.html #, python-format msgid "" "Deleting the %(object_name)s '%(escaped_object)s' would result in deleting " @@ -146,7 +146,7 @@ msgstr "" "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 +#: templates/admin/delete_confirmation.html #, python-format msgid "" "Deleting the %(object_name)s '%(escaped_object)s' would require deleting the " @@ -155,7 +155,7 @@ 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 +#: templates/admin/delete_confirmation.html #, python-format msgid "" "Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"?" @@ -163,17 +163,17 @@ 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 +#: templates/admin/delete_confirmation.html +#: templates/admin/delete_selected_confirmation.html msgid "Yes, I’m sure" msgstr "Ja, ich bin sicher" -#: templates/admin/delete_confirmation.html:30 -#: templates/admin/delete_selected_confirmation.html:35 +#: templates/admin/delete_confirmation.html +#: templates/admin/delete_selected_confirmation.html msgid "No, take me back" msgstr "Nein, bitte abbrechen" -#: templates/admin/delete_selected_confirmation.html:6 +#: templates/admin/delete_selected_confirmation.html #, python-format msgid "" "Deleting the selected %(objects_name)s would result in deleting related " @@ -184,7 +184,7 @@ msgstr "" "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 +#: templates/admin/delete_selected_confirmation.html #, python-format msgid "" "Deleting the selected %(objects_name)s would require deleting the following " @@ -193,7 +193,7 @@ 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 +#: templates/admin/delete_selected_confirmation.html #, python-format msgid "" "Are you sure you want to delete the selected %(objects_name)s? All of the " @@ -202,99 +202,99 @@ 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 +#: templates/admin/delete_selected_confirmation.html msgid "Summary" msgstr "Zusammenfassung" -#: templates/admin/delete_selected_confirmation.html:18 +#: templates/admin/delete_selected_confirmation.html 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 +#: templates/admin/edit_inline/stacked.html +#: templates/admin/edit_inline/tabular.html +#: templates/nesting/admin/inlines/stacked.html 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 +#: templates/admin/edit_inline/stacked.html +#: templates/admin/edit_inline/tabular.html +#: templates/nesting/admin/inlines/stacked.html 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 +#: templates/admin/edit_inline/stacked.html +#: templates/admin/edit_inline/tabular.html +#: templates/nesting/admin/inlines/stacked.html msgid "View on site" msgstr "Auf der Website anzeigen" -#: templates/admin/edit_inline/tabular.html:33 +#: templates/admin/edit_inline/tabular.html msgid "Delete?" msgstr "Löschen?" -#: templates/admin/finance/statementconfirmed/change_form_object_tools.html:8 +#: templates/admin/finance/statementconfirmed/change_form_object_tools.html msgid "Unconfirm" msgstr "Bestätigung zurücknehmen" -#: templates/admin/finance/statementsubmitted/change_form_object_tools.html:21 +#: templates/admin/finance/statementsubmitted/change_form_object_tools.html msgid "Reduce transactions" msgstr "Überweisungen minimieren" -#: templates/admin/finance/statementsubmitted/change_form_object_tools.html:36 +#: templates/admin/finance/statementsubmitted/change_form_object_tools.html msgid "Overview" msgstr "Übersicht" -#: templates/admin/finance/statementunsubmitted/change_form_object_tools.html:9 +#: templates/admin/finance/statementunsubmitted/change_form_object_tools.html msgid "Submit" msgstr "Einreichen" -#: templates/admin/members/freizeit/change_form_object_tools.html:9 +#: templates/admin/members/freizeit/change_form_object_tools.html msgid "Generate crisis intervention list" msgstr "Kriseninterventionsliste erstellen" -#: templates/admin/members/freizeit/change_form_object_tools.html:16 +#: templates/admin/members/freizeit/change_form_object_tools.html msgid "Generate SJR application" msgstr "SJR Antrag erstellen" -#: templates/admin/members/freizeit/change_form_object_tools.html:23 +#: templates/admin/members/freizeit/change_form_object_tools.html msgid "Generate seminar report" msgstr "Landesjugendplan Antrag erstellen" -#: templates/admin/members/freizeit/change_form_object_tools.html:30 +#: templates/admin/members/freizeit/change_form_object_tools.html msgid "Generate overview" msgstr "Hinweise für Jugendleiter*innen erstellen" -#: templates/admin/members/freizeit/change_form_object_tools.html:38 +#: templates/admin/members/freizeit/change_form_object_tools.html msgid "Finance overview" msgstr "Kostenübersicht" -#: templates/admin/members/member/change_form_object_tools.html:8 +#: templates/admin/members/member/change_form_object_tools.html msgid "Invite as user" msgstr "Als Kompassbenutzer*in einladen" -#: templates/admin/members/memberunconfirmedproxy/change_form_object_tools.html:8 +#: templates/admin/members/memberunconfirmedproxy/change_form_object_tools.html msgid "Demote to waiter" msgstr "Zurück auf die Warteliste setzen" -#: templates/admin/members/memberwaitinglist/change_form_object_tools.html:8 -#: templates/admin/members/memberwaitinglist/submit_line.html:9 +#: templates/admin/members/memberwaitinglist/change_form_object_tools.html +#: templates/admin/members/memberwaitinglist/submit_line.html msgid "Invite to group" msgstr "Zu Gruppe einladen" -#: templates/nesting/admin/inlines/stacked.html:87 +#: templates/nesting/admin/inlines/stacked.html #, python-format msgid "Add another %(verbose_name)s" msgstr "Weiteren %(verbose_name)s hinzufügen" -#: utils.py:15 +#: utils.py 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 msgid "Filetype not supported." msgstr "Dateityp nicht unterstützt." -#: utils.py:45 +#: utils.py msgid "Please keep filesize under {}. Current filesize: {}" msgstr "Maximale Dateigröße {}. Aktuelle Dateigröße: {}." diff --git a/jdav_web/mailer/locale/de/LC_MESSAGES/django.po b/jdav_web/mailer/locale/de/LC_MESSAGES/django.po index 7a7e0ec..b97b0df 100644 --- a/jdav_web/mailer/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/mailer/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-02 22:50+0100\n" +"POT-Creation-Date: 2025-01-01 21:48+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,11 +18,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: mailer/admin.py:69 +#: mailer/admin.py msgid "Send message" msgstr "Nachricht verschicken" -#: mailer/admin.py:96 +#: mailer/admin.py msgid "" "Your account is not connected to a member. Please contact your system " "administrator." @@ -30,7 +30,7 @@ msgstr "" "Dein Account ist nicht mit eine*r Teilnehmer*in verknüpft. Bitte kontaktiere " "deine*n Systemadministrator*in." -#: mailer/admin.py:100 +#: mailer/admin.py msgid "" "Your email address is not an internal email address. Please change your " "email address and try again." @@ -38,58 +38,58 @@ 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 msgid "Failed to send message" msgstr "Fehler beim Senden der Email" -#: mailer/admin.py:106 +#: mailer/admin.py msgid "Failed to send some messages" msgstr "Fehler beim Senden der Emails" -#: mailer/admin.py:108 +#: mailer/admin.py msgid "Successfully sent message" msgstr "Email wurde erfolgreich verschickt" -#: mailer/apps.py:7 +#: mailer/apps.py msgid "mailer" msgstr "Verteiler" -#: mailer/management/commands/notify_active.py:36 +#: mailer/management/commands/notify_active.py #, python-format msgid "Congratulation %(name)s" msgstr "Herzlichen Glückwunsch %(name)s" -#: mailer/models.py:20 +#: mailer/models.py msgid "Only alphanumeric characters, ., - and _ are allowed" msgstr "Nur Buchstaben, Zahlen, ., . und _ sind erlaubt" -#: mailer/models.py:25 +#: mailer/models.py msgid "name" msgstr "Name" -#: mailer/models.py:27 +#: mailer/models.py msgid "Forward to participants" msgstr "Weiterleitung an Teilnehmer*innen" -#: mailer/models.py:30 +#: mailer/models.py msgid "Forward to group" msgstr "Weiterleitung an Gruppe" -#: mailer/models.py:32 +#: mailer/models.py msgid "Restrict to internal email addresses" msgstr "Weiterleitung nur von internen E-Mail Adressen erlaubt" -#: mailer/models.py:33 +#: mailer/models.py 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 +#: mailer/models.py msgid "Allowed sender" msgstr "Erlaubte Absender:innen" -#: mailer/models.py:37 +#: mailer/models.py msgid "" "Only forward e-mails of members of selected groups. Leave empty to allow all " "senders." @@ -97,222 +97,221 @@ 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 msgid "email address" msgstr "Email-Adresse" -#: mailer/models.py:56 +#: mailer/models.py msgid "email addresses" msgstr "Email-Adressen" -#: mailer/models.py:69 +#: mailer/models.py 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." -#: mailer/models.py:77 +#: mailer/models.py msgid "subject" msgstr "Betreff" -#: mailer/models.py:78 +#: mailer/models.py msgid "content" msgstr "Inhalt" -#: mailer/models.py:80 +#: mailer/models.py msgid "to group" msgstr "An Gruppe" -#: mailer/models.py:83 +#: mailer/models.py msgid "to freizeit" msgstr "An Ausfahrt" -#: mailer/models.py:88 +#: mailer/models.py msgid "to notes list" msgstr "An Notizliste" -#: mailer/models.py:93 +#: mailer/models.py msgid "to member" msgstr "An Teilnehmer*innen" -#: mailer/models.py:96 +#: mailer/models.py msgid "reply to participant" msgstr "Antwort an Teilnehmer*innen" -#: mailer/models.py:100 +#: mailer/models.py msgid "reply to custom email address" msgstr "Antwort an Email-Adresse" -#: mailer/models.py:103 +#: mailer/models.py msgid "sent" msgstr "Gesendet" -#: mailer/models.py:104 +#: mailer/models.py msgid "Created by" msgstr "Erstellt von" -#: mailer/models.py:122 +#: mailer/models.py msgid "Some other members" msgstr "Andere Teilnehmer*innen" -#: mailer/models.py:124 +#: mailer/models.py msgid "recipients" msgstr "Empfänger" -#: mailer/models.py:196 +#: mailer/models.py msgid "message" msgstr "Nachricht" -#: mailer/models.py:197 +#: mailer/models.py msgid "messages" msgstr "Nachrichten" -#: mailer/models.py:199 +#: mailer/models.py msgid "Can submit mails" msgstr "Kann Mails verschicken" -#: mailer/models.py:220 +#: mailer/models.py 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." -#: mailer/models.py:227 +#: mailer/models.py msgid "file" msgstr "Datei" -#: mailer/models.py:232 +#: mailer/models.py msgid "Empty" msgstr "Leer" -#: mailer/models.py:235 +#: mailer/models.py msgid "attachment" msgstr "Anhang" -#: mailer/models.py:236 +#: mailer/models.py msgid "attachments" msgstr "Anhänge" -#: mailer/templates/mailer/change_form.html:11 +#: mailer/templates/mailer/change_form.html msgid "Save and send mail" msgstr "Speichern und Email senden" -#: mailer/templates/mailer/confirm_send.html:7 +#: mailer/templates/mailer/confirm_send.html msgid "Do you really want to send these mails?" msgstr "Möchtest du diese Emails wirklich verschicken?" -#: mailer/templates/mailer/confirm_send.html:13 +#: mailer/templates/mailer/confirm_send.html msgid "already sent" msgstr "schon verschickt" -#: mailer/templates/mailer/confirm_send.html:19 +#: mailer/templates/mailer/confirm_send.html msgid "" "Some messages have already been sent! Do you really want to resend them?" msgstr "" "Einige Emails wurden schon versendet! Möchtest du diese wirklich nochmal " "senden?" -#: mailer/templates/mailer/confirm_send.html:30 +#: mailer/templates/mailer/confirm_send.html msgid "Send" msgstr "Senden" -#: mailer/templates/mailer/confirm_send.html:35 +#: mailer/templates/mailer/confirm_send.html msgid "Cancel" msgstr "Abbruch" -#: mailer/templates/mailer/confirmation_sent.html:4 -#: mailer/templates/mailer/unsubscribe.html:5 -#: mailer/templates/mailer/unsubscribe.html:25 +#: mailer/templates/mailer/confirmation_sent.html +#: mailer/templates/mailer/unsubscribe.html msgid "Unsubscribe" msgstr "Vom Newsletter abmelden" -#: mailer/templates/mailer/confirmation_sent.html:7 +#: mailer/templates/mailer/confirmation_sent.html msgid "Sent confirmation mail to" msgstr "Bestätigungsmail gesendet an" -#: mailer/templates/mailer/confirmation_sent.html:7 +#: mailer/templates/mailer/confirmation_sent.html msgid "Follow the link in your mail to confirm your unsubscription." msgstr "Folge dem Link in der Email, um die Abmeldung zu bestätigen." -#: mailer/templates/mailer/index.html:2 +#: mailer/templates/mailer/index.html msgid "This is the mailer app!" msgstr "Das ist die Mailer App!" -#: mailer/templates/mailer/send.html:2 +#: mailer/templates/mailer/send.html msgid "Here you can send new emails!" msgstr "Hier kannst du neue Emails verschicken!" -#: mailer/templates/mailer/send.html:11 +#: mailer/templates/mailer/send.html msgid "Subject:" msgstr "Betreff" -#: mailer/templates/mailer/send.html:14 +#: mailer/templates/mailer/send.html msgid "Content:" msgstr "Inhalt:" -#: mailer/templates/mailer/send.html:17 +#: mailer/templates/mailer/send.html msgid "Receiving group:" msgstr "Erhaltende Gruppe" -#: mailer/templates/mailer/send.html:24 +#: mailer/templates/mailer/send.html msgid "Send mail" msgstr "Email senden" -#: mailer/templates/mailer/subscribe.html:5 +#: mailer/templates/mailer/subscribe.html msgid "Here you can register yourself to the newsletter" msgstr "Hier kannst du dich für den Newsletter anmelden." -#: mailer/templates/mailer/subscribe.html:16 +#: mailer/templates/mailer/subscribe.html msgid "Prename" msgstr "Vorname" -#: mailer/templates/mailer/subscribe.html:21 +#: mailer/templates/mailer/subscribe.html msgid "Lastname" msgstr "Nachname" -#: mailer/templates/mailer/subscribe.html:26 mailer/views.py:60 +#: mailer/templates/mailer/subscribe.html mailer/views.py msgid "Birthdate" msgstr "Geburtsdatum" -#: mailer/templates/mailer/subscribe.html:37 -#: mailer/templates/mailer/unsubscribe.html:20 +#: mailer/templates/mailer/subscribe.html +#: mailer/templates/mailer/unsubscribe.html msgid "Email address" msgstr "Email-Adresse" -#: mailer/templates/mailer/subscribe.html:42 +#: mailer/templates/mailer/subscribe.html msgid "Register" msgstr "Registrieren" -#: mailer/templates/mailer/subscribed.html:3 +#: mailer/templates/mailer/subscribed.html msgid "Subscribed successfully" msgstr "Erfolgreich angemeldet" -#: mailer/templates/mailer/unsubscribe.html:9 +#: mailer/templates/mailer/unsubscribe.html msgid "Here you can unsubscribe from the newsletter" msgstr "Hier kannst du dich vom Newsletter abmelden" -#: mailer/templates/mailer/unsubscribed.html:8 +#: mailer/templates/mailer/unsubscribed.html msgid "Successfully unsubscribed from the newsletter for " msgstr "Newsletter erfolgreich abbestellt für " -#: mailer/views.py:36 +#: mailer/views.py msgid "Can't verify this link. Try again!" msgstr "Ungültiger Link. Bitte nochmal versuchen!" -#: mailer/views.py:48 +#: mailer/views.py msgid "Please fill in every field" msgstr "Bitte jedes Feld ausfüllen!" -#: mailer/views.py:50 +#: mailer/views.py msgid "Unsubscription confirmation" msgstr "Abmeldebestätigung" -#: mailer/views.py:83 +#: mailer/views.py msgid "Please fill in every field!" msgstr "Bitte jedes Feld ausfüllen!" -#: mailer/views.py:90 +#: mailer/views.py msgid "Member already exists" msgstr "Mitglied schon vorhanden" diff --git a/jdav_web/material/locale/de/LC_MESSAGES/django.po b/jdav_web/material/locale/de/LC_MESSAGES/django.po index 12557af..c438219 100644 --- a/jdav_web/material/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/material/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-01 16:23+0100\n" +"POT-Creation-Date: 2025-01-01 21:48+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,95 +18,95 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: material/admin.py:29 +#: material/admin.py msgid "Age" msgstr "Alter" -#: material/admin.py:34 +#: material/admin.py msgid "Not too old" msgstr "Nicht zu alt" -#: material/admin.py:35 +#: material/admin.py msgid "Too old" msgstr "Zu alt" -#: material/apps.py:7 +#: material/apps.py msgid "material" msgstr "Material" -#: material/models.py:16 +#: material/models.py msgid "Name" msgstr "Name" -#: material/models.py:22 material/models.py:39 +#: material/models.py msgid "Material category" msgstr "Materialtyp" -#: material/models.py:23 +#: material/models.py msgid "Material categories" msgstr "Materialtypen" -#: material/models.py:32 +#: material/models.py msgid "name" msgstr "Name" -#: material/models.py:33 +#: material/models.py msgid "description" msgstr "Beschreibung" -#: material/models.py:34 +#: material/models.py msgid "quantity" msgstr "Anzahl" -#: material/models.py:35 +#: material/models.py msgid "purchase date" msgstr "Kaufdatum" -#: material/models.py:36 +#: material/models.py msgid "lifetime (years)" msgstr "Lebenszeit (Jahre)" -#: material/models.py:37 +#: material/models.py msgid "photo" msgstr "Bild" -#: material/models.py:50 +#: material/models.py msgid "Quantity" msgstr "Anzahl" -#: material/models.py:57 +#: material/models.py msgid "Thumbnail" msgstr "Bild" -#: material/models.py:64 +#: material/models.py msgid "Owners" msgstr "Verantwortliche" -#: material/models.py:74 +#: material/models.py msgid "Not too old?" msgstr "Nicht zu alt?" -#: material/models.py:77 +#: material/models.py msgid "material part" msgstr "Materialteil" -#: material/models.py:78 +#: material/models.py msgid "material parts" msgstr "Materialteile" -#: material/models.py:84 +#: material/models.py msgid "owner" msgstr "Besitzer" -#: material/models.py:85 +#: material/models.py msgid "count" msgstr "Anzahl" -#: material/models.py:92 +#: material/models.py msgid "ownership" msgstr "Besitzer" -#: material/models.py:93 +#: material/models.py msgid "ownerships" msgstr "Verantwortliche" diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index 8694a02..dd80e3e 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -23,10 +23,10 @@ from django.contrib.contenttypes.admin import GenericTabularInline 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 + Sum, Case, Q, F, When, Value, IntegerField, Subquery, OuterRef, ExpressionWrapper from django.forms import Textarea, RadioSelect, TypedChoiceField, CheckboxInput from django.shortcuts import render -from django.core.exceptions import PermissionDenied +from django.core.exceptions import PermissionDenied, ValidationError from .pdf import render_tex, fill_pdf_form, merge_pdfs, serve_pdf from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin @@ -43,6 +43,8 @@ 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, RestrictedFileField +from schwifty import IBAN + #from easy_select2 import apply_select2 @@ -161,6 +163,20 @@ class RegistrationFilter(admin.SimpleListFilter): 'display': title } +class MemberAdminForm(forms.ModelForm): + + class Meta: + model = Member + fields = '__all__' + + # check iban validity using schwifty package + def clean_iban(self): + iban_str = self.cleaned_data.get('iban') + if len(iban_str) > 0: + iban = IBAN(iban_str, allow_invalid=True) + if not iban.is_valid: + raise ValidationError(_("The entered IBAN is not valid.")) + return iban_str # Register your models here. class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): @@ -223,6 +239,8 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): actions = ['request_echo', 'invite_as_user_action'] list_per_page = 25 + form = MemberAdminForm + sensitive_fields = ['iban', 'registration_form', 'comments'] field_view_permissions = { @@ -561,6 +579,12 @@ class WaiterInviteForm(forms.Form): label=_('Group')) +class WaiterInviteTextForm(forms.Form): + _selected_action = forms.CharField(widget=forms.MultipleHiddenInput) + text_template = forms.CharField(label=_('Invitation text'), + widget=forms.Textarea(attrs={'rows': 30, 'cols': 100})) + + class InvitationToGroupAdmin(admin.TabularInline): model = InvitationToGroup fields = ['group', 'date', 'status'] @@ -572,6 +596,20 @@ class InvitationToGroupAdmin(admin.TabularInline): return False +class AgeFilter(admin.SimpleListFilter): + title = _('Age') + parameter_name = 'age' + + def lookups(self, request, model_admin): + return [(n, str(n)) for n in range(101)] + + def queryset(self, request, queryset): + age = self.value() + if not age: + return queryset + return queryset.filter(birth_date_delta=age) + + class InvitedToGroupFilter(admin.SimpleListFilter): title = _('Pending group invitation for group') parameter_name = 'pending_group_invitation' @@ -593,14 +631,19 @@ class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin): list_display = ('name', 'birth_date', 'age', 'gender', 'application_date', 'latest_group_invitation', 'confirmed_mail', 'waiting_confirmed', 'sent_reminders') search_fields = ('prename', 'lastname', 'email') - list_filter = ['confirmed_mail', 'gender', InvitedToGroupFilter] - actions = ['ask_for_registration', 'ask_for_wait_confirmation'] + list_filter = ['confirmed_mail', InvitedToGroupFilter, AgeFilter, 'gender'] + actions = ['ask_for_registration_action', 'ask_for_wait_confirmation'] inlines = [InvitationToGroupAdmin] readonly_fields= ['application_date', 'sent_reminders'] def has_add_permission(self, request, obj=None): return False + def age(self, obj): + return obj.birth_date_delta + age.short_description=_('age') + age.admin_order_field = 'birth_date_delta' + def ask_for_wait_confirmation(self, request, queryset): """Asks the waiting person to confirm their waiting status.""" for waiter in queryset: @@ -609,38 +652,6 @@ class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin): _("Successfully asked %(name)s to confirm their waiting status.") % {'name': waiter.name}) ask_for_wait_confirmation.short_description = _('Ask selected waiters to confirm their waiting status') - def ask_for_registration(self, request, queryset): - """Asks the waiting person to register with all required data.""" - if "apply" in request.POST: - try: - group = Group.objects.get(pk=request.POST['group']) - except Group.DoesNotExist: - 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) - messages.success(request, - _("Successfully invited %(name)s to %(group)s.") % {'name': waiter.name, 'group': waiter.invited_for_group.name}) - - return HttpResponseRedirect(request.get_full_path()) - context = dict(self.admin_site.each_context(request), - title=_('Select group for invitation'), - opts=self.opts, - waiters=queryset.all(), - form=WaiterInviteForm(initial={'_selected_action': queryset.values_list('id', flat=True)})) - return render(request, - 'admin/invite_selected_for_group.html', - context=context) - ask_for_registration.short_description = _('Offer waiter a place in a group.') - def response_change(self, request, waiter): ret = super(MemberWaitingListAdmin, self).response_change(request, waiter) if "_invite" in request.POST: @@ -669,11 +680,30 @@ class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin): return custom_urls + urls def get_queryset(self, request): - queryset = super().get_queryset(request) + now = timezone.now() + age_expr = ExpressionWrapper( + Case( + When(birth_date__month__gte=now.month, birth_date__day__gt=now.day, then=now.year - F('birth_date__year') - 1), + default=now.year - F('birth_date__year'), + ), + output_field=IntegerField() + ) + queryset = super().get_queryset(request).annotate(birth_date_delta=age_expr) return queryset.prefetch_related('invitationtogroup_set') + def ask_for_registration_action(self, request, queryset): + return self.invite_view(request, queryset) + ask_for_registration_action.short_description = _('Offer waiter a place in a group.') + def invite_view(self, request, object_id): - waiter = MemberWaitingList.objects.get(pk=object_id) + if type(object_id) == str: + waiter = MemberWaitingList.objects.get(pk=object_id) + queryset = [waiter] + id_list = [waiter.pk] + else: + waiter = None + queryset = object_id + id_list = queryset.values_list('id', flat=True) if "apply" in request.POST: try: @@ -687,22 +717,49 @@ class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin): 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()) + context = dict(self.admin_site.each_context(request), + title=_('Select group for invitation'), + opts=self.opts, + group=group, + queryset=queryset, + form=WaiterInviteTextForm(initial={ + '_selected_action': id_list, + 'text_template': group.get_invitation_text_template() + })) + if waiter: + context = dict(context, object=waiter, waiter=waiter) + return render(request, + 'admin/invite_for_group_text.html', + context=context) + + if "send" in request.POST: + try: + group = Group.objects.get(pk=request.POST['group']) + text_template = request.POST['text_template'] + except (Group.DoesNotExist, KeyError): + messages.error(request, + _("An error occurred while trying to invite said members. Please try again.")) + return HttpResponseRedirect(request.get_full_path()) + for w in queryset: + w.invite_to_group(group, text_template=text_template) + messages.success(request, + _("Successfully invited %(name)s to %(group)s.") % {'name': w.name, 'group': w.invited_for_group.name}) - waiter.invited_for_group = group - waiter.save() - waiter.invite_to_group(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,))) + if waiter: + return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), + args=(object_id,))) + else: + return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) context = dict(self.admin_site.each_context(request), title=_('Select group for invitation'), opts=self.opts, - object=waiter, - waiter=waiter, - form=WaiterInviteForm(initial={'_selected_action': [waiter.pk]})) + queryset=queryset, + form=WaiterInviteForm(initial={ + '_selected_action': id_list + })) + if waiter: + context = dict(context, object=waiter, waiter=waiter) return render(request, 'admin/invite_for_group.html', context=context) diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po index 551b8c9..c987ec0 100644 --- a/jdav_web/members/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/members/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-31 18:13+0100\n" +"POT-Creation-Date: 2025-01-01 21:39+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,190 +18,200 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: members/admin.py:127 members/models.py:412 +#: members/admin.py members/models.py msgid "Registration complete" msgstr "Anmeldung vollständig" -#: members/admin.py:133 +#: members/admin.py msgid "True" msgstr "Ja" -#: members/admin.py:134 +#: members/admin.py msgid "False" msgstr "Nein" -#: members/admin.py:135 +#: members/admin.py msgid "All" msgstr "Alle" -#: members/admin.py:185 members/admin.py:414 +#: members/admin.py +msgid "The entered IBAN is not valid." +msgstr "Die eingegebene IBAN ist ungültig." + +#: members/admin.py msgid "Contact information" msgstr "Kontaktinformationen" -#: members/admin.py:190 members/admin.py:419 +#: members/admin.py msgid "Skills" msgstr "Fähigkeiten" -#: members/admin.py:195 members/admin.py:424 +#: members/admin.py msgid "Others" msgstr "Sonstiges" -#: members/admin.py:201 members/admin.py:429 +#: members/admin.py msgid "Organizational" msgstr "Organisatorisches" -#: members/admin.py:282 +#: members/admin.py msgid "Compose new mail to selected members" msgstr "Neue Nachricht an ausgewählte Teilnehmer*innen verfassen" -#: members/admin.py:288 +#: members/admin.py msgid "Echo required" msgstr "Rückmeldung erforderlich" -#: members/admin.py:290 +#: members/admin.py msgid "Successfully requested echo from selected members." msgstr "" "Rückmeldungsaufforderung erfolgreich an ausgewählte Teilnehmer*innen " "verschickt." -#: members/admin.py:291 +#: members/admin.py msgid "Request echo from selected members" msgstr "Rückmeldungsaufforderung an ausgewählte Teilnehmer*innen verschicken" -#: members/admin.py:300 +#: members/admin.py #, python-format msgid "%(name)s does not have a DAV360 email address or is already registered." msgstr "%(name)s hat keine DAV360 E-Mail Adresse oder ist bereits registriert." -#: members/admin.py:302 +#: members/admin.py #, python-format msgid "Successfully invited %(name)s as user." msgstr "Erfolgreich %(name)s aufgefordert Zugangsdaten zu wählen." -#: members/admin.py:304 +#: members/admin.py msgid "Successfully invited selected members to join as users." msgstr "" "Erfolgreich ausgewählte Teilnehmer*innen aufgefordert Zugangsdaten zu wählen." -#: members/admin.py:306 +#: members/admin.py msgid "Some members have been invited, others could not be invited." msgstr "" "Manche Teilnehmer*innen wurden eingeladen, andere konnten nicht eingeladen " "werden." -#: members/admin.py:313 members/admin.py:330 +#: members/admin.py msgid "Permission denied." msgstr "Fehlende Berechtigungen." -#: members/admin.py:320 members/admin.py:354 -#: members/templates/admin/invite_as_user.html:21 +#: members/admin.py members/templates/admin/invite_as_user.html msgid "Invite as user" msgstr "Kompass Zugangsdaten wählen lassen" -#: members/admin.py:325 +#: members/admin.py msgid "Invite selected members to join Kompass as users." msgstr "Ausgewählte Teilnehmer*innen Kompass Zugangsdaten wählen lassen." -#: members/admin.py:336 +#: members/admin.py msgid "Member not found." msgstr "Teilnehmer*in nicht gefunden." -#: members/admin.py:340 +#: members/admin.py #, python-format msgid "%(name)s already has login data." msgstr "%(name)s hat schon Zugangsdaten." -#: members/admin.py:345 +#: members/admin.py #, python-format msgid "The configured email address for %(name)s is not an internal one." msgstr "Die für %(name)s eingestellte E-Mail Adresse ist keine DAV360 Adresse." -#: members/admin.py:359 +#: members/admin.py #, python-format msgid "%(name)s already has a pending invitation as user." msgstr "" "%(name)s hat bereits eine ausstehende Aufforderung Zugangsdaten zu wählen." -#: members/admin.py:377 +#: members/admin.py msgid "activity" msgstr "Aktivität" -#: members/admin.py:387 members/models.py:56 members/models.py:1572 +#: members/admin.py members/models.py msgid "Name" msgstr "Name" -#: members/admin.py:478 +#: members/admin.py msgid "Successfully requested mail confirmation from selected registrations." msgstr "Aufforderung zur Bestätigung der Email Adresse versendet." -#: members/admin.py:479 +#: members/admin.py msgid "Request mail confirmation from selected registrations" msgstr "Aufforderung zur Bestätigung der Email Adresse versenden" -#: members/admin.py:486 members/admin.py:551 +#: members/admin.py #, python-format msgid "Successfully confirmed %(name)s." msgstr "Registrierung von %(name)s erfolgreich bestätigt." -#: members/admin.py:490 members/admin.py:554 +#: members/admin.py #, python-format msgid "Can't confirm. %(name)s has unconfirmed email addresses." msgstr "Bestätigung nicht möglich. %(name)s hat unbestätigte Emailadressen." -#: members/admin.py:495 +#: members/admin.py msgid "Successfully confirmed multiple registrations." msgstr "Erfolgreich mehrere Registrierungen bestätigt." -#: members/admin.py:497 +#: members/admin.py msgid "" "Failed to confirm some registrations because of unconfirmed email addresses." msgstr "" "Einige Bestätigungen fehlgeschlagen, weil Emailadressen noch nicht bestätigt " "sind." -#: members/admin.py:498 +#: members/admin.py msgid "Confirm selected registrations" msgstr "Ausgewählte Registrierungen bestätigen" -#: members/admin.py:521 +#: members/admin.py msgid "Demote selected registrations to waiters." msgstr "Ausgewählte Registrierungen zurück auf die Warteliste setzen." -#: members/admin.py:537 +#: members/admin.py msgid "Demote member to waiter" msgstr "Ausgewählte Registrierung zurück auf die Warteliste setzen." -#: members/admin.py:546 +#: members/admin.py #, python-format msgid "Successfully demoted %(name)s to waiter." msgstr "%(name)s zurück auf die Warteliste gesetzt." -#: members/admin.py:561 members/models.py:419 members/models.py:822 -#: members/models.py:1317 +#: members/admin.py members/models.py msgid "Group" msgstr "Gruppe" -#: members/admin.py:576 +#: members/admin.py +msgid "Invitation text" +msgstr "Einladungstext" + +#: members/admin.py msgid "Pending group invitation for group" msgstr "Ausstehende Gruppeneinladung für Gruppe" -#: members/admin.py:609 +#: members/admin.py #, python-format msgid "Successfully asked %(name)s to confirm their waiting status." msgstr "Erfolgreich %(name)s aufgefordert den Wartelistenplatz zu bestätigen." -#: members/admin.py:610 +#: members/admin.py msgid "Ask selected waiters to confirm their waiting status" msgstr "Wartende auffordern den Wartelistenplatz zu bestätigen" -#: members/admin.py:619 members/admin.py:683 +#: members/admin.py +msgid "Offer waiter a place in a group." +msgstr "Personen auf der Warteliste einen Gruppenplatz anbieten." + +#: members/admin.py msgid "" "An error occurred while trying to invite said members. Please try again." msgstr "" "Beim Einladen dieser Personen ist ein Fehler aufgetreten. Bitte versuche es " "nochmal. " -#: members/admin.py:623 members/admin.py:688 +#: members/admin.py msgid "" "The selected group does not have a contact email. Please first set a contact " "email and then try again." @@ -209,43 +219,39 @@ msgstr "" "Die ausgewählte Gruppe hat keine Kontakt E-Mail Adresse. Bitte stelle eine " "Kontakt E-Mail Adresse ein und versuche es erneut." -#: members/admin.py:631 members/admin.py:695 -#, python-format -msgid "Successfully invited %(name)s to %(group)s." -msgstr "Erfolgreich %(name)s zu Gruppe %(group)s eingeladen." - -#: members/admin.py:635 members/admin.py:701 +#: members/admin.py msgid "Select group for invitation" msgstr "Wähle Gruppe für Einladung aus" -#: members/admin.py:642 -msgid "Offer waiter a place in a group." -msgstr "Personen auf der Warteliste einen Gruppenplatz anbieten." +#: members/admin.py +#, python-format +msgid "Successfully invited %(name)s to %(group)s." +msgstr "Erfolgreich %(name)s zu Gruppe %(group)s eingeladen." -#: members/admin.py:718 members/models.py:72 +#: members/admin.py members/models.py msgid "name" msgstr "Name" -#: members/admin.py:719 +#: members/admin.py msgid "" "The group name may only consist of letters, numerals, _, -, :, * and spaces." msgstr "" "Der Gruppenname darf nur aus Buchstaben, Zahlen, _, -, :, * oder Leerzeichen " "bestehen." -#: members/admin.py:748 +#: members/admin.py msgid "Difficulty" msgstr "Schwierigkeit" -#: members/admin.py:751 +#: members/admin.py msgid "Tour type" msgstr "Art der Tour" -#: members/admin.py:754 members/models.py:1048 +#: members/admin.py members/models.py msgid "Means of transportation" msgstr "Verkehrsmittel" -#: members/admin.py:781 +#: members/admin.py msgid "" "Please list here all expenses in relation with this excursion and upload " "relevant bills. These have to be permanently stored for the application of " @@ -258,7 +264,7 @@ msgstr "" "einzelnen Posten wird dabei auf der LJP-Kostenübersicht angezeigt (sinnvoll " "wären z.B. Anreise, Verpflegung, Material etc.)." -#: members/admin.py:799 +#: members/admin.py msgid "" "Here you can work on a seminar report for applying for financial " "contributions from Landesjugendplan (LJP). More information on creating a " @@ -271,7 +277,7 @@ msgstr "" "wahlweise nur TN-Liste und Kostenübersicht kannst du anschließend " "herunterladen." -#: members/admin.py:807 +#: members/admin.py msgid "" "Please list all participants (also youth leaders) of this excursion. Here " "you can still make changes just before departure and hence generate the " @@ -282,33 +288,34 @@ msgstr "" "jederzeit die aktuelle Teilnehmer*innenliste für die Krisenintervention " "generieren." -#: members/admin.py:853 +#: members/admin.py #, python-format msgid "You are not allowed to view all members on note list %(name)s." msgstr "" "Du hast nicht die nötigen Rechte um alle Teilnehmer*innen der Notizliste " "%(name)s anzusehen." -#: members/admin.py:863 +#: members/admin.py msgid "Generate PDF summary" msgstr "Übersicht erstellen" -#: members/admin.py:867 +#: members/admin.py msgid "Full report" msgstr "Vollständiger Seminarbericht" -#: members/admin.py:868 +#: members/admin.py msgid "Costs and participants only" msgstr "Nur Kosten und Teilnehmende" -#: members/admin.py:869 +#: members/admin.py msgid "Mode" msgstr "Modus" -#: members/admin.py:870 +#: members/admin.py msgid "Prepend V32" msgstr "V32 Formblatt einfügen" +#: members/admin.py #: members/admin.py:880 msgid "Invoice" msgstr "Beleg" @@ -322,42 +329,41 @@ msgstr "" "teilweise relevant für die Zuschüsse aus dem Jugendetat (Verkehrsmittel, " "Fahrstrecke in km)." -#: members/admin.py:925 +#: members/admin.py #, python-format msgid "You are not allowed to view all members on excursion %(name)s." msgstr "" "Du hast nicht die nötigen Rechte um alle Teilnehmer*innen der Ausfahrt " "%(name)s anzusehen." -#: members/admin.py:933 +#: members/admin.py msgid "Generate crisis intervention list" msgstr "Kriseninterventionsliste erstellen" -#: members/admin.py:941 +#: members/admin.py msgid "Generate overview" msgstr "Hinweise für Jugendleiter erstellen" -#: members/admin.py:945 members/admin.py:977 -#: members/templates/admin/generate_seminar_report.html:21 +#: members/admin.py members/templates/admin/generate_seminar_report.html msgid "Generate seminar report" msgstr "Landesjugendplan Antrag erstellen" -#: members/admin.py:958 +#: members/admin.py msgid "Please select a mode." msgstr "Bitte wähle einen Modus aus." -#: members/admin.py:963 +#: members/admin.py msgid "" "Full mode is only available, if the seminar report section is filled out." msgstr "" "Der vollständiger Modus ist nur verfügbar, wenn der Seminarbericht " "ausgefüllt ist. " -#: members/admin.py:981 members/admin.py:1013 -#: members/templates/admin/generate_sjr_application.html:21 +#: members/admin.py msgid "Generate SJR application" msgstr "SJR Antrag erstellen" +#: members/admin.py #: members/admin.py:1002 msgid "Please select an invoice." msgstr "Bitte wähle einen Beleg aus." @@ -368,7 +374,7 @@ msgstr "" "Keine Abrechnung angelegt. Bitte lege eine Abrechnung and und versuche es " "erneut." -#: members/admin.py:1021 +#: members/admin.py msgid "" "Successfully submited statement. The finance department will notify you as " "soon as possible." @@ -376,253 +382,255 @@ msgstr "" "Abrechnung erfolgreich eingericht. Die Finanzabteilung wird sich bei dir so " "schnell wie möglich melden." -#: members/admin.py:1024 -#: members/templates/admin/freizeit_finance_overview.html:21 +#: members/admin.py members/templates/admin/freizeit_finance_overview.html msgid "Finance overview" msgstr "Kostenübersicht" -#: members/apps.py:7 +#: members/apps.py msgid "member administration" msgstr "Teilnehmer*innenverwaltung" -#: members/models.py:42 +#: members/models.py msgid "Monday" msgstr "Montag" -#: members/models.py:43 +#: members/models.py msgid "Tuesday" msgstr "Dienstag" -#: members/models.py:44 +#: members/models.py msgid "Wednesday" msgstr "Mittwoch" -#: members/models.py:45 +#: members/models.py msgid "Thursday" msgstr "Donnerstag" -#: members/models.py:46 +#: members/models.py msgid "Friday" msgstr "Freitag" -#: members/models.py:47 +#: members/models.py msgid "Saturday" msgstr "Samstag" -#: members/models.py:48 +#: members/models.py msgid "Sunday" msgstr "Sonntag" -#: members/models.py:57 members/models.py:1034 +#: members/models.py msgid "Description" msgstr "Beschreibung" -#: members/models.py:63 members/models.py:1026 -#: members/templates/members/change_member.html:18 +#: members/models.py members/templates/members/change_member.html msgid "Activity" msgstr "Aktivität" -#: members/models.py:64 +#: members/models.py msgid "Activities" msgstr "Aktivitäten" -#: members/models.py:73 +#: members/models.py msgid "description" msgstr "Beschreibung" -#: members/models.py:74 +#: members/models.py msgid "show on website" msgstr "Auf der Webseite anzeigen" -#: members/models.py:75 +#: members/models.py msgid "lowest year" msgstr "Ab Jahrgang" -#: members/models.py:76 +#: members/models.py msgid "highest year" msgstr "Bis Jahrgang" -#: members/models.py:77 +#: members/models.py msgid "youth leaders" msgstr "Jugendleiter" -#: members/models.py:79 +#: members/models.py msgid "week day" msgstr "Wochentag" -#: members/models.py:80 members/models.py:1399 +#: members/models.py msgid "Starting time" msgstr "Zeitpunkt" -#: members/models.py:81 +#: members/models.py msgid "Ending time" msgstr "Endzeitpunkt" -#: members/models.py:83 +#: members/models.py msgid "Contact email" msgstr "Kontakt Email" -#: members/models.py:93 members/models.py:276 +#: members/models.py msgid "group" msgstr "Gruppe" -#: members/models.py:94 +#: members/models.py msgid "groups" msgstr "Gruppen" -#: members/models.py:110 +#: members/models.py msgid "prename" msgstr "Vorname" -#: members/models.py:111 +#: members/models.py msgid "last name" msgstr "Nachname" -#: members/models.py:114 +#: members/models.py msgid "Email confirmed" msgstr "Emailadresse bestätigt" -#: members/models.py:132 members/models.py:203 members/models.py:250 +#: members/models.py msgid "phone number" msgstr "Telefonnummer (mobil)" -#: members/models.py:163 +#: members/models.py msgid "Email confirmation needed" msgstr "Email Bestätigung erforderlich" -#: members/models.py:213 +#: members/models.py msgid "birth date" msgstr "Geburtsdatum" -#: members/models.py:218 +#: members/models.py msgid "Gender" msgstr "Gender" -#: members/models.py:219 +#: members/models.py msgid "comments" msgstr "Kommentare" -#: members/models.py:247 +#: members/models.py +msgid "age" +msgstr "Alter" + +#: members/models.py msgid "Alternative email confirmed" msgstr "Alternative E-Mail Adresse bestätigt" -#: members/models.py:251 +#: members/models.py msgid "street and house number" msgstr "Straße und Hausnummer" -#: members/models.py:252 +#: members/models.py msgid "Postcode" msgstr "PLZ" -#: members/models.py:254 +#: members/models.py msgid "town" msgstr "Stadt" -#: members/models.py:255 +#: members/models.py msgid "Address extra" msgstr "Adress-Zusatz" -#: members/models.py:256 +#: members/models.py msgid "Country" msgstr "Land" -#: members/models.py:258 +#: members/models.py msgid "Good conduct certificate presented on" msgstr "Führungszeugnis vorgelegt am" -#: members/models.py:259 +#: members/models.py msgid "Joined on" msgstr "Eintritt" -#: members/models.py:260 +#: members/models.py msgid "Left on" msgstr "Austritt" -#: members/models.py:261 +#: members/models.py msgid "Has key" msgstr "Hat Jugendraumschlüssel" -#: members/models.py:262 +#: members/models.py msgid "Has a free ticket for the climbing gym" msgstr "Hat Freikarte für Kletterhalle" -#: members/models.py:263 +#: members/models.py msgid "DAV badge number" msgstr "DAV Mitgliedsnummer" -#: members/models.py:264 +#: members/models.py msgid "Knows how to swim" msgstr "Kann schwimmen" -#: members/models.py:265 +#: members/models.py msgid "Climbing badge" msgstr "Kletterschein" -#: members/models.py:266 +#: members/models.py msgid "Alpine experience" msgstr "Alpine Erfahrung" -#: members/models.py:267 +#: members/models.py msgid "Allergies" msgstr "Allergieen" -#: members/models.py:268 +#: members/models.py msgid "Medication" msgstr "Medikamente" -#: members/models.py:269 +#: members/models.py msgid "Tetanus vaccination" msgstr "Tetanusimpfung" -#: members/models.py:270 +#: members/models.py msgid "Photos may be taken" msgstr "Fotoerlaubnis" -#: members/models.py:271 +#: members/models.py msgid "Legal guardians" msgstr "Erziehungsberechtigte" -#: members/models.py:273 +#: members/models.py msgid "May cancel a group appointment independently" msgstr "Darf sich allein von der Gruppenstunde abmelden" -#: members/models.py:280 +#: members/models.py msgid "receives newsletter" msgstr "Erhält den Newsletter" -#: members/models.py:284 +#: members/models.py msgid "created" msgstr "erstellt" -#: members/models.py:285 +#: members/models.py msgid "Active" msgstr "Aktiv" -#: members/models.py:286 +#: members/models.py msgid "registration form" msgstr "Anmeldeformular" -#: members/models.py:295 +#: members/models.py msgid "image" msgstr "Bild" -#: members/models.py:304 +#: members/models.py msgid "Echoed" msgstr "Rückgemeldet" -#: members/models.py:305 +#: members/models.py msgid "Confirmed" msgstr "Bestätigt" -#: members/models.py:307 +#: members/models.py msgid "Login data" msgstr "Zugangsdaten" -#: members/models.py:309 +#: members/models.py msgid "waitinglist application date" msgstr "Wartelistenbewerbungsdatum" -#: members/models.py:311 +#: members/models.py msgid "" "If the person registered from the waitinglist, this is their application " "date." @@ -630,391 +638,386 @@ msgstr "" "Falls sich die Person über die Warteliste angemeldet hat ist dies ihr " "Bewerbungsdatum." -#: members/models.py:340 +#: members/models.py msgid "Good conduct certificate valid" msgstr "Führungszeugnis gültig" -#: members/models.py:422 +#: members/models.py msgid "member" msgstr "Teilnehmer*in" -#: members/models.py:423 +#: members/models.py msgid "members" msgstr "Teilnehmer*innen" -#: members/models.py:501 +#: members/models.py msgid "Upload registration form" msgstr "Anmeldeformular hochladen" -#: members/models.py:512 +#: members/models.py #, python-format msgid "New unconfirmed registration for group %(group)s" msgstr "Neue unbestätigte Registrierung für Gruppe %(group)s" -#: members/models.py:738 +#: members/models.py msgid "Set login data for Kompass" msgstr "Zugangsdaten für Kompass wählen" -#: members/models.py:773 members/models.py:982 members/models.py:993 -#: members/models.py:1348 members/models.py:1355 +#: members/models.py msgid "Member" msgstr "Teilnehmer*in" -#: members/models.py:780 +#: members/models.py msgid "Emergency contact" msgstr "Notfallkontakt" -#: members/models.py:781 +#: members/models.py msgid "Emergency contacts" msgstr "Notfallkontakte" -#: members/models.py:801 +#: members/models.py msgid "Unconfirmed registration" msgstr "Unbestätigte Registrierung" -#: members/models.py:802 +#: members/models.py msgid "Unconfirmed registrations" msgstr "Unbestätigte Registrierungen" -#: members/models.py:821 members/models.py:866 +#: members/models.py msgid "Waiter" msgstr "Wartende Person" -#: members/models.py:823 +#: members/models.py msgid "Invitation date" msgstr "Einladungsdatum" -#: members/models.py:824 members/templates/members/reject_success.html:6 -#: members/templates/members/reject_success.html:11 +#: members/models.py members/templates/members/reject_success.html msgid "Invitation rejected" msgstr "Einladung abgelehnt" -#: members/models.py:828 +#: members/models.py msgid "Invitation to group" msgstr "Gruppeneinladung" -#: members/models.py:829 +#: members/models.py msgid "Invitations to groups" msgstr "Gruppeneinladungen" -#: members/models.py:836 +#: members/models.py msgid "Rejected" msgstr "Abgelehnt" -#: members/models.py:838 +#: members/models.py msgid "Expired" msgstr "Abgelaufen" -#: members/models.py:840 +#: members/models.py msgid "Undecided" msgstr "Ausstehend" -#: members/models.py:841 +#: members/models.py msgid "Status" msgstr "Status" -#: members/models.py:852 +#: members/models.py msgid "Do you want to tell us something else?" msgstr "Möchtest du uns noch etwas mitteilen?" -#: members/models.py:853 +#: members/models.py msgid "application date" msgstr "Bewerbungsdatum" -#: members/models.py:855 +#: members/models.py msgid "Last wait confirmation" msgstr "Letzte Wartebestätigung" -#: members/models.py:859 +#: members/models.py msgid "Last reminder" msgstr "Letzte Erinnerung" -#: members/models.py:860 +#: members/models.py msgid "Missed reminders" msgstr "Verpasste Erinnerungen" -#: members/models.py:867 +#: members/models.py msgid "Waiters" msgstr "Warteliste" -#: members/models.py:882 +#: members/models.py msgid "Latest group invitation" msgstr "Letzte Gruppeneinladung" -#: members/models.py:899 +#: members/models.py msgid "Waiting status confirmed" msgstr "Wartelistenplatz bestätigt" -#: members/models.py:906 +#: members/models.py msgid "Waiting confirmation needed" msgstr "Wartelistenplatzbestätigung erforderlich" -#: members/models.py:961 +#: members/models.py msgid "Invitation to trial group meeting" msgstr "Einladung zu Schnupperstunde" -#: members/models.py:973 +#: members/models.py msgid "Unregistered from waiting list" msgstr "Von der Warteliste abgemeldet" -#: members/models.py:987 +#: members/models.py msgid "Comment" msgstr "Kommentar" -#: members/models.py:994 members/models.py:1356 +#: members/models.py msgid "Members" msgstr "Teilnehmer*innen" -#: members/models.py:1028 +#: members/models.py msgid "Place" msgstr "Stützpunkt / Ort" -#: members/models.py:1029 +#: members/models.py msgid "Destination (optional)" msgstr "ggf. Ziel" -#: members/models.py:1031 +#: members/models.py msgid "e.g. a peak" msgstr "z.B. ein Gipfel" -#: members/models.py:1032 +#: members/models.py msgid "Begin" msgstr "Anfang" -#: members/models.py:1033 +#: members/models.py msgid "End (optional)" msgstr "Ende" -#: members/models.py:1036 +#: members/models.py msgid "Groups" msgstr "Gruppen" -#: members/models.py:1049 +#: members/models.py msgid "Kilometers traveled" msgstr "Fahrstrecke in Kilometer" -#: members/models.py:1052 +#: members/models.py msgid "Categories" msgstr "Kategorien" -#: members/models.py:1053 +#: members/models.py msgid "easy" msgstr "leicht" -#: members/models.py:1053 +#: members/models.py msgid "medium" msgstr "mittel" -#: members/models.py:1053 +#: members/models.py msgid "hard" msgstr "schwer" -#: members/models.py:1063 members/models.py:1379 -#: members/templates/admin/freizeit_finance_overview.html:26 +#: members/models.py members/templates/admin/freizeit_finance_overview.html msgid "Excursion" msgstr "Ausfahrt" -#: members/models.py:1064 +#: members/models.py msgid "Excursions" msgstr "Ausfahrten" -#: members/models.py:1294 members/models.py:1370 members/models.py:1586 +#: members/models.py msgid "Title" msgstr "Titel" -#: members/models.py:1295 members/models.py:1313 members/models.py:1587 +#: members/models.py msgid "Date" msgstr "Datum" -#: members/models.py:1314 +#: members/models.py msgid "Location" msgstr "Ort" -#: members/models.py:1315 +#: members/models.py msgid "Topic" msgstr "Thema" -#: members/models.py:1339 +#: members/models.py msgid "Jugendleiter" msgstr "Jugendleiter" -#: members/models.py:1342 +#: members/models.py msgid "Klettertreff" msgstr "Klettertreff" -#: members/models.py:1343 +#: members/models.py msgid "Klettertreffs" msgstr "Klettertreffs" -#: members/models.py:1361 +#: members/models.py msgid "Password" msgstr "Passwort" -#: members/models.py:1364 +#: members/models.py msgid "registration password" msgstr "Registrierungspassort" -#: members/models.py:1365 +#: members/models.py msgid "registration passwords" msgstr "Registrierungspasswörter" -#: members/models.py:1372 +#: members/models.py msgid "Alpinistic goals" msgstr "Alpintechnische Ziele" -#: members/models.py:1373 +#: members/models.py msgid "Pedagogic goals" msgstr "Pädagogische Ziele" -#: members/models.py:1374 +#: members/models.py msgid "Content and methods" msgstr "Inhalte und Methoden" -#: members/models.py:1375 +#: members/models.py msgid "Evaluation" msgstr "Wertung" -#: members/models.py:1376 +#: members/models.py msgid "Experiences and possible improvements" msgstr "Erfahrungen und Verbesserungsvorschläge" -#: members/models.py:1385 members/models.py:1406 +#: members/models.py msgid "LJP Proposal" msgstr "Seminarbericht" -#: members/models.py:1386 +#: members/models.py msgid "LJP Proposals" msgstr "Seminarberichte" -#: members/models.py:1400 +#: members/models.py msgid "Duration in hours" msgstr "Dauer in Stunden" -#: members/models.py:1403 +#: members/models.py msgid "Activity and method" msgstr "Art der Aktion inkl. Methode" -#: members/models.py:1411 +#: members/models.py msgid "Intervention" msgstr "Aktion" -#: members/models.py:1412 +#: members/models.py msgid "Interventions" msgstr "Aktionen" -#: members/models.py:1514 members/models.py:1544 +#: members/models.py msgid "May list members" msgstr "Darf folgende Teilnehmer*innen listen" -#: members/models.py:1516 members/models.py:1546 +#: members/models.py msgid "May view members" msgstr "Darf folgende Teilnehmer*innen anzeigen" -#: members/models.py:1518 members/models.py:1548 +#: members/models.py msgid "May change members" msgstr "Darf folgende Teilnehmer*innen ändern" -#: members/models.py:1520 members/models.py:1550 +#: members/models.py msgid "May delete members" msgstr "Darf folgende Teilnehmer*innen löschen" -#: members/models.py:1524 members/models.py:1554 +#: members/models.py msgid "May list members of groups" msgstr "Darf Teilnehmer*innen folgender Gruppen listen" -#: members/models.py:1526 members/models.py:1556 +#: members/models.py msgid "May view members of groups" msgstr "Darf Teilnehmer*innen folgender Gruppen anzeigen" -#: members/models.py:1528 members/models.py:1558 +#: members/models.py msgid "May change members of groups" msgstr "Darf Teilnehmer*innen folgender Gruppen ändern" -#: members/models.py:1530 members/models.py:1560 +#: members/models.py msgid "May delete members of groups" msgstr "Darf Teilnehmer*innen folgender Gruppen löschen" -#: members/models.py:1533 members/models.py:1534 members/models.py:1537 +#: members/models.py msgid "Permissions" msgstr "Berechtigungen" -#: members/models.py:1563 members/models.py:1564 members/models.py:1567 +#: members/models.py msgid "Group permissions" msgstr "Gruppenberechtigungen" -#: members/models.py:1573 +#: members/models.py msgid "Permission needed" msgstr "Freigabe erforderlich" -#: members/models.py:1576 +#: members/models.py msgid "Training category" msgstr "Fortbildungstyp" -#: members/models.py:1577 +#: members/models.py msgid "Training categories" msgstr "Fortbildungstypen" -#: members/models.py:1588 +#: members/models.py msgid "Category" msgstr "Kategorien" -#: members/models.py:1589 +#: members/models.py msgid "Comments" msgstr "Kommentar" -#: members/models.py:1590 +#: members/models.py msgid "Participated" msgstr "Teilgenommmen" -#: members/models.py:1591 +#: members/models.py msgid "Passed" msgstr "Bestanden" -#: members/models.py:1594 +#: members/models.py msgid "Training" msgstr "Fortbildung" -#: members/models.py:1595 +#: members/models.py msgid "Trainings" msgstr "Fortbildungen" -#: members/templates/admin/demote_to_waiter.html:17 -#: members/templates/admin/freizeit_finance_overview.html:17 -#: members/templates/admin/generate_seminar_report.html:17 -#: members/templates/admin/generate_sjr_application.html:17 -#: members/templates/admin/invite_as_user.html:17 -#: members/templates/admin/invite_for_group.html:17 -#: members/templates/admin/invite_selected_as_user.html:17 -#: members/templates/admin/invite_selected_for_group.html:17 +#: members/templates/admin/demote_to_waiter.html +#: members/templates/admin/freizeit_finance_overview.html +#: members/templates/admin/generate_seminar_report.html +#: members/templates/admin/invite_as_user.html +#: members/templates/admin/invite_for_group.html +#: members/templates/admin/invite_for_group_text.html +#: members/templates/admin/invite_selected_as_user.html +#: members/templates/admin/invite_selected_for_group.html msgid "Home" msgstr "Start" -#: members/templates/admin/demote_to_waiter.html:20 -#: members/templates/admin/demote_to_waiter.html:25 +#: members/templates/admin/demote_to_waiter.html msgid "Demote to waiter" msgstr "Zurück auf die Warteliste setzen" -#: members/templates/admin/demote_to_waiter.html:27 +#: members/templates/admin/demote_to_waiter.html msgid "" "Do you want to demote the following unconfirmed registrations to waiters?" msgstr "Möchtest du die folgenden Personen zurück auf die Warteliste setzen?" -#: members/templates/admin/demote_to_waiter.html:45 +#: members/templates/admin/demote_to_waiter.html msgid "Demote" msgstr "Zurück auf die Warteliste setzen" -#: members/templates/admin/demote_to_waiter.html:46 -#: members/templates/admin/freizeit_finance_overview.html:154 -#: members/templates/admin/generate_seminar_report.html:60 -#: members/templates/admin/generate_sjr_application.html:48 -#: members/templates/admin/invite_as_user.html:37 -#: members/templates/admin/invite_for_group.html:52 -#: members/templates/admin/invite_selected_as_user.html:49 -#: members/templates/admin/invite_selected_for_group.html:53 +#: members/templates/admin/demote_to_waiter.html +#: members/templates/admin/freizeit_finance_overview.html +#: members/templates/admin/generate_seminar_report.html +#: members/templates/admin/invite_as_user.html +#: members/templates/admin/invite_for_group.html +#: members/templates/admin/invite_selected_as_user.html +#: members/templates/admin/invite_selected_for_group.html msgid "Cancel" msgstr "Abbrechen" -#: members/templates/admin/freizeit_finance_overview.html:29 +#: members/templates/admin/freizeit_finance_overview.html msgid "" "\n" "Here you see an estimate on the expected costs and contributions by the " @@ -1025,36 +1028,34 @@ msgstr "" "Hier siehst du eine Schätzung der erwarteten Kosten und Zuschüsse. Dies ist " "kein garantierter Kostenplan.\n" -#: members/templates/admin/freizeit_finance_overview.html:34 -#: members/templates/admin/freizeit_finance_overview.html:100 +#: members/templates/admin/freizeit_finance_overview.html msgid "Expenses" msgstr "Ausgaben" -#: members/templates/admin/freizeit_finance_overview.html:35 +#: members/templates/admin/freizeit_finance_overview.html msgid "You listed the following expenses:" msgstr "Du hast die folgenden Ausgaben angegeben:" -#: members/templates/admin/freizeit_finance_overview.html:39 +#: members/templates/admin/freizeit_finance_overview.html msgid "Explanation" msgstr "Erklärung" -#: members/templates/admin/freizeit_finance_overview.html:40 +#: members/templates/admin/freizeit_finance_overview.html msgid "Amount" msgstr "Betrag" -#: members/templates/admin/freizeit_finance_overview.html:58 +#: members/templates/admin/freizeit_finance_overview.html #, python-format msgid "The total expected expenses are %(total_bills_theoretic)s €." msgstr "" "Insgesamt belaufen sich die geschätzten Ausgaben auf " "%(total_bills_theoretic)s €." -#: members/templates/admin/freizeit_finance_overview.html:60 -#: members/templates/admin/freizeit_finance_overview.html:108 +#: members/templates/admin/freizeit_finance_overview.html msgid "Contributions by the association" msgstr "Sektionszuschüsse" -#: members/templates/admin/freizeit_finance_overview.html:63 +#: members/templates/admin/freizeit_finance_overview.html #, python-format msgid "" "According to the contribution guidelines,\n" @@ -1063,7 +1064,7 @@ msgstr "" "Gemäß den Zuschussrichtlinien erhalten %(staff_count)s Jugendleiter*innen " "Zuschüsse. Jeweils sind das" -#: members/templates/admin/freizeit_finance_overview.html:69 +#: members/templates/admin/freizeit_finance_overview.html #, python-format msgid "" "%(nights)s nights for %(price_per_night)s€ per night making a total of " @@ -1072,7 +1073,7 @@ msgstr "" "%(nights)s Nächte zum Preis von %(price_per_night)s€ pro Nacht. Das ergibt " "eine Gesamtsumme von %(nights_per_yl)s€." -#: members/templates/admin/freizeit_finance_overview.html:72 +#: members/templates/admin/freizeit_finance_overview.html #, python-format msgid "" "%(duration)s days for %(allowance_per_day)s€ per day making a total of " @@ -1081,7 +1082,7 @@ msgstr "" "%(duration)s Tage für %(allowance_per_day)s€ pro Tag. Das ergibt eine " "Gesamtsumme von %(allowance_per_yl)s€." -#: members/templates/admin/freizeit_finance_overview.html:75 +#: members/templates/admin/freizeit_finance_overview.html #, python-format msgid "" "%(kilometers_traveled)s km by %(means_of_transport)s (%(euro_per_km)s € / " @@ -1090,7 +1091,7 @@ msgstr "" "%(kilometers_traveled)s km mit %(means_of_transport)s (%(euro_per_km)s€ / " "km). Das ergibt eine Gesamtsumme von %(transportation_per_yl)s€." -#: members/templates/admin/freizeit_finance_overview.html:80 +#: members/templates/admin/freizeit_finance_overview.html #, python-format msgid "" "In total these are contributions of %(total_per_yl)s€ times %(staff_count)s, " @@ -1099,11 +1100,11 @@ msgstr "" "Insgesamt sind das Kosten von %(total_per_yl)s€ mal %(staff_count)s, " "insgesamt also %(total_staff)s€." -#: members/templates/admin/freizeit_finance_overview.html:83 +#: members/templates/admin/freizeit_finance_overview.html msgid "LJP contributions" msgstr "LJP Zuschüsse" -#: members/templates/admin/freizeit_finance_overview.html:86 +#: members/templates/admin/freizeit_finance_overview.html #, python-format msgid "" "By submitting a seminar report, you may apply for LJP contributions. In this " @@ -1118,23 +1119,23 @@ msgstr "" "Gesamtausgaben erhalten. Das resultiert in einem Gesamtzuschuss von " "%(ljp_contributions)s€." -#: members/templates/admin/freizeit_finance_overview.html:91 +#: members/templates/admin/freizeit_finance_overview.html msgid "Summary" msgstr "Zusammenfassung" -#: members/templates/admin/freizeit_finance_overview.html:94 +#: members/templates/admin/freizeit_finance_overview.html msgid "This is the estimated cost and contribution summary:" msgstr "Das ist die geschätzte Kosten- und Zuschussübersicht." -#: members/templates/admin/freizeit_finance_overview.html:116 +#: members/templates/admin/freizeit_finance_overview.html msgid "Potential LJP contributions" msgstr "Mögliche LJP Zuschüsse" -#: members/templates/admin/freizeit_finance_overview.html:124 +#: members/templates/admin/freizeit_finance_overview.html msgid "Remaining costs" msgstr "Verbleibende Kosten" -#: members/templates/admin/freizeit_finance_overview.html:133 +#: members/templates/admin/freizeit_finance_overview.html msgid "" "Positive remaining costs indicate that the estimated costs exceed the " "estimated contributions, while negative\n" @@ -1145,7 +1146,7 @@ msgstr "" "geschätzten Zuschüsse übersteigen, während negative Kosten\n" " bedeuten, dass die geschätzten Zuschüsse die geschätzten Kosten übersteigen." -#: members/templates/admin/freizeit_finance_overview.html:137 +#: members/templates/admin/freizeit_finance_overview.html msgid "" "Note that this cost calculation expects you to apply for LJP contributions. " "On the\n" @@ -1155,11 +1156,11 @@ msgstr "" "beantragst. Auf der Hauptseite dieser Ausfahrt kannst du dir eine Vorlage " "und alle Formblätter für einen solchen Antrag erstellen lassen." -#: members/templates/admin/freizeit_finance_overview.html:142 +#: members/templates/admin/freizeit_finance_overview.html msgid "Submit statement" msgstr "Abrechnung einreichen" -#: members/templates/admin/freizeit_finance_overview.html:144 +#: members/templates/admin/freizeit_finance_overview.html msgid "" "Did you already complete this excursion? If yes, please check if all listed " "expenses are correct\n" @@ -1172,15 +1173,15 @@ msgstr "" "Finanzreferat ein. Wenn du fortschreitest sind keine weiteren Änderungen an " "der Abrechnung mehr möglich." -#: members/templates/admin/freizeit_finance_overview.html:153 +#: members/templates/admin/freizeit_finance_overview.html msgid "Submit" msgstr "Einreichen" -#: members/templates/admin/freizeit_finance_overview.html:158 +#: members/templates/admin/freizeit_finance_overview.html msgid "Statement submitted" msgstr "Abrechnung eingereicht" -#: members/templates/admin/freizeit_finance_overview.html:160 +#: members/templates/admin/freizeit_finance_overview.html msgid "" "The statement for this excursion was already submitted. The finance " "department is currently processing your\n" @@ -1190,11 +1191,12 @@ msgstr "" "Finanzreferat bearbeitet deine Abrechnung zur Zeit und kommt " "schnellstmöglich auf dich zurück." -#: members/templates/admin/freizeit_finance_overview.html:163 +#: members/templates/admin/freizeit_finance_overview.html +#: members/templates/admin/invite_for_group_text.html msgid "Back" msgstr "Zurück" -#: members/templates/admin/generate_seminar_report.html:27 +#: members/templates/admin/generate_seminar_report.html msgid "" "Here you can generate a seminar report suitable for the LJP. A report\n" "always contains a head page with the basic information on the seminar." @@ -1206,7 +1208,7 @@ msgstr "" "Teilnehemendenliste, eine Kostenübersicht und eine detaillierte didaktische " "Planung enthalten. " -#: members/templates/admin/generate_seminar_report.html:31 +#: members/templates/admin/generate_seminar_report.html msgid "" "Expenses with same short description are automatically summed up and shown " "as one expense in the\n" @@ -1215,7 +1217,7 @@ msgstr "" "In der Kostenübersicht werden Ausgaben mit der gleichen Kurzbeschreibung " "automatisch aufsummiert und zu einer Ausgabe zusammengefasst." -#: members/templates/admin/generate_seminar_report.html:36 +#: members/templates/admin/generate_seminar_report.html msgid "" "Full report: Include learning goals and a detailed, tabularized time " "schedule. This requires\n" @@ -1225,7 +1227,7 @@ msgstr "" "tabellierten Zeitplan dar. Dies benötigt, dass der Seminarbericht in der " "Ausfahrt ausgefüllt ist." -#: members/templates/admin/generate_seminar_report.html:40 +#: members/templates/admin/generate_seminar_report.html msgid "" "Costs and participants only: Only show a list of participants and costs. In " "this case you\n" @@ -1235,7 +1237,7 @@ msgstr "" "Kosten an. In diesem Fall musst du Lernziele und einen Zeitplan manuell " "hinzufügen." -#: members/templates/admin/generate_seminar_report.html:46 +#: members/templates/admin/generate_seminar_report.html msgid "You may also choose to include the V32 attachment." msgstr "" "Ein LJP Antrag benötigt immer ein Formblatt (in unserem Fall V32-1 " @@ -1243,30 +1245,11 @@ msgstr "" "vorausfüllen lassen und dem Antrag hinzufügen. Bitte fülle die verbleibenden " "Felder im Formblatt selbst aus und unterschreibe das PDF." -#: members/templates/admin/generate_seminar_report.html:59 -#: members/templates/admin/generate_sjr_application.html:47 +#: members/templates/admin/generate_seminar_report.html msgid "Generate" msgstr "Erstellen" -#: members/templates/admin/generate_sjr_application.html:27 -msgid "Here you can generate an allowance application for the SJR." -msgstr "Hier kannst du einen Zuschussantrag für den SJR Heidelberg generieren." - -#: members/templates/admin/generate_sjr_application.html:30 -msgid "" -"The application needs to be complemented with an invoice from the trip as " -"proof." -msgstr "" -"Für den Zuschussantrag benötigst du eine Rechnung als Beleg, dass die " -"Ausfahrt stattgefunden hat. Bitte wähle hier einen Beleg aus." - -#: members/templates/admin/generate_sjr_application.html:41 -msgid "" -"Please send this application form to the jdav finance officer via email." -msgstr "Anschließend kannst du das Zuschussformular herunterladen. Sende es " -"bitte per E-Mail an den Finanzwart der jdav." - -#: members/templates/admin/invite_as_user.html:27 +#: members/templates/admin/invite_as_user.html #, python-format msgid "" "Do you want to invite %(member)s to set their login data for Kompass? They " @@ -1279,40 +1262,83 @@ msgstr "" "%(member)s wird eine E-Mail mit einem Link erhalten, um, nach Eingabe eines " "der aktiven Registrierungspasswörter, Benutzername und Passwort zu setzen." -#: members/templates/admin/invite_as_user.html:36 -#: members/templates/admin/invite_for_group.html:51 -#: members/templates/admin/invite_selected_as_user.html:48 -#: members/templates/admin/invite_selected_for_group.html:52 +#: members/templates/admin/invite_as_user.html +#: members/templates/admin/invite_for_group.html +#: members/templates/admin/invite_selected_as_user.html +#: members/templates/admin/invite_selected_for_group.html msgid "Invite" msgstr "Einladen" -#: members/templates/admin/invite_for_group.html:21 +#: members/templates/admin/invite_for_group.html +#: members/templates/admin/invite_for_group_text.html msgid "Invite to group" msgstr "Zu Gruppe einladen" -#: members/templates/admin/invite_for_group.html:26 -#: members/templates/admin/invite_selected_for_group.html:25 +#: members/templates/admin/invite_for_group.html +#: members/templates/admin/invite_for_group_text.html +#: members/templates/admin/invite_selected_for_group.html msgid "Invite to a group" msgstr "Zu einer Gruppe einladen" -#: members/templates/admin/invite_for_group.html:28 +#: members/templates/admin/invite_for_group.html msgid "You are inviting:" msgstr "Du lädst die folgende Person ein:" -#: members/templates/admin/invite_for_group.html:39 +#: members/templates/admin/invite_for_group.html +msgid "You are inviting the following waiters for registration:" +msgstr "Du lädst die folgenden Wartenden zur Schnupperstunde ein:" + +#: members/templates/admin/invite_for_group.html #, python-format msgid "Please choose the group that you want to invite %(waiter)s to." msgstr "Bitte wähle die Gruppe aus zu der du %(waiter)s einladen möchtest." -#: members/templates/admin/invite_selected_as_user.html:20 +#: members/templates/admin/invite_for_group.html +msgid "To which group do you want to invite these waiters?" +msgstr "Zu welcher Gruppe möchtest du diese Wartenden einladen?" + +#: members/templates/admin/invite_for_group_text.html +#, python-format +msgid "" +"You are inviting the following waiter for registration in group %(group)s." +msgstr "" +"Du lädst den*die folgende Wartende*n zu einer Schnupperstunde in der Gruppe " +"%(group)s ein:" + +#: members/templates/admin/invite_for_group_text.html +#, python-format +msgid "" +"You are inviting the following waiters for registration in group %(group)s." +msgstr "" +"Du lädst die folgenden Wartenden zu einer Schnupperstunde in der Gruppe " +"%(group)s ein:" + +#: members/templates/admin/invite_for_group_text.html +#, python-brace-format +msgid "" +"The following text will be sent as an invitation email. The patterns\n" +"{name}, {link} and {invitation_reject_link} will be automatically replaced " +"by personalized\n" +"data upon sending. Please adapt if needed and confirm." +msgstr "" +"Der folgende Text wird in der Einladungsmail verschickt. Die Platzhalter " +"{name}, {link} und {invitation_reject_link} werden beim Senden automatisch " +"durch personalisierte Daten ersetzt. Bitte passe den Text falls nötig an und " +"schicke die Einladung anschließend ab." + +#: members/templates/admin/invite_for_group_text.html +msgid "Send" +msgstr "Senden" + +#: members/templates/admin/invite_selected_as_user.html msgid "Invite multiple members as users" msgstr "Mehrere Teilnehmer*innen Zugangsdaten wählen lassen" -#: members/templates/admin/invite_selected_as_user.html:26 +#: members/templates/admin/invite_selected_as_user.html msgid "You selected the following members:" msgstr "Du hast die folgenden Teilnehmer*innen ausgewählt:" -#: members/templates/admin/invite_selected_as_user.html:38 +#: members/templates/admin/invite_selected_as_user.html msgid "" "Do you want to invite these members to set their login data for Kompass? " "They will\n" @@ -1325,77 +1351,75 @@ msgstr "" "eines der aktiven Registrierungspasswörter, Benutzername und Passwort zu " "setzen." -#: members/templates/admin/invite_selected_for_group.html:20 +#: members/templates/admin/invite_selected_for_group.html msgid "Invite multiple waiters" msgstr "Mehrere Wartende einladen" -#: members/templates/admin/invite_selected_for_group.html:27 +#: members/templates/admin/invite_selected_for_group.html msgid "You selected the following waiters:" msgstr "Du hast die folgenden Wartenden ausgewählt:" -#: members/templates/admin/invite_selected_for_group.html:40 +#: members/templates/admin/invite_selected_for_group.html msgid "Please choose the group you want these waiters to be invited for." msgstr "" "Bitte wähle die Gruppe aus zu der du die obigen Wartenden einladen möchtest." -#: members/templates/admin/klettertreff_overview.html:9 +#: members/templates/admin/klettertreff_overview.html msgid "date" msgstr "Datum" -#: members/templates/members/change_member.html:7 +#: members/templates/members/change_member.html msgid "Participations:" msgstr "Ausfahrtteilnahmen:" -#: members/templates/members/change_member.html:15 +#: members/templates/members/change_member.html msgid "Qualities:" msgstr "Fähigkeiten:" -#: members/templates/members/change_member.html:19 +#: members/templates/members/change_member.html msgid "Skill level" msgstr "Fähigkeitsniveau" -#: members/templates/members/change_member_unconfirmed.html:11 +#: members/templates/members/change_member_unconfirmed.html msgid "Save and confirm registration" msgstr "Speichern und Registrierung bestätigen" -#: members/templates/members/echo.html:6 members/templates/members/echo.html:13 -#: members/templates/members/echo_failed.html:10 -#: members/templates/members/echo_password.html:6 -#: members/templates/members/echo_password.html:11 -#: members/templates/members/echo_success.html:10 -#: members/templates/members/echo_wrong_password.html:6 -#: members/templates/members/echo_wrong_password.html:11 +#: members/templates/members/echo.html +#: members/templates/members/echo_failed.html +#: members/templates/members/echo_password.html +#: members/templates/members/echo_success.html +#: members/templates/members/echo_wrong_password.html msgid "Echo" msgstr "Rückmeldung" -#: members/templates/members/echo.html:15 +#: members/templates/members/echo.html msgid "Thanks for echoing back. Here is your current data:" msgstr "" "Vielen Dank, dass du dich rückmeldest. Hier siehst du deine aktuellen Daten. " "Falls sich etwas geändert hat, trage das bitte hier ein." -#: members/templates/members/echo_failed.html:5 +#: members/templates/members/echo_failed.html msgid "Echo failed" msgstr "Rückmeldung fehlgeschlagen" -#: members/templates/members/echo_failed.html:12 -#: members/templates/members/invited_registration_failed.html:12 +#: members/templates/members/echo_failed.html +#: members/templates/members/invited_registration_failed.html msgid "Something went wrong. The key you supplied is" msgstr "Etwas ist schief gegangen. Der verwendete Code ist" -#: members/templates/members/echo_failed.html:14 -#: members/templates/members/invited_registration_failed.html:14 -#: members/templates/members/register_failed.html:14 +#: members/templates/members/echo_failed.html +#: members/templates/members/invited_registration_failed.html +#: members/templates/members/register_failed.html msgid "If you think this is a mistake, please" msgstr "Wenn du denkst, dass das ein Fehler ist, " -#: members/templates/members/echo_failed.html:15 -#: members/templates/members/invited_registration_failed.html:14 -#: members/templates/members/register_failed.html:15 +#: members/templates/members/echo_failed.html +#: members/templates/members/invited_registration_failed.html +#: members/templates/members/register_failed.html msgid "contact us." msgstr "kontaktiere uns." -#: members/templates/members/echo_password.html:13 +#: members/templates/members/echo_password.html msgid "" "Thanks for echoing back. Please enter the password, which you can find in " "the email we sent you.\n" @@ -1403,63 +1427,61 @@ msgstr "" "Bitte gib dein Passwort ein. Weitere Informationen zur Rückmeldung findest " "du in der E-Mail.\n" -#: members/templates/members/echo_password.html:24 -#: members/templates/members/register_password.html:22 -#: members/templates/members/register_waiting_list.html:39 +#: members/templates/members/echo_password.html +#: members/templates/members/register_password.html +#: members/templates/members/register_waiting_list.html msgid "submit" msgstr "Bestätigen" -#: members/templates/members/echo_success.html:5 +#: members/templates/members/echo_success.html msgid "Echo successful" msgstr "Rückmeldung erfolgreich" -#: members/templates/members/echo_success.html:12 +#: members/templates/members/echo_success.html msgid "Thank you" msgstr "Danke" -#: members/templates/members/echo_success.html:12 +#: members/templates/members/echo_success.html msgid "Your data was successfully updated." msgstr "Deine Daten wurden erfolgreich aktualisiert." -#: members/templates/members/echo_wrong_password.html:13 +#: members/templates/members/echo_wrong_password.html msgid "You entered a wrong password to often." msgstr "" "Du hast zu oft ein falsches Passwort eingegeben. Bitte frage deinen " "Jugendleiter nach einem korrekten Passwort." -#: members/templates/members/invited_registration_failed.html:5 -#: members/templates/members/register_failed.html:5 +#: members/templates/members/invited_registration_failed.html +#: members/templates/members/register_failed.html msgid "Registration failed" msgstr "Registrierung fehlgeschlagen" -#: members/templates/members/invited_registration_failed.html:10 -#: members/templates/members/register.html:6 -#: members/templates/members/register_failed.html:10 -#: members/templates/members/register_password.html:6 -#: members/templates/members/register_success.html:6 -#: members/templates/members/register_wrong_password.html:6 -#: members/templates/members/upload_registration_form.html:6 -#: members/templates/members/upload_registration_form_invalid.html:6 -#: members/templates/members/upload_registration_form_success.html:6 +#: members/templates/members/invited_registration_failed.html +#: members/templates/members/register.html +#: members/templates/members/register_failed.html +#: members/templates/members/register_password.html +#: members/templates/members/register_success.html +#: members/templates/members/register_wrong_password.html +#: members/templates/members/upload_registration_form.html +#: members/templates/members/upload_registration_form_invalid.html +#: members/templates/members/upload_registration_form_success.html msgid "Registration" msgstr "Registrierung" -#: members/templates/members/mail_confirmation_invalid.html:6 -#: members/templates/members/mail_confirmation_invalid.html:11 +#: members/templates/members/mail_confirmation_invalid.html msgid "Mail confirmation failed" msgstr "Emailbestätigung fehlgeschlagen" -#: members/templates/members/mail_confirmation_invalid.html:13 -#: members/templates/members/waiting_confirmation_invalid.html:19 +#: members/templates/members/mail_confirmation_invalid.html +#: members/templates/members/waiting_confirmation_invalid.html msgid "The supplied link is invalid." msgstr "Der verwendete Link ist ungültig." -#: members/templates/members/mail_confirmation_success.html:6 -#: members/templates/members/mail_confirmation_success.html:11 +#: members/templates/members/mail_confirmation_success.html msgid "Mail confirmed" msgstr "Emailadresse bestätigt" -#: members/templates/members/mail_confirmation_success.html:14 +#: members/templates/members/mail_confirmation_success.html #, python-format msgid "" "The email address %(email)s was successfully confirmed as parents email of " @@ -1468,7 +1490,7 @@ msgstr "" "Die Emailadresse %(email)s wurde erfolgreich als Emailadresse der Eltern von " "%(name)s bestätigt." -#: members/templates/members/mail_confirmation_success.html:17 +#: members/templates/members/mail_confirmation_success.html #, python-format msgid "" "The email address %(email)s was successfully confirmed as personal email of " @@ -1477,28 +1499,27 @@ msgstr "" "Die Emailadresse %(email)s wurde erfolgreich als persönliche Emailadresse " "von %(name)s bestätigt." -#: members/templates/members/member_form.html:13 +#: members/templates/members/member_form.html msgid "Emergency contacts:" msgstr "Notfallkontakte:" -#: members/templates/members/member_form.html:20 -#: members/templates/members/member_form.html:43 +#: members/templates/members/member_form.html msgid "Remove" msgstr "Entfernen" -#: members/templates/members/member_form.html:24 +#: members/templates/members/member_form.html msgid "Add more" msgstr "Weiteren hinzufügen" -#: members/templates/members/member_form.html:28 +#: members/templates/members/member_form.html #, python-format msgid "I am already or will become a member of the DAV %(sektion)s soon." msgstr "" "Ich bin bereits Mitglied der DAV %(sektion)s, oder beantrage die " "Mitgliedschaft zeitnah." -#: members/templates/members/member_form.html:30 -#: members/templates/members/register_waiting_list.html:36 +#: members/templates/members/member_form.html +#: members/templates/members/register_waiting_list.html #, python-format msgid "" "I agree that my data is stored and processed on the server of the JDAV " @@ -1507,33 +1528,33 @@ msgstr "" "Ich bin einverstanden, dass meine Daten auf dem Server der JDAV %(sektion)s " "gespeichert und verarbeitet werden." -#: members/templates/members/member_form.html:37 +#: members/templates/members/member_form.html msgid "Save" msgstr "Speichern" -#: members/templates/members/member_form.html:103 +#: members/templates/members/member_form.html msgid "This file is bigger than the maximal allowed file size of 5 MiB." msgstr "Diese Datei ist größer als die maximal erlaubte Dateigröße von 5 MiB." -#: members/templates/members/register.html:13 -#: members/templates/members/register_password.html:11 -#: members/templates/members/register_success.html:11 -#: members/templates/members/register_wrong_password.html:11 -#: members/templates/members/upload_registration_form.html:11 -#: members/templates/members/upload_registration_form_invalid.html:11 -#: members/templates/members/upload_registration_form_success.html:11 +#: members/templates/members/register.html +#: members/templates/members/register_password.html +#: members/templates/members/register_success.html +#: members/templates/members/register_wrong_password.html +#: members/templates/members/upload_registration_form.html +#: members/templates/members/upload_registration_form_invalid.html +#: members/templates/members/upload_registration_form_success.html msgid "Register" msgstr "Registrieren" -#: members/templates/members/register.html:15 +#: members/templates/members/register.html msgid "Here you can register for group" msgstr "Hier kannst du dich registrieren für die Gruppe" -#: members/templates/members/register_failed.html:12 +#: members/templates/members/register_failed.html msgid "Something went wrong while processing your registration." msgstr "Etwas ist schief gelaufen, bei der Verarbeitung deiner Registrierung." -#: members/templates/members/register_password.html:13 +#: members/templates/members/register_password.html #, python-format msgid "" "Thanks for your interest in participating in the JDAV %(sektion)s. Please " @@ -1542,7 +1563,7 @@ msgstr "" "Danke für dein Interesse bei der JDAV %(sektion)s teilzunehmen. Bitte gib " "das Passwort ein, das du von deinem Jugendleiter erhalten hast." -#: members/templates/members/register_success.html:13 +#: members/templates/members/register_success.html msgid "" "Your registration succeeded. We summarized your registration in this " "registration\n" @@ -1556,11 +1577,11 @@ msgstr "" "allgemeinen Teilnahmebedingungen. Falls du zustimmst,\n" "unterschreibe das Formular und lade hier einen Scan oder ein Bild hoch." -#: members/templates/members/register_success.html:18 +#: members/templates/members/register_success.html msgid "Please remember to confirm your email address." msgstr "Bitte denk daran, deine E-Mail Adresse(n) zu bestätigen." -#: members/templates/members/register_success.html:19 +#: members/templates/members/register_success.html msgid "" "The coordinating team will process your registration when your email address " "is confirmed." @@ -1568,55 +1589,52 @@ msgstr "" "Unser Jugendleiterteam wird deine Registrierung bearbeiten, wenn deine " "Emailadressen bestätigt sind." -#: members/templates/members/register_success.html:21 +#: members/templates/members/register_success.html msgid "" "The coordinating team will process your registration as soon as possible." msgstr "" "Unser Jugendleiterteam wird deine Registrierung so schnell wie möglich " "bearbeiten." -#: members/templates/members/register_waiting_list.html:6 +#: members/templates/members/register_waiting_list.html msgid "Registration for waiting list" msgstr "Registrierung für die Warteliste" -#: members/templates/members/register_waiting_list_success.html:6 -#: members/templates/members/register_waiting_list_success.html:11 +#: members/templates/members/register_waiting_list_success.html msgid "Registration for waiting list." msgstr "Registrierung für die Warteliste." -#: members/templates/members/register_waiting_list_success.html:13 +#: members/templates/members/register_waiting_list_success.html msgid "Your registration for the waiting list was successful." msgstr "Du wurdest auf die Warteliste gesetzt." -#: members/templates/members/register_waiting_list_success.html:14 +#: members/templates/members/register_waiting_list_success.html msgid "Please remember to confirm all email addresses that you entered." msgstr "Bitte denk daran, deine E-Mail Adresse(n) zu bestätigen." -#: members/templates/members/register_waiting_list_success.html:15 +#: members/templates/members/register_waiting_list_success.html msgid "We will notify you if there is a vacant place in one of our groups." msgstr "" "Wir werden dich umgehend benachrichtigen, wenn es einen freien Platz in " "einer unserer Gruppen gibt." -#: members/templates/members/register_wrong_password.html:13 +#: members/templates/members/register_wrong_password.html msgid "" "You entered a wrong password to often. Please ask your youth leader again." msgstr "" "Du hast zu oft ein falsches Passwort eingegeben. Bitte frage deinen " "Jugendleiter nach einem korrekten Passwort." -#: members/templates/members/reject_invalid.html:6 -#: members/templates/members/reject_invalid.html:11 -#: members/templates/members/reject_invitation.html:6 -#: members/templates/members/reject_invitation.html:11 +#: members/templates/members/reject_invalid.html +#: members/templates/members/reject_invitation.html msgid "Reject invitation" msgstr "Einladung ablehnen" -#: members/templates/members/reject_invalid.html:13 +#: members/templates/members/reject_invalid.html msgid "This invitation is invalid or expired." msgstr "Diese Einladung ist ungültig oder abgelaufen." -#: members/templates/members/reject_invitation.html:13 +#: members/templates/members/reject_invitation.html #, python-format msgid "" "You were invited to a trial group meeting of the group %(groupname)s. On " @@ -1626,7 +1644,7 @@ msgstr "" "Du wurdest zu einer Schnupperstunde in der Gruppe %(groupname)s eingeladen. " "Auf dieser Seite kannst du die Einladung ablehnen." -#: members/templates/members/reject_invitation.html:17 +#: members/templates/members/reject_invitation.html msgid "" "You may either reject this specific invitation, because\n" "the time of the group does not fit your calendear, or unregister from the " @@ -1636,15 +1654,15 @@ msgstr "" "Termin keine Zeit hast, oder dich ganz von der Warteliste abmelden. Achtung: " "Das kann nicht rückgängig gemacht werden!" -#: members/templates/members/reject_invitation.html:26 +#: members/templates/members/reject_invitation.html msgid "Reject this invitation and stay on the waitinglist" msgstr "Diese Einladung ablehnen und auf der Warteliste bleiben" -#: members/templates/members/reject_invitation.html:27 +#: members/templates/members/reject_invitation.html msgid "Leave the waitinglist" msgstr "Warteliste verlassen" -#: members/templates/members/reject_success.html:15 +#: members/templates/members/reject_success.html msgid "" "You successfully unregistered from the waitinglist. If you want to rejoin " "the waitinglist\n" @@ -1654,7 +1672,7 @@ msgstr "" "einem späteren Zeitpunkt wieder auf die Warteliste setzen lassen möchtest " "kannst du das auf unserer Webseite machen.\n" -#: members/templates/members/reject_success.html:19 +#: members/templates/members/reject_success.html #, python-format msgid "" "You successfully rejected the invitation to a trial group meeting of the " @@ -1666,7 +1684,7 @@ msgstr "" "abgelehnt. Wenn ein Platz in einer anderen Gruppe frei wird, erhältst du " "eine neue Einladung.\n" -#: members/templates/members/upload_registration_form.html:14 +#: members/templates/members/upload_registration_form.html #, python-format msgid "" "We summarized your registration in our registration\n" @@ -1680,7 +1698,7 @@ msgstr "" "zustimmst, unterschreibe bitte das Formular und lade hier einen Scan oder " "ein Bild hoch." -#: members/templates/members/upload_registration_form.html:19 +#: members/templates/members/upload_registration_form.html msgid "" "If you are not an adult yet, please let someone responsible for you sign the " "agreement." @@ -1688,15 +1706,15 @@ msgstr "" "Bist du noch nicht volljährig? Dann lass bitte eine erziehungsberechtigte " "Person das Anmeldeformular unterschreiben." -#: members/templates/members/upload_registration_form.html:30 +#: members/templates/members/upload_registration_form.html msgid "Upload" msgstr "Hochladen" -#: members/templates/members/upload_registration_form_invalid.html:13 +#: members/templates/members/upload_registration_form_invalid.html msgid "The supplied key for uploading a registration form is invalid." msgstr "Der verwendete Link zum Hochladen eines Anmeldeformulars ist ungültig." -#: members/templates/members/upload_registration_form_success.html:13 +#: members/templates/members/upload_registration_form_success.html msgid "" "Thank you for uploading the registration form. Our team will process your " "registration shortly." @@ -1704,12 +1722,11 @@ msgstr "" "Danke für das Hochladen des Anmeldeformulars. Unser Jugendleiterteam wird " "deine Registrierung so schnell wie möglich bearbeiten." -#: members/templates/members/waiting_confirmation_invalid.html:6 -#: members/templates/members/waiting_confirmation_invalid.html:11 +#: members/templates/members/waiting_confirmation_invalid.html msgid "Waiting confirmation failed" msgstr "Wartelistenplatzbestätigung fehlgeschlagen" -#: members/templates/members/waiting_confirmation_invalid.html:15 +#: members/templates/members/waiting_confirmation_invalid.html msgid "" "Unfortunately, you did not confirm your intention to stay on the waiting " "list in time. You lost your spot on the list. You can" @@ -1717,16 +1734,15 @@ msgstr "" "Leider hast du deinen Wartelistenplatz nicht rechtzeitig bestätigt und hast " "somit deinen Platz verloren. Du kannst" -#: members/templates/members/waiting_confirmation_invalid.html:16 +#: members/templates/members/waiting_confirmation_invalid.html msgid "rejoin the waiting list" msgstr "der Warteliste erneut beitreten" -#: members/templates/members/waiting_confirmation_success.html:6 -#: members/templates/members/waiting_confirmation_success.html:11 +#: members/templates/members/waiting_confirmation_success.html msgid "Waiting confirmed" msgstr "Wartelistenplatz bestätigt" -#: members/templates/members/waiting_confirmation_success.html:14 +#: members/templates/members/waiting_confirmation_success.html #, python-format msgid "" "Thank you %(prename)s for your interest in staying on the waiting list.\n" @@ -1735,7 +1751,7 @@ msgstr "" "Danke %(prename)s für dein Interesse auf der Warteliste zu bleiben.\n" "Dein Platz ist bereits bestätigt." -#: members/templates/members/waiting_confirmation_success.html:18 +#: members/templates/members/waiting_confirmation_success.html #, python-format msgid "" "Thank you %(prename)s for your interest in staying on the waiting list.\n" @@ -1744,41 +1760,40 @@ msgstr "" "Danke %(prename)s für dein Interesse auf der Warteliste zu bleiben.\n" "Dein Platz wurde bestätigt." -#: members/views.py:37 members/views.py:76 +#: members/views.py msgid "Prename of the member." msgstr "Vorname des Teilnehmenden" -#: members/views.py:38 members/views.py:77 +#: members/views.py msgid "Lastname of the member." msgstr "Nachname des Teilnehmenden" -#: members/views.py:39 +#: members/views.py msgid "phone number of child or parent" msgstr "Telefonnummer des Teilnehmenden oder einer Kontaktperson" -#: members/views.py:40 +#: members/views.py msgid "email of child if available, otherwise parental email address" msgstr "" "Falls verfügbar, E-Mailadresse des Teilnehmenden, sonst einer Kontaktperson" -#: members/views.py:41 +#: members/views.py msgid "optional additional email address" msgstr "Optionale zusätzliche E-Mailadresse" -#: members/views.py:114 members/views.py:203 +#: members/views.py msgid "The entered password is wrong." msgstr "Das eingegebene Passwort ist falsch." -#: members/views.py:149 members/views.py:155 members/views.py:163 -#: members/views.py:423 +#: members/views.py msgid "invalid" msgstr "ungültig" -#: members/views.py:166 members/views.py:425 +#: members/views.py msgid "expired" msgstr "abgelaufen" -#: members/views.py:175 +#: members/views.py msgid "Invalid emergency contacts" msgstr "Ungültige Notfallkontakte" diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index 4de99ee..4bef718 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -97,6 +97,24 @@ class Group(models.Model): # return if the group has all relevant time slot information filled return self.weekday and self.start_time and self.end_time + def get_invitation_text_template(self): + """The text template used to invite waiters to this group. This contains + placeholders for the name of the waiter and personalized links.""" + if self.show_website: + group_link = '({url}) '.format(url=prepend_base_url(reverse('startpage:gruppe_detail', args=[self.name]))) + else: + group_link = '' + if self.has_time_info(): + group_time = settings.GROUP_TIME_AVAILABLE_TEXT.format(weekday=WEEKDAYS[self.weekday][1], + start_time=self.start_time.strftime('%H:%M'), + end_time=self.end_time.strftime('%H:%M')) + else: + group_time = settings.GROUP_TIME_UNAVAILABLE_TEXT.format(contact_email=self.contact_email) + return settings.INVITE_TEXT.format(group_time=group_time, + group_name=self.name, + group_link=group_link, + contact_email=self.contact_email) + class MemberManager(models.Manager): def get_queryset(self): @@ -221,10 +239,11 @@ class Person(Contact): class Meta(CommonModel.Meta): abstract = True - @property def age(self): """Age of member""" return relativedelta(datetime.today(), self.birth_date).years + age.admin_order_field = 'birth_date' + age.short_description = _('age') @property def birth_date_str(self): @@ -945,27 +964,21 @@ class MemberWaitingList(Person): except InvitationToGroup.DoesNotExist: return False - def invite_to_group(self, group): - if group.show_website: - group_link = '({url}) '.format(url=prepend_base_url(reverse('startpage:gruppe_detail', args=[group.name]))) - else: - group_link = '' - if group.has_time_info(): - group_time = settings.GROUP_TIME_AVAILABLE_TEXT.format(weekday=WEEKDAYS[group.weekday][1], - start_time=group.start_time.strftime('%H:%M'), - end_time=group.end_time.strftime('%H:%M')) - else: - group_time = settings.GROUP_TIME_UNAVAILABLE_TEXT.format(contact_email=group.contact_email) + def invite_to_group(self, group, text_template=None): + """ + Invite waiter to given group. Stores a new group invitation + and sends a personalized e-mail based on the passed template. + """ + self.invited_for_group = group + self.save() + if not text_template: + text_template = group.get_invitation_text_template() invitation = InvitationToGroup(group=group, waiter=self) invitation.save() self.send_mail(_("Invitation to trial group meeting"), - settings.INVITE_TEXT.format(name=self.prename, - group_time=group_time, - group_name=group.name, - group_link=group_link, - contact_email=group.contact_email, - link=get_registration_link(invitation.key), - invitation_reject_link=get_invitation_reject_link(invitation.key)), + text_template.format(name=self.prename, + link=get_registration_link(invitation.key), + invitation_reject_link=get_invitation_reject_link(invitation.key)), cc=group.contact_email.email) def unregister(self): @@ -1191,9 +1204,9 @@ class Freizeit(CommonModel): members = set(map(lambda x: x.member, self.membersonlist.distinct())) total = len(members) total_b27_local = len([m for m in members - if m.age <= 27 and settings.SEKTION in m.town]) + if m.age() <= 27 and settings.SEKTION in m.town]) total_b27_non_local = len([m for m in members - if m.age <= 27 and not settings.SEKTION in m.town]) + if m.age() <= 27 and not settings.SEKTION in m.town]) jls = self.jugendleiter.distinct() title = self.ljpproposal.title if hasattr(self, 'ljpproposal') else self.name base = {'Haushaltsjahr': str(datetime.now().year), @@ -1218,7 +1231,7 @@ class Freizeit(CommonModel): suffix = '12' base['Vor- und Nachname' + suffix] = m.name base['Anschrift' + suffix] = m.address - base['Alter' + suffix] = str(m.age) + base['Alter' + suffix] = str(m.age()) base['Status' + suffix] = str(2) return base diff --git a/jdav_web/members/templates/admin/invite_for_group.html b/jdav_web/members/templates/admin/invite_for_group.html index dfde466..87410ff 100644 --- a/jdav_web/members/templates/admin/invite_for_group.html +++ b/jdav_web/members/templates/admin/invite_for_group.html @@ -17,7 +17,9 @@ {% translate 'Home' %}{{ opts.app_config.verbose_name }}{{ opts.verbose_name_plural|capfirst }} +{% if object %} › {{ object|truncatewords:"18" }} +{% endif %} › {% translate 'Invite to group' %} {% endblock %} @@ -25,18 +27,28 @@ {% block content %}

{% translate "Invite to a group" %}

+{% if waiter %} {% trans "You are inviting:" %} +{% else %} +{% trans "You are inviting the following waiters for registration:" %} +{% endif %}

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

+{% if waiter %} {% blocktrans %}Please choose the group that you want to invite {{ waiter }} to.{% endblocktrans %} +{% else %} +{% blocktrans %}To which group do you want to invite these waiters?{% endblocktrans %} +{% endif %}

@@ -47,7 +59,7 @@

- + {% translate "Cancel" %}

diff --git a/jdav_web/members/templates/admin/invite_for_group_text.html b/jdav_web/members/templates/admin/invite_for_group_text.html new file mode 100644 index 0000000..fd71733 --- /dev/null +++ b/jdav_web/members/templates/admin/invite_for_group_text.html @@ -0,0 +1,68 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls static %} + +{% block extrahead %} + {{ block.super }} + {{ media }} + + + +{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} invite-waiter +{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +

{% translate "Invite to a group" %}

+

+{% if waiter %} +{% blocktrans %}You are inviting the following waiter for registration in group {{ group }}.{% endblocktrans %} +{% else %} +{% blocktrans %}You are inviting the following waiters for registration in group {{ group }}.{% endblocktrans %} +{% endif %} +

+

+

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

+ +

+{% blocktrans %}The following text will be sent as an invitation email. The patterns +{name}, {link} and {invitation_reject_link} will be automatically replaced by personalized +data upon sending. Please adapt if needed and confirm.{% endblocktrans %} +

+ + + {% csrf_token %} +

+ {{form}} +

+
+
+

+ + + + {% translate "Back" %} +

+
+ + +{% endblock %} diff --git a/jdav_web/members/tests.py b/jdav_web/members/tests.py index 673d658..440446f 100644 --- a/jdav_web/members/tests.py +++ b/jdav_web/members/tests.py @@ -6,8 +6,10 @@ from django.test import TestCase, Client, RequestFactory from django.utils import timezone, translation from django.conf import settings from django.urls import reverse +from unittest import skip from .models import Member, Group, PermissionMember, PermissionGroup, Freizeit, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE,\ - MemberNoteList, NewMemberOnList, confirm_mail_by_key, EmergencyContact + MemberNoteList, NewMemberOnList, confirm_mail_by_key, EmergencyContact,\ + DIVERSE, MALE, FEMALE from django.db import connection from django.db.migrations.executor import MigrationExecutor @@ -18,7 +20,7 @@ def create_custom_user(username, groups, prename, lastname): user = User.objects.create_user( username=username, password='secret' ) - member = Member.objects.create(prename=prename, lastname=lastname, birth_date=timezone.localdate(), email=settings.TEST_MAIL) + member = Member.objects.create(prename=prename, lastname=lastname, birth_date=timezone.localdate(), email=settings.TEST_MAIL, gender=DIVERSE) member.user = user member.save() user.is_staff = True @@ -37,22 +39,22 @@ class BasicMemberTestCase(TestCase): self.spiel = Group.objects.create(name="Spielkinder") self.fritz = Member.objects.create(prename="Fritz", lastname="Wulter", birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=DIVERSE) self.fritz.group.add(self.jl) self.fritz.group.add(self.alp) self.fritz.save() self.lara = Member.objects.create(prename="Lara", lastname="Wallis", birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=DIVERSE) self.lara.group.add(self.alp) self.lara.save() self.fridolin = Member.objects.create(prename="Fridolin", lastname="Spargel", birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=MALE) self.fridolin.group.add(self.alp) self.fridolin.group.add(self.spiel) self.fridolin.save() self.lise = Member.objects.create(prename="Lise", lastname="Lotte", birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=FEMALE) class MemberTestCase(BasicMemberTestCase): @@ -66,11 +68,11 @@ class MemberTestCase(BasicMemberTestCase): self.ja = Group.objects.create(name="Jugendausschuss") self.peter = Member.objects.create(prename="Peter", lastname="Keks", birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=MALE) self.anna = Member.objects.create(prename="Anna", lastname="Keks", birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=FEMALE) self.lisa = Member.objects.create(prename="Lisa", lastname="Keks", birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=DIVERSE) self.peter.group.add(self.ja) self.anna.group.add(self.ja) self.lisa.group.add(self.ja) @@ -128,7 +130,7 @@ class PDFTestCase(TestCase): for i in range(7): m = Member.objects.create(prename='Lise {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=FEMALE) NewMemberOnList.objects.create(member=m, comments='a' * i, memberlist=self.ex) NewMemberOnList.objects.create(member=m, comments='a' * i, memberlist=self.note) @@ -158,6 +160,13 @@ class PDFTestCase(TestCase): self._test_pdf('notes_list') self._test_pdf('notes_list', username='standard', invalid=True) + def test_sjr_application(self): + self._test_pdf('sjr_application') + self._test_pdf('sjr_application', username='standard', invalid=True) + + # TODO: Since generating a seminar report requires more input now, this test rightly + # fails. Replace this test with one that fills the POST form and generates a pdf. + @skip("Currently rightly fails, because expected behaviour changed.") def test_seminar_report(self): self._test_pdf('seminar_report') self._test_pdf('seminar_report', username='standard', invalid=True) @@ -200,21 +209,21 @@ class AdminTestCase(TestCase): for i in range(3): m = Member.objects.create(prename='Fritz {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=MALE) m.group.add(cool_kids) m.save() for i in range(7): m = Member.objects.create(prename='Lise {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=FEMALE) m.group.add(super_kids) m.save() for i in range(5): m = Member.objects.create(prename='Lulla {}'.format(i), lastname='Hulla', birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=DIVERSE) m.group.add(staff) m.save() m = Member.objects.create(prename='Peter', lastname='Hulla', birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=MALE) m.group.add(staff) p1.list_members.add(m) @@ -256,7 +265,7 @@ class MemberAdminTestCase(AdminTestCase): for i in range(1): m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=MALE) m.group.add(mega_kids) m.save() @@ -383,7 +392,7 @@ class FreizeitAdminTestCase(AdminTestCase): for i in range(7): m = Member.objects.create(prename='Lise {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), - email=settings.TEST_MAIL) + email=settings.TEST_MAIL, gender=FEMALE) NewMemberOnList.objects.create(member=m, comments='a' * i, memberlist=ex) def test_changelist(self): @@ -413,11 +422,17 @@ class FreizeitAdminTestCase(AdminTestCase): response = c.get(url) self.assertEqual(response.status_code, 200, 'Response code is not 200.') + @skip("The filtering is currently (intentionally) disabled.") + def test_add_queryset_filter(self): + """Test if queryset on `jugendleiter` field is properly filtered by permissions.""" u = User.objects.get(username='standard') + c = self._login('standard') + + url = reverse('admin:members_freizeit_add') request = self.factory.get(url) request.user = u - #staff = Group.objects.get(name='Jugendleiter') + field = Freizeit._meta.get_field('jugendleiter') queryset = self.admin.formfield_for_manytomany(field, request).queryset self.assertQuerysetEqual(queryset, u.member.filter_queryset_by_permissions(model=Member), @@ -472,6 +487,7 @@ class MailConfirmationTestCase(BasicMemberTestCase): # father's mail should now be confirmed self.assertTrue(self.father.confirmed_mail, msg='After confirming by key, the mail should be confirmed.') + @skip("Currently, emergency contact email addresses are not required to be confirmed.") def test_emergency_contact_confirmation(self): # request mail confirmation of fritz, should also ask for confirmation of father requested_confirmation = self.fritz.request_mail_confirmation() diff --git a/jdav_web/startpage/locale/de/LC_MESSAGES/django.po b/jdav_web/startpage/locale/de/LC_MESSAGES/django.po index 4705c40..db3bb87 100644 --- a/jdav_web/startpage/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/startpage/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-04 00:04+0100\n" +"POT-Creation-Date: 2025-01-01 21:48+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,94 +18,93 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: startpage/admin.py:21 startpage/admin.py:36 startpage/models.py:18 -#: startpage/models.py:40 +#: startpage/admin.py startpage/models.py msgid "URL" msgstr "URL" -#: startpage/admin.py:22 startpage/admin.py:37 +#: startpage/admin.py msgid "The url may only consist of letters, numerals, _, -, :, * and spaces." msgstr "" "Die URL darf nur aus Buchstaben, Zahlen, _, -, :, * oder Leerzeichen " "bestehen." -#: startpage/models.py:17 startpage/models.py:39 +#: startpage/models.py msgid "Title" msgstr "Titel" -#: startpage/models.py:19 startpage/models.py:42 +#: startpage/models.py msgid "website text" msgstr "Webseitentext" -#: startpage/models.py:20 +#: startpage/models.py msgid "Show in navigation" msgstr "In Navigation anzeigen" -#: startpage/models.py:23 startpage/models.py:63 +#: startpage/models.py msgid "Section" msgstr "Abschnitt" -#: startpage/models.py:24 +#: startpage/models.py msgid "Sections" msgstr "Abschnitte" -#: startpage/models.py:41 +#: startpage/models.py msgid "Date" msgstr "Datum" -#: startpage/models.py:44 +#: startpage/models.py msgid "Groups" msgstr "Gruppen" -#: startpage/models.py:45 +#: startpage/models.py msgid "detailed" msgstr "detailliert" -#: startpage/models.py:47 +#: startpage/models.py msgid "section" msgstr "Abschnitt" -#: startpage/models.py:54 +#: startpage/models.py msgid "Post" msgstr "Eintrag" -#: startpage/models.py:55 +#: startpage/models.py msgid "Posts" msgstr "Einträge" -#: startpage/models.py:79 +#: startpage/models.py msgid "file" msgstr "Datei" -#: startpage/models.py:85 +#: startpage/models.py msgid "Empty" msgstr "Leer" -#: startpage/models.py:88 +#: startpage/models.py msgid "image" msgstr "Bild" -#: startpage/models.py:89 +#: startpage/models.py msgid "images" msgstr "Bilder" -#: startpage/models.py:96 startpage/models.py:97 +#: startpage/models.py msgid "Member" msgstr "Mitglied" -#: startpage/models.py:98 +#: startpage/models.py msgid "Description" msgstr "Beschreibung" -#: startpage/models.py:99 +#: startpage/models.py msgid "Tag" msgstr "Tag" -#: startpage/models.py:102 +#: startpage/models.py msgid "Person" msgstr "Person" -#: startpage/models.py:103 +#: startpage/models.py msgid "Persons" msgstr "Personen" diff --git a/jdav_web/startpage/tests.py b/jdav_web/startpage/tests.py index c012738..8a8ceef 100644 --- a/jdav_web/startpage/tests.py +++ b/jdav_web/startpage/tests.py @@ -1,46 +1,55 @@ from django.test import TestCase, Client from django.urls import reverse +from django.conf import settings from members.models import Group from .models import Post, Section -class ModelsTestCase(TestCase): +class BasicTestCase(TestCase): def setUp(self): orga = Section.objects.create(title='Organisation', urlname='orga', website_text='Section is a about everything.') - Post.objects.create(title='Climbing is fun', urlname='climbing-is-fun', website_text='Climbing is fun!') + recent = Section.objects.create(title='Recent', urlname=settings.RECENT_SECTION, website_text='Recently recent.') + reports = Section.objects.create(title='Reports', urlname=settings.REPORTS_SECTION, website_text='Reporty reports.') + Post.objects.create(title='Climbing is fun', urlname='climbing-is-fun', website_text='Climbing is fun!', + section=recent) + Post.objects.create(title='Last trip', urlname='last-trip', website_text='A fun trip.', + section=reports) Post.objects.create(title='Staff', urlname='staff', website_text='This is our staff: Peter.', section=orga) + Group.objects.create(name='CrazyClimbers', show_website=True) + Group.objects.create(name='SuperClimbers', show_website=False) + +class ModelsTestCase(BasicTestCase): def test_str(self): orga = Section.objects.get(urlname='orga') self.assertEqual(str(orga), orga.title, 'String representation does not match title.') post = Post.objects.get(urlname='staff', section=orga) self.assertEqual(post.absolute_section(), orga.title, 'Displayed section of post does not match section title.') self.assertEqual(str(post), post.title, 'String representation does not match title.') - for post in Post.objects.filter(section=None): - self.assertEqual(post.absolute_section(), 'Aktuelles', 'Displayed section of post does not "Aktuelles".') def test_absolute_urlnames(self): orga = Section.objects.get(urlname='orga') + recent = Section.objects.get(urlname=settings.RECENT_SECTION) + reports = Section.objects.get(urlname=settings.REPORTS_SECTION) self.assertEqual(orga.absolute_urlname(), '/de/orga') post1 = Post.objects.get(urlname='staff', section=orga) self.assertEqual(post1.absolute_urlname(), '/de/orga/staff') - post2 = Post.objects.get(urlname='climbing-is-fun', section=None) - self.assertEqual(post2.absolute_urlname(), '/de/aktuelles/climbing-is-fun') - - -class ViewTestCase(TestCase): - def setUp(self): - orga = Section.objects.create(title='Organisation', urlname='orga', website_text='Section is a about everything.') - Post.objects.create(title='Climbing is fun', urlname='climbing-is-fun', website_text='Climbing is fun!') - Post.objects.create(title='Staff', urlname='staff', website_text='This is our staff: Peter.', - section=orga) - Group.objects.create(name='CrazyClimbers', show_website=True) - Group.objects.create(name='SuperClimbers', show_website=False) - + self.assertEqual(post1.absolute_urlname(), reverse('startpage:post', args=(orga.urlname, 'staff'))) + post2 = Post.objects.get(urlname='climbing-is-fun', section=recent) + self.assertEqual(post2.absolute_urlname(), + '/de/{name}/climbing-is-fun'.format(name=settings.RECENT_SECTION)) + self.assertEqual(post2.absolute_urlname(), reverse('startpage:post', args=(recent.urlname, 'climbing-is-fun'))) + post3 = Post.objects.get(urlname='last-trip', section=reports) + self.assertEqual(post3.absolute_urlname(), + '/de/{name}/last-trip'.format(name=settings.REPORTS_SECTION)) + self.assertEqual(post3.absolute_urlname(), reverse('startpage:post', args=(reports.urlname, 'last-trip'))) + + +class ViewTestCase(BasicTestCase): def test_index(self): c = Client() url = reverse('startpage:index') @@ -49,7 +58,7 @@ class ViewTestCase(TestCase): def test_posts_no_category(self): c = Client() - url = reverse('startpage:post', args=('aktuelles', 'climbing-is-fun')) + url = reverse('startpage:post', args=(settings.RECENT_SECTION, 'climbing-is-fun')) response = c.get(url) self.assertEqual(response.status_code, 200, 'Response code is not 200 for climbing post.') @@ -67,7 +76,13 @@ class ViewTestCase(TestCase): def test_section_recent(self): c = Client() - url = reverse('startpage:aktuelles') + url = reverse('startpage:' + settings.RECENT_SECTION) + response = c.get(url) + self.assertEqual(response.status_code, 200, 'Response code is not 200 for section page.') + + def test_section_reports(self): + c = Client() + url = reverse('startpage:' + settings.REPORTS_SECTION) response = c.get(url) self.assertEqual(response.status_code, 200, 'Response code is not 200 for section page.') diff --git a/jdav_web/static/js/qrcode.js b/jdav_web/static/js/qrcode.js new file mode 100644 index 0000000..5507c15 --- /dev/null +++ b/jdav_web/static/js/qrcode.js @@ -0,0 +1,614 @@ +/** + * @fileoverview + * - Using the 'QRCode for Javascript library' + * - Fixed dataset of 'QRCode for Javascript library' for support full-spec. + * - this library has no dependencies. + * + * @author davidshimjs + * @see http://www.d-project.com/ + * @see http://jeromeetienne.github.com/jquery-qrcode/ + */ +var QRCode; + +(function () { + //--------------------------------------------------------------------- + // QRCode for JavaScript + // + // Copyright (c) 2009 Kazuhiko Arase + // + // URL: http://www.d-project.com/ + // + // Licensed under the MIT license: + // http://www.opensource.org/licenses/mit-license.php + // + // The word "QR Code" is registered trademark of + // DENSO WAVE INCORPORATED + // http://www.denso-wave.com/qrcode/faqpatent-e.html + // + //--------------------------------------------------------------------- + function QR8bitByte(data) { + this.mode = QRMode.MODE_8BIT_BYTE; + this.data = data; + this.parsedData = []; + + // Added to support UTF-8 Characters + for (var i = 0, l = this.data.length; i < l; i++) { + var byteArray = []; + var code = this.data.charCodeAt(i); + + if (code > 0x10000) { + byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18); + byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12); + byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6); + byteArray[3] = 0x80 | (code & 0x3F); + } else if (code > 0x800) { + byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12); + byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6); + byteArray[2] = 0x80 | (code & 0x3F); + } else if (code > 0x80) { + byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6); + byteArray[1] = 0x80 | (code & 0x3F); + } else { + byteArray[0] = code; + } + + this.parsedData.push(byteArray); + } + + this.parsedData = Array.prototype.concat.apply([], this.parsedData); + + if (this.parsedData.length != this.data.length) { + this.parsedData.unshift(191); + this.parsedData.unshift(187); + this.parsedData.unshift(239); + } + } + + QR8bitByte.prototype = { + getLength: function (buffer) { + return this.parsedData.length; + }, + write: function (buffer) { + for (var i = 0, l = this.parsedData.length; i < l; i++) { + buffer.put(this.parsedData[i], 8); + } + } + }; + + function QRCodeModel(typeNumber, errorCorrectLevel) { + this.typeNumber = typeNumber; + this.errorCorrectLevel = errorCorrectLevel; + this.modules = null; + this.moduleCount = 0; + this.dataCache = null; + this.dataList = []; + } + + QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);} + return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row=7){this.setupTypeNumber(test);} + if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);} + this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}} + return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;} + for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}} + for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}} + this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex>>bitIndex)&1)==1);} + var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;} + this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}} + row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;itotalDataCount*8){throw new Error("code length overflow. (" + +buffer.getLengthInBits() + +">" + +totalDataCount*8 + +")");} + if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);} + while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);} + while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;} + buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;} + buffer.put(QRCodeModel.PAD1,8);} + return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r=0)?modPoly.get(modIndex):0;}} + var totalCodeCount=0;for(var i=0;i=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));} + return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));} + return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;} + return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i5){lostPoint+=(3+sameCount-5);}}} + for(var row=0;row=256){n-=255;} + return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);} + if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));} + this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]]; + + function _isSupportCanvas() { + return typeof CanvasRenderingContext2D != "undefined"; + } + + // android 2.x doesn't support Data-URI spec + function _getAndroid() { + var android = false; + var sAgent = navigator.userAgent; + + if (/android/i.test(sAgent)) { // android + android = true; + var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i); + + if (aMat && aMat[1]) { + android = parseFloat(aMat[1]); + } + } + + return android; + } + + var svgDrawer = (function() { + + var Drawing = function (el, htOption) { + this._el = el; + this._htOption = htOption; + }; + + Drawing.prototype.draw = function (oQRCode) { + var _htOption = this._htOption; + var _el = this._el; + var nCount = oQRCode.getModuleCount(); + var nWidth = Math.floor(_htOption.width / nCount); + var nHeight = Math.floor(_htOption.height / nCount); + + this.clear(); + + function makeSVG(tag, attrs) { + var el = document.createElementNS('http://www.w3.org/2000/svg', tag); + for (var k in attrs) + if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]); + return el; + } + + var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight}); + svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); + _el.appendChild(svg); + + svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"})); + svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"})); + + for (var row = 0; row < nCount; row++) { + for (var col = 0; col < nCount; col++) { + if (oQRCode.isDark(row, col)) { + var child = makeSVG("use", {"x": String(col), "y": String(row)}); + child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template") + svg.appendChild(child); + } + } + } + }; + Drawing.prototype.clear = function () { + while (this._el.hasChildNodes()) + this._el.removeChild(this._el.lastChild); + }; + return Drawing; + })(); + + var useSVG = document.documentElement.tagName.toLowerCase() === "svg"; + + // Drawing in DOM by using Table tag + var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () { + var Drawing = function (el, htOption) { + this._el = el; + this._htOption = htOption; + }; + + /** + * Draw the QRCode + * + * @param {QRCode} oQRCode + */ + Drawing.prototype.draw = function (oQRCode) { + var _htOption = this._htOption; + var _el = this._el; + var nCount = oQRCode.getModuleCount(); + var nWidth = Math.floor(_htOption.width / nCount); + var nHeight = Math.floor(_htOption.height / nCount); + var aHTML = ['']; + + for (var row = 0; row < nCount; row++) { + aHTML.push(''); + + for (var col = 0; col < nCount; col++) { + aHTML.push(''); + } + + aHTML.push(''); + } + + aHTML.push('
'); + _el.innerHTML = aHTML.join(''); + + // Fix the margin values as real size. + var elTable = _el.childNodes[0]; + var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2; + var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2; + + if (nLeftMarginTable > 0 && nTopMarginTable > 0) { + elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px"; + } + }; + + /** + * Clear the QRCode + */ + Drawing.prototype.clear = function () { + this._el.innerHTML = ''; + }; + + return Drawing; + })() : (function () { // Drawing in Canvas + function _onMakeImage() { + this._elImage.src = this._elCanvas.toDataURL("image/png"); + this._elImage.style.display = "block"; + this._elCanvas.style.display = "none"; + } + + // Android 2.1 bug workaround + // http://code.google.com/p/android/issues/detail?id=5141 + if (this._android && this._android <= 2.1) { + var factor = 1 / window.devicePixelRatio; + var drawImage = CanvasRenderingContext2D.prototype.drawImage; + CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) { + if (("nodeName" in image) && /img/i.test(image.nodeName)) { + for (var i = arguments.length - 1; i >= 1; i--) { + arguments[i] = arguments[i] * factor; + } + } else if (typeof dw == "undefined") { + arguments[1] *= factor; + arguments[2] *= factor; + arguments[3] *= factor; + arguments[4] *= factor; + } + + drawImage.apply(this, arguments); + }; + } + + /** + * Check whether the user's browser supports Data URI or not + * + * @private + * @param {Function} fSuccess Occurs if it supports Data URI + * @param {Function} fFail Occurs if it doesn't support Data URI + */ + function _safeSetDataURI(fSuccess, fFail) { + var self = this; + self._fFail = fFail; + self._fSuccess = fSuccess; + + // Check it just once + if (self._bSupportDataURI === null) { + var el = document.createElement("img"); + var fOnError = function() { + self._bSupportDataURI = false; + + if (self._fFail) { + self._fFail.call(self); + } + }; + var fOnSuccess = function() { + self._bSupportDataURI = true; + + if (self._fSuccess) { + self._fSuccess.call(self); + } + }; + + el.onabort = fOnError; + el.onerror = fOnError; + el.onload = fOnSuccess; + el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data. + return; + } else if (self._bSupportDataURI === true && self._fSuccess) { + self._fSuccess.call(self); + } else if (self._bSupportDataURI === false && self._fFail) { + self._fFail.call(self); + } + }; + + /** + * Drawing QRCode by using canvas + * + * @constructor + * @param {HTMLElement} el + * @param {Object} htOption QRCode Options + */ + var Drawing = function (el, htOption) { + this._bIsPainted = false; + this._android = _getAndroid(); + + this._htOption = htOption; + this._elCanvas = document.createElement("canvas"); + this._elCanvas.width = htOption.width; + this._elCanvas.height = htOption.height; + el.appendChild(this._elCanvas); + this._el = el; + this._oContext = this._elCanvas.getContext("2d"); + this._bIsPainted = false; + this._elImage = document.createElement("img"); + this._elImage.alt = "Scan me!"; + this._elImage.style.display = "none"; + this._el.appendChild(this._elImage); + this._bSupportDataURI = null; + }; + + /** + * Draw the QRCode + * + * @param {QRCode} oQRCode + */ + Drawing.prototype.draw = function (oQRCode) { + var _elImage = this._elImage; + var _oContext = this._oContext; + var _htOption = this._htOption; + + var nCount = oQRCode.getModuleCount(); + var nWidth = _htOption.width / nCount; + var nHeight = _htOption.height / nCount; + var nRoundedWidth = Math.round(nWidth); + var nRoundedHeight = Math.round(nHeight); + + _elImage.style.display = "none"; + this.clear(); + + for (var row = 0; row < nCount; row++) { + for (var col = 0; col < nCount; col++) { + var bIsDark = oQRCode.isDark(row, col); + var nLeft = col * nWidth; + var nTop = row * nHeight; + _oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; + _oContext.lineWidth = 1; + _oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; + _oContext.fillRect(nLeft, nTop, nWidth, nHeight); + + // 안티 앨리어싱 방지 처리 + _oContext.strokeRect( + Math.floor(nLeft) + 0.5, + Math.floor(nTop) + 0.5, + nRoundedWidth, + nRoundedHeight + ); + + _oContext.strokeRect( + Math.ceil(nLeft) - 0.5, + Math.ceil(nTop) - 0.5, + nRoundedWidth, + nRoundedHeight + ); + } + } + + this._bIsPainted = true; + }; + + /** + * Make the image from Canvas if the browser supports Data URI. + */ + Drawing.prototype.makeImage = function () { + if (this._bIsPainted) { + _safeSetDataURI.call(this, _onMakeImage); + } + }; + + /** + * Return whether the QRCode is painted or not + * + * @return {Boolean} + */ + Drawing.prototype.isPainted = function () { + return this._bIsPainted; + }; + + /** + * Clear the QRCode + */ + Drawing.prototype.clear = function () { + this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height); + this._bIsPainted = false; + }; + + /** + * @private + * @param {Number} nNumber + */ + Drawing.prototype.round = function (nNumber) { + if (!nNumber) { + return nNumber; + } + + return Math.floor(nNumber * 1000) / 1000; + }; + + return Drawing; + })(); + + /** + * Get the type by string length + * + * @private + * @param {String} sText + * @param {Number} nCorrectLevel + * @return {Number} type + */ + function _getTypeNumber(sText, nCorrectLevel) { + var nType = 1; + var length = _getUTF8Length(sText); + + for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) { + var nLimit = 0; + + switch (nCorrectLevel) { + case QRErrorCorrectLevel.L : + nLimit = QRCodeLimitLength[i][0]; + break; + case QRErrorCorrectLevel.M : + nLimit = QRCodeLimitLength[i][1]; + break; + case QRErrorCorrectLevel.Q : + nLimit = QRCodeLimitLength[i][2]; + break; + case QRErrorCorrectLevel.H : + nLimit = QRCodeLimitLength[i][3]; + break; + } + + if (length <= nLimit) { + break; + } else { + nType++; + } + } + + if (nType > QRCodeLimitLength.length) { + throw new Error("Too long data"); + } + + return nType; + } + + function _getUTF8Length(sText) { + var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a'); + return replacedText.length + (replacedText.length != sText ? 3 : 0); + } + + /** + * @class QRCode + * @constructor + * @example + * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie"); + * + * @example + * var oQRCode = new QRCode("test", { + * text : "http://naver.com", + * width : 128, + * height : 128 + * }); + * + * oQRCode.clear(); // Clear the QRCode. + * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode. + * + * @param {HTMLElement|String} el target element or 'id' attribute of element. + * @param {Object|String} vOption + * @param {String} vOption.text QRCode link data + * @param {Number} [vOption.width=256] + * @param {Number} [vOption.height=256] + * @param {String} [vOption.colorDark="#000000"] + * @param {String} [vOption.colorLight="#ffffff"] + * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H] + */ + QRCode = function (el, vOption) { + this._htOption = { + width : 256, + height : 256, + typeNumber : 4, + colorDark : "#000000", + colorLight : "#ffffff", + correctLevel : QRErrorCorrectLevel.H + }; + + if (typeof vOption === 'string') { + vOption = { + text : vOption + }; + } + + // Overwrites options + if (vOption) { + for (var i in vOption) { + this._htOption[i] = vOption[i]; + } + } + + if (typeof el == "string") { + el = document.getElementById(el); + } + + if (this._htOption.useSVG) { + Drawing = svgDrawer; + } + + this._android = _getAndroid(); + this._el = el; + this._oQRCode = null; + this._oDrawing = new Drawing(this._el, this._htOption); + + if (this._htOption.text) { + this.makeCode(this._htOption.text); + } + }; + + /** + * Make the QRCode + * + * @param {String} sText link data + */ + QRCode.prototype.makeCode = function (sText) { + this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel); + this._oQRCode.addData(sText); + this._oQRCode.make(); + this._el.title = sText; + this._oDrawing.draw(this._oQRCode); + this.makeImage(); + }; + + /** + * Make the Image from Canvas element + * - It occurs automatically + * - Android below 3 doesn't support Data-URI spec. + * + * @private + */ + QRCode.prototype.makeImage = function () { + if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) { + this._oDrawing.makeImage(); + } + }; + + /** + * Clear the QRCode + */ + QRCode.prototype.clear = function () { + this._oDrawing.clear(); + }; + + /** + * @name QRCode.CorrectLevel + */ + QRCode.CorrectLevel = QRErrorCorrectLevel; +})(); diff --git a/requirements.txt b/requirements.txt index b490373..1ad9aa9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -alabaster==0.7.16 amqp==5.0.9 asgiref==3.4.1 auditlog3==1.0.1 @@ -48,9 +47,11 @@ pytz==2021.3 redis==4.1.0 requests==2.32.3 rules==3.3 +schwifty==2024.11.0 six==1.16.0 snowballstemmer==2.2.0 Sphinx==7.4.7 +sphinxawesome-theme==5.3.2 sphinxcontrib-applehelp==2.0.0 sphinxcontrib-devhelp==2.0.0 sphinxcontrib-htmlhelp==2.1.0