labimotion 2.4.0.rc2 → 2.4.0.rc4

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/lib/labimotion/apis/generic_dataset_api.rb +41 -3
  4. data/lib/labimotion/apis/generic_element_api.rb +60 -8
  5. data/lib/labimotion/apis/generic_klass_api.rb +50 -6
  6. data/lib/labimotion/apis/klass_share_api.rb +646 -0
  7. data/lib/labimotion/apis/labimotion_api.rb +1 -0
  8. data/lib/labimotion/apis/labimotion_doi_api.rb +24 -10
  9. data/lib/labimotion/apis/labimotion_template_browse_api.rb +13 -1
  10. data/lib/labimotion/apis/segment_api.rb +61 -10
  11. data/lib/labimotion/constants.rb +14 -0
  12. data/lib/labimotion/entities/application_entity.rb +13 -1
  13. data/lib/labimotion/entities/generic_klass_entity.rb +125 -0
  14. data/lib/labimotion/entities/klass_share_entity.rb +48 -0
  15. data/lib/labimotion/helpers/dataset_helpers.rb +123 -1
  16. data/lib/labimotion/helpers/element_helpers.rb +206 -4
  17. data/lib/labimotion/helpers/generic_helpers.rb +276 -4
  18. data/lib/labimotion/helpers/param_helpers.rb +68 -0
  19. data/lib/labimotion/helpers/segment_helpers.rb +87 -4
  20. data/lib/labimotion/libs/ai_egress_guard.rb +84 -0
  21. data/lib/labimotion/libs/ai_template.rb +1482 -0
  22. data/lib/labimotion/libs/data/datacite/labimotion_template.html.erb +67 -0
  23. data/lib/labimotion/libs/file_extractor.rb +210 -0
  24. data/lib/labimotion/libs/owner_resolver.rb +50 -0
  25. data/lib/labimotion/libs/ownership_audit.rb +73 -0
  26. data/lib/labimotion/libs/share_notifier.rb +114 -0
  27. data/lib/labimotion/libs/share_resolver.rb +369 -0
  28. data/lib/labimotion/models/concerns/template_doi.rb +133 -0
  29. data/lib/labimotion/models/dataset_klass.rb +1 -1
  30. data/lib/labimotion/models/element_klass.rb +1 -1
  31. data/lib/labimotion/models/klass_share.rb +126 -0
  32. data/lib/labimotion/models/segment_klass.rb +1 -1
  33. data/lib/labimotion/usecases/build_template_doi_xml.rb +69 -23
  34. data/lib/labimotion/usecases/release_template_doi.rb +42 -17
  35. data/lib/labimotion/usecases/template_doi_helpers.rb +28 -6
  36. data/lib/labimotion/usecases/update_template_publication_metadata.rb +71 -2
  37. data/lib/labimotion/utils/prop.rb +1 -0
  38. data/lib/labimotion/version.rb +1 -1
  39. data/lib/labimotion.rb +11 -0
  40. metadata +28 -2
@@ -11,7 +11,11 @@ module Labimotion
11
11
  class BuildTemplateDoiXml
12
12
  include ::DataCitePublisher
13
13
 
14
- TEMPLATE_PATH = Rails.root.join('app/publish/datacite_metadata_labimotion_template.html.erb')
14
+ # The template ships with the gem. A host app may still override it by
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'
15
19
 
16
20
  RESOURCE_LABEL = {
17
21
  'element' => 'LabIMotion Element Template',
@@ -19,21 +23,28 @@ module Labimotion
19
23
  'dataset' => 'LabIMotion Dataset Template'
20
24
  }.freeze
21
25
 
22
- def initialize(record, doi = nil, current_user = nil)
26
+ def initialize(record, doi = nil)
23
27
  @record = record
24
28
  @doi = doi || ::Doi.labimotion_latest_doi(record)
25
- @current_user = current_user
26
29
  end
27
30
 
28
31
  # Returns the XML string, or nil when no DOI has been reserved yet.
29
32
  def call
30
33
  return nil if @doi.nil?
31
34
 
32
- ERB.new(File.read(TEMPLATE_PATH), trim_mode: '-').result(binding)
35
+ ERB.new(File.read(template_path), trim_mode: '-').result(binding)
33
36
  end
34
37
 
35
38
  private
36
39
 
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
+
37
48
  def full_doi
38
49
  @doi.full_doi
39
50
  end
