members: add option to request echo from members

v1-0-stable
Christian Merten 3 years ago
parent 91d69cbdef
commit 0a83174148
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

@ -31,9 +31,10 @@ urlpatterns += i18n_patterns(
re_path(r'^jet/', include('jet.urls', 'jet')), # Django JET URLS
re_path(r'^admin/', RedirectView.as_view(url='/kompass')),
re_path(r'^newsletter/', include('mailer.urls', namespace="mailer")),
re_path(r'^members/', include('members.urls', namespace="members")),
re_path(r'^LBAlpin/Programm(/)?(20)?[0-9]{0,2}', include('ludwigsburgalpin.urls',
namespace="ludwigsburgalpin")),
re_path(r'^$', include('startpage.urls')),
re_path(r'^$', include('startpage.urls', namespace="startpage")),
)
# TODO: django serving from MEDIA_URL should be disabled in production stage

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-01-12 17:38+0100\n"
"POT-Creation-Date: 2022-10-02 12:55+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

@ -1,47 +1,13 @@
{% extends "general/base.html" %}
{% load i18n %}
{% load static %}
<html>
<head>
<title>
Ludwigsburg Alpin Terminverwaltung
</title>
<link rel="shortcut icon" href="{% static "ludwigsburgalpin/img/favicon.ico" %}" type="image/x-icon">
<link rel="stylesheet" href="{% static "ludwigsburgalpin/base.css" %}">
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script>
$( function() {
$( ".datepicker" ).datepicker({
dateFormat: "dd.mm.yy"
});
} );
</script>
</head>
{% block title %}
Ludwigsburg Alpin Terminverwaltung
{% endblock %}
<body>
<div class="navbar-header">
<a class="navbar-brand" href="http://www.alpenverein-ludwigsburg.de">
<img class="navbar-logo" src="{% static "ludwigsburgalpin/img/logo_dav.png" %}">
</a>
<ul class="navbar">
<li><a href="/">Jugendgruppen</a></li>
<li class="current"><a href="{% url "ludwigsburgalpin:index" %}">Ludwigsburg Alpin</a></li>
<li><a href="/kompass">Kompass</a></li>
</ul>
</div>
<div id=content>
{% block content %}
{% endblock %}
</div>
</body>
</html>
{% block navbar %}
<li><a href="/">Jugendgruppen</a></li>
<li class="current"><a href="{% url "ludwigsburgalpin:index" %}">Ludwigsburg Alpin</a></li>
<li><a href="/kompass">Kompass</a></li>
{% endblock %}

