accept, request or question new device

This commit is contained in:
Tom Irgang 2023-05-29 16:58:36 +02:00
parent 3aebeed113
commit e85cee2265
30 changed files with 755 additions and 53 deletions

View file

@ -53,10 +53,13 @@ INSTALLED_APPS = [
"crispy_forms", "crispy_forms",
"crispy_bootstrap5", "crispy_bootstrap5",
"import_export", "import_export",
"ckeditor",
"ckeditor_uploader",
] ]
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5" CRISPY_TEMPLATE_PACK = "bootstrap5"
CKEDITOR_UPLOAD_PATH = "uploads/"
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
@ -166,7 +169,7 @@ AUTHENTICATION_BACKENDS = [
"repapp.backends.OneTimeLoginBackend", "repapp.backends.OneTimeLoginBackend",
] ]
LOGIN_REDIRECT_URL = "/guest/profile/" LOGIN_REDIRECT_URL = "/guest/"
OIDC_RP_CLIENT_ID = "repapp" OIDC_RP_CLIENT_ID = "repapp"
OIDC_RP_CLIENT_SECRET = os.getenv("DJANGO_OIDC_RP_CLIENT_SECRET", None) OIDC_RP_CLIENT_SECRET = os.getenv("DJANGO_OIDC_RP_CLIENT_SECRET", None)

View file

