From 2229687db92f22f1c1bdfff40fb8cb001cc30372 Mon Sep 17 00:00:00 2001 From: Anton Krytskyi Date: Mon, 29 Sep 2025 16:59:02 +0300 Subject: [PATCH] allow gdpr delete for sole contribs --- osf/models/user.py | 27 ++++++++++++++++--- osf_tests/test_user.py | 61 +++++++++++++++++++++++++----------------- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/osf/models/user.py b/osf/models/user.py index d0c3cf22b2d..bdd859dbafc 100644 --- a/osf/models/user.py +++ b/osf/models/user.py @@ -1979,13 +1979,34 @@ def gdpr_delete(self): def _validate_no_public_entities(self): """ Ensure that the user doesn't have any public facing resources like Registrations or Preprints + that would be left with other contributors after this deletion. + + Allow GDPR deletion if the user is the sole contributor on a public Registration or Preprint. """ - from osf.models import Preprint + from osf.models import Preprint, AbstractNode + + registrations_with_others = AbstractNode.objects.annotate( + contrib_count=Count('_contributors', distinct=True), + ).filter( + _contributors=self, + deleted__isnull=True, + type='osf.registration', + contrib_count__gt=1 + ).exists() - if self.nodes.filter(deleted__isnull=True, type='osf.registration').exists(): + if registrations_with_others: raise UserStateError('You cannot delete this user because they have one or more registrations.') - if Preprint.objects.filter(_contributors=self, ever_public=True, deleted__isnull=True).exists(): + preprints_with_others = Preprint.objects.annotate( + contrib_count=Count('_contributors', distinct=True), + ).filter( + _contributors=self, + ever_public=True, + deleted__isnull=True, + contrib_count__gt=1 + ).exists() + + if preprints_with_others: raise UserStateError('You cannot delete this user because they have one or more preprints.') def _validate_and_remove_resource_for_gdpr_delete(self, resources, hard_delete): diff --git a/osf_tests/test_user.py b/osf_tests/test_user.py index 6cddff997c0..d4dc84b0a96 100644 --- a/osf_tests/test_user.py +++ b/osf_tests/test_user.py @@ -31,8 +31,6 @@ NotableDomain, PreprintContributor, DraftRegistrationContributor, - DraftRegistration, - DraftNode, UserSessionMap, NotificationType, ) from osf.models.institution_affiliation import get_user_by_institution_identity @@ -2134,26 +2132,6 @@ def test_can_gdpr_delete_personal_nodes(self, user): user.gdpr_delete() assert user.nodes.exclude(is_deleted=True).count() == 0 - def test_can_gdpr_delete_personal_registrations(self, user, registration_with_draft_node): - assert DraftRegistration.objects.all().count() == 1 - assert DraftNode.objects.all().count() == 1 - - with pytest.raises(UserStateError) as exc_info: - user.gdpr_delete() - - assert exc_info.value.args[0] == 'You cannot delete this user because they have one or more registrations.' - assert DraftRegistration.objects.all().count() == 1 - assert DraftNode.objects.all().count() == 1 - - registration_with_draft_node.remove_node(Auth(user)) - assert DraftRegistration.objects.all().count() == 1 - assert DraftNode.objects.all().count() == 1 - user.gdpr_delete() - - # DraftNodes soft-deleted, DraftRegistions hard-deleted - assert user.nodes.exclude(is_deleted=True).count() == 0 - assert DraftRegistration.objects.all().count() == 0 - def test_can_gdpr_delete_shared_nodes_with_multiple_admins(self, user, project_with_two_admins): user.gdpr_delete() @@ -2162,7 +2140,7 @@ def test_can_gdpr_delete_shared_nodes_with_multiple_admins(self, user, project_w def test_can_gdpr_delete_shared_draft_registration_with_multiple_admins(self, user, registration): other_admin = AuthUserFactory() draft_registrations = user.draft_registrations.get() - draft_registrations.add_contributor(other_admin, permissions='admin') + draft_registrations.add_contributor(other_admin, auth=Auth(user), permissions='admin') assert draft_registrations.contributors.all().count() == 2 registration.delete_registration_tree(save=True) @@ -2170,20 +2148,53 @@ def test_can_gdpr_delete_shared_draft_registration_with_multiple_admins(self, us assert draft_registrations.contributors.get() == other_admin assert user.nodes.filter(deleted__isnull=True).count() == 0 - def test_cant_gdpr_delete_registrations(self, user, registration): + def test_cant_gdpr_delete_multiple_contributors_registrations(self, user, registration): + registration.is_public = True + other_user = AuthUserFactory() + registration.add_contributor(other_user, auth=Auth(user), permissions='admin') + registration.save() + + assert registration.contributors.count() == 2 with pytest.raises(UserStateError) as exc_info: user.gdpr_delete() assert exc_info.value.args[0] == 'You cannot delete this user because they have one or more registrations.' - def test_cant_gdpr_delete_preprints(self, user, preprint): + def test_cant_gdpr_delete_multiple_contributors_preprints(self, user, preprint): + other_user = AuthUserFactory() + preprint.add_contributor(other_user, auth=Auth(user), permissions='admin') + preprint.save() with pytest.raises(UserStateError) as exc_info: user.gdpr_delete() assert exc_info.value.args[0] == 'You cannot delete this user because they have one or more preprints.' + def test_can_gdpr_delete_sole_contributor_registration(self, user): + registration = RegistrationFactory(creator=user) + registration.save() + + assert registration.contributors.count() == 1 + assert registration.contributors.first() == user + + user.gdpr_delete() + + assert user.fullname == 'Deleted user' + assert user.deleted is not None + + def test_can_gdpr_delete_sole_contributor_preprint(self, user): + preprint = PreprintFactory(creator=user) + preprint.save() + + assert preprint.contributors.count() == 1 + assert preprint.contributors.first() == user + + user.gdpr_delete() + + assert user.fullname == 'Deleted user' + assert user.deleted is not None + def test_cant_gdpr_delete_shared_node_if_only_admin(self, user, project_user_is_only_admin): with pytest.raises(UserStateError) as exc_info: