diff --git a/concordia/admin/actions.py b/concordia/admin/actions.py index 8cb6ec4ea..2a366daac 100644 --- a/concordia/admin/actions.py +++ b/concordia/admin/actions.py @@ -14,8 +14,12 @@ def anonymize_action(modeladmin, request, queryset): count = queryset.count() for user_account in queryset: user_account.username = "Anonymized %s" % uuid.uuid4() + user_account.first_name = "" + user_account.last_name = "" user_account.email = "" user_account.set_unusable_password() + user_account.is_staff = False + user_account.is_superuser = False user_account.is_active = False user_account.save() diff --git a/concordia/forms.py b/concordia/forms.py index 2bf8f5ea1..cb5db1883 100644 --- a/concordia/forms.py +++ b/concordia/forms.py @@ -109,6 +109,12 @@ def clean_email(self): return data +class AccountDeletionForm(forms.Form): + def __init__(self, *, request, **kwargs): + self.request = request + super().__init__(**kwargs) + + class ContactUsForm(forms.Form): referrer = forms.CharField( label="Referring Page", widget=forms.HiddenInput(), required=False diff --git a/concordia/templates/account/account_deletion.html b/concordia/templates/account/account_deletion.html new file mode 100644 index 000000000..7cac1bdcf --- /dev/null +++ b/concordia/templates/account/account_deletion.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% load bootstrap4 %} + +{% block main_content %} +
+ +
+
+ + + +
+
+
+
+

Delete your account?

+
+
+

This cannot be undone!

+
+
+ {% csrf_token %} +
+ {% bootstrap_button "Delete Account" button_type="submit" button_class="btn btn-primary rounded-0" name="submit_delete" %} +
+
+
+
+{% endblock main_content %} diff --git a/concordia/templates/account/profile.html b/concordia/templates/account/profile.html index 42e534741..7fd9ae190 100644 --- a/concordia/templates/account/profile.html +++ b/concordia/templates/account/profile.html @@ -91,6 +91,13 @@

Account Settings

+
+
+ +
+
diff --git a/concordia/templates/emails/delete_account_body.txt b/concordia/templates/emails/delete_account_body.txt new file mode 100644 index 000000000..06b317440 --- /dev/null +++ b/concordia/templates/emails/delete_account_body.txt @@ -0,0 +1,4 @@ +Your By the People account has been deleted. + +Sorry to see you go, +-- The By the People team diff --git a/concordia/templates/emails/delete_account_subject.txt b/concordia/templates/emails/delete_account_subject.txt new file mode 100644 index 000000000..c1dfa939e --- /dev/null +++ b/concordia/templates/emails/delete_account_subject.txt @@ -0,0 +1 @@ +Your By the People account has been deleted diff --git a/concordia/urls.py b/concordia/urls.py index 9a389a8a2..199bc8b7f 100644 --- a/concordia/urls.py +++ b/concordia/urls.py @@ -242,6 +242,11 @@ views.EmailReconfirmationView.as_view(), name="email-reconfirmation", ), + path( + "account/delete/", + views.AccountDeletionView.as_view(), + name="account-deletion", + ), path( ".well-known/change-password", # https://wicg.github.io/change-password-url/ RedirectView.as_view(pattern_name="password_change"), diff --git a/concordia/views.py b/concordia/views.py index 414910a2e..3e5ba541d 100644 --- a/concordia/views.py +++ b/concordia/views.py @@ -3,6 +3,7 @@ import os import random import re +import uuid from functools import wraps from logging import getLogger from smtplib import SMTPException @@ -15,6 +16,7 @@ from captcha.models import CaptchaStore from django.conf import settings from django.contrib import messages +from django.contrib.auth import logout from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.views import ( @@ -63,6 +65,7 @@ from concordia.api_views import APIDetailView, APIListView from concordia.forms import ( + AccountDeletionForm, ActivateAndSetPasswordForm, AllowInactivePasswordResetForm, ContactUsForm, @@ -645,6 +648,75 @@ def send_reconfirmation_email(self, user): ) +@method_decorator(never_cache, name="dispatch") +class AccountDeletionView(LoginRequiredMixin, FormView): + template_name = "account/account_deletion.html" + form_class = AccountDeletionForm + success_url = reverse_lazy("homepage") + email_body_template = "emails/delete_account_body.txt" + email_subject_template = "emails/delete_account_subject.txt" + + def get_form_kwargs(self): + # We expose the request object to the form so we can use it + # to log the user out after deletion + kwargs = super().get_form_kwargs() + kwargs["request"] = self.request + return kwargs + + def form_valid(self, form): + self.delete_user(form.request.user, form.request) + return super().form_valid(form) + + def delete_user(self, user, request=None): + logger.info("Deletion request for %s", user) + email = user.email + if user.transcription_set.exists(): + logger.info("Anonymizing %s", user) + user.username = "Anonymized %s" % uuid.uuid4() + user.first_name = "" + user.last_name = "" + user.email = "" + user.set_unusable_password() + user.is_staff = False + user.is_superuser = False + user.is_active = False + user.save() + else: + logger.info("Deleting %s", user) + user.delete() + self.send_deletion_email(email) + if request: + logout(request) + return redirect("homepage") + + def send_deletion_email(self, email): + context = {} + subject = render_to_string( + template_name=self.email_subject_template, + context=context, + request=self.request, + ) + # Ensure subject is a single line + subject = "".join(subject.splitlines()) + message = render_to_string( + template_name=self.email_body_template, + context=context, + request=self.request, + ) + try: + send_mail( + subject, + message=message, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[email], + ) + except SMTPException: + logger.exception( + "Unable to send account deletion email to %s", + email, + ) + + @method_decorator(default_cache_control, name="dispatch") class HomeView(ListView): template_name = "home.html"