@ -23,6 +23,8 @@ from django.conf.urls.static import static
urlpatterns = [ urlpatterns = [
path("", include("repapp.urls")), path("", include("repapp.urls")),
path('oidc/', include('mozilla_django_oidc.urls')), path('oidc/', include('mozilla_django_oidc.urls')),
path('accounts/', include('django.contrib.auth.urls')), # user management for guests not needed, use one-time-login only
# path('accounts/', include('django.contrib.auth.urls')),
path('ckeditor/', include('ckeditor_uploader.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -3,7 +3,8 @@ Django forms for RepApp.
""" """
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from crispy_forms.layout import Field from ckeditor.widgets import CKEditorWidget
from .models import Question, Device
EMPTY_VALUES = (None, '') EMPTY_VALUES = (None, '')
@ -16,7 +17,7 @@ class HoneypotWidget(forms.TextInput):
""" """
is_hidden = True is_hidden = True
def __init__(self, attrs=None, html_comment=False, *args, **kwargs): def __init__(self, *args, attrs=None, html_comment=False, **kwargs):
self.html_comment = html_comment self.html_comment = html_comment
super(HoneypotWidget, self).__init__(attrs, *args, **kwargs) super(HoneypotWidget, self).__init__(attrs, *args, **kwargs)
if 'class' not in self.attrs: if 'class' not in self.attrs:
@ -113,3 +114,18 @@ class RegisterGuest(forms.Form):
max_length=200 max_length=200
) )
accept_agb = HoneypotField(label="") accept_agb = HoneypotField(label="")
class Mail(forms.Form):
"""
Form for rejecting a device.
"""
message = forms.CharField(widget=CKEditorWidget(), label="")
class UpdateDeviceForm(forms.ModelForm):
class Meta:
model = Question
fields = ['question']
question = forms.CharField(widget=CKEditorWidget(
attrs={'style': 'width:100%;'}), label="Frage")

View file

@ -48,6 +48,7 @@ def process_message(message: MailMessage, guest: Guest):
if question: if question:
if question.device.guest.mail == message.from_: if question.device.guest.mail == message.from_:
question.answer = content question.answer = content
question.answered = True
question.save() question.save()
# TODO: save attachments and add to message # TODO: save attachments and add to message
logger.info( logger.info(

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-05-29 10:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0004_alter_onetimelogin_secret'),
]
operations = [
migrations.AddField(
model_name='device',
name='status',
field=models.IntegerField(default=0, verbose_name='Status'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-05-29 12:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0005_device_status'),
]
operations = [
migrations.AlterField(
model_name='question',
name='answer',
field=models.TextField(null=True, verbose_name='Antwort'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-05-29 12:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0006_alter_question_answer'),
]
operations = [
migrations.AddField(
model_name='question',
name='open',
field=models.BooleanField(default=True, verbose_name='offen'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-05-29 14:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0007_question_open'),
]
operations = [
migrations.AddField(
model_name='question',
name='sent',
field=models.BooleanField(default=False, verbose_name='gesendet'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-05-29 14:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0008_question_sent'),
]
operations = [
migrations.AddField(
model_name='question',
name='answered',
field=models.BooleanField(default=False, verbose_name='beantwortet'),
),
]

View file

@ -113,6 +113,7 @@ class Device(models.Model):
type_plate_picture = models.FileField( type_plate_picture = models.FileField(
upload_to=device_directory_path, null=True, verbose_name=_("Foto vom Typenschild")) upload_to=device_directory_path, null=True, verbose_name=_("Foto vom Typenschild"))
confirmed = models.BooleanField(verbose_name=_("Bestätigung gesendet?")) confirmed = models.BooleanField(verbose_name=_("Bestätigung gesendet?"))
status = models.IntegerField(verbose_name=_("Status"), default=0)
guest = models.ForeignKey( 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 = models.ForeignKey(
@ -123,7 +124,7 @@ class Device(models.Model):
verbose_name_plural = _('Geräte') verbose_name_plural = _('Geräte')
def __str__(self): def __str__(self):
return f'Gerät {self.device}' return f'Gerät {self.device} - {self.manufacturer}'
class Appointment(models.Model): class Appointment(models.Model):
@ -153,7 +154,7 @@ class Question(models.Model):
A Question is a request for information form a Organisator or a Reparateur for a Device. A Question is a request for information form a Organisator or a Reparateur for a Device.
""" """
question = models.TextField(verbose_name=_("Frage")) question = models.TextField(verbose_name=_("Frage"))
answer = models.TextField(verbose_name=_("Antwort")) answer = models.TextField(verbose_name=_("Antwort"), null=True)
date = models.DateField(verbose_name=_( date = models.DateField(verbose_name=_(
"Erstellungsdatum"), default=django.utils.timezone.now) "Erstellungsdatum"), default=django.utils.timezone.now)
organisator = models.ForeignKey( organisator = models.ForeignKey(
@ -162,6 +163,10 @@ class Question(models.Model):
Reparateur, on_delete=models.CASCADE, null=True, verbose_name=_("Reparateur")) Reparateur, on_delete=models.CASCADE, null=True, verbose_name=_("Reparateur"))
device = models.ForeignKey( device = models.ForeignKey(
Device, on_delete=models.CASCADE, verbose_name=_("Gerät")) Device, on_delete=models.CASCADE, verbose_name=_("Gerät"))
open = models.BooleanField(verbose_name=_("offen"), default=True)
sent = models.BooleanField(verbose_name=_("gesendet"), default=False)
answered = models.BooleanField(
verbose_name=_("beantwortet"), default=False)
class Meta: class Meta:
verbose_name = _('Frage') verbose_name = _('Frage')

View file

@ -15,6 +15,11 @@
{% endblock title %} {% endblock title %}
</title> </title>
<link rel="stylesheet" href="{% static 'repapp/css/bootstrap.min.css' %}" /> <link rel="stylesheet" href="{% static 'repapp/css/bootstrap.min.css' %}" />
<style>
.django-ckeditor-widget{
display: block !important
}
</style>
{% block head %} {% block head %}
{% endblock head %} {% endblock head %}
</head> </head>
@ -39,10 +44,6 @@
<li class="nav-item"> <li class="nav-item">
<a href="{% url 'logout' %}" class="nav-link">Abmelden</a> <a href="{% url 'logout' %}" class="nav-link">Abmelden</a>
</li> </li>
{% else %}
<li class="nav-item">
<a href="{% url 'login' %}" class="nav-link">Gast Login</a>
</li>
{% endif %} {% endif %}
<li class="nav-item"> <li class="nav-item">
<a href="{% url 'member' %}" class="nav-link">Mitglieder</a> <a href="{% url 'member' %}" class="nav-link">Mitglieder</a>

View file

@ -1,6 +0,0 @@
Guten Tag,
wir haben ein Benutzerkonto für Sie erstellt. Sie können sich im Web-Broswer unter {{ link }} um ihre Daten einzusehen und zu ändern.
Ihr Anmeldedaten sind:
Benutzername: {{ username }}
Passwort: {{ password }}
Herzliche Grüße sendet dir das Organisationsteam vom Repair-Café Hilpoltstein!

View file

@ -1,3 +0,0 @@
Guten Tag,
wir haben für Sie einen Einmal-Ammeldelink erstellt. Wenn sie {{ link }} im Web-Broswer öffnen können sie auf ihre Informationen zugreifen.
Herzliche Grüße sendet dir das Organisationsteam vom Repair-Café Hilpoltstein!

View file

@ -0,0 +1,4 @@
<p>Guten Tag,</p>
<p>Wir benötigen mehr Informationen zu ihrem Gerät {{ device.device }} - {{ device.manufacturer }}.</p>
<p>HIER FRAGE EINFÜGEN</p>
<p>Herzliche Grüße sendet dir das Organisationsteam vom Repair-Café Hilpoltstein!</p>

View file

@ -0,0 +1 @@
Repair-Café Hilpoltstein - Q#{{ question.pk }}: Rückfrage zu {{ device.device }}

View file

@ -1,8 +0,0 @@
Guten Tag,
dies ist eine automatische Nachricht, die du erhältst, nachdem du unser Anmeldeformular abgesendet hast und deine Angaben in unserer Datenbank gespeichert wurden.
Vielen Dank für die Anmeldung des Geräts {{ device.device }} - {{ device.manufacturer }} zur Reparatur im Repair-Café am {{ cafe.event_date|date }}.
Sie können die Daten zu ihrer Geräteanmeldung <a href="{{ login_url }}">hier</a> einsehen.
Die Organisation unserer Repair-Cafés beginnt i.a. einige Tage vor dem jeweiligen Termin. Dann erstellen wir anhand der angemeldeten Geräte und der Spezialgebiete der anwesenden Reparateure einen Zeitplan.
Sobald dieser Zeitplan fertig gestellt ist, erhältst du von einem Mitglied unseres Organisationsteams eine Mail. Diese beinhaltet im Normalfall den Reparaturtermin inkl. der Uhrzeit, für die die Reparatur vorgesehen ist. In Ausnahmefällen (z.B. wenn kein Spezialist für die Gerätegruppe anwesend sein wird oder überbucht ist) ist es aber auch möglich, dass wir deinen gewünschten Termin absagen und dir einen Alternativtermin vorschlagen müssen.
Falls du in der Zwischenzeit Rückfragen hast oder deinen Wunschtermin doch nicht wahrnehmen kannst, antworte bitte einfach auf diese Nachricht.
Herzliche Grüße sendet dir das Organisationsteam vom Repair-Café Hilpoltstein!

View file

@ -0,0 +1,6 @@
<p>Guten Tag,</p>
<p>Leider können wir das Gerät {{ device.device }} - {{ device.manufacturer }} nicht annehmen.</p>
<p>
In unserem Repair-Café gibt es leider keinen passenden Reparateur mit den notwenidgen Fähigkeiten um das Gerät zu reparieren.
</p>
<p>Herzliche Grüße sendet dir das Organisationsteam vom Repair-Café Hilpoltstein!</p>

View file

@ -0,0 +1 @@
Repair-Café Hilpoltstein - Geräteablehnung {{ device.device }}

View file

@ -8,7 +8,7 @@
{% block content %} {% block content %}
<p> <p>
<a href="{% url 'create_cafe' %}" class="btn btn-primary">Neues Repair-Cafés anlegen</a> <a href="{% url 'create_cafe' %}" class="btn btn-primary">Neues Repair-Cafés anlegen</a>
<a href="javascript:history.back()" class="btn btn-primary">Zurück</a> <a onclick="history.back()" class="btn btn-primary">Zurück</a>
</p> </p>
<table class="table"> <table class="table">
<thead> <thead>

View file

@ -0,0 +1,23 @@
{% extends "repapp/base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% block title %}
- Rückfrage zum Gerät
{% endblock title %}
{% block page_title %}
Rückfrage zum Gerät
{% endblock page_title %}
{% block content %}
<p>
<a onclick="history.back()" class="btn btn-primary">Zurück</a>
</p>
<hr class="mt-0 mb-4">
<h1 class="mt-2">Rückfrage zum Gerät</h1>
<form action="{% url 'device_question' device.pk %}" method="post">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-primary submit_button">Senden</button>
</form>
<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
<script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
{% endblock content %}

View file

@ -0,0 +1,28 @@
{% extends "repapp/base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% block title %}
- Rückfrage zum Gerät
{% endblock title %}
{% block page_title %}
Rückfrage zum Gerät
{% endblock page_title %}
{% block content %}
<p>
<a onclick="history.back()" class="btn btn-primary">Zurück</a>
</p>
<hr class="mt-0 mb-4">
<h1 class="mt-2">Rückfrage zum Gerät</h1>
<h2>{{ question.device.device }} - {{ question.device.manufacturer }} vom {{ question.date|date }}</h2>
{% if show_guest %}<p>Gast: {{ question.device.guest.name }} ({{ question.device.guest.mail }})</p>{% endif %}
{% if question.device.cafe %}<p>Repair-Café: {{ question.device.cafe.event_date|date }}</p>{% endif %}
{% if question.organisator %}<p>Organisator: {{ question.organisator.name }}</p>{% endif %}
{% if question.reparateur %}<p>Reparateur: {{ question.reparateur.name }}</p>{% endif %}
<form action="{% url 'edit_device_question' question.pk %}" method="post">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-primary submit_button">Als eMail senden</button>
</form>
<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
<script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
{% endblock content %}

View file

@ -7,8 +7,7 @@
{% endblock page_title %} {% endblock page_title %}
{% block content %} {% block content %}
<p> <p>
<a href="{% url 'create_guest' %}" class="btn btn-primary">Neuen Gast anlegen</a> <a onclick="history.back()" class="btn btn-primary">Zurück</a>
<a href="javascript:history.back()" class="btn btn-primary">Zurück</a>
</p> </p>
<table class="table"> <table class="table">
<thead> <thead>
@ -19,7 +18,6 @@
<th scope="col">Wohnort</th> <th scope="col">Wohnort</th>
<th scope="col">eMail Adresse</th> <th scope="col">eMail Adresse</th>
<th scope="col">Bearbeiten</th> <th scope="col">Bearbeiten</th>
<th scope="col">Löschen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View file

@ -17,10 +17,119 @@
// TODO: Button nächstes Repair-Café // TODO: Button nächstes Repair-Café
<hr class="mt-0 mb-4"> <hr class="mt-0 mb-4">
<h1 class="mt-2">Geräte</h1> <h1 class="mt-2">Geräte</h1>
// TODO: Liste mit neuen Geräten <table class="table">
<thead>
<tr>
<th scope="col">Art des Geräts</th>
<th scope="col">Hersteller & Modell/Typ</th>
<th scope="col">Name des Gastes</th>
<th scope="col">eMail des Gastes</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for device in devices %}
<tr>
<td>{{ device.device }}</td>
<td>{{ device.manufacturer }}</td>
<td>{{ device.guest.name }}</td>
<td>{{ device.guest.mail }}</td>
<td>
<a href="{% url 'review_device' device.pk %}" class="btn btn-primary">überpüfen</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4">Es gibt keine neuen Geräte.</td>
</tr>
{% endfor %}
</tbody>
</table>
<hr class="mt-0 mb-4"> <hr class="mt-0 mb-4">
<h1 class="mt-2">Rückfragen</h1> <h1 class="mt-2">Rückfragen</h1>
// TODO: Liste mit Antworten auf Rückfragen <table class="table">
<thead>
<tr>
<th scope="col">Geräts</th>
<th scope="col">Hersteller</th>
<th scope="col">Gast</th>
<th scope="col">Organisator</th>
<th scope="col">Reparateur</th>
<th scope="col">gesendet</th>
<th scope="col">beantwortet</th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for question in questions_not_sent %}
<tr>
<td>{{ question.device.device }}</td>
<td>{{ question.device.manufacturer }}</td>
<td>{{ question.device.guest.name }}</td>
<td>{{ question.organisator.mail }}</td>
<td>{{ question.reparateur.mail }}</td>
<td>
{% if question.sent %}
Ja
{% else %}
Nein
{% endif %}
</td>
<td>
{% if question.answered %}
Ja
{% else %}
Nein
{% endif %}
</td>
<td>
<a href="{% url 'view_device_question' question.pk %}"
class="btn btn-primary">ansehen</a>
</td>
<td>
{% if not question.sent %}
<a href="{% url 'edit_device_question' question.pk %}"
class="btn btn-primary">senden</a>
{% endif %}
</td>
</tr>
{% endfor %}
{% for question in questions_open_and_answered %}
<tr>
<td>{{ question.device.device }}</td>
<td>{{ question.device.manufacturer }}</td>
<td>{{ question.device.guest.name }}</td>
<td>{{ question.organisator.mail }}</td>
<td>{{ question.reparateur.mail }}</td>
<td>
{% if question.sent %}
Ja
{% else %}
Nein
{% endif %}
</td>
<td>
{% if question.answered %}
Ja
{% else %}
Nein
{% endif %}
</td>
<td>
<a href="{% url 'view_device_question' question.pk %}"
class="btn btn-primary">ansehen</a>
</td>
<td>
{% if not question.sent %}
<a href="{% url 'edit_device_question' question.pk %}"
class="btn btn-primary">senden</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<hr class="mt-0 mb-4"> <hr class="mt-0 mb-4">
<h1 class="mt-2">Nachrichten</h1> <h1 class="mt-2">Nachrichten</h1>
// TODO: Liste mit neuen Nachrichten // TODO: Liste mit neuen Nachrichten

View file

@ -0,0 +1,41 @@
{% extends "repapp/base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% block title %}
- Rückfrage zum Gerät
{% endblock title %}
{% block page_title %}
Rückfrage zum Gerät
{% endblock page_title %}
{% block content %}
<p>
<a onclick="history.back()" class="btn btn-primary">Zurück</a>
</p>
<hr class="mt-0 mb-4">
<h1 class="mt-2">Rückfrage zum Gerät</h1>
<h2>{{ question.device.device }} - {{ question.device.manufacturer }} vom {{ question.date|date }}</h2>
{% if show_guest %}<p>Gast: {{ question.device.guest.name }} ({{ question.device.guest.mail }})</p>{% endif %}
{% if question.device.cafe %}<p>Repair-Café: {{ question.device.cafe.event_date|date }}</p>{% endif %}
{% if question.organisator %}<p>Organisator: {{ question.organisator.name }}</p>{% endif %}
{% if question.reparateur %}<p>Reparateur: {{ question.reparateur.name }}</p>{% endif %}
<p>
Offen:
{% if question.open %}
Ja
{% else %}
Nein
{% endif %}
</p>
<hr class="mt-0 mb-4">
<h3>Frage</h3>
<div>{{ question.question|safe }}</div>
<hr class="mt-0 mb-4">
<h3>Antwort</h3>
{% if question.answer %}
<div>
<pre>{{ question.answer|safe }}</pre>
</div>
{% else %}
Der Gast hat noch nicht geantwortet.
{% endif %}
{% endblock content %}

View file

@ -0,0 +1,72 @@
{% extends "repapp/base.html" %}
{% load crispy_forms_tags %}
{% block title %}
- Gerät überprüfen
{% endblock title %}
{% block page_title %}
Gerät überprüfen
{% endblock page_title %}
{% block content %}
<p>
<a onclick="history.back()" class="btn btn-primary">Zurück</a>
<a href="{% url 'review_device_accept' device.pk %}"
class="btn btn-primary">Annehmen</a>
<a href="{% url 'review_device_reject' device.pk %}"
class="btn btn-primary">Ablehnen</a>
<a href="{% url 'device_question' device.pk %}" class="btn btn-primary">Rückfrage</a>
</p>
<hr class="mt-0 mb-4">
<h1 class="mt-2">Gerät</h1>
<table class="table">
<tbody>
<tr>
<th scope="row">Art des Geräts</th>
<td>{{ device.device }}</td>
</tr>
<tr>
<th scope="row">Hersteller & Modell/Typ</th>
<td>{{ device.manufacturer }}</td>
</tr>
<tr>
<th scope="row">Fehlerbeschreibung</th>
<td>{{ device.error }}</td>
</tr>
<tr>
<th scope="row">Folgetermin</th>
<td>
{% if device.follow_up %}
Ja
{% else %}
Nein
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Foto vom Gerät</th>
<td>
{% if device.device_picture %}
<img alt="Foto vom Gerät"
height="100%"
width="50%"
src="/media/{{ device.device_picture }}" />
{% else %}
kein Bild
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Foto vom Typenschild</th>
<td>
{% if device.device_picture %}
<img alt="Foto vom Typenschild"
height="100%"
width="50%"
src="/media/{{ device.type_plate_picture }}" />
{% else %}
kein Bild
{% endif %}
</td>
</tr>
</tbody>
</table>
{% endblock content %}

View file

@ -0,0 +1,23 @@
{% extends "repapp/base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% block title %}
- Gerät ablehnen
{% endblock title %}
{% block page_title %}
Gerät ablehnen
{% endblock page_title %}
{% block content %}
<p>
<a onclick="history.back()" class="btn btn-primary">Zurück</a>
</p>
<hr class="mt-0 mb-4">
<h1 class="mt-2">Gerät ablehnen</h1>
<form action="{% url 'review_device_reject' device.pk %}" method="post">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-primary submit_button">Senden</button>
</form>
<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
<script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
{% endblock content %}

View file

@ -25,8 +25,20 @@ urlpatterns = [
path("onetimelogin/<str:secret>/", path("onetimelogin/<str:secret>/",
views.one_time_login, name="one_time_login"), views.one_time_login, name="one_time_login"),
path("member/", views.member, name="member"), path("member/", views.member, name="member"),
path("logout/", views.user_logout, name="logout"),
path("orga/select/", views.select_role, name="select_role"), path("orga/select/", views.select_role, name="select_role"),
path("orga/", views.orga, name="orga"), path("orga/", views.orga, name="orga"),
path("orga/review/<int:device>/", views.review_device, name="review_device"),
path("orga/review/<int:device>/accept/",
views.review_device_accept, name="review_device_accept"),
path("orga/review/<int:device>/reject/",
views.RejectDeviceFormView.as_view(), name="review_device_reject"),
path("member/device/<int:device>/question/",
views.QuestionDeviceFormView.as_view(), name="device_question"),
path("member/device/<int:pk>/question/edit",
views.QuestionUpdateView.as_view(), name="edit_device_question"),
path("member/question/<int:question>/",
views.question, name="view_device_question"),
path("repa/", views.repa, name="repa"), path("repa/", views.repa, name="repa"),
path("cafe/", views.CafeView.as_view(), name="cafe"), path("cafe/", views.CafeView.as_view(), name="cafe"),
path("cafe/create", views.CafeCreateView.as_view(), name="create_cafe"), path("cafe/create", views.CafeCreateView.as_view(), name="create_cafe"),
@ -38,5 +50,4 @@ urlpatterns = [
path("cron", views.cron, name="cron"), path("cron", views.cron, name="cron"),
path("bootstrap", views.bootstrap, name="bootstrap"), path("bootstrap", views.bootstrap, name="bootstrap"),
path("process_mails", views.process_mails, name="process_mails"), path("process_mails", views.process_mails, name="process_mails"),
] ]

View file

@ -8,6 +8,7 @@ import time
import os import os
import logging import logging
from hashlib import sha256 from hashlib import sha256
from bs4 import BeautifulSoup
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from django.core.mail import send_mail from django.core.mail import send_mail
@ -19,10 +20,10 @@ from django.http import HttpResponseRedirect, HttpResponse
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login, logout
from django.utils.timezone import now from django.utils.timezone import now
from .forms import RegisterDevice, RegisterGuest from .forms import RegisterDevice, RegisterGuest, Mail, UpdateDeviceForm
from .models import (Cafe, Device, Guest, OneTimeLogin, from .models import (Cafe, Device, Guest, OneTimeLogin, Question,
CustomUser, Organisator, Reparateur) CustomUser, Organisator, Reparateur)
from . import mail_interface from . import mail_interface
@ -38,12 +39,11 @@ def send_one_time_login_mail(secret, mail, request):
f'/onetimelogin/{secret}/') f'/onetimelogin/{secret}/')
subject = render_to_string( subject = render_to_string(
'repapp/mail/mail_one_time_login_subject.html').replace('\n', '') 'repapp/mail/mail_one_time_login_subject.html').replace('\n', '')
text = render_to_string('repapp/mail/mail_one_time_login_text.html', {
'link': url,
})
html = render_to_string('repapp/mail/mail_one_time_login_html.html', { html = render_to_string('repapp/mail/mail_one_time_login_html.html', {
'link': url, 'link': url,
}) })
soup = BeautifulSoup(html)
text = soup.get_text('\n')
send_ok = send_mail( send_ok = send_mail(
subject=subject, subject=subject,
@ -90,18 +90,14 @@ def send_confirmation_mails(device, guest, cafe, request):
subject = render_to_string('repapp/mail/mail_register_device_subject.html', { subject = render_to_string('repapp/mail/mail_register_device_subject.html', {
'cafe': cafe, 'cafe': cafe,
}).replace('\n', '') }).replace('\n', '')
text = render_to_string('repapp/mail/mail_register_device_text.html', {
'guest': guest,
'device': device,
'cafe': cafe,
'login_url': login_url,
})
html = render_to_string('repapp/mail/mail_register_device_html.html', { html = render_to_string('repapp/mail/mail_register_device_html.html', {
'guest': guest, 'guest': guest,
'device': device, 'device': device,
'cafe': cafe, 'cafe': cafe,
'login_url': login_url, 'login_url': login_url,
}) })
soup = BeautifulSoup(html)
text = soup.get_text('\n')
mail_count = send_mail( mail_count = send_mail(
subject=subject, subject=subject,
@ -127,16 +123,13 @@ def send_guest_account_mail(guest, password, request):
url = request.build_absolute_uri('/guest/profile/') url = request.build_absolute_uri('/guest/profile/')
subject = render_to_string( subject = render_to_string(
'repapp/mail/mail_new_guest_subject.html').replace('\n', '') 'repapp/mail/mail_new_guest_subject.html').replace('\n', '')
text = render_to_string('repapp/mail/mail_new_guest_text.html', {
'link': url,
'username': guest.user.email,
'password': password,
})
html = render_to_string('repapp/mail/mail_new_guest_html.html', { html = render_to_string('repapp/mail/mail_new_guest_html.html', {
'link': url, 'link': url,
'username': guest.user.email, 'username': guest.user.email,
'password': password, 'password': password,
}) })
soup = BeautifulSoup(html)
text = soup.get_text('\n')
send_ok = send_mail( send_ok = send_mail(
subject=subject, subject=subject,
@ -152,6 +145,62 @@ def send_guest_account_mail(guest, password, request):
'Sie haben ihre Benutzerdaten per eMail erhalten.') 'Sie haben ihre Benutzerdaten per eMail erhalten.')
def send_device_reject_mail(device, reply, request):
"""
Send a mail to reject the given device.
"""
subject = render_to_string(
'repapp/mail/mail_reject_device_subject.html', {
'device': device
}).replace('\n', '')
soup = BeautifulSoup(reply)
text = soup.get_text('\n')
send_ok = send_mail(
subject=subject,
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=[device.guest.mail],
fail_silently=True,
html_message=reply
)
if send_ok > 0:
messages.add_message(request, messages.INFO,
f'Das Gerät {device.device} wurde abgelehnt.')
def send_device_question_mail(question, request):
"""
Send a mail to ask more details about an device.
"""
subject = render_to_string(
'repapp/mail/mail_question_device_subject.html', {
'device': question.device,
'question': question,
}).replace('\n', '')
soup = BeautifulSoup(question.question)
text = soup.get_text('\n')
send_ok = send_mail(
subject=subject,
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=[question.device.guest.mail],
fail_silently=True,
html_message=question.question
)
if send_ok > 0:
question.sent = True
question.save()
messages.add_message(request, messages.INFO,
f'Die Rückfrage zum Gerät {question.device.device} wurde gesendet.')
def is_member(user): def is_member(user):
""" """
Test is a user is a Repair-Café member. Test is a user is a Repair-Café member.
@ -325,7 +374,8 @@ class RegisterGuestFormView(generic.edit.FormView):
guest.user = user guest.user = user
guest.save() guest.save()
send_guest_account_mail(guest, password, self.request) # Simplify flow for guests, hide account and use only one-time-login
# send_guest_account_mail(guest, password, self.request)
send_confirmation_mails(device, guest, cafe, self.request) send_confirmation_mails(device, guest, cafe, self.request)
@ -450,9 +500,231 @@ def orga(request):
logger.warning('The user %s is no organisator!', str(request.user)) logger.warning('The user %s is no organisator!', str(request.user))
raise PermissionDenied('Not organisator!') raise PermissionDenied('Not organisator!')
devices = Device.objects.filter(status=0).order_by('-date')
questions_not_sent = Question.objects.filter(sent=False).order_by('-date')
questions_open_and_answered = Question.objects.filter(
open=True, answered=True).order_by('-date')
return render( return render(
request, request,
"repapp/orga/main.html" "repapp/orga/main.html",
{
'devices': devices,
'questions_not_sent': questions_not_sent,
'questions_open_and_answered': questions_open_and_answered,
}
)
@login_required(login_url=reverse_lazy('member'))
def review_device(request, device):
"""
Review new device registration.
"""
# TODO: test
if not is_organisator(request.user):
logger.warning('The user %s is no organisator!', str(request.user))
raise PermissionDenied('Not organisator!')
device = get_object_or_404(Device, pk=device)
return render(
request,
"repapp/orga/review_device.html",
{
'device': device
}
)
@login_required(login_url=reverse_lazy('member'))
def review_device_accept(request, device):
"""
Review new device registration.
"""
# TODO: test
if not is_organisator(request.user):
logger.warning('The user %s is no organisator!', str(request.user))
raise PermissionDenied('Not organisator!')
device = get_object_or_404(Device, pk=device)
device.status = 3
device.save()
return HttpResponseRedirect(reverse_lazy('orga'))
class RejectDeviceFormView(LoginRequiredMixin, UserPassesTestMixin, generic.edit.FormView):
"""
View for rejecting a device.
"""
template_name = "repapp/orga/review_device_reject.html"
form_class = Mail
login_url = reverse_lazy('member')
def test_func(self):
return is_organisator(self.request.user)
def form_valid(self, form):
device = self.kwargs['device']
device = get_object_or_404(Device, pk=device)
message = form.cleaned_data['message']
send_device_reject_mail(device, message, self.request)
device.status = -1
device.save()
return HttpResponseRedirect(
reverse_lazy('orga')
)
def get_initial(self, **kwargs):
device = self.kwargs['device']
device = get_object_or_404(Device, pk=device)
text = render_to_string('repapp/mail/mail_reject_device_html.html', {
'device': device,
})
initial = super(RejectDeviceFormView, self).get_initial()
initial['message'] = text
return initial
def get_context_data(self, **kwargs):
device = self.kwargs['device']
device = get_object_or_404(Device, pk=device)
context = super(RejectDeviceFormView, self).get_context_data(
**kwargs
)
context["device"] = device
return context
class QuestionDeviceFormView(LoginRequiredMixin, UserPassesTestMixin, generic.edit.FormView):
"""
View for rejecting a device.
"""
template_name = "repapp/orga/device_question.html"
form_class = Mail
login_url = reverse_lazy('member')
def test_func(self):
return is_organisator(self.request.user)
def form_valid(self, form):
device = self.kwargs['device']
device = get_object_or_404(Device, pk=device)
question = None
message = form.cleaned_data['message']
mail = self.request.user.email
organisator = Organisator.objects.filter(mail=mail).first()
if organisator:
question = Question(
question=message,
organisator=organisator,
device=device
)
else:
raise PermissionDenied('Not valid member found!')
question.save()
send_device_question_mail(question, self.request)
device.status = 1
device.save()
return HttpResponseRedirect(
reverse_lazy('orga')
)
def get_initial(self, **kwargs):
device = self.kwargs['device']
device = get_object_or_404(Device, pk=device)
text = render_to_string('repapp/mail/mail_question_device_html.html', {
'device': device,
})
initial = super(QuestionDeviceFormView, self).get_initial()
initial['message'] = text
return initial
def get_context_data(self, **kwargs):
device = self.kwargs['device']
device = get_object_or_404(Device, pk=device)
context = super(QuestionDeviceFormView, self).get_context_data(
**kwargs
)
context["device"] = device
return context
class QuestionUpdateView(LoginRequiredMixin, UserPassesTestMixin, generic.edit.UpdateView):
"""
Organisator view for edit a Repair-Café.
"""
# TODO: create test
login_url = reverse_lazy('member')
template_name = "repapp/orga/edit_question.html"
model = Question
form_class = UpdateDeviceForm
success_url = reverse_lazy('orga')
def test_func(self):
return is_organisator(self.request.user)
def form_valid(self, form):
response = super(QuestionUpdateView, self).form_valid(form)
question = self.kwargs['pk']
question = get_object_or_404(Question, pk=question)
send_device_question_mail(question, self.request)
return response
def get_context_data(self, **kwargs):
question = self.kwargs['pk']
question = get_object_or_404(Question, pk=question)
context = super(QuestionUpdateView, self).get_context_data(
**kwargs
)
context["question"] = question
context["show_guest"] = True
return context
@login_required(login_url=reverse_lazy('member'))
def question(request, question):
"""
Review new device registration.
"""
# TODO: test
if not is_member(request.user):
raise PermissionDenied('Not member!')
question = get_object_or_404(Question, pk=question)
show_guest = False
if is_organisator(request.user):
show_guest = True
return render(
request,
"repapp/orga/question.html",
{
'question': question,
'show_guest': show_guest
}
) )
@ -633,3 +905,11 @@ class GuestUpdateView(LoginRequiredMixin, UserPassesTestMixin, generic.edit.Upda
def test_func(self): def test_func(self):
return is_organisator(self.request.user) return is_organisator(self.request.user)
def user_logout(request):
"""
Log current user out.
"""
logout(request)
return HttpResponseRedirect(reverse_lazy('index'))

View file

@ -12,3 +12,5 @@ djlint==1.23.3
django-import-export==3.2.0 django-import-export==3.2.0
selenium==4.9.0 selenium==4.9.0
imap-tools==1.0.0 imap-tools==1.0.0
django-ckeditor==6.5.1
beautifulsoup4==4.12.2

View file

@ -6,3 +6,5 @@ crispy-bootstrap5==0.7
mozilla-django-oidc==3.0.0 mozilla-django-oidc==3.0.0
django-import-export==3.2.0 django-import-export==3.2.0
imap-tools==1.0.0 imap-tools==1.0.0
django-ckeditor==6.5.1
beautifulsoup4==4.12.2