diff --git a/site/setup.cfg b/site/setup.cfg index d544ff169..4ef350aac 100644 --- a/site/setup.cfg +++ b/site/setup.cfg @@ -52,6 +52,7 @@ invenio_config.module = zenodo_rdm = zenodo_rdm.config invenio_requests.types = legacy_record_upgrade = zenodo_rdm.legacy.requests:LegacyRecordUpgrade + community_manage_record = zenodo_rdm.legacy.requests:CommunityManageRecord invenio_access.actions = media_files_management_action = zenodo_rdm.generators:media_files_management_action diff --git a/site/zenodo_rdm/legacy/requests/__init__.py b/site/zenodo_rdm/legacy/requests/__init__.py index 1d1fec7ea..68c51e152 100644 --- a/site/zenodo_rdm/legacy/requests/__init__.py +++ b/site/zenodo_rdm/legacy/requests/__init__.py @@ -8,6 +8,7 @@ """Request types for ZenodoRDM.""" +from .community_manage_record import CommunityManageRecord from .record_upgrade import LegacyRecordUpgrade -__all__ = ("LegacyRecordUpgrade",) +__all__ = ("LegacyRecordUpgrade", "CommunityManageRecord") diff --git a/site/zenodo_rdm/legacy/requests/community_manage_record.py b/site/zenodo_rdm/legacy/requests/community_manage_record.py new file mode 100644 index 000000000..634aa1c4c --- /dev/null +++ b/site/zenodo_rdm/legacy/requests/community_manage_record.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2023 CERN. +# +# Zenodo is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Zenodo community manage record request.""" + +from flask_login import current_user +from invenio_access.permissions import system_identity +from invenio_communities.proxies import current_communities +from invenio_pidstore.models import PersistentIdentifier +from invenio_rdm_records.proxies import ( + current_rdm_records_service, + current_record_communities_service, +) +from invenio_rdm_records.records import RDMParent, RDMRecord +from invenio_records_resources.services.uow import IndexRefreshOp, RecordCommitOp +from invenio_requests.customizations import CommentEventType, RequestType, actions +from invenio_requests.proxies import current_events_service, current_requests_service +from invenio_search.engine import dsl + + +def _remove_permission_flag(record, uow): + """Remove can_community_manage_record permission flag.""" + try: + record.parent.permission_flags.pop("can_community_manage_record") + record.parent.commit() + uow.register(RecordCommitOp(record.parent)) + except (KeyError, AttributeError): + # if field or flag is absent just continue with an action + pass + + +def _remove_record_from_communities(record, communities_ids): + """Remove record from communities.""" + if communities_ids: + communities = [] + for community_id in communities_ids: + communities.append({"id": community_id}) + + data = dict(communities=communities) + current_record_communities_service.remove(system_identity, record["id"], data) + + +def _create_comment(request, content, uow): + """Create a comment event.""" + comment = {"payload": {"content": content}} + + current_events_service.create( + system_identity, request.id, comment, CommentEventType, uow=uow + ) + + # make event immediately available in search + uow.register(IndexRefreshOp(indexer=current_events_service.indexer)) + + +def _get_legacy_records_by_user(): + """Find legacy records of a specific user.""" + return current_rdm_records_service.search( + system_identity, + extra_filter=dsl.query.Bool( + "must", + must=[ + dsl.Q("terms", **{"parent.access.owned_by.user": [current_user.id]}), + # indicator that record is a legacy one + dsl.Q( + "exists", + field="parent.permission_flags.can_community_manage_record", + ), + ], + ), + ) + + +def _resolve_record(legacy_record): + """Get record byt its pid.""" + record_pid = PersistentIdentifier.query.filter( + PersistentIdentifier.pid_value == legacy_record["id"] + ).one() + record_id = str(record_pid.object_uuid) + return RDMRecord.get_record(record_id) + + +# +# Actions +# +class SubmitAction(actions.SubmitAction): + """Submit action.""" + + def execute(self, identity, uow): + """Execute the submit action.""" + self.request["title"] = "Communities manage legacy records" + + # example: "May 11, 2024" + expires_at = self.request.expires_at.strftime("%B %d, %Y") + self.request["description"] = ( + "

Some of your records, that are going through migration process are part " + "of the communities that don't belong to you.
Accept this request to keep the old " + "behaviour and allow community curators to manage (edit, create new version, add to " + "another community, etc.) corresponding record.
In case of declining this " + "request all your legacy records will be removed from all communities " + "that you are not an owner of.

If you do not perform any action by " + f"{expires_at}, the permission for community curators to manage the record " + "will automatically be fully granted.

