email notifications, messages, device view

This commit is contained in:
Tom Irgang 2023-04-20 20:52:24 +02:00
parent 24dba867d7
commit 0f9cce9515
15 changed files with 325 additions and 65 deletions

2
.gitignore vendored
View file

@ -1,4 +1,6 @@
.env
rc_hip/static rc_hip/static
rc_hip/static/** rc_hip/static/**

View file

@ -6,8 +6,8 @@
"words": [ "words": [
"Reparateur", "Reparateur",
"Reparateure", "Reparateure",
"reparateur_id", "reparateur",
"organisator_id", "organisator",
"Reparateurs", "Reparateurs",
"repapp" "repapp"
], ],

7
env.example Normal file
View file

@ -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=""

View file

@ -143,3 +143,9 @@ CSRF_TRUSTED_ORIGINS = [
'https://repapp.rc-hip.de', 'https://repapp.rc-hip.de',
'http://127.0.0.1:8020' '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')

View file

@ -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'),
),
]

View file

@ -60,6 +60,7 @@ class Guest(models.Model):
phone = models.CharField(max_length=200, verbose_name=_("Telefonnummer")) phone = models.CharField(max_length=200, verbose_name=_("Telefonnummer"))
residence = models.CharField(max_length=200, verbose_name=_("Wohnort")) residence = models.CharField(max_length=200, verbose_name=_("Wohnort"))
mail = models.CharField(max_length=200, verbose_name=_("eMail")) mail = models.CharField(max_length=200, verbose_name=_("eMail"))
confirmed = models.BooleanField(verbose_name=_("Bestätigt"), default=False)
class Meta: class Meta:
verbose_name = _('Gast') 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é. 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")) 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")) device = models.CharField(max_length=200, verbose_name=_("Gerät"))
error = models.TextField(verbose_name=_("Fehler")) error = models.TextField(verbose_name=_("Fehler"))
follow_up = models.BooleanField(verbose_name=_("Folgetermin")) 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")) 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: class Meta:
verbose_name = _('Gerät') verbose_name = _('Gerät')
@ -95,11 +101,11 @@ class Appointment(models.Model):
""" """
time = models.TimeField(verbose_name=_("Zeit")) time = models.TimeField(verbose_name=_("Zeit"))
confirmed = models.BooleanField(verbose_name=_("bestätigt")) confirmed = models.BooleanField(verbose_name=_("bestätigt"))
cafe_id = models.ForeignKey( cafe = models.ForeignKey(
Cafe, on_delete=models.CASCADE, verbose_name=_("Repair-Café")) Cafe, on_delete=models.CASCADE, verbose_name=_("Repair-Café"))
reparateur_id = models.ForeignKey( reparateur = models.ForeignKey(
Reparateur, on_delete=models.CASCADE, verbose_name=_("Reparateur")) 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")) Device, on_delete=models.CASCADE, null=True, verbose_name=_("Gerät"))
class Meta: class Meta:
@ -107,7 +113,7 @@ class Appointment(models.Model):
verbose_name_plural = _('Termine') verbose_name_plural = _('Termine')
def __str__(self): 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): class Candidate(models.Model):
@ -116,9 +122,9 @@ class Candidate(models.Model):
or pre-assigned Reparateur. or pre-assigned Reparateur.
""" """
confirmed = models.BooleanField(verbose_name=_("bestätigt")) confirmed = models.BooleanField(verbose_name=_("bestätigt"))
cafe_id = models.ForeignKey( cafe = models.ForeignKey(
Cafe, on_delete=models.CASCADE, verbose_name=_("Repair-Café")) 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")) Device, on_delete=models.CASCADE, null=True, verbose_name=_("Gerät"))
class Meta: class Meta:
@ -126,7 +132,7 @@ class Candidate(models.Model):
verbose_name_plural = _('Kandidaten') verbose_name_plural = _('Kandidaten')
def __str__(self): 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): class Question(models.Model):
@ -136,11 +142,11 @@ class Question(models.Model):
question = models.TextField(verbose_name=_("Frage")) question = models.TextField(verbose_name=_("Frage"))
answer = models.TextField(verbose_name=_("Antwort")) answer = models.TextField(verbose_name=_("Antwort"))
date = models.DateField(verbose_name=_("Erstellungsdatum")) date = models.DateField(verbose_name=_("Erstellungsdatum"))
organisator_id = models.ForeignKey( organisator = models.ForeignKey(
Organisator, on_delete=models.CASCADE, null=True, verbose_name=_("Organisator")) 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")) 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")) Device, on_delete=models.CASCADE, verbose_name=_("Gerät"))
class Meta: class Meta:
@ -148,4 +154,4 @@ class Question(models.Model):
verbose_name_plural = _('Fragen') verbose_name_plural = _('Fragen')
def __str__(self): 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}'

View file

@ -2,27 +2,33 @@
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta <meta name="viewport"
name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
content="width=device-width, initial-scale=1, shrink-to-fit=no" <link rel="stylesheet"
/> href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
<link integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
rel="stylesheet" crossorigin="anonymous" />
href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous" crossorigin="anonymous"></script>
/> <title>RepApp
<script {% block title %}
src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" {% endblock title %}
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" </title>
crossorigin="anonymous"
></script>
<title>RepApp{% block title %}{% endblock title %}</title>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-8">{% block content %} {% endblock %}</div> <div class="col-8">
{% if messages %}
{% for message in messages %}
<div class="alert {% if message.tags %}alert-{{ message.tags }}{% else %}alert-primary{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% block content %}
{% endblock content %}
</div>
</div> </div>
</div> </div>
</body> </body>

View file

@ -0,0 +1,18 @@
{% extends "repapp/base.html" %}
{% block title %}
- {{ device.device }}
{% endblock title %}
{% block content %}
<h1 class="mt-2">Gerät: {{ device.device }}</h1>
<hr class="mt-0 mb-4">
<p>Fehler:</p>
<p>{{ device.error }}</p>
<p>
Folgetermin:
{% if device.follow_up %}
Ja
{% else %}
Nein
{% endif %}
</p>
{% endblock content %}

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="de">
<body>
<p>Hallo {{ guest.name }},</p>
<p>
Ihre Anmeldung von {{ device.device }} für das Repair-Café am {{ cafe.event_date|date }}
ist bei uns angekommen.
</p>
<p>
Bitte bestätigen sie ihre Anmeldung in dem Sie
<a href="https://repapp.rc-hip.de/confirm/{{ device.identifier }}/code/{{ device.secret }}">hier</a>
klicken.
</p>
<p>
Viele Grüße,
<br>
Ihr Repair-Café
</p>
</body>
</html>

View file

@ -0,0 +1 @@
Anmeldung von {{ device.device }} für das Repair-Café am {{ cafe.event_date|date }}

View file

@ -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é

View file

@ -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 }})

View file

@ -0,0 +1,12 @@
{% extends "repapp/base.html" %}
{% block title %}
{% endblock title %}
{% block content %}
<h1 class="mt-2">Anmeldung abgeschlossen</h1>
<hr class="mt-0 mb-4">
<p>Ihr Gerät ist erfolgreich angemeldet!</p>
<p>
Wir versuchen für sie einen Termin bei einem geeigneten Reparateur zu finden.
Wenn wir einen Termin gefunden haben werden Sie per eMail benachrichtigt.
</p>
{% endblock content %}

View file

@ -8,15 +8,19 @@ from . import views
urlpatterns = [ urlpatterns = [
path("", views.IndexView.as_view(), name="index"), path("", views.IndexView.as_view(), name="index"),
path( path(
"cafe/<int:cafe_id>/", "cafe/<int:cafe>/",
views.RegisterDeviceFormView.as_view(), views.RegisterDeviceFormView.as_view(),
name="register_device" name="register_device"
), ),
path( path(
"cafe/<int:cafe_id>/device/<str:device_identifier>/<str:mail>", "cafe/<int:cafe>/device/<str:deviceentifier>/<str:mail>",
views.RegisterGuestFormView.as_view(), views.RegisterGuestFormView.as_view(),
name="register_guest" name="register_guest"
), ),
path("cafe/<int:cafe_id>/device/<str:device_identifier>/guest/<str:guest_identifier>", path("cafe/<int:cafe>/device/<str:deviceentifier>/guest/<str:guestentifier>",
views.register_device_final, name="register_device_final"), views.register_device_final, name="register_device_final"),
path("confirm/<str:deviceentifier>/code/<str:device_secret>",
views.register_device_confirm, name="register_device_final"),
path("device/<str:deviceentifier>",
views.device_view, name="view_device"),
] ]

View file

@ -2,12 +2,17 @@
Views of RepApp. Views of RepApp.
""" """
import datetime import datetime
import os
import random
from hashlib import sha256 from hashlib import sha256
from django.views import generic from django.views import generic
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect, Http404
from .models import Cafe, Device, Guest 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 from .forms import RegisterDevice, RegisterGuest
@ -18,6 +23,8 @@ class IndexView(generic.ListView):
template_name = "repapp/index.html" template_name = "repapp/index.html"
def get_queryset(self): 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()) return Cafe.objects.filter(event_date__gte=datetime.date.today())
@ -26,44 +33,50 @@ class RegisterDeviceFormView(generic.edit.FormView):
form_class = RegisterDevice form_class = RegisterDevice
def form_valid(self, form): def form_valid(self, form):
cafe_id = self.kwargs['cafe_id'] cafe = self.kwargs['cafe']
cafe = get_object_or_404(Cafe, pk=cafe_id) cafe = get_object_or_404(Cafe, pk=cafe)
mail = form.cleaned_data['mail'] mail = form.cleaned_data['mail']
device = form.cleaned_data['device'] device = form.cleaned_data['device']
identifier = sha256( identifier = sha256(
f'{device}{mail}{datetime.datetime.now()}'.encode('utf-8') f'{device}{mail}{datetime.datetime.now()}'.encode('utf-8')
).hexdigest() ).hexdigest()
secret = sha256(
f'{device}{mail}{datetime.datetime.now()}{random.randint(0, 999999)}'.encode(
'utf-8')
).hexdigest()
device = Device( device = Device(
identifier=identifier, identifier=identifier,
secret=secret,
device=device, device=device,
error=form.cleaned_data['error'], error=form.cleaned_data['error'],
follow_up=form.cleaned_data['follow_up'], follow_up=form.cleaned_data['follow_up'],
cafe=cafe,
) )
device.save() device.save()
guest = Guest.objects.filter(mail=mail).first() guest = Guest.objects.filter(mail=mail).first()
if guest: if guest:
device.guest_id = guest device.guest = guest
device.save() device.save()
return HttpResponseRedirect( return HttpResponseRedirect(
reverse_lazy('register_device_final', kwargs={ reverse_lazy('register_device_final', kwargs={
'cafe_id': cafe.pk, 'cafe': cafe.pk,
'device_identifier': device.identifier, 'deviceentifier': device.identifier,
'guest_identifier': guest.identifier}) 'guestentifier': guest.identifier})
) )
else: else:
return HttpResponseRedirect( return HttpResponseRedirect(
reverse_lazy('register_guest', kwargs={ reverse_lazy('register_guest', kwargs={
'cafe_id': cafe.pk, 'cafe': cafe.pk,
'device_identifier': device.identifier, 'deviceentifier': device.identifier,
'mail': mail}) 'mail': mail})
) )
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
cafe_id = self.kwargs['cafe_id'] cafe = self.kwargs['cafe']
cafe = get_object_or_404(Cafe, pk=cafe_id) cafe = get_object_or_404(Cafe, pk=cafe)
context = super(RegisterDeviceFormView, self).get_context_data( context = super(RegisterDeviceFormView, self).get_context_data(
**kwargs **kwargs
@ -77,11 +90,11 @@ class RegisterGuestFormView(generic.edit.FormView):
form_class = RegisterGuest form_class = RegisterGuest
def form_valid(self, form): def form_valid(self, form):
cafe_id = self.kwargs['cafe_id'] cafe = self.kwargs['cafe']
cafe = get_object_or_404(Cafe, pk=cafe_id) cafe = get_object_or_404(Cafe, pk=cafe)
device_identifier = self.kwargs['device_identifier'] deviceentifier = self.kwargs['deviceentifier']
device = get_object_or_404(Device, identifier=device_identifier) device = get_object_or_404(Device, identifier=deviceentifier)
name = form.cleaned_data['name'] name = form.cleaned_data['name']
residence = form.cleaned_data['residence'] residence = form.cleaned_data['residence']
@ -97,22 +110,22 @@ class RegisterGuestFormView(generic.edit.FormView):
) )
guest.save() guest.save()
device.guest_id = guest device.guest = guest
device.save() device.save()
return HttpResponseRedirect( return HttpResponseRedirect(
reverse_lazy('register_device_final', kwargs={ reverse_lazy('register_device_final', kwargs={
'cafe_id': cafe.pk, 'cafe': cafe.pk,
'device_identifier': device.identifier, 'deviceentifier': device.identifier,
'guest_identifier': guest.identifier}) 'guestentifier': guest.identifier})
) )
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
cafe_id = self.kwargs['cafe_id'] cafe = self.kwargs['cafe']
cafe = get_object_or_404(Cafe, pk=cafe_id) cafe = get_object_or_404(Cafe, pk=cafe)
device_identifier = self.kwargs['device_identifier'] deviceentifier = self.kwargs['deviceentifier']
device = get_object_or_404(Device, identifier=device_identifier) device = get_object_or_404(Device, identifier=deviceentifier)
context = super(RegisterGuestFormView, self).get_context_data( context = super(RegisterGuestFormView, self).get_context_data(
**kwargs **kwargs
@ -124,15 +137,92 @@ class RegisterGuestFormView(generic.edit.FormView):
return context return context
def register_device_final(request, cafe_id, device_identifier, guest_identifier): def register_device_final(request, cafe, deviceentifier, guestentifier):
cafe = get_object_or_404(Cafe, pk=cafe_id) cafe = get_object_or_404(Cafe, pk=cafe)
device = get_object_or_404(Device, identifier=device_identifier) device = get_object_or_404(Device, identifier=deviceentifier)
guest = get_object_or_404(Guest, identifier=guest_identifier) guest = get_object_or_404(Guest, identifier=guestentifier)
print(cafe)
print(device) subject = render_to_string('repapp/mail/mail_register_device_subject.html', {
print(guest) '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( return render(
request, request,
"repapp/register_device_final.html", "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},
)