Skip to content

Commit 41bc15c

Browse files
committed
v14: make any thesis migration steps optional
1 parent 640bcba commit 41bc15c

4 files changed

Lines changed: 395 additions & 36 deletions

File tree

-36.4 KB
Binary file not shown.
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
# SPDX-FileCopyrightText: 2025 CERN.
2+
# SPDX-FileCopyrightText: 2026 Graz University of Technology.
3+
# SPDX-License-Identifier: MIT
4+
5+
"""Reference helpers for the "Thesis" / "Dissertation" resource type change (v14).
6+
7+
These functions are reference material, not a runnable script. They are here so
8+
you can copy and adapt the parts that fit your instance when carrying out the
9+
*optional* resource type change described in the v14 upgrade guide. Read them,
10+
lift what you need into your own `invenio shell` snippet or script, and always
11+
test against a copy of your data first.
12+
13+
Two independent operations are shown:
14+
15+
- `run_update_for_resource_type` rewrites every published record and unpublished
16+
draft with resource type publication-thesis to publication-dissertation (in
17+
metadata.resource_type and in any metadata.related_identifiers), going through
18+
the service layer so DataCite DOI metadata is re-registered and records are
19+
re-indexed.
20+
21+
- `run_update_doi_metadata_for_resource_type` re-registers the DataCite DOI
22+
metadata of every published record of a resource type, without changing the
23+
record. Use it when you keep your own resource type id but changed its
24+
props.datacite_general (e.g. to "Dissertation") in your vocabulary: the DataCite
25+
serializer reads props.datacite_general live from the vocabulary, so only a DOI
26+
update is needed.
27+
28+
The resource type rewrite has been tested for the following scenarios:
29+
1. Draft with resource type publication-thesis
30+
2. Record with resource type publication-thesis with a DOI and no draft
31+
3. Record with resource type publication-thesis with no DOI and an existing draft
32+
4. Records with multiple versions
33+
"""
34+
35+
from click import secho
36+
from invenio_access.permissions import system_identity
37+
from invenio_db import db
38+
from invenio_drafts_resources.resources.records.errors import DraftNotCreatedError
39+
from invenio_rdm_records.proxies import current_rdm_records_service as records_service
40+
from invenio_rdm_records.services.errors import RecordDeletedException
41+
from invenio_search.api import RecordsSearchV2
42+
43+
44+
def run_upgrade(migrate_record, migrate_draft):
45+
"""Run upgrade on selected records and drafts.
46+
47+
Args:
48+
migrate_record (callable): Function to migrate a record.
49+
migrate_draft (callable): Function to migrate a draft.
50+
"""
51+
errored_record_ids = []
52+
errored_draft_ids = []
53+
54+
# Handle published records
55+
published_records = (
56+
RecordsSearchV2(index=records_service.record_cls.index._name)
57+
.filter(
58+
"query_string",
59+
query="metadata.resource_type.id:publication-thesis OR metadata.related_identifiers.resource_type.id:publication-thesis",
60+
)
61+
.source(["id"])
62+
.scan()
63+
) # Only need to fetch the record IDs to make the query faster
64+
# Convert the search results to a list to avoid keeping the scroll context open, as it errors out after 15 minutes
65+
published_record_ids = [result["id"] for result in published_records]
66+
for record_id in published_record_ids:
67+
try:
68+
migrate_record(record_id)
69+
except Exception as error:
70+
secho(f"> Error {repr(error)}", fg="red")
71+
secho(f"Record {record_id} failed to update", fg="red")
72+
errored_record_ids.append((record_id, error))
73+
74+
# Handle draft records
75+
draft_records = (
76+
RecordsSearchV2(index=records_service.draft_cls.index._name)
77+
.filter("term", has_draft=False)
78+
.filter(
79+
"query_string",
80+
query="metadata.resource_type.id:publication-thesis OR metadata.related_identifiers.resource_type.id:publication-thesis",
81+
)
82+
.source(["id"])
83+
.scan()
84+
)
85+
# Convert the search results to a list to avoid keeping the scroll context open, as it errors out after 15 minutes
86+
draft_record_ids = [result["id"] for result in draft_records]
87+
for draft_id in draft_record_ids:
88+
try:
89+
migrate_draft(draft_id)
90+
except Exception as error:
91+
secho(f"> Error {repr(error)}", fg="red")
92+
secho(f"Draft {draft_id} failed to update", fg="red")
93+
errored_draft_ids.append((draft_id, error))
94+
95+
if len(errored_record_ids) > 0:
96+
secho(f"Errored record IDs: {errored_record_ids}", fg="red")
97+
else:
98+
secho("records have been updated successfully", fg="green")
99+
100+
if len(errored_draft_ids) > 0:
101+
secho(f"Errored draft IDs: {errored_draft_ids}", fg="red")
102+
else:
103+
secho("drafts have been updated successfully", fg="green")
104+
105+
106+
def run_update_for_resource_type():
107+
"""Run update for resource type."""
108+
109+
def migrate_resource_type_in_record(record_id):
110+
"""
111+
Update resource type from publication-thesis to publication-dissertation.
112+
113+
We go through the service layer to automatically trigger the DOI update and re-indexing.
114+
"""
115+
secho(f"Updating resource type for record {record_id}", fg="yellow")
116+
record = records_service.read(system_identity, record_id, include_deleted=True)
117+
if record.data["metadata"]["resource_type"][
118+
"id"
119+
] != "publication-thesis" and not any(
120+
related_identifier.get("resource_type", {}).get("id")
121+
== "publication-thesis"
122+
for related_identifier in record.data["metadata"].get(
123+
"related_identifiers", []
124+
)
125+
):
126+
secho(
127+
f"Skipping record <{record.id}> because it doesn't have resource-type 'publication-thesis'!",
128+
fg="yellow",
129+
)
130+
return
131+
132+
try:
133+
draft = records_service.read_draft(system_identity, record.id)
134+
# Step 1: Update the resource type in the record via low-level API
135+
# We need to make sure we don't publish the record with different metadata
136+
secho(
137+
f"Record <{record.id}> has an existing draft <{draft.id}>! Updating record via low-level API.",
138+
fg="yellow",
139+
)
140+
# Update the record directly without affecting the draft
141+
if (
142+
record._record["metadata"]["resource_type"]["id"]
143+
== "publication-thesis"
144+
):
145+
record._record["metadata"]["resource_type"][
146+
"id"
147+
] = "publication-dissertation"
148+
for related_identifier in record._record["metadata"].get(
149+
"related_identifiers", []
150+
):
151+
if (
152+
related_identifier.get("resource_type", {}).get("id")
153+
== "publication-thesis"
154+
):
155+
related_identifier["resource_type"][
156+
"id"
157+
] = "publication-dissertation"
158+
# Save the record changes and reindex
159+
secho(
160+
f"Record <{record.id}> has been updated... committing changes.",
161+
fg="green",
162+
)
163+
record._record.commit()
164+
# Step 2: Update the resource type in the draft
165+
if draft._record["metadata"]["resource_type"]["id"] == "publication-thesis":
166+
draft._record["metadata"]["resource_type"][
167+
"id"
168+
] = "publication-dissertation"
169+
for related_identifier in draft._record["metadata"].get(
170+
"related_identifiers", []
171+
):
172+
if (
173+
related_identifier.get("resource_type", {}).get("id")
174+
== "publication-thesis"
175+
):
176+
related_identifier["resource_type"][
177+
"id"
178+
] = "publication-dissertation"
179+
# After updating the record, update the draft's fork_version_id to match the record's new version_id, to avoid conflicts when publishing
180+
draft._record.fork_version_id = record._record.revision_id
181+
draft._record.commit()
182+
# Commit the changes for both the record and the draft in one transaction
183+
db.session.commit()
184+
records_service.indexer.index(record._record)
185+
records_service.draft_indexer.index(draft._record)
186+
secho(f"Draft <{draft.id}> has been updated successfully.", fg="green")
187+
# Update DOI metadata if record has DOI
188+
if (record._record.get("pids") or {}).get("doi"):
189+
records_service.pids.register_or_update(
190+
system_identity, record.id, "doi", parent=False
191+
)
192+
secho(
193+
f"DOI metadata for record {record.id} has been updated successfully.",
194+
fg="green",
195+
)
196+
except DraftNotCreatedError:
197+
# If the draft didn't exist, we simply edit and publish the record
198+
draft = records_service.edit(system_identity, record.id)
199+
if draft.data["metadata"]["resource_type"]["id"] == "publication-thesis":
200+
draft.data["metadata"]["resource_type"][
201+
"id"
202+
] = "publication-dissertation"
203+
for related_identifier in draft.data["metadata"].get(
204+
"related_identifiers", []
205+
):
206+
if (
207+
related_identifier.get("resource_type", {}).get("id")
208+
== "publication-thesis"
209+
):
210+
related_identifier["resource_type"][
211+
"id"
212+
] = "publication-dissertation"
213+
updated_draft = records_service.update_draft(
214+
system_identity, draft.id, draft.data
215+
)
216+
record = records_service.publish(system_identity, updated_draft.id)
217+
except RecordDeletedException:
218+
# If the draft was deleted, we ignore it
219+
# In the future, we should add include_deleted to read_draft and update the draft metadata in these cases
220+
secho(f"Draft <{draft.id}> has been deleted, skipping...", fg="yellow")
221+
222+
secho(f"Record <{record.id}> has been updated successfully.", fg="green")
223+
224+
def migrate_resource_type_in_draft(draft_id):
225+
"""
226+
Update resource type from publication-thesis to publication-dissertation.
227+
228+
We go through the service layer to automatically trigger the DOI update and re-indexing.
229+
"""
230+
secho(f"Updating resource type for draft {draft_id}", fg="yellow")
231+
draft = records_service.edit(system_identity, draft_id)
232+
if draft.data["metadata"]["resource_type"][
233+
"id"
234+
] != "publication-thesis" and not any(
235+
related_identifier.get("resource_type", {}).get("id")
236+
== "publication-thesis"
237+
for related_identifier in draft.data["metadata"].get(
238+
"related_identifiers", []
239+
)
240+
):
241+
secho(
242+
f"Skipping draft <{draft.id}> because it doesn't have resource-type 'publication-thesis'!",
243+
fg="yellow",
244+
)
245+
return
246+
247+
if draft.data["metadata"]["resource_type"]["id"] == "publication-thesis":
248+
draft.data["metadata"]["resource_type"]["id"] = "publication-dissertation"
249+
for related_identifier in draft.data["metadata"].get("related_identifiers", []):
250+
if (
251+
related_identifier.get("resource_type", {}).get("id")
252+
== "publication-thesis"
253+
):
254+
related_identifier["resource_type"]["id"] = "publication-dissertation"
255+
updated_draft = records_service.update_draft(
256+
system_identity, draft.id, draft.data
257+
)
258+
secho(f"Draft <{updated_draft.id}> has been updated successfully.", fg="green")
259+
260+
secho("Resource type update has started.", fg="green")
261+
262+
run_upgrade(
263+
migrate_resource_type_in_record,
264+
migrate_resource_type_in_draft,
265+
)
266+
267+
secho("Resource type update has finished.", fg="green")
268+
269+
270+
def run_update_doi_metadata_for_resource_type(resource_type_id="publication-thesis"):
271+
"""Re-register DataCite DOI metadata for published records of a resource type.
272+
273+
Use this after changing the resource type's props.datacite_general in your
274+
vocabulary and reloading the fixture. The DataCite serializer reads
275+
props.datacite_general live from the vocabulary, so re-registering each DOI
276+
pushes the new value to DataCite; the records are not otherwise changed. Only
277+
version DOIs are refreshed here (pass parent=True to also update the concept
278+
DOI).
279+
"""
280+
secho(
281+
f"DOI metadata update for resource type {resource_type_id} has started.",
282+
fg="green",
283+
)
284+
285+
errored_record_ids = []
286+
287+
published_records = (
288+
RecordsSearchV2(index=records_service.record_cls.index._name)
289+
.filter(
290+
"query_string",
291+
query=f"metadata.resource_type.id:{resource_type_id} OR metadata.related_identifiers.resource_type.id:{resource_type_id}",
292+
)
293+
.source(["id"])
294+
.scan()
295+
)
296+
# Convert the search results to a list to avoid keeping the scroll context open, as it errors out after 15 minutes
297+
published_record_ids = [result["id"] for result in published_records]
298+
for record_id in published_record_ids:
299+
try:
300+
record = records_service.read(system_identity, record_id)
301+
# Only records that already have a DOI can have their metadata updated
302+
if not (record._record.get("pids") or {}).get("doi"):
303+
secho(
304+
f"Skipping record <{record_id}> because it has no DOI!",
305+
fg="yellow",
306+
)
307+
continue
308+
records_service.pids.register_or_update(
309+
system_identity, record_id, "doi", parent=False
310+
)
311+
secho(
312+
f"DOI metadata for record {record_id} has been updated successfully.",
313+
fg="green",
314+
)
315+
except Exception as error:
316+
secho(f"> Error {repr(error)}", fg="red")
317+
secho(f"Record {record_id} failed to update", fg="red")
318+
errored_record_ids.append((record_id, error))
319+
320+
if len(errored_record_ids) > 0:
321+
secho(f"Errored record IDs: {errored_record_ids}", fg="red")
322+
else:
323+
secho("DOI metadata has been updated successfully", fg="green")

0 commit comments

Comments
 (0)