@@ -65,35 +76,70 @@ module Labimotion
65
76
  end
66
77
 
67
78
  def creators
68
- Array(publication['authors']).filter_map do |author|
69
- next unless author.is_a?(Hash)
70
-
71
- given = author['givenName'].to_s.strip
72
- family = author['familyName'].to_s.strip
73
- next if given.empty? && family.empty?
79
+ Array(publication['authors']).filter_map { |author| person(author) }
80
+ end
74
81
 
75
- { 'givenName' => h(given), 'familyName' => h(family),
76
- 'orcid' => h(author['orcid'].to_s.strip), 'affiliation' => h(author['affiliation'].to_s.strip) }
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)))
77
92
  end
78
93
  end
79
94
 
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
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
100
+
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)
85
107
 
86
- given = user.try(:first_name).to_s.strip
87
- family = user.try(:last_name).to_s.strip
108
+ given = entry['givenName'].to_s.strip
109
+ family = entry['familyName'].to_s.strip
88
110
  return nil if given.empty? && family.empty?
89
111
 
90
- orcid = user.respond_to?(:orcid) ? user.orcid.to_s.strip : ''
91
- { 'givenName' => h(given), 'familyName' => h(family), 'orcid' => h(orcid) }
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}") }
92
116
  end
93
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
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.
94
137
  def publication
95
- @publication ||=
96
- (@record.properties_template.is_a?(Hash) ? @record.properties_template['publication'] : nil) || {}
138
+ @publication ||= ::Doi.labimotion_doi_publication(@doi).presence || record_publication
139
+ end
140
+
141
+ def record_publication
142
+ (@record.properties_template.is_a?(Hash) ? @record.properties_template['publication'] : nil) || {}
97
143
  end
98
144
 
99
145
  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
- return failure('publication metadata is incomplete') unless metadata_ready?
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
- 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
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
- doi.update!(minted: true, minted_at: Time.current)
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,26 @@ module Labimotion
73
85
  record.update!(attrs) if attrs.any?
74
86
  end
75
87
 
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)
88
+ # Everything the publication form owns that DataCite needs, named so the
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
+ 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
79
101
 
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? }
102
+ # The template's current publication metadata — what this release publishes.
103
+ def publication
104
+ @publication ||= begin
105
+ pub = record.properties_template.is_a?(Hash) ? record.properties_template['publication'] : nil
106
+ pub.is_a?(Hash) ? pub : {}
107
+ end
83
108
  end
84
109
 
85
110
  def failure(message)
@@ -15,11 +15,29 @@ module Labimotion
15
15
  # admin rights over it. Keys are plural, matching the LabIMotion gem's
16
16
  # `authenticate_admin!` and `User#generic_admin`.
17
17
  GENERIC_ADMIN_KEY = {
18
- ::Labimotion::ElementKlass => 'elements',
19
- ::Labimotion::SegmentKlass => 'segments',
20
- ::Labimotion::DatasetKlass => 'datasets'
18
+ ::Labimotion::ElementKlass => ::Labimotion::Constants::Family::ELEMENT,
19
+ ::Labimotion::SegmentKlass => ::Labimotion::Constants::Family::SEGMENT,
20
+ ::Labimotion::DatasetKlass => ::Labimotion::Constants::Family::DATASET
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
- "#{host.chomp('/')}/home/genericHub?#{query}"
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 license].freeze
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,18 +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) }
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) }
42
52
  slice
43
53
  end
44
54
 
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
+
45
94
  def normalize_author(author)
46
95
  return nil unless author.is_a?(Hash)
47
96
 
48
- h = author.stringify_keys.slice('givenName', 'familyName', 'orcid', 'affiliation', 'affiliation_id')
97
+ h = author.stringify_keys.slice(*PERSON_KEYS)
49
98
  return nil if h.values_at('givenName', 'familyName').all? { |v| v.to_s.strip.empty? }
50
99
 
100
+ ror = normalize_ror(h['ror_id']) || ror_for(h['affiliation_id'])
101
+ ror ? h['ror_id'] = ror : h.delete('ror_id')
51
102
  h
52
103
  end
53
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
+
54
123
  def failure(message)
55
124
  @error = message
56
125
  self
@@ -1,6 +1,7 @@
1
1
  module Labimotion
2
2
  ## Converter State
3
3
  class Prop
4
+ CONVERTER_FIELD_UINT_PREFIX = '___unit___'.freeze
4
5
  LAYERS = 'layers'.freeze
