diff --git a/cspell.json b/cspell.json index 3987eac..4b33dee 100644 --- a/cspell.json +++ b/cspell.json @@ -18,7 +18,10 @@ "Keycloak", "tabindex", "onetimelogin", - "Ammeldelink" + "Ammeldelink", + "orga", + "repa", + "organisators" ], "ignoreWords": [], "import": [] diff --git a/rc_hip/rc_hip/settings.py b/rc_hip/rc_hip/settings.py index 81459fe..78d64cf 100644 --- a/rc_hip/rc_hip/settings.py +++ b/rc_hip/rc_hip/settings.py @@ -179,10 +179,10 @@ OIDC_OP_TOKEN_ENDPOINT = "https://sso.makes-hacks-hip.de/realms/Makes-Hacks-Hip/ 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/" + LOGIN_REDIRECT_URL = "http://127.0.0.1:8000/member" LOGOUT_REDIRECT_URL = "http://127.0.0.1:8000/" else: - LOGIN_REDIRECT_URL = "https://anmeldung.repaircafe-hilpoltstein.de/" + LOGIN_REDIRECT_URL = "https://anmeldung.repaircafe-hilpoltstein.de/member" LOGOUT_REDIRECT_URL = "https://anmeldung.repaircafe-hilpoltstein.de/" diff --git a/rc_hip/repapp/fixtures/cafe-data.json b/rc_hip/repapp/fixtures/cafe-data.json index da8e763..59ff18a 100644 --- a/rc_hip/repapp/fixtures/cafe-data.json +++ b/rc_hip/repapp/fixtures/cafe-data.json @@ -5,7 +5,7 @@ "fields": { "location": "Haus Einstein' (beim Gymnasium)", "address": "Patersholzer Weg 19, Hilpoltstein", - "event_date": "2023-05-13" + "event_date": "2024-05-13" } }, { diff --git a/rc_hip/repapp/forms.py b/rc_hip/repapp/forms.py index 6c01cfa..cc8650a 100644 --- a/rc_hip/repapp/forms.py +++ b/rc_hip/repapp/forms.py @@ -19,13 +19,13 @@ class HoneypotWidget(forms.TextInput): def __init__(self, attrs=None, html_comment=False, *args, **kwargs): self.html_comment = html_comment super(HoneypotWidget, self).__init__(attrs, *args, **kwargs) - if not 'class' in self.attrs: + if 'class' not in self.attrs: self.attrs['style'] = 'display:none' def render(self, *args, **kwargs): value = super(HoneypotWidget, self).render(*args, **kwargs) if self.html_comment: - value = '' % value + value = f'' return value diff --git a/rc_hip/repapp/live_tests.py b/rc_hip/repapp/live_tests.py index 4ed713f..f030013 100644 --- a/rc_hip/repapp/live_tests.py +++ b/rc_hip/repapp/live_tests.py @@ -5,7 +5,6 @@ import random from pathlib import Path from hashlib import sha256 from django.contrib.staticfiles.testing import StaticLiveServerTestCase -from django.contrib.auth import get_user_model from selenium.webdriver.common.by import By from selenium.webdriver.chrome.webdriver import WebDriver from selenium.webdriver import ActionChains, Keys @@ -202,11 +201,3 @@ class WorkflowTests(StaticLiveServerTestCase): self.assertTrue("Test Hersteller" in self.selenium.page_source) self.assertTrue("/device/" in self.selenium.current_url) time.sleep(1) - - def test_member_login(self): - self.selenium.get( - f"{self.live_server_url}/member/login/") - time.sleep(1) - self.selenium.find_element(By.CLASS_NAME, 'login_link').click() - time.sleep(1) - self.assertTrue('sso.makes-hacks-hip.de' in self.selenium.current_url) diff --git a/rc_hip/repapp/templates/repapp/base.html b/rc_hip/repapp/templates/repapp/base.html index b9df74a..91436ab 100644 --- a/rc_hip/repapp/templates/repapp/base.html +++ b/rc_hip/repapp/templates/repapp/base.html @@ -33,7 +33,7 @@ Repair-Café {% if user.is_authenticated %} {% else %} - {% endif %} + diff --git a/rc_hip/repapp/templates/repapp/bootstrap.html b/rc_hip/repapp/templates/repapp/bootstrap.html index 1777fbc..192d367 100644 --- a/rc_hip/repapp/templates/repapp/bootstrap.html +++ b/rc_hip/repapp/templates/repapp/bootstrap.html @@ -45,7 +45,7 @@ Gast Anmeldung {% endif %} diff --git a/rc_hip/repapp/templates/repapp/index.html b/rc_hip/repapp/templates/repapp/index.html index 8421850..b1bfb2c 100644 --- a/rc_hip/repapp/templates/repapp/index.html +++ b/rc_hip/repapp/templates/repapp/index.html @@ -2,326 +2,35 @@ {% block title %} - Repair-Cafés {% endblock title %} -{% block head %} - -{% endblock head %} {% block page_title %} Anmeldung {% endblock page_title %} {% block content %} -

Repair-Café auswählen

-
- + + + + + + + + + + + {% for cafe in object_list %} + + + + + + + {% empty %} + + + + {% endfor %} + +
DatumOrtAdresseGerät anmelden
{{ cafe.event_date|date:"l" }}, {{ cafe.event_date|date }}{{ cafe.location }}{{ cafe.address }} + Gerät anmelden +
Es sind keine Repair-Cafés angelegt.
{% endblock content %} diff --git a/rc_hip/repapp/templates/repapp/member_login.html b/rc_hip/repapp/templates/repapp/member_login.html deleted file mode 100644 index a3a4585..0000000 --- a/rc_hip/repapp/templates/repapp/member_login.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "repapp/base.html" %} -{% block title %} - Mitarbeiter Anmeldung -{% endblock title %} -{% block page_title %} - Mitarbeiter - {% if user.is_authenticated %} - Anmelden - {% else %} - Abmelden - {% endif %} -{% endblock page_title %} -{% block content %} -
- {% 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/templates/repapp/orga/cafe.html b/rc_hip/repapp/templates/repapp/orga/cafe.html new file mode 100644 index 0000000..0db2d30 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/orga/cafe.html @@ -0,0 +1,45 @@ +{% extends "repapp/base.html" %} +{% block title %} + - Repair-Cafés +{% endblock title %} +{% block page_title %} + Repair-Cafés +{% endblock page_title %} +{% block content %} +

+ Neues Repair-Cafés anlegen + Zurück +

+ + + + + + + + + + + + + {% for cafe in object_list %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
IDDatumOrtAdresseBearbeitenLöschen
{{ cafe.pk }}{{ cafe.event_date|date:"l" }}, {{ cafe.event_date|date }}{{ cafe.location }}{{ cafe.address }} + Bearbeiten + + Löschen +
Es sind keine Repair-Cafés angelegt.
+{% endblock content %} diff --git a/rc_hip/repapp/templates/repapp/orga/create_cafe.html b/rc_hip/repapp/templates/repapp/orga/create_cafe.html new file mode 100644 index 0000000..738637c --- /dev/null +++ b/rc_hip/repapp/templates/repapp/orga/create_cafe.html @@ -0,0 +1,15 @@ +{% extends "repapp/base.html" %} +{% load crispy_forms_tags %} +{% block title %} + - Repair-Café anlegen +{% endblock title %} +{% block page_title %} + Repair-Café anlegen +{% endblock page_title %} +{% block content %} +
+ {% csrf_token %} + {{ form|crispy }} + +
+{% endblock content %} diff --git a/rc_hip/repapp/templates/repapp/orga/delete_cafe.html b/rc_hip/repapp/templates/repapp/orga/delete_cafe.html new file mode 100644 index 0000000..3617efd --- /dev/null +++ b/rc_hip/repapp/templates/repapp/orga/delete_cafe.html @@ -0,0 +1,16 @@ +{% extends "repapp/base.html" %} +{% load crispy_forms_tags %} +{% block title %} + - Repair-Café anlegen +{% endblock title %} +{% block page_title %} + Repair-Café anlegen +{% endblock page_title %} +{% block content %} +
+ {% csrf_token %} +

Soll das Repair-Café am {{ cafe.event_date|date:"l" }}, {{ cafe.event_date|date }} wirklich gelöscht werden?

+ {{ form|crispy }} + +
+{% endblock content %} diff --git a/rc_hip/repapp/templates/repapp/orga/edit_cafe.html b/rc_hip/repapp/templates/repapp/orga/edit_cafe.html new file mode 100644 index 0000000..b7da85f --- /dev/null +++ b/rc_hip/repapp/templates/repapp/orga/edit_cafe.html @@ -0,0 +1,15 @@ +{% extends "repapp/base.html" %} +{% load crispy_forms_tags %} +{% block title %} + - Repair-Café anlegen +{% endblock title %} +{% block page_title %} + Repair-Café anlegen +{% endblock page_title %} +{% block content %} +
+ {% csrf_token %} + {{ form|crispy }} + +
+{% endblock content %} diff --git a/rc_hip/repapp/templates/repapp/orga/edit_guest.html b/rc_hip/repapp/templates/repapp/orga/edit_guest.html new file mode 100644 index 0000000..3b3edc1 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/orga/edit_guest.html @@ -0,0 +1,15 @@ +{% extends "repapp/base.html" %} +{% load crispy_forms_tags %} +{% block title %} + - Repair-Café anlegen +{% endblock title %} +{% block page_title %} + Repair-Café anlegen +{% endblock page_title %} +{% block content %} +
+ {% csrf_token %} + {{ form|crispy }} + +
+{% endblock content %} diff --git a/rc_hip/repapp/templates/repapp/orga/guest.html b/rc_hip/repapp/templates/repapp/orga/guest.html new file mode 100644 index 0000000..a587a6b --- /dev/null +++ b/rc_hip/repapp/templates/repapp/orga/guest.html @@ -0,0 +1,44 @@ +{% extends "repapp/base.html" %} +{% block title %} + - Gäste +{% endblock title %} +{% block page_title %} + Gäste +{% endblock page_title %} +{% block content %} +

+ Neuen Gast anlegen + Zurück +

+ + + + + + + + + + + + + + {% for guest in object_list %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
IDNameTelefonWohnorteMail AdresseBearbeitenLöschen
{{ guest.pk }}{{ guest.name }}{{ guest.phone }}{{ guest.residence }}{{ guest.mail }} + Bearbeiten +
Es sind keine Gäste verfügbar.
+{% endblock content %} diff --git a/rc_hip/repapp/templates/repapp/orga/main.html b/rc_hip/repapp/templates/repapp/orga/main.html new file mode 100644 index 0000000..788764c --- /dev/null +++ b/rc_hip/repapp/templates/repapp/orga/main.html @@ -0,0 +1,27 @@ +{% extends "repapp/base.html" %} +{% load crispy_forms_tags %} +{% block title %} + - Organisation +{% endblock title %} +{% block page_title %} + Organisation +{% endblock page_title %} +{% block content %} +

+ Repair-Cafés + Gäste +

+
+

Repair-Cafés

+ Neues Repair-Café anlegen + // TODO: Button nächstes Repair-Café +
+

Geräte

+ // TODO: Liste mit neuen Geräten +
+

Rückfragen

+ // TODO: Liste mit Antworten auf Rückfragen +
+

Nachrichten

+ // TODO: Liste mit neuen Nachrichten +{% endblock content %} diff --git a/rc_hip/repapp/templates/repapp/orga/select_role.html b/rc_hip/repapp/templates/repapp/orga/select_role.html new file mode 100644 index 0000000..2933f68 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/orga/select_role.html @@ -0,0 +1,16 @@ +{% extends "repapp/base.html" %} +{% load crispy_forms_tags %} +{% block title %} + - Organisation +{% endblock title %} +{% block page_title %} + Organisation +{% endblock page_title %} +{% block content %} +

+ Weiter als Organisator +

+

+ Weiter als Reparateur +

+{% endblock content %} diff --git a/rc_hip/repapp/templates/repapp/repa/main.html b/rc_hip/repapp/templates/repapp/repa/main.html new file mode 100644 index 0000000..179e8d5 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/repa/main.html @@ -0,0 +1,10 @@ +{% extends "repapp/base.html" %} +{% load crispy_forms_tags %} +{% block title %} + - Reparateur +{% endblock title %} +{% block page_title %} + Reparateur +{% endblock page_title %} +{% block content %} +{% endblock content %} diff --git a/rc_hip/repapp/tests.py b/rc_hip/repapp/tests.py index 3d05dde..7312652 100644 --- a/rc_hip/repapp/tests.py +++ b/rc_hip/repapp/tests.py @@ -271,27 +271,6 @@ class ViewsTest(TestCase): response, 'type="text" name="accept_agb" style="display:none"') self.assertContains(response, 'csrfmiddlewaretoken') - def test_member_login(self): - """ - Test member login view - """ - client = Client(enforce_csrf_checks=True) - response = client.get(reverse('member_login')) - - self.assertEqual(response.status_code, 200) - - # ensure OIDC link is displayed - self.assertContains(response, "Anmelden mit Makes-Hacks-Hip") - - client.login(username="testuser@example.com", password="aTestPassword") - response = client.get(reverse('member_login')) - self.assertEqual(response.status_code, 200) - - # ensure logout button is displayed - self.assertContains(response, "Abmelden") - # ensure user is displayed - self.assertContains(response, "testuser@example.com") - def test_send_one_time_login_mail(self): """ Test that a mail is sent with the right link included. diff --git a/rc_hip/repapp/urls.py b/rc_hip/repapp/urls.py index b2ee999..ea75c77 100644 --- a/rc_hip/repapp/urls.py +++ b/rc_hip/repapp/urls.py @@ -24,8 +24,17 @@ urlpatterns = [ views.profile, name="guest_profile"), path("onetimelogin//", views.one_time_login, name="one_time_login"), - path("member/login/", - views.member_login, name="member_login"), + path("member/", views.member, name="member"), + path("orga/select/", views.select_role, name="select_role"), + path("orga/", views.orga, name="orga"), + path("repa/", views.repa, name="repa"), + path("cafe/", views.CafeView.as_view(), name="cafe"), + path("cafe/create", views.CafeCreateView.as_view(), name="create_cafe"), + path("cafe//edit", views.CafeUpdateView.as_view(), name="edit_cafe"), + path("cafe//delete", + views.CafeDeleteView.as_view(), name="delete_cafe"), + path("guest/", views.GuestView.as_view(), name="guest"), + path("guest//edit", views.GuestUpdateView.as_view(), name="edit_guest"), path("cron", views.cron, name="cron"), path("bootstrap", views.bootstrap, name="bootstrap"), path("process_mails", views.process_mails, name="process_mails"), diff --git a/rc_hip/repapp/views.py b/rc_hip/repapp/views.py index e3cfcd1..692dcf4 100644 --- a/rc_hip/repapp/views.py +++ b/rc_hip/repapp/views.py @@ -6,6 +6,7 @@ import random import string import time import os +import logging from hashlib import sha256 from django.template.loader import render_to_string from django.urls import reverse @@ -17,6 +18,7 @@ from django.shortcuts import get_object_or_404, render from django.http import HttpResponseRedirect, HttpResponse from django.core.exceptions import PermissionDenied from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth import authenticate, login from django.utils.timezone import now from .forms import RegisterDevice, RegisterGuest @@ -25,6 +27,9 @@ from .models import (Cafe, Device, Guest, OneTimeLogin, from . import mail_interface +logger = logging.getLogger(__name__) + + def send_one_time_login_mail(secret, mail, request): """ Send a mail with a one time login link. @@ -40,7 +45,7 @@ def send_one_time_login_mail(secret, mail, request): 'link': url, }) - ok = send_mail( + send_ok = send_mail( subject=subject, message=text, from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""), @@ -49,7 +54,7 @@ def send_one_time_login_mail(secret, mail, request): html_message=html ) - if ok > 0: + if send_ok > 0: messages.add_message(request, messages.INFO, 'Sie haben einen neuen Login per eMail erhalten.') @@ -133,7 +138,7 @@ def send_guest_account_mail(guest, password, request): 'password': password, }) - ok = send_mail( + send_ok = send_mail( subject=subject, message=text, from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""), @@ -142,7 +147,7 @@ def send_guest_account_mail(guest, password, request): html_message=html ) - if ok > 0: + if send_ok > 0: messages.add_message(request, messages.INFO, 'Sie haben ihre Benutzerdaten per eMail erhalten.') @@ -156,6 +161,22 @@ def is_member(user): return organisator or reparateur +def is_organisator(user): + """ + Test is a user is a Repair-Café organisator. + """ + organisator = Organisator.objects.filter(mail=user.email).first() + return organisator is not None + + +def is_reparateur(user): + """ + Test is a user is a Repair-Café reparateur. + """ + reparateur = Reparateur.objects.filter(mail=user.email).first() + return reparateur is not None + + def create_one_time_login(user, url) -> str: """ create_one_time_login creates a one time login object for guest user logins. @@ -397,16 +418,92 @@ def profile(request): ) -def member_login(request): +def member(request): """ Login page for repair cafe members, using OIDC. """ + # TODO: test + logger.debug('Member-View: User: %s', str(request.user)) + if request.user.is_anonymous or not is_member(request.user): + return HttpResponseRedirect(reverse_lazy('oidc_authentication_init')) + + if is_organisator(request.user) and is_reparateur(request.user): + return HttpResponseRedirect(reverse_lazy('select_role')) + + if is_organisator(request.user): + return HttpResponseRedirect(reverse_lazy('orga')) + + if is_reparateur(request.user): + return HttpResponseRedirect(reverse_lazy('repa')) + + logger.warning('User %s is no member!', str(request.user)) + raise PermissionDenied('No member!') + + +@login_required(login_url=reverse_lazy('member')) +def orga(request): + """ + Landing page for organisators. + """ + # TODO: test + if not is_organisator(request.user): + logger.warning('The user %s is no organisator!', str(request.user)) + raise PermissionDenied('Not organisator!') + return render( request, - "repapp/member_login.html" + "repapp/orga/main.html" ) +@login_required(login_url=reverse_lazy('member')) +def select_role(request): + """ + Landing page for organisators. + """ + # TODO: test + if not is_organisator(request.user): + logger.warning('The user %s is no organisator!', str(request.user)) + raise PermissionDenied('Not organisator!') + + if not is_reparateur(request.user): + logger.warning('The user %s is no reparateur!', str(request.user)) + raise PermissionDenied('Not reparateur!') + + return render( + request, + "repapp/orga/select_role.html" + ) + + +@login_required(login_url=reverse_lazy('member')) +def repa(request): + """ + Organisator main menu. + """ + # TODO: test + if not is_reparateur(request.user): + logger.warning('The user %s is no reparateur!', str(request.user)) + raise PermissionDenied('Not reparateur!') + + return render( + request, + "repapp/repa/main.html" + ) + + +class CafeView(LoginRequiredMixin, generic.ListView): + """ + List of all Repair-Cafés. + """ + # TODO: test + login_url = reverse_lazy('member') + template_name = "repapp/orga/cafe.html" + + def get_queryset(self): + return Cafe.objects.all() + + def cron(request): """ View to trigger automated regular tasks. @@ -462,3 +559,77 @@ def bootstrap(request): request, "repapp/bootstrap.html" ) + + +class CafeCreateView(LoginRequiredMixin, UserPassesTestMixin, generic.edit.CreateView): + """ + Organisator view for creating a new Repair-Café. + """ + # TODO: create test + login_url = reverse_lazy('member') + template_name = "repapp/orga/create_cafe.html" + model = Cafe + fields = ['event_date', 'location', 'address'] + success_url = reverse_lazy('cafe') + + def test_func(self): + return is_organisator(self.request.user) + + +class CafeUpdateView(LoginRequiredMixin, UserPassesTestMixin, generic.edit.UpdateView): + """ + Organisator view for edit a Repair-Café. + """ + # TODO: create test + login_url = reverse_lazy('member') + template_name = "repapp/orga/edit_cafe.html" + model = Cafe + fields = ['event_date', 'location', 'address'] + success_url = reverse_lazy('cafe') + + def test_func(self): + logger.debug('User passes test: user: %s, result: %s.', + self.request.user, is_organisator(self.request.user)) + return is_organisator(self.request.user) + + +class CafeDeleteView(LoginRequiredMixin, UserPassesTestMixin, generic.edit.DeleteView): + """ + Organisator view for edit a Repair-Café. + """ + # TODO: create test + login_url = reverse_lazy('member') + template_name = "repapp/orga/delete_cafe.html" + model = Cafe + success_url = reverse_lazy('cafe') + + def test_func(self): + logger.debug('User passes test: user: %s, result: %s.', + self.request.user, is_organisator(self.request.user)) + return is_organisator(self.request.user) + + +class GuestView(LoginRequiredMixin, generic.ListView): + """ + List of all Repair-Cafés. + """ + # TODO: test + login_url = reverse_lazy('member') + template_name = "repapp/orga/guest.html" + + def get_queryset(self): + return Guest.objects.all() + + +class GuestUpdateView(LoginRequiredMixin, UserPassesTestMixin, generic.edit.UpdateView): + """ + Organisator view for edit a guest. + """ + # TODO: create test + login_url = reverse_lazy('member') + template_name = "repapp/orga/edit_guest.html" + model = Guest + success_url = reverse_lazy('guest') + + def test_func(self): + return is_organisator(self.request.user)