diff --git a/jdav_web/jdav_web/urls.py b/jdav_web/jdav_web/urls.py index c57033d..d17ed73 100644 --- a/jdav_web/jdav_web/urls.py +++ b/jdav_web/jdav_web/urls.py @@ -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 diff --git a/jdav_web/locale/de/LC_MESSAGES/django.po b/jdav_web/locale/de/LC_MESSAGES/django.po index d45a19b..f6f5eee 100644 --- a/jdav_web/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/locale/de/LC_MESSAGES/django.po @@ -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 \n" "Language-Team: LANGUAGE \n" diff --git a/jdav_web/ludwigsburgalpin/templates/ludwigsburgalpin/base.html b/jdav_web/ludwigsburgalpin/templates/ludwigsburgalpin/base.html index 37238a7..104138b 100644 --- a/jdav_web/ludwigsburgalpin/templates/ludwigsburgalpin/base.html +++ b/jdav_web/ludwigsburgalpin/templates/ludwigsburgalpin/base.html @@ -1,47 +1,13 @@ +{% extends "general/base.html" %} {% load i18n %} {% load static %} - - - - Ludwigsburg Alpin Terminverwaltung - - - - - - - - +{% block title %} +Ludwigsburg Alpin Terminverwaltung +{% endblock %} - - - - -
- - {% block content %} - {% endblock %} - -
- - - - +{% block navbar %} +
  • Jugendgruppen
  • +
  • Ludwigsburg Alpin
  • +
  • Kompass
  • +{% endblock %} diff --git a/jdav_web/mailer/mailutils.py b/jdav_web/mailer/mailutils.py index 46f317d..2d1a278 100644 --- a/jdav_web/mailer/mailutils.py +++ b/jdav_web/mailer/mailutils.py @@ -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') diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index e676344..0534006 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -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 diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po index 1ea72d0..88168f8 100644 --- a/jdav_web/members/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/members/locale/de/LC_MESSAGES/django.po @@ -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 \n" "Language-Team: LANGUAGE \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)" diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index 07a6c67..54099d7 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -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)""" diff --git a/jdav_web/members/templates/members/base.html b/jdav_web/members/templates/members/base.html new file mode 100644 index 0000000..4199dcf --- /dev/null +++ b/jdav_web/members/templates/members/base.html @@ -0,0 +1,9 @@ +{% extends "general/base.html" %} + +{% load i18n %} +{% load static %} + +{% block navbar %} +
  • Jugendgruppen
  • +
  • Mitglied
  • +{% endblock %} diff --git a/jdav_web/members/templates/members/echo.html b/jdav_web/members/templates/members/echo.html new file mode 100644 index 0000000..e968d51 --- /dev/null +++ b/jdav_web/members/templates/members/echo.html @@ -0,0 +1,30 @@ +{% extends "members/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %} +{% trans "Echo" %} +{% endblock %} + +{% block content %} + + + +

    {% trans "Echo" %}

    + +

    {% trans "Thanks for echoing back. Here is your current data:" %}

    + +{% if error_message %} +

    {{ error_message }}

    +{% endif %} + +
    + + {% csrf_token %} + {{form}} +
    + +

    +
    + +{% endblock %} diff --git a/jdav_web/members/templates/members/echo_failed.html b/jdav_web/members/templates/members/echo_failed.html new file mode 100644 index 0000000..8e7ee67 --- /dev/null +++ b/jdav_web/members/templates/members/echo_failed.html @@ -0,0 +1,17 @@ +{% extends "members/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %} +{% trans "Echo failed" %} +{% endblock %} + +{% block content %} + +

    {% trans "Echo" %}

    + +

    {% trans "Something went wrong. The key you supplied is" %} {{ reason }}.

    + +

    {% trans "If you think this is a mistake, please" %} {% trans "contact us." %}

    + +{% endblock %} diff --git a/jdav_web/members/templates/members/echo_success.html b/jdav_web/members/templates/members/echo_success.html new file mode 100644 index 0000000..b1ae8f6 --- /dev/null +++ b/jdav_web/members/templates/members/echo_success.html @@ -0,0 +1,14 @@ +{% extends "members/base.html" %} +{% load i18n %} + +{% block title %} +{% trans "Echo successful" %} +{% endblock %} + +{% block content %} + +

    {% trans "Echo" %}

    + +

    {% trans "Thank you" %} {{name}}. {% trans "Your data was successfully updated." %}

    + +{% endblock %} diff --git a/jdav_web/members/urls.py b/jdav_web/members/urls.py new file mode 100644 index 0000000..5a4e10a --- /dev/null +++ b/jdav_web/members/urls.py @@ -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'), +] diff --git a/jdav_web/members/views.py b/jdav_web/members/views.py index 91ea44a..eb30521 100644 --- a/jdav_web/members/views.py +++ b/jdav_web/members/views.py @@ -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")) diff --git a/jdav_web/startpage/urls.py b/jdav_web/startpage/urls.py index 29516d1..2d313ca 100644 --- a/jdav_web/startpage/urls.py +++ b/jdav_web/startpage/urls.py @@ -2,6 +2,7 @@ from django.urls import re_path from . import views +app_name = "startpage" urlpatterns = [ re_path(r'^$', views.index, name='index') ] diff --git a/jdav_web/ludwigsburgalpin/static/ludwigsburgalpin/base.css b/jdav_web/static/general/base.css similarity index 100% rename from jdav_web/ludwigsburgalpin/static/ludwigsburgalpin/base.css rename to jdav_web/static/general/base.css diff --git a/jdav_web/ludwigsburgalpin/static/ludwigsburgalpin/img/favicon.ico b/jdav_web/static/general/img/favicon.ico similarity index 100% rename from jdav_web/ludwigsburgalpin/static/ludwigsburgalpin/img/favicon.ico rename to jdav_web/static/general/img/favicon.ico diff --git a/jdav_web/ludwigsburgalpin/static/ludwigsburgalpin/img/logo_dav.png b/jdav_web/static/general/img/logo_dav.png similarity index 100% rename from jdav_web/ludwigsburgalpin/static/ludwigsburgalpin/img/logo_dav.png rename to jdav_web/static/general/img/logo_dav.png diff --git a/jdav_web/templates/general/base.html b/jdav_web/templates/general/base.html new file mode 100644 index 0000000..e60545d --- /dev/null +++ b/jdav_web/templates/general/base.html @@ -0,0 +1,43 @@ +{% load i18n %} +{% load static %} + + + + + {% block title %} + {% endblock %} + + + + + + + + + + + + + +
    + {% block content %} + {% endblock %} +
    + + + +