5
6
  FIELDS = 'fields'.freeze
6
7
  SUBFIELDS = 'sub_fields'.freeze
@@ -2,5 +2,5 @@
2
2
 
3
3
  ## Labimotion Version
4
4
  module Labimotion
5
- VERSION = '2.4.0.rc2'
5
+ VERSION = '2.4.0.rc4'
6
6
  end
data/lib/labimotion.rb CHANGED
@@ -29,6 +29,7 @@ module Labimotion
29
29
  autoload :VocabularyAPI, 'labimotion/apis/vocabulary_api'
30
30
  autoload :UserAPI, 'labimotion/apis/user_api'
31
31
  autoload :UserKlassSettingsAPI, 'labimotion/apis/user_klass_settings_api'
32
+ autoload :KlassShareAPI, 'labimotion/apis/klass_share_api'
32
33
  autoload :MttAPI, 'labimotion/apis/mtt_api'
33
34
  autoload :ElementVariationAPI, 'labimotion/apis/element_variation_api'
34
35
  autoload :LabimotionDoiAPI, 'labimotion/apis/labimotion_doi_api'
@@ -58,6 +59,7 @@ module Labimotion
58
59
  autoload :VocabularyEntity, 'labimotion/entities/vocabulary_entity'
59
60
  autoload :UserEntity, 'labimotion/entities/user_entity'
60
61
  autoload :UserKlassSettingEntity, 'labimotion/entities/user_klass_setting_entity'
62
+ autoload :KlassShareEntity, 'labimotion/entities/klass_share_entity'
61
63
  autoload :ElementVariationEntity, 'labimotion/entities/element_variation_entity'
62
64
  autoload :LabimotionTemplateDoiEntity, 'labimotion/entities/labimotion_template_doi_entity'
63
65
 
@@ -82,6 +84,9 @@ module Labimotion
82
84
  autoload :UpdateTemplatePublicationMetadata, 'labimotion/usecases/update_template_publication_metadata'
83
85
 
84
86
  ######## Libs
87
+ autoload :AiTemplate, 'labimotion/libs/ai_template'
88
+ autoload :AiEgressGuard, 'labimotion/libs/ai_egress_guard'
89
+ autoload :FileExtractor, 'labimotion/libs/file_extractor'
85
90
  autoload :Converter, 'labimotion/libs/converter'
86
91
  autoload :DatasetBuilder, 'labimotion/libs/dataset_builder'
87
92
  autoload :NmrMapper, 'labimotion/libs/nmr_mapper'
@@ -99,6 +104,10 @@ module Labimotion
99
104
  autoload :ElementVariationHeader, 'labimotion/libs/element_variation_header'
100
105
  autoload :ExportElementVariations, 'labimotion/libs/export_element_variations'
101
106
  autoload :ImportElementVariations, 'labimotion/libs/import_element_variations'
107
+ autoload :OwnerResolver, 'labimotion/libs/owner_resolver'
108
+ autoload :OwnershipAudit, 'labimotion/libs/ownership_audit'
109
+ autoload :ShareNotifier, 'labimotion/libs/share_notifier'
110
+ autoload :ShareResolver, 'labimotion/libs/share_resolver'
102
111
 
103
112
  ######## Utils
104
113
  autoload :Prop, 'labimotion/utils/prop'
@@ -137,6 +146,7 @@ module Labimotion
137
146
  autoload :StdLayer, 'labimotion/models/std_layer'
138
147
  autoload :StdLayersRevision, 'labimotion/models/std_layers_revision'
139
148
  autoload :UserKlassSetting, 'labimotion/models/user_klass_setting'
149
+ autoload :KlassShare, 'labimotion/models/klass_share'
140
150
 
141
151
  autoload :CellLine, 'labimotion/models/cellline'
142
152
  autoload :DeviceDescription, 'labimotion/models/device_description'
@@ -157,4 +167,5 @@ module Labimotion
157
167
  autoload :Datasetable, 'labimotion/models/concerns/datasetable'
158
168
  autoload :AttachmentConverter, 'labimotion/models/concerns/attachment_converter'
159
169
  autoload :LinkedProperties, 'labimotion/models/concerns/linked_properties'
170
+ autoload :TemplateDoi, 'labimotion/models/concerns/template_doi'
160
171
  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.0.rc2
