From d473e67cd3e71ebcdb505fb3cf9aeab739681f15 Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Tue, 21 Jul 2026 08:12:50 +0200 Subject: [PATCH 1/2] feat(ep): allow referees to view new record versions * Changed the permissions so that EP referees are able to view versions of the restricted/internal record that were created after the approval was requested. --- site/cds_rdm/generators.py | 52 +++++++++++++-------- site/cds_rdm/requests/committee_approval.py | 14 +++--- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/site/cds_rdm/generators.py b/site/cds_rdm/generators.py index 93e54c69..cc362d14 100644 --- a/site/cds_rdm/generators.py +++ b/site/cds_rdm/generators.py @@ -8,10 +8,13 @@ """Permissions generators.""" +from datetime import datetime + from flask import current_app from flask_principal import RoleNeed, UserNeed from invenio_access import action_factory from invenio_access.permissions import Permission +from invenio_rdm_records.services.generators import AccessGrant from invenio_records_permissions.generators import AuthenticatedUser, Generator from invenio_search.engine import dsl @@ -180,20 +183,27 @@ def query_filter(self, **kwargs): COMMITTEE_APPROVAL_GRANT_ORIGIN_PREFIX = "committee-approval:" COMMITTEE_APPROVAL_GRANT_PERMISSION = "committee-review" +COMMITTEE_APPROVAL_ACCESS_GRANT = AccessGrant(COMMITTEE_APPROVAL_GRANT_PERMISSION) + +def committee_approval_grant_origin(version_index: int): + """Return the grant origin string for a specific record version index.""" + return f"{COMMITTEE_APPROVAL_GRANT_ORIGIN_PREFIX}{version_index}" -def committee_approval_grant_origin(version_uuid): - """Return the grant origin string for a specific record version UUID.""" - return f"{COMMITTEE_APPROVAL_GRANT_ORIGIN_PREFIX}{version_uuid}" + +def get_version_index_from_origin(origin: str): + """Returns just the version index number from the grant origin.""" + version_str = origin.removeprefix(COMMITTEE_APPROVAL_GRANT_ORIGIN_PREFIX) + if not version_str.isdigit(): + return None + return int(version_str) class CommitteeRefereeVersionGrant(Generator): - """Read access for committee referees scoped to the exact version they reviewed. + """Read access for committee referees scoped to the exact version they reviewed, as well as newer versions created after the approval was granted. - Grants are stored on the parent with a custom permission level - ``"committee-review"`` and ``origin="committee-approval:"``. - Only the version whose UUID matches the origin will satisfy this generator — - other versions in the same family are excluded. + Later versions than the one for which the review was requested are only be accessible + if they were created after the review was approved. On submit → grant added (version UUID = topic.id at submit time). On accept → grant kept; referees retain permanent access to that version. @@ -204,16 +214,18 @@ def needs(self, record=None, **kwargs): """Return the RoleNeed if this version has a matching committee review grant.""" if record is None: return [] - version_origin = committee_approval_grant_origin(record.id) - return { - grant.to_need() - for grant in record.parent.access.grants - if ( - grant.permission == COMMITTEE_APPROVAL_GRANT_PERMISSION - and grant.origin == version_origin - ) - } - def query_filter(self, **kwargs): - """Not used for search filters.""" - return [] + needs = [] + for grant in record.parent.access.grants: + if grant.permission != COMMITTEE_APPROVAL_GRANT_PERMISSION: + continue + + grant_version = get_version_index_from_origin(grant.origin) + if grant_version is not None and record.versions.index >= grant_version: + needs.append(grant.to_need()) + + return needs + + def query_filter(self, identity=None, **kwargs): + """Filter for records with a committee review grant for one of the identity's roles.""" + return COMMITTEE_APPROVAL_ACCESS_GRANT.query_filter(identity, kwargs=kwargs) diff --git a/site/cds_rdm/requests/committee_approval.py b/site/cds_rdm/requests/committee_approval.py index b1da19ad..a633870c 100644 --- a/site/cds_rdm/requests/committee_approval.py +++ b/site/cds_rdm/requests/committee_approval.py @@ -67,7 +67,9 @@ def _resolve_community_config(request): "The record is not part of any community. " "It must belong to a committee-approval-enabled community before submitting." ) - committee_communities = current_app.config.get("CDS_COMMITTEE_APPROVAL_COMMUNITIES", {}) + committee_communities = current_app.config.get( + "CDS_COMMITTEE_APPROVAL_COMMUNITIES", {} + ) if default_community_id in committee_communities: return committee_communities[default_community_id] raise ValidationError( @@ -98,9 +100,9 @@ def execute(self, identity: Identity, uow: UnitOfWork) -> None: config = _resolve_community_config(self.request) # Reject if the parent already carries an approval report number. - if ((topic.parent.get("permission_flags") or {}).get("committee_approval") or {}).get( - "reportnumber" - ): + if ( + (topic.parent.get("permission_flags") or {}).get("committee_approval") or {} + ).get("reportnumber"): raise ValidationError( "A version of this record already has an approval report number assigned. " "A new committee approval request cannot be submitted." @@ -146,7 +148,7 @@ def execute(self, identity: Identity, uow: UnitOfWork) -> None: subject_type="role", subject_id=config["referee_group"], permission=COMMITTEE_APPROVAL_GRANT_PERMISSION, - origin=committee_approval_grant_origin(topic.id), + origin=committee_approval_grant_origin(topic.versions.index), ) uow.register( ParentRecordCommitOp( @@ -272,7 +274,7 @@ def execute(self, identity: Identity, uow: UnitOfWork) -> None: def _remove_committee_approval_grant(request, uow): """Remove the committee approval referee grant from the record's parent.""" topic = request.topic.resolve() - origin = committee_approval_grant_origin(topic.id) + origin = committee_approval_grant_origin(topic.versions.index) topic.parent.access.grants[:] = [ g for g in topic.parent.access.grants if g.origin != origin ] From c837d9a7520daa170ed443f2a6bbe674d7bda321 Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Thu, 23 Jul 2026 11:09:50 +0200 Subject: [PATCH 2/2] fix(ep): remove warning when creating new version of accepted record --- .../record_details/NewVersionButton.js | 58 ++++++------------- 1 file changed, 19 insertions(+), 39 deletions(-) diff --git a/assets/js/components/record_details/NewVersionButton.js b/assets/js/components/record_details/NewVersionButton.js index f3b0fcdd..892f02b5 100644 --- a/assets/js/components/record_details/NewVersionButton.js +++ b/assets/js/components/record_details/NewVersionButton.js @@ -27,12 +27,8 @@ export const NewVersionButton = ({ onError, record, disabled, ...uiProps }) => { committeeApproval?.approval_label || i18next.t("Committee approval"); const handleClick = useCallback(async () => { - if ( - ["submitted", "accepted"].includes(committeeApprovalStatus) && - !showModal - ) { - // If a request exists, we discourage creating a new version. - // This applies even if the request has been accepted. + if (committeeApprovalStatus === "submitted" && !showModal) { + // If a request is pending, we discourage creating a new version. setShowModal(true); return; } else if (showModal) { @@ -83,41 +79,25 @@ export const NewVersionButton = ({ onError, record, disabled, ...uiProps }) => { - {committeeApprovalStatus === "submitted" && - i18next.t("{{approvalLabel}} request pending", { - approvalLabel, - })} - {committeeApprovalStatus === "accepted" && - i18next.t("{{approvalLabel}} already complete", { - approvalLabel, - })} + {i18next.t("{{approvalLabel}} request pending", { + approvalLabel, + })} - {committeeApprovalStatus === "submitted" && ( - <> -

- {i18next.t( - "An {{approvalLabel}} request is already pending for an existing version of this record. " + - "A new version will not be taken into account for the {{approvalLabel}} request.", - { - approvalLabel, - } - )} -

-

- {i18next.t( - "Creating a new version while the request is pending is not recommended." - )} -

- - )} - {committeeApprovalStatus === "accepted" && ( -

- {i18next.t( - "A version of this record has already been approved. Creating a new version following an approved request is not recommended." - )} -

- )} +

+ {i18next.t( + "An {{approvalLabel}} request is already pending for an existing version of this record. " + + "A new version will not be taken into account for the {{approvalLabel}} request.", + { + approvalLabel, + } + )} +

+

+ {i18next.t( + "Creating a new version while the request is pending is not recommended." + )} +