-
Notifications
You must be signed in to change notification settings - Fork 27
fix(ep_approval): Fix error handling, add test for single record flow #917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,4 +84,8 @@ node_modules | |
|
|
||
| # local tmp folder | ||
| tmp | ||
| scripts | ||
| scripts | ||
|
|
||
| # nvm, python | ||
| .nvmrc | ||
| .python-version | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,8 +9,7 @@ | |
| """CDS RDM service components.""" | ||
|
|
||
| from flask import current_app | ||
| from flask_principal import ActionNeed | ||
| from invenio_access import Permission | ||
| from invenio_access.permissions import system_user_id | ||
| from invenio_communities.proxies import current_communities | ||
| from invenio_drafts_resources.services.records.components import ServiceComponent | ||
| from invenio_i18n import gettext as _ | ||
|
|
@@ -132,22 +131,19 @@ def publish(self, identity, draft=None, record=None, **kwargs): | |
| class CommitteeApprovalComponent(ServiceComponent): | ||
| """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 committee approval report-number pattern | ||
| (e.g. CERN-EP-*). | ||
| 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). | ||
| 1. Blocks everyone except the system process from adding/modifying/deleting | ||
| ``apprn`` scheme identifiers — including admins via the UI. | ||
| 2. Regenerates the ``apprn`` metadata identifier from parent committee_approval | ||
| only when ``source_internal_version`` is set on the parent. This covers | ||
| two cases: | ||
| - Public approved copy in the two-record flow (views.py sets | ||
| ``source_internal_version`` to the internal record's recid). | ||
| - Single-record migration case (migrate_cdsrn_to_apprn.py sets | ||
| ``source_internal_version`` to the record's own recid). | ||
| The internal record in the normal flow never has ``source_internal_version`` | ||
| on its parent, so it never carries the apprn identifier. | ||
| """ | ||
|
|
||
| def _is_privileged(self, identity): | ||
| """Return True if the identity is system or has superuser access.""" | ||
| return identity.id == "system" or Permission( | ||
| ActionNeed("superuser-access") | ||
| ).allows(identity) | ||
|
|
||
| def _committee_approval_prefixes(self): | ||
| """Return the set of fixed prefixes from all configured committee communities. | ||
|
|
||
|
|
@@ -162,9 +158,9 @@ def _committee_approval_prefixes(self): | |
| prefixes.add(prefix) | ||
| return prefixes | ||
|
|
||
| def _validate_identifier_changes(self, identity, data, record): | ||
| """Raise ValidationError if the user is modifying protected identifiers.""" | ||
| if self._is_privileged(identity): | ||
| def _validate_identifier_changes(self, identity, data, record, errors): | ||
| """Raise ValidationError if a non-system identity modifies apprn.""" | ||
| if identity.id == system_user_id: | ||
| return | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this raise instead of return to keep inline with the docstring?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but we return if the user is system. We raise for non-system users as per docstring, do I miss something?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I mis-read, sorry!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wasn't a test added to test the fix of the initial bug?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I missed adding it before, we should add it. |
||
|
|
||
| incoming_identifiers = (data.get("metadata") or {}).get("identifiers", []) | ||
|
|
@@ -179,7 +175,7 @@ def _validate_identifier_changes(self, identity, data, record): | |
| } | ||
| if incoming_apprn != stored_apprn: | ||
| error_msg = _( | ||
| "The 'apprn' identifier is system-managed and cannot be " | ||
| "The approval report number is system-managed and cannot be " | ||
| "added, modified, or removed manually." | ||
| ) | ||
|
|
||
|
|
@@ -216,7 +212,7 @@ def _validate_identifier_changes(self, identity, data, record): | |
| "field": f"metadata.identifiers.{index}.identifier", | ||
| "messages": [ | ||
| _( | ||
| f"The value '{val}' matches an EP approval " | ||
| f"The value '{val}' matches an approval " | ||
| "report number pattern and cannot be used as " | ||
| "a CDS report number." | ||
| ) | ||
|
|
@@ -226,36 +222,59 @@ def _validate_identifier_changes(self, identity, data, record): | |
| if errors: | ||
| raise ValidationErrorWithMessageAsList(errors) | ||
|
|
||
| def _should_sync_apprn(self, record, committee_approval): | ||
| """Return True if apprn should be synced with parent committee_approval.""" | ||
| reportnumber = committee_approval.get("reportnumber") | ||
| source_internal = committee_approval.get("source_internal_version") | ||
| if reportnumber and source_internal: | ||
| return True | ||
| return False | ||
|
|
||
| def _regenerate_apprn_identifier(self, record, data): | ||
| """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 | ||
| parent by the ``publish_public_record`` view. | ||
| committee_approval.reportnumber is the source of truth (a list of strings). | ||
| The metadata apprn entries are derived from it exactly — no other apprn | ||
| entries are preserved. | ||
| """ | ||
| ea = ( | ||
| committee_approval = ( | ||
| (record.parent.get("permission_flags") if record.parent else None) or {} | ||
| ).get("committee_approval") or {} | ||
| reportnumber = ea.get("reportnumber") | ||
| source_internal = ea.get("source_internal_version") | ||
| identifiers = [ | ||
| i | ||
| for i in (data.get("metadata") or {}).get("identifiers", []) | ||
| if i.get("scheme") != "apprn" | ||
| ] | ||
| if reportnumber and source_internal: | ||
| identifiers = [ | ||
| {"scheme": "apprn", "identifier": reportnumber} | ||
| ] + identifiers | ||
| data.setdefault("metadata", {})["identifiers"] = identifiers | ||
|
|
||
| existing_identifiers = (data.get("metadata") or {}).get("identifiers", []) | ||
| app_rn = [i for i in existing_identifiers if i.get("scheme") == "apprn"] | ||
| not_apprn = [i for i in existing_identifiers if i.get("scheme") != "apprn"] | ||
|
|
||
| if self._should_sync_apprn(record, committee_approval): | ||
| reportnumbers = committee_approval.get("reportnumber") or [] | ||
| apprn = [{"scheme": "apprn", "identifier": rn} for rn in reportnumbers] | ||
| new_identifiers = apprn + not_apprn | ||
| else: | ||
| # check if there are any remaining apprn identifiers and raise a validation error if they exist | ||
| if app_rn: | ||
| errors = [ | ||
| { | ||
| "field": "metadata.identifiers", | ||
| "messages": [ | ||
| _( | ||
| "The approval report number is system-managed and cannot be " | ||
| "added, modified, or removed manually.", | ||
| ) | ||
| ], | ||
| } | ||
| ] | ||
| raise ValidationErrorWithMessageAsList(errors) | ||
| new_identifiers = not_apprn | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if the record does not have a
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shall we raise a validation error or warning when for some reason the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes |
||
|
|
||
| data.setdefault("metadata", {})["identifiers"] = new_identifiers | ||
|
|
||
| def create(self, identity, data=None, record=None, errors=None, **kwargs): | ||
| """Validate apprn identifier on draft creation.""" | ||
| self._validate_identifier_changes(identity, data, record) | ||
| self._validate_identifier_changes(identity, data, record, errors) | ||
|
|
||
| def update_draft(self, identity, data=None, record=None, errors=None, **kwargs): | ||
| """Validate and regenerate apprn identifier on draft update.""" | ||
| self._validate_identifier_changes(identity, data, record) | ||
| self._validate_identifier_changes(identity, data, record, errors) | ||
| self._regenerate_apprn_identifier(record, data) | ||
|
|
||
| def publish(self, identity, draft=None, record=None, **kwargs): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -244,7 +244,7 @@ def publish_public_record(pid_value): | |
| ) | ||
| if cern_scientific_community_id: | ||
| try: | ||
| current_record_communities_service.add( | ||
| _, errors = current_record_communities_service.add( | ||
| system_identity, | ||
| new_record.data["id"], | ||
| data={ | ||
|
|
@@ -257,7 +257,7 @@ def publish_public_record(pid_value): | |
| "content": ( | ||
| f"This inclusion request was automatically " | ||
| f"generated when publishing the EP-approved " | ||
| f"public record for {report_number}. The " | ||
| f"public record for {', '.join(report_number)}. The " | ||
| f"document has been reviewed and approved by " | ||
| f"the EP Publication Committee." | ||
| ) | ||
|
|
@@ -283,6 +283,7 @@ def publish_public_record(pid_value): | |
| pf["committee_approval"] = { | ||
| **ea, | ||
| "approved_public_version": new_record_id, | ||
| "source_public_version": src_id, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we have documentation describing these keys?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will add some documentation in our internal docs |
||
| } | ||
| src_rec_obj.parent["permission_flags"] = pf | ||
| src_rec_obj.parent.commit() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this leftover?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if we are still using that, but I upgraded locally to 3.14 and upgraded the information there too.