diff --git a/self_registration/main/management/__init__.py b/self_registration/main/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/self_registration/main/management/commands/__init__.py b/self_registration/main/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/self_registration/main/management/commands/delete_expired_subjects.py b/self_registration/main/management/commands/delete_expired_subjects.py new file mode 100644 index 0000000000000000000000000000000000000000..f733deb62f2b6f071f59434a1462547b2cd91533 --- /dev/null +++ b/self_registration/main/management/commands/delete_expired_subjects.py @@ -0,0 +1,45 @@ +# (c) 2020 MPIB , +# +# This file is part of castellum-self-registration. +# +# castellum-self-registration is free software; you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# Castellum is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with Castellum. If not, see +# . + +from django.conf import settings +from django.core.management.base import BaseCommand +from django.utils import timezone + +from self_registration.main.models import SelfRegisteredSubject + + +def delete_expired_subjects(): + return SelfRegisteredSubject.objects.filter( + confirmed=False, + created_at__lte=timezone.now()-settings.SUBJECT_EXPIRATION_PERIOD + ).delete() + + +class Command(BaseCommand): + help = 'Delete expired self-registered subjects.' + + def handle(self, *args, **options): + count, _ = delete_expired_subjects() + if options['verbosity'] > 0: + self.stdout.write( + "{count} self-registered subjects were deleted, as they had not been confirmed " + "within {period} days.".format( + count=count, + period=settings.SUBJECT_EXPIRATION_PERIOD.days + ) + ) diff --git a/self_registration/main/migrations/0002_email_confirmation.py b/self_registration/main/migrations/0002_email_confirmation.py new file mode 100644 index 0000000000000000000000000000000000000000..cd858c56c10287ca0fa97d68f21f0fcdc5764b7a --- /dev/null +++ b/self_registration/main/migrations/0002_email_confirmation.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.7 on 2021-09-13 09:43 + +from django.db import migrations, models +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='selfregisteredsubject', + name='confirmed', + field=models.BooleanField(default=False, verbose_name='Confirmed'), + ), + migrations.AddField( + model_name='selfregisteredsubject', + name='created_at', + field=models.DateField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created at'), + preserve_default=False, + ), + migrations.AddField( + model_name='selfregisteredsubject', + name='verification_token', + field=models.UUIDField(default=uuid.uuid4, verbose_name='Token'), + ), + ] diff --git a/self_registration/main/models.py b/self_registration/main/models.py index 882797f4279e2df8aa811f0133ab3011558b5e8d..61aaed8c7aa3f2a99762f2321c11897402a87e24 100644 --- a/self_registration/main/models.py +++ b/self_registration/main/models.py @@ -16,6 +16,7 @@ # License along with Castellum. If not, see # . +import uuid from django.db import models from django.utils.translation import gettext_lazy as _ @@ -37,3 +38,10 @@ class SelfRegisteredSubject(models.Model): email = models.EmailField(_('Email'), max_length=128, blank=True) phone_number = PhoneNumberField(_('Phone number'), max_length=32, blank=True) date_of_birth = DateField(_('Date of birth'), blank=True, null=True) + confirmed = models.BooleanField(_('Confirmed'), default=False) + verification_token = models.UUIDField(_('Token'), default=uuid.uuid4) + created_at = models.DateField(_('Created at'), auto_now_add=True) + + @property + def full_name(self): + return " ".join(filter(None, [self.first_name, self.last_name])) diff --git a/self_registration/main/templates/base.html b/self_registration/main/templates/base.html index 9f020ca5aafaf78ba31fb10ea151885a8d4c15fc..ba8380929136da74201545f2d91762d5bfa2715d 100644 --- a/self_registration/main/templates/base.html +++ b/self_registration/main/templates/base.html @@ -31,7 +31,6 @@ - {% block extra_scripts %}{% endblock %} diff --git a/self_registration/main/templates/main/selfregisteredsubject_confirmed.html b/self_registration/main/templates/main/selfregisteredsubject_confirmed.html new file mode 100644 index 0000000000000000000000000000000000000000..abe9e367f2ad1bc0554593974673386c93c13374 --- /dev/null +++ b/self_registration/main/templates/main/selfregisteredsubject_confirmed.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +

{% translate 'Your email address has been confirmed. The data you have entered will be added to the subject database. The next time we have a matching study you will be contacted. You can close this window. The link from the email is now invalid.' %}

+{% endblock %} diff --git a/self_registration/main/templates/main/selfregisteredsubject_created.html b/self_registration/main/templates/main/selfregisteredsubject_created.html new file mode 100644 index 0000000000000000000000000000000000000000..02d0c9b2688fd6f2a853d8aac90caee31b3b2ff9 --- /dev/null +++ b/self_registration/main/templates/main/selfregisteredsubject_created.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +

{% translate 'Please confirm your email address by clicking the link in the email we have just sent you. After that your information will be added to the subject database.' %}

