From 69eca6c268286843e68941c8e1d6825c6caaa02f Mon Sep 17 00:00:00 2001 From: Maximilian Moser Date: Thu, 7 Sep 2023 13:59:00 +0200 Subject: [PATCH] moderation: delete a user's records when blocking them --- .../requests/user_moderation/actions.py | 87 +++++++++++++++++-- .../requests/test_user_moderation_actions.py | 32 +++++++ 2 files changed, 114 insertions(+), 5 deletions(-) diff --git a/invenio_rdm_records/requests/user_moderation/actions.py b/invenio_rdm_records/requests/user_moderation/actions.py index 1a4113374..fc0189f20 100644 --- a/invenio_rdm_records/requests/user_moderation/actions.py +++ b/invenio_rdm_records/requests/user_moderation/actions.py @@ -1,24 +1,101 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2023 CERN. +# Copyright (C) 2023 TU Wien. # # Invenio-RDM-Records is free software; you can redistribute it and/or modify # it under the terms of the MIT License; see LICENSE file for more details. """RDM user moderation action.""" from invenio_access.permissions import system_identity +from invenio_pidstore.errors import PIDDoesNotExistError +from invenio_vocabularies.proxies import current_service -from invenio_rdm_records.proxies import current_rdm_records_service +from ...proxies import current_rdm_records_service +from ...records.systemfields.deletion_status import RecordDeletionStatusEnum + + +def _get_records_for_user(user_id): + """Helper function for getting all the records of the user. + + Note: This function performs DB queries yielding all records for a given + user (which is not hard-limited in the system) and performs service calls + on each of them. Thus, this function has the potential of being a very + heavy operation, and should not be called as part of the handling of an + HTTP request! + """ + record_cls = current_rdm_records_service.record_cls + model_cls = record_cls.model_cls + parent_cls = record_cls.parent_record_cls + parent_model_cls = parent_cls.model_cls + + # get all the parent records owned by the blocked user + parent_recs = [ + parent_cls(m.data, model=m) + for m in parent_model_cls.query.filter( + parent_model_cls.json["access"]["owned_by"]["user"].as_string() == user_id + ).all() + ] + + # get all child records of the chosen parent records + recs = [ + record_cls(m.data, model=m) + for m in model_cls.query.filter( + model_cls.parent_id.in_([p.id for p in parent_recs]) + ).all() + ] + + return recs def on_block(user_id, uow=None, **kwargs): - """Removes records that belong to a user.""" - pass + """Removes records that belong to a user. + + Note: This function operates on all records of a user and thus has the potential + to be a very heavy operation! Thus it should not be called as part of the handling + of an HTTP request! + """ + user_id = str(user_id) + tombstone_data = {"note": "User was blocked"} + + # set the removal reason if the vocabulary item exists + try: + removal_reason_id = kwargs.get("removal_reason_id", "misconduct") + vocab = current_service.read( + identity=system_identity, id_=("removalreasons", removal_reason_id) + ) + tombstone_data["removal_reason"] = {"id": vocab.id} + except PIDDoesNotExistError: + pass + + # soft-delete all the published records of that user + for rec in _get_records_for_user(user_id): + if not rec.deletion_status.is_deleted: + current_rdm_records_service.delete_record( + system_identity, + rec.pid.pid_value, + tombstone_data, + uow=uow, + ) def on_restore(user_id, uow=None, **kwargs): - """Restores records that belong to a user.""" - pass + """Restores records that belong to a user. + + Note: This function operates on all records of a user and thus has the potential + to be a very heavy operation! Thus it should not be called as part of the handling + of an HTTP request! + """ + user_id = str(user_id) + + # restore all the deleted records of that user + for rec in _get_records_for_user(user_id): + if rec.deletion_status == RecordDeletionStatusEnum.DELETED: + current_rdm_records_service.restore_record( + system_identity, + rec.pid.pid_value, + uow=uow, + ) def on_approve(user_id, uow=None, **kwargs): diff --git a/tests/requests/test_user_moderation_actions.py b/tests/requests/test_user_moderation_actions.py index d72ddc58c..f11db8011 100644 --- a/tests/requests/test_user_moderation_actions.py +++ b/tests/requests/test_user_moderation_actions.py @@ -1,6 +1,7 @@ # # -*- coding: utf-8 -*- # # # # Copyright (C) 2023 CERN. +# # Copyright (C) 2023 TU Wien. # # # # Invenio-RDM is free software; you can redistribute it and/or modify # # it under the terms of the MIT License; see LICENSE file for more details. @@ -94,3 +95,34 @@ def test_user_moderation_approve( hits = post_approval_records.to_dict()["hits"]["hits"] is_verified = all([hit["parent"]["is_verified"] for hit in hits]) assert is_verified == True + + +def test_user_moderation_decline( + running_app, mod_identity, unverified_user, es_clear, minimal_record, mocker +): + """Tests user moderation action after decline. + + All of the user's records should be deleted. + """ + # Create a record + draft = records_service.create(unverified_user.identity, minimal_record) + record = records_service.publish(id_=draft.id, identity=unverified_user.identity) + assert not record._record.deletion_status.is_deleted + assert record._record.tombstone is None + + # Fetch moderation request that was created on publish and decline the user + res = current_requests_service.search( + system_identity, params={"q": f"topic.user:{unverified_user.id}"} + ) + assert res.total == 1 + mod_request = res.to_dict()["hits"]["hits"][0] + current_requests_service.execute_action( + mod_identity, id_=mod_request["id"], action="decline" + ) + + # The user's record should now be deleted + record = records_service.read( + id_=draft.id, identity=system_identity, with_deleted=True + ) + assert record._record.deletion_status.is_deleted + assert record._record.tombstone is not None