Merge branch 'main' into MK/sjr_select_invoice

pull/103/head
mariusrklein 11 months ago
commit 03a3119974

@ -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
```

@ -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

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

@ -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

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

@ -0,0 +1,7 @@
.. _development_manual/architecture:
=================
Architecture
=================
tbd

@ -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 <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>`_.
.. note::
Please read this page carefully before contributing.
Miscellaneous
-------------
- version control with `git <https://git-scm.com/>`_
- 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: ``<username>/<feature>``.
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 <https://git.jdav-hd.merten.dev/digitales/kompass/issues>`_ to work on or create a new one
- branch out to an own branch (naming convention: ``<username>/<feature>``) 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 <https://www.sphinx-doc.org/>`_ and `awsome sphinx theme <https://sphinxawesome.xyz/>`_ the source code is located in ``docs/``.
- All documentation is written in `reStructuredText <https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html>`_ and uses the `sphinx directives <https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html>`_.
- The directives can vary due to the theme, see the `awesome sphinx theme documentation <https://sphinxawesome.xyz/demo/notes/>`_.
- 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 <development_manual/contributing/documentation>` your code properly and write tests.
.. note::
Still open / to decide:
- linting
- (auto) formatting
- reliable tests via ci/cd pipeline

@ -0,0 +1,7 @@
.. _development_manual/deployment:
=====================
Production Deployment
=====================
tbd

@ -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 <development_manual/contributing>`
Documentation
-------------
Structure
- :ref:`Nutzer Dokumentation <user_manual/index>` auf deutsch
- :ref:`Development Documentation <development_manual/index>` auf englisch
.. seealso::
:ref:`Contributing #Documentation <development_manual/contributing/documentation>`

@ -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 <https://docs.docker.com/engine/install/>`_.
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 <command>``. 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 <step-3>` above.

@ -0,0 +1,7 @@
.. _development_manual/testing:
=================
Testing
=================
To run the tests, you can use the docker setup under ``docker/test``.

@ -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

@ -1,9 +1,9 @@
.. _excursions:
.. _user_manual/excursions:
Ausfahrten
==========
Neben der :ref:`Teilnehmer\*innenverwaltung <members>` ist das Abwickeln von Ausfahrten
Neben der :ref:`Teilnehmer\*innenverwaltung <user_manual/members>` ist das Abwickeln von Ausfahrten
die zweite wichtige Aufgabe des Kompass. Eine Ausfahrt für die eigene Jugendgruppe
anbieten ist neben der ganzen inhaltlichen Arbeit auch jede Menge bürokratischer Aufwand. Der Kompass
versucht dir von diesem Aufwand so viel wie möglich abzunehmen.
@ -43,6 +43,8 @@ Seminarbericht direkt ein und lass dir den Papierkram vom Kompass erledigen.
SJR Antrag
----------
tbd
Abrechnung
----------

@ -1,3 +1,5 @@
.. _user_manual/finance:
Finanzen
========

@ -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 <members>`
Manche Einträge wiederum kannst du einsehen, aber nicht bearbeiten. Für mehr Details siehe :ref:`Teilnehmer\*innenverwaltung <user_manual/members>`
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/

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

@ -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 <mailto:digitales@jdav-hd.de?subject=Kompass Feedback>`_.
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.

@ -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

@ -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 <members>`
:ref:`Teilnehmer\*innenverwaltung <user_manual/members>`
.. _group-registration-password:

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,12 +18,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: finance/admin.py:76
#: finance/admin.py
#, 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."

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

@ -7,6 +7,8 @@
<script src="{% static 'admin/js/cancel.js' %}" async></script>
<script type="text/javascript" src="{% static "admin/js/vendor/jquery/jquery.js" %}"></script>
<script type="text/javascript" src="{% static "admin/js/jquery.init.js" %}"></script>
<script type="text/javascript" src="{% static "js/qrcode.js" %}"></script>
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} admin-view
@ -36,6 +38,7 @@
<td>{% trans "Amount" %}</td>
<td>{% trans "Reference" %}</td>
<td>{% trans "Ledger" %}</td>
<td>{% trans "QR Code" %}</td>
</th>
{% for transaction in statement.transaction_set.all %}
<tr>
@ -54,11 +57,53 @@
<td>
{{ transaction.ledger }}
</td>
<td>
<a href="#" data-text="{{ transaction.code }}">{% trans "Show" %}</a>
</td>
</tr>
{% endfor %}
</table>
</p>
<div id="qr_code" style="display: none;"></div>
<script type="text/javascript">
const links = document.querySelectorAll('td a');
const imageContainer = document.getElementById('qr_code');
// Add click event listeners to all links
links.forEach(link => {
link.addEventListener('click', function (event) {
event.preventDefault(); // Prevent default link behavior
const imageText = this.getAttribute('data-text'); // Get the image path from the data attribute
imageContainer.innerHTML = '';
// Update the image element
if(imageText == "") {
imageContainer.innerHTML = '{% trans "No QR code can be displayed." %}';
} else {
var qrcode = new QRCode(imageContainer, {
text: imageText,
width: 128,
height: 128,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.M
});
}
imageContainer.style.display = 'block'; // Show the image if hidden
links.forEach(link => {link.text = '{% trans "Show" %}'});
link.text = '{% trans "Showing" %}';
});
});
</script>
<form action="" method="post">
{% csrf_token %}
<p>

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

