diff --git a/bookwyrm/migrations/0183_auto_20231105_1607.py b/bookwyrm/migrations/0183_auto_20231105_1607.py new file mode 100644 index 000000000..390b56a9f --- /dev/null +++ b/bookwyrm/migrations/0183_auto_20231105_1607.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.20 on 2023-11-05 16:07 + +from django.db import migrations +from bookwyrm.models import User + + +def erase_deleted_user_data(apps, schema_editor): + """Retroactively clear user data""" + for user in User.get_permanently_deleted_users(): + user.erase_user_data() + user.save(broadcast=False) + user.erase_user_statuses(broadcast=False) + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0182_auto_20231027_1122"), + ] + + operations = [ + migrations.RunPython( + erase_deleted_user_data, reverse_code=migrations.RunPython.noop + ) + ] diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 625a7d289..017db31d3 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -8,7 +8,7 @@ from django.contrib.auth.models import AbstractUser from django.contrib.postgres.fields import ArrayField, CICharField from django.core.exceptions import PermissionDenied, ObjectDoesNotExist from django.dispatch import receiver -from django.db import models, transaction +from django.db import models, transaction, IntegrityError from django.utils import timezone from django.utils.translation import gettext_lazy as _ from model_utils import FieldTracker @@ -263,6 +263,13 @@ class User(OrderedCollectionPageMixin, AbstractUser): is_active=True, ).distinct() + @classmethod + def get_permanently_deleted_users(cls): + return cls.objects.filter( + is_active=False, + deactivation_reason__in=["self_deletion", "moderator_deletion"], + ).distinct() + def update_active_date(self): """this user is here! they are doing things!""" self.last_active_date = timezone.now() @@ -415,10 +422,24 @@ class User(OrderedCollectionPageMixin, AbstractUser): self.name = None self.favorites.set([]) - def erase_user_statuses(self): + def erase_user_statuses(self, broadcast=True): """Wipe the data on all the user's statuses""" + # safety valve: make sure the user is deleted + if not self.is_permanently_deleted: + raise IntegrityError( + "Attempted to delete statuses for improperly deleted user" + ) + for status in self.status_set.all(): - status.delete() + status.delete(broadcast=broadcast) + + @property + def is_permanently_deleted(self): + """is this user inactive, or really truly deleted?""" + return not self.is_active and self.deactivation_reason in [ + "self_deletion", + "moderator_deletion", + ] def deactivate(self): """Disable the user but allow them to reactivate""" diff --git a/bookwyrm/tests/models/test_user_model.py b/bookwyrm/tests/models/test_user_model.py index de39d5467..47db1bc9a 100644 --- a/bookwyrm/tests/models/test_user_model.py +++ b/bookwyrm/tests/models/test_user_model.py @@ -2,6 +2,7 @@ import json from unittest.mock import patch from django.contrib.auth.models import Group +from django.db import IntegrityError from django.test import TestCase import responses @@ -265,6 +266,25 @@ class User(TestCase): self.assertIsNone(status.content) self.assertIsNotNone(status.deleted_date) + @patch("bookwyrm.suggested_users.remove_user_task.delay") + @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async") + @patch("bookwyrm.activitystreams.add_status_task.delay") + def test_delete_user_erase_statuses(self, *_): + """erase user statuses when user is deleted""" + status = models.Status.objects.create(user=self.user, content="hello") + self.assertFalse(status.deleted) + self.assertIsNotNone(status.content) + self.assertIsNone(status.deleted_date) + + self.user.deactivate() + with self.assertRaises(IntegrityError): + self.user.erase_user_statuses() + + status.refresh_from_db() + self.assertFalse(status.deleted) + self.assertIsNotNone(status.content) + self.assertIsNone(status.deleted_date) + def test_admins_no_admins(self): """list of admins""" result = models.User.admins() @@ -302,3 +322,40 @@ class User(TestCase): results = models.User.admins() self.assertEqual(results.count(), 1) self.assertEqual(results.first(), self.user) + + def test_get_permanently_deleted_users(self): + + with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( + "bookwyrm.activitystreams.populate_stream_task.delay" + ), patch("bookwyrm.lists_stream.populate_lists_task.delay"): + active_user = models.User.objects.create_user( + f"activeuser@{DOMAIN}", + "activeuser@activeuser.activeuser", + "activeuserword", + local=True, + localname="active", + ) + deleted_user = models.User.objects.create_user( + f"deleteduser@{DOMAIN}", + "deleteduser@deleteduser.deleteduser", + "deleteduserword", + local=True, + localname="deleted", + is_active=False, + deactivation_reason="self_deletion", + ) + inactive_user = models.User.objects.create_user( + f"inactiveuser@{DOMAIN}", + "inactiveuser@inactiveuser.inactiveuser", + "inactiveuserword", + local=True, + localname="inactive", + is_active=False, + deactivation_reason="self_deactivation", + ) + + deleted_users = models.User.get_permanently_deleted_users() + + self.assertTrue(deleted_users.filter(localname="deleted").exists()) + self.assertFalse(deleted_users.filter(localname="active").exists()) + self.assertFalse(deleted_users.filter(localname="inactive").exists())