rework started: one-time-login
This commit is contained in:
parent
1437a796c8
commit
49329a709e
39 changed files with 853 additions and 564 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
The repapp is split into different modules, realized as Django apps, to improve the maintainability.
|
||||
|
||||
## one time login
|
||||
## One time login
|
||||
|
||||
The one time login module provides a login for guests using a one time secret as part of an URL.
|
||||
|
||||
|
|
@ -11,7 +11,25 @@ The one time login module provides a login for guests using a one time secret as
|
|||
- The templates use the `settings.ORGANIZATION` variable to fill the organization name.
|
||||
- The Django mail module is used to send emails. See https://docs.djangoproject.com/en/4.2/topics/email/ for details how to configure the mail host.
|
||||
|
||||
## mail interface
|
||||
### Configuration
|
||||
|
||||
To enable the one time login links, an additional authentication backend must get added:
|
||||
|
||||
```Python
|
||||
# Repapp specific authentication backends
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
# Backend for one time logins
|
||||
"one_time_login.authentication_backends.OneTimeLoginBackend",
|
||||
]
|
||||
```
|
||||
The one time login urls must get added using namespace `one_time_login`:
|
||||
|
||||
|
||||
## repapp members
|
||||
```Python
|
||||
urlpatterns = i18n_patterns(
|
||||
...
|
||||
path(_('one_time_login/'),
|
||||
include('one_time_login.urls', namespace='one_time_login')),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -18,6 +18,88 @@ 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")
|
||||
DEFAULT_FROM_EMAIL = os.getenv("DJANGO_SENDER_ADDRESS", "")
|
||||
```
|
||||
See https://docs.djangoproject.com/en/4.2/topics/email/ for more details.
|
||||
|
||||
## Translations
|
||||
|
||||
Repapp is using localization, and comes with german and english translations.
|
||||
|
||||
Enable the `LocaleMiddleware`:
|
||||
|
||||
```Python
|
||||
MIDDLEWARE = [
|
||||
...
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
# enable translation
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
We use the following localization settings:
|
||||
|
||||
```Python
|
||||
LANGUAGE_CODE = 'de'
|
||||
TIME_ZONE = 'Europe/Berlin'
|
||||
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
LANGUAGES = [
|
||||
('de', _('German')),
|
||||
('en', _('English')),
|
||||
]
|
||||
|
||||
LOCALE_PATHS = [
|
||||
BASE_DIR / 'locale',
|
||||
]
|
||||
```
|
||||
|
||||
Also the URLs are localized, and the language is choosen using i18n URL patterns:
|
||||
|
||||
```Python
|
||||
...
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
urlpatterns = i18n_patterns(
|
||||
path('admin/', admin.site.urls),
|
||||
path(_('one_time_login/'),
|
||||
include('one_time_login.urls', namespace='one_time_login')),
|
||||
# URLs for rosetta translation interface
|
||||
path('rosetta/', include('rosetta.urls')),
|
||||
)
|
||||
```
|
||||
|
||||
### Rosetta
|
||||
|
||||
For translating the interface, we use rosetta. For more details see https://pypi.org/project/django-rosetta/.
|
||||
|
||||
You need to install rosetta (`pip install django-rosetta`) and enable it in the apps:
|
||||
|
||||
```Python
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
# enable rosetta online translation interface
|
||||
'rosetta',
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
Rosatta is made availabel at `rosetta/`.
|
||||
|
||||
## One time login
|
||||
|
||||
To enable the one time login links, an additional authentication backend must get added:
|
||||
|
||||
```Python
|
||||
# Repapp specific authentication backends
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
# Backend for one time logins
|
||||
"one_time_login.authentication_backends.OneTimeLoginBackend",
|
||||
]
|
||||
```
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ The code of the Repapp is hosted on Github: https://github.com/makes-hacks-hip/R
|
|||
- Clone the Github repository
|
||||
- Create a Python environment: `python -m venv venv`
|
||||
- Activate the Python environment: `source venv/bin/activate`
|
||||
- Change to the Django project folder: `cd rc_hip`
|
||||
- Install the dependencies: `pip install -r requirements_prod.txt` and `pip install -r requirements_dev.txt`
|
||||
- Change to the Django project folder: `cd repapp`
|
||||
- Setup the database:
|
||||
- `python manage.py makemigrations`
|
||||
- `python manage.py migrate`
|
||||
|
|
@ -38,4 +39,4 @@ docker run --rm -d \
|
|||
## Tests
|
||||
|
||||
- Unit tests: `python manage.py test`
|
||||
- LiveServer tests: `python manage.py test repapp.live_tests.WorkflowTests`
|
||||
- LiveServer tests: `python manage.py test repapp.live_tests`
|
||||
|
|
|
|||
|
|
@ -24,11 +24,7 @@ RepApp unterstützt pro Benutzer-Gruppe verschiedenen Anmelde-Konzepte.
|
|||
|
||||
### Gäste
|
||||
|
||||
Für Gäste wird automatsch ein Benutzerkonto angelegt, mit einem generierten, sicheren Passwort. Mit der eMail Adresse und dem Passwort kann ein Gast sich in der RepApp anmelden um seine Daten sehen und bearbeiten zu können.
|
||||
|
||||
### Einmal-Login
|
||||
|
||||
Für Gäste gibt es Einmal-Logins als Alternative. Ein Einmal-Login ist ein Geheimnis und eine Ziel-URL, das dem Gast in From eines Anmelde-Links in einer eMail mitgeteilt wird. Mit diesem Link kann der Gast sich einmalig anmelden, da das Geheimnis danach potentiell Dritten bekannt ist, z.B. über die Browser History.
|
||||
Für Gäste gibt es Einmal-Logins. Ein Einmal-Login ist ein Geheimnis und eine Ziel-URL, das dem Gast in From eines Anmelde-Links in einer eMail mitgeteilt wird. Mit diesem Link kann der Gast sich einmalig anmelden, da das Geheimnis danach potentiell Dritten bekannt ist, z.B. über die Browser History.
|
||||
|
||||
Nach dem erfolgreichen Login wird der Gast automatisch zu der hinterlegten URL weitergeleitet. Dieser Mechanismus erlaubt es einem Gast "per Klick" Zugriff auf geschützte Daten zu geben.
|
||||
|
||||
|
|
@ -37,48 +33,3 @@ Wenn ein Gast versucht den Einmal-Link nochmals zu verwenden, schlägt dies Fehl
|
|||
### Mitarbeiter
|
||||
|
||||
Der Login für Mitarbeiter ist nur über das Single-Sign-On der Repair-Cafés erlaubt (Keycloak), das mittels OIDC angebunden ist. Alle Mitarbeiter im Repair-Café haben dort bereits einen Benutzer, was zum einen sicherstellt dass es sich um einen Mitarbeiter handelt, und zum anderen auch die Veraltung der Benutzer in RepApp vereinfacht.
|
||||
|
||||
## Geräte
|
||||
|
||||
Zur Reparatur angemeldete Geräte können in den folgenden Zuständen sein:
|
||||
|
||||
- (0) Neu: Das Gerät wurde vom Gast angemeldet und muss als nächstes durch einen Organisator überprüft werden.
|
||||
- (-1) Neu: Das Gerät wurde abgelehnt.
|
||||
- (1) Rückfrage: Bei der Überprüfung durch einen Organisator wurde festgestellt dass benötigte Informationen fehlen und eine Rückfrage an den Gast gesendet. Die Antwort ist noch nicht eingetroffen.
|
||||
- (2) Rückfrage beantwortet: Eine Rückfrage der Organisation wurde durch den Gast beantwortet. Als nächstes muss die Antwort durch einen Organisator überprüft werden.
|
||||
- (3) Warteliste: Das Gerät wurde durch die Organisation überprüft und kann einem Repair-Café zugeordnet werden. Jetzt können Reparateure das Gerät einsehen.
|
||||
- (4) Reserviert: Ein Reparateur hat sich das Gerät zugeordnet.
|
||||
- (6) Zugeordnet: Das Gerät ist einem Repair-Café zugeordnet.
|
||||
- (7) Gebucht: Das Gerät ist einem Reparaturtermin zugeordnet.
|
||||
|
||||
## Konfigurationsparameter
|
||||
|
||||
- DJANGO_SECRET_KEY: Secret Key für das Django Framework.
|
||||
- DJANGO_DEBUG: Debug-Modus verwenden? Standard ist True.
|
||||
|
||||
- DJANGO_EMAIL_HOST: eMail Server
|
||||
- DJANGO_EMAIL_PORT: eMail Server Port
|
||||
- DJANGO_EMAIL_HOST_USER: Benutzername für den eMail Server
|
||||
- DJANGO_EMAIL_HOST_PASSWORD: Passwort für den eMail Server
|
||||
- DJANGO_EMAIL_USE_TLS: TLS verwenden? Standard ist True.
|
||||
|
||||
- OIDC_RP_CLIENT_SECRET: Secret für den OIDC Prozess. Ist auch im Keycloak konfiguriert.
|
||||
|
||||
### Feste Konfiguration
|
||||
|
||||
- ALLOWED_HOSTS: Erlaubte Domainnamen. ("127.0.0.1", "localhost", "repapp.rc-hip.de", "anmeldung.repaircafe-hilpoltstein.de")
|
||||
- LANGUAGE_CODE: de
|
||||
- TIME_ZONE: Europe/Berlin
|
||||
- CSRF_TRUSTED_ORIGINS: Siehe ALLOWED_HOSTS.
|
||||
- AUTH_USER_MODEL: CustomUser in RepApp Models definiert.
|
||||
- AUTHENTICATION_BACKENDS: Standard + Backends aus RepApp Backends.
|
||||
|
||||
- OIDC_RP_CLIENT_ID: OIDC Client ID, im Keycloak konfiguriert.
|
||||
- OIDC_RP_SIGN_ALGO: RS256
|
||||
- OIDC_OP_JWKS_ENDPOINT: Keycloak URL
|
||||
- OIDC_OP_AUTHORIZATION_ENDPOINT: Keycloak URL
|
||||
- OIDC_OP_TOKEN_ENDPOINT: Keycloak URL
|
||||
- OIDC_OP_USER_ENDPOINT: Keycloak URL
|
||||
|
||||
- LOGIN_REDIRECT_URL: Debug: 127.0.0.1:8000, sonst https://anmeldung.repaircafe-hilpoltstein.de/
|
||||
- LOGOUT_REDIRECT_URL: Siehe LOGIN_REDIRECT_URL
|
||||
|
|
|
|||
152
docs/models.md
152
docs/models.md
|
|
@ -1,152 +0,0 @@
|
|||
# Daten
|
||||
|
||||
|
||||
## Benutzer
|
||||
|
||||
Ein Repapp-Benutzer erweitert den Standard-Benutzer um eine eindeutige eMail-Adresse, um diese als Identifikationsmerkmal verwenden zu können.
|
||||
|
||||
## Cafe
|
||||
|
||||
Ein Cafe ist ein Repair-Café Termin.
|
||||
|
||||
Attribute:
|
||||
|
||||
- Ort: Beschreibung wo das Repair-Café stattfindet
|
||||
- Adresse: Adresse an der das Repair-Café stattfindet
|
||||
- Datum: Datum der Veranstaltung
|
||||
|
||||
Annahmen:
|
||||
|
||||
- Ein Repair-Café beginnt um 13:00 Uhr.
|
||||
- Ein Repair-Café hat drei aufeinanderfolgende Zeitslots pro Reparateur mit je einer Stunde
|
||||
|
||||
## Reparateur
|
||||
|
||||
Ein Reparateur ist ein Mitglied des Repair-Cafés das Gäste bei der Reparatur ihrer Geräte unterstützt.
|
||||
|
||||
Attribute:
|
||||
|
||||
- Name: Name des Reparateurs
|
||||
- Mail: eMail-Adresse für Benachrichtigungen
|
||||
|
||||
Annahmen:
|
||||
|
||||
- Reparateure sind immer verfügbar, d.h. Zeitslots für jeden Reparateur werden zu einem neuen Repair-Café Termin automatisch hinzugefügt.
|
||||
- Reparateure können alles reparieren. Geräte Kategorien, Skills, ... werden im Moment nicht berücksichtigt.
|
||||
|
||||
## Organisator
|
||||
|
||||
Ein Organisator ist ein Mitglied des Repair-Cafés das Reparaturtermine mit den Gästen vereinbart.
|
||||
|
||||
Attribute:
|
||||
|
||||
- Name: Name des Organisators
|
||||
- Mail: eMail-Adresse für Benachrichtigungen
|
||||
|
||||
## Gast
|
||||
|
||||
Ein Gast ist ein Gerätebesitzer, der ein Gerät zur Reparatur anmeldet.
|
||||
|
||||
Attribute:
|
||||
|
||||
- Name: Name des Gastes
|
||||
- Telefon: Festnetz oder Mobilnummer des Gastes
|
||||
- Wohnort: Wohnort des Gastes
|
||||
- Mail: eMail-Adresse des Gastes für Benachrichtigungen
|
||||
|
||||
Technische Attribute:
|
||||
|
||||
- Identifier: SHA256 Hash aus Name + Wohnort + Timestamp
|
||||
- Benutzer: Referenz zum Benutzer der für diesen Gast erstellt wurde.
|
||||
|
||||
## Gerät
|
||||
|
||||
Ein Gerät ist ein defekter Gegenstand der im Rahmen eines Repair-Cafés repariert werden soll.
|
||||
|
||||
Attribute:
|
||||
|
||||
- Erstellungsdatum: Datum an dem diese Gerät angemeldet wurde.
|
||||
- Gerät: Bezeichnung des Geräts
|
||||
- Hersteller: Hersteller des Geräts
|
||||
- Fehler: Beschreibung des Defekts
|
||||
- Folgetermin: Boolesches Flag das anzeigt ob es sich um einen Folgetermin handelt.
|
||||
- Foto vom Gerät
|
||||
- Foto vom Typenschild
|
||||
- Bestätigung gesendet?: Ein boolesches Flag das anzeigt ob eine eMail als Anmeldebestätigung gesendet wurde.
|
||||
|
||||
Technische Attribute:
|
||||
|
||||
- Identifier: SHA256 Hash aus Gerät + Gast + Timestamp
|
||||
- Gast: Referenz zum Gast der das Gerät besitzt
|
||||
- Cafe: Referenz zum Repair-Café für das diese Gerät angemeldet ist
|
||||
|
||||
## Termin
|
||||
|
||||
Ein Termin ist eine Zuordnung von einem Gerät zu einem Zeitslot der Repair-Café Veranstaltung und einem Reparateur.
|
||||
Ein Termin ist bestätigt, wenn eine Organisator eine Termineinladung gesendet hat und er Gast diese bestätigt hat.
|
||||
|
||||
Attribute:
|
||||
|
||||
- Uhrzeit: Uhrzeit des Termins
|
||||
- Betätigt: Boolesches Flag. True wenn die Einladung vom Gast bestätigt wurde.
|
||||
|
||||
Technische Attribute:
|
||||
|
||||
- Cafe: Referenz zum Cafe
|
||||
- Reparateur: Referenz um Reparateur oder NULL
|
||||
- Gerät: Referenz zum Gerät
|
||||
|
||||
## Frage
|
||||
|
||||
Eine Frage ist eine Rückfrage nach mehr Informationen zu einem Gerät, erstellt von einem Organisator oder einem Reparateur.
|
||||
|
||||
Attribute:
|
||||
|
||||
- Frage: Frage zum Gerät oder Defekt
|
||||
- Antwort: Antwort des Gastes
|
||||
- Erstellungsdatum: Datum der Erstellung der Rückfrage
|
||||
|
||||
Technische Attribute:
|
||||
|
||||
- Organisator: Referenz zum Organisator oder NULL
|
||||
- Reparateur: Referenz zum Reparateur oder NULL
|
||||
- Gerät: Referenz zum Gerät
|
||||
|
||||
## Kandidat
|
||||
|
||||
Ein Kandidat ist ein Geräte ohne festen Termin.
|
||||
Diese Geräte werden von den Gästen zu Beginn des Repair-Café gebracht und am Ende abgeholt.
|
||||
Falls ein Reparateur Zeit hat kann er eines dieser Geräte reparieren.
|
||||
|
||||
- Bestätigt: Boolesches Flag. True wenn die Einladung vom Gast bestätigt wurde.
|
||||
|
||||
Technische Attribute:
|
||||
|
||||
- Cafe: Referenz zum Cafe
|
||||
- Gerät: Referenz zum Gerät
|
||||
|
||||
## Einmal-Login
|
||||
|
||||
Ein Einmal-Login ist ein Geheimnis das es einem Gast erlaubt sich einmalig damit anzumelden.
|
||||
|
||||
- Geheimnis: Geheimnis um den Gast zu identifizieren.
|
||||
- URL: URL die nach der Anmeldung angezeigt werden soll.
|
||||
- Erstellungsdatum: Datum an dem der Einmal-Login angelegt wurde.
|
||||
- Login benutzt?: Boolesches Flag. True wenn der Einmal-Login bereits benutzt wurde.
|
||||
- Login Datum: Datum an dem der Einmal-Login benutzt wurde, oder Datum der Erstellung, da das Feld aus technischen Gründen nicht leer sein kann.
|
||||
|
||||
Technische Attribute:
|
||||
|
||||
- Benutzer: Referenz zum Benutzer
|
||||
|
||||
## Nachricht
|
||||
|
||||
Eine Nachricht ist eine Anfrage von einem Gast.
|
||||
|
||||
- Nachricht: Anfrage des Gastes.
|
||||
- Antwort: Antwort an den Gast.
|
||||
- Erstellungsdatum: Datum an dem die Anfrage erstellt wurde.
|
||||
|
||||
Technische Attribute:
|
||||
|
||||
- Gast: Referenz zum Gast
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
# 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)
|
||||
- [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)
|
||||
- [ ] 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)
|
||||
- [ ] 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)
|
||||
- [ ] M1: Als Gast möchte ich eine Termineinladung per eMail bestätigen können. (A35)
|
||||
|
||||
- [ ] Als Gast möchte ich mein angemeldetes Gerät und dessen Status einsehen können. (A4)
|
||||
- [ ] 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)
|
||||
|
||||
## Reparateur
|
||||
|
||||
- [ ] 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)
|
||||
- [ ] 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 ein angemeldetes Gerät ablehnen können. (A9)
|
||||
- [ ] Als Reparateur möchte ich benachrichtigt werden wenn ein Gast eine mir zugeordnete Geräteanmeldung löscht. (A25)
|
||||
|
||||
## Organisator
|
||||
|
||||
- [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)
|
||||
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
# Ansichten
|
||||
|
||||
## Landing Page: Repair-Cafés (S1)
|
||||
|
||||
URL: /
|
||||
|
||||
Name: index
|
||||
|
||||
Sicherheit: Die Seite ist ohne Zugangsbeschränkung oder Anmeldung erreichbar.
|
||||
|
||||
Diese Seite zeigt eine Liste der zukünftigen Repair-Café Termine.
|
||||
Sie enthält pro Repair-Café einen Link um ein Gerät für dieses Repair-Café anzumelden.
|
||||
|
||||
## Gerät anmelden (S2)
|
||||
|
||||
URL: cafe/int:cafe/
|
||||
|
||||
Name: register_device
|
||||
|
||||
Sicherheit: Die Seite ist ohne Zugangsbeschränkung oder Anmeldung erreichbar.
|
||||
|
||||
Diese Seite zeigt das Formular zum anmelden der Geräte.
|
||||
Das Formular hat die Felder `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`, ein Kontrollkästchen `Datenschutz` und einen Knopf `Absenden` zum senden des Formulars.
|
||||
|
||||
Das Repair-Café zu dem die Anmeldung gehört ist über eine ID in der URL der Seite festgelegt.
|
||||
|
||||
Falls die eMail Adresse zu einem bekannten Gast gehört, wird beim absenden des Formulars eine eMail-Benachrichtigung and die Organisatoren und Bestätigung an den Gast gesendet.
|
||||
|
||||
## Gast anmelden (S3)
|
||||
|
||||
URL: cafe/int:cafe/device/str:device_identifier/mail/str:mail/
|
||||
|
||||
Name: register_guest
|
||||
|
||||
Sicherheit: Die Seite ist ohne Zugangsbeschränkung oder Anmeldung erreichbar.
|
||||
|
||||
Diese Seite zeigt das Formular zum anmelden eines neuen Gastes.
|
||||
Das Formular hat die Felder `Name`, `Telefon`, `Wohnort` und einen Knopf `Absenden` zum senden des Formulars.
|
||||
|
||||
Die eMail-Adresse und das Gerät zu dem die Gast-Kontaktdaten gehören ist über IDs in der URL der Seite festgelegt.
|
||||
|
||||
Beim absenden des Formulars eine eMail-Benachrichtigung and die Organisatoren und Bestätigung an den Gast gesendet.
|
||||
|
||||
## Anmeldung bestätigt (S7)
|
||||
|
||||
URL: cafe/int:cafe/device/str:device_identifier/confirm/
|
||||
|
||||
Name: register_device_final
|
||||
|
||||
Sicherheit: Die Seite ist ohne Zugangsbeschränkung oder Anmeldung erreichbar.
|
||||
|
||||
Die Seite Anmeldung bestätigt zeigt einen Hinweis dass die Anmeldung des Gerätes erfolgreich abgeschlossen wurde.
|
||||
|
||||
Die Geräte ID ist in der URL der Seite festgelegt.
|
||||
|
||||
## Geräte Detailseite (S5)
|
||||
|
||||
URL: device/str:device_identifier/
|
||||
|
||||
Name: view_device
|
||||
|
||||
Sicherheit: Diese Seite ist für alle Mitarbeiter und den Gast der das Gerät angemeldet hat erreichbar.
|
||||
|
||||
Die Geräte Detailseite zeigt die Informationen `Art des Geräts`, `Hersteller & Modell/Typ`, `Fehlerbeschreibung`, `Foto vom Gerät`, `Foto vom Typenschild` und `Folgetermin` an.
|
||||
|
||||
Das Gerät ist über IDs in der URL der Seite festgelegt.
|
||||
|
||||
## Gast Detailseite (S6)
|
||||
|
||||
URL: guest/profile/
|
||||
|
||||
Name: guest_profile
|
||||
|
||||
Sicherheit: Die Seite enthält persönliche Daten und ist nur für den Gast zu dem sie gehört erreichbar.
|
||||
|
||||
Die Gast Detailseite hat die Felder `Name`, `eMail`, `Telefon` und `Wohnort` die mit den Angaben des Gastes ausgefüllt sind.
|
||||
Weiter enthält die Seite einen Knopf `Aktualisieren` der das Formular absendet.
|
||||
Die Ansicht enthält auch eine Liste mit Links zu allen Geräten die der Gast angemeldet hat.
|
||||
|
||||
Der Gast ist über den angemeldeten Benutzer festgelegt.
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
# Abläufe
|
||||
|
||||
## Gast: Anmeldung eines defekten Gerätes
|
||||
|
||||
#### Variante: Neuer Gast (F1)
|
||||
|
||||
Als neuer Gast der ein defekten Geräte anmelden möchte,
|
||||
|
||||
- gehe ich auf die Seite (S1) der Repair-Café Termine. Dort klicke ich auf `Gerät anmelden` bei dem richtigen Repair-Café Termin. Dieser Klick bringt mich auf die nächste Seite.
|
||||
- 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 (S3) enthält ein Formular mit den Feldern `Name`, `Telefon` und `Wohnort`. 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.
|
||||
|
||||
### Variante: Bekannter Gast (existierende eMail Adresse) (F2)
|
||||
|
||||
Als bekannter Gast der ein defekten Geräte anmelden möchte,
|
||||
|
||||
- gehe ich auf die Seite (S1) der Repair-Café Termine. Dort klicke ich auf `Gerät anmelden` bei dem richtigen Repair-Café Termin. Dieser Klick bringt mich auf die nächste Seite.
|
||||
- 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.
|
||||
|
||||
## Organisator: Neues Gerät
|
||||
|
||||
### Variante: Akzeptieren für ausgewähltes Repair-Café
|
||||
|
||||
Als Organisator habe ich eine eMail bekommen dass ein neues Gerät angemeldet wurde. Ich möchte dieses Gerät nun ansehen un die Eingangsüberprüfung durchführen.
|
||||
- Ich öffne die RepApp im Browser
|
||||
- Im Menü klicke ich auf Mitarbeiter, was mich zur Single Sign On Anmeldung bringt.
|
||||
- Ich melde mich mit meinem Repair-Café Konto an und werde automatisch zur Hauptmenü für die Organisatoren weitergeleitet.
|
||||
- Im Hauptmenü sehe ich das neue Gerät im Bereich Geräte.
|
||||
- Ich klicke auf den Knopf "Details" für das neue Gerät, und mein Browser öffnet eine Seite in der ich die Details zu dem Gerät sehe. Auf der Seite sind Knöpfe: `Annehmen für gewähltes Repair-Café`, `Annehmen für anderes Repair-Café`, `Ablehnen` und `Rückfrage`. Alle notwendigen Informationen sind vorhanden und ich entscheide mich das Gerät für das ausgewählte Repair-Café zu akzeptieren. Der Klick auf `Annehmen für gewähltes Repair-Café` bringt mich zurück zum Hauptmenü.
|
||||
- In der Liste der neuen Geräte ist das gerade bearbeitete Gerät nicht mehr sichtbar.
|
||||
|
||||
### Variante: Akzeptieren für späteres Repair-Café
|
||||
|
||||
Als Organisator habe ich eine eMail bekommen dass ein neues Gerät angemeldet wurde. Ich möchte dieses Gerät nun ansehen un die Eingangsüberprüfung durchführen.
|
||||
- Ich öffne die RepApp im Browser
|
||||
- Im Menü klicke ich auf Mitarbeiter, was mich zur Single Sign On Anmeldung bringt.
|
||||
- Ich melde mich mit meinem Repair-Café Konto an und werde automatisch zur Hauptmenü für die Organisatoren weitergeleitet.
|
||||
- Im Hauptmenü sehe ich das neue Gerät im Bereich Geräte.
|
||||
- Ich klicke auf den Knopf "Details" für das neue Gerät, und mein Browser öffnet eine Seite in der ich die Details zu dem Gerät sehe. Auf der Seite sind Knöpfe: `Annehmen für gewähltes Repair-Café`, `Annehmen für anderes Repair-Café`, `Ablehnen` und `Rückfrage`. Alle notwendigen Informationen sind vorhanden, aber da ich weiß dass im nächsten Repair-Café kein geeigneter Reparateur verfügbar ist entscheide ich mich das Gerät für ein späteres Repair-Café zu akzeptieren. Der Klick auf `Annehmen für anderes Repair-Café` bringt mich zurück zum Hauptmenü.
|
||||
- In der Liste der neuen Geräte ist das gerade bearbeitete Gerät nicht mehr sichtbar.
|
||||
|
||||
### Variante: Ablehnen
|
||||
|
||||
Als Organisator habe ich eine eMail bekommen dass ein neues Gerät angemeldet wurde. Ich möchte dieses Gerät nun ansehen un die Eingangsüberprüfung durchführen.
|
||||
- Ich öffne die RepApp im Browser
|
||||
- Im Menü klicke ich auf Mitarbeiter, was mich zur Single Sign On Anmeldung bringt.
|
||||
- Ich melde mich mit meinem Repair-Café Konto an und werde automatisch zur Hauptmenü für die Organisatoren weitergeleitet.
|
||||
- Im Hauptmenü sehe ich das neue Gerät im Bereich Geräte.
|
||||
- Ich klicke auf den Knopf "Details" für das neue Gerät, und mein Browser öffnet eine Seite in der ich die Details zu dem Gerät sehe. Auf der Seite sind Knöpfe: `Annehmen für gewähltes Repair-Café`, `Annehmen für anderes Repair-Café`, `Ablehnen` und `Rückfrage`. Alle notwendigen Informationen sind vorhanden, aber es gibt keinen geeigneten Reparateur im Repair-Café, und ich entscheide mich das Gerät abzulehnen. Der Klick auf `Ablehnen` bringt mich zu einer neuen Seite.
|
||||
- Auf der neuen Seite sehe ich ein Formular mit einem Textbereich der die Überschrift "Antwort an den Gast" trägt. Im Formular ist bereits ein Standard-Text zur Ablehnung eines Geräts. Ich passe den Text an um dem Gast mitzuteilen dass wir leider keinen geeigneten Reparateur haben und klicke auf den Knopf `Absenden`. Der Klick bringt mich zurück zum Hauptmenü.
|
||||
- In der Liste der neuen Geräte ist das gerade bearbeitete Gerät nicht mehr sichtbar.
|
||||
|
||||
### Variante: Rückfrage
|
||||
|
||||
Als Organisator habe ich eine eMail bekommen dass ein neues Gerät angemeldet wurde. Ich möchte dieses Gerät nun ansehen un die Eingangsüberprüfung durchführen.
|
||||
- Ich öffne die RepApp im Browser
|
||||
- Im Menü klicke ich auf Mitarbeiter, was mich zur Single Sign On Anmeldung bringt.
|
||||
- Ich melde mich mit meinem Repair-Café Konto an und werde automatisch zur Hauptmenü für die Organisatoren weitergeleitet.
|
||||
- Im Hauptmenü sehe ich das neue Gerät im Bereich Geräte.
|
||||
- Ich klicke auf den Knopf "Details" für das neue Gerät, und mein Browser öffnet eine Seite in der ich die Details zu dem Gerät sehe. Auf der Seite sind Knöpfe: `Annehmen für gewähltes Repair-Café`, `Annehmen für anderes Repair-Café`, `Ablehnen` und `Rückfrage`. Da notwendige Informationen fehlen entscheide ich mich eine Rückfrage an den Gast zu stellen, und drücke den Knopf `Rückfrage`. Der Klick bringt mich zu einer neuen Seite.
|
||||
- Auf der neuen Seite sehe ich ein Formular mit einem Textbereich der die Überschrift "Rückfrage" trägt. Im Formular ist bereits ein Standard-Text für Rückfragen. Ich passe den Text an um die notwendigen Informationen anzufragen und klicke auf den Knopf `Absenden`. Der Klick bringt mich zurück zum Hauptmenü.
|
||||
- In der Liste der neuen Geräte ist das gerade bearbeitete Gerät nicht mehr sichtbar.
|
||||
|
|
@ -3,10 +3,6 @@ site_url: https://makes-hacks-hip.github.io/RepApp/
|
|||
nav:
|
||||
- RepApp:
|
||||
- Beschreibung: 'index.md'
|
||||
- Anforderungen: 'use_cases.md'
|
||||
- Abläufe: 'workflows.md'
|
||||
- Ansichten: 'views.md'
|
||||
- Daten-Modell: 'models.md'
|
||||
- 'Developer documentation':
|
||||
- Overview: 'dev/index.md'
|
||||
- Users: 'dev/users.md'
|
||||
|
|
|
|||
118
repapp/locale/de/LC_MESSAGES/django.po
Normal file
118
repapp/locale/de/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-08-29 09:17+0200\n"
|
||||
"PO-Revision-Date: 2023-08-29 09:17+0200\n"
|
||||
"Last-Translator: <thomas@irgang.eu>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Translated-Using: django-rosetta 0.9.9\n"
|
||||
|
||||
#: one_time_login/models.py:17
|
||||
msgid "Secret"
|
||||
msgstr "Geheimnis"
|
||||
|
||||
#: one_time_login/models.py:18
|
||||
msgid "URL"
|
||||
msgstr "URL"
|
||||
|
||||
#: one_time_login/models.py:20
|
||||
msgid "Creation date"
|
||||
msgstr "Erstellungsdatum"
|
||||
|
||||
#: one_time_login/models.py:22
|
||||
msgid "Was the login used?"
|
||||
msgstr "Wurde der Link bereits verwendet?"
|
||||
|
||||
#: one_time_login/models.py:23
|
||||
msgid "Login date"
|
||||
msgstr "Login Datum"
|
||||
|
||||
#: one_time_login/models.py:25
|
||||
msgid "User"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: one_time_login/models.py:28
|
||||
msgid "One time login"
|
||||
msgstr "Einmal-Anmeldung"
|
||||
|
||||
#: one_time_login/models.py:29
|
||||
msgid "One time logins"
|
||||
msgstr "Einmal-Anmeldungen"
|
||||
|
||||
#: one_time_login/templates/one_time_login/mail_content.html:11
|
||||
#: one_time_login/templates/one_time_login/mail_content.txt:2
|
||||
msgid "Hello"
|
||||
msgstr "Hallo"
|
||||
|
||||
#: one_time_login/templates/one_time_login/mail_content.html:13
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" we prepared a one time login for you. Click <a href=\"%(link)s\">here</a> to use the link.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Wir haben eine Einmal-Anmeldung für Sie vorbereitet. Klicken Sie auf <a href=\"%(link)s\">diesen</a> Link um sich anzumelden."
|
||||
|
||||
#: one_time_login/templates/one_time_login/mail_content.html:18
|
||||
#: one_time_login/templates/one_time_login/mail_content.txt:8
|
||||
msgid "Kind regards"
|
||||
msgstr "Viele Grüße,"
|
||||
|
||||
#: one_time_login/templates/one_time_login/mail_content.txt:4
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"we prepared a one time login for you. Open %(link)s in your browser to use the link.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Wir haben eine Einmal-Anmeldung für Sie vorbereitet. Öffnen Sie %(link)s in ihrem Browser um sich anzumelden.\n"
|
||||
|
||||
#: one_time_login/templates/one_time_login/mail_subject.txt:1
|
||||
msgid "one time login"
|
||||
msgstr "Einmal-Anmeldung"
|
||||
|
||||
#: one_time_login/utils.py:76
|
||||
msgid "A new login link was sent by mail."
|
||||
msgstr "Eine neue Einmal-Anmeldung wurde per eMail gesendet."
|
||||
|
||||
#: one_time_login/utils.py:79
|
||||
msgid "Sending of one time login link failed!"
|
||||
msgstr "Senden der Einmal-Anmeldung ist fehlgeschlagen!"
|
||||
|
||||
#: one_time_login/utils.py:90
|
||||
msgid "Login was used already."
|
||||
msgstr "Die Einmal-Anmeldung wurde bereits verwendet."
|
||||
|
||||
#: one_time_login/views.py:25
|
||||
msgid "Login successful!"
|
||||
msgstr "Die Anmeldung war erfolgreich."
|
||||
|
||||
#: one_time_login/views.py:28
|
||||
msgid "Login failed!"
|
||||
msgstr "Die Anmeldung ist fehlgeschlagen!"
|
||||
|
||||
#: repapp/settings.py:179
|
||||
msgid "German"
|
||||
msgstr "Deutsch"
|
||||
|
||||
#: repapp/settings.py:180
|
||||
msgid "English"
|
||||
msgstr "Englisch"
|
||||
|
||||
#: repapp/urls.py:24
|
||||
#| msgid "one time login"
|
||||
msgid "one_time_login/"
|
||||
msgstr "einmal-anmeldung/"
|
||||
118
repapp/locale/en/LC_MESSAGES/django.po
Normal file
118
repapp/locale/en/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-08-29 09:17+0200\n"
|
||||
"PO-Revision-Date: 2023-08-29 09:18+0200\n"
|
||||
"Last-Translator: <thomas@irgang.eu>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Translated-Using: django-rosetta 0.9.9\n"
|
||||
|
||||
#: one_time_login/models.py:17
|
||||
msgid "Secret"
|
||||
msgstr "Secret"
|
||||
|
||||
#: one_time_login/models.py:18
|
||||
msgid "URL"
|
||||
msgstr "URL"
|
||||
|
||||
#: one_time_login/models.py:20
|
||||
msgid "Creation date"
|
||||
msgstr "Creation date"
|
||||
|
||||
#: one_time_login/models.py:22
|
||||
msgid "Was the login used?"
|
||||
msgstr "Was the login used?"
|
||||
|
||||
#: one_time_login/models.py:23
|
||||
msgid "Login date"
|
||||
msgstr "Login date"
|
||||
|
||||
#: one_time_login/models.py:25
|
||||
msgid "User"
|
||||
msgstr "User"
|
||||
|
||||
#: one_time_login/models.py:28
|
||||
msgid "One time login"
|
||||
msgstr "One time login"
|
||||
|
||||
#: one_time_login/models.py:29
|
||||
msgid "One time logins"
|
||||
msgstr "One time logins"
|
||||
|
||||
#: one_time_login/templates/one_time_login/mail_content.html:11
|
||||
#: one_time_login/templates/one_time_login/mail_content.txt:2
|
||||
msgid "Hello"
|
||||
msgstr "Hello"
|
||||
|
||||
#: one_time_login/templates/one_time_login/mail_content.html:13
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" we prepared a one time login for you. Click <a href=\"%(link)s\">here</a> to use the link.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"we prepared a one time login for you. Click <a href=\"%(link)s\">here</a> to use the link."
|
||||
|
||||
#: one_time_login/templates/one_time_login/mail_content.html:18
|
||||
#: one_time_login/templates/one_time_login/mail_content.txt:8
|
||||
msgid "Kind regards"
|
||||
msgstr "Kind regards"
|
||||
|
||||
#: one_time_login/templates/one_time_login/mail_content.txt:4
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"we prepared a one time login for you. Open %(link)s in your browser to use the link.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"we prepared a one time login for you. Open %(link)s in your browser to use the link.\n"
|
||||
|
||||
#: one_time_login/templates/one_time_login/mail_subject.txt:1
|
||||
msgid "one time login"
|
||||
msgstr "one time login"
|
||||
|
||||
#: one_time_login/utils.py:76
|
||||
msgid "A new login link was sent by mail."
|
||||
msgstr "A new login link was sent by mail."
|
||||
|
||||
#: one_time_login/utils.py:79
|
||||
msgid "Sending of one time login link failed!"
|
||||
msgstr "Sending of one time login link failed!"
|
||||
|
||||
#: one_time_login/utils.py:90
|
||||
msgid "Login was used already."
|
||||
msgstr "Login was used already."
|
||||
|
||||
#: one_time_login/views.py:25
|
||||
msgid "Login successful!"
|
||||
msgstr "Login successful!"
|
||||
|
||||
#: one_time_login/views.py:28
|
||||
msgid "Login failed!"
|
||||
msgstr "Login failed!"
|
||||
|
||||
#: repapp/settings.py:179
|
||||
msgid "German"
|
||||
msgstr "German"
|
||||
|
||||
#: repapp/settings.py:180
|
||||
msgid "English"
|
||||
msgstr "English"
|
||||
|
||||
#: repapp/urls.py:24
|
||||
#| msgid "one time login"
|
||||
msgid "one_time_login/"
|
||||
msgstr "one-time-login/"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MailIntefaceConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'mail_inteface'
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import os
|
||||
import datetime
|
||||
import random
|
||||
import logging
|
||||
from hashlib import sha256
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .models import OneTimeLogin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_one_time_login(user, url) -> str:
|
||||
"""
|
||||
create_one_time_login creates a one time login object for guest user logins.
|
||||
"""
|
||||
secret = sha256(
|
||||
f'{user.email}{url}{datetime.datetime.now()}{random.randint(0,9999999)}'.encode(
|
||||
'utf-8')
|
||||
).hexdigest()
|
||||
secret_hash = sha256(secret.encode('utf-8')).hexdigest()
|
||||
otl = OneTimeLogin(
|
||||
secret=secret_hash,
|
||||
user=user,
|
||||
url=url,
|
||||
)
|
||||
otl.save()
|
||||
return secret
|
||||
|
||||
|
||||
def send_one_time_login_mail(secret, mail, request):
|
||||
"""
|
||||
Send a mail with a one time login link.
|
||||
"""
|
||||
url = request.build_absolute_uri(
|
||||
f'/onetimelogin/{secret}/')
|
||||
subject = render_to_string('one_time_login/mail_subject.txt', {
|
||||
'organization': settings.ORGANIZATION,
|
||||
}).replace('\n', '')
|
||||
html = render_to_string('one_time_login/mail_content.html', {
|
||||
'link': url,
|
||||
'organization': settings.ORGANIZATION,
|
||||
})
|
||||
text = render_to_string('one_time_login/mail_content.html', {
|
||||
'link': url,
|
||||
'organization': settings.ORGANIZATION,
|
||||
})
|
||||
|
||||
send_ok = send_mail(
|
||||
subject=subject,
|
||||
message=text,
|
||||
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
|
||||
recipient_list=[mail],
|
||||
fail_silently=True,
|
||||
html_message=html
|
||||
)
|
||||
|
||||
if send_ok > 0:
|
||||
messages.add_message(request, messages.INFO, _(
|
||||
'A new login link was sent by mail.'))
|
||||
26
repapp/one_time_login/authentication_backends.py
Normal file
26
repapp/one_time_login/authentication_backends.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"""
|
||||
Authentication backends for RepApp.
|
||||
"""
|
||||
import logging
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from .models import OneTimeLogin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OneTimeLoginBackend(ModelBackend):
|
||||
"""
|
||||
OneTimeLoginBackend implements a login using a onetime secret.
|
||||
"""
|
||||
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
login = OneTimeLogin.objects.filter(secret=username).first()
|
||||
if login:
|
||||
logger.debug(
|
||||
'One time login authentication for secret %s was successful', username)
|
||||
return login.user
|
||||
else:
|
||||
logger.info(
|
||||
'One time login authentication for secret %s failed!', username)
|
||||
return None
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
|
|
@ -7,12 +8,14 @@
|
|||
<title>One time login</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello,</p>
|
||||
<p>{% trans "Hello" %},</p>
|
||||
<p>
|
||||
{% blocktrans with link=link %}
|
||||
we prepared a one time login for you. Click <a href="{{ link }}">here</a> to use the link.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
Kind regards,
|
||||
{% trans "Kind regards" %},
|
||||
{{ organization }}
|
||||
</p>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
Hello,
|
||||
{% load i18n %}
|
||||
{% trans "Hello" %},
|
||||
|
||||
{% blocktrans with link=link %}
|
||||
we prepared a one time login for you. Open {{ link }} in your browser to use the link.
|
||||
{% endblocktrans %}
|
||||
|
||||
Kind regards,
|
||||
{% trans "Kind regards" %},
|
||||
{{ organization }}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{{ organization }} - Einmal-Ammeldelink
|
||||
{% load i18n %}{{ organization }} - {% trans "one time login" %}
|
||||
|
|
@ -1,3 +1,211 @@
|
|||
from django.test import TestCase
|
||||
import logging
|
||||
from django.test import TestCase, Client
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.messages.storage.fallback import FallbackStorage
|
||||
from django.contrib.messages import get_messages
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import reverse
|
||||
from django.core import mail
|
||||
from django.utils import translation
|
||||
from .utils import create_one_time_login, one_time_login, send_one_time_login_mail
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OneTimeLoginModelTestCase(TestCase):
|
||||
"""
|
||||
Tests all logic contained in the OneTimeLogin model.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
user = get_user_model().objects.create_user(
|
||||
"john",
|
||||
"lennon@thebeatles.com",
|
||||
"johnpassword")
|
||||
user.save()
|
||||
|
||||
def test_get_absolute_url(self):
|
||||
user = get_user_model().objects.get(username='john')
|
||||
url = "/test/url"
|
||||
|
||||
otl = create_one_time_login(user, url)
|
||||
|
||||
abs_url = otl.get_absolute_url()
|
||||
|
||||
logger.debug('test_get_absolute_url: url %s', abs_url)
|
||||
|
||||
assert otl.secret in abs_url, 'secret is part of URL'
|
||||
|
||||
|
||||
class UtilsTestCase(TestCase):
|
||||
"""
|
||||
Tests all logic contained in the util functions.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
# prepare request factory
|
||||
self.factory = RequestFactory()
|
||||
|
||||
# prepare test user
|
||||
user = get_user_model().objects.create_user(
|
||||
"john",
|
||||
"lennon@thebeatles.com",
|
||||
"johnpassword")
|
||||
user.save()
|
||||
|
||||
# use english translations
|
||||
translation.activate('en')
|
||||
|
||||
def test_create_one_time_login(self):
|
||||
user = get_user_model().objects.get(username='john')
|
||||
url = "/test/url"
|
||||
|
||||
otl = create_one_time_login(user, url)
|
||||
|
||||
assert len(otl.secret) > 10, 'length of secret'
|
||||
assert otl.user is user
|
||||
assert otl.url is url
|
||||
|
||||
def test_one_time_login_ok(self):
|
||||
user = get_user_model().objects.get(username='john')
|
||||
url = "/test/url"
|
||||
|
||||
otl = create_one_time_login(user, url)
|
||||
request = self.factory.get(otl.get_absolute_url())
|
||||
|
||||
(otl_user, otl_url) = one_time_login(request, otl.secret)
|
||||
|
||||
logger.debug('test_one_time_login_ok: user %s, url %s',
|
||||
otl_user, otl_url)
|
||||
|
||||
assert otl_user is not None, 'user was found'
|
||||
assert otl_user == user, 'user is the right one'
|
||||
assert otl_url == url, 'URL is the right one'
|
||||
|
||||
def test_one_time_login_wrong(self):
|
||||
secret = "a_wrong_secret"
|
||||
|
||||
url = reverse('one_time_login:login', args=[secret])
|
||||
|
||||
request = self.factory.get(url)
|
||||
|
||||
(otl_user, otl_url) = one_time_login(request, secret)
|
||||
|
||||
logger.debug('test_one_time_login_wrong: user %s, url %s',
|
||||
otl_user, otl_url)
|
||||
|
||||
assert otl_user is None, 'user was not found'
|
||||
assert otl_url is None, 'no redirect URL'
|
||||
|
||||
def test_one_time_login_used(self):
|
||||
user = get_user_model().objects.get(username='john')
|
||||
url = "/test/url"
|
||||
|
||||
otl = create_one_time_login(user, url)
|
||||
request = self.factory.get(otl.get_absolute_url())
|
||||
# Add support django messaging framework
|
||||
setattr(request, 'session', 'session')
|
||||
messages = FallbackStorage(request)
|
||||
setattr(request, '_messages', messages)
|
||||
|
||||
(otl_user, otl_url) = one_time_login(request, otl.secret)
|
||||
|
||||
logger.debug('test_one_time_login_used: first try: user %s, url %s',
|
||||
otl_user, otl_url)
|
||||
|
||||
assert otl_user is not None, 'user was found'
|
||||
assert otl_user == user, 'user is the right one'
|
||||
assert otl_url == url, 'URL is the right one'
|
||||
|
||||
(otl_user, otl_url) = one_time_login(request, otl.secret)
|
||||
|
||||
logger.debug('test_one_time_login_used: second try: user %s, url %s',
|
||||
otl_user, otl_url)
|
||||
|
||||
assert otl_user is None, 'user was not found'
|
||||
assert otl_url is None, 'no redirect URL'
|
||||
|
||||
messages = [m.message for m in get_messages(request)]
|
||||
|
||||
logger.debug('test_one_time_login_used: messages %r', messages)
|
||||
|
||||
assert len(messages) == 2, 'there are two message'
|
||||
assert 'used already' in str(messages[0]), 'already used message'
|
||||
assert 'new login link' in str(messages[1]), 'new login sent'
|
||||
|
||||
def test_send_one_time_login_mail(self):
|
||||
user = get_user_model().objects.get(username='john')
|
||||
url = "/test/url"
|
||||
|
||||
otl = create_one_time_login(user, url)
|
||||
request = self.factory.get(otl.get_absolute_url())
|
||||
# Add support django messaging framework
|
||||
setattr(request, 'session', 'session')
|
||||
setattr(request, '_messages', FallbackStorage(request))
|
||||
|
||||
send_one_time_login_mail(otl.secret, user.email, request)
|
||||
|
||||
messages = [m.message for m in get_messages(request)]
|
||||
|
||||
logger.debug('test_send_one_time_login_mail: messages %r', messages)
|
||||
|
||||
assert len(messages) == 1, 'there are two message'
|
||||
assert 'new login link' in str(messages[0]), 'new login sent'
|
||||
|
||||
logger.debug('test_send_one_time_login_mail: outbox %r',
|
||||
mail.outbox)
|
||||
|
||||
sent_mail = mail.outbox[0]
|
||||
|
||||
logger.debug('test_send_one_time_login_mail: to %r',
|
||||
sent_mail.to)
|
||||
logger.debug('test_send_one_time_login_mail: content %r',
|
||||
sent_mail.body)
|
||||
|
||||
assert otl.user.email in sent_mail.to, 'receiver address'
|
||||
assert otl.get_absolute_url() in sent_mail.body, 'mail content'
|
||||
|
||||
|
||||
class ViewsTestCase(TestCase):
|
||||
"""
|
||||
Test one time login views.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
# prepare test user
|
||||
user = get_user_model().objects.create_user(
|
||||
"john",
|
||||
"lennon@thebeatles.com",
|
||||
"johnpassword")
|
||||
user.save()
|
||||
|
||||
# use english translations
|
||||
translation.activate('en')
|
||||
|
||||
def test_one_time_login_ok(self):
|
||||
user = get_user_model().objects.get(username='john')
|
||||
url = reverse('one_time_login:protected')
|
||||
|
||||
otl = create_one_time_login(user, url)
|
||||
|
||||
client = Client()
|
||||
|
||||
response = client.get(otl.get_absolute_url())
|
||||
|
||||
logger.debug('test_one_time_login: response %r', response)
|
||||
|
||||
assert response.status_code == 302, 'response is redirect'
|
||||
assert response.url == reverse(
|
||||
'one_time_login:protected'), 'redirect URL'
|
||||
|
||||
def test_one_time_login_wrong(self):
|
||||
client = Client()
|
||||
|
||||
response = client.get(
|
||||
reverse('one_time_login:login', args=['1234567']))
|
||||
|
||||
logger.debug('test_one_time_login: response %r', response)
|
||||
|
||||
assert response.status_code == 302, 'response is redirect'
|
||||
assert response.url == '/', 'redirect URL'
|
||||
|
|
|
|||
|
|
@ -9,5 +9,7 @@ app_name = 'one_time_login'
|
|||
|
||||
|
||||
urlpatterns = [
|
||||
path("<str:secret>/", views.one_time_login, name="login"),
|
||||
path("protected/", views.protected_test, name="protected"),
|
||||
path("dummy/", views.dummy_content, name="dummy"),
|
||||
path("<str:secret>/", views.login_view, name="login"),
|
||||
]
|
||||
|
|
|
|||
121
repapp/one_time_login/utils.py
Normal file
121
repapp/one_time_login/utils.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import datetime
|
||||
import random
|
||||
import logging
|
||||
from hashlib import sha256
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import translation
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.timezone import now
|
||||
from django.contrib.auth import authenticate
|
||||
from .models import OneTimeLogin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_one_time_login(user, url) -> OneTimeLogin:
|
||||
"""
|
||||
create_one_time_login creates a one time login object for guest 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()
|
||||
|
||||
logger.debug('One time login created for user %s and URL %s', user, url)
|
||||
|
||||
return otl
|
||||
|
||||
|
||||
def send_one_time_login_mail(secret, mail, request, language=None):
|
||||
"""
|
||||
Send a mail with a one time login link.
|
||||
"""
|
||||
if language is None:
|
||||
language = settings.LANGUAGE_CODE
|
||||
|
||||
url = request.build_absolute_uri(reverse_lazy(
|
||||
'one_time_login:login', kwargs={'secret': secret}))
|
||||
|
||||
cur_language = translation.get_language()
|
||||
try:
|
||||
translation.activate(language)
|
||||
subject = render_to_string('one_time_login/mail_subject.txt', {
|
||||
'organization': settings.ORGANIZATION,
|
||||
}).replace('\n', '')
|
||||
html = render_to_string('one_time_login/mail_content.html', {
|
||||
'link': url,
|
||||
'organization': settings.ORGANIZATION,
|
||||
})
|
||||
text = render_to_string('one_time_login/mail_content.html', {
|
||||
'link': url,
|
||||
'organization': settings.ORGANIZATION,
|
||||
})
|
||||
finally:
|
||||
translation.activate(cur_language)
|
||||
|
||||
send_ok = send_mail(
|
||||
subject=subject,
|
||||
message=text,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list=[mail],
|
||||
fail_silently=True,
|
||||
html_message=html
|
||||
)
|
||||
|
||||
logger.debug('One time login mail sent to mail %s', mail)
|
||||
|
||||
if send_ok > 0:
|
||||
messages.add_message(request, messages.INFO, _(
|
||||
'A new login link was sent by mail.'))
|
||||
else:
|
||||
messages.add_message(request, messages.ERROR, _(
|
||||
'Sending of one time login link failed!'))
|
||||
|
||||
|
||||
def one_time_login(request, secret):
|
||||
"""
|
||||
Evaluates a one time login request.
|
||||
"""
|
||||
try:
|
||||
otl = OneTimeLogin.objects.get(secret=secret)
|
||||
except OneTimeLogin.DoesNotExist:
|
||||
logger.info('No one time login for secret %s', secret)
|
||||
return (None, None)
|
||||
|
||||
if otl.login_used:
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('Login was used already.'))
|
||||
logger.info('One time login with secret %s for user %s and URL %s was already used',
|
||||
secret, otl.user, otl.url)
|
||||
|
||||
if otl.user.email:
|
||||
logger.debug(
|
||||
'Creating and sending one time login for user %s and URL %s', otl.user, otl.url)
|
||||
# create and send a new one time login
|
||||
new_secret = create_one_time_login(otl.user, otl.url)
|
||||
send_one_time_login_mail(new_secret, otl.user.email, request)
|
||||
|
||||
return (None, None)
|
||||
|
||||
else:
|
||||
otl.login_used = True
|
||||
otl.login_date = now()
|
||||
otl.save()
|
||||
|
||||
user = authenticate(request, username=secret, password=None)
|
||||
|
||||
logger.info('Authenticated user %s with one time login.', user)
|
||||
|
||||
return (user, otl.url)
|
||||
|
|
@ -1,43 +1,52 @@
|
|||
import time
|
||||
import logging
|
||||
from hashlib import sha256
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .models import OneTimeLogin
|
||||
from .api import create_one_time_login, send_one_time_login_mail
|
||||
from .utils import one_time_login
|
||||
|
||||
|
||||
def one_time_login(request, secret: str):
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def login_view(request, secret: str):
|
||||
"""
|
||||
View for one time login.
|
||||
"""
|
||||
# waste a little time as brute force protection
|
||||
time.sleep(1)
|
||||
|
||||
secret_hash = sha256(secret.encode('utf-8')).hexdigest()
|
||||
otl = get_object_or_404(OneTimeLogin, secret=secret_hash)
|
||||
logger.debug('one_time_login: secret %s', secret)
|
||||
|
||||
if otl.login_used:
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('Login was used already.'))
|
||||
new_secret = create_one_time_login(otl.user, otl.url)
|
||||
send_one_time_login_mail(new_secret, otl.user.email, request)
|
||||
return HttpResponseRedirect(reverse_lazy('index'))
|
||||
else:
|
||||
otl.login_used = True
|
||||
otl.login_date = now()
|
||||
otl.save()
|
||||
(user, url) = one_time_login(request, secret)
|
||||
|
||||
user = authenticate(request, username=secret_hash, password=None)
|
||||
logger.debug('one_time_login: user %s, url %s', user, url)
|
||||
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
messages.add_message(request, messages.INFO, _('Login successful!'))
|
||||
return HttpResponseRedirect(otl.url)
|
||||
if url is not None:
|
||||
logger.debug('one_time_login: redirecting to url %s', url)
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
return HttpResponseRedirect('/')
|
||||
|
||||
messages.add_message(request, messages.ERROR, _('Login failed!'))
|
||||
return HttpResponseRedirect(reverse_lazy('index'))
|
||||
return HttpResponseRedirect('/')
|
||||
|
||||
|
||||
@login_required
|
||||
def protected_test(request):
|
||||
return HttpResponse('protected content')
|
||||
|
||||
|
||||
@login_required
|
||||
def dummy_content(request):
|
||||
return HttpResponse("dummy content")
|
||||
|
|
|
|||
58
repapp/repapp/live_tests.py
Normal file
58
repapp/repapp/live_tests.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import time
|
||||
import logging
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import translation
|
||||
from django.urls import reverse
|
||||
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
||||
from selenium.webdriver.chrome.webdriver import WebDriver
|
||||
from one_time_login.utils import create_one_time_login
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LiveOneTimeLoginTests(StaticLiveServerTestCase):
|
||||
"""
|
||||
Live server test for one time login
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.selenium = WebDriver()
|
||||
cls.selenium.implicitly_wait(50)
|
||||
cls.selenium.maximize_window()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.selenium.quit()
|
||||
super().tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# prepare test user
|
||||
user = get_user_model().objects.create_user(
|
||||
"john",
|
||||
"lennon@thebeatles.com",
|
||||
"johnpassword")
|
||||
user.save()
|
||||
|
||||
# use english translations
|
||||
translation.activate('en')
|
||||
|
||||
# prepare one time login
|
||||
user = get_user_model().objects.get(username='john')
|
||||
self.url = reverse('one_time_login:protected')
|
||||
otl = create_one_time_login(user, self.url)
|
||||
self.otl_url = otl.get_absolute_url()
|
||||
|
||||
def test_one_time_login(self):
|
||||
url = f"{self.live_server_url}{self.otl_url}"
|
||||
logger.debug('Opening one time login URL %s', url)
|
||||
self.selenium.get(url)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
self.assertTrue('protected content' in self.selenium.page_source)
|
||||
self.assertTrue(self.url in self.selenium.current_url)
|
||||
|
|
@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
|
|||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
|
@ -38,11 +39,15 @@ INSTALLED_APPS = [
|
|||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
# enable rosetta online translation interface
|
||||
'rosetta',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
# enable translation
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
|
|
@ -104,13 +109,13 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
# LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
# TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
# USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
# USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
|
|
@ -160,3 +165,33 @@ 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")
|
||||
DEFAULT_FROM_EMAIL = os.getenv("DJANGO_SENDER_ADDRESS", "")
|
||||
|
||||
# Repapp language settings
|
||||
LANGUAGE_CODE = 'de'
|
||||
TIME_ZONE = 'Europe/Berlin'
|
||||
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
LANGUAGES = [
|
||||
('de', _('German')),
|
||||
('en', _('English')),
|
||||
]
|
||||
|
||||
LOCALE_PATHS = [
|
||||
BASE_DIR / 'locale',
|
||||
]
|
||||
|
||||
# Repapp specific authentication backends
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
# Backend for one time logins
|
||||
"one_time_login.authentication_backends.OneTimeLoginBackend",
|
||||
]
|
||||
|
||||
if DEBUG:
|
||||
AUTHENTICATION_BACKENDS += [
|
||||
# Default backend for admin login
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -16,8 +16,13 @@ Including another URLconf
|
|||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
urlpatterns = [
|
||||
urlpatterns = i18n_patterns(
|
||||
path('admin/', admin.site.urls),
|
||||
path('one_time_login/', include('one_time_login.urls', namespace='one_time_login')),
|
||||
]
|
||||
path(_('one_time_login/'),
|
||||
include('one_time_login.urls', namespace='one_time_login')),
|
||||
# URLs for rosetta translation interface
|
||||
path('rosetta/', include('rosetta.urls')),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RepappMembersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'repapp_members'
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
|
@ -1,16 +1,9 @@
|
|||
autopep8==2.0.2
|
||||
Django==4.2
|
||||
pep8==1.7.1
|
||||
cssbeautifier==1.14.7
|
||||
djlint==1.32.1
|
||||
jsbeautifier==1.14.7
|
||||
pycodestyle==2.10.0
|
||||
pylint==2.17.2
|
||||
pylint-django==2.5.3
|
||||
pylint-plugin-utils==0.7
|
||||
django-crispy-forms==2.0
|
||||
crispy-bootstrap4==2022.1
|
||||
crispy-bootstrap5==0.7
|
||||
mozilla-django-oidc==3.0.0
|
||||
djlint==1.23.3
|
||||
django-import-export==3.2.0
|
||||
selenium==4.9.0
|
||||
imap-tools==1.0.0
|
||||
django-ckeditor==6.5.1
|
||||
beautifulsoup4==4.12.2
|
||||
|
|
|
|||
|
|
@ -1,10 +1,2 @@
|
|||
Django==4.2
|
||||
gunicorn==20.1.0
|
||||
django-crispy-forms==2.0
|
||||
crispy-bootstrap4==2022.1
|
||||
crispy-bootstrap5==0.7
|
||||
mozilla-django-oidc==3.0.0
|
||||
django-import-export==3.2.0
|
||||
imap-tools==1.0.0
|
||||
django-ckeditor==6.5.1
|
||||
beautifulsoup4==4.12.2
|
||||
django-rosetta==0.9.9
|
||||
|
|
|
|||
Reference in a new issue