@ -5,6 +5,7 @@ import os
NOT_SENT, SENT, PARTLY_SENT = 0, 1, 2
HOST = os.environ.get('DJANGO_ALLOWED_HOST', 'localhost:8000').split(",")[0]
HOST = "localhost:8008"
def send(subject, content, sender, recipients, message_id=None, reply_to=None,
@ -65,4 +66,9 @@ def get_unsubscribe_link(member):
return "https://{}/newsletter/unsubscribe?key={}".format(HOST, key)
def get_echo_link(member):
key = member.generate_echo_key()
return "https://{}/members/echo?key={}".format(HOST, key)
mail_root = os.environ.get('EMAIL_SENDING_ADDRESS', 'christian@localhost')

@ -5,6 +5,8 @@ import subprocess
import shutil
import time
import unicodedata
import random
import string
from django.http import HttpResponse, HttpResponseRedirect
from wsgiref.util import FileWrapper
@ -22,6 +24,7 @@ from django.shortcuts import render
from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, Klettertreff,
KlettertreffAttendee, ActivityCategory, OldMemberOnList, MemberList,
annotate_activity_score)
from mailer.mailutils import send as send_mail, get_echo_link, mail_root
from django.conf import settings
#from easy_select2 import apply_select2
@ -70,9 +73,9 @@ class MemberAdmin(admin.ModelAdmin):
fields = ['prename', 'lastname', 'email', 'email_parents', 'cc_email_parents', 'street', 'plz',
'town', 'phone_number', 'phone_number_parents', 'birth_date', 'group',
'gets_newsletter', 'registered', 'registration_form', 'active',
'not_waiting', 'comments']
'not_waiting', 'echoed', 'comments']
list_display = ('name', 'birth_date', 'age', 'get_group', 'gets_newsletter',
'registered', 'active', 'not_waiting', 'comments', 'activity_score')
'registered', 'active', 'not_waiting', 'echoed', 'comments', 'activity_score')
search_fields = ('prename', 'lastname')
list_filter = ('group', 'gets_newsletter', RegistrationFilter, 'active',
'not_waiting')
@ -82,7 +85,7 @@ class MemberAdmin(admin.ModelAdmin):
#}
change_form_template = "members/change_member.html"
#ordering = ('activity_score',)
actions = ['send_mail_to']
actions = ['send_mail_to', 'request_echo']
def get_queryset(self, request):
queryset = super().get_queryset(request)
@ -104,6 +107,30 @@ class MemberAdmin(admin.ModelAdmin):
return HttpResponseRedirect("/admin/mailer/message/add/?members={}".format(query))
send_mail_to.short_description = _('Compose new mail to selected members')
def request_echo(self, request, queryset):
for member in queryset:
send_mail("Wichtig: Rückmeldung erforderlich!",
"""Hallo {name},
um unsere Daten auf dem aktuellen Stand zu halten, brauchen wir eine
kurze Bestätigung von dir. Dafür besuche einfach diesen Link:
{link}
Dort kannst du deine Daten überprüfen und ändern. Falls du nicht innerhalb von
30 Tagen deine Daten bestätigst, wirst du aus unserer Datenbank gelöscht und
erhälst in Zukunft keine Mails mehr von uns.
Bei Fragen, wende dich gerne an jugendreferent@jdav-ludwigsburg.de.
Viele Grüße
Deine JDAV Ludwigsburg""".format(name=member.prename, link=get_echo_link(member)),
mail_root,
[member.email, member.email_parents] if member.email_parents and member.cc_email_parents
else member.email)
messages.success(request, _("Successfully requested echo from selected members."))
request_echo.short_description = _('Request echo from selected members')
def activity_score(self, obj):
score = obj._activity_score
# show 1 to 5 climbers based on activity in last year

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-10-01 10:19+0200\n"
"POT-Creation-Date: 2022-10-02 12:55+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,51 +18,59 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: members/admin.py:30 members/models.py:79
#: members/admin.py:33 members/models.py:79
msgid "Registration complete"
msgstr "Anmeldung vollständig"
#: members/admin.py:36
#: members/admin.py:39
msgid "True"
msgstr "Ja"
#: members/admin.py:37
#: members/admin.py:40
msgid "False"
msgstr "Nein"
#: members/admin.py:38
#: members/admin.py:41
msgid "All"
msgstr "Alle"
#: members/admin.py:105
#: members/admin.py:108
msgid "Compose new mail to selected members"
msgstr "Neue Nachricht an ausgewählte Teilnehmer verfassen"
#: members/admin.py:122
#: members/admin.py:131
msgid "Successfully requested echo from selected members."
msgstr "Rückmeldungsaufforderung erfolgreich an ausgewählte Teilnehmer verschickt."
#: members/admin.py:132
msgid "Request echo from selected members"
msgstr "Rückmeldungsaufforderung an ausgewählte Teilnehmer verschicken"
#: members/admin.py:149
msgid "activity"
msgstr "Aktivität"
#: members/admin.py:137
#: members/admin.py:164
msgid "Difficulty"
msgstr "Schwierigkeit"
#: members/admin.py:140 members/admin.py:143
#: members/admin.py:167 members/admin.py:170
msgid "Tour type"
msgstr "Art der Tour"
#: members/admin.py:441
#: members/admin.py:468
msgid "Convert to PDF"
msgstr "Kriseninterventionsliste erstellen"
#: members/admin.py:550
#: members/admin.py:577
msgid "Generate overview"
msgstr "Hinweise für Jugendleiter erstellen"
#: members/admin.py:647
#: members/admin.py:674
msgid "Generate list for LJP"
msgstr "LJP Liste erstellen"
#: members/apps.py:7 members/models.py:168
#: members/apps.py:7 members/models.py:181
msgid "members"
msgstr "Teilnehmer"
@ -74,7 +82,7 @@ msgstr "Name"
msgid "Description"
msgstr "Beschreibung"
#: members/models.py:32 members/models.py:192 members/models.py:271
#: members/models.py:32 members/models.py:205 members/models.py:284
#: members/templates/members/change_member.html:17
msgid "Activity"
msgstr "Aktivität"
@ -167,93 +175,97 @@ msgstr "NICHT Warteliste"
msgid "registration form"
msgstr "Anmeldeformular"
#: members/models.py:164 members/models.py:353
#: members/models.py:92
msgid "Echoed"
msgstr "Rückgemeldet"
#: members/models.py:177 members/models.py:366
msgid "Group"
msgstr "Gruppe"
#: members/models.py:167
#: members/models.py:180
msgid "member"
msgstr "Teilnehmer"
#: members/models.py:194 members/models.py:273
#: members/models.py:207 members/models.py:286
msgid "Place"
msgstr "Ort"
#: members/models.py:195 members/models.py:274
#: members/models.py:208 members/models.py:287
msgid "Destination (optional)"
msgstr "Ziel (optional)"
#: members/models.py:197 members/models.py:276 members/models.py:331
#: members/models.py:349
#: members/models.py:210 members/models.py:289 members/models.py:344
#: members/models.py:362
msgid "Date"
msgstr "Datum"
#: members/models.py:198 members/models.py:277
#: members/models.py:211 members/models.py:290
msgid "End (optional)"
msgstr "Ende"
#: members/models.py:200 members/models.py:279
#: members/models.py:213 members/models.py:292
msgid "Groups"
msgstr "Gruppen"
#: members/models.py:208 members/models.py:292
#: members/models.py:221 members/models.py:305
msgid "Categories"
msgstr "Kategorien"
#: members/models.py:209 members/models.py:293
#: members/models.py:222 members/models.py:306
msgid "easy"
msgstr "leicht"
#: members/models.py:209 members/models.py:293
#: members/models.py:222 members/models.py:306
msgid "medium"
msgstr "mittel"
#: members/models.py:209 members/models.py:293
#: members/models.py:222 members/models.py:306
msgid "hard"
msgstr "schwer"
#: members/models.py:218
#: members/models.py:231
msgid "Memberlist"
msgstr "Teilnehmerliste"
#: members/models.py:219
#: members/models.py:232
msgid "Memberlists"
msgstr "Teilnehmerlisten"
#: members/models.py:237 members/models.py:245 members/models.py:253
#: members/models.py:264 members/models.py:384 members/models.py:391
#: members/models.py:250 members/models.py:258 members/models.py:266
#: members/models.py:277 members/models.py:397 members/models.py:404
msgid "Member"
msgstr "Teilnehmer"
#: members/models.py:239 members/models.py:258
#: members/models.py:252 members/models.py:271
msgid "Comment"
msgstr "Kommentar"
#: members/models.py:246 members/models.py:265 members/models.py:392
#: members/models.py:259 members/models.py:278 members/models.py:405
msgid "Members"
msgstr "Teilnehmer"
#: members/models.py:330
#: members/models.py:343
msgid "Title"
msgstr "Titel"
#: members/models.py:350
#: members/models.py:363
msgid "Location"
msgstr "Ort"
#: members/models.py:351
#: members/models.py:364
msgid "Topic"
msgstr "Thema"
#: members/models.py:375
#: members/models.py:388
msgid "Jugendleiter"
msgstr "Jugendleiter"
#: members/models.py:378
#: members/models.py:391
msgid "Klettertreff"
msgstr "Klettertreff"
#: members/models.py:379
#: members/models.py:392
msgid "Klettertreffs"
msgstr "Klettertreffs"
@ -273,6 +285,58 @@ msgstr "Fähigkeiten:"
msgid "Skill level"
msgstr "Fähigkeitsniveau"
#: members/templates/members/echo.html:6 members/templates/members/echo.html:13
#: members/templates/members/echo_failed.html:11
#: members/templates/members/echo_success.html:10
msgid "Echo"
msgstr "Rückmeldung"
#: members/templates/members/echo.html:15
msgid "Thanks for echoing back. Here is your current data:"
msgstr ""
"Vielen Dank, dass du dich rückmeldest. Hier siehst du deine aktuellen Daten. "
"Falls sich etwas geändert hat, trage das bitte hier ein."
#: members/templates/members/echo.html:27
msgid "submit"
msgstr "Rückmelden"
#: members/templates/members/echo_failed.html:6
msgid "Echo failed"
msgstr "Rückmeldung fehlgeschlagen"
#: members/templates/members/echo_failed.html:13
msgid "Something went wrong. The key you supplied is"
msgstr "Etwas ist schief gegangen. Der verwendete Code ist"
#: members/templates/members/echo_failed.html:15
msgid "If you think this is a mistake, please"
msgstr "Wenn du denkst, dass das ein Fehler ist, "
#: members/templates/members/echo_failed.html:15
msgid "contact us."
msgstr "kontaktiere uns."
#: members/templates/members/echo_success.html:5
msgid "Echo successful"
msgstr "Rückmeldung erfolgreich"
#: members/templates/members/echo_success.html:12
msgid "Thank you"
msgstr "Danke"
#: members/templates/members/echo_success.html:12
msgid "Your data was successfully updated."
msgstr "Deine Daten wurden erfolgreich aktualisiert."
#: members/views.py:46 members/views.py:67
msgid "invalid"
msgstr "ungültig"
#: members/views.py:48
msgid "expired"
msgstr "abgelaufen"
#~ msgid "minimum age (years)"
#~ msgstr "Mindestalter (Jahre)"

