diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6591e7cb8..0ed2373c5 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.1.2 +current_version = 4.0.1 commit = True tag = True diff --git a/.github/workflows/check-codestyle.yml b/.github/workflows/check-codestyle.yml index 230a4826a..f64b6a15b 100644 --- a/.github/workflows/check-codestyle.yml +++ b/.github/workflows/check-codestyle.yml @@ -13,10 +13,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python 3.13 uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: 3.13 - name: Install dependencies via apt run: | diff --git a/.gitignore b/.gitignore index 92c20b272..ac9198088 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ Session.vim # PyCharm .idea +# macOS +.DS_Store + # python/django *.pyc *.pyo diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b2e41b458..cb9033518 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,14 +2,14 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 23.7.0 hooks: - id: black - repo: local diff --git a/CONTRIBUTORS b/CONTRIBUTORS index b9fb8b01f..f853d5f30 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,3 +1,6 @@ +Konrad Gößmann +Dominik Rimpf +franztv82 Johannes Walcher Johannes Ostner Sebastian Faul diff --git a/Dockerfile b/Dockerfile index 455467044..fb3a1821e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bullseye +FROM debian:trixie ARG CONTAINER_VERSION="unknown" @@ -9,14 +9,15 @@ ENV HELFERTOOL_CONFIG_FILE="/config/helfertool.yaml" RUN apt-get update && apt-get full-upgrade -y && \ apt-get install --no-install-recommends -y \ supervisor nginx rsyslog pwgen curl \ - python3 python3-pip python3-dev uwsgi uwsgi-plugin-python3 \ - build-essential libldap2-dev libsasl2-dev libmariadb-dev libmagic1 \ + python3 python3-venv python3-dev uwsgi uwsgi-plugin-python3 \ + build-essential pkg-config ldap-utils libldap2-dev libsasl2-dev libmariadb-dev libpq-dev libmagic1 \ texlive-latex-extra texlive-plain-generic texlive-fonts-recommended texlive-lang-german && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /usr/share/doc/* && \ # add user, some directories and set file permissions useradd --shell /bin/bash --home-dir /helfertool --create-home helfertool --uid 10001 && \ mkdir -p /config /data /log /helfertool/run && \ + chmod 0755 /helfertool/ && \ chmod -R 0777 /helfertool/run && \ # nginx always writes to /var/log/nginx/error.log before reading the config # so we redirect it to a writable location @@ -34,13 +35,13 @@ COPY deployment/container/healthcheck.sh /usr/local/bin/healthcheck RUN echo $CONTAINER_VERSION > /helfertool/container_version && \ # install python libs cd /helfertool/src/ && \ - pip3 install -U pip && \ - pip3 install -r requirements.txt -r requirements_prod.txt && \ + python3 -m venv /helfertool/venv/ && \ + /helfertool/venv/bin/pip install wheel -r requirements.txt -r requirements_prod.txt && \ rm -rf /root/.cache/pip/ && \ # generate compressed CSS/JS files - HELFERTOOL_CONFIG_FILE=/dev/null python3 manage.py compress --force && \ + HELFERTOOL_CONFIG_FILE=/dev/null /helfertool/venv/bin/python manage.py compress --force && \ # copy static files - HELFERTOOL_CONFIG_FILE=/dev/null python3 manage.py collectstatic --noinput && \ + HELFERTOOL_CONFIG_FILE=/dev/null /helfertool/venv/bin/python manage.py collectstatic --noinput && \ chmod -R go+rX /helfertool/static && \ # fix permissions chmod +x /usr/local/bin/helfertool /usr/local/bin/healthcheck diff --git a/README.md b/README.md index 64ee27408..e5934880b 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Please feel free to create issues here in Github! # License -Copyright (C) 2015-2022 Sven Hertle and contributors +Copyright (C) 2015-2026 Sven Hertle and contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as diff --git a/deployment/container/etc/nginx.conf b/deployment/container/etc/nginx.conf index 16bea5cb2..137376aec 100644 --- a/deployment/container/etc/nginx.conf +++ b/deployment/container/etc/nginx.conf @@ -50,6 +50,8 @@ http { uwsgi_pass django; include /etc/nginx/uwsgi_params; + client_max_body_size 50M; + # CSP is set here, not in django add_header Content-Security-Policy "default-src 'none'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'"; } diff --git a/deployment/container/etc/supervisord.conf b/deployment/container/etc/supervisord.conf index 2ba0a52cb..166ef875b 100644 --- a/deployment/container/etc/supervisord.conf +++ b/deployment/container/etc/supervisord.conf @@ -42,7 +42,7 @@ stderr_logfile_backups=2 priority=10 [program:celery] -command=celery -A helfertool worker -c %(ENV_HELFERTOOL_TASK_WORKERS)s --pidfile=/helfertool/run/celery.pid +command=/helfertool/venv/bin/celery -A helfertool worker -c %(ENV_HELFERTOOL_TASK_WORKERS)s --pidfile=/helfertool/run/celery.pid directory=/helfertool/src autostart=true autorestart=true @@ -53,7 +53,7 @@ stderr_logfile_backups=2 priority=20 [program:celerybeat] -command=celery -A helfertool beat --schedule=/data/tmp/celerybeat-schedule --pidfile=/helfertool/run/celerybeat.pid +command=/helfertool/venv/bin/celery -A helfertool beat --schedule=/data/tmp/celerybeat-schedule --pidfile=/helfertool/run/celerybeat.pid directory=/helfertool/src autostart=true autorestart=true diff --git a/deployment/container/etc/uwsgi.conf b/deployment/container/etc/uwsgi.conf index 170ea63a7..c397b185f 100644 --- a/deployment/container/etc/uwsgi.conf +++ b/deployment/container/etc/uwsgi.conf @@ -1,7 +1,8 @@ [uwsgi] -plugin = python39 +plugin = python313 chdir = /helfertool/src +venv = /helfertool/venv wsgi-file = /helfertool/src/helfertool/wsgi.py socket = /helfertool/run/uwsgi.sock diff --git a/deployment/container/healthcheck.sh b/deployment/container/healthcheck.sh index 3a718a81c..23bb5e054 100644 --- a/deployment/container/healthcheck.sh +++ b/deployment/container/healthcheck.sh @@ -12,7 +12,7 @@ die () { # check webserver (with a host header that is allowed) cd /helfertool/src -host="$(python3 manage.py shell -c "from django.conf import settings ; print(settings.ALLOWED_HOSTS[0] if settings.ALLOWED_HOSTS else '')")" +host="$(/helfertool/venv/bin/python manage.py shell -c "from django.conf import settings ; print(settings.ALLOWED_HOSTS[0] if settings.ALLOWED_HOSTS else '')")" curl --fail --silent --output /dev/null -H "Host: $host" http://localhost:8000 || die "Error on HTTP query" # check supervisord diff --git a/deployment/container/helfertool.sh b/deployment/container/helfertool.sh index 6e829921b..841a28552 100644 --- a/deployment/container/helfertool.sh +++ b/deployment/container/helfertool.sh @@ -34,7 +34,7 @@ mkdir -p /data/media /data/tmp /helfertool/run/tmp # command: init if [ "$1" = "init" ] ; then # initialise database with default settings - python3 manage.py loaddata toolsettings + /helfertool/venv/bin/python manage.py loaddata toolsettings # command: reload elif [ "$1" = "reload" ] ; then @@ -54,7 +54,7 @@ elif [ "$1" = "postrotate" ] ; then # command: manage elif [ "$1" = "manage" ] ; then shift - python3 manage.py $@ + /helfertool/venv/bin/python manage.py $@ # command: run elif [ "$1" = "run" ] ; then @@ -82,8 +82,8 @@ elif [ "$1" = "run" ] ; then sed "s/will_be_replaced/$(pwgen 40 1)/g" /helfertool/etc/supervisord.conf > /helfertool/run/supervisord.conf # run migrations and go - python3 manage.py migrate --noinput - python3 manage.py createcachetable + /helfertool/venv/bin/python manage.py migrate --noinput + /helfertool/venv/bin/python manage.py createcachetable exec supervisord --nodaemon --configuration /helfertool/run/supervisord.conf # help message diff --git a/scripts/container.sh b/scripts/container.sh index 4412135f9..2caf5d56d 100755 --- a/scripts/container.sh +++ b/scripts/container.sh @@ -63,8 +63,9 @@ set -u # command: build if [ "$action" = "build" ] ; then # build without cache and with updated base image - container_version="$(date --utc --iso-8601=seconds)" + container_version="$(date -u -Iseconds)" podman build --no-cache --pull \ + --arch=amd64 \ --build-arg CONTAINER_VERSION="$container_version" \ --format docker \ -t "$container_name:$container_tag" . @@ -73,6 +74,7 @@ if [ "$action" = "build" ] ; then elif [ "$action" = "fastbuild" ] ; then # build with cache, as fast as possible podman build \ + --arch=amd64 \ --build-arg CONTAINER_VERSION="fastbuild" \ --format docker \ -t "$container_name:$container_tag" . diff --git a/src/account/forms/__init__.py b/src/account/forms/__init__.py index ffcaa743a..9a4cb6a26 100644 --- a/src/account/forms/__init__.py +++ b/src/account/forms/__init__.py @@ -1,3 +1,4 @@ from .account import CreateUserForm, EditUserForm, DeleteUserForm, MergeUserForm from .agreement import AgreementForm, UserAgreementForm from .delete import DeleteForm +from .password_reset import CustomPasswordResetForm, CustomSetPasswordForm diff --git a/src/account/forms/account.py b/src/account/forms/account.py index 03c083580..8c9bfe94a 100644 --- a/src/account/forms/account.py +++ b/src/account/forms/account.py @@ -3,7 +3,8 @@ from django.contrib.auth import get_user_model from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import Group -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ +from django.views.decorators.debug import sensitive_variables from django_select2.forms import Select2Widget @@ -15,6 +16,7 @@ from ..templatetags.globalpermissions import has_adduser_group, has_addevent_group, has_sendnews_group +import secrets import logging logger = logging.getLogger("helfertool.account") @@ -88,18 +90,44 @@ class Meta: ), } + no_password = forms.BooleanField( + label=_("Do not set password now"), + help_text=_("The user is notified via email and can set the password (via the password reset)"), + required=False, + ) + def __init__(self, *args, **kwargs): super(CreateUserForm, self).__init__(*args, **kwargs) + # compared to the django default, we need some data for f in ("email", "first_name", "last_name"): self.fields[f].required = True + # if no_password is set, the password is not required, see clean() for more validation + for f in ("password1", "password2"): + self.fields[f].required = False + + self.fields["no_password"].widget.attrs["onChange"] = "handle_password()" + + @sensitive_variables("password") def clean(self): # add LOCAL_USER_CHAR to the beginning char = settings.LOCAL_USER_CHAR if char and not self.cleaned_data.get("username").startswith(char): self.cleaned_data["username"] = char + self.cleaned_data.get("username") + # if no_password is set, set a randomly generated password + # otherwise, check if password is set + if self.cleaned_data["no_password"]: + password = secrets.token_urlsafe(100) + self.cleaned_data["password1"] = password + self.cleaned_data["password2"] = password + else: + if not self.cleaned_data["password1"]: + self.add_error("password1", _("Password is required")) + if not self.cleaned_data["password2"]: + self.add_error("password2", _("Password is required")) + return super(CreateUserForm, self).clean() @@ -113,9 +141,15 @@ def __init__(self, *args, **kwargs): super(EditUserForm, self).__init__(*args, **kwargs) - # set required fields + # set attributes for name/email fields + # if user is local, the fields are required + # if users is from external idp, the fields cannot be changed for f in ("first_name", "last_name", "email"): - self.fields[f].required = True + if self.instance.has_usable_password(): + self.fields[f].required = True + else: + self.fields[f].help_text = _("Managed by external identity provider") + self.fields[f].disabled = True # adjust labels of active and superuser flags self._active_initial = self.instance.is_active diff --git a/src/account/forms/agreement.py b/src/account/forms/agreement.py index 93b02a72c..37d3a2964 100644 --- a/src/account/forms/agreement.py +++ b/src/account/forms/agreement.py @@ -1,8 +1,6 @@ from django import forms from django.conf import settings -from django.utils.translation import ugettext_lazy as _ - -from ckeditor.widgets import CKEditorWidget +from django.utils.translation import gettext_lazy as _ from helfertool.forms import DatePicker @@ -20,13 +18,6 @@ class Meta: "end": DatePicker, } - # According to the documentation django-modeltranslations copies the - # widget from the original field. - # But when setting BLEACH_DEFAULT_WIDGET this does not happen. - # Therefore set it manually... - for lang, name in settings.LANGUAGES: - widgets["text_{}".format(lang)] = CKEditorWidget() - def __init__(self, *args, **kwargs): super(AgreementForm, self).__init__(*args, **kwargs) diff --git a/src/account/forms/password_reset.py b/src/account/forms/password_reset.py new file mode 100644 index 000000000..2da6366fe --- /dev/null +++ b/src/account/forms/password_reset.py @@ -0,0 +1,75 @@ +from django.conf import settings +from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm +from django.core.mail import EmailMessage +from django.template.loader import get_template + +from axes.helpers import get_client_ip_address + +from captcha.fields import CaptchaField +from helfertool.forms import CustomCaptchaTextInput + +import logging + +logger = logging.getLogger("helfertool.account") + + +class CustomPasswordResetForm(PasswordResetForm): + def __init__(self, *args, **kwargs): + super(CustomPasswordResetForm, self).__init__(*args, **kwargs) + + if not settings.CAPTCHAS_PASSWORD_RESET: + self.fields.pop("captcha") + + def save(self, *args, **kwargs): + super(CustomPasswordResetForm, self).save(*args, **kwargs) + + # log password reset attempt + email = self.cleaned_data["email"] + ip_address = get_client_ip_address(kwargs.get("request")) + logger.info( + "password resetattempt", + extra={ + "email": email, + "ip": ip_address, + }, + ) + + captcha = CaptchaField(widget=CustomCaptchaTextInput) + + +class CustomSetPasswordForm(SetPasswordForm): + def save(self, commit=True): + user = super(CustomSetPasswordForm, self).save(commit) + + # log password reset + logger.info( + "password reset", + extra={ + "changed_user": user.username, + }, + ) + + # sent confirmation mail to user + context = { + "firstname": user.first_name, + "page_title": settings.PAGE_TITLE, + "contact_mail": settings.CONTACT_MAIL, + } + subject_template = get_template("account/password_reset/completed_mail_subject.txt") + subject = subject_template.render(context).strip() + + text_template = get_template("account/password_reset/completed_mail.txt") + text = text_template.render(context) + + # sent it and handle errors + mail = EmailMessage( + subject, + text, + settings.EMAIL_SENDER_ADDRESS, + [user.email], # to + reply_to=[settings.EMAIL_SENDER_ADDRESS], + ) + + mail.send(fail_silently=True) + + return user diff --git a/src/account/locale/de/LC_MESSAGES/django.mo b/src/account/locale/de/LC_MESSAGES/django.mo index cd890dd7c..4364dc839 100644 Binary files a/src/account/locale/de/LC_MESSAGES/django.mo and b/src/account/locale/de/LC_MESSAGES/django.mo differ diff --git a/src/account/locale/de/LC_MESSAGES/django.po b/src/account/locale/de/LC_MESSAGES/django.po index dbd7d406d..7bcfe8db7 100644 --- a/src/account/locale/de/LC_MESSAGES/django.po +++ b/src/account/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-07-25 19:30+0200\n" +"POT-Creation-Date: 2026-01-25 15:26+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -16,46 +16,63 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 3.0.1\n" +"X-Generator: Poedit 3.8\n" -#: account/forms/account.py:125 account/forms/account.py:133 +#: account/forms/account.py:94 +msgid "Do not set password now" +msgstr "Passwort jetzt nicht setzen" + +#: account/forms/account.py:95 +msgid "" +"The user is notified via email and can set the password (via the password " +"reset)" +msgstr "" +"Benutzer:in wird per E-Mail benachrichtigt und kann das Passwort festlegen " +"(durch einen Passwort Reset)" + +#: account/forms/account.py:127 account/forms/account.py:129 +msgid "Password is required" +msgstr "Passwort ist erforderlich" + +#: account/forms/account.py:151 account/forms/account.py:159 +#: account/forms/account.py:167 msgid "Managed by external identity provider" msgstr "Durch externen Identity Provider verwaltet" -#: account/forms/account.py:129 account/templates/account/list_users.html:35 +#: account/forms/account.py:163 account/templates/account/list_users.html:35 #: account/templates/account/user_permissions.html:10 msgid "Administrator" msgstr "Administrator:in" -#: account/forms/account.py:139 account/templates/account/list_users.html:43 +#: account/forms/account.py:173 account/templates/account/list_users.html:43 #: account/templates/account/user_permissions.html:19 msgid "Add users" msgstr "Benutzer:innen hinzufügen" -#: account/forms/account.py:147 account/templates/account/list_users.html:39 +#: account/forms/account.py:181 account/templates/account/list_users.html:39 #: account/templates/account/user_permissions.html:14 msgid "Add events" msgstr "Veranstaltungen hinzufügen" -#: account/forms/account.py:156 account/templates/account/list_users.html:49 +#: account/forms/account.py:190 account/templates/account/list_users.html:49 #: account/templates/account/user_permissions.html:24 msgid "Send newsletter" msgstr "Newsletter versenden" -#: account/forms/account.py:243 +#: account/forms/account.py:277 msgid "Other user, which will be deleted" msgstr "Andere Benutzer:in, welche gelöscht wird" -#: account/forms/account.py:268 +#: account/forms/account.py:302 msgid "User does not exist" msgstr "Benutzer:in existiert nicht" -#: account/forms/agreement.py:35 +#: account/forms/agreement.py:26 #, python-format msgid "Text (%(lang)s)" msgstr "Text (%(lang)s)" -#: account/models/agreement.py:13 +#: account/models/agreement.py:15 #: account/templates/account/delete_agreement.html:11 #: account/templates/account/delete_user.html:15 #: account/templates/account/merge_user.html:16 @@ -63,21 +80,21 @@ msgstr "Text (%(lang)s)" msgid "Name" msgstr "Name" -#: account/models/agreement.py:17 +#: account/models/agreement.py:21 msgid "Text" msgstr "Text" -#: account/models/agreement.py:21 +#: account/models/agreement.py:25 #: account/templates/account/delete_agreement.html:15 msgid "Start date" msgstr "Startdatum" -#: account/models/agreement.py:25 +#: account/models/agreement.py:29 #: account/templates/account/delete_agreement.html:19 msgid "End date" msgstr "Enddatum" -#: account/models/agreement.py:32 +#: account/models/agreement.py:36 msgid "End date must be after start date." msgstr "Enddatum muss nach Startdatum liegen." @@ -85,10 +102,45 @@ msgstr "Enddatum muss nach Startdatum liegen." msgid "Add user" msgstr "Benutzer:in hinzufügen" -#: account/templates/account/add_user.html:16 +#: account/templates/account/add_user.html:22 msgid "Add" msgstr "Hinzufügen" +#: account/templates/account/add_user_mail.txt:1 +#, python-format +msgid "" +"Hello %(firstname)s,\n" +"\n" +"your user for %(page_title)s was created.\n" +"\n" +"Your username is: %(username)s\n" +"\n" +"You can set your password here by using the password reset: " +"%(password_reset_url)s\n" +"\n" +"If you did not expect this mail, please contact %(contact_mail)s.\n" +"\n" +"Best regards" +msgstr "" +"Hallo %(firstname)s,\n" +"\n" +"dein Benutzer für %(page_title)s wurde erstellt.\n" +"\n" +"Der Benutzername ist: %(username)s\n" +"\n" +"Du kannst dein Passwort hier setzen, indem du den Passwort Reset nutzt: " +"%(password_reset_url)s\n" +"\n" +"Wenn du diese E-Mail nicht erwartet hast, wende dich bitte an " +"%(contact_mail)s.\n" +"\n" +"Viele Grüße" + +#: account/templates/account/add_user_mail_subject.txt:3 +#, python-format +msgid "Your access to %(page_title)s" +msgstr "Dein Zugang zu %(page_title)s" + #: account/templates/account/delete_agreement.html:5 msgid "Delete user agreement" msgstr "Nutzungsvereinbarung löschen" @@ -116,6 +168,7 @@ msgstr "Account Daten" #: account/templates/account/delete_user.html:11 #: account/templates/account/merge_user.html:12 +#: account/templates/account/password_reset/completed.html:10 #: account/templates/account/view_user.html:15 msgid "Login" msgstr "Login" @@ -209,7 +262,7 @@ msgstr "Kein Filter" msgid "Disabled" msgstr "Deaktiviert" -#: account/templates/account/list_users.html:87 +#: account/templates/account/list_users.html:99 msgid "No users found." msgstr "Keine Benutzer:in gefunden." @@ -230,6 +283,109 @@ msgstr "Benutzer:in zur Löschung" msgid "Merge" msgstr "Zusammenfassen" +#: account/templates/account/password_reset/completed.html:5 +#: account/templates/account/password_reset/confirm.html:5 +#: account/templates/account/password_reset/form.html:5 +#: account/templates/account/password_reset/sent.html:5 +msgid "Password reset" +msgstr "Passwort zurücksetzen" + +#: account/templates/account/password_reset/completed.html:7 +msgid "Your password has been set." +msgstr "Dein Passwort wurde gesetzt." + +#: account/templates/account/password_reset/completed_mail.txt:1 +#, python-format +msgid "" +"Hello %(firstname)s,\n" +"\n" +"your password for %(page_title)s has been successfully reset.\n" +"\n" +"If you have not done this yourself, please contact %(contact_mail)s " +"immediately.\n" +"\n" +"Best regards" +msgstr "" +"Hallo %(firstname)s,\n" +"\n" +"dein Passwort für %(page_title)s wurde erfolgreich zurückgesetzt.\n" +"\n" +"Wenn du dies nicht selbst getan hast, wende dich bitte umgehend an " +"%(contact_mail)s.\n" +"\n" +"Viele Grüße" + +#: account/templates/account/password_reset/completed_mail_subject.txt:3 +#, python-format +msgid "Successful password reset for %(page_title)s" +msgstr "Erfolgreicher Passwort Reset für %(page_title)s" + +#: account/templates/account/password_reset/confirm.html:20 +msgid "Set password" +msgstr "Passwort setzen" + +#: account/templates/account/password_reset/confirm.html:23 +msgid "The password reset link was invalid." +msgstr "Der Link zum Zurücksetzen des Passworts war ungültig." + +#: account/templates/account/password_reset/confirm_mail.txt:2 +#, python-format +msgid "" +"Hello %(firstname)s,\n" +"\n" +"you're receiving this email because you requested a password reset for " +"%(page_title)s.\n" +"\n" +"Please set a new password on the following page:" +msgstr "" +"Hallo %(firstname)s,\n" +"\n" +"du erhältst diese E-Mail, weil du das Zurücksetzen des Passworts für " +"%(page_title)s angefordert hast.\n" +"\n" +"Bitte setze auf der folgenden Seite ein neues Passwort:" + +#: account/templates/account/password_reset/confirm_mail.txt:10 +#, python-format +msgid "" +"Your username is: %(username)s\n" +"\n" +"Best regards" +msgstr "" +"Dein Benutzername ist: %(username)s\n" +"\n" +"Viele Grüße" + +#: account/templates/account/password_reset/confirm_mail_subject.txt:4 +#, python-format +msgid "Password reset for %(page_title)s" +msgstr "Passwort zurücksetzen für %(page_title)s" + +#: account/templates/account/password_reset/form.html:7 +msgid "" +"Enter your email address, and you will receive email instructions for " +"setting a new password." +msgstr "" +"Gib deine E-Mail Adresse ein und du wirst die Anweisungen zum Setzen eines " +"neuen Passworts per E-Mail erhalten." + +#: account/templates/account/password_reset/form.html:12 +msgid "The password reset only works for local user accounts." +msgstr "Das Zurücksetzen des Passworts funktioniert nur für lokale Accounts." + +#: account/templates/account/password_reset/form.html:32 +msgid "Reset password" +msgstr "Passwort zurücksetzen" + +#: account/templates/account/password_reset/sent.html:7 +msgid "" +"We’ve emailed you instructions for setting your password, if an account " +"exists with the email you entered." +msgstr "" +"Wir haben dir eine E-Mail mit Anweisungen zum Setzen deines Passworts " +"geschickt, falls ein Account mit der von dir angegebenen E-Mail-Adresse " +"existiert." + #: account/templates/account/view_user.html:6 msgid "My account" msgstr "Mein Account" @@ -263,16 +419,24 @@ msgstr "Gestern" msgid "%(last_login_ago)s ago" msgstr "Vor %(last_login_ago)s" -#: account/views/account.py:35 +#: account/views/account.py:70 +msgid "" +"Failed to send the notification email. Please contact the user on your own." +msgstr "" +"Die E-Mail zur Benachrichtigung konnte nicht gesendet werden. Bitte " +"kontaktiere den Benutzer selbst." + +#: account/views/account.py:73 #, python-format msgid "Added user %(username)s" msgstr "Benutzer:in %(username)s hinzugefügt" -#: account/views/account.py:90 +#: account/views/account.py:129 msgid "Changed password successfully" msgstr "Das Passwort wurde erfolgreich geändert" -#: account/views/account.py:187 +#: account/views/account.py:226 +#, python-brace-format msgid "Merged user {} into {}" msgstr "Benutzer:in {} mit {} zusammengefasst" diff --git a/src/account/migrations/0001_initial.py b/src/account/migrations/0001_initial.py index 91f079a31..6cbbf641c 100644 --- a/src/account/migrations/0001_initial.py +++ b/src/account/migrations/0001_initial.py @@ -9,7 +9,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/src/account/migrations/0002_auto_20190202_2336.py b/src/account/migrations/0002_auto_20190202_2336.py index 9c0c1f713..dfcc94ba7 100644 --- a/src/account/migrations/0002_auto_20190202_2336.py +++ b/src/account/migrations/0002_auto_20190202_2336.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - dependencies = [ ("account", "0001_initial"), ] diff --git a/src/account/migrations/0003_auto_20190203_1239.py b/src/account/migrations/0003_auto_20190203_1239.py index 1436dd292..0d1169532 100644 --- a/src/account/migrations/0003_auto_20190203_1239.py +++ b/src/account/migrations/0003_auto_20190203_1239.py @@ -3,11 +3,10 @@ from __future__ import unicode_literals from django.db import migrations, models -import django.utils.datetime_safe +import datetime class Migration(migrations.Migration): - dependencies = [ ("account", "0002_auto_20190202_2336"), ] @@ -20,7 +19,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="agreement", name="start", - field=models.DateField(default=django.utils.datetime_safe.datetime.now, verbose_name="Start date"), + field=models.DateField(default=datetime.datetime.now, verbose_name="Start date"), preserve_default=False, ), ] diff --git a/src/account/migrations/0004_agreement_end.py b/src/account/migrations/0004_agreement_end.py index 66f2d5c43..1284eb485 100644 --- a/src/account/migrations/0004_agreement_end.py +++ b/src/account/migrations/0004_agreement_end.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - dependencies = [ ("account", "0003_auto_20190203_1239"), ] diff --git a/src/account/migrations/0005_auto_20210523_1533.py b/src/account/migrations/0005_auto_20210523_1533.py index 7ceac6a0d..6c0d427ae 100644 --- a/src/account/migrations/0005_auto_20210523_1533.py +++ b/src/account/migrations/0005_auto_20210523_1533.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("account", "0004_agreement_end"), ] diff --git a/src/account/migrations/0006_alter_agreement_text_alter_agreement_text_de_and_more.py b/src/account/migrations/0006_alter_agreement_text_alter_agreement_text_de_and_more.py new file mode 100644 index 000000000..32cf34cdb --- /dev/null +++ b/src/account/migrations/0006_alter_agreement_text_alter_agreement_text_de_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.9 on 2026-01-12 20:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("account", "0005_auto_20210523_1533"), + ] + + operations = [ + migrations.AlterField( + model_name="agreement", + name="text", + field=models.TextField(), + ), + migrations.AlterField( + model_name="agreement", + name="text_de", + field=models.TextField(null=True), + ), + migrations.AlterField( + model_name="agreement", + name="text_en", + field=models.TextField(null=True), + ), + ] diff --git a/src/account/migrations/0007_alter_agreement_text_alter_agreement_text_de_and_more.py b/src/account/migrations/0007_alter_agreement_text_alter_agreement_text_de_and_more.py new file mode 100644 index 000000000..2ce42eac7 --- /dev/null +++ b/src/account/migrations/0007_alter_agreement_text_alter_agreement_text_de_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.9 on 2026-01-12 21:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("account", "0006_alter_agreement_text_alter_agreement_text_de_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="agreement", + name="text", + field=models.TextField(verbose_name="Text"), + ), + migrations.AlterField( + model_name="agreement", + name="text_de", + field=models.TextField(null=True, verbose_name="Text"), + ), + migrations.AlterField( + model_name="agreement", + name="text_en", + field=models.TextField(null=True, verbose_name="Text"), + ), + ] diff --git a/src/account/models/agreement.py b/src/account/models/agreement.py index b4593d37b..9cf10c96f 100644 --- a/src/account/models/agreement.py +++ b/src/account/models/agreement.py @@ -1,8 +1,10 @@ from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django_bleach.models import BleachField +from django.utils.translation import gettext_lazy as _ + +from django_prose_editor.fields import ProseEditorField +from helfertool.utils import PROSE_EDITOR_DEFAULT_EXTENSIONS import datetime @@ -13,7 +15,9 @@ class Agreement(models.Model): verbose_name=_("Name"), ) - text = BleachField( + text = ProseEditorField( + extensions=PROSE_EDITOR_DEFAULT_EXTENSIONS, + sanitize=True, verbose_name=_("Text"), ) diff --git a/src/account/static/account/js/add_user.js b/src/account/static/account/js/add_user.js new file mode 100644 index 000000000..9d95b5699 --- /dev/null +++ b/src/account/static/account/js/add_user.js @@ -0,0 +1,20 @@ +function handle_password() +{ + var $no_password = $("#id_no_password").is(':checked'); + + if($no_password) { + $("#id_password1").removeAttr('required'); + $("#id_password2").removeAttr('required'); + + $("#id_password1").parent().hide() + $("#id_password2").parent().hide() + } else { + $("#id_password1").attr('required', ''); + $("#id_password2").attr('required', ''); + + $("#id_password1").parent().show() + $("#id_password2").parent().show() + } +} + +handle_password(); diff --git a/src/account/templates/account/add_user.html b/src/account/templates/account/add_user.html index 1b6840b42..5ced22246 100644 --- a/src/account/templates/account/add_user.html +++ b/src/account/templates/account/add_user.html @@ -1,5 +1,5 @@ {% extends "helfertool/admin.html" %} -{% load i18n django_bootstrap5 icons toolsettings %} +{% load i18n django_bootstrap5 icons toolsettings static %} {% block content %}
{% trans "No users found." %}
{% endif %} diff --git a/src/account/templates/account/password_reset/completed.html b/src/account/templates/account/password_reset/completed.html new file mode 100644 index 000000000..c498acfca --- /dev/null +++ b/src/account/templates/account/password_reset/completed.html @@ -0,0 +1,12 @@ +{% extends "helfertool/base.html" %} +{% load i18n django_bootstrap5 icons toolsettings %} + +{% block content %} +{% translate "Your password has been set." %}
+ + + {% icon "sign-in-alt" %} {% trans "Login" %} + +{% endblock %} diff --git a/src/account/templates/account/password_reset/completed_mail.txt b/src/account/templates/account/password_reset/completed_mail.txt new file mode 100644 index 000000000..eed682811 --- /dev/null +++ b/src/account/templates/account/password_reset/completed_mail.txt @@ -0,0 +1,8 @@ +{% load i18n %}{% autoescape off %}{% blocktranslate %}Hello {{ firstname }}, + +your password for {{ page_title }} has been successfully reset. + +If you have not done this yourself, please contact {{ contact_mail }} immediately. + +Best regards{% endblocktranslate %} +{% endautoescape %} diff --git a/src/account/templates/account/password_reset/completed_mail_subject.txt b/src/account/templates/account/password_reset/completed_mail_subject.txt new file mode 100644 index 000000000..3d97cb70a --- /dev/null +++ b/src/account/templates/account/password_reset/completed_mail_subject.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% autoescape off %} +{% blocktrans %}Successful password reset for {{ page_title }}{% endblocktrans %} +{% endautoescape %} diff --git a/src/account/templates/account/password_reset/confirm.html b/src/account/templates/account/password_reset/confirm.html new file mode 100644 index 000000000..33307d96e --- /dev/null +++ b/src/account/templates/account/password_reset/confirm.html @@ -0,0 +1,25 @@ +{% extends "helfertool/base.html" %} +{% load i18n django_bootstrap5 icons toolsettings %} + +{% block content %} +{% translate "The password reset link was invalid." %}
+ {% endif %} +{% endblock %} diff --git a/src/account/templates/account/password_reset/confirm_mail.txt b/src/account/templates/account/password_reset/confirm_mail.txt new file mode 100644 index 000000000..fe0c07226 --- /dev/null +++ b/src/account/templates/account/password_reset/confirm_mail.txt @@ -0,0 +1,13 @@ +{% load i18n toolsettings %}{% djangosetting "PAGE_TITLE" as page_title %}{% autoescape off %} +{% blocktranslate with firstname=user.first_name %}Hello {{ firstname }}, + +you're receiving this email because you requested a password reset for {{ page_title }}. + +Please set a new password on the following page:{% endblocktranslate %} + +{{ protocol }}://{{ domain }}{% url 'account:password_reset_confirm' uidb64=uid token=token %} + +{% blocktranslate with username=user.get_username %}Your username is: {{ username }} + +Best regards{% endblocktranslate %} +{% endautoescape %} diff --git a/src/account/templates/account/password_reset/confirm_mail_subject.txt b/src/account/templates/account/password_reset/confirm_mail_subject.txt new file mode 100644 index 000000000..909b117b9 --- /dev/null +++ b/src/account/templates/account/password_reset/confirm_mail_subject.txt @@ -0,0 +1,5 @@ +{% load i18n toolsettings %} +{% djangosetting "PAGE_TITLE" as page_title %} +{% autoescape off %} +{% blocktrans with page_title=page_title %}Password reset for {{ page_title }}{% endblocktrans %} +{% endautoescape %} diff --git a/src/account/templates/account/password_reset/form.html b/src/account/templates/account/password_reset/form.html new file mode 100644 index 000000000..378379486 --- /dev/null +++ b/src/account/templates/account/password_reset/form.html @@ -0,0 +1,34 @@ +{% extends "helfertool/base.html" %} +{% load i18n django_bootstrap5 icons toolsettings %} + +{% block content %} +{% translate "Enter your email address, and you will receive email instructions for setting a new password." %}
+ + {% djangosetting "OIDC_CUSTOM_PROVIDER_NAME" as OIDC_CUSTOM_PROVIDER_NAME %} + {% djangosetting "AUTH_LDAP_SERVER_URI" as AUTH_LDAP_SERVER_URI %} + {% if OIDC_CUSTOM_PROVIDER_NAME or AUTH_LDAP_SERVER_URI %} +{% translate "The password reset only works for local user accounts." %}
+ {%endif %} + + +{% endblock %} diff --git a/src/account/templates/account/password_reset/sent.html b/src/account/templates/account/password_reset/sent.html new file mode 100644 index 000000000..8b8ff3956 --- /dev/null +++ b/src/account/templates/account/password_reset/sent.html @@ -0,0 +1,8 @@ +{% extends "helfertool/base.html" %} +{% load i18n django_bootstrap5 icons toolsettings %} + +{% block content %} +{% translate "We’ve emailed you instructions for setting your password, if an account exists with the email you entered." %}
+{% endblock %} diff --git a/src/account/templatetags/lastlogin.py b/src/account/templatetags/lastlogin.py index e585a7702..34ce6169d 100644 --- a/src/account/templatetags/lastlogin.py +++ b/src/account/templatetags/lastlogin.py @@ -1,6 +1,6 @@ from django import template from django.utils.timesince import timesince -from django.utils.timezone import is_aware, utc +from django.utils.timezone import is_aware from django.utils.translation import gettext_lazy as _ import datetime @@ -15,7 +15,7 @@ def lastlogin(user): return _("Never") login_date = _to_date(user.last_login) - now_date = _to_date(datetime.datetime.now(utc if is_aware(login_date) else None)) + now_date = _to_date(datetime.datetime.now(datetime.timezone.utc if is_aware(login_date) else None)) if login_date == now_date: return _("Today") diff --git a/src/account/urls.py b/src/account/urls.py index 1308b63b5..ec1300bec 100644 --- a/src/account/urls.py +++ b/src/account/urls.py @@ -1,22 +1,59 @@ -from django.conf.urls import url +from django.contrib.auth import views as auth_views +from django.urls import path, reverse_lazy from . import views +from .forms import CustomPasswordResetForm, CustomSetPasswordForm app_name = "account" urlpatterns = [ + # password reset + path( + "reset/", + auth_views.PasswordResetView.as_view( + form_class=CustomPasswordResetForm, + template_name="account/password_reset/form.html", + success_url=reverse_lazy("account:password_reset_sent"), + email_template_name="account/password_reset/confirm_mail.txt", + subject_template_name="account/password_reset/confirm_mail_subject.txt", + ), + name="password_reset", + ), + path( + "reset/sent/", + auth_views.PasswordResetDoneView.as_view( + template_name="account/password_reset/sent.html", + ), + name="password_reset_sent", + ), + path( + "reset/{% trans "No events found." %}
+ {% endif %} +{% endblock %} diff --git a/src/adminautomation/templates/adminautomation/mail/event_archive_automation.txt b/src/adminautomation/templates/adminautomation/mail/event_archive_automation.txt new file mode 100644 index 000000000..ca07046f2 --- /dev/null +++ b/src/adminautomation/templates/adminautomation/mail/event_archive_automation.txt @@ -0,0 +1,11 @@ +{% load i18n %}{% blocktrans with eventname=event.name|safe %}Hello, + +the event {{ eventname }} has meanwhile taken place. Therefore, the personal data must be deleted. + +Please archive the event now (Edit event -> Archive event). + +This will delete all personal data. The remaining data will be retained and can be reused for future events. The number of registered people will also be saved.{% endblocktrans %} +{% if docs %} +{% trans "Further information:" %} {{ docs|safe }} +{% endif %} +{% trans "Thank you!" %} diff --git a/src/adminautomation/templates/adminautomation/mail/event_archive_automation_subject.txt b/src/adminautomation/templates/adminautomation/mail/event_archive_automation_subject.txt new file mode 100644 index 000000000..36423c96a --- /dev/null +++ b/src/adminautomation/templates/adminautomation/mail/event_archive_automation_subject.txt @@ -0,0 +1 @@ +{% load i18n %}{% blocktrans with eventname=event.name|safe page_title=page_title|safe %}Deletion of personal data for {{ eventname }} ({{ page_title }}){% endblocktrans %} diff --git a/src/adminautomation/tests.py b/src/adminautomation/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/src/adminautomation/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/adminautomation/urls.py b/src/adminautomation/urls.py new file mode 100644 index 000000000..7fc2da93e --- /dev/null +++ b/src/adminautomation/urls.py @@ -0,0 +1,13 @@ +from django.urls import path + +from . import views + +app_name = "adminautomation" +urlpatterns = [ + path("manage/archivestatus/", views.event_archive_status, name="event_archive_status"), + path( + "{% trans "You can export names, contact information and addresses of all helpers here. The export does not contain special badges." %}
- -{% trans "Please use this data responsibly." %}
- - - {% icon "download" %} - {% trans "Export" %} - -{% trans "No data missing." %}
- {% endif %} -{% endblock %} diff --git a/src/corona/templates/corona/not_active.html b/src/corona/templates/corona/not_active.html deleted file mode 100644 index 769b9ae51..000000000 --- a/src/corona/templates/corona/not_active.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "helfertool/admin.html" %} -{% load i18n django_bootstrap5 %} - -{% block content %} -- {% blocktrans trimmed %} - At this event the 2G regulation is applied. - This means that you must be either vaccinated or recovered to have access. - The proof will be checked at the entrance and access without it is not possible. - {% endblocktrans %} -
- -{% trans "A negative test is not sufficient!" %}
- {% elif event.corona_settings.rules == "2Gplus" %} -- {% blocktrans trimmed %} - At this event the 2G plus regulation is applied. - This means that you must be either vaccinated or recovered and provide a negative antigen rapid test to have access. - The proof will be checked at the entrance and access without it is not possible. - {% endblocktrans %} -
- {% elif event.corona_settings.rules == "3G" %} -- {% blocktrans trimmed %} - At this event the 3G regulation is applied. - This means that you must be either vaccinated, recovered or provide a negative official test to have access. - The proof will be checked at the entrance and access without it is not possible. - {% endblocktrans %} -
- {% elif event.corona_settings.rules == "3Gplus" %} -- {% blocktrans trimmed %} - At this event the 3G plus regulation is applied. - This means that you must be either vaccinated, recovered or provide a negative PCR test to have access. - The proof will be checked at the entrance and access without it is not possible. - {% endblocktrans %} -
- -{% trans "A negative antigen rapid test is not sufficient!" %}
- {% endif %} - -{% trans "We are required to collect address and contact information for contact tracking purposes." %}
- - {% include "corona/partials/dataform.html" with form=form %} -| {% trans "Name" %}: | -{{ helper.firstname }} {{ helper.surname }} | -
|---|---|
| {% trans "E-Mail" %}: | -{{ helper.email }} | -
| {% trans "Mobile phone" %}: | -{{ helper.phone }} | -
|
- {% trans "Address" %}: - - {% icon "pencil-alt" %} {% trans "Edit" %} - - |
-
- {{ data.street }} - {{ data.zip }} {{ data.city }} - {{ data.country.name }} - |
-
- - {% icon "plus" %} {% trans "Add data" %} - -
- -