@ -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.

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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, Im 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: {}."

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"

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

File diff suppressed because it is too large Load Diff

@ -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

@ -17,7 +17,9 @@
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
{% if object %}
&rsaquo; <a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a>
{% endif %}
&rsaquo; {% translate 'Invite to group' %}
</div>
{% endblock %}
@ -25,18 +27,28 @@
{% block content %}
<h2>{% translate "Invite to a group" %}</h2>
<p>
{% if waiter %}
{% trans "You are inviting:" %}
{% else %}
{% trans "You are inviting the following waiters for registration:" %}
{% endif %}
</p>
<p>
<ul>
{% for waiter in queryset %}
<li>
<a href="{% url 'admin:members_memberwaitinglist_change' waiter.id %}">{{ waiter }}</a>
</li>
{% endfor %}
</ul>
</p>
<p>
{% 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 %}
</p>
<form action="" method="post">
@ -47,7 +59,7 @@
<br>
<div>
<p>
<input type="hidden" name="action" value="ask_for_registration">
<input type="hidden" name="action" value="ask_for_registration_action">
<input class="default" style="color: $default-link-color" type="submit" name="apply" value="{% translate 'Invite' %}">
<a href="#" class="button cancel-link">{% translate "Cancel" %}</a>
</p>

@ -0,0 +1,68 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}
{% block extrahead %}
{{ block.super }}
{{ media }}
<script src="{% static 'admin/js/cancel.js' %}" async></script>
<script type="text/javascript" src="{% static "admin/js/vendor/jquery/jquery.js" %}"></script>
<script type="text/javascript" src="{% static "admin/js/jquery.init.js" %}"></script>
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} invite-waiter
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
{% if object %}
&rsaquo; <a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a>
{% endif %}
&rsaquo; {% translate 'Invite to group' %}
</div>
{% endblock %}
{% block content %}
<h2>{% translate "Invite to a group" %}</h2>
<p>
{% 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 %}
</p>
<p>
<ul>
{% for waiter in queryset %}
<li>
<a href="{% url 'admin:members_memberwaitinglist_change' waiter.id %}">{{ waiter }}</a>
</li>
{% endfor %}
</ul>
</p>
<p>
{% 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 %}
</p>
<form action="" method="post">
{% csrf_token %}
<p>
{{form}}
</p>
<br>
<div>
<p>
<input type="hidden" name="group" value="{{ group.pk }}">
<input type="hidden" name="action" value="ask_for_registration_action">
<input class="default" style="color: $default-link-color" type="submit" name="send" value="{% translate 'Send' %}">
<a href="#" class="button cancel-link">{% translate "Back" %}</a>
</p>
</div>
</form>
{% endblock %}

@ -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()

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"

@ -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.')