4
+ version: 2.4.0.rc4
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-10 00:00:00.000000000 Z
12
+ date: 2026-08-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: caxlsx
@@ -25,6 +25,20 @@ dependencies:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
27
  version: '4.0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: pdf-reader
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '2.11'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '2.11'
28
42
  - !ruby/object:Gem::Dependency
29
43
  name: rails
30
44
  requirement: !ruby/object:Gem::Requirement
@@ -77,6 +91,7 @@ files:
77
91
  - lib/labimotion/apis/generic_dataset_api.rb
78
92
  - lib/labimotion/apis/generic_element_api.rb
79
93
  - lib/labimotion/apis/generic_klass_api.rb
94
+ - lib/labimotion/apis/klass_share_api.rb
80
95
  - lib/labimotion/apis/labimotion_api.rb
81
96
  - lib/labimotion/apis/labimotion_doi_api.rb
82
97
  - lib/labimotion/apis/labimotion_hub_api.rb
@@ -105,6 +120,7 @@ files:
105
120
  - lib/labimotion/entities/generic_klass_entity.rb
106
121
  - lib/labimotion/entities/generic_public_entity.rb
107
122
  - lib/labimotion/entities/klass_revision_entity.rb
123
+ - lib/labimotion/entities/klass_share_entity.rb
108
124
  - lib/labimotion/entities/labimotion_template_doi_entity.rb
109
125
  - lib/labimotion/entities/properties_entity.rb
110
126
  - lib/labimotion/entities/segment_entity.rb
@@ -125,8 +141,11 @@ files:
125
141
  - lib/labimotion/helpers/search_helpers.rb
126
142
  - lib/labimotion/helpers/segment_helpers.rb
127
143
  - lib/labimotion/helpers/vocabulary_helpers.rb
144
+ - lib/labimotion/libs/ai_egress_guard.rb
145
+ - lib/labimotion/libs/ai_template.rb
128
146
  - lib/labimotion/libs/attachment_handler.rb
129
147
  - lib/labimotion/libs/converter.rb
148
+ - lib/labimotion/libs/data/datacite/labimotion_template.html.erb
130
149
  - lib/labimotion/libs/data/mapper/Chemwiki.json
131
150
  - lib/labimotion/libs/data/mapper/Source.json
132
151
  - lib/labimotion/libs/data/vocab/Standard.json
@@ -137,11 +156,16 @@ files:
137
156
  - lib/labimotion/libs/export_dataset.rb
138
157
  - lib/labimotion/libs/export_element.rb
139
158
  - lib/labimotion/libs/export_element_variations.rb
159
+ - lib/labimotion/libs/file_extractor.rb
140
160
  - lib/labimotion/libs/import_element_variations.rb
141
161
  - lib/labimotion/libs/linked_element.rb
142
162
  - lib/labimotion/libs/nmr_mapper.rb
163
+ - lib/labimotion/libs/owner_resolver.rb
164
+ - lib/labimotion/libs/ownership_audit.rb
143
165
  - lib/labimotion/libs/properties_handler.rb
144
166
  - lib/labimotion/libs/sample_association.rb
167
+ - lib/labimotion/libs/share_notifier.rb
168
+ - lib/labimotion/libs/share_resolver.rb
145
169
  - lib/labimotion/libs/template_hub.rb
146
170
  - lib/labimotion/libs/template_matcher.rb
147
171
  - lib/labimotion/libs/vocabulary_handler.rb
@@ -158,6 +182,7 @@ files:
158
182
  - lib/labimotion/models/concerns/linked_properties.rb
159
183
  - lib/labimotion/models/concerns/metadata_validation.rb
160
184
  - lib/labimotion/models/concerns/segmentable.rb
185
+ - lib/labimotion/models/concerns/template_doi.rb
161
186
  - lib/labimotion/models/concerns/workflow.rb
162
187
  - lib/labimotion/models/dataset.rb
163
188
  - lib/labimotion/models/dataset_klass.rb
@@ -175,6 +200,7 @@ files:
175
200
  - lib/labimotion/models/elements_sample.rb
176
201
  - lib/labimotion/models/elements_wellplate.rb
177
202
  - lib/labimotion/models/hub_log.rb
203
+ - lib/labimotion/models/klass_share.rb
178
204
  - lib/labimotion/models/reaction.rb
179
205
  - lib/labimotion/models/research_plan.rb
180
206
  - lib/labimotion/models/sample.rb