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!
+
+
+
+
+{% 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"