@ -87,6 +87,9 @@ class Member(models.Model):
'image/jpeg',
'image/png',
'image/gif'])
echo_key = models.CharField(max_length=32, default="")
echo_expire = models.DateTimeField(default=timezone.now)
echoed = models.BooleanField(default=True, verbose_name=_('Echoed'))
def __str__(self):
"""String representation"""
@ -103,6 +106,13 @@ class Member(models.Model):
self.save()
return self.unsubscribe_key
def generate_echo_key(self):
self.echo_key = uuid.uuid4().hex
self.echo_expire = timezone.now() + timezone.timedelta(days=30)
self.echoed = False
self.save()
return self.echo_key
def unsubscribe(self, key):
if self.unsubscribe_key == key and timezone.now() <\
self.unsubscribe_expire:
@ -114,6 +124,9 @@ class Member(models.Model):
else:
return False
def may_echo(self, key):
return self.echo_key == key and timezone.now() < self.echo_expire
@property
def name(self):
"""Returning whole name (prename + lastname)"""

@ -0,0 +1,9 @@
{% extends "general/base.html" %}
{% load i18n %}
{% load static %}
{% block navbar %}
<li><a href="/">Jugendgruppen</a></li>
<li class="current"><a href="" %}">Mitglied</a></li>
{% endblock %}

