diff --git a/jdav_web/jdav_web/settings/components/jet.py b/jdav_web/jdav_web/settings/components/jet.py index 96c77bd..110e110 100644 --- a/jdav_web/jdav_web/settings/components/jet.py +++ b/jdav_web/jdav_web/settings/components/jet.py @@ -5,9 +5,10 @@ JET_DEFAULT_THEME = 'jdav-green' JET_CHANGE_FORM_SIBLING_LINKS = False JET_SIDE_MENU_ITEMS = [ - {'app_label': 'auth', 'permissions': ['auth'], 'items': [ + {'app_label': 'logindata', 'permissions': ['auth'], 'items': [ {'name': 'authgroup', 'permissions': ['auth.group'] }, {'name': 'logindatum', 'permissions': ['auth.user']}, + {'name': 'registrationpassword', 'permissions': ['auth.user']}, ]}, {'app_label': 'django_celery_beat', 'permissions': ['django_celery_beat'], 'items': [ {'name': 'crontabschedule'}, diff --git a/jdav_web/jdav_web/settings/components/texts.py b/jdav_web/jdav_web/settings/components/texts.py index 5116c8f..6371e57 100644 --- a/jdav_web/jdav_web/settings/components/texts.py +++ b/jdav_web/jdav_web/settings/components/texts.py @@ -140,3 +140,18 @@ verschickt. Wenn Du in Zukunft keine Emails mehr erhalten möchtest, kannst Du hier den Newsletter deabonnieren: {link}""" % { 'SEKTION': SEKTION } + + +INVITE_AS_USER_TEXT = """Hallo {name}, + +du bist Jugendleiter:in in der Sektion %(SEKTION)s. Die Verwaltung unserer Jugendgruppen, +Ausfahrten und Finanzen erfolgt in unserer Online Plattform Kompass. Deine Stammdaten sind +dort bereits hinterlegt. Damit du dich auch anmelden kannst, folge bitte dem folgenden Link +und wähle ein Passwort. + +{link} + +Bei Fragen, wende dich gerne an %(RESPONSIBLE_MAIL)s. + +Viele Grüße +Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION, 'RESPONSIBLE_MAIL': RESPONSIBLE_MAIL } diff --git a/jdav_web/jdav_web/urls.py b/jdav_web/jdav_web/urls.py index 4c5f283..e78056a 100644 --- a/jdav_web/jdav_web/urls.py +++ b/jdav_web/jdav_web/urls.py @@ -27,11 +27,12 @@ admin.site.index_title = _('Startpage') admin.site.site_header = 'Kompass' urlpatterns += i18n_patterns( - re_path(r'^kompass/?', admin.site.urls), + re_path(r'^kompass/?', admin.site.urls, name='kompass'), re_path(r'^jet/', include('jet.urls', 'jet')), # Django JET URLS re_path(r'^admin/?', RedirectView.as_view(url='/kompass')), re_path(r'^newsletter/', include('mailer.urls', namespace="mailer")), re_path(r'^members/', include('members.urls', namespace="members")), + re_path(r'^login/', include('logindata.urls', namespace="logindata")), re_path(r'^LBAlpin/Programm(/)?(20)?[0-9]{0,2}', include('ludwigsburgalpin.urls', namespace="ludwigsburgalpin")), re_path(r'^_nested_admin/', include('nested_admin.urls')), diff --git a/jdav_web/logindata/admin.py b/jdav_web/logindata/admin.py index be22d5c..377e974 100644 --- a/jdav_web/logindata/admin.py +++ b/jdav_web/logindata/admin.py @@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin, GroupAdmin as BaseAuthGroupAdmin from django.contrib.auth.models import User as BaseUser, Group as BaseAuthGroup -from .models import AuthGroup, LoginDatum +from .models import AuthGroup, LoginDatum, RegistrationPassword from members.models import Member # Register your models here. @@ -40,7 +40,7 @@ class LoginDatumAdmin(BaseUserAdmin): None, { "classes": ("wide",), - "fields": ("username", "usable_password", "password1", "password2"), + "fields": ("username", "password1", "password2"), }, ), ) @@ -49,3 +49,4 @@ admin.site.unregister(BaseUser) admin.site.unregister(BaseAuthGroup) admin.site.register(LoginDatum, LoginDatumAdmin) admin.site.register(AuthGroup, AuthGroupAdmin) +admin.site.register(RegistrationPassword) diff --git a/jdav_web/logindata/apps.py b/jdav_web/logindata/apps.py index bb1c250..b1a5370 100644 --- a/jdav_web/logindata/apps.py +++ b/jdav_web/logindata/apps.py @@ -1,6 +1,8 @@ from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ class LoginDataConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'logindata' + verbose_name = _('Authentication') diff --git a/jdav_web/logindata/migrations/0001_initial.py b/jdav_web/logindata/migrations/0001_initial.py new file mode 100644 index 0000000..0a26b33 --- /dev/null +++ b/jdav_web/logindata/migrations/0001_initial.py @@ -0,0 +1,55 @@ +# Generated by Django 4.0.1 on 2024-11-23 21:15 + +import django.contrib.auth.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='RegistrationPassword', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=100, verbose_name='Password')), + ], + ), + migrations.CreateModel( + name='AuthGroup', + fields=[ + ], + options={ + 'verbose_name': 'Permission group', + 'verbose_name_plural': 'Permission groups', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('auth.group',), + managers=[ + ('objects', django.contrib.auth.models.GroupManager()), + ], + ), + migrations.CreateModel( + name='LoginDatum', + fields=[ + ], + options={ + 'verbose_name': 'Login Datum', + 'verbose_name_plural': 'Login Data', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('auth.user',), + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/jdav_web/logindata/models.py b/jdav_web/logindata/models.py index af8cfcd..c86deaa 100644 --- a/jdav_web/logindata/models.py +++ b/jdav_web/logindata/models.py @@ -9,7 +9,6 @@ class AuthGroup(BaseAuthGroup): proxy = True verbose_name = _('Permission group') verbose_name_plural = _('Permission groups') - app_label = "auth" class LoginDatum(BaseUser): @@ -17,4 +16,31 @@ class LoginDatum(BaseUser): proxy = True verbose_name = _('Login Datum') verbose_name_plural = _('Login Data') - app_label = "auth" + + +class RegistrationPassword(models.Model): + """ + A password that can be used to register after inviting a member. + """ + password = models.CharField(max_length=100, verbose_name=_('Password')) + + def __str__(self): + return self.password + + class Meta: + verbose_name = _('Active registration password') + verbose_name_plural = _('Active registration passwords') + +def initial_user_setup(user, member): + try: + standard_group = AuthGroup.objects.get(name='Standard') + except AuthGroup.DoesNotExist: + return False + + user.is_staff = True + user.save() + user.groups.add(standard_group) + member.user = user + member.invite_as_user_key = '' + member.save() + return True diff --git a/jdav_web/logindata/templates/logindata/register_failed.html b/jdav_web/logindata/templates/logindata/register_failed.html new file mode 100644 index 0000000..433815a --- /dev/null +++ b/jdav_web/logindata/templates/logindata/register_failed.html @@ -0,0 +1,17 @@ +{% extends "members/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %} +{% trans "Registration" %} +{% endblock %} + +{% block content %} + +
{% trans "Something went wrong. The registration key is invalid or has expired." %}
+ +{% trans "If you think this is a mistake, please" %} {% trans "contact us." %}
+ +{% endblock %} diff --git a/jdav_web/logindata/templates/logindata/register_form.html b/jdav_web/logindata/templates/logindata/register_form.html new file mode 100644 index 0000000..f917961 --- /dev/null +++ b/jdav_web/logindata/templates/logindata/register_form.html @@ -0,0 +1,33 @@ +{% extends "members/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %} +{% trans "Register" %} +{% endblock %} + +{% block content %} + + + +{% trans "Welcome, " %} {{ member.prename }}. +{% blocktrans %}To set your personal login data, please enter the password that you received.{% endblocktrans %}
+ +{% if error_message %} +{{ error_message }}
+{% endif %} + + + +{% endblock %} diff --git a/jdav_web/logindata/templates/logindata/register_password.html b/jdav_web/logindata/templates/logindata/register_password.html new file mode 100644 index 0000000..df26e47 --- /dev/null +++ b/jdav_web/logindata/templates/logindata/register_password.html @@ -0,0 +1,26 @@ +{% extends "members/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %} +{% trans "Register" %} +{% endblock %} + +{% block content %} + +{% trans "Welcome, " %} {{ member.prename }}. {% blocktrans %}To set your personal login data for Kompass, please enter the password that you received.{% endblocktrans%}
+ +{% if error_message %} +{{ error_message }}
+{% endif %} + + + +{% endblock %} diff --git a/jdav_web/logindata/templates/logindata/register_success.html b/jdav_web/logindata/templates/logindata/register_success.html new file mode 100644 index 0000000..b357fe4 --- /dev/null +++ b/jdav_web/logindata/templates/logindata/register_success.html @@ -0,0 +1,15 @@ +{% extends "members/base.html" %} +{% load i18n %} + +{% block title %} +{% trans "Registration successful" %} +{% endblock %} + +{% block content %} + +{% blocktrans %}You successfully set your login data. You can now proceed to{% endblocktrans%} +login.
+ +{% endblock %} diff --git a/jdav_web/logindata/urls.py b/jdav_web/logindata/urls.py new file mode 100644 index 0000000..80d08b5 --- /dev/null +++ b/jdav_web/logindata/urls.py @@ -0,0 +1,8 @@ +from django.urls import re_path + +from . import views + +app_name = "logindata" +urlpatterns = [ + re_path(r'^register', views.register , name='register'), +] diff --git a/jdav_web/logindata/views.py b/jdav_web/logindata/views.py index 91ea44a..4415e09 100644 --- a/jdav_web/logindata/views.py +++ b/jdav_web/logindata/views.py @@ -1,3 +1,76 @@ +from django import forms from django.shortcuts import render +from django.http import HttpResponseRedirect +from django.utils.translation import gettext_lazy as _ +from django.urls import reverse +from django.contrib.auth.forms import UserCreationForm +from members.models import Member +from .models import initial_user_setup, RegistrationPassword + + +def render_register_password(request, key, member, error_message=''): + return render(request, 'logindata/register_password.html', + context={'key': key, + 'member': member, + 'error_message': error_message}) + + +def render_register_failed(request): + return render(request, 'logindata/register_failed.html') + + +def render_register_form(request, key, password, member, form): + return render(request, 'logindata/register_form.html', + context={'key': key, + 'password': password, + 'member': member, + 'form': form}) + + +def render_register_success(request): + return render(request, 'logindata/register_success.html') + # Create your views here. +def register(request): + if request.method == 'GET' and 'key' not in request.GET: + return HttpResponseRedirect(reverse('startpage:index')) + if request.method == 'POST' and 'key' not in request.POST: + return HttpResponseRedirect(reverse('startpage:index')) + + key = request.GET['key'] if request.method == 'GET' else request.POST['key'] + if not key: + return render_register_failed(request) + try: + member = Member.objects.get(invite_as_user_key=key) + except (Member.DoesNotExist, Member.MultipleObjectsReturned): + return render_register_failed(request) + + if request.method == 'GET': + return render_register_password(request, request.GET['key'], member) + + if 'password' not in request.POST: + return render_register_failed(request) + + password = request.POST['password'] + + # check if the entered password is one of the active registration passwords + if RegistrationPassword.objects.filter(password=password).count() == 0: + return render_register_password(request, key, member, error_message=_('You entered a wrong password.')) + + if "save" in request.POST: + form = UserCreationForm(request.POST) + if not form.is_valid(): + # form is invalid, reprint form with (automatic) error messages + return render_register_form(request, key, password, member, form) + user = form.save(commit=False) + success = initial_user_setup(user, member) + if success: + return render_register_success(request) + else: + return render_register_failed(request) + else: + prefill = { + 'username': '{prename}.{lastname}'.format(prename=member.prename.lower(), lastname=member.lastname.lower()) } + form = UserCreationForm(initial=prefill) + return render_register_form(request, key, password, member, form) diff --git a/jdav_web/mailer/mailutils.py b/jdav_web/mailer/mailutils.py index af2f38f..701de61 100644 --- a/jdav_web/mailer/mailutils.py +++ b/jdav_web/mailer/mailutils.py @@ -81,5 +81,9 @@ def get_mail_confirmation_link(key): return prepend_base_url("/members/mail/confirm?key={}".format(key)) +def get_invite_as_user_key(key): + return prepend_base_url("/login/register?key={}".format(key)) + + def prepend_base_url(absolutelink): return "{protocol}://{base}{link}".format(protocol=settings.PROTOCOL, base=settings.BASE_URL, link=absolutelink) diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index 63f08e7..4423481 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -214,7 +214,7 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): #} change_form_template = "members/change_member.html" ordering = ('lastname',) - actions = ['send_mail_to', 'request_echo'] + actions = ['request_echo', 'invite_as_user'] list_per_page = 25 sensitive_fields = ['iban', 'registration_form', 'comments'] @@ -234,6 +234,24 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): 'has_free_ticket_gym': 'members.may_change_organizationals', } + def get_urls(self): + urls = super().get_urls() + + def wrap(view): + def wrapper(*args, **kwargs): + return self.admin_site.admin_view(view)(*args, **kwargs) + + wrapper.model_admin = self + return update_wrapper(wrapper, view) + + custom_urls = [ + path( + "