labimotion 2.2.0.rc18 → 2.2.0.rc20

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac9e13a86048207de58add4ce3c2670dd3d3ece7b334103a4b8afef774309bd7
4
- data.tar.gz: a21c3957630ea45d248eec5f49999e31424d3ea30c70b3d9b34b11dbe3105c12
3
+ metadata.gz: c9df5db25f00061785cdfbc01efc3bcf1331ce6e028f4059e766a20adbda0c93
4
+ data.tar.gz: b3a45b638d8d2cf2e44cdd97ec0b4306af12f3abc25e44c414b1f49783b38f5e
5
5
  SHA512:
6
- metadata.gz: 7ec1e065535fe7fbfa1c94f82f1f5f06057857aa6439b41fab7e8570b4b584276a9625fd5d806431a6abe3307b147107f6e91469c65b351bcf3b5a9f868d8d83
7
- data.tar.gz: 4ab8e424d00107d029dbe0cdd97740a2d2470994121c1d82b40fd902c5bbd1d1712849756da75631f675666ee39f09a4d56f0839452eb0a29a9b8a78c1ef37bc
6
+ metadata.gz: 7ffd28b485709f2bc8f795e1ecd4dee5fb1a0ebbe412f9fee0dd8635ad5807757f3d88609bb6ca8e5a80c5c8fc2709dc9d2ce70545db4ee53be0ddee84d4bad8
7
+ data.tar.gz: 8e06a2b44be3cbc288838b1aede00a788e740df1f1eacb62467772c8addf610d3644807f13de409a1fee3da362651d01ce92508d5a24c2c89480ff1647e18a4d
@@ -432,6 +432,7 @@ 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'
435
436
  optional :el_type, type: String, desc: 'element klass name'
436
437
  optional :from_date, type: Integer, desc: 'created_date from in ms'
437
438
  optional :to_date, type: Integer, desc: 'created_date to in ms'
@@ -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&.properties_release&.dig(Labimotion::Prop::LAYERS, layer, Labimotion::Prop::FIELDS)&.pluck('field') || []
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,21 +247,27 @@ module Labimotion
247
247
  end
248
248
 
249
249
  def list_serialized_elements(params, current_user)
250
- scope = Labimotion::Element.none
251
-
250
+ collection_id =
252
251
  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
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
260
+
261
+ scope =
262
+ if collection_id
263
+ Labimotion::Element
264
+ .joins(:element_klass, :collections_elements)
265
+ .where(
266
+ element_klasses: { name: params[:el_type] },
267
+ collections_elements: { collection_id: collection_id },
268
+ ).includes(:tag, collections: :sync_collections_users)
262
269
  else
263
- # All collection of current_user
264
- scope = Labimotion::Element.for_user(current_user.id)
270
+ Labimotion::Element.none
265
271
  end
266
272
 
267
273
  ## TO DO: refactor labimotion
@@ -272,7 +278,7 @@ module Labimotion
272
278
  layer, field = params[:sort_column].split('.')
273
279
 
274
280
  element_klass = Labimotion::ElementKlass.find_by(name: params[:el_type])
275
- allowed_fields = element_klass&.properties_release&.dig(Labimotion::Prop::LAYERS, layer, Labimotion::Prop::FIELDS)&.pluck('field') || []
281
+ allowed_fields = element_klass.properties_release.dig(Labimotion::Prop::LAYERS, layer, Labimotion::Prop::FIELDS)&.pluck('field') || []
276
282
 
277
283
  if field.in?(allowed_fields)
278
284
  query = ActiveRecord::Base.sanitize_sql(
@@ -0,0 +1,55 @@
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? %>
19
+ <affiliation><%= 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 contributor %>
33
+ <contributors>
34
+ <contributor contributorType="Researcher">
35
+ <contributorName nameType="Personal"><%= contributor['familyName'] %>, <%= contributor['givenName'] %></contributorName>
36
+ <givenName><%= contributor['givenName'] %></givenName>
37
+ <familyName><%= contributor['familyName'] %></familyName>
38
+ <% if contributor['orcid'].present? %>
39
+ <nameIdentifier schemeURI="https://orcid.org/" nameIdentifierScheme="ORCID"><%= contributor['orcid'] %></nameIdentifier>
40
+ <% end %>
41
+ </contributor>
42
+ </contributors>
43
+ <% end %>
44
+ <language>en</language>
45
+ <resourceType resourceTypeGeneral="Model"><%= resource_label %></resourceType>
46
+ <version><%= version %></version>
47
+ <rightsList>
48
+ <rights><%= license %></rights>
49
+ </rightsList>
50
+ <descriptions>
51
+ <% if description.present? %>
52
+ <description xml:lang="en-US" descriptionType="Abstract"><%= description %></description>
53
+ <% end %>
54
+ </descriptions>
55
+ </resource>
@@ -26,9 +26,9 @@ module Labimotion
26
26
  joins(collections: :user).where(collections: { user_id: user_id })
27
27
  )