@ -0,0 +1,30 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Echo" %}
{% endblock %}
{% block content %}
<link rel="stylesheet" href="{% static "ludwigsburgalpin/termine.css" static %}">
<h1>{% trans "Echo" %}</h1>
<p>{% trans "Thanks for echoing back. Here is your current data:" %}</p>
{% if error_message %}
<p><b>{{ error_message }}</b></p>
{% endif %}
<form action="" method="post">
<table class="termine">
{% csrf_token %}
{{form}}
</table>
<input type="hidden" name="key" value="{{ key }}">
<p><input type="submit" value="{% trans "submit" %}"/></p>
</form>
{% endblock %}

@ -0,0 +1,17 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Echo failed" %}
{% endblock %}
{% block content %}
<h1>{% trans "Echo" %}</h1>
<p><b>{% trans "Something went wrong. The key you supplied is" %} {{ reason }}.</b></p>
<p>{% trans "If you think this is a mistake, please" %} <a href="mailto:jugendreferent@jdav-ludwigsburg.de">{% trans "contact us." %}</a></p>
{% endblock %}

@ -0,0 +1,14 @@
{% extends "members/base.html" %}
{% load i18n %}
{% block title %}
{% trans "Echo successful" %}
{% endblock %}
{% block content %}
<h1>{% trans "Echo" %}</h1>
<p>{% trans "Thank you" %} {{name}}. {% trans "Your data was successfully updated." %}</p>
{% endblock %}

