Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 19 additions & 39 deletions assets/js/components/record_details/NewVersionButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -83,41 +79,25 @@ export const NewVersionButton = ({ onError, record, disabled, ...uiProps }) => {
<Modal.Header>
<Icon name="warning sign" color="yellow" className="mr-10" />

{committeeApprovalStatus === "submitted" &&
i18next.t("{{approvalLabel}} request pending", {
approvalLabel,
})}
{committeeApprovalStatus === "accepted" &&
i18next.t("{{approvalLabel}} already complete", {
approvalLabel,
})}
{i18next.t("{{approvalLabel}} request pending", {
approvalLabel,
})}
</Modal.Header>
<Modal.Content>
{committeeApprovalStatus === "submitted" && (
<>
<p>
{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,
}
)}
</p>
<p>
{i18next.t(
"Creating a new version while the request is pending is not recommended."
)}
</p>
</>
)}
{committeeApprovalStatus === "accepted" && (
<p>
{i18next.t(
"A version of this record has already been approved. Creating a new version following an approved request is not recommended."
)}
</p>
)}
<p>
{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,
}
)}
</p>
<p>
{i18next.t(
"Creating a new version while the request is pending is not recommended."
)}
</p>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setShowModal(false)} floated="left">
Expand Down
52 changes: 32 additions & 20 deletions site/cds_rdm/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:<version-uuid>"``.
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.
Expand All @@ -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)
14 changes: 8 additions & 6 deletions site/cds_rdm/requests/committee_approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
]
Expand Down
Loading