From 3f30722d6b872fe94baf08037598254f5df0d9b2 Mon Sep 17 00:00:00 2001 From: Gavin Wahl Date: Fri, 25 Oct 2024 13:49:12 -0600 Subject: [PATCH] Option to delete user data (#1368) Fixes #965 --- config/urls.py | 2 ++ templates/includes/_form_input.html | 5 +++++ templates/users/delete.html | 21 +++++++++++++++++++++ templates/users/profile.html | 5 ++++- users/forms.py | 10 ++++++++++ users/models.py | 19 ++++++++++++++++++- users/views.py | 29 +++++++++++++++++++++++++++-- 7 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 templates/users/delete.html diff --git a/config/urls.py b/config/urls.py index bcf55d8d..e1871e43 100755 --- a/config/urls.py +++ b/config/urls.py @@ -66,6 +66,7 @@ from users.views import ( ProfileView, UserViewSet, UserAvatar, + DeleteUserView, ) from versions.api import ImportVersionsView, VersionViewSet from versions.feeds import AtomVersionFeed, RSSVersionFeed @@ -103,6 +104,7 @@ urlpatterns = ( ), path("accounts/", include("allauth.urls")), path("users/me/", CurrentUserProfileView.as_view(), name="profile-account"), + path("users/me/delete/", DeleteUserView.as_view(), name="profile-delete"), path("users//", ProfileView.as_view(), name="profile-user"), path("users/avatar/", UserAvatar.as_view(), name="user-avatar"), path("api/v1/users/me/", CurrentUserAPIView.as_view(), name="current-user"), diff --git a/templates/includes/_form_input.html b/templates/includes/_form_input.html index 9d8ea786..02d5ce68 100644 --- a/templates/includes/_form_input.html +++ b/templates/includes/_form_input.html @@ -19,4 +19,9 @@ {% else %} {% render_field field class="w-full bg-white rounded border-gray-300 dark:text-white text-slate dark:border-slate dark:bg-charcoal" placeholder="" %} {% endif %} +{% if field.help_text %} + + {{ field.help_text }} + +{% endif %} diff --git a/templates/users/delete.html b/templates/users/delete.html new file mode 100644 index 00000000..2a020653 --- /dev/null +++ b/templates/users/delete.html @@ -0,0 +1,21 @@ +{% extends "users/profile_base.html" %} + +{% load i18n %} + +{% block content %} +
+

{% trans "Delete Account" %}

+
+ {% csrf_token %} + {% for field in form %} +
+ {% include "includes/_form_input.html" %} +
+ {% endfor %} + + Cancel +
+
+{% endblock content %} diff --git a/templates/users/profile.html b/templates/users/profile.html index 4d6b66b7..d247b48c 100644 --- a/templates/users/profile.html +++ b/templates/users/profile.html @@ -112,7 +112,10 @@ {% endif %} - +
+

{% trans "Delete Account" %}

+ {% trans 'Delete account' %} +
diff --git a/users/forms.py b/users/forms.py index d58f00d1..5311147f 100644 --- a/users/forms.py +++ b/users/forms.py @@ -134,3 +134,13 @@ class UserProfilePhotoForm(forms.ModelForm): user.save() return user + + +class DeleteAccountForm(forms.Form): + verify = forms.CharField(help_text='To verify, type "delete my account" above.') + + def clean_verify(self): + verify = self.cleaned_data["verify"] + if self.cleaned_data["verify"] != "delete my account": + raise forms.ValidationError('Please enter "delete my account"') + return verify diff --git a/users/models.py b/users/models.py index f2f885f2..85f20a8f 100644 --- a/users/models.py +++ b/users/models.py @@ -1,3 +1,4 @@ +import uuid import logging import os @@ -10,7 +11,7 @@ from django.contrib.auth.models import ( ) from django.core.files import File from django.core.mail import send_mail -from django.db import models +from django.db import models, transaction from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone @@ -292,6 +293,22 @@ class User(BaseUser): return None return f"https://github.com/{self.github_username}" + @transaction.atomic + def delete_account(self): + self.socialaccount_set.all().delete() + self.preferences.delete() + self.is_active = False + self.set_unusable_password() + self.display_name = "John Doe" + self.first_name = "John" + self.last_name = "Doe" + self.email = "deleted-{}@example.com".format(uuid.uuid4()) + image = self.image + transaction.on_commit(lambda: image.delete()) + self.image = None + self.image_thumbnail = None + self.save() + class LastSeen(models.Model): """ diff --git a/users/views.py b/users/views.py index c4f91a12..16340ca6 100644 --- a/users/views.py +++ b/users/views.py @@ -1,10 +1,11 @@ from allauth.account import app_settings from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib import auth from django.contrib.messages.views import SuccessMessageMixin from django.http import HttpResponseRedirect from django.urls import reverse_lazy -from django.views.generic import DetailView +from django.views.generic import DetailView, DeleteView from django.views.generic.base import TemplateView from allauth.account.forms import ChangePasswordForm, ResetPasswordForm @@ -16,7 +17,12 @@ from rest_framework import generics from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated, AllowAny -from .forms import PreferencesForm, UserProfileForm, UserProfilePhotoForm +from .forms import ( + PreferencesForm, + UserProfileForm, + UserProfilePhotoForm, + DeleteAccountForm, +) from .models import User from .permissions import CustomUserPermissions from .serializers import UserSerializer, FullUserSerializer, CurrentUserSerializer @@ -294,3 +300,22 @@ class UserAvatar(TemplateView): context["user"] = self.request.user context["mobile"] = self.request.GET.get("ui") return context + + +class DeleteUserView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): + template_name = "users/delete.html" + success_url = "/" + success_message = "Your profile was successfully deleted." + form_class = DeleteAccountForm + + def get_object(self): + return self.request.user + + def form_valid(self, form): + success_url = self.get_success_url() + auth.logout(self.request) + self.object.delete_account() + success_message = self.get_success_message(form.cleaned_data) + if success_message: + messages.success(self.request, success_message) + return HttpResponseRedirect(success_url)