labimotion 2.2.0.rc22 → 2.3.0.rc1
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/generic_element_api.rb +0 -1
- data/lib/labimotion/apis/labimotion_doi_api.rb +10 -24
- data/lib/labimotion/apis/labimotion_template_browse_api.rb +1 -13
- data/lib/labimotion/helpers/element_helpers.rb +15 -21
- data/lib/labimotion/models/concerns/element_fetchable.rb +2 -2
- data/lib/labimotion/models/element.rb +2 -2
- data/lib/labimotion/usecases/build_template_doi_xml.rb +23 -69
- data/lib/labimotion/usecases/release_template_doi.rb +17 -42
- data/lib/labimotion/usecases/template_doi_helpers.rb +3 -25
- data/lib/labimotion/usecases/update_template_publication_metadata.rb +2 -71
- data/lib/labimotion/utils/search.rb +2 -2
- data/lib/labimotion/utils/units.rb +177 -20
- data/lib/labimotion/version.rb +1 -1
- data/lib/labimotion.rb +0 -1
- metadata +2 -4
- data/lib/labimotion/libs/data/datacite/labimotion_template.html.erb +0 -67
- data/lib/labimotion/models/concerns/template_doi.rb +0 -133
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed76c84363276af955102072d3491035f014dedb9fcdc835b4ec9c2e9ba199ee
|
|
4
|
+
data.tar.gz: 8a5e3a1d1c5ac1fb497d278c1f9a3237dfc0f93271729e05c602339879d6a204
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d9d033f42be38117263ab30d502a3eafa5f9fa3b983f834dd1b1e14ab55537c91fafafd5b1643a6d3e2ea5e2e2ec17a8a072220cef56a4b80a34deca8f6600c
|
|
7
|
+
data.tar.gz: 926082586397c99e47f634733d2e258b463a9340809e45d5a0d4a788ff508c0335279e80baabef1f89b851691777d764b8657b6825b40a420e962dab4c6e6f36
|
|
@@ -432,7 +432,6 @@ module Labimotion
|
|
|
432
432
|
desc 'Return serialized elements of current user'
|
|
433
433
|
params do
|
|
434
434
|
optional :collection_id, type: Integer, desc: 'Collection id'
|
|
435
|
-
optional :sync_collection_id, type: Integer, desc: 'SyncCollectionsUser id'
|
|
436
435
|
optional :el_type, type: String, desc: 'element klass name'
|
|
437
436
|
optional :from_date, type: Integer, desc: 'created_date from in ms'
|
|
438
437
|
optional :to_date, type: Integer, desc: 'created_date to in ms'
|
|
@@ -7,10 +7,6 @@ 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
|
-
# `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
|
|
14
10
|
|
|
15
11
|
helpers do
|
|
16
12
|
def resolve_klass!(type, id)
|
|
@@ -45,36 +41,25 @@ module Labimotion
|
|
|
45
41
|
)
|
|
46
42
|
end
|
|
47
43
|
|
|
48
|
-
# Public, read-only view of one released DOI version
|
|
49
|
-
|
|
50
|
-
def released_doi_version(record, doi)
|
|
44
|
+
# Public, read-only view of one released DOI version.
|
|
45
|
+
def released_doi_version(doi)
|
|
51
46
|
full = doi.full_doi
|
|
52
47
|
{
|
|
53
48
|
doi: full,
|
|
54
49
|
doi_url: "https://dx.doi.org/#{full}",
|
|
55
50
|
version: ::Doi.labimotion_doi_version(doi),
|
|
56
|
-
minted_at: doi.minted_at
|
|
57
|
-
publication: released_publication(record, doi)
|
|
51
|
+
minted_at: doi.minted_at
|
|
58
52
|
}
|
|
59
53
|
end
|
|
60
54
|
|
|
61
|
-
# The snapshot taken when this version was released. DOIs released before
|
|
62
|
-
# snapshots were kept have none, and fall back to the template's current
|
|
63
|
-
# publication — the best available answer for them.
|
|
64
|
-
def released_publication(record, doi)
|
|
65
|
-
publication = ::Doi.labimotion_doi_publication(doi).presence ||
|
|
66
|
-
(record.properties_template || {})['publication']
|
|
67
|
-
publication.is_a?(Hash) ? publication.slice(*PUBLIC_PUBLICATION_KEYS) : {}
|
|
68
|
-
end
|
|
69
|
-
|
|
70
55
|
# Public payload for a template that has at least one released DOI: the
|
|
71
56
|
# latest released DOI promoted to the top level, plus its publication
|
|
72
|
-
# metadata, hub deep-link and every released version.
|
|
73
|
-
#
|
|
74
|
-
# than the template's work-in-progress. `record_id` matches the hub grid's
|
|
75
|
-
# row id (identifier || uuid) so the frontend can merge it.
|
|
57
|
+
# metadata, hub deep-link and every released version. `record_id` matches
|
|
58
|
+
# the hub grid's row id (identifier || uuid) so the frontend can merge it.
|
|
76
59
|
def released_template_doi(record, dois)
|
|
77
|
-
versions = dois.sort_by(&:id).map { |doi| released_doi_version(
|
|
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') : {}
|
|
78
63
|
versions.last.merge(
|
|
79
64
|
record_id: record.try(:identifier).presence || record.try(:uuid).presence || record.id,
|
|
80
65
|
identifier: record.try(:identifier),
|
|
@@ -83,6 +68,7 @@ module Labimotion
|
|
|
83
68
|
template_version: record.try(:version),
|
|
84
69
|
next_doi_version: ::Doi.labimotion_version_segment(record),
|
|
85
70
|
template_url: Labimotion::TemplateDoiHelpers.template_url(record),
|
|
71
|
+
publication: publication,
|
|
86
72
|
versions: versions
|
|
87
73
|
)
|
|
88
74
|
end
|
|
@@ -139,7 +125,7 @@ module Labimotion
|
|
|
139
125
|
version: ::Doi.labimotion_doi_version(doi),
|
|
140
126
|
full_doi: doi.full_doi,
|
|
141
127
|
minted: doi.minted == true,
|
|
142
|
-
xml: Labimotion::BuildTemplateDoiXml.new(record, doi).call
|
|
128
|
+
xml: Labimotion::BuildTemplateDoiXml.new(record, doi, current_user).call
|
|
143
129
|
}
|
|
144
130
|
end
|
|
145
131
|
{ versions: versions }
|
|
@@ -9,10 +9,6 @@ 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
|
-
# `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
|
|
16
12
|
# type => [revision model, foreign key to the klass]
|
|
17
13
|
REVISION_BY_TYPE = {
|
|
18
14
|
'element' => [::Labimotion::ElementKlassesRevision, :element_klass_id],
|
|
@@ -80,14 +76,7 @@ module Labimotion
|
|
|
80
76
|
# The template's DOI publication metadata (shown alongside the DOI).
|
|
81
77
|
def template_publication(record)
|
|
82
78
|
pub = (record.properties_template || {})['publication']
|
|
83
|
-
pub.is_a?(Hash) ? pub.slice(
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# The publication a released DOI was published with. Falls back to the
|
|
87
|
-
# template's current metadata for DOIs released before snapshots were kept.
|
|
88
|
-
def released_publication(record, doi)
|
|
89
|
-
pub = ::Doi.labimotion_doi_publication(doi)
|
|
90
|
-
pub.is_a?(Hash) && pub.any? ? pub.slice(*PUBLICATION_KEYS) : template_publication(record)
|
|
79
|
+
pub.is_a?(Hash) ? pub.slice('title', 'description', 'authors', 'license') : {}
|
|
91
80
|
end
|
|
92
81
|
|
|
93
82
|
# Released (minted) DOIs of the template, each tagged with its DOI version
|
|
@@ -99,7 +88,6 @@ module Labimotion
|
|
|
99
88
|
full = doi.full_doi
|
|
100
89
|
{ version: ::Doi.labimotion_doi_version(doi), doi: full,
|
|
101
90
|
doi_url: "https://dx.doi.org/#{full}", minted_at: doi.minted_at,
|
|
102
|
-
publication: released_publication(record, doi),
|
|
103
91
|
xml: Labimotion::BuildTemplateDoiXml.new(record, doi).call }
|
|
104
92
|
end
|
|
105
93
|
end
|
|
@@ -215,7 +215,7 @@ module Labimotion
|
|
|
215
215
|
layer, field = params[:sort_column].split('.')
|
|
216
216
|
|
|
217
217
|
element_klass = Labimotion::ElementKlass.find_by(name: params[:el_type])
|
|
218
|
-
allowed_fields = element_klass
|
|
218
|
+
allowed_fields = element_klass&.properties_release&.dig(Labimotion::Prop::LAYERS, layer, Labimotion::Prop::FIELDS)&.pluck('field') || []
|
|
219
219
|
|
|
220
220
|
if field.in?(allowed_fields)
|
|
221
221
|
query = ActiveRecord::Base.sanitize_sql(
|
|
@@ -247,27 +247,21 @@ module Labimotion
|
|
|
247
247
|
end
|
|
248
248
|
|
|
249
249
|
def list_serialized_elements(params, current_user)
|
|
250
|
-
|
|
251
|
-
if params[:collection_id]
|
|
252
|
-
Collection
|
|
253
|
-
.belongs_to_or_shared_by(current_user.id, current_user.group_ids)
|
|
254
|
-
.find_by(id: params[:collection_id])&.id
|
|
255
|
-
elsif params[:sync_collection_id]
|
|
256
|
-
current_user
|
|
257
|
-
.all_sync_in_collections_users
|
|
258
|
-
.find_by(id: params[:sync_collection_id])&.collection&.id
|
|
259
|
-
end
|
|
250
|
+
scope = Labimotion::Element.none
|
|
260
251
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
252
|
+
if params[:collection_id]
|
|
253
|
+
begin
|
|
254
|
+
collection = Collection.accessible_for(current_user).find(params[:collection_id])
|
|
255
|
+
scope = collection.elements
|
|
256
|
+
.joins(:element_klass)
|
|
257
|
+
.where(element_klasses: { name: params[:el_type] })
|
|
258
|
+
.includes(:tag)
|
|
259
|
+
rescue ActiveRecord::RecordNotFound
|
|
260
|
+
Labimotion::Element.none
|
|
261
|
+
end
|
|
269
262
|
else
|
|
270
|
-
|
|
263
|
+
# All collection of current_user
|
|
264
|
+
scope = Labimotion::Element.for_user(current_user.id)
|
|
271
265
|
end
|
|
272
266
|
|
|
273
267
|
## TO DO: refactor labimotion
|
|
@@ -278,7 +272,7 @@ module Labimotion
|
|
|
278
272
|
layer, field = params[:sort_column].split('.')
|
|
279
273
|
|
|
280
274
|
element_klass = Labimotion::ElementKlass.find_by(name: params[:el_type])
|
|
281
|
-
allowed_fields = element_klass
|
|
275
|
+
allowed_fields = element_klass&.properties_release&.dig(Labimotion::Prop::LAYERS, layer, Labimotion::Prop::FIELDS)&.pluck('field') || []
|
|
282
276
|
|
|
283
277
|
if field.in?(allowed_fields)
|
|
284
278
|
query = ActiveRecord::Base.sanitize_sql(
|
|
@@ -26,9 +26,9 @@ module Labimotion
|
|
|
26
26
|
joins(collections: :user).where(collections: { user_id: user_id })
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
-
# Shared
|
|
29
|
+
# Shared records
|
|
30
30
|
shared = apply_filters.call(
|
|
31
|
-
|
|
31
|
+
left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
# Combine (remove duplicates), order, and limit
|
|
@@ -141,9 +141,9 @@ module Labimotion
|
|
|
141
141
|
joins(collections: :user).where(collections: { user_id: user_id })
|
|
142
142
|
)
|
|
143
143
|
|
|
144
|
-
# Shared
|
|
144
|
+
# Shared elements
|
|
145
145
|
shared = apply_filters.call(
|
|
146
|
-
|
|
146
|
+
left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
|
|
147
147
|
)
|
|
148
148
|
|
|
149
149
|
# Combine (remove duplicates), order, and limit
|
|
@@ -11,11 +11,7 @@ module Labimotion
|
|
|
11
11
|
class BuildTemplateDoiXml
|
|
12
12
|
include ::DataCitePublisher
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
# placing its own copy at HOST_TEMPLATE_PATH, which is how it used to be
|
|
16
|
-
# sourced — hence the lookup order below.
|
|
17
|
-
GEM_TEMPLATE_PATH = File.expand_path('../libs/data/datacite/labimotion_template.html.erb', __dir__)
|
|
18
|
-
HOST_TEMPLATE_PATH = 'app/publish/datacite_metadata_labimotion_template.html.erb'
|
|
14
|
+
TEMPLATE_PATH = Rails.root.join('app/publish/datacite_metadata_labimotion_template.html.erb')
|
|
19
15
|
|
|
20
16
|
RESOURCE_LABEL = {
|
|
21
17
|
'element' => 'LabIMotion Element Template',
|
|
@@ -23,28 +19,21 @@ module Labimotion
|
|
|
23
19
|
'dataset' => 'LabIMotion Dataset Template'
|
|
24
20
|
}.freeze
|
|
25
21
|
|
|
26
|
-
def initialize(record, doi = nil)
|
|
22
|
+
def initialize(record, doi = nil, current_user = nil)
|
|
27
23
|
@record = record
|
|
28
24
|
@doi = doi || ::Doi.labimotion_latest_doi(record)
|
|
25
|
+
@current_user = current_user
|
|
29
26
|
end
|
|
30
27
|
|
|
31
28
|
# Returns the XML string, or nil when no DOI has been reserved yet.
|
|
32
29
|
def call
|
|
33
30
|
return nil if @doi.nil?
|
|
34
31
|
|
|
35
|
-
ERB.new(File.read(
|
|
32
|
+
ERB.new(File.read(TEMPLATE_PATH), trim_mode: '-').result(binding)
|
|
36
33
|
end
|
|
37
34
|
|
|
38
35
|
private
|
|
39
36
|
|
|
40
|
-
# Resolved per call rather than at load time, so the class no longer needs
|
|
41
|
-
# Rails to be booted just to be autoloaded.
|
|
42
|
-
def template_path
|
|
43
|
-
# `Rails.root` is still nil if Rails is loaded but the app has not booted.
|
|
44
|
-
host = defined?(Rails) && Rails.respond_to?(:root) ? Rails.root&.join(HOST_TEMPLATE_PATH) : nil
|
|
45
|
-
host && File.exist?(host) ? host : GEM_TEMPLATE_PATH
|
|
46
|
-
end
|
|
47
|
-
|
|
48
37
|
def full_doi
|
|
49
38
|
@doi.full_doi
|
|
50
39
|
end
|
|
@@ -76,70 +65,35 @@ module Labimotion
|
|
|
76
65
|
end
|
|
77
66
|
|
|
78
67
|
def creators
|
|
79
|
-
Array(publication['authors']).filter_map
|
|
80
|
-
|
|
68
|
+
Array(publication['authors']).filter_map do |author|
|
|
69
|
+
next unless author.is_a?(Hash)
|
|
81
70
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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)))
|
|
92
|
-
end
|
|
93
|
-
end
|
|
71
|
+
given = author['givenName'].to_s.strip
|
|
72
|
+
family = author['familyName'].to_s.strip
|
|
73
|
+
next if given.empty? && family.empty?
|
|
94
74
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
types.include?(type) ? type : TemplateDoiHelpers::DEFAULT_CONTRIBUTOR_TYPE
|
|
75
|
+
{ 'givenName' => h(given), 'familyName' => h(family),
|
|
76
|
+
'orcid' => h(author['orcid'].to_s.strip), 'affiliation' => h(author['affiliation'].to_s.strip) }
|
|
77
|
+
end
|
|
99
78
|
end
|
|
100
79
|
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return nil unless entry.is_a?(Hash)
|
|
80
|
+
# The acting user, rendered as a DataCite "Researcher" contributor. Nil
|
|
81
|
+
# (no contributors block) when there is no user or they have no name.
|
|
82
|
+
def contributor
|
|
83
|
+
user = @current_user
|
|
84
|
+
return nil unless user
|
|
107
85
|
|
|
108
|
-
given =
|
|
109
|
-
family =
|
|
86
|
+
given = user.try(:first_name).to_s.strip
|
|
87
|
+
family = user.try(:last_name).to_s.strip
|
|
110
88
|
return nil if given.empty? && family.empty?
|
|
111
89
|
|
|
112
|
-
|
|
113
|
-
{ 'givenName' => h(given), 'familyName' => h(family), 'orcid' => h(
|
|
114
|
-
'affiliation' => h(entry['affiliation'].to_s.strip),
|
|
115
|
-
'ror' => ror.empty? ? '' : h("https://ror.org/#{ror}") }
|
|
90
|
+
orcid = user.respond_to?(:orcid) ? user.orcid.to_s.strip : ''
|
|
91
|
+
{ 'givenName' => h(given), 'familyName' => h(family), 'orcid' => h(orcid) }
|
|
116
92
|
end
|
|
117
93
|
|
|
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
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# What this DOI renders: the snapshot taken when it was released, so every
|
|
133
|
-
# released version keeps showing the metadata it was published with rather
|
|
134
|
-
# than the template's latest. A DOI that is still reserved has no snapshot
|
|
135
|
-
# and renders the template's current publication, which is what is still
|
|
136
|
-
# being edited for it.
|
|
137
94
|
def publication
|
|
138
|
-
@publication ||=
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def record_publication
|
|
142
|
-
(@record.properties_template.is_a?(Hash) ? @record.properties_template['publication'] : nil) || {}
|
|
95
|
+
@publication ||=
|
|
96
|
+
(@record.properties_template.is_a?(Hash) ? @record.properties_template['publication'] : nil) || {}
|
|
143
97
|
end
|
|
144
98
|
|
|
145
99
|
def h(value)
|
|
@@ -24,8 +24,7 @@ module Labimotion
|
|
|
24
24
|
return failure('doi not reserved') if @doi.nil?
|
|
25
25
|
return self if @doi.minted
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
return failure("publication metadata is incomplete: #{missing.join(', ')}") if missing.any?
|
|
27
|
+
return failure('publication metadata is incomplete') unless metadata_ready?
|
|
29
28
|
|
|
30
29
|
publish_at_datacite
|
|
31
30
|
self
|
|
@@ -38,33 +37,22 @@ module Labimotion
|
|
|
38
37
|
private
|
|
39
38
|
|
|
40
39
|
def publish_at_datacite
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
if send_to_datacite?
|
|
41
|
+
mds = ::Repo::Datacite::Mds.new
|
|
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
|
|
43
49
|
|
|
44
50
|
ActiveRecord::Base.transaction do
|
|
45
|
-
|
|
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))
|
|
51
|
+
doi.update!(minted: true, minted_at: Time.current)
|
|
50
52
|
stamp_release_on_record
|
|
51
53
|
end
|
|
52
54
|
end
|
|
53
55
|
|
|
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
|
-
|
|
68
56
|
def datacite_error(step, response)
|
|
69
57
|
status = response.respond_to?(:code) ? response.code : 'unknown'
|
|
70
58
|
"datacite #{step} failed (status #{status})"
|
|
@@ -85,26 +73,13 @@ module Labimotion
|
|
|
85
73
|
record.update!(attrs) if attrs.any?
|
|
86
74
|
end
|
|
87
75
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# every template cites a work.
|
|
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
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def named?(person)
|
|
99
|
-
person.is_a?(Hash) && person['familyName'].to_s.strip.present?
|
|
100
|
-
end
|
|
76
|
+
def metadata_ready?
|
|
77
|
+
publication = record.properties_template.is_a?(Hash) ? record.properties_template['publication'] : nil
|
|
78
|
+
return false unless publication.is_a?(Hash)
|
|
101
79
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
pub = record.properties_template.is_a?(Hash) ? record.properties_template['publication'] : nil
|
|
106
|
-
pub.is_a?(Hash) ? pub : {}
|
|
107
|
-
end
|
|
80
|
+
publication['title'].to_s.strip.present? &&
|
|
81
|
+
publication['description'].to_s.strip.present? &&
|
|
82
|
+
Array(publication['authors']).any? { |a| a.is_a?(Hash) && a['familyName'].to_s.strip.present? }
|
|
108
83
|
end
|
|
109
84
|
|
|
110
85
|
def failure(message)
|
|
@@ -20,24 +20,6 @@ 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
|
-
|
|
41
23
|
module_function
|
|
42
24
|
|
|
43
25
|
# True once the template's latest DOI has been released (minted) and the
|
|
@@ -67,17 +49,13 @@ module Labimotion
|
|
|
67
49
|
# Used as the DOI's DataCite landing page and surfaced in the UI.
|
|
68
50
|
def template_url(record)
|
|
69
51
|
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'
|
|
70
54
|
record_id = record.try(:identifier).presence || record.try(:uuid).presence || record.id
|
|
71
55
|
query = "type=finder&kind=#{type}&id=#{record_id}"
|
|
72
56
|
version = record.try(:version)
|
|
73
57
|
query += "&version=#{version}" if version.present?
|
|
74
|
-
"#{
|
|
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('/')
|
|
58
|
+
"#{host.chomp('/')}/home/genericHub?#{query}"
|
|
81
59
|
end
|
|
82
60
|
|
|
83
61
|
# A user may manage a template's DOI iff they hold the LabIMotion
|
|
@@ -7,12 +7,7 @@ module Labimotion
|
|
|
7
7
|
class UpdateTemplatePublicationMetadata
|
|
8
8
|
include TemplateDoiHelpers
|
|
9
9
|
|
|
10
|
-
ALLOWED_KEYS = %w[title description authors
|
|
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
|
|
10
|
+
ALLOWED_KEYS = %w[title description authors license].freeze
|
|
16
11
|
|
|
17
12
|
attr_reader :record, :current_user, :publication, :error
|
|
18
13
|
|
|
@@ -44,82 +39,18 @@ module Labimotion
|
|
|
44
39
|
def normalized_publication
|
|
45
40
|
slice = publication.stringify_keys.slice(*ALLOWED_KEYS)
|
|
46
41
|
slice['authors'] = Array(slice['authors']).filter_map { |a| normalize_author(a) }
|
|
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) }
|
|
52
42
|
slice
|
|
53
43
|
end
|
|
54
44
|
|
|
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
|
|
68
|
-
|
|
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)
|
|
72
|
-
end
|
|
73
|
-
|
|
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
|
|
86
|
-
|
|
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]
|
|
92
|
-
end
|
|
93
|
-
|
|
94
45
|
def normalize_author(author)
|
|
95
46
|
return nil unless author.is_a?(Hash)
|
|
96
47
|
|
|
97
|
-
h = author.stringify_keys.slice(
|
|
48
|
+
h = author.stringify_keys.slice('givenName', 'familyName', 'orcid', 'affiliation', 'affiliation_id')
|
|
98
49
|
return nil if h.values_at('givenName', 'familyName').all? { |v| v.to_s.strip.empty? }
|
|
99
50
|
|
|
100
|
-
ror = normalize_ror(h['ror_id']) || ror_for(h['affiliation_id'])
|
|
101
|
-
ror ? h['ror_id'] = ror : h.delete('ror_id')
|
|
102
51
|
h
|
|
103
52
|
end
|
|
104
53
|
|
|
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
|
-
|
|
123
54
|
def failure(message)
|
|
124
55
|
@error = message
|
|
125
56
|
self
|
|
@@ -12,7 +12,7 @@ module Labimotion
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def self.elements_search(params, current_user, c_id, dl)
|
|
15
|
-
collection = Collection.
|
|
15
|
+
collection = Collection.accessible_for(current_user).find(c_id)
|
|
16
16
|
element_scope = Labimotion::Element.joins(:collections_elements).where('collections_elements.collection_id = ?', collection.id).joins(:element_klass).where('element_klasses.id = elements.element_klass_id AND element_klasses.name = ?', params[:selection][:genericElName])
|
|
17
17
|
element_scope = element_scope.where('elements.name like (?)', "%#{params[:selection][:searchName]}%") if params[:selection][:searchName].present?
|
|
18
18
|
element_scope = element_scope.where('elements.short_label like (?)', "%#{params[:selection][:searchShowLabel]}%") if params[:selection][:searchShowLabel].present?
|
|
@@ -97,7 +97,7 @@ module Labimotion
|
|
|
97
97
|
def self.samples_search(c_id = @c_id)
|
|
98
98
|
sqls = []
|
|
99
99
|
sps = params[:selection][:searchProperties]
|
|
100
|
-
collection = Collection.
|
|
100
|
+
collection = Collection.accessible_for(current_user).find(c_id)
|
|
101
101
|
element_scope = Sample.joins(:collections_samples).where('collections_samples.collection_id = ?', collection.id)
|
|
102
102
|
return element_scope if sps.empty?
|
|
103
103
|
|
|
@@ -51,6 +51,20 @@ module Labimotion
|
|
|
51
51
|
{ "key": "pmol", "label": "pmol", "nm": 1.0e12 }
|
|
52
52
|
]
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
"type": "numeric",
|
|
56
|
+
"field": "areal_density",
|
|
57
|
+
"label": "Areal density",
|
|
58
|
+
"default": "",
|
|
59
|
+
"position": 38,
|
|
60
|
+
"placeholder": "areal density",
|
|
61
|
+
"units": [
|
|
62
|
+
{ "key": "kg_m2", "label": "kg m<sup>-2</sup>", "nm": 1 },
|
|
63
|
+
{ "key": "t_m2", "label": "t m<sup>-2</sup>", "nm": 0.001 },
|
|
64
|
+
{ "key": "kg_cm2", "label": "kg cm<sup>-2</sup>", "nm": 1e-4 },
|
|
65
|
+
{ "key": "g_m2", "label": "g m<sup>-2</sup>", "nm": 1000 }
|
|
66
|
+
]
|
|
67
|
+
},
|
|
54
68
|
{
|
|
55
69
|
"type": "numeric",
|
|
56
70
|
"field": "molarity",
|
|
@@ -93,16 +107,34 @@ module Labimotion
|
|
|
93
107
|
"field": "conductivity",
|
|
94
108
|
"label": "Conductivity",
|
|
95
109
|
"default": "",
|
|
96
|
-
"position":
|
|
110
|
+
"position": 62,
|
|
97
111
|
"placeholder": "conductivity",
|
|
98
|
-
"units": [
|
|
112
|
+
"units": [
|
|
113
|
+
{ "key": "s_m", "label": "S/m", "nm": 1 },
|
|
114
|
+
{ "key": "ms_cm", "label": "mS/cm", "nm": 10 }
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"type": "numeric",
|
|
119
|
+
"field": "currency",
|
|
120
|
+
"label": "Currency",
|
|
121
|
+
"default": "",
|
|
122
|
+
"position": 64,
|
|
123
|
+
"placeholder": "Currency",
|
|
124
|
+
"units": [
|
|
125
|
+
{ "key": "eur", "label": "EUR", "nm": 1, "unit_type": "eur" },
|
|
126
|
+
{ "key": "chf", "label": "CHF", "nm": 1, "unit_type": "chf" },
|
|
127
|
+
{ "key": "gbp", "label": "GBP", "nm": 1, "unit_type": "gbp" },
|
|
128
|
+
{ "key": "usd", "label": "USD", "nm": 1, "unit_type": "usd" },
|
|
129
|
+
{ "key": "jpy", "label": "JPY", "nm": 1, "unit_type": "jpy" }
|
|
130
|
+
]
|
|
99
131
|
},
|
|
100
132
|
{
|
|
101
133
|
"type": "numeric",
|
|
102
134
|
"field": "current",
|
|
103
135
|
"label": "Current",
|
|
104
136
|
"default": "",
|
|
105
|
-
"position":
|
|
137
|
+
"position": 67,
|
|
106
138
|
"placeholder": "Current",
|
|
107
139
|
"units": [
|
|
108
140
|
{ "key": "A", "label": "A", "nm": 1 },
|
|
@@ -251,6 +283,17 @@ module Labimotion
|
|
|
251
283
|
{ "key": "k_j", "label": "kJ", "nm": 1 }
|
|
252
284
|
]
|
|
253
285
|
},
|
|
286
|
+
{
|
|
287
|
+
"type": "numeric",
|
|
288
|
+
"field": "free_energy",
|
|
289
|
+
"label": "Free Energy/enthalpy",
|
|
290
|
+
"default": "",
|
|
291
|
+
"position": 89,
|
|
292
|
+
"placeholder": "Free Energy/enthalpy",
|
|
293
|
+
"units": [
|
|
294
|
+
{ "key": "kj_mol", "label": "kJ/mol", "nm": 1 }
|
|
295
|
+
]
|
|
296
|
+
},
|
|
254
297
|
{
|
|
255
298
|
"type": "numeric",
|
|
256
299
|
"field": "enzyme_activity",
|
|
@@ -260,9 +303,9 @@ module Labimotion
|
|
|
260
303
|
"placeholder": "Enzyme activity",
|
|
261
304
|
"units": [
|
|
262
305
|
{ "key": "u_l", "label": "U/L", "nm": 1 },
|
|
263
|
-
{ "key": "u_ml", "label": "U/mL", "nm":
|
|
264
|
-
{ "key": "u_g", "label": "U/g", "nm":
|
|
265
|
-
{ "key": "u_mg", "label": "U/mg", "nm":
|
|
306
|
+
{ "key": "u_ml", "label": "U/mL", "nm": 1e-3 },
|
|
307
|
+
{ "key": "u_g", "label": "U/g", "nm": 1e-3 },
|
|
308
|
+
{ "key": "u_mg", "label": "U/mg", "nm": 1e-6 }
|
|
266
309
|
]
|
|
267
310
|
},
|
|
268
311
|
{
|
|
@@ -284,9 +327,10 @@ module Labimotion
|
|
|
284
327
|
"position": 100,
|
|
285
328
|
"placeholder": "Flow rate",
|
|
286
329
|
"units": [
|
|
287
|
-
{ "key": "
|
|
288
|
-
{ "key": "ml_min", "label": "
|
|
289
|
-
{ "key": "
|
|
330
|
+
{ "key": "l_m", "label": "L/min", "nm": 1 },
|
|
331
|
+
{ "key": "ml_min", "label": "mL/min", "nm": 1000 },
|
|
332
|
+
{ "key": "ul_min", "label": "µL/min", "nm": 1000000 },
|
|
333
|
+
{ "key": "ml_h", "label": "mL/h", "nm": 60000 }
|
|
290
334
|
]
|
|
291
335
|
},
|
|
292
336
|
{
|
|
@@ -297,19 +341,26 @@ module Labimotion
|
|
|
297
341
|
"position": 103,
|
|
298
342
|
"placeholder": "frequency",
|
|
299
343
|
"units": [
|
|
300
|
-
{ "key": "
|
|
301
|
-
{ "key": "
|
|
302
|
-
{ "key": "
|
|
344
|
+
{ "key": "hz", "label": "Hz", "nm": 1 },
|
|
345
|
+
{ "key": "khz", "label": "kHz", "nm": 1e-3 },
|
|
346
|
+
{ "key": "mhz", "label": "MHz", "nm": 1e-6 }
|
|
303
347
|
]
|
|
304
348
|
},
|
|
305
349
|
{
|
|
306
350
|
"type": "numeric",
|
|
307
351
|
"field": "heating_rate",
|
|
308
|
-
"label": "Heating rate",
|
|
352
|
+
"label": "Heating rate (Temperature ramp)",
|
|
309
353
|
"default": "",
|
|
310
354
|
"position": 106,
|
|
311
355
|
"placeholder": "heating rate",
|
|
312
|
-
"units": [
|
|
356
|
+
"units": [
|
|
357
|
+
{ "key": "k_min", "label": "K/min", "nm": 1 },
|
|
358
|
+
{ "key": "k_s", "label": "K/s", "nm": 0.016666666666667 },
|
|
359
|
+
{ "key": "k_h", "label": "K/h", "nm": 60 },
|
|
360
|
+
{ "key": "degc_min", "label": "°C min<sup>-1</sup>", "nm": 1 },
|
|
361
|
+
{ "key": "degc_s", "label": "°C s<sup>-1</sup>", "nm": 0.016666666666667 },
|
|
362
|
+
{ "key": "degc_h", "label": "°C h<sup>-1</sup>", "nm": 60 }
|
|
363
|
+
]
|
|
313
364
|
},
|
|
314
365
|
{
|
|
315
366
|
"type": "numeric",
|
|
@@ -346,6 +397,29 @@ module Labimotion
|
|
|
346
397
|
{ "key": "ug", "label": "µg", "nm": 1000000 }
|
|
347
398
|
]
|
|
348
399
|
},
|
|
400
|
+
{
|
|
401
|
+
"type": "numeric",
|
|
402
|
+
"field": "mass_fraction",
|
|
403
|
+
"label": "Mass fraction",
|
|
404
|
+
"default": "",
|
|
405
|
+
"position": 123,
|
|
406
|
+
"placeholder": "mass fraction",
|
|
407
|
+
"units": [
|
|
408
|
+
{ "key": "wt_p", "label": "wt.%", "nm": 1 }
|
|
409
|
+
]
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"type": "numeric",
|
|
413
|
+
"field": "mass_loading",
|
|
414
|
+
"label": "Mass loading",
|
|
415
|
+
"default": "",
|
|
416
|
+
"position": 125,
|
|
417
|
+
"placeholder": "mass loading",
|
|
418
|
+
"units": [
|
|
419
|
+
{ "key": "g_cm2", "label": "g cm<sup>-2</sup>", "nm": 1 },
|
|
420
|
+
{ "key": "mg_cm2", "label": "mg cm<sup>-2</sup>", "nm": 1000 }
|
|
421
|
+
]
|
|
422
|
+
},
|
|
349
423
|
{
|
|
350
424
|
"type": "numeric",
|
|
351
425
|
"field": "mass_molecule",
|
|
@@ -358,6 +432,28 @@ module Labimotion
|
|
|
358
432
|
{ "key": "kilo_dalton", "label": "kD", "nm": 1 }
|
|
359
433
|
]
|
|
360
434
|
},
|
|
435
|
+
{
|
|
436
|
+
"type": "numeric",
|
|
437
|
+
"field": "molar_conductivity",
|
|
438
|
+
"label": "Molar conductivity",
|
|
439
|
+
"default": "",
|
|
440
|
+
"position": 127,
|
|
441
|
+
"placeholder": "molar conductivity",
|
|
442
|
+
"units": [
|
|
443
|
+
{ "key": "s_cm2_mol", "label": "S cm<sup>2</sup> mol<sup>-1</sup>", "nm": 1 }
|
|
444
|
+
]
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
"type": "numeric",
|
|
448
|
+
"field": "molar_entropy",
|
|
449
|
+
"label": "Molar Entropy",
|
|
450
|
+
"default": "",
|
|
451
|
+
"position": 128,
|
|
452
|
+
"placeholder": "Molar Entropy",
|
|
453
|
+
"units": [
|
|
454
|
+
{ "key": "j_mol_k", "label": "J/(mol*K)", "nm": 1 }
|
|
455
|
+
]
|
|
456
|
+
},
|
|
361
457
|
{
|
|
362
458
|
"type": "numeric",
|
|
363
459
|
"field": "molecular_weight",
|
|
@@ -402,6 +498,30 @@ module Labimotion
|
|
|
402
498
|
{ "key": "mbar", "label": "mbar", "nm": 1013.25 }
|
|
403
499
|
]
|
|
404
500
|
},
|
|
501
|
+
{
|
|
502
|
+
"type": "numeric",
|
|
503
|
+
"field": "process_rate",
|
|
504
|
+
"label": "Process rate",
|
|
505
|
+
"default": "",
|
|
506
|
+
"position": 143,
|
|
507
|
+
"placeholder": "Process rate",
|
|
508
|
+
"units": [
|
|
509
|
+
{ "key": "1_s", "label": "1/s", "nm": 1 },
|
|
510
|
+
{ "key": "1_min", "label": "1/min", "nm": 60 },
|
|
511
|
+
{ "key": "1_h", "label": "1/h", "nm": 3600 }
|
|
512
|
+
]
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
"type": "numeric",
|
|
516
|
+
"field": "rate_constant",
|
|
517
|
+
"label": "Rate constant",
|
|
518
|
+
"default": "",
|
|
519
|
+
"position": 146,
|
|
520
|
+
"placeholder": "Rate constant",
|
|
521
|
+
"units": [
|
|
522
|
+
{ "key": "1_m_s", "label": "1/(M*s)", "nm": 1 }
|
|
523
|
+
]
|
|
524
|
+
},
|
|
405
525
|
{
|
|
406
526
|
"type": "numeric",
|
|
407
527
|
"field": "reaction_rate",
|
|
@@ -410,8 +530,8 @@ module Labimotion
|
|
|
410
530
|
"position": 150,
|
|
411
531
|
"placeholder": "Reaction rate",
|
|
412
532
|
"units": [
|
|
413
|
-
{ "key": "mol_lmin", "label": "mol
|
|
414
|
-
{ "key": "mol_lsec", "label": "mol
|
|
533
|
+
{ "key": "mol_lmin", "label": "mol L<sup>-1</sup> min<sup>-1</sup>", "nm": 1 },
|
|
534
|
+
{ "key": "mol_lsec", "label": "mol L<sup>-1</sup> s<sup>-1</sup>", "nm": 0.016666666666667 }
|
|
415
535
|
]
|
|
416
536
|
},
|
|
417
537
|
{
|
|
@@ -448,8 +568,9 @@ module Labimotion
|
|
|
448
568
|
"units": [
|
|
449
569
|
{ "key": "ma_g", "label": "mA/g", "nm": 1000, "unit_type": "mass" },
|
|
450
570
|
{ "key": "a_g", "label": "A/g", "nm": 1, "unit_type": "mass" },
|
|
571
|
+
{ "key": "ma_cm2", "label": "mA/cm<sup>2</sup>", "nm": 1000, "unit_type": "area" },
|
|
451
572
|
{ "key": "a_cm2", "label": "A/cm<sup>2</sup>", "nm": 1, "unit_type": "area" },
|
|
452
|
-
{ "key": "
|
|
573
|
+
{ "key": "a_mm2", "label": "A/mm<sup>2</sup>", "nm": 0.01, "unit_type": "area" }
|
|
453
574
|
]
|
|
454
575
|
},
|
|
455
576
|
{
|
|
@@ -495,7 +616,7 @@ module Labimotion
|
|
|
495
616
|
{ "key": "cm_s", "label": "cm/s", "nm": 1 },
|
|
496
617
|
{ "key": "mm_s", "label": "mm/s", "nm": 10 },
|
|
497
618
|
{ "key": "um_m", "label": "µm/min", "nm": 600000 },
|
|
498
|
-
{ "key": "nm_m", "label": "nm/min", "nm":
|
|
619
|
+
{ "key": "nm_m", "label": "nm/min", "nm": 600000000 },
|
|
499
620
|
{ "key": "cm_h", "label": "cm/h", "nm": 3600 },
|
|
500
621
|
{ "key": "mm_h", "label": "mm/h", "nm": 36000 }
|
|
501
622
|
]
|
|
@@ -540,6 +661,31 @@ module Labimotion
|
|
|
540
661
|
{ "key": "K", "label": "K" }
|
|
541
662
|
]
|
|
542
663
|
},
|
|
664
|
+
{
|
|
665
|
+
"type": "numeric",
|
|
666
|
+
"field": "tensile_force",
|
|
667
|
+
"label": "Tensile force",
|
|
668
|
+
"default": "",
|
|
669
|
+
"position": 185,
|
|
670
|
+
"placeholder": "tensile force",
|
|
671
|
+
"units": [
|
|
672
|
+
{ "key": "n", "label": "N", "nm": 1 },
|
|
673
|
+
{ "key": "kn", "label": "kN", "nm": 0.001 },
|
|
674
|
+
{ "key": "mn", "label": "mN", "nm": 1000 }
|
|
675
|
+
]
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
"type": "numeric",
|
|
679
|
+
"field": "tensile_stress",
|
|
680
|
+
"label": "Tensile stress",
|
|
681
|
+
"default": "",
|
|
682
|
+
"position": 186,
|
|
683
|
+
"placeholder": "tensile stress",
|
|
684
|
+
"units": [
|
|
685
|
+
{ "key": "n_cm2", "label": "N cm<sup>-2</sup>", "nm": 1 },
|
|
686
|
+
{ "key": "n_mm2", "label": "N mm<sup>-2</sup>", "nm": 0.01 }
|
|
687
|
+
]
|
|
688
|
+
},
|
|
543
689
|
{
|
|
544
690
|
"type": "numeric",
|
|
545
691
|
"field": "turnover_number",
|
|
@@ -575,7 +721,7 @@ module Labimotion
|
|
|
575
721
|
"field": "voltage",
|
|
576
722
|
"label": "Voltage",
|
|
577
723
|
"default": "",
|
|
578
|
-
"position":
|
|
724
|
+
"position": 206,
|
|
579
725
|
"placeholder": "voltage",
|
|
580
726
|
"units": [
|
|
581
727
|
{ "key": "mv", "label": "mV", "nm": 1000 },
|
|
@@ -597,6 +743,17 @@ module Labimotion
|
|
|
597
743
|
{ "key": "nl", "label": "nl", "nm": 1000000000 }
|
|
598
744
|
]
|
|
599
745
|
},
|
|
746
|
+
{
|
|
747
|
+
"type": "numeric",
|
|
748
|
+
"field": "volume_fraction",
|
|
749
|
+
"label": "Volume fraction",
|
|
750
|
+
"default": "",
|
|
751
|
+
"position": 218,
|
|
752
|
+
"placeholder": "volume fraction",
|
|
753
|
+
"units": [
|
|
754
|
+
{ "key": "vol_p", "label": "vol.%", "nm": 1 }
|
|
755
|
+
]
|
|
756
|
+
},
|
|
600
757
|
{
|
|
601
758
|
"type": "numeric",
|
|
602
759
|
"field": "volumes_metric",
|
|
@@ -610,6 +767,6 @@ module Labimotion
|
|
|
610
767
|
{ "key": "m3", "label": "m³", "nm": 0.000001 }
|
|
611
768
|
]
|
|
612
769
|
}
|
|
613
|
-
|
|
770
|
+
]
|
|
614
771
|
end
|
|
615
772
|
end
|
data/lib/labimotion/version.rb
CHANGED
data/lib/labimotion.rb
CHANGED
|
@@ -148,5 +148,4 @@ module Labimotion
|
|
|
148
148
|
autoload :Datasetable, 'labimotion/models/concerns/datasetable'
|
|
149
149
|
autoload :AttachmentConverter, 'labimotion/models/concerns/attachment_converter'
|
|
150
150
|
autoload :LinkedProperties, 'labimotion/models/concerns/linked_properties'
|
|
151
|
-
autoload :TemplateDoi, 'labimotion/models/concerns/template_doi'
|
|
152
151
|
end
|
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.
|
|
4
|
+
version: 2.3.0.rc1
|
|
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-03 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: caxlsx
|
|
@@ -111,7 +111,6 @@ files:
|
|
|
111
111
|
- lib/labimotion/helpers/vocabulary_helpers.rb
|
|
112
112
|
- lib/labimotion/libs/attachment_handler.rb
|
|
113
113
|
- lib/labimotion/libs/converter.rb
|
|
114
|
-
- lib/labimotion/libs/data/datacite/labimotion_template.html.erb
|
|
115
114
|
- lib/labimotion/libs/data/mapper/Chemwiki.json
|
|
116
115
|
- lib/labimotion/libs/data/mapper/Source.json
|
|
117
116
|
- lib/labimotion/libs/data/vocab/Standard.json
|
|
@@ -137,7 +136,6 @@ files:
|
|
|
137
136
|
- lib/labimotion/models/concerns/linked_properties.rb
|
|
138
137
|
- lib/labimotion/models/concerns/metadata_validation.rb
|
|
139
138
|
- lib/labimotion/models/concerns/segmentable.rb
|
|
140
|
-
- lib/labimotion/models/concerns/template_doi.rb
|
|
141
139
|
- lib/labimotion/models/concerns/workflow.rb
|
|
142
140
|
- lib/labimotion/models/dataset.rb
|
|
143
141
|
- lib/labimotion/models/dataset_klass.rb
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<resource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://datacite.org/schema/kernel-4" xsi:schemaLocation="http://datacite.org/schema/kernel-4 https://schema.datacite.org/meta/kernel-4/metadata.xsd">
|
|
3
|
-
<identifier identifierType="DOI">https://dx.doi.org/<%= full_doi %></identifier>
|
|
4
|
-
<creators>
|
|
5
|
-
<% if creators.empty? %>
|
|
6
|
-
<creator>
|
|
7
|
-
<creatorName>(:unav)</creatorName>
|
|
8
|
-
</creator>
|
|
9
|
-
<% end %>
|
|
10
|
-
<% creators.each do |creator| %>
|
|
11
|
-
<creator>
|
|
12
|
-
<creatorName nameType="Personal"><%= creator['familyName'] %>, <%= creator['givenName'] %></creatorName>
|
|
13
|
-
<givenName><%= creator['givenName'] %></givenName>
|
|
14
|
-
<familyName><%= creator['familyName'] %></familyName>
|
|
15
|
-
<% if creator['orcid'].present? %>
|
|
16
|
-
<nameIdentifier schemeURI="https://orcid.org/" nameIdentifierScheme="ORCID"><%= creator['orcid'] %></nameIdentifier>
|
|
17
|
-
<% end %>
|
|
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
|
-
<% end %>
|
|
21
|
-
</creator>
|
|
22
|
-
<% end %>
|
|
23
|
-
</creators>
|
|
24
|
-
<titles>
|
|
25
|
-
<title xml:lang="en-US"><%= title %></title>
|
|
26
|
-
</titles>
|
|
27
|
-
<%= render_publisher(publisher_config) %>
|
|
28
|
-
<publicationYear><%= publication_year %></publicationYear>
|
|
29
|
-
<subjects>
|
|
30
|
-
<subject>Chemistry</subject>
|
|
31
|
-
</subjects>
|
|
32
|
-
<% if contributors.any? %>
|
|
33
|
-
<contributors>
|
|
34
|
-
<% contributors.each do |contributor| %>
|
|
35
|
-
<contributor contributorType="<%= contributor['contributorType'] %>">
|
|
36
|
-
<contributorName nameType="Personal"><%= contributor['familyName'] %>, <%= contributor['givenName'] %></contributorName>
|
|
37
|
-
<givenName><%= contributor['givenName'] %></givenName>
|
|
38
|
-
<familyName><%= contributor['familyName'] %></familyName>
|
|
39
|
-
<% if contributor['orcid'].present? %>
|
|
40
|
-
<nameIdentifier schemeURI="https://orcid.org/" nameIdentifierScheme="ORCID"><%= contributor['orcid'] %></nameIdentifier>
|
|
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 %>
|
|
45
|
-
</contributor>
|
|
46
|
-
<% end %>
|
|
47
|
-
</contributors>
|
|
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 %>
|
|
56
|
-
<language>en</language>
|
|
57
|
-
<resourceType resourceTypeGeneral="Model"><%= resource_label %></resourceType>
|
|
58
|
-
<version><%= version %></version>
|
|
59
|
-
<rightsList>
|
|
60
|
-
<rights><%= license %></rights>
|
|
61
|
-
</rightsList>
|
|
62
|
-
<descriptions>
|
|
63
|
-
<% if description.present? %>
|
|
64
|
-
<description xml:lang="en-US" descriptionType="Abstract"><%= description %></description>
|
|
65
|
-
<% end %>
|
|
66
|
-
</descriptions>
|
|
67
|
-
</resource>
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# TemplateDoi concern
|
|
4
|
-
#
|
|
5
|
-
# LabIMotion template DOI logic for the host's Doi model. The host owns the DOI
|
|
6
|
-
# record and the generic DataCite plumbing (build_suffix/align_suffix, the MDS
|
|
7
|
-
# client, the DataCitePublisher concern); this adds only the LabIMotion-specific
|
|
8
|
-
# class methods that the gem's template DOI usecases and APIs call on ::Doi.
|
|
9
|
-
#
|
|
10
|
-
# The host wires it up with a single line:
|
|
11
|
-
# class Doi < ApplicationRecord
|
|
12
|
-
# include Labimotion::TemplateDoi
|
|
13
|
-
# end
|
|
14
|
-
#
|
|
15
|
-
# Assumes the host `dois` table has a jsonb `metadata` column (the DOI version
|
|
16
|
-
# is stamped there) and that the deterministic suffix survives Doi#align_suffix
|
|
17
|
-
# unchanged (reserve sets inchikey == suffix and version_count: 0 for that).
|
|
18
|
-
module Labimotion
|
|
19
|
-
module TemplateDoi
|
|
20
|
-
extend ActiveSupport::Concern
|
|
21
|
-
|
|
22
|
-
# Literal namespace shared by every LabIMotion template DOI suffix.
|
|
23
|
-
LABIMOTION_DOI_NAMESPACE = 'labimotion'
|
|
24
|
-
|
|
25
|
-
# ActiveSupport::Concern extends this into the including class, so every
|
|
26
|
-
# method below becomes a class method on the host's Doi.
|
|
27
|
-
module ClassMethods
|
|
28
|
-
# Deterministic DOI suffix for a LabIMotion template klass. The same suffix
|
|
29
|
-
# is used at reserve and at release time, so both share one DOI:
|
|
30
|
-
# element -> labimotion/element/<name>/<identifier>/<doi_version>
|
|
31
|
-
# segment -> labimotion/segment/<element>/<identifier>/<doi_version>
|
|
32
|
-
# dataset -> labimotion/dataset/<ols_term_id>/<identifier>/<doi_version>
|
|
33
|
-
# `<identifier>` is the first UUID group (8 hex chars) of the template's
|
|
34
|
-
# identifier/uuid, falling back to the record id. `<doi_version>` is the DOI
|
|
35
|
-
# version (see .labimotion_version_segment). Callers may pass an explicit
|
|
36
|
-
# template version; otherwise the record's current version is used.
|
|
37
|
-
def build_labimotion_suffix(record, version = nil)
|
|
38
|
-
parts = labimotion_suffix_parts(record)
|
|
39
|
-
raise ArgumentError, "unsupported template: #{record.class}" if parts.nil?
|
|
40
|
-
|
|
41
|
-
segment = labimotion_version_segment(record, version)
|
|
42
|
-
return nil if segment.blank?
|
|
43
|
-
|
|
44
|
-
segments = parts.map { |part| sanitize_doi_part(part) }.reject(&:empty?)
|
|
45
|
-
"#{segments.join('/')}/#{segment}"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# The DOI version segment for a template version. A DOI is published per major
|
|
49
|
-
# release: the .0 release opens that version (X.0 -> "X") and later minor
|
|
50
|
-
# revisions roll into the NEXT DOI (X.y, y>=1 -> "X+1"). Sub-1.0 and
|
|
51
|
-
# unversioned/blank templates map to the first DOI (v1).
|
|
52
|
-
# 0.x -> "1" 1.0 -> "1" 1.1..2.0 -> "2" 2.1..3.0 -> "3"
|
|
53
|
-
def labimotion_version_segment(record, version = nil)
|
|
54
|
-
raw = (version.presence || record.try(:version)).to_s.strip
|
|
55
|
-
parts = raw.split('.')
|
|
56
|
-
major = parts[0].to_i
|
|
57
|
-
minor = parts[1].to_i
|
|
58
|
-
return '1' if major.zero?
|
|
59
|
-
return major.to_s if minor.zero?
|
|
60
|
-
|
|
61
|
-
(major + 1).to_s
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# The DOIs reserved for a LabIMotion template, in reservation order.
|
|
65
|
-
def labimotion_dois(record)
|
|
66
|
-
where(doiable_id: record.id, doiable_type: record.class.name).order(:id).to_a
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# The current (most recently reserved) DOI for a template, or nil.
|
|
70
|
-
def labimotion_latest_doi(record)
|
|
71
|
-
labimotion_dois(record).last
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# The DOI version (next-major integer) recorded on a reserved template DOI.
|
|
75
|
-
def labimotion_doi_version(doi)
|
|
76
|
-
doi.metadata.to_h.dig('labimotion', 'doi_version')
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Metadata stamped on a reserved template DOI so the version it represents
|
|
80
|
-
# survives on the record (used to gate reserving the next version).
|
|
81
|
-
def labimotion_doi_metadata(record, version = nil)
|
|
82
|
-
{ 'labimotion' => { 'doi_version' => labimotion_version_segment(record, version) } }
|
|
83
|
-
end
|
|
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
|
-
|
|
105
|
-
def labimotion_suffix_parts(record)
|
|
106
|
-
# identifier/uuid may both be blank for a draft, which would collapse the
|
|
107
|
-
# scope (colliding across templates). Fall back to the stable record id so
|
|
108
|
-
# every template gets a unique, deterministic suffix. Resolved lazily so an
|
|
109
|
-
# unsupported record (no #id) still falls through to the ArgumentError in the
|
|
110
|
-
# caller. The identifier is shortened to its first UUID group (8 hex chars).
|
|
111
|
-
# A segment's <element> is the name of the ElementKlass it belongs to.
|
|
112
|
-
identifier = lambda do
|
|
113
|
-
raw = record.try(:identifier).presence || record.try(:uuid).presence || record.id
|
|
114
|
-
raw.to_s.split('-').first
|
|
115
|
-
end
|
|
116
|
-
case record
|
|
117
|
-
when ::Labimotion::ElementKlass
|
|
118
|
-
[LABIMOTION_DOI_NAMESPACE, 'element', record.name, identifier.call]
|
|
119
|
-
when ::Labimotion::SegmentKlass
|
|
120
|
-
[LABIMOTION_DOI_NAMESPACE, 'segment', record.element_klass&.name, identifier.call]
|
|
121
|
-
when ::Labimotion::DatasetKlass
|
|
122
|
-
[LABIMOTION_DOI_NAMESPACE, 'dataset', record.ols_term_id, identifier.call]
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# Keeps a suffix segment DOI-safe: trims, collapses whitespace to hyphens and
|
|
127
|
-
# drops anything outside [A-Za-z0-9._-] (colons stripped, as elsewhere here).
|
|
128
|
-
def sanitize_doi_part(value)
|
|
129
|
-
value.to_s.strip.gsub(/\s+/, '-').gsub(/[^A-Za-z0-9._-]/, '')
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
end
|