+{% endblock %} diff --git a/self_registration/main/views.py b/self_registration/main/views.py index b1a69c4e04915f43e033a4414fc18dd921c133a4..5ad7b151224db65604ad4a68e4e0b203bde7b47e 100644 --- a/self_registration/main/views.py +++ b/self_registration/main/views.py @@ -18,10 +18,14 @@ from django.contrib import messages +from django.shortcuts import get_object_or_404 +from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.generic import CreateView +from django.views.generic import TemplateView from .models import SelfRegisteredSubject +from ..utils.mail import send_confirmation_mail class SelfRegisteredSubjectCreateView(CreateView): @@ -29,8 +33,29 @@ class SelfRegisteredSubjectCreateView(CreateView): fields = ['first_name', 'last_name', 'gender', 'date_of_birth', 'email', 'phone_number'] def get_success_url(self): - return self.request.path + return reverse('subject-created') def form_valid(self, form, *args): - messages.success(self.request, _('Data has been saved.')) + if not send_confirmation_mail(form.instance, self.request): + messages.error( + self.request, + _('There have been errors while trying to send the confirmation mail.'), + ) + return super().form_invalid(form, *args) return super().form_valid(form, *args) + + +class SelfRegisteredSubjectCreatedView(TemplateView): + template_name = 'main/selfregisteredsubject_created.html' + + +class SelfRegisteredSubjectConfirmView(TemplateView): + template_name = 'main/selfregisteredsubject_confirmed.html' + + def get(self, request, *args, **kwargs): + subject = get_object_or_404( + SelfRegisteredSubject, verification_token=self.kwargs.get('token'), confirmed=False + ) + subject.confirmed = True + subject.save() + return super().get(request, *args, **kwargs) diff --git a/self_registration/settings/default.py b/self_registration/settings/default.py index ed8a757b7724cf23bce51a97ab2fb5d135251336..aaee59f9e2fbfd8b7904fd8cf5866323a5bfa011 100644 --- a/self_registration/settings/default.py +++ b/self_registration/settings/default.py @@ -1,5 +1,8 @@ from pathlib import Path +import datetime + + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -88,6 +91,7 @@ LOCALE_PATHS = [ BASE_DIR / 'locale', ] + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ @@ -121,8 +125,21 @@ BOOTSTRAP4 = { # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field - DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +SELF_REGISTRATION_CONFIRMATION_MAIL_SUBJECT = 'Please confirm your email address' + +SELF_REGISTRATION_CONFIRMATION_MAIL_BODY = ( + "Guten Tag {name},\n\n" + "bitte bestätigen Sie ihre Registrierung für Castellum, indem sie auf den folgenden " + "Link klicken: {confirmation_link}.\n" + "Sollten Sie sich nicht für Castellum registriert haben, so ignorieren Sie diese " + "E-Mail bitte." +) + TIME_ZONE = 'Europe/Berlin' PHONENUMBER_DEFAULT_REGION = 'DE' + +# Time in days after which unconfirmed self-registered subjects will be deleted +SUBJECT_EXPIRATION_PERIOD = datetime.timedelta(days=7) diff --git a/self_registration/settings/development.py b/self_registration/settings/development.py index 3d6ce533782a41ff70273675f31539d74f609955..1abb413dfa51db590fb1b6ddc18a02539e933294 100644 --- a/self_registration/settings/development.py +++ b/self_registration/settings/development.py @@ -19,3 +19,4 @@ DATABASES = { } } +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/self_registration/urls.py b/self_registration/urls.py index 09ef79ee3ed0cc9be2b3aafc0b51ffb8c587b74f..d372fdd92677c1ab24d9ebfe9c1865745e32fed3 100644 --- a/self_registration/urls.py +++ b/self_registration/urls.py @@ -1,24 +1,36 @@ -"""self_registration URL Configuration +# (c) 2020 MPIB , +# +# This file is part of castellum-self-registration. +# +# castellum-self-registration is free software; you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# Castellum is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with Castellum. If not, see +# . -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/3.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.contrib import admin from django.urls import path +from .main.views import SelfRegisteredSubjectConfirmView from .main.views import SelfRegisteredSubjectCreateView +from .main.views import SelfRegisteredSubjectCreatedView + urlpatterns = [ path('admin/', admin.site.urls), path('create/', SelfRegisteredSubjectCreateView.as_view(), name='subject-create'), + path('created/', SelfRegisteredSubjectCreatedView.as_view(), name='subject-created'), + path( + 'confirm//', + SelfRegisteredSubjectConfirmView.as_view(), + name='subject-confirmed', + ), ] diff --git a/self_registration/utils/mail.py b/self_registration/utils/mail.py new file mode 100644 index 0000000000000000000000000000000000000000..d0916e21e753e57d11a2cc871d97beeb730ce0d2 --- /dev/null +++ b/self_registration/utils/mail.py @@ -0,0 +1,38 @@ +# (c) 2020 MPIB , +# +# This file is part of castellum-self-registration. +# +# castellum-self-registration is free software; you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# Castellum is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with Castellum. If not, see +# . + +from django.conf import settings +from django.core.mail import EmailMessage +from django.urls import reverse + + +def send_confirmation_mail(selfregistered_subject, request): + subject = settings.SELF_REGISTRATION_CONFIRMATION_MAIL_SUBJECT + from_email = None + to_email = selfregistered_subject.email + confirmation_link = request.build_absolute_uri( + reverse('subject-confirmed', args=[selfregistered_subject.verification_token]) + ) + email_body = settings.SELF_REGISTRATION_CONFIRMATION_MAIL_BODY.format( + name=selfregistered_subject.full_name, + confirmation_link=confirmation_link, + ) + email = EmailMessage( + subject, email_body, from_email, to=[to_email] + ) + return email.send(fail_silently=True)