@ -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 <a href="http://www.d-project.com/" target="_blank">http://www.d-project.com/</a>
* @see <a href="http://jeromeetienne.github.com/jquery-qrcode/" target="_blank">http://jeromeetienne.github.com/jquery-qrcode/</a>
*/
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<this.moduleCount;row++){this.modules[row]=new Array(this.moduleCount);for(var col=0;col<this.moduleCount;col++){this.modules[row][col]=null;}}
this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(test,maskPattern);if(this.typeNumber>=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<this.modules.length;row++){var y=row*cs;for(var col=0;col<this.modules[row].length;col++){var x=col*cs;var dark=this.modules[row][col];if(dark){qr_mc.beginFill(0,100);qr_mc.moveTo(x,y);qr_mc.lineTo(x+cs,y);qr_mc.lineTo(x+cs,y+cs);qr_mc.lineTo(x,y+cs);qr_mc.endFill();}}}
return qr_mc;},setupTimingPattern:function(){for(var r=8;r<this.moduleCount-8;r++){if(this.modules[r][6]!=null){continue;}
this.modules[r][6]=(r%2==0);}
for(var c=8;c<this.moduleCount-8;c++){if(this.modules[6][c]!=null){continue;}
this.modules[6][c]=(c%2==0);}},setupPositionAdjustPattern:function(){var pos=QRUtil.getPatternPosition(this.typeNumber);for(var i=0;i<pos.length;i++){for(var j=0;j<pos.length;j++){var row=pos[i];var col=pos[j];if(this.modules[row][col]!=null){continue;}
for(var r=-2;r<=2;r++){for(var c=-2;c<=2;c++){if(r==-2||r==2||c==-2||c==2||(r==0&&c==0)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}}}},setupTypeNumber:function(test){var bits=QRUtil.getBCHTypeNumber(this.typeNumber);for(var i=0;i<18;i++){var mod=(!test&&((bits>>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<data.length){dark=(((data[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;i<dataList.length;i++){var data=dataList[i];buffer.put(data.mode,4);buffer.put(data.getLength(),QRUtil.getLengthInBits(data.mode,typeNumber));data.write(buffer);}
var totalDataCount=0;for(var i=0;i<rsBlocks.length;i++){totalDataCount+=rsBlocks[i].dataCount;}
if(buffer.getLengthInBits()>totalDataCount*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<rsBlocks.length;r++){var dcCount=rsBlocks[r].dataCount;var ecCount=rsBlocks[r].totalCount-dcCount;maxDcCount=Math.max(maxDcCount,dcCount);maxEcCount=Math.max(maxEcCount,ecCount);dcdata[r]=new Array(dcCount);for(var i=0;i<dcdata[r].length;i++){dcdata[r][i]=0xff&buffer.buffer[i+offset];}
offset+=dcCount;var rsPoly=QRUtil.getErrorCorrectPolynomial(ecCount);var rawPoly=new QRPolynomial(dcdata[r],rsPoly.getLength()-1);var modPoly=rawPoly.mod(rsPoly);ecdata[r]=new Array(rsPoly.getLength()-1);for(var i=0;i<ecdata[r].length;i++){var modIndex=i+modPoly.getLength()-ecdata[r].length;ecdata[r][i]=(modIndex>=0)?modPoly.get(modIndex):0;}}
var totalCodeCount=0;for(var i=0;i<rsBlocks.length;i++){totalCodeCount+=rsBlocks[i].totalCount;}
var data=new Array(totalCodeCount);var index=0;for(var i=0;i<maxDcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<dcdata[r].length){data[index++]=dcdata[r][i];}}}
for(var i=0;i<maxEcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<ecdata[r].length){data[index++]=ecdata[r][i];}}}
return data;};var QRMode={MODE_NUMBER:1<<0,MODE_ALPHA_NUM:1<<1,MODE_8BIT_BYTE:1<<2,MODE_KANJI:1<<3};var QRErrorCorrectLevel={L:1,M:0,Q:3,H:2};var QRMaskPattern={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var QRUtil={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:(1<<10)|(1<<8)|(1<<5)|(1<<4)|(1<<2)|(1<<1)|(1<<0),G18:(1<<12)|(1<<11)|(1<<10)|(1<<9)|(1<<8)|(1<<5)|(1<<2)|(1<<0),G15_MASK:(1<<14)|(1<<12)|(1<<10)|(1<<4)|(1<<1),getBCHTypeInfo:function(data){var d=data<<10;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)>=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;i<errorCorrectLength;i++){a=a.multiply(new QRPolynomial([1,QRMath.gexp(i)],0));}
return a;},getLengthInBits:function(mode,type){if(1<=type&&type<10){switch(mode){case QRMode.MODE_NUMBER:return 10;case QRMode.MODE_ALPHA_NUM:return 9;case QRMode.MODE_8BIT_BYTE:return 8;case QRMode.MODE_KANJI:return 8;default:throw new Error("mode:"+mode);}}else if(type<27){switch(mode){case QRMode.MODE_NUMBER:return 12;case QRMode.MODE_ALPHA_NUM:return 11;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 10;default:throw new Error("mode:"+mode);}}else if(type<41){switch(mode){case QRMode.MODE_NUMBER:return 14;case QRMode.MODE_ALPHA_NUM:return 13;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 12;default:throw new Error("mode:"+mode);}}else{throw new Error("type:"+type);}},getLostPoint:function(qrCode){var moduleCount=qrCode.getModuleCount();var lostPoint=0;for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount;col++){var sameCount=0;var dark=qrCode.isDark(row,col);for(var r=-1;r<=1;r++){if(row+r<0||moduleCount<=row+r){continue;}
for(var c=-1;c<=1;c++){if(col+c<0||moduleCount<=col+c){continue;}
if(r==0&&c==0){continue;}
if(dark==qrCode.isDark(row+r,col+c)){sameCount++;}}}
if(sameCount>5){lostPoint+=(3+sameCount-5);}}}
for(var row=0;row<moduleCount-1;row++){for(var col=0;col<moduleCount-1;col++){var count=0;if(qrCode.isDark(row,col))count++;if(qrCode.isDark(row+1,col))count++;if(qrCode.isDark(row,col+1))count++;if(qrCode.isDark(row+1,col+1))count++;if(count==0||count==4){lostPoint+=3;}}}
for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount-6;col++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row,col+1)&&qrCode.isDark(row,col+2)&&qrCode.isDark(row,col+3)&&qrCode.isDark(row,col+4)&&!qrCode.isDark(row,col+5)&&qrCode.isDark(row,col+6)){lostPoint+=40;}}}
for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount-6;row++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row+1,col)&&qrCode.isDark(row+2,col)&&qrCode.isDark(row+3,col)&&qrCode.isDark(row+4,col)&&!qrCode.isDark(row+5,col)&&qrCode.isDark(row+6,col)){lostPoint+=40;}}}
var darkCount=0;for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount;row++){if(qrCode.isDark(row,col)){darkCount++;}}}
var ratio=Math.abs(100*darkCount/moduleCount/moduleCount-50)/5;lostPoint+=ratio*10;return lostPoint;}};var QRMath={glog:function(n){if(n<1){throw new Error("glog("+n+")");}
return QRMath.LOG_TABLE[n];},gexp:function(n){while(n<0){n+=255;}
while(n>=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<<i;}
for(var i=8;i<256;i++){QRMath.EXP_TABLE[i]=QRMath.EXP_TABLE[i-4]^QRMath.EXP_TABLE[i-5]^QRMath.EXP_TABLE[i-6]^QRMath.EXP_TABLE[i-8];}
for(var i=0;i<255;i++){QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]]=i;}
function QRPolynomial(num,shift){if(num.length==undefined){throw new Error(num.length+"/"+shift);}
var offset=0;while(offset<num.length&&num[offset]==0){offset++;}
this.num=new Array(num.length-offset+shift);for(var i=0;i<num.length-offset;i++){this.num[i]=num[i+offset];}}
QRPolynomial.prototype={get:function(index){return this.num[index];},getLength:function(){return this.num.length;},multiply:function(e){var num=new Array(this.getLength()+e.getLength()-1);for(var i=0;i<this.getLength();i++){for(var j=0;j<e.getLength();j++){num[i+j]^=QRMath.gexp(QRMath.glog(this.get(i))+QRMath.glog(e.get(j)));}}
return new QRPolynomial(num,0);},mod:function(e){if(this.getLength()-e.getLength()<0){return this;}
var ratio=QRMath.glog(this.get(0))-QRMath.glog(e.get(0));var num=new Array(this.getLength());for(var i=0;i<this.getLength();i++){num[i]=this.get(i);}
for(var i=0;i<e.getLength();i++){num[i]^=QRMath.gexp(QRMath.glog(e.get(i))+ratio);}
return new QRPolynomial(num,0).mod(e);}};function QRRSBlock(totalCount,dataCount){this.totalCount=totalCount;this.dataCount=dataCount;}
QRRSBlock.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];QRRSBlock.getRSBlocks=function(typeNumber,errorCorrectLevel){var rsBlock=QRRSBlock.getRsBlockTable(typeNumber,errorCorrectLevel);if(rsBlock==undefined){throw new Error("bad rs block @ typeNumber:"+typeNumber+"/errorCorrectLevel:"+errorCorrectLevel);}
var length=rsBlock.length/3;var list=[];for(var i=0;i<length;i++){var count=rsBlock[i*3+0];var totalCount=rsBlock[i*3+1];var dataCount=rsBlock[i*3+2];for(var j=0;j<count;j++){list.push(new QRRSBlock(totalCount,dataCount));}}
return list;};QRRSBlock.getRsBlockTable=function(typeNumber,errorCorrectLevel){switch(errorCorrectLevel){case QRErrorCorrectLevel.L:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+0];case QRErrorCorrectLevel.M:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+1];case QRErrorCorrectLevel.Q:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+2];case QRErrorCorrectLevel.H:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+3];default:return undefined;}};function QRBitBuffer(){this.buffer=[];this.length=0;}
QRBitBuffer.prototype={get:function(index){var bufIndex=Math.floor(index/8);return((this.buffer[bufIndex]>>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i<length;i++){this.putBit(((num>>>(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 = ['<table style="border:0;border-collapse:collapse;">'];
for (var row = 0; row < nCount; row++) {
aHTML.push('<tr>');
for (var col = 0; col < nCount; col++) {
aHTML.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:' + nWidth + 'px;height:' + nHeight + 'px;background-color:' + (oQRCode.isDark(row, col) ? _htOption.colorDark : _htOption.colorLight) + ';"></td>');
}
aHTML.push('</tr>');
}
aHTML.push('</table>');
_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;
})();

@ -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

Loading…
Cancel
Save