tests started, docs rework started
This commit is contained in:
parent
8e96c18b7a
commit
2a1a24ced7
8 changed files with 200 additions and 15 deletions
|
|
@ -4,6 +4,8 @@ RepApp ist eine Web App um Reparaturen im Rahmen eines Repair-Cafés zu organisi
|
|||
|
||||
## Benutzer
|
||||
|
||||
RepApp unterscheidet zwischen den folgenden Benutzer-Typen:
|
||||
|
||||
### Gast
|
||||
|
||||
Ein Gast ist ein Gerätebesitzer der ein oder mehrere Geräte zur Reparatur anmelden möchte.
|
||||
|
|
@ -18,6 +20,9 @@ Ein Organisator ist Mitglied des Repair-Cafés das die Termine mit den Gästen o
|
|||
|
||||
## Anwendungsfälle
|
||||
|
||||
Die Anwendungsfälle sind in Meilensteine unterteilt.
|
||||
Zu Meilenstein 1 (M1) gehören alle Anwendungsfälle die notwendig sind um RepApp "produktiv" für die Anmeldung von Geräten zu verwenden.
|
||||
|
||||
### Gast
|
||||
|
||||
- [x] M1: Als Gast möchte ich einen Termin für eine Reparatur vereinbaren. (A1)
|
||||
|
|
@ -30,7 +35,9 @@ Ein Organisator ist Mitglied des Repair-Cafés das die Termine mit den Gästen o
|
|||
- [ ] 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)
|
||||
- [ ] M1: Als Gast möchte ich ausschließlich per eMail mit dem System interagieren können. (A30)
|
||||
- [ ] M1: Als Gast möchte per eMail Fragen ans Team stellen können. (A33)
|
||||
- [ ] M1: Als Gast möchte ich per eMail auf Rückfragen antworten können. (A34)
|
||||
|
||||
### Reparateur
|
||||
|
||||
|
|
@ -272,7 +279,7 @@ Die Seite enthält persönliche Daten und ist nur nach Anmeldung erreichbar.
|
|||
|
||||
### Anmeldung eines defekten Gerätes
|
||||
|
||||
#### Variante: Neuer Gast
|
||||
#### Variante: Neuer Gast (F1)
|
||||
|
||||
Als neuer Gast der ein defekten Geräte anmelden möchte,
|
||||
|
||||
|
|
@ -282,7 +289,7 @@ Als neuer Gast der ein defekten Geräte anmelden möchte,
|
|||
- 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.
|
||||
|
||||
#### Variante: Bekannter Gast (existierende eMail Adresse)
|
||||
#### Variante: Bekannter Gast (existierende eMail Adresse) (F2)
|
||||
|
||||
Als bekannter Gast der ein defekten Geräte anmelden möchte,
|
||||
|
||||
|
|
|
|||
|
|
@ -163,7 +163,6 @@ AUTH_USER_MODEL = "repapp.CustomUser"
|
|||
AUTHENTICATION_BACKENDS = [
|
||||
"repapp.backends.EmailBackend",
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
# "mozilla_django_oidc.auth.OIDCAuthenticationBackend",
|
||||
"repapp.backends.KeycloakOIDCAB",
|
||||
"repapp.backends.OneTimeLoginBackend",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from django.contrib import admin
|
|||
from import_export.admin import ImportExportModelAdmin, ExportActionMixin
|
||||
from import_export import resources
|
||||
from .models import (Organisator, Cafe, Question, Device, CustomUser,
|
||||
Reparateur, Appointment, Guest, Candidate, OneTimeLogin)
|
||||
Reparateur, Appointment, Guest, Candidate, OneTimeLogin, Message)
|
||||
|
||||
|
||||
class GuestResource(resources.ModelResource):
|
||||
|
|
@ -96,13 +96,16 @@ class ExportAdmin(ImportExportModelAdmin, ExportActionMixin):
|
|||
]
|
||||
|
||||
|
||||
# Register all models of RepApp for the admin interface.
|
||||
admin.site.register(Organisator)
|
||||
admin.site.register(Cafe)
|
||||
admin.site.register(Reparateur)
|
||||
admin.site.register(CustomUser)
|
||||
admin.site.register(OneTimeLogin)
|
||||
admin.site.register(Message)
|
||||
# Make the "data" models exportable.
|
||||
admin.site.register(Question, ExportAdmin)
|
||||
admin.site.register(Guest, ExportAdmin)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ class EmailBackend(ModelBackend):
|
|||
"""
|
||||
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
userModel = get_user_model()
|
||||
user_model = get_user_model()
|
||||
try:
|
||||
user = userModel.objects.get(email=username)
|
||||
except userModel.DoesNotExist:
|
||||
user = user_model.objects.get(email=username)
|
||||
except user_model.DoesNotExist:
|
||||
return None
|
||||
else:
|
||||
if user.check_password(password):
|
||||
|
|
@ -41,7 +41,8 @@ class EmailBackend(ModelBackend):
|
|||
|
||||
class KeycloakOIDCAB(OIDCAuthenticationBackend):
|
||||
"""
|
||||
KeycloakOIDCAB allows a login using Open ID Connect (with the Repair-Café Keycloak Single Sign On server)
|
||||
KeycloakOIDCAB allows a login using Open ID Connect
|
||||
(with the Repair-Café Keycloak Single Sign On server)
|
||||
"""
|
||||
|
||||
def create_user(self, claims):
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ from django.core.exceptions import ValidationError
|
|||
|
||||
|
||||
class HoneypotField(forms.BooleanField):
|
||||
"""
|
||||
Simple honeypot field.
|
||||
"""
|
||||
default_widget = forms.CheckboxInput(
|
||||
{'style': 'display:none !important;', 'tabindex': '-1', 'autocomplete': 'off'})
|
||||
|
||||
|
|
@ -22,6 +25,9 @@ class HoneypotField(forms.BooleanField):
|
|||
|
||||
|
||||
class RegisterDevice(forms.Form):
|
||||
"""
|
||||
Form for registering a device for a repair cafe.
|
||||
"""
|
||||
mail = forms.EmailField(
|
||||
label="eMail Adresse",
|
||||
help_text="Diese eMail-Adresse wird für Aktualisierungen"
|
||||
|
|
@ -49,7 +55,8 @@ class RegisterDevice(forms.Form):
|
|||
)
|
||||
follow_up = forms.BooleanField(
|
||||
label="Folgetermin",
|
||||
help_text="Kreuzen Sie diese Kästchen an wenn sie mit diesem Gerät bereits bei einem Repair-Café Termin waren.",
|
||||
help_text="Kreuzen Sie diese Kästchen an wenn sie mit diesem Gerät "
|
||||
"bereits bei einem Repair-Café Termin waren.",
|
||||
required=False
|
||||
)
|
||||
confirm_repair = forms.BooleanField(
|
||||
|
|
@ -72,6 +79,9 @@ class RegisterDevice(forms.Form):
|
|||
|
||||
|
||||
class RegisterGuest(forms.Form):
|
||||
"""
|
||||
Form for registering new guests.
|
||||
"""
|
||||
name = forms.CharField(
|
||||
label="Name",
|
||||
max_length=200
|
||||
|
|
|
|||
|
|
@ -175,6 +175,9 @@ class Candidate(models.Model):
|
|||
|
||||
|
||||
class CustomUser(AbstractUser):
|
||||
"""
|
||||
Custom user object with unique email.
|
||||
"""
|
||||
email = models.EmailField(unique=True, verbose_name=_("eMail Adresse"))
|
||||
|
||||
class Meta:
|
||||
|
|
|
|||
|
|
@ -1 +1,137 @@
|
|||
# Create your tests here.
|
||||
"""
|
||||
Tests for RepApp.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
import django.utils.timezone
|
||||
from django.test import TestCase
|
||||
from django.test import Client
|
||||
from django.urls import reverse
|
||||
from .backends import OneTimeLoginBackend, EmailBackend
|
||||
from .models import OneTimeLogin, CustomUser, Cafe
|
||||
|
||||
|
||||
class OneTimeLoginTest(TestCase):
|
||||
"""
|
||||
Tests for the one time login feature.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
user = CustomUser(
|
||||
email="user@example.com",
|
||||
)
|
||||
user.save()
|
||||
self.user = user
|
||||
|
||||
otl = OneTimeLogin(
|
||||
secret="LetMeIn",
|
||||
user=user,
|
||||
url="/test/url",
|
||||
)
|
||||
otl.save()
|
||||
self.otl = otl
|
||||
|
||||
def test_allowed_one_time_login(self):
|
||||
"""
|
||||
Test that one time login is allowed.
|
||||
"""
|
||||
backend = OneTimeLoginBackend()
|
||||
result = backend.authenticate(None, self.otl.secret, None)
|
||||
|
||||
self.assertEqual(result, self.user)
|
||||
|
||||
def test_unknown_one_time_login(self):
|
||||
"""
|
||||
Test that one time login is not allowed for unknown secret.
|
||||
"""
|
||||
|
||||
backend = OneTimeLoginBackend()
|
||||
result = backend.authenticate(None, "other phrase", None)
|
||||
|
||||
self.assertEqual(result, None)
|
||||
|
||||
|
||||
class EmailLoginTest(TestCase):
|
||||
"""
|
||||
Tests for the one time login feature.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
user = CustomUser(
|
||||
email="user@example.com"
|
||||
)
|
||||
user.set_password("aPassword")
|
||||
user.save()
|
||||
self.user = user
|
||||
|
||||
def test_allowed_email_login(self):
|
||||
"""
|
||||
Test that email login is allowed.
|
||||
"""
|
||||
backend = EmailBackend()
|
||||
result = backend.authenticate(None, self.user.email, "aPassword")
|
||||
|
||||
self.assertEqual(result, self.user)
|
||||
|
||||
def test_unknown_email_login(self):
|
||||
"""
|
||||
Test that email login is not allowed for unknown user.
|
||||
"""
|
||||
|
||||
backend = EmailBackend()
|
||||
result = backend.authenticate(
|
||||
None, "other@example.com", "somePassword")
|
||||
|
||||
self.assertEqual(result, None)
|
||||
|
||||
def test_wrong_password_login(self):
|
||||
"""
|
||||
Test that email login is not allowed for wrong password.
|
||||
"""
|
||||
|
||||
backend = EmailBackend()
|
||||
result = backend.authenticate(
|
||||
None, self.user.email, "wrongPassword")
|
||||
|
||||
self.assertEqual(result, None)
|
||||
|
||||
|
||||
class UtilsTest(TestCase):
|
||||
"""
|
||||
Test for utility functions.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ViewsTest(TestCase):
|
||||
"""
|
||||
Test for RepApp views.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
cafe = Cafe(location="neuer Ort", address="neue Adresse",
|
||||
event_date=django.utils.timezone.now())
|
||||
cafe.save()
|
||||
self.cafe = cafe
|
||||
|
||||
date = datetime.now() - timedelta(days=7)
|
||||
old_cafe = Cafe(location="alter Ort", address="alte Adresse",
|
||||
event_date=date)
|
||||
old_cafe.save()
|
||||
self.old_cafe = old_cafe
|
||||
|
||||
def test_index(self):
|
||||
"""
|
||||
Test index page.
|
||||
"""
|
||||
client = Client(enforce_csrf_checks=True)
|
||||
response = client.get(reverse('index'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# ensure future cafe is displayed
|
||||
self.assertContains(response, self.cafe.location)
|
||||
self.assertContains(response, self.cafe.address)
|
||||
|
||||
# ensure old cafe is not displayed
|
||||
self.assertNotContains(response, self.old_cafe.location)
|
||||
self.assertNotContains(response, self.old_cafe.address)
|
||||
|
|
|
|||
|
|
@ -103,6 +103,9 @@ class RegisterDeviceFormView(generic.edit.FormView):
|
|||
|
||||
|
||||
class RegisterGuestFormView(generic.edit.FormView):
|
||||
"""
|
||||
View for registering a new guest.
|
||||
"""
|
||||
template_name = "repapp/register_guest.html"
|
||||
form_class = RegisterGuest
|
||||
|
||||
|
|
@ -137,7 +140,8 @@ class RegisterGuestFormView(generic.edit.FormView):
|
|||
user = CustomUser.objects.create_user(
|
||||
username=name,
|
||||
email=mail,
|
||||
password=password)
|
||||
)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
send_guest_account_mail(guest, password, self.request)
|
||||
|
|
@ -168,6 +172,9 @@ class RegisterGuestFormView(generic.edit.FormView):
|
|||
|
||||
|
||||
def register_device_final(request, cafe, device_identifier):
|
||||
"""
|
||||
View to confirm device registration.
|
||||
"""
|
||||
cafe = get_object_or_404(Cafe, pk=cafe)
|
||||
device = get_object_or_404(Device, identifier=device_identifier)
|
||||
|
||||
|
|
@ -180,6 +187,9 @@ def register_device_final(request, cafe, device_identifier):
|
|||
|
||||
@login_required
|
||||
def device_view(request, device_identifier):
|
||||
"""
|
||||
View for showing device details.
|
||||
"""
|
||||
user = request.user
|
||||
if not user:
|
||||
raise PermissionDenied()
|
||||
|
|
@ -200,6 +210,9 @@ def device_view(request, device_identifier):
|
|||
|
||||
@login_required
|
||||
def profile(request):
|
||||
"""
|
||||
View for showing guest details.
|
||||
"""
|
||||
user = request.user
|
||||
if not user:
|
||||
raise PermissionDenied()
|
||||
|
|
@ -216,6 +229,9 @@ def profile(request):
|
|||
|
||||
|
||||
def member_login(request):
|
||||
"""
|
||||
Login page for repair cafe members, using OIDC.
|
||||
"""
|
||||
return render(
|
||||
request,
|
||||
"repapp/member_login.html"
|
||||
|
|
@ -223,14 +239,24 @@ def member_login(request):
|
|||
|
||||
|
||||
def cron(request):
|
||||
"""
|
||||
View to trigger automated regular tasks.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def process_mails(request):
|
||||
"""
|
||||
View to trigger processing of email in inbox.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def one_time_login(request, secret):
|
||||
"""
|
||||
View for one time login.
|
||||
"""
|
||||
# waste a little time as brute force protection
|
||||
time.sleep(1)
|
||||
|
||||
otl = get_object_or_404(OneTimeLogin, secret=secret)
|
||||
|
|
|
|||
Reference in a new issue