labimotion 2.2.0.rc20 → 2.2.0.rc21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/labimotion/apis/labimotion_doi_api.rb +22 -10
- data/lib/labimotion/apis/labimotion_template_browse_api.rb +11 -1
- data/lib/labimotion/models/concerns/template_doi.rb +20 -0
- data/lib/labimotion/usecases/build_template_doi_xml.rb +19 -12
- data/lib/labimotion/usecases/release_template_doi.rb +43 -17
- data/lib/labimotion/usecases/update_template_publication_metadata.rb +25 -1
- data/lib/labimotion/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 006f7a0d5c1bc994e1ed1be862881e579b83a91844e176858571471a4225a6e5
|
|
4
|
+
data.tar.gz: 9d40a31313cc7717bd0a9cb971e457bc6c2ad93a3818a3ad305dec05ad9bc406
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 399cb835f06e3cc7e30a8c59f3e64ae7738da938a7bb4a68e3ffd9c331854e36c84743b90a07769407c910d2b49042996734d2572517932a97869a18f0e6174b
|
|
7
|
+
data.tar.gz: 8ce94d81213aba4205aff04328c3eba89ca095b84301ddf7eee22497e0964635a0998735175f47fa00642f4d47d481e9ef0aa95bb1b04357705a1c1ad21c3c6b
|
|
@@ -7,6 +7,8 @@ module Labimotion
|
|
|
7
7
|
# rubocop:disable Metrics/BlockLength
|
|
8
8
|
# Grape route and helper DSL blocks are necessarily large.
|
|
9
9
|
KLASS_TYPES = Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE.keys.freeze
|
|
10
|
+
# Publication fields exposed on the public (anonymous) DOI endpoints.
|
|
11
|
+
PUBLIC_PUBLICATION_KEYS = %w[title description authors contributor license].freeze
|
|
10
12
|
|
|
11
13
|
helpers do
|
|
12
14
|
def resolve_klass!(type, id)
|
|
@@ -41,25 +43,36 @@ module Labimotion
|
|
|
41
43
|
)
|
|
42
44
|
end
|
|
43
45
|
|
|
44
|
-
# Public, read-only view of one released DOI version
|
|
45
|
-
|
|
46
|
+
# Public, read-only view of one released DOI version, carrying the
|
|
47
|
+
# publication metadata that version was released with.
|
|
48
|
+
def released_doi_version(record, doi)
|
|
46
49
|
full = doi.full_doi
|
|
47
50
|
{
|
|
48
51
|
doi: full,
|
|
49
52
|
doi_url: "https://dx.doi.org/#{full}",
|
|
50
53
|
version: ::Doi.labimotion_doi_version(doi),
|
|
51
|
-
minted_at: doi.minted_at
|
|
54
|
+
minted_at: doi.minted_at,
|
|
55
|
+
publication: released_publication(record, doi)
|
|
52
56
|
}
|
|
53
57
|
end
|
|
54
58
|
|
|
59
|
+
# The snapshot taken when this version was released. DOIs released before
|
|
60
|
+
# snapshots were kept have none, and fall back to the template's current
|
|
61
|
+
# publication — the best available answer for them.
|
|
62
|
+
def released_publication(record, doi)
|
|
63
|
+
publication = ::Doi.labimotion_doi_publication(doi).presence ||
|
|
64
|
+
(record.properties_template || {})['publication']
|
|
65
|
+
publication.is_a?(Hash) ? publication.slice(*PUBLIC_PUBLICATION_KEYS) : {}
|
|
66
|
+
end
|
|
67
|
+
|
|
55
68
|
# Public payload for a template that has at least one released DOI: the
|
|
56
69
|
# latest released DOI promoted to the top level, plus its publication
|
|
57
|
-
# metadata, hub deep-link and every released version.
|
|
58
|
-
#
|
|
70
|
+
# metadata, hub deep-link and every released version. Each version brings
|
|
71
|
+
# its own publication, so the top-level one is the latest release's rather
|
|
72
|
+
# than the template's work-in-progress. `record_id` matches the hub grid's
|
|
73
|
+
# row id (identifier || uuid) so the frontend can merge it.
|
|
59
74
|
def released_template_doi(record, dois)
|
|
60
|
-
versions = dois.sort_by(&:id).map { |doi| released_doi_version(doi) }
|
|
61
|
-
publication = (record.properties_template || {})['publication']
|
|
62
|
-
publication = publication.is_a?(Hash) ? publication.slice('title', 'description', 'authors', 'license') : {}
|
|
75
|
+
versions = dois.sort_by(&:id).map { |doi| released_doi_version(record, doi) }
|
|
63
76
|
versions.last.merge(
|
|
64
77
|
record_id: record.try(:identifier).presence || record.try(:uuid).presence || record.id,
|
|
65
78
|
identifier: record.try(:identifier),
|
|
@@ -68,7 +81,6 @@ module Labimotion
|
|
|
68
81
|
template_version: record.try(:version),
|
|
69
82
|
next_doi_version: ::Doi.labimotion_version_segment(record),
|
|
70
83
|
template_url: Labimotion::TemplateDoiHelpers.template_url(record),
|
|
71
|
-
publication: publication,
|
|
72
84
|
versions: versions
|
|
73
85
|
)
|
|
74
86
|
end
|
|
@@ -125,7 +137,7 @@ module Labimotion
|
|
|
125
137
|
version: ::Doi.labimotion_doi_version(doi),
|
|
126
138
|
full_doi: doi.full_doi,
|
|
127
139
|
minted: doi.minted == true,
|
|
128
|
-
xml: Labimotion::BuildTemplateDoiXml.new(record, doi
|
|
140
|
+
xml: Labimotion::BuildTemplateDoiXml.new(record, doi).call
|
|
129
141
|
}
|
|
130
142
|
end
|
|
131
143
|
{ versions: versions }
|
|
@@ -9,6 +9,8 @@ module Labimotion
|
|
|
9
9
|
class LabimotionTemplateBrowseAPI < Grape::API
|
|
10
10
|
KLASS_BY_TYPE = Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE
|
|
11
11
|
KLASS_TYPES = KLASS_BY_TYPE.keys.freeze
|
|
12
|
+
# Publication fields exposed alongside a template / its released DOIs.
|
|
13
|
+
PUBLICATION_KEYS = %w[title description authors contributor license].freeze
|
|
12
14
|
# type => [revision model, foreign key to the klass]
|
|
13
15
|
REVISION_BY_TYPE = {
|
|
14
16
|
'element' => [::Labimotion::ElementKlassesRevision, :element_klass_id],
|
|
@@ -76,7 +78,14 @@ module Labimotion
|
|
|
76
78
|
# The template's DOI publication metadata (shown alongside the DOI).
|
|
77
79
|
def template_publication(record)
|
|
78
80
|
pub = (record.properties_template || {})['publication']
|
|
79
|
-
pub.is_a?(Hash) ? pub.slice(
|
|
81
|
+
pub.is_a?(Hash) ? pub.slice(*PUBLICATION_KEYS) : {}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The publication a released DOI was published with. Falls back to the
|
|
85
|
+
# template's current metadata for DOIs released before snapshots were kept.
|
|
86
|
+
def released_publication(record, doi)
|
|
87
|
+
pub = ::Doi.labimotion_doi_publication(doi)
|
|
88
|
+
pub.is_a?(Hash) && pub.any? ? pub.slice(*PUBLICATION_KEYS) : template_publication(record)
|
|
80
89
|
end
|
|
81
90
|
|
|
82
91
|
# Released (minted) DOIs of the template, each tagged with its DOI version
|
|
@@ -88,6 +97,7 @@ module Labimotion
|
|
|
88
97
|
full = doi.full_doi
|
|
89
98
|
{ version: ::Doi.labimotion_doi_version(doi), doi: full,
|
|
90
99
|
doi_url: "https://dx.doi.org/#{full}", minted_at: doi.minted_at,
|
|
100
|
+
publication: released_publication(record, doi),
|
|
91
101
|
xml: Labimotion::BuildTemplateDoiXml.new(record, doi).call }
|
|
92
102
|
end
|
|
93
103
|
end
|
|
@@ -82,6 +82,26 @@ module Labimotion
|
|
|
82
82
|
{ 'labimotion' => { 'doi_version' => labimotion_version_segment(record, version) } }
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
# The publication metadata a released template DOI was published with.
|
|
86
|
+
# Nil while the DOI is still reserved, and for DOIs released before
|
|
87
|
+
# snapshots were kept — callers fall back to the template's current
|
|
88
|
+
# publication in both cases.
|
|
89
|
+
def labimotion_doi_publication(doi)
|
|
90
|
+
doi.metadata.to_h.dig('labimotion', 'publication')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The DOI's metadata with the publication snapshotted into it, keeping
|
|
94
|
+
# the DOI version already stamped there. Taken at release: the template's
|
|
95
|
+
# publication keeps being edited for the next version, and this version
|
|
96
|
+
# must stop tracking those edits.
|
|
97
|
+
def labimotion_metadata_with_publication(doi, publication)
|
|
98
|
+
metadata = doi.metadata.to_h
|
|
99
|
+
# Deep-duped: the snapshot must not alias the template's publication,
|
|
100
|
+
# which goes on being edited for the next version.
|
|
101
|
+
labimotion = (metadata['labimotion'] || {}).merge('publication' => publication.deep_dup)
|
|
102
|
+
metadata.merge('labimotion' => labimotion)
|
|
103
|
+
end
|
|
104
|
+
|
|
85
105
|
def labimotion_suffix_parts(record)
|
|
86
106
|
# identifier/uuid may both be blank for a draft, which would collapse the
|
|
87
107
|
# scope (colliding across templates). Fall back to the stable record id so
|
|
@@ -23,10 +23,9 @@ module Labimotion
|
|
|
23
23
|
'dataset' => 'LabIMotion Dataset Template'
|
|
24
24
|
}.freeze
|
|
25
25
|
|
|
26
|
-
def initialize(record, doi = nil
|
|
26
|
+
def initialize(record, doi = nil)
|
|
27
27
|
@record = record
|
|
28
28
|
@doi = doi || ::Doi.labimotion_latest_doi(record)
|
|
29
|
-
@current_user = current_user
|
|
30
29
|
end
|
|
31
30
|
|
|
32
31
|
# Returns the XML string, or nil when no DOI has been reserved yet.
|
|
@@ -89,23 +88,31 @@ module Labimotion
|
|
|
89
88
|
end
|
|
90
89
|
end
|
|
91
90
|
|
|
92
|
-
# The
|
|
93
|
-
# (no contributors block) when
|
|
91
|
+
# The author picked from the author list, rendered as a DataCite
|
|
92
|
+
# "Researcher" contributor. Nil (no contributors block) when none has been
|
|
93
|
+
# picked — it is never inferred from whoever happens to be publishing.
|
|
94
94
|
def contributor
|
|
95
|
-
|
|
96
|
-
return nil unless
|
|
95
|
+
picked = publication['contributor']
|
|
96
|
+
return nil unless picked.is_a?(Hash)
|
|
97
97
|
|
|
98
|
-
given =
|
|
99
|
-
family =
|
|
98
|
+
given = picked['givenName'].to_s.strip
|
|
99
|
+
family = picked['familyName'].to_s.strip
|
|
100
100
|
return nil if given.empty? && family.empty?
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
{ 'givenName' => h(given), 'familyName' => h(family), 'orcid' => h(orcid) }
|
|
102
|
+
{ 'givenName' => h(given), 'familyName' => h(family), 'orcid' => h(picked['orcid'].to_s.strip) }
|
|
104
103
|
end
|
|
105
104
|
|
|
105
|
+
# What this DOI renders: the snapshot taken when it was released, so every
|
|
106
|
+
# released version keeps showing the metadata it was published with rather
|
|
107
|
+
# than the template's latest. A DOI that is still reserved has no snapshot
|
|
108
|
+
# and renders the template's current publication, which is what is still
|
|
109
|
+
# being edited for it.
|
|
106
110
|
def publication
|
|
107
|
-
@publication ||=
|
|
108
|
-
|
|
111
|
+
@publication ||= ::Doi.labimotion_doi_publication(@doi).presence || record_publication
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def record_publication
|
|
115
|
+
(@record.properties_template.is_a?(Hash) ? @record.properties_template['publication'] : nil) || {}
|
|
109
116
|
end
|
|
110
117
|
|
|
111
118
|
def h(value)
|
|
@@ -24,7 +24,8 @@ module Labimotion
|
|
|
24
24
|
return failure('doi not reserved') if @doi.nil?
|
|
25
25
|
return self if @doi.minted
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
missing = missing_metadata
|
|
28
|
+
return failure("publication metadata is incomplete: #{missing.join(', ')}") if missing.any?
|
|
28
29
|
|
|
29
30
|
publish_at_datacite
|
|
30
31
|
self
|
|
@@ -37,22 +38,33 @@ module Labimotion
|
|
|
37
38
|
private
|
|
38
39
|
|
|
39
40
|
def publish_at_datacite
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# DataCite requires the metadata to exist before the DOI can be minted.
|
|
43
|
-
metadata = mds.upload_metadata(BuildTemplateDoiXml.new(record, doi, current_user).call)
|
|
44
|
-
return failure(datacite_error('metadata upload', metadata)) unless metadata.is_a?(Net::HTTPSuccess)
|
|
45
|
-
|
|
46
|
-
response = mds.mint(doi.full_doi, TemplateDoiHelpers.template_url(record))
|
|
47
|
-
return failure(datacite_error('mint', response)) unless response.is_a?(Net::HTTPSuccess)
|
|
48
|
-
end
|
|
41
|
+
datacite_failure = send_to_datacite? ? mint_at_datacite : nil
|
|
42
|
+
return failure(datacite_failure) if datacite_failure
|
|
49
43
|
|
|
50
44
|
ActiveRecord::Base.transaction do
|
|
51
|
-
|
|
45
|
+
# The publication is snapshotted onto the DOI as it is minted, so this
|
|
46
|
+
# version keeps rendering what it was published with once the template's
|
|
47
|
+
# metadata moves on to the next version.
|
|
48
|
+
doi.update!(minted: true, minted_at: Time.current,
|
|
49
|
+
metadata: ::Doi.labimotion_metadata_with_publication(doi, publication))
|
|
52
50
|
stamp_release_on_record
|
|
53
51
|
end
|
|
54
52
|
end
|
|
55
53
|
|
|
54
|
+
# Uploads the metadata and mints the DOI at DataCite. Returns an error
|
|
55
|
+
# message, or nil when both calls succeeded.
|
|
56
|
+
def mint_at_datacite
|
|
57
|
+
mds = ::Repo::Datacite::Mds.new
|
|
58
|
+
# DataCite requires the metadata to exist before the DOI can be minted.
|
|
59
|
+
metadata = mds.upload_metadata(BuildTemplateDoiXml.new(record, doi).call)
|
|
60
|
+
return datacite_error('metadata upload', metadata) unless metadata.is_a?(Net::HTTPSuccess)
|
|
61
|
+
|
|
62
|
+
response = mds.mint(doi.full_doi, TemplateDoiHelpers.template_url(record))
|
|
63
|
+
return datacite_error('mint', response) unless response.is_a?(Net::HTTPSuccess)
|
|
64
|
+
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
56
68
|
def datacite_error(step, response)
|
|
57
69
|
status = response.respond_to?(:code) ? response.code : 'unknown'
|
|
58
70
|
"datacite #{step} failed (status #{status})"
|
|
@@ -73,13 +85,27 @@ module Labimotion
|
|
|
73
85
|
record.update!(attrs) if attrs.any?
|
|
74
86
|
end
|
|
75
87
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
# Everything the publication form owns that DataCite needs, named so the
|
|
89
|
+
# modal can say what is still missing. The contributor is one of the
|
|
90
|
+
# authors and has to be picked in the form — a DOI is never released
|
|
91
|
+
# crediting whoever happened to press the button.
|
|
92
|
+
def missing_metadata
|
|
93
|
+
missing = %w[title description].reject { |key| publication[key].to_s.strip.present? }
|
|
94
|
+
missing << 'authors' unless Array(publication['authors']).any? { |author| named?(author) }
|
|
95
|
+
missing << 'contributor' unless named?(publication['contributor'])
|
|
96
|
+
missing
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def named?(person)
|
|
100
|
+
person.is_a?(Hash) && person['familyName'].to_s.strip.present?
|
|
101
|
+
end
|
|
79
102
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
103
|
+
# The template's current publication metadata — what this release publishes.
|
|
104
|
+
def publication
|
|
105
|
+
@publication ||= begin
|
|
106
|
+
pub = record.properties_template.is_a?(Hash) ? record.properties_template['publication'] : nil
|
|
107
|
+
pub.is_a?(Hash) ? pub : {}
|
|
108
|
+
end
|
|
83
109
|
end
|
|
84
110
|
|
|
85
111
|
def failure(message)
|
|
@@ -7,7 +7,7 @@ module Labimotion
|
|
|
7
7
|
class UpdateTemplatePublicationMetadata
|
|
8
8
|
include TemplateDoiHelpers
|
|
9
9
|
|
|
10
|
-
ALLOWED_KEYS = %w[title description authors license].freeze
|
|
10
|
+
ALLOWED_KEYS = %w[title description authors contributor license].freeze
|
|
11
11
|
|
|
12
12
|
attr_reader :record, :current_user, :publication, :error
|
|
13
13
|
|
|
@@ -39,9 +39,33 @@ module Labimotion
|
|
|
39
39
|
def normalized_publication
|
|
40
40
|
slice = publication.stringify_keys.slice(*ALLOWED_KEYS)
|
|
41
41
|
slice['authors'] = Array(slice['authors']).filter_map { |a| normalize_author(a) }
|
|
42
|
+
picked = picked_contributor(slice['authors'], slice['contributor'])
|
|
43
|
+
picked ? slice['contributor'] = picked : slice.delete('contributor')
|
|
42
44
|
slice
|
|
43
45
|
end
|
|
44
46
|
|
|
47
|
+
# The contributor is one of the authors, so it is matched back against the
|
|
48
|
+
# saved author list and stored as that author. Anything that matches none
|
|
49
|
+
# of them — a stale pick whose author has since been removed, or a name
|
|
50
|
+
# that was never on the list — is dropped.
|
|
51
|
+
def picked_contributor(authors, contributor)
|
|
52
|
+
picked = normalize_author(contributor)
|
|
53
|
+
return nil if picked.nil?
|
|
54
|
+
|
|
55
|
+
authors.find { |author| same_person?(author, picked) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# ORCID identifies a person when both sides carry one; otherwise fall back
|
|
59
|
+
# to the name pair, which is what the editor matches on too.
|
|
60
|
+
def same_person?(one, other)
|
|
61
|
+
orcids = [one, other].map { |a| a['orcid'].to_s.strip }
|
|
62
|
+
return orcids[0].casecmp?(orcids[1]) if orcids.all?(&:present?)
|
|
63
|
+
|
|
64
|
+
%w[givenName familyName].all? do |key|
|
|
65
|
+
one[key].to_s.strip.casecmp?(other[key].to_s.strip)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
45
69
|
def normalize_author(author)
|
|
46
70
|
return nil unless author.is_a?(Hash)
|
|
47
71
|
|
data/lib/labimotion/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: labimotion
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.2.0.
|
|
4
|
+
version: 2.2.0.rc21
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chia-Lin Lin
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-07-
|
|
12
|
+
date: 2026-07-28 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: caxlsx
|