From 017410fa6bc4b7188a35112c0c1de61e993b98e8 Mon Sep 17 00:00:00 2001 From: Tom Irgang Date: Sun, 23 Apr 2023 12:55:10 +0200 Subject: [PATCH 1/8] users started --- rc_hip/rc_hip/settings.py | 5 +++ rc_hip/rc_hip/urls.py | 2 + rc_hip/repapp/admin.py | 1 - ...fe_address_alter_device_device_and_more.py | 6 ++- rc_hip/repapp_users/__init__.py | 0 rc_hip/repapp_users/admin.py | 4 ++ rc_hip/repapp_users/apps.py | 6 +++ rc_hip/repapp_users/backends.py | 15 +++++++ rc_hip/repapp_users/forms.py | 0 .../repapp_users/migrations/0001_initial.py | 44 +++++++++++++++++++ rc_hip/repapp_users/migrations/__init__.py | 0 rc_hip/repapp_users/models.py | 14 ++++++ rc_hip/repapp_users/templates/login.html | 33 ++++++++++++++ rc_hip/repapp_users/tests.py | 3 ++ rc_hip/repapp_users/urls.py | 4 ++ rc_hip/repapp_users/views.py | 3 ++ 16 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 rc_hip/repapp_users/__init__.py create mode 100644 rc_hip/repapp_users/admin.py create mode 100644 rc_hip/repapp_users/apps.py create mode 100644 rc_hip/repapp_users/backends.py create mode 100644 rc_hip/repapp_users/forms.py create mode 100644 rc_hip/repapp_users/migrations/0001_initial.py create mode 100644 rc_hip/repapp_users/migrations/__init__.py create mode 100644 rc_hip/repapp_users/models.py create mode 100644 rc_hip/repapp_users/templates/login.html create mode 100644 rc_hip/repapp_users/tests.py create mode 100644 rc_hip/repapp_users/urls.py create mode 100644 rc_hip/repapp_users/views.py diff --git a/rc_hip/rc_hip/settings.py b/rc_hip/rc_hip/settings.py index b5c9c16..c602302 100644 --- a/rc_hip/rc_hip/settings.py +++ b/rc_hip/rc_hip/settings.py @@ -41,6 +41,7 @@ ALLOWED_HOSTS = [ # Application definition INSTALLED_APPS = [ + 'repapp_users.apps.RepappUsersConfig', 'repapp.apps.RepappConfig', 'django.contrib.admin', 'django.contrib.auth', @@ -150,3 +151,7 @@ EMAIL_PORT = (int)(os.getenv("DJANGO_EMAIL_PORT", '25')) EMAIL_HOST_USER = os.getenv("DJANGO_EMAIL_HOST_USER", None) EMAIL_HOST_PASSWORD = os.getenv("DJANGO_EMAIL_HOST_PASSWORD", None) EMAIL_USE_TLS = os.getenv("DJANGO_EMAIL_USE_TLS", 'true') in ('true', '1', 't') + +AUTH_USER_MODEL = "repapp_users.CustomUser" +AUTHENTICATION_BACKENDS = [ + "repapp_users.backends.EmailBackend", "django.contrib.auth.backends.ModelBackend"] diff --git a/rc_hip/rc_hip/urls.py b/rc_hip/rc_hip/urls.py index 2469ed7..e83b2f9 100644 --- a/rc_hip/rc_hip/urls.py +++ b/rc_hip/rc_hip/urls.py @@ -22,5 +22,7 @@ from django.conf.urls.static import static urlpatterns = [ path("", include("repapp.urls")), + path("", include("repapp_users.urls")), + path('users/', include('django.contrib.auth.urls')), path('admin/', admin.site.urls), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/rc_hip/repapp/admin.py b/rc_hip/repapp/admin.py index 0bccd55..f08860d 100644 --- a/rc_hip/repapp/admin.py +++ b/rc_hip/repapp/admin.py @@ -2,7 +2,6 @@ Admin interface configuration for RepApp. """ from django.contrib import admin - from .models import (Organisator, Cafe, Question, Device, Reparateur, Appointment, Guest, Candidate) diff --git a/rc_hip/repapp/migrations/0013_alter_cafe_address_alter_device_device_and_more.py b/rc_hip/repapp/migrations/0013_alter_cafe_address_alter_device_device_and_more.py index 0155f4c..dd81294 100644 --- a/rc_hip/repapp/migrations/0013_alter_cafe_address_alter_device_device_and_more.py +++ b/rc_hip/repapp/migrations/0013_alter_cafe_address_alter_device_device_and_more.py @@ -18,7 +18,8 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='device', name='device', - field=models.CharField(max_length=200, verbose_name='Art des Geräts'), + field=models.CharField( + max_length=200, verbose_name='Art des Geräts'), ), migrations.AlterField( model_name='device', @@ -28,6 +29,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='device', name='manufacturer', - field=models.CharField(max_length=200, verbose_name='Hersteller & Modell/Typ'), + field=models.CharField( + max_length=200, verbose_name='Hersteller & Modell/Typ'), ), ] diff --git a/rc_hip/repapp_users/__init__.py b/rc_hip/repapp_users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rc_hip/repapp_users/admin.py b/rc_hip/repapp_users/admin.py new file mode 100644 index 0000000..5cd50eb --- /dev/null +++ b/rc_hip/repapp_users/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import CustomUser + +admin.site.register(CustomUser) diff --git a/rc_hip/repapp_users/apps.py b/rc_hip/repapp_users/apps.py new file mode 100644 index 0000000..3ed1ca5 --- /dev/null +++ b/rc_hip/repapp_users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RepappUsersConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'repapp_users' diff --git a/rc_hip/repapp_users/backends.py b/rc_hip/repapp_users/backends.py new file mode 100644 index 0000000..169d764 --- /dev/null +++ b/rc_hip/repapp_users/backends.py @@ -0,0 +1,15 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend + + +class EmailBackend(ModelBackend): + def authenticate(self, request, username=None, password=None, **kwargs): + UserModel = get_user_model() + try: + user = UserModel.objects.get(email=username) + except UserModel.DoesNotExist: + return None + else: + if user.check_password(password): + return user + return None diff --git a/rc_hip/repapp_users/forms.py b/rc_hip/repapp_users/forms.py new file mode 100644 index 0000000..e69de29 diff --git a/rc_hip/repapp_users/migrations/0001_initial.py b/rc_hip/repapp_users/migrations/0001_initial.py new file mode 100644 index 0000000..c67c117 --- /dev/null +++ b/rc_hip/repapp_users/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2 on 2023-04-23 10:29 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='CustomUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('email', models.EmailField(max_length=254, unique=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/rc_hip/repapp_users/migrations/__init__.py b/rc_hip/repapp_users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rc_hip/repapp_users/models.py b/rc_hip/repapp_users/models.py new file mode 100644 index 0000000..2770e12 --- /dev/null +++ b/rc_hip/repapp_users/models.py @@ -0,0 +1,14 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class CustomUser(AbstractUser): + email = models.EmailField(unique=True, verbose_name=_("eMail Adresse")) + + class Meta: + verbose_name = _('Benutzer') + verbose_name_plural = _('Benutzer') + + def __str__(self): + return f'{self.email}' diff --git a/rc_hip/repapp_users/templates/login.html b/rc_hip/repapp_users/templates/login.html new file mode 100644 index 0000000..f2307c7 --- /dev/null +++ b/rc_hip/repapp_users/templates/login.html @@ -0,0 +1,33 @@ +{% extends "repapp/base.html" %} +{% block content %} + {% if form.errors %}

Your username and password didn't match. Please try again.

{% endif %} + {% if next %} + {% if user.is_authenticated %} +

+ Your account doesn't have access to this page. To proceed, + please login with an account that has access. +

+ {% else %} +

Please login to see this page.

+ {% endif %} + {% endif %} +
+ {% csrf_token %} + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+ + +
+ {# Assumes you setup the password_reset view in your URLconf #} +

+ Lost password? +

+{% endblock %} diff --git a/rc_hip/repapp_users/tests.py b/rc_hip/repapp_users/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/rc_hip/repapp_users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/rc_hip/repapp_users/urls.py b/rc_hip/repapp_users/urls.py new file mode 100644 index 0000000..ad783e1 --- /dev/null +++ b/rc_hip/repapp_users/urls.py @@ -0,0 +1,4 @@ +from django.urls import path, include + +urlpatterns = [ +] diff --git a/rc_hip/repapp_users/views.py b/rc_hip/repapp_users/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/rc_hip/repapp_users/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 70c36a44f549480acf4e0f84481715dd98077bd6 Mon Sep 17 00:00:00 2001 From: Tom Irgang Date: Sun, 23 Apr 2023 13:56:52 +0200 Subject: [PATCH 2/8] guest users --- rc_hip/rc_hip/settings.py | 2 + rc_hip/rc_hip/urls.py | 3 +- .../templates/repapp/guest_profile.html | 28 +++++++++++ rc_hip/repapp/urls.py | 2 + rc_hip/repapp/views.py | 47 ++++++++++++++++++- rc_hip/repapp_users/forms.py | 0 ...stomuser_options_alter_customuser_email.py | 22 +++++++++ rc_hip/repapp_users/templates/login.html | 33 ------------- .../templates/registration/login.html | 13 +++++ rc_hip/repapp_users/urls.py | 4 -- rc_hip/repapp_users/views.py | 3 -- 11 files changed, 114 insertions(+), 43 deletions(-) create mode 100644 rc_hip/repapp/templates/repapp/guest_profile.html delete mode 100644 rc_hip/repapp_users/forms.py create mode 100644 rc_hip/repapp_users/migrations/0002_alter_customuser_options_alter_customuser_email.py delete mode 100644 rc_hip/repapp_users/templates/login.html create mode 100644 rc_hip/repapp_users/templates/registration/login.html delete mode 100644 rc_hip/repapp_users/urls.py delete mode 100644 rc_hip/repapp_users/views.py diff --git a/rc_hip/rc_hip/settings.py b/rc_hip/rc_hip/settings.py index c602302..54412c7 100644 --- a/rc_hip/rc_hip/settings.py +++ b/rc_hip/rc_hip/settings.py @@ -155,3 +155,5 @@ EMAIL_USE_TLS = os.getenv("DJANGO_EMAIL_USE_TLS", 'true') in ('true', '1', 't') AUTH_USER_MODEL = "repapp_users.CustomUser" AUTHENTICATION_BACKENDS = [ "repapp_users.backends.EmailBackend", "django.contrib.auth.backends.ModelBackend"] + +LOGIN_REDIRECT_URL = '/guest/profile/' diff --git a/rc_hip/rc_hip/urls.py b/rc_hip/rc_hip/urls.py index e83b2f9..b0ce1c6 100644 --- a/rc_hip/rc_hip/urls.py +++ b/rc_hip/rc_hip/urls.py @@ -22,7 +22,6 @@ from django.conf.urls.static import static urlpatterns = [ path("", include("repapp.urls")), - path("", include("repapp_users.urls")), - path('users/', include('django.contrib.auth.urls')), + path('accounts/', include('django.contrib.auth.urls')), path('admin/', admin.site.urls), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/rc_hip/repapp/templates/repapp/guest_profile.html b/rc_hip/repapp/templates/repapp/guest_profile.html new file mode 100644 index 0000000..b4d4bbc --- /dev/null +++ b/rc_hip/repapp/templates/repapp/guest_profile.html @@ -0,0 +1,28 @@ +{% extends "repapp/base.html" %} +{% block title %} + - Gast Daten +{% endblock title %} +{% block content %} +

Ihre Daten

+
+ + + + + + + + + + + + + + + + + + + +
eMail Adresse{{ guest.mail }}
Name{{ guest.name }}
Wohnort{{ guest.residence }}
Telefonnummer{{ guest.phone }}
+{% endblock content %} diff --git a/rc_hip/repapp/urls.py b/rc_hip/repapp/urls.py index f56ab76..5cddc8e 100644 --- a/rc_hip/repapp/urls.py +++ b/rc_hip/repapp/urls.py @@ -21,4 +21,6 @@ urlpatterns = [ views.register_device_final, name="register_device_final"), path("device//", views.device_view, name="view_device"), + path("guest/profile/", + views.profile, name="guest_profile"), ] diff --git a/rc_hip/repapp/views.py b/rc_hip/repapp/views.py index 1a4b75a..209e7fb 100644 --- a/rc_hip/repapp/views.py +++ b/rc_hip/repapp/views.py @@ -3,14 +3,19 @@ Views of RepApp. """ import datetime import os +import random +import string from hashlib import sha256 from django.views import generic from django.urls import reverse_lazy from django.shortcuts import get_object_or_404, render from django.http import HttpResponseRedirect +from django.core.exceptions import PermissionDenied from django.contrib import messages from django.core.mail import send_mail from django.template.loader import render_to_string +from django.contrib.auth.decorators import login_required +from repapp_users.models import CustomUser from .models import Cafe, Device, Guest, Organisator from .forms import RegisterDevice, RegisterGuest @@ -156,6 +161,7 @@ class RegisterGuestFormView(generic.edit.FormView): device = get_object_or_404(Device, identifier=device_identifier) name = form.cleaned_data['name'] + mail = self.kwargs['mail'] residence = form.cleaned_data['residence'] identifier = sha256( f'{name}{residence}{datetime.datetime.now()}'.encode('utf-8') @@ -165,13 +171,25 @@ class RegisterGuestFormView(generic.edit.FormView): name=name, phone=form.cleaned_data['phone'], residence=residence, - mail=self.kwargs['mail'], + mail=mail, ) guest.save() device.guest = guest device.save() + if not CustomUser.objects.filter(email=mail).exists(): + password = ''.join(random.choice(string.ascii_letters) + for i in range(10)) + user = CustomUser.objects.create_user( + username=name, + email=mail, + password=password) + user.save() + + print(f'{mail} {password}') + # TODO: send account creation email + send_confirmation_mails(device, guest, cafe, self.request) return HttpResponseRedirect( @@ -208,11 +226,38 @@ def register_device_final(request, cafe, device_identifier): ) +@login_required def device_view(request, device_identifier): + user = request.user + if not user: + raise PermissionDenied() + device = get_object_or_404(Device, identifier=device_identifier) + if not device.guest: + raise PermissionDenied() + + # TODO: Guests can only view their devices, Reparateur and Orga can view all devices. return render( request, "repapp/device_view.html", {"device": device}, ) + + +@login_required +def profile(request): + user = request.user + if not user: + raise PermissionDenied() + + guest = get_object_or_404(Guest, mail=user.email) + + return render( + request, + "repapp/guest_profile.html", + { + 'user': user, + 'guest': guest + } + ) diff --git a/rc_hip/repapp_users/forms.py b/rc_hip/repapp_users/forms.py deleted file mode 100644 index e69de29..0000000 diff --git a/rc_hip/repapp_users/migrations/0002_alter_customuser_options_alter_customuser_email.py b/rc_hip/repapp_users/migrations/0002_alter_customuser_options_alter_customuser_email.py new file mode 100644 index 0000000..bb1e189 --- /dev/null +++ b/rc_hip/repapp_users/migrations/0002_alter_customuser_options_alter_customuser_email.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2 on 2023-04-23 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('repapp_users', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='customuser', + options={'verbose_name': 'Benutzer', 'verbose_name_plural': 'Benutzer'}, + ), + migrations.AlterField( + model_name='customuser', + name='email', + field=models.EmailField(max_length=254, unique=True, verbose_name='eMail Adresse'), + ), + ] diff --git a/rc_hip/repapp_users/templates/login.html b/rc_hip/repapp_users/templates/login.html deleted file mode 100644 index f2307c7..0000000 --- a/rc_hip/repapp_users/templates/login.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends "repapp/base.html" %} -{% block content %} - {% if form.errors %}

Your username and password didn't match. Please try again.

{% endif %} - {% if next %} - {% if user.is_authenticated %} -

- Your account doesn't have access to this page. To proceed, - please login with an account that has access. -

- {% else %} -

Please login to see this page.

- {% endif %} - {% endif %} -
- {% csrf_token %} - - - - - - - - - -
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
- - -
- {# Assumes you setup the password_reset view in your URLconf #} -

- Lost password? -

-{% endblock %} diff --git a/rc_hip/repapp_users/templates/registration/login.html b/rc_hip/repapp_users/templates/registration/login.html new file mode 100644 index 0000000..c292974 --- /dev/null +++ b/rc_hip/repapp_users/templates/registration/login.html @@ -0,0 +1,13 @@ +{% extends "repapp/base.html" %} +{% load crispy_forms_tags %} +{% block title %} + - Gast Login +{% endblock title %} +{% block content %} +

Gast Login

+
+ {% csrf_token %} + {{ form|crispy }} + +
+{% endblock content %} diff --git a/rc_hip/repapp_users/urls.py b/rc_hip/repapp_users/urls.py deleted file mode 100644 index ad783e1..0000000 --- a/rc_hip/repapp_users/urls.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.urls import path, include - -urlpatterns = [ -] diff --git a/rc_hip/repapp_users/views.py b/rc_hip/repapp_users/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/rc_hip/repapp_users/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. From a612239083d6403ffaae57fdb7c0a6fecdec239d Mon Sep 17 00:00:00 2001 From: Tom Irgang Date: Sun, 23 Apr 2023 17:40:19 +0200 Subject: [PATCH 3/8] Keycloak login --- rc_hip/rc_hip/settings.py | 26 +++++++++- rc_hip/rc_hip/urls.py | 1 + rc_hip/repapp/templates/repapp/base.html | 9 ++++ .../repapp/templates/repapp/member_login.html | 26 ++++++++++ rc_hip/repapp/urls.py | 2 + rc_hip/repapp/views.py | 8 ++- rc_hip/repapp_users/backends.py | 50 +++++++++++++++++++ requirements_dev.txt | 2 + requirements_prod.txt | 1 + 9 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 rc_hip/repapp/templates/repapp/member_login.html diff --git a/rc_hip/rc_hip/settings.py b/rc_hip/rc_hip/settings.py index 54412c7..b3baec8 100644 --- a/rc_hip/rc_hip/settings.py +++ b/rc_hip/rc_hip/settings.py @@ -41,16 +41,17 @@ ALLOWED_HOSTS = [ # Application definition INSTALLED_APPS = [ - 'repapp_users.apps.RepappUsersConfig', 'repapp.apps.RepappConfig', 'django.contrib.admin', 'django.contrib.auth', + 'mozilla_django_oidc', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'crispy_forms', 'crispy_bootstrap4', + 'repapp_users.apps.RepappUsersConfig', ] CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap4' @@ -154,6 +155,27 @@ EMAIL_USE_TLS = os.getenv("DJANGO_EMAIL_USE_TLS", 'true') in ('true', '1', 't') AUTH_USER_MODEL = "repapp_users.CustomUser" AUTHENTICATION_BACKENDS = [ - "repapp_users.backends.EmailBackend", "django.contrib.auth.backends.ModelBackend"] + "repapp_users.backends.EmailBackend", + "django.contrib.auth.backends.ModelBackend", + # 'mozilla_django_oidc.auth.OIDCAuthenticationBackend', + 'repapp_users.backends.KeycloakOIDCAB' +] LOGIN_REDIRECT_URL = '/guest/profile/' + +OIDC_RP_CLIENT_ID = "repapp" +OIDC_RP_CLIENT_SECRET = os.getenv("DJANGO_OIDC_RP_CLIENT_SECRET", None) + +OIDC_RP_SIGN_ALGO = "RS256" +OIDC_OP_JWKS_ENDPOINT = "https://sso.makes-hacks-hip.de/realms/Makes-Hacks-Hip/protocol/openid-connect/certs" + +OIDC_OP_AUTHORIZATION_ENDPOINT = "https://sso.makes-hacks-hip.de/realms/Makes-Hacks-Hip/protocol/openid-connect/auth" +OIDC_OP_TOKEN_ENDPOINT = "https://sso.makes-hacks-hip.de/realms/Makes-Hacks-Hip/protocol/openid-connect/token" +OIDC_OP_USER_ENDPOINT = "https://sso.makes-hacks-hip.de/realms/Makes-Hacks-Hip/protocol/openid-connect/userinfo" + +if DEBUG: + LOGIN_REDIRECT_URL = "http://127.0.0.1:8000/" + LOGOUT_REDIRECT_URL = "http://127.0.0.1:8000/" +else: + LOGIN_REDIRECT_URL = "https://repapp.rc-hip.de/" + LOGOUT_REDIRECT_URL = "https://repapp.rc-hip.de/" diff --git a/rc_hip/rc_hip/urls.py b/rc_hip/rc_hip/urls.py index b0ce1c6..9cd3412 100644 --- a/rc_hip/rc_hip/urls.py +++ b/rc_hip/rc_hip/urls.py @@ -22,6 +22,7 @@ from django.conf.urls.static import static urlpatterns = [ path("", include("repapp.urls")), + path('oidc/', include('mozilla_django_oidc.urls')), path('accounts/', include('django.contrib.auth.urls')), path('admin/', admin.site.urls), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/rc_hip/repapp/templates/repapp/base.html b/rc_hip/repapp/templates/repapp/base.html index acb2fa5..1e78f2b 100644 --- a/rc_hip/repapp/templates/repapp/base.html +++ b/rc_hip/repapp/templates/repapp/base.html @@ -34,5 +34,14 @@ + diff --git a/rc_hip/repapp/templates/repapp/member_login.html b/rc_hip/repapp/templates/repapp/member_login.html new file mode 100644 index 0000000..c93b392 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/member_login.html @@ -0,0 +1,26 @@ +{% extends "repapp/base.html" %} +{% block title %} + - Mitarbeiter Login +{% endblock title %} +{% block content %} +

+ Mitarbeiter + {% if user.is_authenticated %} + Logout + {% else %} + Login + {% endif %} +

+
+
+ {% if user.is_authenticated %} +

Angemeldeter Benutzer: {{ user.email }}

+
+ {% csrf_token %} + +
+ {% else %} + Anmelden mit Makes-Hacks-Hip + {% endif %} +
+{% endblock content %} diff --git a/rc_hip/repapp/urls.py b/rc_hip/repapp/urls.py index 5cddc8e..24277d5 100644 --- a/rc_hip/repapp/urls.py +++ b/rc_hip/repapp/urls.py @@ -23,4 +23,6 @@ urlpatterns = [ views.device_view, name="view_device"), path("guest/profile/", views.profile, name="guest_profile"), + path("member/login/", + views.member_login, name="member_login"), ] diff --git a/rc_hip/repapp/views.py b/rc_hip/repapp/views.py index 209e7fb..2f04e8d 100644 --- a/rc_hip/repapp/views.py +++ b/rc_hip/repapp/views.py @@ -257,7 +257,13 @@ def profile(request): request, "repapp/guest_profile.html", { - 'user': user, 'guest': guest } ) + + +def member_login(request): + return render( + request, + "repapp/member_login.html" + ) diff --git a/rc_hip/repapp_users/backends.py b/rc_hip/repapp_users/backends.py index 169d764..1ee9555 100644 --- a/rc_hip/repapp_users/backends.py +++ b/rc_hip/repapp_users/backends.py @@ -1,5 +1,8 @@ +import unicodedata from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend +from mozilla_django_oidc.auth import OIDCAuthenticationBackend +from repapp.models import Organisator, Reparateur class EmailBackend(ModelBackend): @@ -13,3 +16,50 @@ class EmailBackend(ModelBackend): if user.check_password(password): return user return None + + +def generate_username(email): + # Using Python 3 and Django 1.11+, usernames can contain alphanumeric + # (ascii and unicode), _, @, +, . and - characters. So we normalize + # it and slice at 150 characters. + return unicodedata.normalize('NFKC', email)[:150] + + +def create_repapp_user(user): + organisator = Organisator.objects.filter(mail=user.email).first() + if not organisator: + reparateur = Reparateur.objects.filter(mail=user.email).first() + if not reparateur: + reparateur = Reparateur( + name=user.username, + mail=user.email, + ) + reparateur.save() + else: + reparateur.name = user.username + reparateur.save() + else: + organisator.name = user.username + organisator.save() + + +class KeycloakOIDCAB(OIDCAuthenticationBackend): + def create_user(self, claims): + user = super(KeycloakOIDCAB, self).create_user(claims) + + user.username = claims.get( + 'preferred_username', generate_username(user.email)) + user.save() + + create_repapp_user(user) + + return user + + def update_user(self, user, claims): + user.username = claims.get( + 'preferred_username', generate_username(user.email)) + user.save() + + create_repapp_user(user) + + return user diff --git a/requirements_dev.txt b/requirements_dev.txt index f8fb2e1..dc77789 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -6,3 +6,5 @@ pylint-django==2.5.3 pylint-plugin-utils==0.7 django-crispy-forms==2.0 crispy-bootstrap4==2022.1 +mozilla-django-oidc==3.0.0 +djlint==1.23.3 diff --git a/requirements_prod.txt b/requirements_prod.txt index 28faded..231eaca 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -2,3 +2,4 @@ Django==4.2 gunicorn==20.1.0 django-crispy-forms==2.0 crispy-bootstrap4==2022.1 +mozilla-django-oidc==3.0.0 From e5aacf7e19d13c0882ed41eb6f891c19394e856c Mon Sep 17 00:00:00 2001 From: Tom Irgang Date: Sun, 23 Apr 2023 21:26:39 +0200 Subject: [PATCH 4/8] Demo feedback --- docs/index.md | 42 ++++++++++++++++++++++----------------- rc_hip/rc_hip/settings.py | 1 + rc_hip/repapp/admin.py | 8 +++++++- requirements_dev.txt | 1 + requirements_prod.txt | 1 + 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/docs/index.md b/docs/index.md index 06eda45..3209418 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,38 +20,44 @@ Ein Organisator ist Mitglied des Repair-Cafés das die Termine mit den Gästen o ### Gast -- [x] Als Gast möchte ich einen Termin für eine Reparatur vereinbaren. (A1) -- [x] Als Gast möchte ich eine Bestätigung für eine Reparaturanfrage bekommen. (A2) -- [ ] Als Gast möchte ich eine Terminbenachrichtigung oder eine Absage bekommen. (A3) +- [x] M1: Als Gast möchte ich einen Termin für eine Reparatur vereinbaren. (A1) +- [x] M1: Als Gast möchte ich eine Bestätigung für eine Reparaturanfrage bekommen. (A2) +- [ ] M1: Als Gast möchte ich eine Terminbenachrichtigung oder eine Absage bekommen. (A3) - [ ] Als Gast möchte ich mein angemeldetes Gerät und dessen Status einsehen können. (A4) -- [ ] Als Gast möchte ich Rückfragen zum Gerät empfangen und diese beantworten können. (A5) +- [ ] M1: Als Gast möchte ich Rückfragen zum Gerät empfangen und diese beantworten können. (A5) +- [ ] M1: Als Gast möchte ich Fragen ans Repair-Café stellen können. (A26) - [ ] Als Gast möchte ich ein Gerät für eine Folgereparatur anmelden können. (A6) - [ ] Als Gast möchte ich meine Daten ansehen können. (A21) - [ ] Als Gast möchte ich meine Daten löschen können. (A22) - [ ] Als Gast möchte ich eine Geräteanmeldung löschen können. (A23) +- [ ] M1: Als Gast möchte ausschließlich per eMail mit dem System interagieren können. (A30) ### Reparateur -- [ ] Als Reparateur möchte ich die angemeldeten Geräte ansehen können. (A7) -- [ ] Als Reparateur möchte ich mir ein angemeldetes Gerät zuordnen können. (A8) +- [ ] M1: Als Reparateur möchte ich die angemeldeten Geräte ansehen können. (A7) +- [ ] M1: Als Reparateur möchte ich mir ein angemeldetes Gerät zuordnen können. (A8) - [ ] Als Reparateur möchte ich ein angemeldetes Gerät ablehnen können. (A9) -- [ ] Als Reparateur möchte ich Rückfragen zum Gerät stellen können. (A10) -- [ ] Als Reparateur möchte ich eine Benachrichtigung bekommen wenn eine Rückfrage beantwortet wird. (A11) -- [ ] Als Reparateur möchte ich eine Benachrichtigung bekommen wenn mir ein Gerät zugeordnet wird. (A12) -- [ ] Als Reparateur möchte ich mich für ein Repair-Café anmelden können. (A13) -- [ ] Als Reparateur möchte ich mich für ein Repair-Café abmelden können. (A14) +- [ ] M1: Als Reparateur möchte ich Rückfragen zum Gerät stellen können. (A10) +- [ ] M1: Als Reparateur möchte ich eine Benachrichtigung bekommen wenn eine Rückfrage beantwortet wird. (A11) +- [ ] M1: Als Reparateur möchte ich eine Benachrichtigung bekommen wenn mir ein Gerät zugeordnet wird. (A12) +- [ ] M1: Als Reparateur möchte ich mich für ein Repair-Café anmelden können. (A13) +- [ ] M1: Als Reparateur möchte ich mich für ein Repair-Café abmelden können. (A14) - [ ] Als Reparateur möchte ich benachrichtigt werden wenn ein Gast eine mir zugeordnete Geräteanmeldung löscht. (A25) ### Organisator -- [x] Als Organisator möchte ich benachrichtigt werden wenn ein Gast ein Gerät anmeldet. (A25) -- [ ] Als Organisator möchte ich ein Repair-Café anlegen können. (A15) -- [ ] Als Organisator möchte ich die Geräte einsehen können. (A16) -- [ ] Als Organisator möchte ich eine Rückfrage zu einem Geräte stellen können. (A17) -- [ ] Als Organisator möchte ich ein Geräte ablehnen können. (A18) -- [ ] Als Organisator möchte ich ein Geräte zu einem Termin zuordnen können. (A19) -- [ ] Als Organisator möchte ich einen Reparateur zu einem Termin zuordnen können. (A20) +- [x] M1: Als Organisator möchte ich benachrichtigt werden wenn ein Gast ein Gerät anmeldet. (A25) +- [ ] M1: Als Organisator möchte ich ein Repair-Café anlegen können. (A15) +- [ ] M1: Als Organisator möchte ich die Geräte einsehen können. (A16) +- [ ] M1: Als Organisator möchte ich eine Rückfrage zu einem Geräte stellen können. (A17) +- [ ] M1: Als Organisator möchte ich ein Geräte ablehnen können. (A18) +- [ ] M1: Als Organisator möchte ich ein Geräte zu einem Termin zuordnen können. (A19) +- [ ] M1: Als Organisator möchte ich einen Reparateur zu einem Termin zuordnen können. (A20) +- [ ] M1: Als Organisator möchte ich ein Gerät bearbeiten können. (A28) +- [ ] M1: Als Organisator möchte ich ein Gerät absagen können, wenn der Gast mich informiert hat dass er nicht kommt. (A29) - [ ] Als Organisator möchte ich benachrichtigt werden wenn ein Gast eine Geräteanmeldung löscht. (A24) +- [ ] Als Organisator möchte ich alle Daten exportieren können. (A31) +- [ ] Als Organisator möchte ich Geräte auf ein späteres Repair-Café verschieben können. (A32) ## Daten diff --git a/rc_hip/rc_hip/settings.py b/rc_hip/rc_hip/settings.py index b3baec8..5e75556 100644 --- a/rc_hip/rc_hip/settings.py +++ b/rc_hip/rc_hip/settings.py @@ -52,6 +52,7 @@ INSTALLED_APPS = [ 'crispy_forms', 'crispy_bootstrap4', 'repapp_users.apps.RepappUsersConfig', + 'import_export', ] CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap4' diff --git a/rc_hip/repapp/admin.py b/rc_hip/repapp/admin.py index f08860d..c2abfc3 100644 --- a/rc_hip/repapp/admin.py +++ b/rc_hip/repapp/admin.py @@ -2,14 +2,20 @@ Admin interface configuration for RepApp. """ from django.contrib import admin +from import_export.admin import ImportExportModelAdmin from .models import (Organisator, Cafe, Question, Device, Reparateur, Appointment, Guest, Candidate) + +class DeviceAdmin(ImportExportModelAdmin): + resource_classes = [Device] + + admin.site.register(Organisator) admin.site.register(Cafe) admin.site.register(Question) admin.site.register(Guest) -admin.site.register(Device) +admin.site.register(Device, DeviceAdmin) admin.site.register(Reparateur) admin.site.register(Appointment) admin.site.register(Candidate) diff --git a/requirements_dev.txt b/requirements_dev.txt index dc77789..d8c96dd 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -8,3 +8,4 @@ django-crispy-forms==2.0 crispy-bootstrap4==2022.1 mozilla-django-oidc==3.0.0 djlint==1.23.3 +django-import-export==3.2.0 diff --git a/requirements_prod.txt b/requirements_prod.txt index 231eaca..c3e0daa 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -3,3 +3,4 @@ gunicorn==20.1.0 django-crispy-forms==2.0 crispy-bootstrap4==2022.1 mozilla-django-oidc==3.0.0 +django-import-export==3.2.0 From c74e09585ff378f17a3bbed4afccf7e540e516d0 Mon Sep 17 00:00:00 2001 From: Tom Irgang Date: Mon, 24 Apr 2023 08:20:59 +0200 Subject: [PATCH 5/8] data export in admin --- rc_hip/repapp/admin.py | 68 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/rc_hip/repapp/admin.py b/rc_hip/repapp/admin.py index c2abfc3..7b3578c 100644 --- a/rc_hip/repapp/admin.py +++ b/rc_hip/repapp/admin.py @@ -2,20 +2,72 @@ Admin interface configuration for RepApp. """ from django.contrib import admin -from import_export.admin import ImportExportModelAdmin +from import_export.admin import ImportExportModelAdmin, ExportActionMixin +from import_export import resources from .models import (Organisator, Cafe, Question, Device, Reparateur, Appointment, Guest, Candidate) -class DeviceAdmin(ImportExportModelAdmin): - resource_classes = [Device] +class GuestResource(resources.ModelResource): + class Meta: + model = Guest + + @staticmethod + def get_display_name(): + return "Gäste" + + +class DeviceResource(resources.ModelResource): + class Meta: + model = Device + + @staticmethod + def get_display_name(): + return "Geräte" + + +class QuestionResource(resources.ModelResource): + class Meta: + model = Question + + @staticmethod + def get_display_name(): + return "Fragen" + + +class AppointmentResource(resources.ModelResource): + class Meta: + model = Appointment + + @staticmethod + def get_display_name(): + return "Termine" + + +class CandidateResource(resources.ModelResource): + class Meta: + model = Candidate + + @staticmethod + def get_display_name(): + return "Kandidaten" + + +class ExportAdmin(ImportExportModelAdmin, ExportActionMixin): + resource_classes = [ + DeviceResource, + GuestResource, + QuestionResource, + AppointmentResource, + CandidateResource, + ] admin.site.register(Organisator) admin.site.register(Cafe) -admin.site.register(Question) -admin.site.register(Guest) -admin.site.register(Device, DeviceAdmin) +admin.site.register(Question, ExportAdmin) +admin.site.register(Guest, ExportAdmin) +admin.site.register(Device, ExportAdmin) admin.site.register(Reparateur) -admin.site.register(Appointment) -admin.site.register(Candidate) +admin.site.register(Appointment, ExportAdmin) +admin.site.register(Candidate, ExportAdmin) From 050dc4cd41c817f5354c65462e53a551496988a6 Mon Sep 17 00:00:00 2001 From: Tom Irgang Date: Mon, 24 Apr 2023 08:27:18 +0200 Subject: [PATCH 6/8] device createion date --- rc_hip/repapp/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rc_hip/repapp/models.py b/rc_hip/repapp/models.py index 0836015..9b3727f 100644 --- a/rc_hip/repapp/models.py +++ b/rc_hip/repapp/models.py @@ -1,6 +1,7 @@ """ This module implements the models of RepApp. """ +import django.utils.timezone from django.db import models from django.utils.translation import gettext_lazy as _ @@ -94,6 +95,8 @@ class Device(models.Model): cafe = models.ForeignKey( Cafe, on_delete=models.CASCADE, null=False, verbose_name=_("Repair-Café")) confirmed = models.BooleanField(verbose_name=_("Bestätigung gesendet?")) + date = models.DateField(verbose_name=_( + "Erstellungsdatum"), default=django.utils.timezone.now) class Meta: verbose_name = _('Gerät') @@ -131,7 +134,8 @@ class Question(models.Model): """ question = models.TextField(verbose_name=_("Frage")) answer = models.TextField(verbose_name=_("Antwort")) - date = models.DateField(verbose_name=_("Erstellungsdatum")) + date = models.DateField(verbose_name=_( + "Erstellungsdatum"), default=django.utils.timezone.now) organisator = models.ForeignKey( Organisator, on_delete=models.CASCADE, null=True, verbose_name=_("Organisator")) reparateur = models.ForeignKey( From 9a8dd3f9fd9c5df5ad2a41142da1d6d3c68decf8 Mon Sep 17 00:00:00 2001 From: Tom Irgang Date: Mon, 24 Apr 2023 18:02:43 +0200 Subject: [PATCH 7/8] implement user logins --- cspell.json | 10 +- docs/index.md | 29 +++ rc_hip/rc_hip/settings.py | 134 +++++++------- rc_hip/repapp/admin.py | 39 +++- rc_hip/repapp/apps.py | 6 + rc_hip/repapp/backends.py | 65 +++++++ rc_hip/repapp/forms.py | 3 + rc_hip/repapp/migrations/0001_initial.py | 163 ++++++++++++++--- ...in_cafeid_alter_termin_gerätid_and_more.py | 41 ----- ...torid_alter_frage_reparateurid_and_more.py | 29 --- ..._question_remove_termin_cafeid_and_more.py | 153 ---------------- ...ter_cafe_event_date_alter_cafe_location.py | 23 --- .../0006_device_phone_device_residence.py | 23 --- ...ter_device_phone_alter_device_residence.py | 23 --- ...evice_mail_remove_device_owner_and_more.py | 50 ----- ...ame_cafeid_appointment_cafe_id_and_more.py | 48 ----- rc_hip/repapp/migrations/0010_candidate.py | 27 --- ...ename_cafe_id_appointment_cafe_and_more.py | 80 -------- ..._secret_remove_guest_confirmed_and_more.py | 49 ----- ...fe_address_alter_device_device_and_more.py | 35 ---- rc_hip/repapp/models.py | 39 ++++ .../templates/registration/login.html | 0 .../repapp/mail/mail_new_guest_html.html | 19 ++ .../repapp/mail/mail_new_guest_subject.html | 1 + .../repapp/mail/mail_new_guest_text.html | 6 + .../repapp/mail/mail_one_time_login_html.html | 16 ++ .../mail/mail_one_time_login_subject.html | 1 + .../repapp/mail/mail_one_time_login_text.html | 3 + .../mail/mail_register_device_html.html | 9 + .../mail/mail_register_device_text.html | 1 + rc_hip/repapp/tests.py | 2 - rc_hip/repapp/urls.py | 5 +- rc_hip/repapp/utils.py | 171 ++++++++++++++++++ rc_hip/repapp/views.py | 108 +++++------ rc_hip/repapp_users/__init__.py | 0 rc_hip/repapp_users/admin.py | 4 - rc_hip/repapp_users/apps.py | 6 - rc_hip/repapp_users/backends.py | 65 ------- .../repapp_users/migrations/0001_initial.py | 44 ----- ...stomuser_options_alter_customuser_email.py | 22 --- rc_hip/repapp_users/migrations/__init__.py | 0 rc_hip/repapp_users/models.py | 14 -- rc_hip/repapp_users/tests.py | 3 - 43 files changed, 679 insertions(+), 890 deletions(-) create mode 100644 rc_hip/repapp/backends.py delete mode 100644 rc_hip/repapp/migrations/0002_alter_termin_cafeid_alter_termin_gerätid_and_more.py delete mode 100644 rc_hip/repapp/migrations/0003_alter_frage_organisatorid_alter_frage_reparateurid_and_more.py delete mode 100644 rc_hip/repapp/migrations/0004_appointment_device_question_remove_termin_cafeid_and_more.py delete mode 100644 rc_hip/repapp/migrations/0005_alter_cafe_event_date_alter_cafe_location.py delete mode 100644 rc_hip/repapp/migrations/0006_device_phone_device_residence.py delete mode 100644 rc_hip/repapp/migrations/0007_alter_device_phone_alter_device_residence.py delete mode 100644 rc_hip/repapp/migrations/0008_guest_remove_device_mail_remove_device_owner_and_more.py delete mode 100644 rc_hip/repapp/migrations/0009_rename_cafeid_appointment_cafe_id_and_more.py delete mode 100644 rc_hip/repapp/migrations/0010_candidate.py delete mode 100644 rc_hip/repapp/migrations/0011_rename_cafe_id_appointment_cafe_and_more.py delete mode 100644 rc_hip/repapp/migrations/0012_remove_device_secret_remove_guest_confirmed_and_more.py delete mode 100644 rc_hip/repapp/migrations/0013_alter_cafe_address_alter_device_device_and_more.py rename rc_hip/{repapp_users => repapp}/templates/registration/login.html (100%) create mode 100644 rc_hip/repapp/templates/repapp/mail/mail_new_guest_html.html create mode 100644 rc_hip/repapp/templates/repapp/mail/mail_new_guest_subject.html create mode 100644 rc_hip/repapp/templates/repapp/mail/mail_new_guest_text.html create mode 100644 rc_hip/repapp/templates/repapp/mail/mail_one_time_login_html.html create mode 100644 rc_hip/repapp/templates/repapp/mail/mail_one_time_login_subject.html create mode 100644 rc_hip/repapp/templates/repapp/mail/mail_one_time_login_text.html create mode 100644 rc_hip/repapp/utils.py delete mode 100644 rc_hip/repapp_users/__init__.py delete mode 100644 rc_hip/repapp_users/admin.py delete mode 100644 rc_hip/repapp_users/apps.py delete mode 100644 rc_hip/repapp_users/backends.py delete mode 100644 rc_hip/repapp_users/migrations/0001_initial.py delete mode 100644 rc_hip/repapp_users/migrations/0002_alter_customuser_options_alter_customuser_email.py delete mode 100644 rc_hip/repapp_users/migrations/__init__.py delete mode 100644 rc_hip/repapp_users/models.py delete mode 100644 rc_hip/repapp_users/tests.py diff --git a/cspell.json b/cspell.json index f3042a7..2e63262 100644 --- a/cspell.json +++ b/cspell.json @@ -9,7 +9,15 @@ "reparateur", "organisator", "Reparateurs", - "repapp" + "repapp", + "oidc", + "anmeldung", + "repaircafe", + "hilpoltstein", + "KeycloakOIDCAB", + "Keycloak", + "tabindex", + "onetimelogin" ], "ignoreWords": [], "import": [] diff --git a/docs/index.md b/docs/index.md index 3209418..3f03358 100644 --- a/docs/index.md +++ b/docs/index.md @@ -290,3 +290,32 @@ Als bekannter Gast der ein defekten Geräte anmelden möchte, - Die nächste Seite (S2) enthält ein Formular mit den Feldern `eMail-Adresse`, `Art des Geräts`, `Hersteller & Modell/Typ`, `Fehlerbeschreibung`, `Foto vom Gerät`, `Foto vom Typenschild`, ein Kontrollkästchen `Folgetermin`, ein Kontrollkästchen `Informationen zur Reparaturabwicklung` une ein Kontrollkästchen `Datenschutz`. Nachdem ich die Felder ausgefüllt habe klicke auf auf `Absenden`. Dieser Klick bringt mich auf die nächste Seite. - Die nächste Seite (S5) bestätigt mir die Anmeldung meines Gerätes. - In meinem eMail Posteingang finde ich ebenfalls eine Bestätigung der Geräteanmeldung. + +## Konfigurationsparameter + +- DJANGO_SECRET_KEY +- DJANGO_DEBUG +- DJANGO_EMAIL_HOST +- DJANGO_EMAIL_PORT +- DJANGO_EMAIL_HOST_USER +- DJANGO_EMAIL_HOST_PASSWORD +- DJANGO_EMAIL_USE_TLS +- OIDC_RP_CLIENT_SECRET + +### Feste Konfiguration + +- ALLOWED_HOSTS +- LANGUAGE_CODE +- TIME_ZONE +- CSRF_TRUSTED_ORIGINS +- AUTH_USER_MODEL +- AUTHENTICATION_BACKENDS +- LOGIN_REDIRECT_URL +- OIDC_RP_CLIENT_ID +- OIDC_RP_SIGN_ALGO +- OIDC_OP_JWKS_ENDPOINT +- OIDC_OP_AUTHORIZATION_ENDPOINT +- OIDC_OP_TOKEN_ENDPOINT +- OIDC_OP_USER_ENDPOINT +- LOGIN_REDIRECT_URL +- LOGOUT_REDIRECT_URL diff --git a/rc_hip/rc_hip/settings.py b/rc_hip/rc_hip/settings.py index 5e75556..61bbc36 100644 --- a/rc_hip/rc_hip/settings.py +++ b/rc_hip/rc_hip/settings.py @@ -1,7 +1,7 @@ """ Django settings for rc_hip project. -Generated by 'django-admin startproject' using Django 4.2. +Generated by "django-admin startproject" using Django 4.2. For more information on this file, see https://docs.djangoproject.com/en/4.2/topics/settings/ @@ -15,7 +15,7 @@ from pathlib import Path mimetypes.add_type("text/css", ".css", True) -# Build paths inside the project like this: BASE_DIR / 'subdir'. +# Build paths inside the project like this: BASE_DIR / "subdir". BASE_DIR = Path(__file__).resolve().parent.parent STATIC_ROOT = os.path.join(BASE_DIR, "static") @@ -25,77 +25,77 @@ MEDIA_ROOT = os.path.join(BASE_DIR, "media") # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -# SECRET_KEY = 'django-insecure-ura&_-(r(&&b$0s!a=07c@k6c!8=8m7$5(4#w8m@79-e%rn6s*' -SECRET_KEY = os.environ.get( - "DJANGO_SECRET_KEY", 'django-insecure-ura&_-(r(&&b$0s!a=07c@k6c!8=8m7$5(4#w8m@79-e%rn6s*') +# SECRET_KEY = "django-insecure-ura&_-(r(&&b$0s!a=07c@k6c!8=8m7$5(4#w8m@79-e%rn6s*" +SECRET_KEY = os.getenv( + "DJANGO_SECRET_KEY", "django-insecure-ura&_-(r(&&b$0s!a=07c@k6c!8=8m7$5(4#w8m@79-e%rn6s*") -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.getenv("DJANGO_DEBUG", 'true').lower() in ('true', '1', 't') +# SECURITY WARNING: don"t run with debug turned on in production! +DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() in ("true", "1", "t") ALLOWED_HOSTS = [ - '127.0.0.1', - 'localhost', - 'repapp.rc-hip.de' + "127.0.0.1", + "localhost", + "repapp.rc-hip.de", + "anmeldung.repaircafe-hilpoltstein.de", ] # Application definition INSTALLED_APPS = [ - 'repapp.apps.RepappConfig', - 'django.contrib.admin', - 'django.contrib.auth', - 'mozilla_django_oidc', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'crispy_forms', - 'crispy_bootstrap4', - 'repapp_users.apps.RepappUsersConfig', - 'import_export', + "repapp.apps.RepappConfig", + "django.contrib.admin", + "django.contrib.auth", + "mozilla_django_oidc", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "crispy_forms", + "crispy_bootstrap4", + "import_export", ] -CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap4' -CRISPY_TEMPLATE_PACK = 'bootstrap4' +CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap4" +CRISPY_TEMPLATE_PACK = "bootstrap4" MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'rc_hip.urls' +ROOT_URLCONF = "rc_hip.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'rc_hip.wsgi.application' +WSGI_APPLICATION = "rc_hip.wsgi.application" # Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'data' / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "data" / "db.sqlite3", } } @@ -105,16 +105,16 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -122,9 +122,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ -LANGUAGE_CODE = 'de' +LANGUAGE_CODE = "de" -TIME_ZONE = 'Europe/Berlin' +TIME_ZONE = "Europe/Berlin" USE_I18N = True @@ -134,35 +134,41 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_URL = 'static/' -MEDIA_URL = 'media/' +STATIC_URL = "static/" +MEDIA_URL = "media/" # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" CSRF_TRUSTED_ORIGINS = [ - 'https://repapp.rc-hip.de', - 'http://127.0.0.1:8020' + "http://127.0.0.1:8020", + "http://localhost:8020", + "http://127.0.0.1:8000", + "http://localhost:8000", + "https://repapp.rc-hip.de", + "https://anmeldung.repaircafe-hilpoltstein.de", ] -EMAIL_HOST = os.getenv("DJANGO_EMAIL_HOST", '') -EMAIL_PORT = (int)(os.getenv("DJANGO_EMAIL_PORT", '25')) +EMAIL_HOST = os.getenv("DJANGO_EMAIL_HOST", "") +EMAIL_PORT = (int)(os.getenv("DJANGO_EMAIL_PORT", "25")) EMAIL_HOST_USER = os.getenv("DJANGO_EMAIL_HOST_USER", None) EMAIL_HOST_PASSWORD = os.getenv("DJANGO_EMAIL_HOST_PASSWORD", None) -EMAIL_USE_TLS = os.getenv("DJANGO_EMAIL_USE_TLS", 'true') in ('true', '1', 't') +EMAIL_USE_TLS = os.getenv("DJANGO_EMAIL_USE_TLS", "true") in ("true", "1", "t") + +AUTH_USER_MODEL = "repapp.CustomUser" -AUTH_USER_MODEL = "repapp_users.CustomUser" AUTHENTICATION_BACKENDS = [ - "repapp_users.backends.EmailBackend", + "repapp.backends.EmailBackend", "django.contrib.auth.backends.ModelBackend", - # 'mozilla_django_oidc.auth.OIDCAuthenticationBackend', - 'repapp_users.backends.KeycloakOIDCAB' + # "mozilla_django_oidc.auth.OIDCAuthenticationBackend", + "repapp.backends.KeycloakOIDCAB", + "repapp.backends.OneTimeLoginBackend", ] -LOGIN_REDIRECT_URL = '/guest/profile/' +LOGIN_REDIRECT_URL = "/guest/profile/" OIDC_RP_CLIENT_ID = "repapp" OIDC_RP_CLIENT_SECRET = os.getenv("DJANGO_OIDC_RP_CLIENT_SECRET", None) diff --git a/rc_hip/repapp/admin.py b/rc_hip/repapp/admin.py index 7b3578c..6f24f09 100644 --- a/rc_hip/repapp/admin.py +++ b/rc_hip/repapp/admin.py @@ -4,12 +4,18 @@ Admin interface configuration for RepApp. from django.contrib import admin from import_export.admin import ImportExportModelAdmin, ExportActionMixin from import_export import resources -from .models import (Organisator, Cafe, Question, Device, - Reparateur, Appointment, Guest, Candidate) +from .models import (Organisator, Cafe, Question, Device, CustomUser, + Reparateur, Appointment, Guest, Candidate, OneTimeLogin) class GuestResource(resources.ModelResource): + """ + Resource wrapper for Guest model. + """ class Meta: + """ + Meta data for resource model. + """ model = Guest @staticmethod @@ -18,7 +24,13 @@ class GuestResource(resources.ModelResource): class DeviceResource(resources.ModelResource): + """ + Resource wrapper for Device model. + """ class Meta: + """ + Meta data for resource model. + """ model = Device @staticmethod @@ -27,7 +39,13 @@ class DeviceResource(resources.ModelResource): class QuestionResource(resources.ModelResource): + """ + Resource wrapper for Question model. + """ class Meta: + """ + Meta data for resource model. + """ model = Question @staticmethod @@ -36,7 +54,13 @@ class QuestionResource(resources.ModelResource): class AppointmentResource(resources.ModelResource): + """ + Resource wrapper for Appointment model. + """ class Meta: + """ + Meta data for resource model. + """ model = Appointment @staticmethod @@ -45,7 +69,13 @@ class AppointmentResource(resources.ModelResource): class CandidateResource(resources.ModelResource): + """ + Resource wrapper for Candidate model. + """ class Meta: + """ + Meta data for resource model. + """ model = Candidate @staticmethod @@ -54,6 +84,9 @@ class CandidateResource(resources.ModelResource): class ExportAdmin(ImportExportModelAdmin, ExportActionMixin): + """ + Admin model for data export, supporting buttons and action. + """ resource_classes = [ DeviceResource, GuestResource, @@ -71,3 +104,5 @@ admin.site.register(Device, ExportAdmin) admin.site.register(Reparateur) admin.site.register(Appointment, ExportAdmin) admin.site.register(Candidate, ExportAdmin) +admin.site.register(CustomUser) +admin.site.register(OneTimeLogin) diff --git a/rc_hip/repapp/apps.py b/rc_hip/repapp/apps.py index 67bb157..3d8d8f3 100644 --- a/rc_hip/repapp/apps.py +++ b/rc_hip/repapp/apps.py @@ -1,6 +1,12 @@ +""" +RepApp application. +""" from django.apps import AppConfig class RepappConfig(AppConfig): + """ + Django config for RepApp application. + """ default_auto_field = 'django.db.models.BigAutoField' name = 'repapp' diff --git a/rc_hip/repapp/backends.py b/rc_hip/repapp/backends.py new file mode 100644 index 0000000..126dcf8 --- /dev/null +++ b/rc_hip/repapp/backends.py @@ -0,0 +1,65 @@ +""" +Authentication backends for RepApp. +""" +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend +from mozilla_django_oidc.auth import OIDCAuthenticationBackend +from .utils import generate_username, create_repapp_user +from .models import OneTimeLogin + + +class OneTimeLoginBackend(ModelBackend): + """ + OneTimeLoginBackend implements a login using a onetime secret. + """ + + def authenticate(self, request, username=None, password=None, **kwargs): + print(f'authenticate {username}') + login = OneTimeLogin.objects.filter(secret=username).first() + if login: + return login.user + else: + return None + + +class EmailBackend(ModelBackend): + """ + EmailBackend allows a login using email address as username. + """ + + def authenticate(self, request, username=None, password=None, **kwargs): + userModel = get_user_model() + try: + user = userModel.objects.get(email=username) + except userModel.DoesNotExist: + return None + else: + if user.check_password(password): + return user + return None + + +class KeycloakOIDCAB(OIDCAuthenticationBackend): + """ + KeycloakOIDCAB allows a login using Open ID Connect (with the Repair-Café Keycloak Single Sign On server) + """ + + def create_user(self, claims): + user = super(KeycloakOIDCAB, self).create_user(claims) + + user.username = claims.get( + 'preferred_username', generate_username(user.email)) + user.save() + + create_repapp_user(user) + + return user + + def update_user(self, user, claims): + user.username = claims.get( + 'preferred_username', generate_username(user.email)) + user.save() + + create_repapp_user(user) + + return user diff --git a/rc_hip/repapp/forms.py b/rc_hip/repapp/forms.py index 376b9e3..9193c73 100644 --- a/rc_hip/repapp/forms.py +++ b/rc_hip/repapp/forms.py @@ -1,3 +1,6 @@ +""" +Django forms for RepApp. +""" from django import forms from django.core.exceptions import ValidationError diff --git a/rc_hip/repapp/migrations/0001_initial.py b/rc_hip/repapp/migrations/0001_initial.py index c5d90c0..6d14d46 100644 --- a/rc_hip/repapp/migrations/0001_initial.py +++ b/rc_hip/repapp/migrations/0001_initial.py @@ -1,7 +1,12 @@ -# Generated by Django 4.2 on 2023-04-16 17:49 +# Generated by Django 4.2 on 2023-04-24 15:16 +from django.conf import settings +import django.contrib.auth.models +import django.contrib.auth.validators from django.db import migrations, models import django.db.models.deletion +import django.utils.timezone +import repapp.models class Migration(migrations.Migration): @@ -9,54 +14,170 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ migrations.CreateModel( - name='Cafe', + name='CustomUser', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('ort', models.CharField(max_length=200)), - ('datum', models.DateField()), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('email', models.EmailField(max_length=254, unique=True, verbose_name='eMail Adresse')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'Benutzer', + 'verbose_name_plural': 'Benutzer', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), ], ), migrations.CreateModel( - name='Gerät', + name='Cafe', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('identifier', models.CharField(max_length=200)), - ('besitzer', models.CharField(max_length=200)), - ('mail', models.CharField(max_length=200)), - ('gerät', models.CharField(max_length=200)), - ('fehler', models.TextField()), - ('folgetermin', models.BooleanField()), + ('location', models.CharField(max_length=200, verbose_name='Ort')), + ('address', models.CharField(max_length=200, verbose_name='Adresse')), + ('event_date', models.DateField(verbose_name='Datum')), ], + options={ + 'verbose_name': 'Repair-Café', + 'verbose_name_plural': 'Repair-Cafés', + }, + ), + migrations.CreateModel( + name='Device', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('identifier', models.CharField(max_length=200, verbose_name='ID')), + ('device', models.CharField(max_length=200, verbose_name='Art des Geräts')), + ('manufacturer', models.CharField(max_length=200, verbose_name='Hersteller & Modell/Typ')), + ('error', models.TextField(verbose_name='Fehlerbeschreibung')), + ('follow_up', models.BooleanField(verbose_name='Folgetermin')), + ('device_picture', models.FileField(null=True, upload_to=repapp.models.device_directory_path, verbose_name='Foto vom Gerät')), + ('type_plate_picture', models.FileField(null=True, upload_to=repapp.models.device_directory_path, verbose_name='Foto vom Typenschild')), + ('confirmed', models.BooleanField(verbose_name='Bestätigung gesendet?')), + ('date', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellungsdatum')), + ('cafe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café')), + ], + options={ + 'verbose_name': 'Gerät', + 'verbose_name_plural': 'Geräte', + }, + ), + migrations.CreateModel( + name='Guest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('identifier', models.CharField(max_length=200, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('phone', models.CharField(max_length=200, verbose_name='Telefonnummer')), + ('residence', models.CharField(max_length=200, verbose_name='Wohnort')), + ('mail', models.CharField(max_length=200, verbose_name='eMail')), + ], + options={ + 'verbose_name': 'Gast', + 'verbose_name_plural': 'Gäste', + }, ), migrations.CreateModel( name='Organisator', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=200)), - ('mail', models.CharField(max_length=200)), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('mail', models.CharField(max_length=200, verbose_name='eMail')), ], + options={ + 'verbose_name': 'Organisator', + 'verbose_name_plural': 'Organisatoren', + }, ), migrations.CreateModel( name='Reparateur', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=200)), - ('mail', models.CharField(max_length=200)), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('mail', models.CharField(max_length=200, verbose_name='eMail')), ], + options={ + 'verbose_name': 'Reparateur', + 'verbose_name_plural': 'Reparateure', + }, ), migrations.CreateModel( - name='Termin', + name='Question', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uhrzeit', models.TimeField()), - ('bestätigt', models.BooleanField()), - ('cafeid', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='repapp.cafe')), - ('gerätid', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='repapp.gerät')), - ('reparateurid', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='repapp.reparateur')), + ('question', models.TextField(verbose_name='Frage')), + ('answer', models.TextField(verbose_name='Antwort')), + ('date', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellungsdatum')), + ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät')), + ('organisator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.organisator', verbose_name='Organisator')), + ('reparateur', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur', verbose_name='Reparateur')), ], + options={ + 'verbose_name': 'Frage', + 'verbose_name_plural': 'Fragen', + }, + ), + migrations.CreateModel( + name='OneTimeLogin', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('secret', models.CharField(max_length=200, unique=True, verbose_name='secret')), + ('url', models.CharField(max_length=200, verbose_name='URL')), + ('created', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellungsdatum')), + ('login_used', models.BooleanField(default=False, verbose_name='Login benutzt?')), + ('login_date', models.DateField(null=True, verbose_name='Login Datum')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Benutzer')), + ], + options={ + 'verbose_name': 'Einmal-Login', + 'verbose_name_plural': 'Einmal-Login', + }, + ), + migrations.AddField( + model_name='device', + name='guest', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.guest', verbose_name='Gast'), + ), + migrations.CreateModel( + name='Candidate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('confirmed', models.BooleanField(verbose_name='bestätigt')), + ('cafe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café')), + ('device', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät')), + ], + options={ + 'verbose_name': 'Kandidat', + 'verbose_name_plural': 'Kandidaten', + }, + ), + migrations.CreateModel( + name='Appointment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time', models.TimeField(verbose_name='Zeit')), + ('confirmed', models.BooleanField(verbose_name='bestätigt')), + ('cafe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café')), + ('device', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät')), + ('reparateur', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur', verbose_name='Reparateur')), + ], + options={ + 'verbose_name': 'Termin', + 'verbose_name_plural': 'Termine', + }, ), ] diff --git a/rc_hip/repapp/migrations/0002_alter_termin_cafeid_alter_termin_gerätid_and_more.py b/rc_hip/repapp/migrations/0002_alter_termin_cafeid_alter_termin_gerätid_and_more.py deleted file mode 100644 index ef73fc2..0000000 --- a/rc_hip/repapp/migrations/0002_alter_termin_cafeid_alter_termin_gerätid_and_more.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 4.2 on 2023-04-16 17:53 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('repapp', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='termin', - name='cafeid', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe'), - ), - migrations.AlterField( - model_name='termin', - name='gerätid', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.gerät'), - ), - migrations.AlterField( - model_name='termin', - name='reparateurid', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur'), - ), - migrations.CreateModel( - name='Frage', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('frage', models.TextField()), - ('antwort', models.TextField()), - ('datum', models.DateField()), - ('gerätid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.gerät')), - ('organisatorid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.organisator')), - ('reparateurid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur')), - ], - ), - ] diff --git a/rc_hip/repapp/migrations/0003_alter_frage_organisatorid_alter_frage_reparateurid_and_more.py b/rc_hip/repapp/migrations/0003_alter_frage_organisatorid_alter_frage_reparateurid_and_more.py deleted file mode 100644 index 97fdd74..0000000 --- a/rc_hip/repapp/migrations/0003_alter_frage_organisatorid_alter_frage_reparateurid_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 4.2 on 2023-04-16 18:12 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('repapp', '0002_alter_termin_cafeid_alter_termin_gerätid_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='frage', - name='organisatorid', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.organisator'), - ), - migrations.AlterField( - model_name='frage', - name='reparateurid', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur'), - ), - migrations.AlterField( - model_name='termin', - name='gerätid', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.gerät'), - ), - ] diff --git a/rc_hip/repapp/migrations/0004_appointment_device_question_remove_termin_cafeid_and_more.py b/rc_hip/repapp/migrations/0004_appointment_device_question_remove_termin_cafeid_and_more.py deleted file mode 100644 index c3a132a..0000000 --- a/rc_hip/repapp/migrations/0004_appointment_device_question_remove_termin_cafeid_and_more.py +++ /dev/null @@ -1,153 +0,0 @@ -# Generated by Django 4.2 on 2023-04-16 18:44 - -import datetime -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('repapp', '0003_alter_frage_organisatorid_alter_frage_reparateurid_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='Appointment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.TimeField(verbose_name='Zeit')), - ('confirmed', models.BooleanField(verbose_name='bestätigt')), - ], - options={ - 'verbose_name': 'Termin', - 'verbose_name_plural': 'Termine', - }, - ), - migrations.CreateModel( - name='Device', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('identifier', models.CharField(max_length=200, verbose_name='ID')), - ('owner', models.CharField(max_length=200, verbose_name='Besitzer')), - ('mail', models.CharField(max_length=200, verbose_name='eMail')), - ('device', models.CharField(max_length=200, verbose_name='Gerät')), - ('error', models.TextField(verbose_name='Fehler')), - ('follow_up', models.BooleanField(verbose_name='Folgetermin')), - ], - options={ - 'verbose_name': 'Gerät', - 'verbose_name_plural': 'Geräte', - }, - ), - migrations.CreateModel( - name='Question', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('question', models.TextField(verbose_name='Frage')), - ('answer', models.TextField(verbose_name='Antwort')), - ('date', models.DateField(verbose_name='Erstellungsdatum')), - ('deviceid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät')), - ], - options={ - 'verbose_name': 'Frage', - 'verbose_name_plural': 'Fragen', - }, - ), - migrations.RemoveField( - model_name='termin', - name='cafeid', - ), - migrations.RemoveField( - model_name='termin', - name='gerätid', - ), - migrations.RemoveField( - model_name='termin', - name='reparateurid', - ), - migrations.AlterModelOptions( - name='cafe', - options={'verbose_name': 'Repair-Café', 'verbose_name_plural': 'Repair-Cafés'}, - ), - migrations.AlterModelOptions( - name='organisator', - options={'verbose_name': 'Organisator', 'verbose_name_plural': 'Organisatoren'}, - ), - migrations.AlterModelOptions( - name='reparateur', - options={'verbose_name': 'Reparateur', 'verbose_name_plural': 'Reparateure'}, - ), - migrations.RemoveField( - model_name='cafe', - name='datum', - ), - migrations.RemoveField( - model_name='cafe', - name='ort', - ), - migrations.AddField( - model_name='cafe', - name='event_date', - field=models.DateField(default=datetime.date(2023, 4, 16), verbose_name='Datum'), - ), - migrations.AddField( - model_name='cafe', - name='location', - field=models.CharField(default='', max_length=200, verbose_name='Ort'), - ), - migrations.AlterField( - model_name='organisator', - name='mail', - field=models.CharField(max_length=200, verbose_name='eMail'), - ), - migrations.AlterField( - model_name='organisator', - name='name', - field=models.CharField(max_length=200, verbose_name='Name'), - ), - migrations.AlterField( - model_name='reparateur', - name='mail', - field=models.CharField(max_length=200, verbose_name='eMail'), - ), - migrations.AlterField( - model_name='reparateur', - name='name', - field=models.CharField(max_length=200, verbose_name='Name'), - ), - migrations.DeleteModel( - name='Frage', - ), - migrations.DeleteModel( - name='Gerät', - ), - migrations.DeleteModel( - name='Termin', - ), - migrations.AddField( - model_name='question', - name='organisatorid', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.organisator', verbose_name='Organisator'), - ), - migrations.AddField( - model_name='question', - name='reparateurid', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur', verbose_name='Reparateur'), - ), - migrations.AddField( - model_name='appointment', - name='cafeid', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café'), - ), - migrations.AddField( - model_name='appointment', - name='deviceid', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät'), - ), - migrations.AddField( - model_name='appointment', - name='reparateurid', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur', verbose_name='Reparateur'), - ), - ] diff --git a/rc_hip/repapp/migrations/0005_alter_cafe_event_date_alter_cafe_location.py b/rc_hip/repapp/migrations/0005_alter_cafe_event_date_alter_cafe_location.py deleted file mode 100644 index 1deaaa8..0000000 --- a/rc_hip/repapp/migrations/0005_alter_cafe_event_date_alter_cafe_location.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2 on 2023-04-16 18:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('repapp', '0004_appointment_device_question_remove_termin_cafeid_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='cafe', - name='event_date', - field=models.DateField(verbose_name='Datum'), - ), - migrations.AlterField( - model_name='cafe', - name='location', - field=models.CharField(max_length=200, verbose_name='Ort'), - ), - ] diff --git a/rc_hip/repapp/migrations/0006_device_phone_device_residence.py b/rc_hip/repapp/migrations/0006_device_phone_device_residence.py deleted file mode 100644 index 39d70b1..0000000 --- a/rc_hip/repapp/migrations/0006_device_phone_device_residence.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2 on 2023-04-16 19:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('repapp', '0005_alter_cafe_event_date_alter_cafe_location'), - ] - - operations = [ - migrations.AddField( - model_name='device', - name='phone', - field=models.CharField(default='', max_length=200, verbose_name='Telefonnummer'), - ), - migrations.AddField( - model_name='device', - name='residence', - field=models.CharField(default='', max_length=200, verbose_name='Wohnort'), - ), - ] diff --git a/rc_hip/repapp/migrations/0007_alter_device_phone_alter_device_residence.py b/rc_hip/repapp/migrations/0007_alter_device_phone_alter_device_residence.py deleted file mode 100644 index 21d8d27..0000000 --- a/rc_hip/repapp/migrations/0007_alter_device_phone_alter_device_residence.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2 on 2023-04-16 19:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('repapp', '0006_device_phone_device_residence'), - ] - - operations = [ - migrations.AlterField( - model_name='device', - name='phone', - field=models.CharField(max_length=200, verbose_name='Telefonnummer'), - ), - migrations.AlterField( - model_name='device', - name='residence', - field=models.CharField(max_length=200, verbose_name='Wohnort'), - ), - ] diff --git a/rc_hip/repapp/migrations/0008_guest_remove_device_mail_remove_device_owner_and_more.py b/rc_hip/repapp/migrations/0008_guest_remove_device_mail_remove_device_owner_and_more.py deleted file mode 100644 index 77829c8..0000000 --- a/rc_hip/repapp/migrations/0008_guest_remove_device_mail_remove_device_owner_and_more.py +++ /dev/null @@ -1,50 +0,0 @@ -# Generated by Django 4.2 on 2023-04-17 05:26 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('repapp', '0007_alter_device_phone_alter_device_residence'), - ] - - operations = [ - migrations.CreateModel( - name='Guest', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('identifier', models.CharField(max_length=200, verbose_name='ID')), - ('name', models.CharField(max_length=200, verbose_name='Name')), - ('phone', models.CharField(max_length=200, verbose_name='Telefonnummer')), - ('residence', models.CharField(max_length=200, verbose_name='Wohnort')), - ('mail', models.CharField(max_length=200, verbose_name='eMail')), - ], - options={ - 'verbose_name': 'Gast', - 'verbose_name_plural': 'Gäste', - }, - ), - migrations.RemoveField( - model_name='device', - name='mail', - ), - migrations.RemoveField( - model_name='device', - name='owner', - ), - migrations.RemoveField( - model_name='device', - name='phone', - ), - migrations.RemoveField( - model_name='device', - name='residence', - ), - migrations.AddField( - model_name='device', - name='guestid', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.guest', verbose_name='Gast'), - ), - ] diff --git a/rc_hip/repapp/migrations/0009_rename_cafeid_appointment_cafe_id_and_more.py b/rc_hip/repapp/migrations/0009_rename_cafeid_appointment_cafe_id_and_more.py deleted file mode 100644 index bb3ed52..0000000 --- a/rc_hip/repapp/migrations/0009_rename_cafeid_appointment_cafe_id_and_more.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 4.2 on 2023-04-17 07:40 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('repapp', '0008_guest_remove_device_mail_remove_device_owner_and_more'), - ] - - operations = [ - migrations.RenameField( - model_name='appointment', - old_name='cafeid', - new_name='cafe_id', - ), - migrations.RenameField( - model_name='appointment', - old_name='deviceid', - new_name='device_id', - ), - migrations.RenameField( - model_name='appointment', - old_name='reparateurid', - new_name='reparateur_id', - ), - migrations.RenameField( - model_name='device', - old_name='guestid', - new_name='guest_id', - ), - migrations.RenameField( - model_name='question', - old_name='deviceid', - new_name='device_id', - ), - migrations.RenameField( - model_name='question', - old_name='organisatorid', - new_name='organisator_id', - ), - migrations.RenameField( - model_name='question', - old_name='reparateurid', - new_name='reparateur_id', - ), - ] diff --git a/rc_hip/repapp/migrations/0010_candidate.py b/rc_hip/repapp/migrations/0010_candidate.py deleted file mode 100644 index 78ef11d..0000000 --- a/rc_hip/repapp/migrations/0010_candidate.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 4.2 on 2023-04-17 07:57 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('repapp', '0009_rename_cafeid_appointment_cafe_id_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='Candidate', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('confirmed', models.BooleanField(verbose_name='bestätigt')), - ('cafe_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café')), - ('device_id', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät')), - ], - options={ - 'verbose_name': 'Kandidat', - 'verbose_name_plural': 'Kandidaten', - }, - ), - ] diff --git a/rc_hip/repapp/migrations/0011_rename_cafe_id_appointment_cafe_and_more.py b/rc_hip/repapp/migrations/0011_rename_cafe_id_appointment_cafe_and_more.py deleted file mode 100644 index 2ecef41..0000000 --- a/rc_hip/repapp/migrations/0011_rename_cafe_id_appointment_cafe_and_more.py +++ /dev/null @@ -1,80 +0,0 @@ -# Generated by Django 4.2 on 2023-04-20 18:43 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('repapp', '0010_candidate'), - ] - - operations = [ - migrations.RenameField( - model_name='appointment', - old_name='cafe_id', - new_name='cafe', - ), - migrations.RenameField( - model_name='appointment', - old_name='device_id', - new_name='device', - ), - migrations.RenameField( - model_name='appointment', - old_name='reparateur_id', - new_name='reparateur', - ), - migrations.RenameField( - model_name='candidate', - old_name='cafe_id', - new_name='cafe', - ), - migrations.RenameField( - model_name='candidate', - old_name='device_id', - new_name='device', - ), - migrations.RenameField( - model_name='device', - old_name='guest_id', - new_name='guest', - ), - migrations.RenameField( - model_name='question', - old_name='device_id', - new_name='device', - ), - migrations.RenameField( - model_name='question', - old_name='organisator_id', - new_name='organisator', - ), - migrations.RenameField( - model_name='question', - old_name='reparateur_id', - new_name='reparateur', - ), - migrations.AddField( - model_name='device', - name='cafe', - field=models.ForeignKey(default=2, on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café'), - preserve_default=False, - ), - migrations.AddField( - model_name='device', - name='confirmed', - field=models.BooleanField(default=False, verbose_name='Bestätigt'), - ), - migrations.AddField( - model_name='device', - name='secret', - field=models.CharField(default='TEST12345', max_length=200, verbose_name='Bestätigungscode'), - ), - migrations.AddField( - model_name='guest', - name='confirmed', - field=models.BooleanField(default=False, verbose_name='Bestätigt'), - ), - ] diff --git a/rc_hip/repapp/migrations/0012_remove_device_secret_remove_guest_confirmed_and_more.py b/rc_hip/repapp/migrations/0012_remove_device_secret_remove_guest_confirmed_and_more.py deleted file mode 100644 index cd4999e..0000000 --- a/rc_hip/repapp/migrations/0012_remove_device_secret_remove_guest_confirmed_and_more.py +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by Django 4.2 on 2023-04-21 15:21 - -from django.db import migrations, models -import repapp.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('repapp', '0011_rename_cafe_id_appointment_cafe_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='device', - name='secret', - ), - migrations.RemoveField( - model_name='guest', - name='confirmed', - ), - migrations.AddField( - model_name='cafe', - name='address', - field=models.CharField(default='', max_length=200, verbose_name='Ort'), - preserve_default=False, - ), - migrations.AddField( - model_name='device', - name='device_picture', - field=models.FileField(null=True, upload_to=repapp.models.device_directory_path, verbose_name='Foto vom Gerät'), - ), - migrations.AddField( - model_name='device', - name='manufacturer', - field=models.CharField(default='', max_length=200, verbose_name='Hersteller'), - preserve_default=False, - ), - migrations.AddField( - model_name='device', - name='type_plate_picture', - field=models.FileField(null=True, upload_to=repapp.models.device_directory_path, verbose_name='Foto vom Typenschild'), - ), - migrations.AlterField( - model_name='device', - name='confirmed', - field=models.BooleanField(verbose_name='Bestätigung gesendet?'), - ), - ] diff --git a/rc_hip/repapp/migrations/0013_alter_cafe_address_alter_device_device_and_more.py b/rc_hip/repapp/migrations/0013_alter_cafe_address_alter_device_device_and_more.py deleted file mode 100644 index dd81294..0000000 --- a/rc_hip/repapp/migrations/0013_alter_cafe_address_alter_device_device_and_more.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 4.2 on 2023-04-23 10:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('repapp', '0012_remove_device_secret_remove_guest_confirmed_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='cafe', - name='address', - field=models.CharField(max_length=200, verbose_name='Adresse'), - ), - migrations.AlterField( - model_name='device', - name='device', - field=models.CharField( - max_length=200, verbose_name='Art des Geräts'), - ), - migrations.AlterField( - model_name='device', - name='error', - field=models.TextField(verbose_name='Fehlerbeschreibung'), - ), - migrations.AlterField( - model_name='device', - name='manufacturer', - field=models.CharField( - max_length=200, verbose_name='Hersteller & Modell/Typ'), - ), - ] diff --git a/rc_hip/repapp/models.py b/rc_hip/repapp/models.py index 9b3727f..8313030 100644 --- a/rc_hip/repapp/models.py +++ b/rc_hip/repapp/models.py @@ -4,6 +4,7 @@ This module implements the models of RepApp. import django.utils.timezone from django.db import models from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.models import AbstractUser class Cafe(models.Model): @@ -73,6 +74,9 @@ class Guest(models.Model): def device_directory_path(instance, filename): + """ + device_directory_path generates a device-specific storage path for file uploads. + """ return f'device_{instance.identifier}/{filename}' @@ -168,3 +172,38 @@ class Candidate(models.Model): def __str__(self): return f'Kandidat {self.cafe.event_date} für Gerät {self.device.device}' + + +class CustomUser(AbstractUser): + email = models.EmailField(unique=True, verbose_name=_("eMail Adresse")) + + class Meta: + verbose_name = _('Benutzer') + verbose_name_plural = _('Benutzer') + + def __str__(self): + return f'{self.email}' + + +class OneTimeLogin(models.Model): + """ + A OneTimeLogin is a secret which can be used once to login a user. + """ + secret = models.CharField(max_length=200, verbose_name=_( + "secret"), unique=True, null=False) + user = models.ForeignKey( + CustomUser, on_delete=models.CASCADE, null=False, verbose_name=_("Benutzer")) + url = models.CharField(max_length=200, verbose_name=_("URL")) + created = models.DateField(verbose_name=_( + "Erstellungsdatum"), default=django.utils.timezone.now) + login_used = models.BooleanField( + verbose_name=_("Login benutzt?"), default=False) + login_date = models.DateField(verbose_name=_( + "Login Datum"), null=True) + + class Meta: + verbose_name = _('Einmal-Login') + verbose_name_plural = _('Einmal-Login') + + def __str__(self): + return f'Einmal-Login {self.user.email}' diff --git a/rc_hip/repapp_users/templates/registration/login.html b/rc_hip/repapp/templates/registration/login.html similarity index 100% rename from rc_hip/repapp_users/templates/registration/login.html rename to rc_hip/repapp/templates/registration/login.html diff --git a/rc_hip/repapp/templates/repapp/mail/mail_new_guest_html.html b/rc_hip/repapp/templates/repapp/mail/mail_new_guest_html.html new file mode 100644 index 0000000..5eeb1f3 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/mail/mail_new_guest_html.html @@ -0,0 +1,19 @@ + + + + + + + Repair-Café Hilpoltstein - Benutzerkonto + + +

Guten Tag,

+

+ wir haben ein Benutzerkonto für Sie erstellt. Klicken sie hier um ihre Daten einzusehen und zu ändern. +

+

Ihr Anmeldedaten sind:

+

Benutzername: {{ username }}

+

Passwort: {{ password }}

+

Herzliche Grüße sendet dir das Organisationsteam vom Repair-Café Hilpoltstein!

+ + diff --git a/rc_hip/repapp/templates/repapp/mail/mail_new_guest_subject.html b/rc_hip/repapp/templates/repapp/mail/mail_new_guest_subject.html new file mode 100644 index 0000000..345b027 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/mail/mail_new_guest_subject.html @@ -0,0 +1 @@ +Repair-Café Hilpoltstein - Benutzerkonto diff --git a/rc_hip/repapp/templates/repapp/mail/mail_new_guest_text.html b/rc_hip/repapp/templates/repapp/mail/mail_new_guest_text.html new file mode 100644 index 0000000..9e7beb6 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/mail/mail_new_guest_text.html @@ -0,0 +1,6 @@ +Guten Tag, +wir haben ein Benutzerkonto für Sie erstellt. Sie können sich im Web-Broswer unter {{ link }} um ihre Daten einzusehen und zu ändern. +Ihr Anmeldedaten sind: +Benutzername: {{ username }} +Passwort: {{ password }} +Herzliche Grüße sendet dir das Organisationsteam vom Repair-Café Hilpoltstein! diff --git a/rc_hip/repapp/templates/repapp/mail/mail_one_time_login_html.html b/rc_hip/repapp/templates/repapp/mail/mail_one_time_login_html.html new file mode 100644 index 0000000..00e5bc2 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/mail/mail_one_time_login_html.html @@ -0,0 +1,16 @@ + + + + + + + Einmal-Anmeldelink + + +

Guten Tag,

+

+ wir haben für Sie einen Einmal-Ammeldelink erstellt. Wenn sie hier klicken können sie auf ihre Informationen zugreifen. +

+

Herzliche Grüße sendet dir das Organisationsteam vom Repair-Café Hilpoltstein!

+ + diff --git a/rc_hip/repapp/templates/repapp/mail/mail_one_time_login_subject.html b/rc_hip/repapp/templates/repapp/mail/mail_one_time_login_subject.html new file mode 100644 index 0000000..ec2c086 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/mail/mail_one_time_login_subject.html @@ -0,0 +1 @@ +Repair-Café Hilpoltstein - Einmal-Ammeldelink diff --git a/rc_hip/repapp/templates/repapp/mail/mail_one_time_login_text.html b/rc_hip/repapp/templates/repapp/mail/mail_one_time_login_text.html new file mode 100644 index 0000000..4c28274 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/mail/mail_one_time_login_text.html @@ -0,0 +1,3 @@ +Guten Tag, +wir haben für Sie einen Einmal-Ammeldelink erstellt. Wenn sie {{ link }} im Web-Broswer öffnen können sie auf ihre Informationen zugreifen. +Herzliche Grüße sendet dir das Organisationsteam vom Repair-Café Hilpoltstein! diff --git a/rc_hip/repapp/templates/repapp/mail/mail_register_device_html.html b/rc_hip/repapp/templates/repapp/mail/mail_register_device_html.html index 99b3537..b3a9b3a 100644 --- a/rc_hip/repapp/templates/repapp/mail/mail_register_device_html.html +++ b/rc_hip/repapp/templates/repapp/mail/mail_register_device_html.html @@ -1,5 +1,11 @@ + + + + + Repair-Café Hilpoltstein Geräteanmeldung +

Guten Tag,

@@ -8,6 +14,9 @@

Vielen Dank für die Anmeldung des Geräts {{ device.device }} - {{ device.manufacturer }} zur Reparatur im Repair-Café am {{ cafe.event_date|date }}.

+

+ Sie können die Daten zu ihrer Geräteanmeldung hier einsehen. +

Die Organisation unserer Repair-Cafés beginnt i.a. einige Tage vor dem jeweiligen Termin. Dann erstellen wir anhand der angemeldeten Geräte und der Spezialgebiete der anwesenden Reparateure einen Zeitplan.

diff --git a/rc_hip/repapp/templates/repapp/mail/mail_register_device_text.html b/rc_hip/repapp/templates/repapp/mail/mail_register_device_text.html index 30110e2..775aa7e 100644 --- a/rc_hip/repapp/templates/repapp/mail/mail_register_device_text.html +++ b/rc_hip/repapp/templates/repapp/mail/mail_register_device_text.html @@ -1,6 +1,7 @@ Guten Tag, dies ist eine automatische Nachricht, die du erhältst, nachdem du unser Anmeldeformular abgesendet hast und deine Angaben in unserer Datenbank gespeichert wurden. Vielen Dank für die Anmeldung des Geräts {{ device.device }} - {{ device.manufacturer }} zur Reparatur im Repair-Café am {{ cafe.event_date|date }}. +Sie können die Daten zu ihrer Geräteanmeldung hier einsehen. Die Organisation unserer Repair-Cafés beginnt i.a. einige Tage vor dem jeweiligen Termin. Dann erstellen wir anhand der angemeldeten Geräte und der Spezialgebiete der anwesenden Reparateure einen Zeitplan. Sobald dieser Zeitplan fertig gestellt ist, erhältst du von einem Mitglied unseres Organisationsteams eine Mail. Diese beinhaltet im Normalfall den Reparaturtermin inkl. der Uhrzeit, für die die Reparatur vorgesehen ist. In Ausnahmefällen (z.B. wenn kein Spezialist für die Gerätegruppe anwesend sein wird oder überbucht ist) ist es aber auch möglich, dass wir deinen gewünschten Termin absagen und dir einen Alternativtermin vorschlagen müssen. Falls du in der Zwischenzeit Rückfragen hast oder deinen Wunschtermin doch nicht wahrnehmen kannst, antworte bitte einfach auf diese Nachricht. diff --git a/rc_hip/repapp/tests.py b/rc_hip/repapp/tests.py index 7ce503c..a39b155 100644 --- a/rc_hip/repapp/tests.py +++ b/rc_hip/repapp/tests.py @@ -1,3 +1 @@ -from django.test import TestCase - # Create your tests here. diff --git a/rc_hip/repapp/urls.py b/rc_hip/repapp/urls.py index 24277d5..d4aac47 100644 --- a/rc_hip/repapp/urls.py +++ b/rc_hip/repapp/urls.py @@ -2,7 +2,6 @@ Urls of RepApp. """ from django.urls import path - from . import views urlpatterns = [ @@ -25,4 +24,8 @@ urlpatterns = [ views.profile, name="guest_profile"), path("member/login/", views.member_login, name="member_login"), + path("cron", views.cron, name="cron"), + path("process_mails", views.process_mails, name="process_mails"), + path("onetimelogin//", + views.one_time_login, name="one_time_login"), ] diff --git a/rc_hip/repapp/utils.py b/rc_hip/repapp/utils.py new file mode 100644 index 0000000..b1623f6 --- /dev/null +++ b/rc_hip/repapp/utils.py @@ -0,0 +1,171 @@ +import os +import unicodedata +import datetime +import random +from hashlib import sha256 +from django.template.loader import render_to_string +from django.urls import reverse +from django.core.mail import send_mail +from django.contrib import messages +from repapp.models import Organisator, Reparateur, OneTimeLogin + + +def create_one_time_login(user, url): + """ + create_one_time_login create a one time login object for user logins. + """ + secret = sha256( + f'{user.email}{url}{datetime.datetime.now()}{random.randint(0,9999999)}'.encode( + 'utf-8') + ).hexdigest() + otl = OneTimeLogin( + secret=secret, + user=user, + url=url, + ) + otl.save() + return otl + + +def send_one_time_login_mail(one_time_login, request): + url = request.build_absolute_uri( + f'/onetimelogin/{one_time_login.secret}/') + subject = render_to_string( + 'repapp/mail/mail_one_time_login_subject.html').replace('\n', '') + text = render_to_string('repapp/mail/mail_one_time_login_text.html', { + 'link': url, + }) + html = render_to_string('repapp/mail/mail_one_time_login_html.html', { + 'link': url, + }) + + ok = send_mail( + subject=subject, + message=text, + from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""), + recipient_list=[f"{one_time_login.user.email}"], + fail_silently=True, + html_message=html + ) + + if ok > 0: + messages.add_message(request, messages.INFO, + 'Sie haben einen neuen Login per eMail erhalten.') + + +def create_repapp_user(user): + organisator = Organisator.objects.filter(mail=user.email).first() + if not organisator: + reparateur = Reparateur.objects.filter(mail=user.email).first() + if not reparateur: + reparateur = Reparateur( + name=user.username, + mail=user.email, + ) + reparateur.save() + else: + reparateur.name = user.username + reparateur.save() + else: + organisator.name = user.username + organisator.save() + + +def generate_username(email): + # Using Python 3 and Django 1.11+, usernames can contain alphanumeric + # (ascii and unicode), _, @, +, . and - characters. So we normalize + # it and slice at 150 characters. + return unicodedata.normalize('NFKC', email)[:150] + + +def send_confirmation_mails(device, guest, cafe, request): + organizers = [] + for organizer in Organisator.objects.all(): + organizers.append(organizer.mail) + + text = render_to_string('repapp/mail/notice_new_device.html', { + 'guest': device.guest, + 'device': device, + 'cafe': device.cafe, + }) + + send_mail( + subject=f"Neues Gerät { device.device }", + message=text, + from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""), + recipient_list=organizers, + fail_silently=True + ) + + path = reverse('view_device', kwargs={ + 'device_identifier': device.identifier}) + url = request.build_absolute_uri(path) + otl = create_one_time_login(guest.user, url) + login_url = request.build_absolute_uri(f'/onetimelogin/{otl.secret}/') + + subject = render_to_string('repapp/mail/mail_register_device_subject.html', { + 'cafe': cafe, + }).replace('\n', '') + text = render_to_string('repapp/mail/mail_register_device_text.html', { + 'guest': guest, + 'device': device, + 'cafe': cafe, + 'login_url': login_url, + }) + html = render_to_string('repapp/mail/mail_register_device_html.html', { + 'guest': guest, + 'device': device, + 'cafe': cafe, + 'login_url': login_url, + }) + + mail_count = send_mail( + subject=subject, + message=text, + from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""), + recipient_list=[f"{guest.mail}"], + fail_silently=True, + html_message=html + ) + + if mail_count > 0: + device.confirmed = True + device.save() + else: + messages.add_message(request, messages.ERROR, + 'Fehler beim senden der Bestätigungs-eMail!') + + +def send_guest_account_mail(guest, password, request): + url = request.build_absolute_uri('/guest/profile/') + subject = render_to_string( + 'repapp/mail/mail_new_guest_subject.html').replace('\n', '') + text = render_to_string('repapp/mail/mail_new_guest_text.html', { + 'link': url, + 'username': guest.user.email, + 'password': password, + }) + html = render_to_string('repapp/mail/mail_new_guest_html.html', { + 'link': url, + 'username': guest.user.email, + 'password': password, + }) + + ok = send_mail( + subject=subject, + message=text, + from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""), + recipient_list=[f"{guest.user.email}"], + fail_silently=True, + html_message=html + ) + + if ok > 0: + messages.add_message(request, messages.INFO, + 'Sie haben ihre Benutzerdaten per eMail erhalten.') + + +def is_member(user): + organisator = Organisator.objects.filter(mail=user.email).first() + reparateur = Reparateur.objects.filter(mail=user.email).first() + return organisator or reparateur diff --git a/rc_hip/repapp/views.py b/rc_hip/repapp/views.py index 2f04e8d..02cdf9d 100644 --- a/rc_hip/repapp/views.py +++ b/rc_hip/repapp/views.py @@ -2,9 +2,9 @@ Views of RepApp. """ import datetime -import os import random import string +import time from hashlib import sha256 from django.views import generic from django.urls import reverse_lazy @@ -12,12 +12,14 @@ from django.shortcuts import get_object_or_404, render from django.http import HttpResponseRedirect from django.core.exceptions import PermissionDenied from django.contrib import messages -from django.core.mail import send_mail -from django.template.loader import render_to_string from django.contrib.auth.decorators import login_required -from repapp_users.models import CustomUser -from .models import Cafe, Device, Guest, Organisator +from django.contrib.auth import authenticate, login +from django.utils.timezone import now +from repapp.models import CustomUser +from .models import Cafe, Device, Guest, OneTimeLogin from .forms import RegisterDevice, RegisterGuest +from .utils import (send_confirmation_mails, create_one_time_login, + send_one_time_login_mail, send_guest_account_mail, is_member) class IndexView(generic.ListView): @@ -32,59 +34,10 @@ class IndexView(generic.ListView): return Cafe.objects.filter(event_date__gte=datetime.date.today()) -def send_confirmation_mails(device, guest, cafe, request): - organizers = [] - for organizer in Organisator.objects.all(): - organizers.append(organizer.mail) - - text = render_to_string('repapp/mail/notice_new_device.html', { - 'guest': device.guest, - 'device': device, - 'cafe': device.cafe, - }) - - send_mail( - subject=f"Neues Gerät { device.device }", - message=text, - from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""), - recipient_list=organizers, - fail_silently=True - ) - - subject = render_to_string('repapp/mail/mail_register_device_subject.html', { - 'guest': guest, - 'device': device, - 'cafe': cafe, - }).replace('\n', '') - text = render_to_string('repapp/mail/mail_register_device_text.html', { - 'guest': guest, - 'device': device, - 'cafe': cafe, - }) - html = render_to_string('repapp/mail/mail_register_device_html.html', { - 'guest': guest, - 'device': device, - 'cafe': cafe, - }) - - ok = send_mail( - subject=subject, - message=text, - from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""), - recipient_list=[f"{guest.mail}"], - fail_silently=True, - html_message=html - ) - - if ok > 0: - device.confirmed = True - device.save() - else: - messages.add_message(request, messages.ERROR, - 'Fehler beim senden der Bestätigungs-eMail!') - - class RegisterDeviceFormView(generic.edit.FormView): + """ + RegisterDeviceFormView shows the form for registering new devices. + """ template_name = "repapp/register_device.html" form_class = RegisterDevice @@ -187,8 +140,7 @@ class RegisterGuestFormView(generic.edit.FormView): password=password) user.save() - print(f'{mail} {password}') - # TODO: send account creation email + send_guest_account_mail(guest, password, self.request) send_confirmation_mails(device, guest, cafe, self.request) @@ -236,7 +188,8 @@ def device_view(request, device_identifier): if not device.guest: raise PermissionDenied() - # TODO: Guests can only view their devices, Reparateur and Orga can view all devices. + if not (is_member(user) or device.guest.mail == user.email): + raise PermissionDenied() return render( request, @@ -267,3 +220,38 @@ def member_login(request): request, "repapp/member_login.html" ) + + +def cron(request): + pass + + +def process_mails(request): + pass + + +def one_time_login(request, secret): + time.sleep(1) + + otl = get_object_or_404(OneTimeLogin, secret=secret) + + if otl.login_used: + messages.add_message(request, messages.ERROR, + 'Der Einmal-Login wurde schon verwendet und ist nichtmehr gültig.') + new_otl = create_one_time_login(otl.user, otl.url) + send_one_time_login_mail(new_otl, request) + return HttpResponseRedirect(reverse_lazy('index')) + else: + otl.login_used = True + otl.login_date = now() + otl.save() + + user = authenticate(request, username=secret, password=None) + + if user is not None: + login(request, user) + messages.add_message(request, messages.INFO, 'Login erfolgreich!') + return HttpResponseRedirect(otl.url) + else: + messages.add_message(request, messages.ERROR, 'Login fehlgeschlagen.') + return HttpResponseRedirect(reverse_lazy('index')) diff --git a/rc_hip/repapp_users/__init__.py b/rc_hip/repapp_users/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/rc_hip/repapp_users/admin.py b/rc_hip/repapp_users/admin.py deleted file mode 100644 index 5cd50eb..0000000 --- a/rc_hip/repapp_users/admin.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.contrib import admin -from .models import CustomUser - -admin.site.register(CustomUser) diff --git a/rc_hip/repapp_users/apps.py b/rc_hip/repapp_users/apps.py deleted file mode 100644 index 3ed1ca5..0000000 --- a/rc_hip/repapp_users/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class RepappUsersConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'repapp_users' diff --git a/rc_hip/repapp_users/backends.py b/rc_hip/repapp_users/backends.py deleted file mode 100644 index 1ee9555..0000000 --- a/rc_hip/repapp_users/backends.py +++ /dev/null @@ -1,65 +0,0 @@ -import unicodedata -from django.contrib.auth import get_user_model -from django.contrib.auth.backends import ModelBackend -from mozilla_django_oidc.auth import OIDCAuthenticationBackend -from repapp.models import Organisator, Reparateur - - -class EmailBackend(ModelBackend): - def authenticate(self, request, username=None, password=None, **kwargs): - UserModel = get_user_model() - try: - user = UserModel.objects.get(email=username) - except UserModel.DoesNotExist: - return None - else: - if user.check_password(password): - return user - return None - - -def generate_username(email): - # Using Python 3 and Django 1.11+, usernames can contain alphanumeric - # (ascii and unicode), _, @, +, . and - characters. So we normalize - # it and slice at 150 characters. - return unicodedata.normalize('NFKC', email)[:150] - - -def create_repapp_user(user): - organisator = Organisator.objects.filter(mail=user.email).first() - if not organisator: - reparateur = Reparateur.objects.filter(mail=user.email).first() - if not reparateur: - reparateur = Reparateur( - name=user.username, - mail=user.email, - ) - reparateur.save() - else: - reparateur.name = user.username - reparateur.save() - else: - organisator.name = user.username - organisator.save() - - -class KeycloakOIDCAB(OIDCAuthenticationBackend): - def create_user(self, claims): - user = super(KeycloakOIDCAB, self).create_user(claims) - - user.username = claims.get( - 'preferred_username', generate_username(user.email)) - user.save() - - create_repapp_user(user) - - return user - - def update_user(self, user, claims): - user.username = claims.get( - 'preferred_username', generate_username(user.email)) - user.save() - - create_repapp_user(user) - - return user diff --git a/rc_hip/repapp_users/migrations/0001_initial.py b/rc_hip/repapp_users/migrations/0001_initial.py deleted file mode 100644 index c67c117..0000000 --- a/rc_hip/repapp_users/migrations/0001_initial.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 4.2 on 2023-04-23 10:29 - -import django.contrib.auth.models -import django.contrib.auth.validators -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ] - - operations = [ - migrations.CreateModel( - name='CustomUser', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('email', models.EmailField(max_length=254, unique=True)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), - ], - options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - ] diff --git a/rc_hip/repapp_users/migrations/0002_alter_customuser_options_alter_customuser_email.py b/rc_hip/repapp_users/migrations/0002_alter_customuser_options_alter_customuser_email.py deleted file mode 100644 index bb1e189..0000000 --- a/rc_hip/repapp_users/migrations/0002_alter_customuser_options_alter_customuser_email.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 4.2 on 2023-04-23 11:56 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('repapp_users', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='customuser', - options={'verbose_name': 'Benutzer', 'verbose_name_plural': 'Benutzer'}, - ), - migrations.AlterField( - model_name='customuser', - name='email', - field=models.EmailField(max_length=254, unique=True, verbose_name='eMail Adresse'), - ), - ] diff --git a/rc_hip/repapp_users/migrations/__init__.py b/rc_hip/repapp_users/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/rc_hip/repapp_users/models.py b/rc_hip/repapp_users/models.py deleted file mode 100644 index 2770e12..0000000 --- a/rc_hip/repapp_users/models.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.contrib.auth.models import AbstractUser -from django.db import models -from django.utils.translation import gettext_lazy as _ - - -class CustomUser(AbstractUser): - email = models.EmailField(unique=True, verbose_name=_("eMail Adresse")) - - class Meta: - verbose_name = _('Benutzer') - verbose_name_plural = _('Benutzer') - - def __str__(self): - return f'{self.email}' diff --git a/rc_hip/repapp_users/tests.py b/rc_hip/repapp_users/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/rc_hip/repapp_users/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. From 188153623e395d9d9fdd6e9716e0544f70e03c8d Mon Sep 17 00:00:00 2001 From: Tom Irgang Date: Mon, 24 Apr 2023 19:25:29 +0200 Subject: [PATCH 8/8] add message model --- rc_hip/repapp/migrations/0002_message.py | 29 ++++++++++++++++++++++++ rc_hip/repapp/models.py | 19 ++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 rc_hip/repapp/migrations/0002_message.py diff --git a/rc_hip/repapp/migrations/0002_message.py b/rc_hip/repapp/migrations/0002_message.py new file mode 100644 index 0000000..040eb04 --- /dev/null +++ b/rc_hip/repapp/migrations/0002_message.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2 on 2023-04-24 17:25 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('repapp', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message', models.TextField(verbose_name='Nachricht')), + ('answer', models.TextField(verbose_name='Antwort')), + ('date', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellungsdatum')), + ('guest', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.guest', verbose_name='Gast')), + ], + options={ + 'verbose_name': 'Nachricht', + 'verbose_name_plural': 'Nachrichten', + }, + ), + ] diff --git a/rc_hip/repapp/models.py b/rc_hip/repapp/models.py index 8313030..833c08b 100644 --- a/rc_hip/repapp/models.py +++ b/rc_hip/repapp/models.py @@ -207,3 +207,22 @@ class OneTimeLogin(models.Model): def __str__(self): return f'Einmal-Login {self.user.email}' + + +class Message(models.Model): + """ + A Message is a request from a guest. + """ + message = models.TextField(verbose_name=_("Nachricht")) + answer = models.TextField(verbose_name=_("Antwort")) + date = models.DateField(verbose_name=_( + "Erstellungsdatum"), default=django.utils.timezone.now) + guest = models.ForeignKey( + Guest, on_delete=models.CASCADE, null=True, verbose_name=_("Gast")) + + class Meta: + verbose_name = _('Nachricht') + verbose_name_plural = _('Nachrichten') + + def __str__(self): + return f'Nachricht vom {self.date} von {self.guest.mail}'