members: invite member as user

pull/73/head
Christian Merten 1 year ago
parent a49aab51b1
commit e178f56369
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

@ -5,9 +5,10 @@ JET_DEFAULT_THEME = 'jdav-green'
JET_CHANGE_FORM_SIBLING_LINKS = False JET_CHANGE_FORM_SIBLING_LINKS = False
JET_SIDE_MENU_ITEMS = [ JET_SIDE_MENU_ITEMS = [
{'app_label': 'auth', 'permissions': ['auth'], 'items': [ {'app_label': 'logindata', 'permissions': ['auth'], 'items': [
{'name': 'authgroup', 'permissions': ['auth.group'] }, {'name': 'authgroup', 'permissions': ['auth.group'] },
{'name': 'logindatum', 'permissions': ['auth.user']}, {'name': 'logindatum', 'permissions': ['auth.user']},
{'name': 'registrationpassword', 'permissions': ['auth.user']},
]}, ]},
{'app_label': 'django_celery_beat', 'permissions': ['django_celery_beat'], 'items': [ {'app_label': 'django_celery_beat', 'permissions': ['django_celery_beat'], 'items': [
{'name': 'crontabschedule'}, {'name': 'crontabschedule'},

@ -140,3 +140,18 @@ verschickt. Wenn Du in Zukunft keine Emails mehr erhalten möchtest,
kannst Du hier den Newsletter deabonnieren: kannst Du hier den Newsletter deabonnieren:
{link}""" % { 'SEKTION': SEKTION } {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 }

@ -27,11 +27,12 @@ admin.site.index_title = _('Startpage')
admin.site.site_header = 'Kompass' admin.site.site_header = 'Kompass'
urlpatterns += i18n_patterns( 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'^jet/', include('jet.urls', 'jet')), # Django JET URLS
re_path(r'^admin/?', RedirectView.as_view(url='/kompass')), re_path(r'^admin/?', RedirectView.as_view(url='/kompass')),
re_path(r'^newsletter/', include('mailer.urls', namespace="mailer")), re_path(r'^newsletter/', include('mailer.urls', namespace="mailer")),
re_path(r'^members/', include('members.urls', namespace="members")), 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', re_path(r'^LBAlpin/Programm(/)?(20)?[0-9]{0,2}', include('ludwigsburgalpin.urls',
namespace="ludwigsburgalpin")), namespace="ludwigsburgalpin")),
re_path(r'^_nested_admin/', include('nested_admin.urls')), re_path(r'^_nested_admin/', include('nested_admin.urls')),

@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin, GroupAdmin as BaseAuthGroupAdmin 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 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 from members.models import Member
# Register your models here. # Register your models here.
@ -40,7 +40,7 @@ class LoginDatumAdmin(BaseUserAdmin):
None, None,
{ {
"classes": ("wide",), "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.unregister(BaseAuthGroup)
admin.site.register(LoginDatum, LoginDatumAdmin) admin.site.register(LoginDatum, LoginDatumAdmin)
admin.site.register(AuthGroup, AuthGroupAdmin) admin.site.register(AuthGroup, AuthGroupAdmin)
admin.site.register(RegistrationPassword)

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

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

@ -9,7 +9,6 @@ class AuthGroup(BaseAuthGroup):
proxy = True proxy = True
verbose_name = _('Permission group') verbose_name = _('Permission group')
verbose_name_plural = _('Permission groups') verbose_name_plural = _('Permission groups')
app_label = "auth"
class LoginDatum(BaseUser): class LoginDatum(BaseUser):
@ -17,4 +16,31 @@ class LoginDatum(BaseUser):
proxy = True proxy = True
verbose_name = _('Login Datum') verbose_name = _('Login Datum')
verbose_name_plural = _('Login Data') 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

@ -0,0 +1,17 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Registration" %}
{% endblock %}
{% block content %}
<h1>{% trans "Set login data" %}</h1>
<p>{% trans "Something went wrong. The registration key is invalid or has expired." %}</p>
<p>{% trans "If you think this is a mistake, please" %} <a href="mailto:jugendreferent@jdav-ludwigsburg.de">{% trans "contact us." %}</a></p>
{% endblock %}

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

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

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

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

@ -1,3 +1,76 @@
from django import forms
from django.shortcuts import render 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. # 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)

@ -81,5 +81,9 @@ def get_mail_confirmation_link(key):
return prepend_base_url("/members/mail/confirm?key={}".format(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): def prepend_base_url(absolutelink):
return "{protocol}://{base}{link}".format(protocol=settings.PROTOCOL, base=settings.BASE_URL, link=absolutelink) return "{protocol}://{base}{link}".format(protocol=settings.PROTOCOL, base=settings.BASE_URL, link=absolutelink)

@ -214,7 +214,7 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
#} #}
change_form_template = "members/change_member.html" change_form_template = "members/change_member.html"
ordering = ('lastname',) ordering = ('lastname',)
actions = ['send_mail_to', 'request_echo'] actions = ['request_echo', 'invite_as_user']
list_per_page = 25 list_per_page = 25
sensitive_fields = ['iban', 'registration_form', 'comments'] sensitive_fields = ['iban', 'registration_form', 'comments']
@ -234,6 +234,24 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
'has_free_ticket_gym': 'members.may_change_organizationals', '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(
"<path:object_id>/inviteasuser/", wrap(self.invite_as_user_view),
name="%s_%s_inviteasuser" % (self.opts.app_label, self.opts.model_name),
),
]
return custom_urls + urls
def get_queryset(self, request): def get_queryset(self, request):
queryset = super().get_queryset(request) queryset = super().get_queryset(request)
return annotate_activity_score(queryset.prefetch_related('group')) return annotate_activity_score(queryset.prefetch_related('group'))
@ -263,9 +281,28 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
continue continue
member.send_mail(_("Echo required"), member.send_mail(_("Echo required"),
settings.ECHO_TEXT.format(name=member.prename, link=get_echo_link(member))) settings.ECHO_TEXT.format(name=member.prename, link=get_echo_link(member)))
messages.success(request, _("Successfully requested echo from selected members.")) messages.success(request, _("Successfully requested echo from selected members."))
request_echo.short_description = _('Request echo from selected members') request_echo.short_description = _('Request echo from selected members')
def invite_as_user(self, request, queryset):
for member in queryset:
member.invite_as_user()
if queryset.count() == 1:
messages.success(request, _('Successfully invited %(name)s as user.') % {'name': queryset[0].name})
else:
messages.success(request, _('Successfully invited selected members to join as users.'))
invite_as_user.short_description = _('Invite selected members to join Kompass as users.')
def invite_as_user_view(self, request, object_id):
try:
m = Member.objects.get(pk=object_id)
except Member.DoesNotExist:
messages.error(request, _("Member not found."))
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
self.invite_as_user(request, Member.objects.filter(pk=object_id))
return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name),
args=(object_id,)))
def activity_score(self, obj): def activity_score(self, obj):
score = obj._activity_score score = obj._activity_score
# show 1 to 5 climbers based on activity in last year # show 1 to 5 climbers based on activity in last year

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

@ -15,7 +15,7 @@ from utils import RestrictedFileField
import os import os
from mailer.mailutils import send as send_mail, get_mail_confirmation_link,\ from mailer.mailutils import send as send_mail, get_mail_confirmation_link,\
prepend_base_url, get_registration_link, get_wait_confirmation_link,\ prepend_base_url, get_registration_link, get_wait_confirmation_link,\
get_invitation_reject_link get_invitation_reject_link, get_invite_as_user_key
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings from django.conf import settings
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
@ -275,6 +275,7 @@ class Member(Person):
confirmed = models.BooleanField(default=True, verbose_name=_('Confirmed')) confirmed = models.BooleanField(default=True, verbose_name=_('Confirmed'))
user = models.OneToOneField(User, blank=True, null=True, on_delete=models.SET_NULL, user = models.OneToOneField(User, blank=True, null=True, on_delete=models.SET_NULL,
verbose_name=_('Login data')) verbose_name=_('Login data'))
invite_as_user_key = models.CharField(max_length=32, default="")
objects = MemberManager() objects = MemberManager()
@ -652,6 +653,14 @@ class Member(Person):
return False return False
def invite_as_user(self):
"""Invites the member to join Kompass as a user."""
self.invite_as_user_key = uuid.uuid4().hex
self.save()
self.send_mail(_('Set login data for Kompass'),
settings.INVITE_AS_USER_TEXT.format(name=self.prename,
link=get_invite_as_user_key(self.invite_as_user_key)))
class EmergencyContact(ContactWithPhoneNumber): class EmergencyContact(ContactWithPhoneNumber):
""" """

@ -349,3 +349,8 @@ table.termine {
border-collapse:separate; border-collapse:separate;
border-spacing: 0 4pt; border-spacing: 0 4pt;
} }
.errorlist {
font-size: 9pt;
color: darkred;
}

@ -0,0 +1,12 @@
{% extends "admin/change_form_object_tools.html" %}
{% load i18n admin_urls %}
{% block object-tools-items %}
<li>
<a class="historylink" href="{% url 'admin:members_member_inviteasuser' original.pk %}">{% trans 'Invite as user' %}</a>
</li>
{{block.super}}
{% endblock %}
Loading…
Cancel
Save