@ -0,0 +1,8 @@
from django.urls import re_path
from . import views
app_name = "mailer"
urlpatterns = [
re_path(r'^echo', views.echo , name='echo'),
]

@ -1,3 +1,67 @@
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from django.http import HttpResponseRedirect
from django.forms import ModelForm, TextInput, DateInput
from members.models import Member
from django.urls import reverse
from django.utils import timezone
# Create your views here.
class MemberForm(ModelForm):
class Meta:
model = Member
fields = ['prename', 'lastname', 'street', 'plz', 'town', 'phone_number',
'phone_number_parents', 'birth_date']
widgets = {
'birth_date': DateInput(format='%d.%m.%Y', attrs={'class': 'datepicker'})
}
def render_echo_failed(request, reason=""):
context = {}
if reason:
context['reason'] = reason
return render(request, 'members/echo_failed.html', context)
def render_echo(request, key, form):
return render(request, 'members/echo.html', {'form': form.as_table(),
'key' : key})
def render_echo_success(request, name):
return render(request, 'members/echo_success.html', {'name': name})
def echo(request):
if request.method == 'GET' and 'key' in request.GET:
try:
key = request.GET['key']
member = Member.objects.get(echo_key=key)
if not member.may_echo(key):
raise KeyError
form = MemberForm(instance=member)
return render_echo(request, key, form)
except Member.DoesNotExist:
return render_echo_failed(request, _("invalid"))
except KeyError:
return render_echo_failed(request, _("expired"))
elif request.method == 'POST':
try:
key = request.POST['key']
member = Member.objects.get(echo_key=key)
if not member.may_echo(key):
raise KeyError
form = MemberForm(request.POST, instance=member)
try:
form.save()
member.echo_key, member.echo_expire = "", timezone.now()
member.echoed = True
member.save()
return render_echo_success(request, member.prename)
except ValueError:
# when input is invalid
form = MemberForm(request.POST)
return render_echo(request, key, form)
except (Member.DoesNotExist, KeyError):
return render_echo_failed(request, _("invalid"))

@ -2,6 +2,7 @@ from django.urls import re_path
from . import views
app_name = "startpage"
urlpatterns = [
re_path(r'^$', views.index, name='index')
]

@ -0,0 +1,43 @@
{% load i18n %}
{% load static %}
<html>
<head>
<title>
{% block title %}
{% endblock %}
</title>
<link rel="shortcut icon" href="{% static "general/img/favicon.ico" %}" type="image/x-icon">
<link rel="stylesheet" href="{% static "general/base.css" %}">
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script>
$( function() {
$( ".datepicker" ).datepicker({
dateFormat: "dd.mm.yy"
});
} );
</script>
</head>
<body>
<div class="navbar-header">
<a class="navbar-brand" href="http://www.alpenverein-ludwigsburg.de">
<img class="navbar-logo" src="{% static "general/img/logo_dav.png" %}">
</a>
<ul class="navbar">
{% block navbar %}
{% endblock %}
</ul>
</div>
<div id=content>
{% block content %}
{% endblock %}
</div>
</body>
</html>
Loading…
Cancel
Save