" + ) + + super().execute(identity, uow) + + +class AcceptAction(actions.AcceptAction): + """Accept action.""" + + def execute(self, identity, uow): + """Grant permission to manage all legacy records of a user to all the communities.""" + legacy_records = _get_legacy_records_by_user() + + for hit in legacy_records.hits: + # remove flag from record parent, permissions logic will do the rest + record = _resolve_record(hit) + _remove_permission_flag(record, uow) + + comment = ( + "You accepted the request. The community curators " + "can now manage all of your legacy records." + ) + _create_comment(self.request, comment, uow) + + super().execute(identity, uow) + + +class DeclineAction(actions.DeclineAction): + """Decline action.""" + + def execute(self, identity, uow): + """Deny access to manage legacy records for community curators.""" + legacy_records = _get_legacy_records_by_user() + + for legacy_record in legacy_records.hits: + record = _resolve_record(legacy_record) + communities = ( + legacy_record["parent"].get("communities", None).get("ids", None) + ) + if communities is not None: + not_my_comm = [] + for community_id in communities: + com_members = current_communities.service.members.search( + system_identity, + community_id, + extra_filter=dsl.query.Bool( + "must", + must=[ + dsl.Q("term", **{"role": "owner"}), + ~dsl.Q("term", **{"user_id": current_user.id}), + ], + ), + ) + if com_members.total > 0: + not_my_comm.append(community_id) + _remove_record_from_communities(record, not_my_comm) + _remove_permission_flag(record, uow) + + super().execute(identity, uow) + + +class ExpireAction(actions.ExpireAction): + """Expire action.""" + + def execute(self, identity, uow): + """Grant permission to manage all legacy records of a user to all the communities.""" + legacy_records = _get_legacy_records_by_user() + + for hit in legacy_records.hits: + # remove flag from record parent, permissions logic will do the rest + record = _resolve_record(hit) + _remove_permission_flag(record, uow) + + comment = ( + "The request was expired. The community curators " + "can now manage all of your legacy records." + ) + _create_comment(self.request, comment, uow) + + super().execute(identity, uow) + + +# +# Request +# +class CommunityManageRecord(RequestType): + """Request for granting permissions to manage a record for its community curators.""" + + type_id = "community-manage-record" + name = "Community manage record" + + available_actions = { + "create": actions.CreateAction, + "submit": SubmitAction, + "delete": actions.DeleteAction, + "accept": AcceptAction, + "cancel": actions.CancelAction, + "decline": DeclineAction, + "expire": ExpireAction, + } + + creator_can_be_none = False + topic_can_be_none = True + + allowed_creator_ref_types = ["user"] + allowed_receiver_ref_types = ["user"] diff --git a/site/zenodo_rdm/legacy/requests/utils.py b/site/zenodo_rdm/legacy/requests/utils.py index 137358e5c..27d975431 100644 --- a/site/zenodo_rdm/legacy/requests/utils.py +++ b/site/zenodo_rdm/legacy/requests/utils.py @@ -15,6 +15,7 @@ from invenio_requests.resolvers.registry import ResolverRegistry from invenio_search.engine import dsl +from .community_manage_record import CommunityManageRecord from .record_upgrade import LegacyRecordUpgrade @@ -82,3 +83,26 @@ def submit_record_upgrade_request(record, uow, comment=None): return current_requests_service.execute_action( system_identity, request_item._request.id, "submit", data=comment, uow=uow ) + + +@unit_of_work() +def submit_community_manage_record_request(user_id, uow, comment=None): + """Create and submit a CommunityManageRecord request.""" + type_ = current_request_type_registry.lookup(CommunityManageRecord.type_id) + receiver = ResolverRegistry.resolve_entity_proxy({"user": user_id}).resolve() + expires_at = datetime.utcnow() + timedelta(weeks=20) + + # create a request + request_item = current_requests_service.create( + system_identity, + {}, + type_, + receiver, + expires_at=expires_at, + uow=uow, + ) + + # submit the request + return current_requests_service.execute_action( + system_identity, request_item._request.id, "submit", data=comment, uow=uow + ) diff --git a/templates/semantic-ui/invenio_requests/community-manage-record/user_dashboard.html b/templates/semantic-ui/invenio_requests/community-manage-record/user_dashboard.html new file mode 100644 index 000000000..b0164ac6c --- /dev/null +++ b/templates/semantic-ui/invenio_requests/community-manage-record/user_dashboard.html @@ -0,0 +1,41 @@ +{# -*- coding: utf-8 -*- + + This file is part of Invenio. + Copyright (C) 2023 CERN. + + Invenio is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +#} + +{# + Renders the Community manage record request details page. +#} + +{% extends "invenio_requests/details/index.html" %} + +{% set active_dashboard_menu_item = 'requests' %} + +{%- block request_header %} + {% set back_button_url = url_for("invenio_app_rdm_users.requests") %} + {% from "invenio_requests/macros/request_header.html" import inclusion_request_header %} + {{ inclusion_request_header( + request=invenio_request, + accepted=request_is_accepted, + back_button_url=back_button_url, + back_button_text=_("Back to requests") + ) }} +{%- endblock request_header %} + +{% block request_timeline %} +
+
+ {{ super() }} +
+
+{% endblock request_timeline %}