From 0f9cce95153e421f1d73f3cc4046a7f5e23347e3 Mon Sep 17 00:00:00 2001 From: Tom Irgang Date: Thu, 20 Apr 2023 20:52:24 +0200 Subject: [PATCH] email notifications, messages, device view --- .gitignore | 2 + cspell.json | 4 +- env.example | 7 + rc_hip/rc_hip/settings.py | 6 + ...ename_cafe_id_appointment_cafe_and_more.py | 80 +++++++++ rc_hip/repapp/models.py | 30 ++-- rc_hip/repapp/templates/repapp/base.html | 40 +++-- .../repapp/templates/repapp/device_view.html | 18 +++ .../mail/mail_register_device_html.html | 20 +++ .../mail/mail_register_device_subject.html | 1 + .../mail/mail_register_device_text.html | 5 + .../repapp/mail/notice_new_device.html | 3 + .../repapp/register_device_confirm.html | 12 ++ rc_hip/repapp/urls.py | 10 +- rc_hip/repapp/views.py | 152 ++++++++++++++---- 15 files changed, 325 insertions(+), 65 deletions(-) create mode 100644 env.example create mode 100644 rc_hip/repapp/migrations/0011_rename_cafe_id_appointment_cafe_and_more.py create mode 100644 rc_hip/repapp/templates/repapp/device_view.html create mode 100644 rc_hip/repapp/templates/repapp/mail/mail_register_device_html.html create mode 100644 rc_hip/repapp/templates/repapp/mail/mail_register_device_subject.html create mode 100644 rc_hip/repapp/templates/repapp/mail/mail_register_device_text.html create mode 100644 rc_hip/repapp/templates/repapp/mail/notice_new_device.html create mode 100644 rc_hip/repapp/templates/repapp/register_device_confirm.html diff --git a/.gitignore b/.gitignore index f2d17b8..306b0ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +.env + rc_hip/static rc_hip/static/** diff --git a/cspell.json b/cspell.json index a821b93..f3042a7 100644 --- a/cspell.json +++ b/cspell.json @@ -6,8 +6,8 @@ "words": [ "Reparateur", "Reparateure", - "reparateur_id", - "organisator_id", + "reparateur", + "organisator", "Reparateurs", "repapp" ], diff --git a/env.example b/env.example new file mode 100644 index 0000000..92c7c23 --- /dev/null +++ b/env.example @@ -0,0 +1,7 @@ + +export DJANGO_EMAIL_HOST="" +export DJANGO_EMAIL_PORT="25" +export DJANGO_EMAIL_HOST_USER="" +export DJANGO_EMAIL_HOST_PASSWORD="" +export DJANGO_EMAIL_USE_TLS="true" +export DJANGO_SENDER_ADDRESS="" diff --git a/rc_hip/rc_hip/settings.py b/rc_hip/rc_hip/settings.py index 4bc1163..c695e11 100644 --- a/rc_hip/rc_hip/settings.py +++ b/rc_hip/rc_hip/settings.py @@ -143,3 +143,9 @@ CSRF_TRUSTED_ORIGINS = [ 'https://repapp.rc-hip.de', 'http://127.0.0.1:8020' ] + +EMAIL_HOST = os.getenv("DJANGO_EMAIL_HOST", '') +EMAIL_PORT = (int)(os.getenv("DJANGO_EMAIL_PORT", '25')) +EMAIL_HOST_USER = os.getenv("DJANGO_EMAIL_HOST_USER", None) +EMAIL_HOST_PASSWORD = os.getenv("DJANGO_EMAIL_HOST_PASSWORD", None) +EMAIL_USE_TLS = os.getenv("DJANGO_EMAIL_USE_TLS", 'true') in ('true', '1', 't') diff --git a/rc_hip/repapp/migrations/0011_rename_cafe_id_appointment_cafe_and_more.py b/rc_hip/repapp/migrations/0011_rename_cafe_id_appointment_cafe_and_more.py new file mode 100644 index 0000000..2ecef41 --- /dev/null +++ b/rc_hip/repapp/migrations/0011_rename_cafe_id_appointment_cafe_and_more.py @@ -0,0 +1,80 @@ +# Generated by Django 4.2 on 2023-04-20 18:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('repapp', '0010_candidate'), + ] + + operations = [ + migrations.RenameField( + model_name='appointment', + old_name='cafe_id', + new_name='cafe', + ), + migrations.RenameField( + model_name='appointment', + old_name='device_id', + new_name='device', + ), + migrations.RenameField( + model_name='appointment', + old_name='reparateur_id', + new_name='reparateur', + ), + migrations.RenameField( + model_name='candidate', + old_name='cafe_id', + new_name='cafe', + ), + migrations.RenameField( + model_name='candidate', + old_name='device_id', + new_name='device', + ), + migrations.RenameField( + model_name='device', + old_name='guest_id', + new_name='guest', + ), + migrations.RenameField( + model_name='question', + old_name='device_id', + new_name='device', + ), + migrations.RenameField( + model_name='question', + old_name='organisator_id', + new_name='organisator', + ), + migrations.RenameField( + model_name='question', + old_name='reparateur_id', + new_name='reparateur', + ), + migrations.AddField( + model_name='device', + name='cafe', + field=models.ForeignKey(default=2, on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café'), + preserve_default=False, + ), + migrations.AddField( + model_name='device', + name='confirmed', + field=models.BooleanField(default=False, verbose_name='Bestätigt'), + ), + migrations.AddField( + model_name='device', + name='secret', + field=models.CharField(default='TEST12345', max_length=200, verbose_name='Bestätigungscode'), + ), + migrations.AddField( + model_name='guest', + name='confirmed', + field=models.BooleanField(default=False, verbose_name='Bestätigt'), + ), + ] diff --git a/rc_hip/repapp/models.py b/rc_hip/repapp/models.py index 5775a44..35ef4dc 100644 --- a/rc_hip/repapp/models.py +++ b/rc_hip/repapp/models.py @@ -60,6 +60,7 @@ class Guest(models.Model): phone = models.CharField(max_length=200, verbose_name=_("Telefonnummer")) residence = models.CharField(max_length=200, verbose_name=_("Wohnort")) mail = models.CharField(max_length=200, verbose_name=_("eMail")) + confirmed = models.BooleanField(verbose_name=_("Bestätigt"), default=False) class Meta: verbose_name = _('Gast') @@ -74,11 +75,16 @@ class Device(models.Model): A Device is a broken device owned by a guest which shall be repaired during a Repair-Café. """ identifier = models.CharField(max_length=200, verbose_name=_("ID")) + secret = models.CharField( + max_length=200, verbose_name=_("Bestätigungscode"), default="TEST12345") device = models.CharField(max_length=200, verbose_name=_("Gerät")) error = models.TextField(verbose_name=_("Fehler")) follow_up = models.BooleanField(verbose_name=_("Folgetermin")) - guest_id = models.ForeignKey( + confirmed = models.BooleanField(verbose_name=_("Bestätigt"), default=False) + guest = models.ForeignKey( Guest, on_delete=models.CASCADE, null=True, verbose_name=_("Gast")) + cafe = models.ForeignKey( + Cafe, on_delete=models.CASCADE, null=False, verbose_name=_("Repair-Café")) class Meta: verbose_name = _('Gerät') @@ -95,11 +101,11 @@ class Appointment(models.Model): """ time = models.TimeField(verbose_name=_("Zeit")) confirmed = models.BooleanField(verbose_name=_("bestätigt")) - cafe_id = models.ForeignKey( + cafe = models.ForeignKey( Cafe, on_delete=models.CASCADE, verbose_name=_("Repair-Café")) - reparateur_id = models.ForeignKey( + reparateur = models.ForeignKey( Reparateur, on_delete=models.CASCADE, verbose_name=_("Reparateur")) - device_id = models.ForeignKey( + device = models.ForeignKey( Device, on_delete=models.CASCADE, null=True, verbose_name=_("Gerät")) class Meta: @@ -107,7 +113,7 @@ class Appointment(models.Model): verbose_name_plural = _('Termine') def __str__(self): - return f'Termin {self.cafe_id.event_date} {self.time} für Gerät {self.device_id.device}' + return f'Termin {self.cafe.event_date} {self.time} für Gerät {self.device.device}' class Candidate(models.Model): @@ -116,9 +122,9 @@ class Candidate(models.Model): or pre-assigned Reparateur. """ confirmed = models.BooleanField(verbose_name=_("bestätigt")) - cafe_id = models.ForeignKey( + cafe = models.ForeignKey( Cafe, on_delete=models.CASCADE, verbose_name=_("Repair-Café")) - device_id = models.ForeignKey( + device = models.ForeignKey( Device, on_delete=models.CASCADE, null=True, verbose_name=_("Gerät")) class Meta: @@ -126,7 +132,7 @@ class Candidate(models.Model): verbose_name_plural = _('Kandidaten') def __str__(self): - return f'Kandidat {self.cafe_id.event_date} für Gerät {self.device_id.device}' + return f'Kandidat {self.cafe.event_date} für Gerät {self.device.device}' class Question(models.Model): @@ -136,11 +142,11 @@ class Question(models.Model): question = models.TextField(verbose_name=_("Frage")) answer = models.TextField(verbose_name=_("Antwort")) date = models.DateField(verbose_name=_("Erstellungsdatum")) - organisator_id = models.ForeignKey( + organisator = models.ForeignKey( Organisator, on_delete=models.CASCADE, null=True, verbose_name=_("Organisator")) - reparateur_id = models.ForeignKey( + reparateur = models.ForeignKey( Reparateur, on_delete=models.CASCADE, null=True, verbose_name=_("Reparateur")) - device_id = models.ForeignKey( + device = models.ForeignKey( Device, on_delete=models.CASCADE, verbose_name=_("Gerät")) class Meta: @@ -148,4 +154,4 @@ class Question(models.Model): verbose_name_plural = _('Fragen') def __str__(self): - return f'Frage vom {self.date} zum Gerät {self.device_id.device}' + return f'Frage vom {self.date} zum Gerät {self.device.device}' diff --git a/rc_hip/repapp/templates/repapp/base.html b/rc_hip/repapp/templates/repapp/base.html index 7d18e85..115c1d2 100644 --- a/rc_hip/repapp/templates/repapp/base.html +++ b/rc_hip/repapp/templates/repapp/base.html @@ -2,27 +2,33 @@ - - - - RepApp{% block title %}{% endblock title %} + + + + RepApp + {% block title %} + {% endblock title %} +
-
{% block content %} {% endblock %}
+
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% block content %} + {% endblock content %} +
diff --git a/rc_hip/repapp/templates/repapp/device_view.html b/rc_hip/repapp/templates/repapp/device_view.html new file mode 100644 index 0000000..e5c67f7 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/device_view.html @@ -0,0 +1,18 @@ +{% extends "repapp/base.html" %} +{% block title %} + - {{ device.device }} +{% endblock title %} +{% block content %} +

Gerät: {{ device.device }}

+
+

Fehler:

+

{{ device.error }}

+

+ Folgetermin: + {% if device.follow_up %} + Ja + {% else %} + Nein + {% endif %} +

+{% endblock content %} diff --git a/rc_hip/repapp/templates/repapp/mail/mail_register_device_html.html b/rc_hip/repapp/templates/repapp/mail/mail_register_device_html.html new file mode 100644 index 0000000..69e5b75 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/mail/mail_register_device_html.html @@ -0,0 +1,20 @@ + + + +

Hallo {{ guest.name }},

+

+ Ihre Anmeldung von {{ device.device }} für das Repair-Café am {{ cafe.event_date|date }} + ist bei uns angekommen. +

+

+ Bitte bestätigen sie ihre Anmeldung in dem Sie + hier + klicken. +

+

+ Viele Grüße, +
+ Ihr Repair-Café +

+ + diff --git a/rc_hip/repapp/templates/repapp/mail/mail_register_device_subject.html b/rc_hip/repapp/templates/repapp/mail/mail_register_device_subject.html new file mode 100644 index 0000000..7268866 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/mail/mail_register_device_subject.html @@ -0,0 +1 @@ +Anmeldung von {{ device.device }} für das Repair-Café am {{ cafe.event_date|date }} diff --git a/rc_hip/repapp/templates/repapp/mail/mail_register_device_text.html b/rc_hip/repapp/templates/repapp/mail/mail_register_device_text.html new file mode 100644 index 0000000..053c2f9 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/mail/mail_register_device_text.html @@ -0,0 +1,5 @@ +Hallo {{ guest.name }}, +Ihre Anmeldung von {{ device.device }} für das Repair-Café am {{ cafe.event_date|date }} ist bei uns angekommen. +Bitte bestätigen sie ihre Anmeldung in dem Sie https://repapp.rc-hip.de/confirm/{{ device.identifier }}/code/{{ device.secret }} in ihrem Web-Browser aufrufen. +Viele Grüße, +Ihr Repair-Café diff --git a/rc_hip/repapp/templates/repapp/mail/notice_new_device.html b/rc_hip/repapp/templates/repapp/mail/notice_new_device.html new file mode 100644 index 0000000..a0dadfb --- /dev/null +++ b/rc_hip/repapp/templates/repapp/mail/notice_new_device.html @@ -0,0 +1,3 @@ +Ein neues Gerät wurde für das Repair-Café am {{ cafe.event_date|date }} angemeldet! +Gerät: {{ device.device }} +Gast: {{ guest.name }} ({{ guest.mail }}) diff --git a/rc_hip/repapp/templates/repapp/register_device_confirm.html b/rc_hip/repapp/templates/repapp/register_device_confirm.html new file mode 100644 index 0000000..e181974 --- /dev/null +++ b/rc_hip/repapp/templates/repapp/register_device_confirm.html @@ -0,0 +1,12 @@ +{% extends "repapp/base.html" %} +{% block title %} +{% endblock title %} +{% block content %} +

Anmeldung abgeschlossen

+
+

Ihr Gerät ist erfolgreich angemeldet!

+

+ Wir versuchen für sie einen Termin bei einem geeigneten Reparateur zu finden. + Wenn wir einen Termin gefunden haben werden Sie per eMail benachrichtigt. +

+{% endblock content %} diff --git a/rc_hip/repapp/urls.py b/rc_hip/repapp/urls.py index 391fe0e..3b7f444 100644 --- a/rc_hip/repapp/urls.py +++ b/rc_hip/repapp/urls.py @@ -8,15 +8,19 @@ from . import views urlpatterns = [ path("", views.IndexView.as_view(), name="index"), path( - "cafe//", + "cafe//", views.RegisterDeviceFormView.as_view(), name="register_device" ), path( - "cafe//device//", + "cafe//device//", views.RegisterGuestFormView.as_view(), name="register_guest" ), - path("cafe//device//guest/", + path("cafe//device//guest/", views.register_device_final, name="register_device_final"), + path("confirm//code/", + views.register_device_confirm, name="register_device_final"), + path("device/", + views.device_view, name="view_device"), ] diff --git a/rc_hip/repapp/views.py b/rc_hip/repapp/views.py index c6271c8..23d944f 100644 --- a/rc_hip/repapp/views.py +++ b/rc_hip/repapp/views.py @@ -2,12 +2,17 @@ Views of RepApp. """ import datetime +import os +import random from hashlib import sha256 from django.views import generic from django.urls import reverse_lazy from django.shortcuts import get_object_or_404, render -from django.http import HttpResponseRedirect -from .models import Cafe, Device, Guest +from django.http import HttpResponseRedirect, Http404 +from django.contrib import messages +from django.core.mail import send_mail +from django.template.loader import render_to_string +from .models import Cafe, Device, Guest, Organisator from .forms import RegisterDevice, RegisterGuest @@ -18,6 +23,8 @@ class IndexView(generic.ListView): template_name = "repapp/index.html" def get_queryset(self): + messages.add_message(self.request, messages.INFO, + 'Dies ist eine Test. Leider können sie sich hier noch nicht anmelden.') return Cafe.objects.filter(event_date__gte=datetime.date.today()) @@ -26,44 +33,50 @@ class RegisterDeviceFormView(generic.edit.FormView): form_class = RegisterDevice def form_valid(self, form): - cafe_id = self.kwargs['cafe_id'] - cafe = get_object_or_404(Cafe, pk=cafe_id) + cafe = self.kwargs['cafe'] + cafe = get_object_or_404(Cafe, pk=cafe) mail = form.cleaned_data['mail'] device = form.cleaned_data['device'] identifier = sha256( f'{device}{mail}{datetime.datetime.now()}'.encode('utf-8') ).hexdigest() + secret = sha256( + f'{device}{mail}{datetime.datetime.now()}{random.randint(0, 999999)}'.encode( + 'utf-8') + ).hexdigest() device = Device( identifier=identifier, + secret=secret, device=device, error=form.cleaned_data['error'], follow_up=form.cleaned_data['follow_up'], + cafe=cafe, ) device.save() guest = Guest.objects.filter(mail=mail).first() if guest: - device.guest_id = guest + device.guest = guest device.save() return HttpResponseRedirect( reverse_lazy('register_device_final', kwargs={ - 'cafe_id': cafe.pk, - 'device_identifier': device.identifier, - 'guest_identifier': guest.identifier}) + 'cafe': cafe.pk, + 'deviceentifier': device.identifier, + 'guestentifier': guest.identifier}) ) else: return HttpResponseRedirect( reverse_lazy('register_guest', kwargs={ - 'cafe_id': cafe.pk, - 'device_identifier': device.identifier, + 'cafe': cafe.pk, + 'deviceentifier': device.identifier, 'mail': mail}) ) def get_context_data(self, **kwargs): - cafe_id = self.kwargs['cafe_id'] - cafe = get_object_or_404(Cafe, pk=cafe_id) + cafe = self.kwargs['cafe'] + cafe = get_object_or_404(Cafe, pk=cafe) context = super(RegisterDeviceFormView, self).get_context_data( **kwargs @@ -77,11 +90,11 @@ class RegisterGuestFormView(generic.edit.FormView): form_class = RegisterGuest def form_valid(self, form): - cafe_id = self.kwargs['cafe_id'] - cafe = get_object_or_404(Cafe, pk=cafe_id) + cafe = self.kwargs['cafe'] + cafe = get_object_or_404(Cafe, pk=cafe) - device_identifier = self.kwargs['device_identifier'] - device = get_object_or_404(Device, identifier=device_identifier) + deviceentifier = self.kwargs['deviceentifier'] + device = get_object_or_404(Device, identifier=deviceentifier) name = form.cleaned_data['name'] residence = form.cleaned_data['residence'] @@ -97,22 +110,22 @@ class RegisterGuestFormView(generic.edit.FormView): ) guest.save() - device.guest_id = guest + device.guest = guest device.save() return HttpResponseRedirect( reverse_lazy('register_device_final', kwargs={ - 'cafe_id': cafe.pk, - 'device_identifier': device.identifier, - 'guest_identifier': guest.identifier}) + 'cafe': cafe.pk, + 'deviceentifier': device.identifier, + 'guestentifier': guest.identifier}) ) def get_context_data(self, **kwargs): - cafe_id = self.kwargs['cafe_id'] - cafe = get_object_or_404(Cafe, pk=cafe_id) + cafe = self.kwargs['cafe'] + cafe = get_object_or_404(Cafe, pk=cafe) - device_identifier = self.kwargs['device_identifier'] - device = get_object_or_404(Device, identifier=device_identifier) + deviceentifier = self.kwargs['deviceentifier'] + device = get_object_or_404(Device, identifier=deviceentifier) context = super(RegisterGuestFormView, self).get_context_data( **kwargs @@ -124,15 +137,92 @@ class RegisterGuestFormView(generic.edit.FormView): return context -def register_device_final(request, cafe_id, device_identifier, guest_identifier): - cafe = get_object_or_404(Cafe, pk=cafe_id) - device = get_object_or_404(Device, identifier=device_identifier) - guest = get_object_or_404(Guest, identifier=guest_identifier) - print(cafe) - print(device) - print(guest) +def register_device_final(request, cafe, deviceentifier, guestentifier): + cafe = get_object_or_404(Cafe, pk=cafe) + device = get_object_or_404(Device, identifier=deviceentifier) + guest = get_object_or_404(Guest, identifier=guestentifier) + + subject = render_to_string('repapp/mail/mail_register_device_subject.html', { + 'guest': guest, + 'device': device, + 'cafe': cafe, + }).replace('\n', '') + text = render_to_string('repapp/mail/mail_register_device_text.html', { + 'guest': guest, + 'device': device, + 'cafe': cafe, + }) + html = render_to_string('repapp/mail/mail_register_device_html.html', { + 'guest': guest, + 'device': device, + 'cafe': cafe, + }) + + ok = send_mail( + subject=subject, + message=text, + from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""), + recipient_list=[f"{guest.mail}"], + fail_silently=True, + html_message=html + ) + + if ok > 0: + messages.add_message(request, messages.INFO, + 'Die Bestätigungs-eMail wurde erfolgreich gesendet.') + else: + messages.add_message(request, messages.ERROR, + 'Fehler beim senden der Bestätigungs-eMail!') + return render( request, "repapp/register_device_final.html", {}, ) + + +def register_device_confirm(request, deviceentifier, device_secret): + device = get_object_or_404(Device, identifier=deviceentifier) + if not device.guest: + raise Http404('Unknown guest!') + + device.confirmed = True + device.save() + + if not device.guest.confirmed: + device.guest.confirmed = True + device.guest.save() + + text = render_to_string('repapp/mail/notice_new_device.html', { + 'guest': device.guest, + 'device': device, + 'cafe': device.cafe, + }) + + organizers = [] + for organizer in Organisator.objects.all(): + organizers.append(organizer.mail) + + send_mail( + subject=f"Neues Gerät { device.device }", + message=text, + from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""), + recipient_list=organizers, + fail_silently=True + ) + + return render( + request, + "repapp/register_device_confirm.html", + {"device": device, "guest": device.guest, "cafe": device.cafe}, + ) + + +def device_view(request, deviceentifier): + device = get_object_or_404(Device, identifier=deviceentifier) + + return render( + request, + "repapp/device_view.html", + {"device": device}, + )