labimotion 2.2.0.rc21 → 2.2.0.rc22
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 +3 -1
- data/lib/labimotion/apis/labimotion_template_browse_api.rb +3 -1
- data/lib/labimotion/libs/data/datacite/labimotion_template.html.erb +16 -4
- data/lib/labimotion/usecases/build_template_doi_xml.rb +44 -17
- data/lib/labimotion/usecases/release_template_doi.rb +3 -4
- data/lib/labimotion/usecases/template_doi_helpers.rb +25 -3
- data/lib/labimotion/usecases/update_template_publication_metadata.rb +65 -20
- 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: c79a031d59f881b26d04e1acb6863bff76a6164e096068268d7495042d8db0f0
|
|
4
|
+
data.tar.gz: 3d84dc19e636a8c57734675e41d58ef16bb821e7d7be34be2cbe7b3196a3b804
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4ad9ee1d24b63e2ebdc8a6bf6ca338fe3170644c63bc50cafcd89a660250fef0747375617b394a8174644ead7461f2dcc1d81fa4095e7365e11b07517781af00
|
|
7
|
+
data.tar.gz: 103e68dc679f80f3a214946361dc70ad5c1204336f567d17aaae4a9aa32298e56c23f4af4f545f07e27c9cdc8ccaea7bcc00e4978918af0b9d5f9baba6188785
|
|
@@ -8,7 +8,9 @@ module Labimotion
|
|
|
8
8
|
# Grape route and helper DSL blocks are necessarily large.
|
|
9
9
|
KLASS_TYPES = Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE.keys.freeze
|
|
10
10
|
# Publication fields exposed on the public (anonymous) DOI endpoints.
|
|
11
|
-
|
|
11
|
+
# `contributor` (singular) is kept for DOIs released before contributors
|
|
12
|
+
# became a list — their snapshot still carries it.
|
|
13
|
+
PUBLIC_PUBLICATION_KEYS = %w[title description authors contributors contributor references license].freeze
|
|
12
14
|
|
|
13
15
|
helpers do
|
|
14
16
|
def resolve_klass!(type, id)
|
|
@@ -10,7 +10,9 @@ module Labimotion
|
|
|
10
10
|
KLASS_BY_TYPE = Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE
|
|
11
11
|
KLASS_TYPES = KLASS_BY_TYPE.keys.freeze
|
|
12
12
|
# Publication fields exposed alongside a template / its released DOIs.
|
|
13
|
-
|
|
13
|
+
# `contributor` (singular) is kept for DOIs released before contributors
|
|
14
|
+
# became a list — their snapshot still carries it.
|
|
15
|
+
PUBLICATION_KEYS = %w[title description authors contributors contributor references license].freeze
|
|
14
16
|
# type => [revision model, foreign key to the klass]
|
|
15
17
|
REVISION_BY_TYPE = {
|
|
16
18
|
'element' => [::Labimotion::ElementKlassesRevision, :element_klass_id],
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
<% if creator['orcid'].present? %>
|
|
16
16
|
<nameIdentifier schemeURI="https://orcid.org/" nameIdentifierScheme="ORCID"><%= creator['orcid'] %></nameIdentifier>
|
|
17
17
|
<% end %>
|
|
18
|
-
<% if creator['affiliation'].present? %>
|
|
19
|
-
<affiliation
|
|
18
|
+
<% if creator['affiliation'].present? || creator['ror'].present? %>
|
|
19
|
+
<affiliation<% if creator['ror'].present? %> affiliationIdentifier="<%= creator['ror'] %>" affiliationIdentifierScheme="ROR" schemeURI="https://ror.org"<% end %>><%= creator['affiliation'] %></affiliation>
|
|
20
20
|
<% end %>
|
|
21
21
|
</creator>
|
|
22
22
|
<% end %>
|
|
@@ -29,18 +29,30 @@
|
|
|
29
29
|
<subjects>
|
|
30
30
|
<subject>Chemistry</subject>
|
|
31
31
|
</subjects>
|
|
32
|
-
<% if
|
|
32
|
+
<% if contributors.any? %>
|
|
33
33
|
<contributors>
|
|
34
|
-
|
|
34
|
+
<% contributors.each do |contributor| %>
|
|
35
|
+
<contributor contributorType="<%= contributor['contributorType'] %>">
|
|
35
36
|
<contributorName nameType="Personal"><%= contributor['familyName'] %>, <%= contributor['givenName'] %></contributorName>
|
|
36
37
|
<givenName><%= contributor['givenName'] %></givenName>
|
|
37
38
|
<familyName><%= contributor['familyName'] %></familyName>
|
|
38
39
|
<% if contributor['orcid'].present? %>
|
|
39
40
|
<nameIdentifier schemeURI="https://orcid.org/" nameIdentifierScheme="ORCID"><%= contributor['orcid'] %></nameIdentifier>
|
|
40
41
|
<% end %>
|
|
42
|
+
<% if contributor['affiliation'].present? || contributor['ror'].present? %>
|
|
43
|
+
<affiliation<% if contributor['ror'].present? %> affiliationIdentifier="<%= contributor['ror'] %>" affiliationIdentifierScheme="ROR" schemeURI="https://ror.org"<% end %>><%= contributor['affiliation'] %></affiliation>
|
|
44
|
+
<% end %>
|
|
41
45
|
</contributor>
|
|
46
|
+
<% end %>
|
|
42
47
|
</contributors>
|
|
43
48
|
<% end %>
|
|
49
|
+
<% if references.any? %>
|
|
50
|
+
<relatedIdentifiers>
|
|
51
|
+
<% references.each do |reference| %>
|
|
52
|
+
<relatedIdentifier relatedIdentifierType="DOI" relationType="References">https://dx.doi.org/<%= reference['doi'] %></relatedIdentifier>
|
|
53
|
+
<% end %>
|
|
54
|
+
</relatedIdentifiers>
|
|
55
|
+
<% end %>
|
|
44
56
|
<language>en</language>
|
|
45
57
|
<resourceType resourceTypeGeneral="Model"><%= resource_label %></resourceType>
|
|
46
58
|
<version><%= version %></version>
|
|
@@ -76,30 +76,57 @@ module Labimotion
|
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
def creators
|
|
79
|
-
Array(publication['authors']).filter_map
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
given = author['givenName'].to_s.strip
|
|
83
|
-
family = author['familyName'].to_s.strip
|
|
84
|
-
next if given.empty? && family.empty?
|
|
79
|
+
Array(publication['authors']).filter_map { |author| person(author) }
|
|
80
|
+
end
|
|
85
81
|
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
# The people credited as DataCite contributors, each in their own role.
|
|
83
|
+
# Held apart from the creators — a contributor need not be one of the
|
|
84
|
+
# authors — and empty (no contributors block) until one is named; they are
|
|
85
|
+
# never inferred from whoever happens to be publishing. DOIs released
|
|
86
|
+
# before contributors became a list carry a single `contributor`.
|
|
87
|
+
def contributors
|
|
88
|
+
list = publication['contributors']
|
|
89
|
+
list = [publication['contributor']].compact unless list.is_a?(Array)
|
|
90
|
+
list.filter_map do |entry|
|
|
91
|
+
person(entry)&.merge('contributorType' => h(contributor_type(entry)))
|
|
88
92
|
end
|
|
89
93
|
end
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return nil unless picked.is_a?(Hash)
|
|
95
|
+
def contributor_type(entry)
|
|
96
|
+
type = entry['contributorType'].to_s.strip
|
|
97
|
+
types = TemplateDoiHelpers::CONTRIBUTOR_TYPES
|
|
98
|
+
types.include?(type) ? type : TemplateDoiHelpers::DEFAULT_CONTRIBUTOR_TYPE
|
|
99
|
+
end
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
# A creator or contributor, escaped for the template. Nameless entries are
|
|
102
|
+
# dropped: DataCite has nowhere to put an ORCID or affiliation on its own.
|
|
103
|
+
# `ror` is the resolver URI DataCite expects in affiliationIdentifier,
|
|
104
|
+
# rebuilt from the bare id that is stored.
|
|
105
|
+
def person(entry)
|
|
106
|
+
return nil unless entry.is_a?(Hash)
|
|
107
|
+
|
|
108
|
+
given = entry['givenName'].to_s.strip
|
|
109
|
+
family = entry['familyName'].to_s.strip
|
|
100
110
|
return nil if given.empty? && family.empty?
|
|
101
111
|
|
|
102
|
-
|
|
112
|
+
ror = entry['ror_id'].to_s.strip
|
|
113
|
+
{ 'givenName' => h(given), 'familyName' => h(family), 'orcid' => h(entry['orcid'].to_s.strip),
|
|
114
|
+
'affiliation' => h(entry['affiliation'].to_s.strip),
|
|
115
|
+
'ror' => ror.empty? ? '' : h("https://ror.org/#{ror}") }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Works this template cites, published as DataCite relatedIdentifiers of
|
|
119
|
+
# relationType "References". Only the DOI travels — a relatedIdentifier
|
|
120
|
+
# carries no title — so entries without one are skipped.
|
|
121
|
+
def references
|
|
122
|
+
Array(publication['references']).filter_map do |reference|
|
|
123
|
+
next unless reference.is_a?(Hash)
|
|
124
|
+
|
|
125
|
+
doi = reference['doi'].to_s.strip
|
|
126
|
+
next if doi.empty?
|
|
127
|
+
|
|
128
|
+
{ 'doi' => h(doi) }
|
|
129
|
+
end
|
|
103
130
|
end
|
|
104
131
|
|
|
105
132
|
# What this DOI renders: the snapshot taken when it was released, so every
|
|
@@ -86,13 +86,12 @@ module Labimotion
|
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
# Everything the publication form owns that DataCite needs, named so the
|
|
89
|
-
# modal can say what is still missing.
|
|
90
|
-
#
|
|
91
|
-
#
|
|
89
|
+
# modal can say what is still missing. Contributors and references are
|
|
90
|
+
# optional: not every template credits a role beyond its authors, and not
|
|
91
|
+
# every template cites a work.
|
|
92
92
|
def missing_metadata
|
|
93
93
|
missing = %w[title description].reject { |key| publication[key].to_s.strip.present? }
|
|
94
94
|
missing << 'authors' unless Array(publication['authors']).any? { |author| named?(author) }
|
|
95
|
-
missing << 'contributor' unless named?(publication['contributor'])
|
|
96
95
|
missing
|
|
97
96
|
end
|
|
98
97
|
|
|
@@ -20,6 +20,24 @@ module Labimotion
|
|
|
20
20
|
::Labimotion::DatasetKlass => 'datasets'
|
|
21
21
|
}.freeze
|
|
22
22
|
|
|
23
|
+
# Where the hub lives, most specific first. PUBLIC_URL is the instance's own
|
|
24
|
+
# public address (the host app's config/application.rb insists on it), so the
|
|
25
|
+
# link follows whichever instance is serving it rather than a hardcoded
|
|
26
|
+
# chemotion-repository.net. The DataCite-facing domains stay as fallbacks for
|
|
27
|
+
# deployments that only set those.
|
|
28
|
+
HOST_ENV_KEYS = %w[PUBLIC_URL REPO_HOST DOI_DOMAIN_TEST DOI_DOMAIN].freeze
|
|
29
|
+
|
|
30
|
+
# The DataCite kernel-4 contributorType vocabulary. A template DOI may name
|
|
31
|
+
# any number of contributors — including none — each credited in one of
|
|
32
|
+
# these roles. The list is closed: DataCite rejects anything else.
|
|
33
|
+
CONTRIBUTOR_TYPES = %w[
|
|
34
|
+
ContactPerson DataCollector DataCurator DataManager Distributor Editor
|
|
35
|
+
HostingInstitution Producer ProjectLeader ProjectManager ProjectMember
|
|
36
|
+
RegistrationAgency RegistrationAuthority RelatedPerson Researcher
|
|
37
|
+
ResearchGroup RightsHolder Sponsor Supervisor WorkPackageLeader Other
|
|
38
|
+
].freeze
|
|
39
|
+
DEFAULT_CONTRIBUTOR_TYPE = 'Researcher'
|
|
40
|
+
|
|
23
41
|
module_function
|
|
24
42
|
|
|
25
43
|
# True once the template's latest DOI has been released (minted) and the
|
|
@@ -49,13 +67,17 @@ module Labimotion
|
|
|
49
67
|
# Used as the DOI's DataCite landing page and surfaced in the UI.
|
|
50
68
|
def template_url(record)
|
|
51
69
|
type = klass_type_for(record)
|
|
52
|
-
host = ENV['REPO_HOST'].presence || ENV['DOI_DOMAIN_TEST'].presence ||
|
|
53
|
-
ENV['DOI_DOMAIN'].presence || 'https://www.chemotion-repository.net'
|
|
54
70
|
record_id = record.try(:identifier).presence || record.try(:uuid).presence || record.id
|
|
55
71
|
query = "type=finder&kind=#{type}&id=#{record_id}"
|
|
56
72
|
version = record.try(:version)
|
|
57
73
|
query += "&version=#{version}" if version.present?
|
|
58
|
-
"#{
|
|
74
|
+
"#{public_host}/home/genericHub?#{query}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The instance serving the hub, without its trailing slash.
|
|
78
|
+
def public_host
|
|
79
|
+
host = HOST_ENV_KEYS.lazy.filter_map { |key| ENV[key].presence }.first
|
|
80
|
+
(host || 'http://localhost:3000').chomp('/')
|
|
59
81
|
end
|
|
60
82
|
|
|
61
83
|
# A user may manage a template's DOI iff they hold the LabIMotion
|
|
@@ -7,7 +7,12 @@ module Labimotion
|
|
|
7
7
|
class UpdateTemplatePublicationMetadata
|
|
8
8
|
include TemplateDoiHelpers
|
|
9
9
|
|
|
10
|
-
ALLOWED_KEYS = %w[title description authors
|
|
10
|
+
ALLOWED_KEYS = %w[title description authors contributors references license].freeze
|
|
11
|
+
REFERENCE_KEYS = %w[doi title url].freeze
|
|
12
|
+
# Authors and contributors are the same shape: a DataCite personal name plus
|
|
13
|
+
# the identifiers that make it resolvable — ORCID for the person, ROR for
|
|
14
|
+
# their organisation. Contributors carry a contributorType on top.
|
|
15
|
+
PERSON_KEYS = %w[givenName familyName orcid affiliation affiliation_id ror_id].freeze
|
|
11
16
|
|
|
12
17
|
attr_reader :record, :current_user, :publication, :error
|
|
13
18
|
|
|
@@ -39,42 +44,82 @@ module Labimotion
|
|
|
39
44
|
def normalized_publication
|
|
40
45
|
slice = publication.stringify_keys.slice(*ALLOWED_KEYS)
|
|
41
46
|
slice['authors'] = Array(slice['authors']).filter_map { |a| normalize_author(a) }
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
# Contributors are people in their own right — none of them need be one of
|
|
48
|
+
# the authors — so they are stored as given rather than matched back
|
|
49
|
+
# against the author list. There may be any number, including none.
|
|
50
|
+
slice['contributors'] = submitted_contributors.filter_map { |c| normalize_contributor(c) }
|
|
51
|
+
slice['references'] = Array(slice['references']).filter_map { |r| normalize_reference(r) }
|
|
44
52
|
slice
|
|
45
53
|
end
|
|
46
54
|
|
|
47
|
-
#
|
|
48
|
-
# saved
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
# A single `contributor` is what the first version of the form sent; a
|
|
56
|
+
# template saved by it is read back into the list rather than dropped.
|
|
57
|
+
def submitted_contributors
|
|
58
|
+
given = publication.stringify_keys
|
|
59
|
+
given['contributors'].is_a?(Array) ? given['contributors'] : [given['contributor']].compact
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A person plus the DataCite role they are credited in. An unknown or
|
|
63
|
+
# missing role falls back to "Researcher" rather than failing the save —
|
|
64
|
+
# the vocabulary is closed, so anything else would be rejected at minting.
|
|
65
|
+
def normalize_contributor(contributor)
|
|
66
|
+
h = normalize_author(contributor)
|
|
67
|
+
return nil unless h
|
|
54
68
|
|
|
55
|
-
|
|
69
|
+
type = contributor.stringify_keys['contributorType'].to_s.strip
|
|
70
|
+
known = TemplateDoiHelpers::CONTRIBUTOR_TYPES.include?(type)
|
|
71
|
+
h.merge('contributorType' => known ? type : TemplateDoiHelpers::DEFAULT_CONTRIBUTOR_TYPE)
|
|
56
72
|
end
|
|
57
73
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return
|
|
74
|
+
# A cited work, identified by its DOI. Anything without one is dropped: the
|
|
75
|
+
# DOI is what goes to DataCite as a relatedIdentifier, and a reference
|
|
76
|
+
# without it would be published as an empty relation.
|
|
77
|
+
def normalize_reference(reference)
|
|
78
|
+
return nil unless reference.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
h = reference.stringify_keys.slice(*REFERENCE_KEYS)
|
|
81
|
+
h['doi'] = sanitize_doi(h['doi'])
|
|
82
|
+
return nil if h['doi'].nil?
|
|
83
|
+
|
|
84
|
+
h.transform_values { |v| v.to_s.strip }
|
|
85
|
+
end
|
|
63
86
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
87
|
+
# Accepts a bare DOI, a doi: prefix or a resolver URL, and keeps the bare
|
|
88
|
+
# "10.x/y" form. Nil for anything that is not a DOI.
|
|
89
|
+
def sanitize_doi(value)
|
|
90
|
+
match = value.to_s.strip.match(%r{(10\.\d{4,9}/\S+)\z})
|
|
91
|
+
match && match[1]
|
|
67
92
|
end
|
|
68
93
|
|
|
69
94
|
def normalize_author(author)
|
|
70
95
|
return nil unless author.is_a?(Hash)
|
|
71
96
|
|
|
72
|
-
h = author.stringify_keys.slice(
|
|
97
|
+
h = author.stringify_keys.slice(*PERSON_KEYS)
|
|
73
98
|
return nil if h.values_at('givenName', 'familyName').all? { |v| v.to_s.strip.empty? }
|
|
74
99
|
|
|
100
|
+
ror = normalize_ror(h['ror_id']) || ror_for(h['affiliation_id'])
|
|
101
|
+
ror ? h['ror_id'] = ror : h.delete('ror_id')
|
|
75
102
|
h
|
|
76
103
|
end
|
|
77
104
|
|
|
105
|
+
# ROR IDs travel either bare ("05f950310") or as a resolver URL; DataCite
|
|
106
|
+
# wants the URI, so the bare form is what gets stored and the URI is built
|
|
107
|
+
# when the XML is rendered.
|
|
108
|
+
def normalize_ror(value)
|
|
109
|
+
value.to_s.strip.sub(%r{\Ahttps?://ror\.org/}i, '').presence
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The authoritative ROR for a picked affiliation. Whoever the editor picked
|
|
113
|
+
# came from an Affiliation record, so its id is the better source than
|
|
114
|
+
# anything retyped — and the current-user payload carries no ROR at all.
|
|
115
|
+
def ror_for(affiliation_id)
|
|
116
|
+
return nil if affiliation_id.blank? || !defined?(::Affiliation)
|
|
117
|
+
|
|
118
|
+
normalize_ror(::Affiliation.where(id: affiliation_id).pick(:ror_id))
|
|
119
|
+
rescue StandardError
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
78
123
|
def failure(message)
|
|
79
124
|
@error = message
|
|
80
125
|
self
|
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.rc22
|
|
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-29 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: caxlsx
|