diff --git a/assets/js/components/record_details/EPApproval.js b/assets/js/components/record_details/CommitteeApproval.js similarity index 89% rename from assets/js/components/record_details/EPApproval.js rename to assets/js/components/record_details/CommitteeApproval.js index 27e149c3..89c1d587 100644 --- a/assets/js/components/record_details/EPApproval.js +++ b/assets/js/components/record_details/CommitteeApproval.js @@ -15,20 +15,20 @@ import { Step, } from "semantic-ui-react"; import { i18next } from "@translations/invenio_app_rdm/i18next"; -import { EPApprovalSubmitModal } from "./EPApprovalSubmitModal"; +import { CommitteeApprovalSubmitModal } from "./CommitteeApprovalSubmitModal"; import { CreatePublicRecordModal } from "./CreatePublicRecordModal"; import PropTypes from "prop-types"; -export class EPApprovalManageSection extends Component { +export class CommitteeApprovalManageSection extends Component { constructor(props) { super(props); const recordManagementDiv = document.getElementById("recordManagement"); - const epApprovalData = recordManagementDiv - ? JSON.parse(recordManagementDiv.dataset.epApproval || "null") + const committeeApprovalData = recordManagementDiv + ? JSON.parse(recordManagementDiv.dataset.committeeApproval || "null") : null; this.state = { - epApproval: epApprovalData, + committeeApproval: committeeApprovalData, submitModalOpen: false, createPublicModalOpen: false, newVersionModalOpen: false, @@ -46,10 +46,11 @@ export class EPApprovalManageSection extends Component { } componentDidUpdate(_prevProps, prevState) { - const { epApproval } = this.state; - const prevEpApproval = prevState.epApproval; + const { committeeApproval } = this.state; + const prevEpApproval = prevState.committeeApproval; if ( - epApproval?.open_request?.status !== prevEpApproval?.open_request?.status + committeeApproval?.open_request?.status !== + prevEpApproval?.open_request?.status ) { this._detachNewVersionInterceptor(); this._attachNewVersionInterceptor(); @@ -61,8 +62,8 @@ export class EPApprovalManageSection extends Component { } _attachNewVersionInterceptor() { - const { epApproval } = this.state; - if (epApproval?.open_request?.status !== "submitted") return; + const { committeeApproval } = this.state; + if (committeeApproval?.open_request?.status !== "submitted") return; // Find the "New version" button rendered by InvenioRDM's RecordManagement. const btn = Array.from(document.querySelectorAll("button")).find( @@ -92,18 +93,19 @@ export class EPApprovalManageSection extends Component { } get shouldRender() { - const { epApproval } = this.state; + const { committeeApproval } = this.state; return ( - epApproval && - (epApproval.community_enrolled || epApproval.is_public_approved_record) + committeeApproval && + (committeeApproval.community_enrolled || + committeeApproval.is_public_approved_record) ); } handleSubmitSuccess = (request) => { this.setState((prev) => ({ submitModalOpen: false, - epApproval: { - ...prev.epApproval, + committeeApproval: { + ...prev.committeeApproval, open_request: { id: request.id, status: "submitted", @@ -122,7 +124,7 @@ export class EPApprovalManageSection extends Component { render() { const { - epApproval, + committeeApproval, submitModalOpen, createPublicModalOpen, newVersionModalOpen, @@ -136,12 +138,12 @@ export class EPApprovalManageSection extends Component { } // Public EP-approved record — show a compact provenance note. - if (epApproval.is_public_approved_record) { + if (committeeApproval.is_public_approved_record) { const { approved_report_number: pubRn, draft_record_id: draftRecordId, can_view_reviewed_version: canViewReviewedVersion, - } = epApproval; + } = committeeApproval; return ( @@ -180,15 +182,15 @@ export class EPApprovalManageSection extends Component { open_request: openRequest, approved_report_number: approvedReportNumber, receiver_group: receiverGroup, - ep_approval: epApprovalField, - } = epApproval; + committee_approval: committeeApprovalField, + } = committeeApproval; // Public record URL: prefer the URL captured at creation time; fall back to // the recid stored on the parent (approved_public_version) for page-load case. const resolvedPublicRecordUrl = publicRecordUrl || - (epApprovalField?.approved_public_version - ? `/records/${epApprovalField.approved_public_version}` + (committeeApprovalField?.approved_public_version + ? `/records/${committeeApprovalField.approved_public_version}` : null); const isPending = openRequest?.status === "submitted"; @@ -232,12 +234,12 @@ export class EPApprovalManageSection extends Component { vertical fluid size="mini" - className="ep-step-group" + className="committee-step-group" > {/* Step 1 — Request for approval */} -
+
{i18next.t("Request for approval")} @@ -283,7 +285,7 @@ export class EPApprovalManageSection extends Component { {canResubmit && ( - -
+
{i18next.t("EP Board review")} @@ -347,7 +349,7 @@ export class EPApprovalManageSection extends Component { disabled={step3Disabled} > -
+
{step3Completed @@ -433,6 +435,6 @@ export class EPApprovalManageSection extends Component { } } -EPApprovalManageSection.propTypes = { +CommitteeApprovalManageSection.propTypes = { record: PropTypes.object.isRequired, }; diff --git a/assets/js/components/record_details/EPApprovalSubmitModal.js b/assets/js/components/record_details/CommitteeApprovalSubmitModal.js similarity index 96% rename from assets/js/components/record_details/EPApprovalSubmitModal.js rename to assets/js/components/record_details/CommitteeApprovalSubmitModal.js index 38c424c8..f4ed421a 100644 --- a/assets/js/components/record_details/EPApprovalSubmitModal.js +++ b/assets/js/components/record_details/CommitteeApprovalSubmitModal.js @@ -27,7 +27,7 @@ const buildInitialForm = (record) => ({ additional_communication: "", }); -export class EPApprovalSubmitModal extends Component { +export class CommitteeApprovalSubmitModal extends Component { constructor(props) { super(props); this.state = { @@ -58,7 +58,7 @@ export class EPApprovalSubmitModal extends Component { payload: { ...form }, }; const response = await http.post( - `/api/records/${record.id}/ep-approval`, + `/api/records/${record.id}/committee-approval`, payload, { headers: { "Content-Type": "application/json" } } ); @@ -187,7 +187,7 @@ export class EPApprovalSubmitModal extends Component { } } -EPApprovalSubmitModal.propTypes = { +CommitteeApprovalSubmitModal.propTypes = { open: PropTypes.bool.isRequired, record: PropTypes.object.isRequired, receiverGroup: PropTypes.string, @@ -195,6 +195,6 @@ EPApprovalSubmitModal.propTypes = { onSuccess: PropTypes.func.isRequired, }; -EPApprovalSubmitModal.defaultProps = { +CommitteeApprovalSubmitModal.defaultProps = { receiverGroup: null, }; diff --git a/assets/js/components/record_details/CreatePublicRecordModal.js b/assets/js/components/record_details/CreatePublicRecordModal.js index 74dddde5..22592b0f 100644 --- a/assets/js/components/record_details/CreatePublicRecordModal.js +++ b/assets/js/components/record_details/CreatePublicRecordModal.js @@ -35,7 +35,7 @@ export class CreatePublicRecordModal extends Component { this.setState({ submitting: true, error: null, alreadyExists: false }); try { const response = await http.post( - `/api/records/${record.id}/ep-approval/publish-public`, + `/api/records/${record.id}/committee-approval/publish-public`, {}, { headers: { "Content-Type": "application/json" } } ); @@ -83,11 +83,11 @@ export class CreatePublicRecordModal extends Component { const versionIndex = record?.versions?.index; const canPublish = agreedToTerms && agreedToCommunity; - const epApprovalEl = document.getElementById("recordManagement"); - const epApproval = epApprovalEl - ? JSON.parse(epApprovalEl.dataset.epApproval || "null") + const committeeApprovalEl = document.getElementById("recordManagement"); + const committeeApproval = committeeApprovalEl + ? JSON.parse(committeeApprovalEl.dataset.committeeApproval || "null") : null; - const communityId = epApproval?.cern_scientific_community_id; + const communityId = committeeApproval?.cern_scientific_community_id; return ( diff --git a/assets/js/components/record_details/RecordVersionItem.js b/assets/js/components/record_details/RecordVersionItem.js index 67d11a79..7352b0ae 100644 --- a/assets/js/components/record_details/RecordVersionItem.js +++ b/assets/js/components/record_details/RecordVersionItem.js @@ -4,24 +4,24 @@ import { i18next } from "@translations/invenio_app_rdm/i18next"; import PropTypes from "prop-types"; import { CopyButton } from "@js/invenio_app_rdm/components/CopyButton"; -function readEpApprovalData() { +function readCommitteeApprovalData() { const el = document.getElementById("recordManagement"); if (!el) return null; try { - return JSON.parse(el.dataset.epApproval || "null"); + return JSON.parse(el.dataset.committeeApproval || "null"); } catch (_) { return null; } } export const RecordVersionItemContent = ({ item, activeVersion, doi }) => { - const epApproval = useMemo(readEpApprovalData, []); + const committeeApproval = useMemo(readCommitteeApprovalData, []); // --- Internal draft side --- - // ep_approval is the parent record's approval dict, shared by all versions. + // committee_approval is the parent record's approval dict, shared by all versions. // Keys: reportnumber, approved_internal_version, approved_public_version, // source_public_version (internal parent); source_internal_version (public parent). - const ea = epApproval?.ep_approval || {}; + const ea = committeeApproval?.committee_approval || {}; const approvedReportNumber = ea.reportnumber; // approved_internal_version: recid of the version that was submitted and approved. const isApprovedVersion = diff --git a/assets/js/invenio_app_rdm/overridableRegistry/mapping.js b/assets/js/invenio_app_rdm/overridableRegistry/mapping.js index 872e7515..c565ec26 100644 --- a/assets/js/invenio_app_rdm/overridableRegistry/mapping.js +++ b/assets/js/invenio_app_rdm/overridableRegistry/mapping.js @@ -12,7 +12,7 @@ import { CDSRecordsResultsListItem } from "../../components/frontpage/overrides/ import { CDSRecordsResultsListItemDescription } from "../../components/search/overrides/CDSRecordsResultsListItemDescription"; import { CDSAffiliationsSuggestions } from "../../components/deposit/overrides/CDSAffiliationsSuggestions"; import { CLCSync } from "../../components/record_details/clc_sync"; -import { EPApprovalManageSection } from "../../components/record_details/EPApproval"; +import { CommitteeApprovalManageSection } from "../../components/record_details/CommitteeApproval"; import { PublishModalComponent, SubmitReviewModalComponent, @@ -24,7 +24,7 @@ import { RecordVersionItemContent } from "../../components/record_details/Record const RecordManagementContainer = (props) => ( <> - + ); diff --git a/assets/less/cds-rdm/ep_approval/ep-workflow.less b/assets/less/cds-rdm/committee_approval/committee-workflow.less similarity index 80% rename from assets/less/cds-rdm/ep_approval/ep-workflow.less rename to assets/less/cds-rdm/committee_approval/committee-workflow.less index f76bfc38..c9bb27fe 100644 --- a/assets/less/cds-rdm/ep_approval/ep-workflow.less +++ b/assets/less/cds-rdm/committee_approval/committee-workflow.less @@ -1,4 +1,4 @@ -.ep-step-group .step { +.committee-step-group .step { .content { flex: 1; } @@ -12,7 +12,7 @@ } } -.ep-action-step { +.committee-action-step { display: flex; justify-content: space-between; align-items: center; @@ -24,7 +24,7 @@ } } -dl.ep-submission-details { +dl.committee-submission-details { dt { font-size: 0.9em; color: @mutedTextColor; @@ -32,7 +32,7 @@ dl.ep-submission-details { } dd { - .ep-details-note { + .committee-details-note { // A little bit darker than @mutedTextColor to distinguish from the labels color: #555; } diff --git a/assets/less/cds-rdm/globals/site.overrides b/assets/less/cds-rdm/globals/site.overrides index 9a05372b..854c5220 100644 --- a/assets/less/cds-rdm/globals/site.overrides +++ b/assets/less/cds-rdm/globals/site.overrides @@ -1,7 +1,7 @@ @import "../mixins.less"; @import "./hero.less"; @import "../administration/harvester-reports.less"; -@import "../ep_approval/ep-workflow.less"; +@import "../committee_approval/committee-workflow.less"; .rdm-logo { padding: 1em 1em 1em 0; diff --git a/invenio.cfg b/invenio.cfg index cec15ece..bf8a3bb5 100644 --- a/invenio.cfg +++ b/invenio.cfg @@ -31,10 +31,10 @@ from invenio_app_rdm.config import \ STATS_AGGREGATIONS as _APP_RDM_STATS_AGGREGATIONS from invenio_app_rdm.config import STATS_EVENTS as _APP_RDM_STATS_EVENTS from invenio_app_rdm.config import NOTIFICATIONS_BUILDERS -from cds_rdm.notifications.ep_approval import ( - EPApprovalAcceptNotificationBuilder, - EPApprovalDeclineNotificationBuilder, - EPApprovalSubmitNotificationBuilder, +from cds_rdm.notifications.committee_approval import ( + CommitteeApprovalAcceptNotificationBuilder, + CommitteeApprovalDeclineNotificationBuilder, + CommitteeApprovalSubmitNotificationBuilder, ) from invenio_cern_sync.sso import cern_keycloak, cern_remote_app_name from invenio_cern_sync.users.profile import CERNUserProfileSchema @@ -452,7 +452,7 @@ CDS_CERN_SCIENTIFIC_COMMUNITY_ID = "" # EP / Publication Approval Workflow # ================================== -CDS_EP_APPROVAL_COMMUNITIES = { +CDS_COMMITTEE_APPROVAL_COMMUNITIES = { # Map community UUID → workflow config. # UUIDs are used (not slugs) because slugs can be renamed. # @@ -793,9 +793,9 @@ NOTIFICATIONS_BUILDERS = { RepositoryReleaseFailureNotificationBuilder.type: RepositoryReleaseFailureNotificationBuilder, RepositoryReleaseCommunityRequiredNotificationBuilder.type: RepositoryReleaseCommunityRequiredNotificationBuilder, RepositoryReleaseCommunitySubmittedNotificationBuilder.type: RepositoryReleaseCommunitySubmittedNotificationBuilder, - EPApprovalSubmitNotificationBuilder.type: EPApprovalSubmitNotificationBuilder, - EPApprovalAcceptNotificationBuilder.type: EPApprovalAcceptNotificationBuilder, - EPApprovalDeclineNotificationBuilder.type: EPApprovalDeclineNotificationBuilder, + CommitteeApprovalSubmitNotificationBuilder.type: CommitteeApprovalSubmitNotificationBuilder, + CommitteeApprovalAcceptNotificationBuilder.type: CommitteeApprovalAcceptNotificationBuilder, + CommitteeApprovalDeclineNotificationBuilder.type: CommitteeApprovalDeclineNotificationBuilder, } NOTIFICATIONS_GROUP_EMAIL_DOMAIN = "cern.ch" diff --git a/package.json b/package.json index 611bc7da..16648749 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,6 @@ "npm": ">=8" }, "name": "invenio-assets", - "peerDependencies": {}, "private": true, "scripts": { "build": "NODE_PRESERVE_SYMLINKS=1 NODE_ENV=production webpack --config ./build/webpack.config.js", @@ -125,4 +124,4 @@ "start": "NODE_PRESERVE_SYMLINKS=1 NODE_ENV=development webpack --watch --progress --config ./build/webpack.config.js" }, "version": "2.0.0" -} \ No newline at end of file +} diff --git a/site/cds_rdm/components.py b/site/cds_rdm/components.py index 68a9be6d..c0d5ca1d 100644 --- a/site/cds_rdm/components.py +++ b/site/cds_rdm/components.py @@ -137,14 +137,14 @@ def publish(self, identity, draft=None, record=None, **kwargs): class CommitteeApprovalComponent(ServiceComponent): - """Guard and sync EP approval identifiers. + """Guard and sync committee approval identifiers. 1. Blocks non-privileged users from adding/modifying/deleting ``apprn`` scheme identifiers — these are system-managed only. 2. Blocks non-privileged users from adding a ``cdsrn`` identifier whose - value matches any configured EP approval report-number pattern + value matches any configured committee approval report-number pattern (e.g. CERN-EP-*). - 3. Regenerates the ``apprn`` metadata identifier from parent ep_approval + 3. Regenerates the ``apprn`` metadata identifier from parent committee_approval on every save — only the public approved record carries it (detected by ``source_internal_version`` on the parent). """ @@ -155,13 +155,13 @@ def _is_privileged(self, identity): ActionNeed("superuser-access") ).allows(identity) - def _ep_approval_prefixes(self): - """Return the set of fixed prefixes from all configured EP communities. + def _committee_approval_prefixes(self): + """Return the set of fixed prefixes from all configured committee communities. E.g. config ``{"prefix": "CERN-EP"}`` → prefix ``"CERN-EP"``. - Used to detect cdsrn values that collide with EP report numbers. + Used to detect cdsrn values that collide with committee report numbers. """ - communities = current_app.config.get("CDS_EP_APPROVAL_COMMUNITIES", {}) + communities = current_app.config.get("CDS_COMMITTEE_APPROVAL_COMMUNITIES", {}) prefixes = set() for cfg in communities.values(): prefix = cfg.get("report_number", {}).get("prefix") @@ -210,8 +210,8 @@ def _validate_identifier_changes(self, identity, data, record): raise ValidationErrorWithMessageAsList(errors) - # Block cdsrn values that look like EP report numbers. - ep_prefixes = self._ep_approval_prefixes() + # Block cdsrn values that look like committee report numbers. + ep_prefixes = self._committee_approval_prefixes() if ep_prefixes: errors = [] for index, ident in enumerate(incoming_identifiers): @@ -234,7 +234,7 @@ def _validate_identifier_changes(self, identity, data, record): raise ValidationErrorWithMessageAsList(errors) def _regenerate_apprn_identifier(self, record, data): - """Keep apprn in metadata.identifiers in sync with parent ep_approval. + """Keep apprn in metadata.identifiers in sync with parent committee_approval. The apprn identifier is only added when ``source_internal_version`` is present on the parent — that key is set exclusively on the public approved record's @@ -242,7 +242,7 @@ def _regenerate_apprn_identifier(self, record, data): """ ea = ( (record.parent.get("permission_flags") if record.parent else None) or {} - ).get("ep_approval") or {} + ).get("committee_approval") or {} reportnumber = ea.get("reportnumber") source_internal = ea.get("source_internal_version") identifiers = [ diff --git a/site/cds_rdm/ext.py b/site/cds_rdm/ext.py index e6bf97ea..347f13e0 100644 --- a/site/cds_rdm/ext.py +++ b/site/cds_rdm/ext.py @@ -7,6 +7,8 @@ """CDS-RDM module.""" +from invenio_records.dumpers import SearchDumperExt + from cds_rdm.clc_sync.resources.config import CLCSyncResourceConfig from cds_rdm.clc_sync.resources.resource import CLCSyncResource from cds_rdm.clc_sync.resources.utils import get_clc_sync_entry @@ -16,13 +18,33 @@ HarvesterDownloadResource, HarvesterDownloadResourceConfig, ) -from cds_rdm.requests.ep_approval_state import get_ep_approval_state +from cds_rdm.requests.committee_approval_state import get_committee_approval_state from . import config from .utils import evaluate_permissions from .views import get_linked_records_search_query +class _StripCommitteeApprovalDumperExt(SearchDumperExt): + """Remove permission_flags.committee_approval from the parent's search document. + + committee_approval contains system-internal workflow state (report number, version + pointers) that should only be read from the DB, not indexed in OpenSearch. + Keeping it out of the index avoids uncontrolled dynamic mapping and prevents + accidental exposure via search queries. + """ + + def dump(self, record, data): + """Strip committee_approval from the dump.""" + try: + data.get("permission_flags", {}).pop("committee_approval", None) + except (AttributeError, TypeError): + pass + + def load(self, data, record_cls): + """Nothing to restore — field is DB-only.""" + + class CDS_RDM_App(object): """CDS-RDM App.""" @@ -39,10 +61,11 @@ def init_config(self, app): def init_app(self, app): """Flask application initialization.""" + self._register_dumper_extensions() self.init_services(app) self.init_resources(app) app.jinja_env.globals["get_clc_sync_entry"] = get_clc_sync_entry - app.jinja_env.globals["get_ep_approval_state"] = get_ep_approval_state + app.jinja_env.globals["get_committee_approval_state"] = get_committee_approval_state app.jinja_env.globals["evaluate_permissions"] = evaluate_permissions # Register filter for building linked records search query app.jinja_env.filters["get_linked_records_search_query"] = ( @@ -50,6 +73,12 @@ def init_app(self, app): ) return app + def _register_dumper_extensions(self): + """Register CDS-specific dumper extensions on InvenioRDM record models.""" + from invenio_rdm_records.records.api import RDMParent + + RDMParent.dumper._extensions.append(_StripCommitteeApprovalDumperExt()) + def init_services(self, app): """Initialize the services for banners.""" self.clc_sync_service = CLCSyncService(config=CLCSyncServiceConfig) diff --git a/site/cds_rdm/generators.py b/site/cds_rdm/generators.py index 7222e289..33296ed4 100644 --- a/site/cds_rdm/generators.py +++ b/site/cds_rdm/generators.py @@ -149,7 +149,7 @@ class EPWorkflowCommunityManager(Generator): """Allows community managers of EP-workflow-enrolled communities. A community is enrolled by having its UUID listed as a key in the - ``CDS_EP_APPROVAL_COMMUNITIES`` config dict. + ``CDS_COMMITTEE_APPROVAL_COMMUNITIES`` config dict. """ def needs(self, record=None, **kwargs): @@ -160,7 +160,7 @@ def needs(self, record=None, **kwargs): """ from invenio_communities.generators import CommunityRoleNeed - ep_communities = current_app.config.get("CDS_EP_APPROVAL_COMMUNITIES", {}) + ep_communities = current_app.config.get("CDS_COMMITTEE_APPROVAL_COMMUNITIES", {}) if record is None: return [] diff --git a/site/cds_rdm/inspire_harvester/jobs.py b/site/cds_rdm/inspire_harvester/jobs.py index 75ef5258..f44b2c7a 100644 --- a/site/cds_rdm/inspire_harvester/jobs.py +++ b/site/cds_rdm/inspire_harvester/jobs.py @@ -12,12 +12,7 @@ from invenio_i18n import gettext as _ from invenio_jobs.jobs import PredefinedArgsSchema from invenio_vocabularies.jobs import ProcessDataStreamJob -from marshmallow import ( - ValidationError, - fields, - validate, - validates_schema, -) +from marshmallow import ValidationError, fields, validate, validates_schema from cds_rdm.inspire_harvester.transform.resource_types import ( ALL_DOCUMENT_TYPES, diff --git a/site/cds_rdm/inspire_harvester/reader.py b/site/cds_rdm/inspire_harvester/reader.py index fbcc7a73..9218850a 100644 --- a/site/cds_rdm/inspire_harvester/reader.py +++ b/site/cds_rdm/inspire_harvester/reader.py @@ -13,9 +13,7 @@ from invenio_vocabularies.datastreams.errors import ReaderError from invenio_vocabularies.datastreams.readers import BaseReader -from cds_rdm.inspire_harvester.transform.resource_types import ( - ALL_DOCUMENT_TYPES, -) +from cds_rdm.inspire_harvester.transform.resource_types import ALL_DOCUMENT_TYPES class InspireHTTPReader(BaseReader): diff --git a/site/cds_rdm/notifications/ep_approval.py b/site/cds_rdm/notifications/committee_approval.py similarity index 75% rename from site/cds_rdm/notifications/ep_approval.py rename to site/cds_rdm/notifications/committee_approval.py index 28511f04..b5b4fdca 100644 --- a/site/cds_rdm/notifications/ep_approval.py +++ b/site/cds_rdm/notifications/committee_approval.py @@ -6,7 +6,7 @@ # Invenio is free software; you can redistribute it and/or modify it # under the terms of the GPL-2.0 License; see LICENSE file for more details. -"""EP Approval notification builders.""" +"""Committee Approval notification builders.""" from __future__ import annotations @@ -30,10 +30,10 @@ from .generators import GroupMembersRecipientGenerator -class EPApprovalNotificationBuilder(NotificationBuilder): - """Base notification builder for EP approval request actions.""" +class CommitteeApprovalNotificationBuilder(NotificationBuilder): + """Base notification builder for committee approval request actions.""" - type: ClassVar[str] = "ep-approval-request" + type: ClassVar[str] = "committee-approval-request" context: ClassVar[list[ContextGenerator]] = [ EntityResolve(key="request"), @@ -66,10 +66,10 @@ def build(cls, identity: Identity, request: Request) -> Notification: ) -class EPApprovalSubmitNotificationBuilder(EPApprovalNotificationBuilder): - """Notify the EP referee group when a request is submitted.""" +class CommitteeApprovalSubmitNotificationBuilder(CommitteeApprovalNotificationBuilder): + """Notify the committee referee group when a request is submitted.""" - type: ClassVar[str] = f"{EPApprovalNotificationBuilder.type}.submit" + type: ClassVar[str] = f"{CommitteeApprovalNotificationBuilder.type}.submit" # created_by omitted: submit can be triggered by system_identity which has # no resolvable user record. recipients: ClassVar[list[RecipientGenerator]] = [ @@ -77,12 +77,12 @@ class EPApprovalSubmitNotificationBuilder(EPApprovalNotificationBuilder): ] -class EPApprovalAcceptNotificationBuilder(EPApprovalNotificationBuilder): +class CommitteeApprovalAcceptNotificationBuilder(CommitteeApprovalNotificationBuilder): """Notify the submitter when their request is accepted.""" - type: ClassVar[str] = f"{EPApprovalNotificationBuilder.type}.accept" + type: ClassVar[str] = f"{CommitteeApprovalNotificationBuilder.type}.accept" context: ClassVar[list[ContextGenerator]] = [ - *EPApprovalNotificationBuilder.context, + *CommitteeApprovalNotificationBuilder.context, EntityResolve(key="request.created_by"), ] recipients: ClassVar[list[RecipientGenerator]] = [ @@ -90,12 +90,12 @@ class EPApprovalAcceptNotificationBuilder(EPApprovalNotificationBuilder): ] -class EPApprovalDeclineNotificationBuilder(EPApprovalNotificationBuilder): +class CommitteeApprovalDeclineNotificationBuilder(CommitteeApprovalNotificationBuilder): """Notify the submitter when their request is declined.""" - type: ClassVar[str] = f"{EPApprovalNotificationBuilder.type}.decline" + type: ClassVar[str] = f"{CommitteeApprovalNotificationBuilder.type}.decline" context: ClassVar[list[ContextGenerator]] = [ - *EPApprovalNotificationBuilder.context, + *CommitteeApprovalNotificationBuilder.context, EntityResolve(key="request.created_by"), ] recipients: ClassVar[list[RecipientGenerator]] = [ diff --git a/site/cds_rdm/requests/__init__.py b/site/cds_rdm/requests/__init__.py index 34dc3420..ce4cfda4 100644 --- a/site/cds_rdm/requests/__init__.py +++ b/site/cds_rdm/requests/__init__.py @@ -8,6 +8,6 @@ """CDS RDM request types.""" -from .ep_approval import EPApprovalRequest +from .committee_approval import CommitteeApprovalRequest -__all__ = ["EPApprovalRequest"] +__all__ = ["CommitteeApprovalRequest"] diff --git a/site/cds_rdm/requests/ep_approval.py b/site/cds_rdm/requests/committee_approval.py similarity index 82% rename from site/cds_rdm/requests/ep_approval.py rename to site/cds_rdm/requests/committee_approval.py index 84752324..1a966a86 100644 --- a/site/cds_rdm/requests/ep_approval.py +++ b/site/cds_rdm/requests/committee_approval.py @@ -6,7 +6,7 @@ # Invenio is free software; you can redistribute it and/or modify it # under the terms of the GPL-2.0 License; see LICENSE file for more details. -"""EP Approval request type.""" +"""Committee Approval request type.""" from __future__ import annotations @@ -35,10 +35,10 @@ EPWorkflowCommunityManager, committee_approval_grant_origin, ) -from ..notifications.ep_approval import ( - EPApprovalAcceptNotificationBuilder, - EPApprovalDeclineNotificationBuilder, - EPApprovalSubmitNotificationBuilder, +from ..notifications.committee_approval import ( + CommitteeApprovalAcceptNotificationBuilder, + CommitteeApprovalDeclineNotificationBuilder, + CommitteeApprovalSubmitNotificationBuilder, ) # PID type stored in pidstore_pid.pid_type (VARCHAR(6) — keep ≤ 6 chars). @@ -52,11 +52,11 @@ def _resolve_community_config(request): - """Resolve the EP approval config for the request's topic record. + """Resolve the committee approval config for the request's topic record. Validates that: - the topic record belongs to at least one community, and - - that community is enrolled in CDS_EP_APPROVAL_COMMUNITIES. + - that community is enrolled in CDS_COMMITTEE_APPROVAL_COMMUNITIES. Raises ``ValueError`` with a descriptive message if either condition fails. """ @@ -65,19 +65,19 @@ def _resolve_community_config(request): if not default_community_id: raise ValidationError( "The record is not part of any community. " - "It must belong to an EP-approval-enabled community before submitting." + "It must belong to a committee-approval-enabled community before submitting." ) - ep_communities = current_app.config.get("CDS_EP_APPROVAL_COMMUNITIES", {}) - if default_community_id in ep_communities: - return ep_communities[default_community_id] + 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( # TODO: do we need i18n for these errors? - "The record's community is not enrolled in the EP approval workflow. " - "Only records in EP-approval-enabled communities can be submitted." + "The record's community is not enrolled in the committee approval workflow. " + "Only records in committee-approval-enabled communities can be submitted." ) -class EPApprovalSubmitAction(actions.CreateAndSubmitAction): +class CommitteeApprovalSubmitAction(actions.CreateAndSubmitAction): """Submit action — validate community enrollment and notify referees.""" def execute(self, identity: Identity, uow: UnitOfWork) -> None: @@ -98,12 +98,12 @@ 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("ep_approval") or {}).get( + 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 EP approval request cannot be submitted." + "A new committee approval request cannot be submitted." ) # Reject if there is already a submitted (pending) request for ANY version @@ -125,7 +125,7 @@ def execute(self, identity: Identity, uow: UnitOfWork) -> None: system_identity, params={ "q": ( - f'({topic_query}) AND type:"ep-approval"' + f'({topic_query}) AND type:"committee-approval"' ' AND status:"submitted"' ), "size": 1, @@ -134,7 +134,7 @@ def execute(self, identity: Identity, uow: UnitOfWork) -> None: ) if existing: raise ValidationError( - "An EP approval request is already pending for this record." + "A committee approval request is already pending for this record." ) # Grant the referee group read access scoped to this specific version. @@ -157,7 +157,7 @@ def execute(self, identity: Identity, uow: UnitOfWork) -> None: uow.register( NotificationOp( - EPApprovalSubmitNotificationBuilder.build( + CommitteeApprovalSubmitNotificationBuilder.build( identity=identity, request=self.request, ) @@ -166,7 +166,7 @@ def execute(self, identity: Identity, uow: UnitOfWork) -> None: super().execute(identity, uow) -class EPApprovalAcceptAction(actions.AcceptAction): +class CommitteeApprovalAcceptAction(actions.AcceptAction): """Accept action — auto-generate the approval report number and assign it.""" def _community_config(self): @@ -223,7 +223,7 @@ def _issue_report_number(self, config: dict, record_uuid: str) -> str: return report_number def execute(self, identity: Identity, uow: UnitOfWork) -> None: - """Execute accept: mint report number and write ep_approval to the parent. + """Execute accept: mint report number and write committee_approval to the parent. All versions in the family share the same parent, so a single write is visible to every version — no propagation or edit/publish cycle needed. @@ -235,9 +235,9 @@ def execute(self, identity: Identity, uow: UnitOfWork) -> None: topic = self.request.topic.resolve() report_number = self._issue_report_number(config, str(topic.id)) - # Write ep_approval into permission_flags — single source of truth. + # Write committee_approval into permission_flags — single source of truth. pf = topic.parent.get("permission_flags") or {} - pf["ep_approval"] = { + pf["committee_approval"] = { "reportnumber": report_number, "datetime": datetime.now(timezone.utc).isoformat(), "approved_internal_version": topic["id"], @@ -260,7 +260,7 @@ def execute(self, identity: Identity, uow: UnitOfWork) -> None: uow.register( NotificationOp( - EPApprovalAcceptNotificationBuilder.build( + CommitteeApprovalAcceptNotificationBuilder.build( identity=identity, request=self.request, ) @@ -269,8 +269,8 @@ def execute(self, identity: Identity, uow: UnitOfWork) -> None: super().execute(identity, uow) -def _remove_ep_approval_grant(request, uow): - """Remove the EP approval referee grant from the record's parent.""" +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) topic.parent.access.grants[:] = [ @@ -284,15 +284,15 @@ def _remove_ep_approval_grant(request, uow): ) -class EPApprovalDeclineAction(actions.DeclineAction): +class CommitteeApprovalDeclineAction(actions.DeclineAction): """Decline action — revoke referee access and notify the submitter.""" def execute(self, identity: Identity, uow: UnitOfWork) -> None: """Execute decline.""" - _remove_ep_approval_grant(self.request, uow) + _remove_committee_approval_grant(self.request, uow) uow.register( NotificationOp( - EPApprovalDeclineNotificationBuilder.build( + CommitteeApprovalDeclineNotificationBuilder.build( identity=identity, request=self.request, ) @@ -301,32 +301,32 @@ def execute(self, identity: Identity, uow: UnitOfWork) -> None: super().execute(identity, uow) -class EPApprovalCancelAction(actions.CancelAction): +class CommitteeApprovalCancelAction(actions.CancelAction): """Cancel action — revoke referee access.""" def execute(self, identity: Identity, uow: UnitOfWork) -> None: """Execute cancel.""" - _remove_ep_approval_grant(self.request, uow) + _remove_committee_approval_grant(self.request, uow) super().execute(identity, uow) -class EPApprovalRequest(RDMBaseRequest): - """EP Approval request type. +class CommitteeApprovalRequest(RDMBaseRequest): + """Committee Approval request type. - Allows community managers of enrolled communities to request EP committee + Allows community managers of enrolled communities to request committee approval for a specific record version. On acceptance CDS auto-generates a report number (e.g. CERN-EP-2026-001) and assigns it to the record. """ - type_id: Final[str] = "ep-approval" - name: Final[str] = _("EP Approval") + type_id: Final[str] = "committee-approval" + name: Final[str] = _("Committee Approval") available_actions: Final[dict] = { **RDMBaseRequest.available_actions, - "create": EPApprovalSubmitAction, - "accept": EPApprovalAcceptAction, - "decline": EPApprovalDeclineAction, - "cancel": EPApprovalCancelAction, + "create": CommitteeApprovalSubmitAction, + "accept": CommitteeApprovalAcceptAction, + "decline": CommitteeApprovalDeclineAction, + "cancel": CommitteeApprovalCancelAction, } available_statuses: Final[dict] = { diff --git a/site/cds_rdm/requests/ep_approval_state.py b/site/cds_rdm/requests/committee_approval_state.py similarity index 87% rename from site/cds_rdm/requests/ep_approval_state.py rename to site/cds_rdm/requests/committee_approval_state.py index 3d2ffa78..2bc533b4 100644 --- a/site/cds_rdm/requests/ep_approval_state.py +++ b/site/cds_rdm/requests/committee_approval_state.py @@ -6,7 +6,7 @@ # Invenio is free software; you can redistribute it and/or modify it # under the terms of the GPL-2.0 License; see LICENSE file for more details. -"""EP Approval state helpers for the record landing page.""" +"""Committee Approval state helpers for the record landing page.""" from flask import current_app, g from invenio_access.permissions import system_identity @@ -16,28 +16,28 @@ def _get_enrolled_community(record_ui): """Return (community_id, community_config) for enrolled communities, else (None, None).""" - ep_communities = current_app.config.get("CDS_EP_APPROVAL_COMMUNITIES", {}) + ep_communities = current_app.config.get("CDS_COMMITTEE_APPROVAL_COMMUNITIES", {}) parent = record_ui.get("parent", {}) if record_ui else {} default_community_id = parent.get("communities", {}).get("default") config = ep_communities.get(default_community_id) if default_community_id else None return (default_community_id, config) if config else (None, None) -def _get_parent_ep_approval(record): - """Read ep_approval dict directly from the parent record object. +def _get_parent_committee_approval(record): + """Read committee_approval dict directly from the parent record object. Returns the dict or {} if not set / record not available. """ try: return (record._record.parent.get("permission_flags") or {}).get( - "ep_approval" + "committee_approval" ) or {} except Exception: return {} def _get_open_request(record_id, parent_record=None): - """Return the most-recent EP approval request for the record family. + """Return the most-recent committee approval request for the record family. Uses parent.get_records_by_parent to build the topic query so we cover all versions without a separate DB scan. @@ -66,7 +66,7 @@ def _get_open_request(record_id, parent_record=None): system_identity, params={ "q": ( - f'({topic_query}) AND type:"ep-approval"' + f'({topic_query}) AND type:"committee-approval"' ' AND (status:"submitted" OR status:"declined" OR status:"accepted")' ), "size": 1, @@ -129,10 +129,10 @@ def _check_can_create_public(can_submit, ea, record_id): return True # fail open — backend will re-validate -def get_ep_approval_state(record_ui, record=None): - """Return EP approval state for the record landing page. +def get_committee_approval_state(record_ui, record=None): + """Return committee approval state for the record landing page. - Reads ep_approval from the parent record object (single source of truth). + Reads committee_approval from the parent record object (single source of truth). No DB scans across versions needed. Returns a dict with: @@ -143,14 +143,14 @@ def get_ep_approval_state(record_ui, record=None): - open_request: dict or None — {id, status, links} - approved_report_number: str or None - approval_date: str or None - - ep_approval: dict — raw parent ep_approval (for frontend version badges) + - committee_approval: dict — raw parent committee_approval (for frontend version badges) - draft_record_id: str or None - receiver_group: str or None """ - # Read ep_approval from the parent. - ea = _get_parent_ep_approval(record) + # Read committee_approval from the parent. + ea = _get_parent_committee_approval(record) - # Early exit: this IS the public EP-approved copy. + # Early exit: this IS the public committee-approved copy. # The public record's parent has source_internal_version set. if ea.get("source_internal_version"): default_community_id = ( @@ -169,7 +169,7 @@ def get_ep_approval_state(record_ui, record=None): "open_request": None, "approved_report_number": ea.get("reportnumber"), "approval_date": None, - "ep_approval": ea, + "committee_approval": ea, "draft_record_id": ea["source_internal_version"], "can_view_reviewed_version": can_view_reviewed_version, "receiver_group": None, @@ -187,7 +187,7 @@ def get_ep_approval_state(record_ui, record=None): "open_request": None, "approved_report_number": None, "approval_date": None, - "ep_approval": {}, + "committee_approval": {}, "draft_record_id": None, "can_view_reviewed_version": False, "receiver_group": None, @@ -219,7 +219,7 @@ def get_ep_approval_state(record_ui, record=None): "open_request": open_request, "approved_report_number": approved_report_number, "approval_date": ea.get("datetime"), - "ep_approval": ea, + "committee_approval": ea, "draft_record_id": None, "can_view_reviewed_version": False, "receiver_group": community_config.get("referee_group"), diff --git a/site/cds_rdm/requests/views.py b/site/cds_rdm/requests/views.py index 6b29752f..e01fa081 100644 --- a/site/cds_rdm/requests/views.py +++ b/site/cds_rdm/requests/views.py @@ -6,7 +6,7 @@ # Invenio is free software; you can redistribute it and/or modify it # under the terms of the GPL-2.0 License; see LICENSE file for more details. -"""CDS EP Approval API views.""" +"""CDS Committee Approval API views.""" import copy @@ -27,17 +27,17 @@ from invenio_requests.proxies import current_requests_service from invenio_requests.resolvers.registry import ResolverRegistry -from .ep_approval import EPApprovalRequest +from .committee_approval import CommitteeApprovalRequest -def create_ep_approval_bp(app): - """Create EP approval API blueprint.""" - bp = Blueprint("cds_ep_approval", __name__) +def create_committee_approval_bp(app): + """Create committee approval API blueprint.""" + bp = Blueprint("cds_committee_approval", __name__) - @bp.route("/records//ep-approval", methods=["POST"]) + @bp.route("/records//committee-approval", methods=["POST"]) @login_required - def submit_ep_approval(pid_value): - """Submit an EP approval request for a published record.""" + def submit_committee_approval(pid_value): + """Submit a committee approval request for a published record.""" try: record = current_rdm_records_service.read( g.identity, pid_value, expand=False @@ -62,10 +62,10 @@ def submit_ep_approval(pid_value): req = current_requests_service.create( identity=g.identity, data={ - "title": f'EP approval for "{title}"', + "title": f'Committee approval for "{title}"', "payload": payload, }, - request_type=EPApprovalRequest, + request_type=CommitteeApprovalRequest, receiver=receiver, topic=record._record, ) @@ -76,7 +76,7 @@ def submit_ep_approval(pid_value): return jsonify(req.to_dict()), 201 - @bp.route("/records//ep-approval/publish-public", methods=["POST"]) + @bp.route("/records//committee-approval/publish-public", methods=["POST"]) @login_required def publish_public_record(pid_value): """Create a public approved record from an approved draft. @@ -85,9 +85,9 @@ def publish_public_record(pid_value): record's enrolled community. Steps: - 1. Read the approved draft — must have ep_approval.reportnumber set on parent. + 1. Read the approved draft — must have committee_approval.reportnumber set on parent. 2. Build a new public record: copy metadata + files, set access=public. - 3. Create draft, import files, write ep_approval to both parents, publish. + 3. Create draft, import files, write committee_approval to both parents, publish. 4. Return the new public record id and links. """ # --- read + authorise --- @@ -98,10 +98,10 @@ def publish_public_record(pid_value): except (PIDDoesNotExistError, PermissionDeniedError) as e: return jsonify({"message": str(e)}), 403 - # Read ep_approval from the internal draft's parent. + # Read committee_approval from the internal draft's parent. src_pid_obj = PersistentIdentifier.get("recid", pid_value) src_rec_obj = RDMRecord.get_record(src_pid_obj.object_uuid) - ea = (src_rec_obj.parent.get("permission_flags") or {}).get("ep_approval") or {} + ea = (src_rec_obj.parent.get("permission_flags") or {}).get("committee_approval") or {} report_number = ea.get("reportnumber") if not report_number: @@ -162,7 +162,7 @@ def publish_public_record(pid_value): src_id = src["id"] # Strip apprn from identifiers — CommitteeApprovalComponent regenerates it - # from ep_approval.reportnumber on every update_draft / publish. + # from committee_approval.reportnumber on every update_draft / publish. new_identifiers = [ i for i in src.get("metadata", {}).get("identifiers", []) @@ -204,10 +204,10 @@ def publish_public_record(pid_value): new_draft._record.files.copy(src_rec_obj.files) new_draft._record.commit() - # Write ep_approval into the public record's permission_flags. + # Write committee_approval into the public record's permission_flags. # source_internal_version marks this as the public copy and links back. pf = new_draft._record.parent.get("permission_flags") or {} - pf["ep_approval"] = { + pf["committee_approval"] = { "reportnumber": report_number, "source_internal_version": src_id, } @@ -216,7 +216,7 @@ def publish_public_record(pid_value): db.session.commit() # update_draft triggers CommitteeApprovalComponent which regenerates - # the apprn identifier (now reads from parent ep_approval). + # the apprn identifier (now reads from parent committee_approval). new_record = current_rdm_records_service.publish( system_identity, new_draft.id ) @@ -265,7 +265,7 @@ def publish_public_record(pid_value): back_link_warning = None try: pf = src_rec_obj.parent.get("permission_flags") or {} - pf["ep_approval"] = { + pf["committee_approval"] = { **ea, "approved_public_version": new_record_id, } diff --git a/site/cds_rdm/templates/semantic-ui/cds_rdm/records/detail.html b/site/cds_rdm/templates/semantic-ui/cds_rdm/records/detail.html index 1d1e56a5..7c58c457 100644 --- a/site/cds_rdm/templates/semantic-ui/cds_rdm/records/detail.html +++ b/site/cds_rdm/templates/semantic-ui/cds_rdm/records/detail.html @@ -9,7 +9,7 @@ {%- set clc_sync_entry = get_clc_sync_entry(record_ui) %} {%- set additional_permissions = evaluate_permissions(record, ['manage_clc_sync']) %} -{%- set ep_approval_state = get_ep_approval_state(record_ui, record) %} +{%- set committee_approval_state = get_committee_approval_state(record_ui, record) %} {# Find the apprn identifier on the public EP-approved record. #} {%- set apprn_identifier = namespace(value=None) %} diff --git a/site/cds_rdm/templates/semantic-ui/cds_rdm/records/manage_menu.html b/site/cds_rdm/templates/semantic-ui/cds_rdm/records/manage_menu.html index 71ca61cb..ae5d1fd9 100644 --- a/site/cds_rdm/templates/semantic-ui/cds_rdm/records/manage_menu.html +++ b/site/cds_rdm/templates/semantic-ui/cds_rdm/records/manage_menu.html @@ -8,4 +8,4 @@ data-allowed-resource-types='{{ config.CLC_SYNC_ALLOWED_RESOURCE_TYPES | tojson }}' data-clc-sync-entry='{{ clc_sync_entry | tojson | safe }}' data-additional-permissions='{{ additional_permissions | tojson }}' -data-ep-approval='{{ ep_approval_state | tojson | safe }}' +data-committee-approval='{{ committee_approval_state | tojson | safe }}' diff --git a/site/pyproject.toml b/site/pyproject.toml index 1c1e5c6b..3f0e3253 100644 --- a/site/pyproject.toml +++ b/site/pyproject.toml @@ -18,7 +18,7 @@ harvester_report = "cds_rdm.administration.harvester_reports:create_harvester_re [project.entry-points."invenio_base.api_blueprints"] clc_sync = "cds_rdm.views:create_cds_clc_sync_bp" -ep_approval = "cds_rdm.requests.views:create_ep_approval_bp" +committee_approval = "cds_rdm.requests.views:create_committee_approval_bp" [project.entry-points."invenio_celery.tasks"] cds_rdm_tasks = "cds_rdm.tasks" @@ -34,7 +34,7 @@ process_inspire = "cds_rdm.inspire_harvester.jobs:ProcessInspireHarvesterJob" legacy = "cds_rdm.minters:legacy_recid_minter" [project.entry-points."invenio_requests.types"] -ep_approval = "cds_rdm.requests:EPApprovalRequest" +committee_approval = "cds_rdm.requests:CommitteeApprovalRequest" [project.entry-points."idutils.custom_schemes"] cdsrn = "cds_rdm.schemes:cds_report_number" diff --git a/site/tests/conftest.py b/site/tests/conftest.py index ef57c019..cdde8941 100644 --- a/site/tests/conftest.py +++ b/site/tests/conftest.py @@ -63,10 +63,10 @@ from cds_rdm.inspire_harvester.reader import InspireHTTPReader from cds_rdm.inspire_harvester.transformer import InspireJsonTransformer from cds_rdm.inspire_harvester.writer import InspireWriter -from cds_rdm.notifications.ep_approval import ( - EPApprovalAcceptNotificationBuilder, - EPApprovalDeclineNotificationBuilder, - EPApprovalSubmitNotificationBuilder, +from cds_rdm.notifications.committee_approval import ( + CommitteeApprovalAcceptNotificationBuilder, + CommitteeApprovalDeclineNotificationBuilder, + CommitteeApprovalSubmitNotificationBuilder, ) from cds_rdm.permissions import ( CDSCommunitiesPermissionPolicy, @@ -314,17 +314,17 @@ def app_config(app_config, mock_datacite_client, mock_crossref_client): ) app_config["NOTIFICATIONS_BUILDERS"] = { - EPApprovalSubmitNotificationBuilder.type: DummyNotificationBuilder, - EPApprovalAcceptNotificationBuilder.type: DummyNotificationBuilder, - EPApprovalDeclineNotificationBuilder.type: DummyNotificationBuilder, + CommitteeApprovalSubmitNotificationBuilder.type: DummyNotificationBuilder, + CommitteeApprovalAcceptNotificationBuilder.type: DummyNotificationBuilder, + CommitteeApprovalDeclineNotificationBuilder.type: DummyNotificationBuilder, CommentRequestEventCreateNotificationBuilder.type: DummyNotificationBuilder, CommentRequestEventReplyNotificationBuilder.type: DummyNotificationBuilder, } - # EP Approval communities — static dummy UUIDs for config-lookup tests. + # Committee Approval communities — static dummy UUIDs for config-lookup tests. # Tests that run the full accept action add their real community UUID at - # fixture time by mutating this dict directly (see ep_enrolled_community fixture). - app_config["CDS_EP_APPROVAL_COMMUNITIES"] = {} + # fixture time by mutating this dict directly (see committee_enrolled_community fixture). + app_config["CDS_COMMITTEE_APPROVAL_COMMUNITIES"] = {} app_config["RDM_RECORDS_SERVICE_COMPONENTS"] = [ CommitteeApprovalComponent, @@ -1659,21 +1659,21 @@ def _publish_record_in_community(identity, record_data, community, service): @pytest.fixture() -def ep_enrolled_community(community_service, running_app): - """Community enrolled in CDS_EP_APPROVAL_COMMUNITIES.""" +def committee_enrolled_community(community_service, running_app): + """Community enrolled in CDS_COMMITTEE_APPROVAL_COMMUNITIES.""" community_data = { "access": { "visibility": "public", "members_visibility": "public", "record_submission_policy": "open", }, - "slug": "ep-enrolled", - "metadata": {"title": "EP Enrolled Community"}, + "slug": "committee-enrolled", + "metadata": {"title": "Committee Enrolled Community"}, } community = community_service.create(system_identity, community_data) Community.index.refresh() - current_app.config["CDS_EP_APPROVAL_COMMUNITIES"][str(community.id)] = { - "label": "EP approval", + current_app.config["CDS_COMMITTEE_APPROVAL_COMMUNITIES"][str(community.id)] = { + "label": "Committee approval", "referee_group": "cds-ph-ep-publication", "report_number": {"prefix": "CERN-EP", "include_year": True, "counter_digits": 3}, } @@ -1681,42 +1681,42 @@ def ep_enrolled_community(community_service, running_app): @pytest.fixture() -def ep_non_enrolled_community(community_service, running_app): - """Community NOT enrolled in CDS_EP_APPROVAL_COMMUNITIES.""" +def committee_non_enrolled_community(community_service, running_app): + """Community NOT enrolled in CDS_COMMITTEE_APPROVAL_COMMUNITIES.""" community_data = { "access": { "visibility": "public", "members_visibility": "public", "record_submission_policy": "open", }, - "slug": "ep-non-enrolled", - "metadata": {"title": "Non-EP Community"}, + "slug": "committee-non-enrolled", + "metadata": {"title": "Non-Committee Community"}, } community = community_service.create(system_identity, community_data) Community.index.refresh() - # Deliberately not added to CDS_EP_APPROVAL_COMMUNITIES. + # Deliberately not added to CDS_COMMITTEE_APPROVAL_COMMUNITIES. return community._record @pytest.fixture() def record_in_enrolled_community( - minimal_restricted_record, uploader, ep_enrolled_community, running_app + minimal_restricted_record, uploader, committee_enrolled_community, running_app ): - """Published record that belongs to an EP-enrolled community.""" + """Published record that belongs to a committee-enrolled community.""" service = current_rdm_records.records_service return _publish_record_in_community( - uploader.identity, minimal_restricted_record, ep_enrolled_community, service + uploader.identity, minimal_restricted_record, committee_enrolled_community, service ) @pytest.fixture() def record_in_non_enrolled_community( - minimal_restricted_record, uploader, ep_non_enrolled_community, running_app + minimal_restricted_record, uploader, committee_non_enrolled_community, running_app ): - """Published record that belongs to a community NOT enrolled in EP approval.""" + """Published record that belongs to a community NOT enrolled in committee approval.""" service = current_rdm_records.records_service return _publish_record_in_community( - uploader.identity, minimal_restricted_record, ep_non_enrolled_community, service + uploader.identity, minimal_restricted_record, committee_non_enrolled_community, service ) diff --git a/site/tests/test_ep_approval.py b/site/tests/test_committee_approval.py similarity index 89% rename from site/tests/test_ep_approval.py rename to site/tests/test_committee_approval.py index bb2e1802..ce640721 100644 --- a/site/tests/test_ep_approval.py +++ b/site/tests/test_committee_approval.py @@ -5,7 +5,7 @@ # CDS-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. -"""Integration tests for the EP Approval workflow.""" +"""Integration tests for the Committee Approval workflow.""" from datetime import date @@ -24,7 +24,10 @@ ) from marshmallow import ValidationError -from cds_rdm.requests.ep_approval import APPRN_PID_TYPE, EPApprovalAcceptAction +from cds_rdm.requests.committee_approval import ( + APPRN_PID_TYPE, + CommitteeApprovalAcceptAction, +) from cds_rdm.schemes import is_approval_report_number # --------------------------------------------------------------------------- @@ -98,7 +101,7 @@ def ep_referee(UserFixture, ep_referee_group, app, db): @pytest.fixture() -def community_manager(UserFixture, ep_enrolled_community, app, db): +def community_manager(UserFixture, committee_enrolled_community, app, db): """A user with community-manager role in the EP-enrolled community. The CommunityRoleNeed is injected directly into the identity because @@ -121,7 +124,7 @@ def community_manager(UserFixture, ep_enrolled_community, app, db): u.create(app, db) UserAggregate.index.refresh() u.identity.provides.add( - CommunityRoleNeed(str(ep_enrolled_community.id), "manager") + CommunityRoleNeed(str(committee_enrolled_community.id), "manager") ) return u @@ -159,11 +162,11 @@ def test_approval_rn_invalid_formats(): # --------------------------------------------------------------------------- -def test_ep_approval_request_type_is_registered(app): - """EPApprovalRequest must be discoverable via the request type registry.""" - request_type = current_request_type_registry.lookup("ep-approval", quiet=True) +def test_committee_approval_request_type_is_registered(app): + """CommitteeApprovalRequest must be discoverable via the request type registry.""" + request_type = current_request_type_registry.lookup("committee-approval", quiet=True) assert request_type is not None - assert request_type.type_id == "ep-approval" + assert request_type.type_id == "committee-approval" # --------------------------------------------------------------------------- @@ -173,13 +176,13 @@ def test_ep_approval_request_type_is_registered(app): def test_generate_report_number_first_of_year(app, db): """First number minted in a year produces seq=1.""" - action = EPApprovalAcceptAction.__new__(EPApprovalAcceptAction) + action = CommitteeApprovalAcceptAction.__new__(CommitteeApprovalAcceptAction) assert action._next_report_number(EP_RN_CONFIG) == f"CERN-EP-{YEAR}-001" def test_generate_report_number_sequential_increment(app, db): """Each call increments the sequence based on existing PIDs.""" - action = EPApprovalAcceptAction.__new__(EPApprovalAcceptAction) + action = CommitteeApprovalAcceptAction.__new__(CommitteeApprovalAcceptAction) prefix = f"CERN-EP-{YEAR}-" for seq in ("001", "002"): @@ -197,7 +200,7 @@ def test_generate_report_number_sequential_increment(app, db): def test_generate_report_number_independent_prefix_counters(app, db): """EP and TH patterns use independent counters — no cross-contamination.""" - action = EPApprovalAcceptAction.__new__(EPApprovalAcceptAction) + action = CommitteeApprovalAcceptAction.__new__(CommitteeApprovalAcceptAction) PersistentIdentifier.create( pid_type=APPRN_PID_TYPE, @@ -217,7 +220,7 @@ def test_generate_report_number_independent_prefix_counters(app, db): # --------------------------------------------------------------------------- -def test_ep_approval_submit_accept_assigns_report_number( +def test_committee_approval_submit_accept_assigns_report_number( record_in_enrolled_community, community_manager, ep_referee, @@ -226,7 +229,7 @@ def test_ep_approval_submit_accept_assigns_report_number( db, ): """Submit → accept: report number is auto-generated and stored on the request.""" - request_type = current_request_type_registry.lookup("ep-approval") + request_type = current_request_type_registry.lookup("committee-approval") request = current_requests_service.create( identity=community_manager.identity, @@ -255,18 +258,18 @@ def test_ep_approval_submit_accept_assigns_report_number( assert str(pid.object_uuid) == str(record_in_enrolled_community._record.id) -def test_ep_approval_second_request_increments_sequence( +def test_committee_approval_second_request_increments_sequence( record_in_enrolled_community, community_manager, ep_referee, ep_request_payload, - ep_enrolled_community, + committee_enrolled_community, minimal_restricted_record, app, db, ): """A second accepted request gets the next sequential report number.""" - request_type = current_request_type_registry.lookup("ep-approval") + request_type = current_request_type_registry.lookup("committee-approval") r1 = current_requests_service.create( identity=community_manager.identity, @@ -287,7 +290,7 @@ def test_ep_approval_second_request_increments_sequence( service = current_rdm_records.records_service record2 = _publish_record_in_community( - community_manager.identity, minimal_restricted_record, ep_enrolled_community, service + community_manager.identity, minimal_restricted_record, committee_enrolled_community, service ) r2 = current_requests_service.create( @@ -312,7 +315,7 @@ def test_ep_approval_second_request_increments_sequence( # --------------------------------------------------------------------------- -def test_ep_approval_submit_decline( +def test_committee_approval_submit_decline( record_in_enrolled_community, community_manager, ep_referee, @@ -321,7 +324,7 @@ def test_ep_approval_submit_decline( db, ): """Submit → decline: status is declined and no report number is issued.""" - request_type = current_request_type_registry.lookup("ep-approval") + request_type = current_request_type_registry.lookup("committee-approval") request = current_requests_service.create( identity=community_manager.identity, @@ -349,7 +352,7 @@ def test_ep_approval_submit_decline( # --------------------------------------------------------------------------- -def test_ep_approval_submit_raises_for_record_without_community( +def test_committee_approval_submit_raises_for_record_without_community( minimal_restricted_record, uploader, ep_referee_group, app, db ): """Submit raises ValidationError when the record belongs to no community.""" @@ -357,7 +360,7 @@ def test_ep_approval_submit_raises_for_record_without_community( draft = service.create(uploader.identity, minimal_restricted_record) record = service.publish(uploader.identity, id_=draft.id) - request_type = current_request_type_registry.lookup("ep-approval") + request_type = current_request_type_registry.lookup("committee-approval") with pytest.raises(ValidationError, match="not part of any community"): current_requests_service.create( identity=system_identity, @@ -375,13 +378,13 @@ def test_ep_approval_submit_raises_for_record_without_community( ) -def test_ep_approval_submit_raises_for_non_enrolled_community( +def test_committee_approval_submit_raises_for_non_enrolled_community( record_in_non_enrolled_community, uploader, ep_referee_group, app, db ): """Submit raises ValidationError when the record's community is not enrolled.""" - request_type = current_request_type_registry.lookup("ep-approval") + request_type = current_request_type_registry.lookup("committee-approval") with pytest.raises( - ValidationError, match="not enrolled in the EP approval workflow" + ValidationError, match="not enrolled in the committee approval workflow" ): current_requests_service.create( identity=system_identity, @@ -400,16 +403,16 @@ def test_ep_approval_submit_raises_for_non_enrolled_community( # --------------------------------------------------------------------------- -# CommitteeApprovalComponent — apprn identifier derived from parent ep_approval +# CommitteeApprovalComponent — apprn identifier derived from parent committee_approval # --------------------------------------------------------------------------- def test_apprn_identifier_derived_from_parent( minimal_restricted_record, uploader, app, db ): - """CommitteeApprovalComponent derives apprn from parent ep_approval. + """CommitteeApprovalComponent derives apprn from parent committee_approval. - EP approval state lives on the parent record (not the version CF). + Committee approval state lives on the parent record (not the version CF). The apprn identifier is only added to records where the parent carries ``source_internal_version`` — that marks the public approved copy. The internal draft and all its versions do NOT carry the apprn identifier. @@ -424,11 +427,11 @@ def test_apprn_identifier_derived_from_parent( report_number = f"CERN-EP-{YEAR}-001" - # Simulate accept: write ep_approval into permission_flags (no source_internal_version). + # Simulate accept: write committee_approval into permission_flags (no source_internal_version). pid_obj = PersistentIdentifier.get("recid", record.id) rec_obj = RDMRecord.get_record(pid_obj.object_uuid) pf = rec_obj.parent.get("permission_flags") or {} - pf["ep_approval"] = { + pf["committee_approval"] = { "reportnumber": report_number, "approved_internal_version": record.id, } @@ -448,7 +451,7 @@ def test_apprn_identifier_derived_from_parent( # Simulate public record: set source_internal_version in permission_flags. pf = rec_obj.parent.get("permission_flags") or {} - pf["ep_approval"] = { + pf["committee_approval"] = { "reportnumber": report_number, "source_internal_version": record.id, } @@ -472,17 +475,17 @@ def test_apprn_identifier_derived_from_parent( # --------------------------------------------------------------------------- -def test_ep_approval_submit_permissions( +def test_committee_approval_submit_permissions( record_in_enrolled_community, uploader, ep_referee_group, ep_request_payload, - ep_enrolled_community, + committee_enrolled_community, app, db, ): """Only community managers/owners of enrolled communities can submit.""" - request_type = current_request_type_registry.lookup("ep-approval") + request_type = current_request_type_registry.lookup("committee-approval") # Plain uploader (reader, not manager) — must be denied. with pytest.raises(PermissionDeniedError): @@ -499,7 +502,7 @@ def test_ep_approval_submit_permissions( # goes through invite→accept which is out of scope for this permission test. from invenio_communities.generators import CommunityRoleNeed - community_id = str(ep_enrolled_community.id) + community_id = str(committee_enrolled_community.id) uploader.identity.provides.add(CommunityRoleNeed(community_id, "manager")) # Now as manager the uploader must be allowed. @@ -535,7 +538,7 @@ def test_referee_grant_added_on_submit_removed_on_decline( COMMITTEE_APPROVAL_GRANT_PERMISSION, ) - request_type = current_request_type_registry.lookup("ep-approval") + request_type = current_request_type_registry.lookup("committee-approval") pid_obj = PersistentIdentifier.get( "recid", record_in_enrolled_community.id @@ -595,7 +598,7 @@ def test_referee_grant_retained_after_accept( COMMITTEE_APPROVAL_GRANT_PERMISSION, ) - request_type = current_request_type_registry.lookup("ep-approval") + request_type = current_request_type_registry.lookup("committee-approval") pid_obj = PersistentIdentifier.get( "recid", record_in_enrolled_community.id @@ -637,7 +640,7 @@ def test_referee_grant_scoped_to_submitted_version( """Referee can read the submitted version but not a new version created afterwards.""" from invenio_rdm_records.proxies import current_rdm_records - request_type = current_request_type_registry.lookup("ep-approval") + request_type = current_request_type_registry.lookup("committee-approval") service = current_rdm_records.records_service # Submit and accept the request for v1. diff --git a/templates/semantic-ui/invenio_notifications/ep-approval-request.submit.jinja b/templates/semantic-ui/invenio_notifications/committee-approval-request.submit.jinja similarity index 83% rename from templates/semantic-ui/invenio_notifications/ep-approval-request.submit.jinja rename to templates/semantic-ui/invenio_notifications/committee-approval-request.submit.jinja index d018f756..fbf826f1 100644 --- a/templates/semantic-ui/invenio_notifications/ep-approval-request.submit.jinja +++ b/templates/semantic-ui/invenio_notifications/committee-approval-request.submit.jinja @@ -1,8 +1,8 @@ -{% set ep_request = notification.context.request %} -{% set record = ep_request.topic %} -{% set request_id = ep_request.id %} +{% set committee_request = notification.context.request %} +{% set record = committee_request.topic %} +{% set request_id = committee_request.id %} {% set record_title = record.metadata.title %} -{% set report_number = ep_request.title | replace('EP approval for "', '') | replace('"', '') %} +{% set report_number = committee_request.title | replace('Committee approval for "', '') | replace('"', '') %} {% if recipient.data.profile is defined and recipient.data.profile.full_name %} {% set recipient_full_name = recipient.data.profile.full_name %} {% else %} @@ -23,7 +23,7 @@ %} {%- block subject -%} - [CDS] {{ _("Request for EP approval of '{record_title}'").format(record_title=record_title) }} + [CDS] {{ _("Request for committee approval of '{record_title}'").format(record_title=record_title) }} {%- endblock subject -%} {%- block html_body -%} @@ -34,7 +34,7 @@ - {{ _("A new EP approval request has been submitted for the following document:") }} + {{ _("A new committee approval request has been submitted for the following document:") }} @@ -66,7 +66,7 @@ {%- block plain_body -%} {{ _("Dear {recipient}").format(recipient=recipient_full_name) }}, -{{ _("A new EP approval request has been submitted for the following document:") }} +{{ _("A new committee approval request has been submitted for the following document:") }} {{ _("Title:") }} {{ record_title }} {{ _("Record:") }} {{ record_link }} @@ -86,7 +86,7 @@ CERN Document Server {{ config.SITE_UI_URL }} {%- block md_body -%} {{ _("Dear {recipient}").format(recipient=recipient_full_name) }}, -{{ _("A new EP approval request has been submitted for the following document:") }} +{{ _("A new committee approval request has been submitted for the following document:") }} **{{ _("Title:") }}** {{ record_title }} **{{ _("Record:") }}** {{ record_link }} diff --git a/templates/semantic-ui/invenio_requests/ep-approval/index.html b/templates/semantic-ui/invenio_requests/committee-approval/index.html similarity index 86% rename from templates/semantic-ui/invenio_requests/ep-approval/index.html rename to templates/semantic-ui/invenio_requests/committee-approval/index.html index 61caead6..0b108a52 100644 --- a/templates/semantic-ui/invenio_requests/ep-approval/index.html +++ b/templates/semantic-ui/invenio_requests/committee-approval/index.html @@ -8,8 +8,8 @@ #} {# - Renders the EP Approval request detail page. - Identical layout to community-submission, with the EP payload fields + Renders the Committee Approval request detail page. + Identical layout to community-submission, with the committee payload fields displayed below the standard request header. #} @@ -24,7 +24,7 @@ {% set p = invenio_request.get("payload", {}) %} {% if p %} -
+
{% if p.get("approved_report_number") %}
{{ _("Approved report number") }}
@@ -40,7 +40,7 @@ {% if p.get("rapid_approval") %} {{ _("Requested") }} {% else %} - {{ _("Not requested") }} + {{ _("Not requested") }} {% endif %}
@@ -51,7 +51,7 @@ {{ _("Yes") if p.get("cb_review_completed") else _("No") }} {% if p.get("cb_review_completed") and p.get("cb_process_type") %} - + ({{ p.cb_process_type }}) {% endif %} @@ -66,7 +66,7 @@ {{ _("Yes") if p.get("paper_signed") else _("No") }} {% if not p.get("paper_signed") and p.get("num_non_signers") %} - + {{ p.num_non_signers }} {{ _("non-signer(s)") }} {% endif %} @@ -80,7 +80,7 @@ {% if p.get("controversy") %} {{ _("Yes") }} {% else %} - + {{ _("No") }} {% endif %}