28
28
 
29
- # Shared records
29
+ # Shared (synced) records
30
30
  shared = apply_filters.call(
31
- left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
31
+ joins(collections: :sync_collections_users).where(sync_collections_users: { user_id: user_id })
32
32
  )
33
33
 
34
34
  # Combine (remove duplicates), order, and limit
@@ -0,0 +1,113 @@
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
+ def labimotion_suffix_parts(record)
86
+ # identifier/uuid may both be blank for a draft, which would collapse the
87
+ # scope (colliding across templates). Fall back to the stable record id so
88
+ # every template gets a unique, deterministic suffix. Resolved lazily so an
89
+ # unsupported record (no #id) still falls through to the ArgumentError in the
90
+ # caller. The identifier is shortened to its first UUID group (8 hex chars).
91
+ # A segment's <element> is the name of the ElementKlass it belongs to.
92
+ identifier = lambda do
93
+ raw = record.try(:identifier).presence || record.try(:uuid).presence || record.id
94
+ raw.to_s.split('-').first
95
+ end
96
+ case record
97
+ when ::Labimotion::ElementKlass
98
+ [LABIMOTION_DOI_NAMESPACE, 'element', record.name, identifier.call]
99
+ when ::Labimotion::SegmentKlass
100
+ [LABIMOTION_DOI_NAMESPACE, 'segment', record.element_klass&.name, identifier.call]
101
+ when ::Labimotion::DatasetKlass
102
+ [LABIMOTION_DOI_NAMESPACE, 'dataset', record.ols_term_id, identifier.call]
103
+ end
104
+ end
105
+
106
+ # Keeps a suffix segment DOI-safe: trims, collapses whitespace to hyphens and
107
+ # drops anything outside [A-Za-z0-9._-] (colons stripped, as elsewhere here).
108
+ def sanitize_doi_part(value)
109
+ value.to_s.strip.gsub(/\s+/, '-').gsub(/[^A-Za-z0-9._-]/, '')
110
+ end
111
+ end
112
+ end
113
+ end
@@ -141,9 +141,9 @@ module Labimotion
141
141
  joins(collections: :user).where(collections: { user_id: user_id })
142
142
  )
143
143
 
144
- # Shared elements
144
+ # Shared (synced) elements
145
145
  shared = apply_filters.call(
146
- left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
146
+ joins(collections: :sync_collections_users).where(sync_collections_users: { user_id: user_id })
147
147
  )
148
148
 
149
149
  # Combine (remove duplicates), order, and limit
@@ -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',
@@ -29,11 +33,19 @@ module Labimotion
29
33
  def call
30
34
  return nil if @doi.nil?
31
35
 
32
- ERB.new(File.read(TEMPLATE_PATH), trim_mode: '-').result(binding)
36
+ ERB.new(File.read(template_path), trim_mode: '-').result(binding)
33
37
  end
34
38
 
35
39
  private
36
40
 
41
+ # Resolved per call rather than at load time, so the class no longer needs
42
+ # Rails to be booted just to be autoloaded.
43
+ def template_path
44
+ # `Rails.root` is still nil if Rails is loaded but the app has not booted.
45
+ host = defined?(Rails) && Rails.respond_to?(:root) ? Rails.root&.join(HOST_TEMPLATE_PATH) : nil
46
+ host && File.exist?(host) ? host : GEM_TEMPLATE_PATH
47
+ end
48
+
37
49
  def full_doi
38
50
  @doi.full_doi
39
51
  end
@@ -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.accessible_for(current_user).find(c_id)
15
+ collection = Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids).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.accessible_for(current_user).find(c_id)
100
+ collection = Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids).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
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  ## Labimotion Version
4
4
  module Labimotion
5
- VERSION = '2.2.0.rc18'
5
+ VERSION = '2.2.0.rc20'
6
6
  end
data/lib/labimotion.rb CHANGED
@@ -148,4 +148,5 @@ 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'
151
152
  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.2.0.rc18
4
+ version: 2.2.0.rc20
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-01 00:00:00.000000000 Z
12
+ date: 2026-07-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: caxlsx
@@ -111,6 +111,7 @@ 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
114
115
  - lib/labimotion/libs/data/mapper/Chemwiki.json
115
116
  - lib/labimotion/libs/data/mapper/Source.json
116
117
  - lib/labimotion/libs/data/vocab/Standard.json
@@ -136,6 +137,7 @@ files:
136
137
  - lib/labimotion/models/concerns/linked_properties.rb
137
138
  - lib/labimotion/models/concerns/metadata_validation.rb
138
139
  - lib/labimotion/models/concerns/segmentable.rb
140
+ - lib/labimotion/models/concerns/template_doi.rb
139
141
  - lib/labimotion/models/concerns/workflow.rb
140
142
  - lib/labimotion/models/dataset.rb
141
143
  - lib/labimotion/models/dataset_klass.rb