labimotion 2.3.2 → 2.3.3

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: b2682c1c72815cd6376ed94350afc4289a414a4f09c71b9788230ec4430b3f8f
4
- data.tar.gz: 603d30166812d812c7c03fdc3c985cc78b2f47965ec76abbfa4481f07d41b88e
3
+ metadata.gz: 3b0302788cd4be249920160317e9a4bc999fe5446a61cadb5d356620da18290b
4
+ data.tar.gz: 8abc1cf487e158576fe998ae09f75cff30f44ac653525a27393e60cb46de1849
5
5
  SHA512:
6
- metadata.gz: b5dae89c2091aa2f7f4e6b59079d13f3c777c07ba0d0891ecc97daa51b8eb571e90b9517c332d035438aaa45d671184b8d0090db382d3b23fa6e96568cf0e940
7
- data.tar.gz: d66337f46db2b8d49e170bd7078cfae5b45d31fcd8deb92d0c625a09037d4858f21c919339d2547500f5bef0aec67728b7221adb73652961e8ae07c81336aee2
6
+ metadata.gz: 6b03a637a3cc878f8560b806da47301d9edfee557b59d83cb4944cee48df1aca4c35d13a74604f45ab5459a522d443095ecd01a02f35ae61b3ad577015e891b6
7
+ data.tar.gz: 1c7f622321fc6f0d4bac046a9f0b14c94d56dffe3b1cda9e0ee64ca6b0cfd63a09bcf12f41680843e1b4a0313265558658edd0ff64dd62dfd2a04afff27321ca
data/CHANGELOG.md CHANGED
@@ -8,6 +8,7 @@
8
8
  * `number`, `integer` and `system-defined` columns accept numbers only. A cell holding anything else is cleared with a warning rather than stored as text, which also cleans up text those columns already hold.
9
9
  * Import warnings name the cell they came from (`Cell I6: ...`), so the user can go straight to it in the spreadsheet.
10
10
  * Bug fixes:
11
+ * New records and element Reload build from the release's metadata (labimotion#326, port of labimotion#321 and labimotion#324, [chem-generic-ui#1438](https://github.com/cllde8/chem-generic-ui/issues/1438), port of [chem-generic-ui#1430](https://github.com/cllde8/chem-generic-ui/issues/1430)). A klass's `metadata` follows every save, drafts included, so a new element, segment or dataset, and an element brought up to the release by Reload, combined the released layers with the draft's layer groups and restrictions. Klass payloads now carry `metadata_release`: the metadata of the klass's active release, read from its revision row, or `null` when no revision matches the klass's uuid. Sent by `generic_elements/klass`, `segments/list_segment_klass` (nested element klasses included), `generic_dataset/klasses`, and on the nested `element_klass` of the element `GET`, create and update responses, with one revision query per klass family; every other payload is unchanged. A client reading `metadata_release ?? metadata` builds from one version. Read-only: no migration.
11
12
  * A copied element, segment or dataset keeps the template release its properties were built on (labimotion#301, labimotion#303). Saves used to stamp every new segment and every dataset save with the klass's current `uuid` and `properties_release`, and an element with whatever the client sent, so a copy of a v1.0.0 record was labelled v2.0.0 and the client never offered Reload. `Labimotion::KlassVersion` now resolves the submitted `properties['klass_uuid']` against the klass's current release and its `*_klasses_revisions`, deleted revisions included, and stores that uuid together with the matching `properties_release`. A new record whose uuid is missing or unknown follows the current release; an existing record keeps its stored uuid when none is sent, then the uuid its stored `properties_release` names, and follows the current release only when it has neither, and a uuid this instance never released (a record imported from an instance with its own release history) keeps the release stored with the record. A new element whose properties carry no uuid follows the template the client built it on; a new segment does the same, and a copied one falls back to the `klass_uuid` of the row it was copied from. The collection import stamps the release the archive names instead of the importing instance's current one: it tries the uuid inside the archived `properties_release`, then `properties['klass_uuid']`, then the `klass_uuid` column, and stores the first one this instance knows with the local release; when none resolves, the archived release keeps the uuid it names (or, naming none, the first uuid the archive carries), and the imported properties carry the same `klass_uuid` as the column. A `klass_uuid` that is not a string is ignored, and a dataset whose klass changed follows the new klass and ignores the client's release. Element create returns 404 when the template is missing or deleted, element update resolves a soft-deleted template (`with_deleted`) before writing anything, and segments and datasets of a deleted template are skipped on save. A new record whose `klass_uuid` this instance cannot resolve keeps it with the `properties_release` the client sent, only when that release names the same uuid. No migration and no client change.
12
13
  * `fetch_for_user`: the shared-records branch joined `collection_shares` directly on the element model (an association that only exists on `Collection`), raising `ActiveRecord::ConfigurationError` — rescued into empty search results. Now joins through `collections`. Fixed for both the basic elements (`ElementFetchable`) and the generic element (`Labimotion::Element`), so `search_by_like` and `search_basic_by_like` return shared records again.
13
14
  * Dependencies:
@@ -13,7 +13,9 @@ module Labimotion
13
13
  desc 'get dataset klasses'
14
14
  get do
15
15
  list = klass_list(true, false)
16
- present list.sort_by(&:place), with: Labimotion::DatasetKlassEntity, root: 'klass'
16
+ # New datasets are built from these klasses, so they need the release's metadata.
17
+ present list.sort_by(&:place), with: Labimotion::DatasetKlassEntity, root: 'klass',
18
+ metadata_releases: Labimotion::ReleaseMetadata.map_for(list)
17
19
  end
18
20
  end
19
21
 
@@ -39,7 +39,9 @@ module Labimotion
39
39
  end
40
40
  get do
41
41
  ek = Labimotion::ElementKlass.find_by(name: params[:name])
42
- present ek, with: Labimotion::ElementKlassEntity, root: 'klass'
42
+ # The ELN builds a new element from this klass, so it needs the release's metadata.
43
+ present ek, with: Labimotion::ElementKlassEntity, root: 'klass',
44
+ metadata_releases: Labimotion::ReleaseMetadata.map_for([ek])
43
45
  rescue StandardError => e
44
46
  Labimotion.log_exception(e, current_user)
45
47
  { klass: [] }
@@ -474,7 +476,10 @@ module Labimotion
474
476
  element: Labimotion::ElementEntity.represent(
475
477
  element,
476
478
  detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels,
477
- policy: @element_policy
479
+ policy: @element_policy,
480
+ # Reload brings the record up to the release, so it needs the release's metadata,
481
+ # not the latest save's.
482
+ metadata_releases: Labimotion::ReleaseMetadata.map_for([element.element_klass])
478
483
  ),
479
484
  attachments: Entities::AttachmentEntity.represent(element&.attachments)
480
485
  }
@@ -495,6 +500,7 @@ module Labimotion
495
500
  with: Labimotion::ElementEntity,
496
501
  root: :element,
497
502
  detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels,
503
+ metadata_releases: Labimotion::ReleaseMetadata.map_for([element.element_klass])
498
504
  )
499
505
  rescue StandardError => e
500
506
  Labimotion.log_exception(e, current_user)
@@ -518,6 +524,7 @@ module Labimotion
518
524
  element: Labimotion::ElementEntity.represent(
519
525
  element,
520
526
  detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels,
527
+ metadata_releases: Labimotion::ReleaseMetadata.map_for([element.element_klass])
521
528
  ),
522
529
  attachments: Entities::AttachmentEntity.represent(element&.attachments),
523
530
  }
@@ -26,7 +26,11 @@ module Labimotion
26
26
  end
27
27
  get do
28
28
  list = klass_list(nil, params[:is_active], false)
29
- present list, with: Labimotion::SegmentKlassEntity, root: 'klass', displayed_in_list: false
29
+ # New segments are built from these klasses. The nested element klasses read the same
30
+ # map, since they are serialized with the same options.
31
+ metadata_releases = Labimotion::ReleaseMetadata.map_for(list.to_a + list.map(&:element_klass))
32
+ present list, with: Labimotion::SegmentKlassEntity, root: 'klass', displayed_in_list: false,
33
+ metadata_releases: metadata_releases
30
34
  end
31
35
  end
32
36
 
@@ -32,7 +32,7 @@ module Labimotion
32
32
  module Tpa
33
33
  # Segments and layers sent along with reaction variations to a third-party
34
34
  # app (see ExportTpaSegments), as segment-klass label => layer keys.
35
- SEGMENT_LAYERS = { 'Mol interactions' => %w[binding_props bind_partners].freeze }.freeze
35
+ SEGMENT_LAYERS = { 'Mol interactions' => %w[general binding_props bind_partners].freeze }.freeze
36
36
  end
37
37
  end
38
38
  end
@@ -10,5 +10,16 @@ module Labimotion
10
10
  expose :properties_release, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
11
11
  expose :metadata, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
12
12
  expose_timestamps(timestamp_fields: %i[released_at created_at updated_at sync_time])
13
+
14
+ # The klass's own `metadata` follows every save, drafts included, so a client building a
15
+ # record from `properties_release` reads this instead: the metadata that release was cut
16
+ # with, or nil when no revision matches the klass's uuid (never released, or imported
17
+ # without one). Exposed only where the endpoint preloaded the map, so no other payload pays
18
+ # a revision query per row.
19
+ expose :metadata_release, **DISPLAYED_IN_LIST_CONDITION, if: :metadata_releases
20
+
21
+ def metadata_release
22
+ options[:metadata_releases][Labimotion::ReleaseMetadata.key_of(object)]
23
+ end
13
24
  end
14
25
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # `underscore`, `constantize` and `index_by` are ActiveSupport core extensions, not plain
4
+ # Ruby. A booted host always has them; declaring them here keeps this file from depending on
5
+ # load order.
6
+ require 'active_support/core_ext/enumerable'
7
+ require 'active_support/core_ext/string/inflections'
8
+
9
+ module Labimotion
10
+ ## ReleaseMetadata
11
+ # Builds the `metadata_releases:` option the klass entities read: each klass's metadata as
12
+ # of its active release. The klass row's own `metadata` follows every save, drafts included,
13
+ # so a client building a record from `properties_release` needs the copy kept on the revision.
14
+ # The active revision is the one whose uuid equals the klass's, which changes only on release.
15
+ class ReleaseMetadata
16
+ # A klass with no revision matching its uuid (never released, or imported without one)
17
+ # is left out of the map, and the entity reads it as nil.
18
+ def self.map_for(klasses)
19
+ Array(klasses).compact.group_by(&:class).each_with_object({}) do |(model, group), map|
20
+ map.merge!(map_for_family(model, group))
21
+ end
22
+ end
23
+
24
+ # Family as well as id: a segment klass payload nests its element klass, and ids repeat
25
+ # across the klass tables.
26
+ def self.key_of(klass)
27
+ "#{klass.class.name.split('::').last}:#{klass.id}"
28
+ end
29
+
30
+ def self.map_for_family(model, klasses)
31
+ by_pair = klasses.reject { |klass| klass.uuid.nil? }.index_by { |klass| [klass.id, klass.uuid] }
32
+ return {} if by_pair.empty?
33
+
34
+ revisions_for(model, by_pair.keys).each_with_object({}) do |(klass_id, uuid, metadata), map|
35
+ # The IN lists cross-match: a revision of one listed klass can carry the uuid of
36
+ # another. Only the exact pair is that klass's active release.
37
+ klass = by_pair[[klass_id, uuid]]
38
+ map[key_of(klass)] = metadata unless klass.nil?
39
+ end
40
+ rescue StandardError => e
41
+ # The release metadata is an addition to a list that must still render, and a failing
42
+ # family must not blank the families that resolved.
43
+ Labimotion.log_exception(e)
44
+ {}
45
+ end
46
+
47
+ # The revision model and its foreign key are named after the klass model.
48
+ def self.revisions_for(model, pairs)
49
+ foreign_key = "#{model.name.underscore.split('/').last}_id"
50
+ "#{model.name}esRevision".constantize
51
+ .where(foreign_key => pairs.map(&:first), uuid: pairs.map(&:last))
52
+ .pluck(foreign_key, :uuid, :metadata)
53
+ end
54
+ private_class_method :map_for_family, :revisions_for
55
+ end
56
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  ## Labimotion Version
4
4
  module Labimotion
5
- VERSION = '2.3.2'
5
+ VERSION = '2.3.3'
6
6
  end
data/lib/labimotion.rb CHANGED
@@ -95,6 +95,7 @@ module Labimotion
95
95
  autoload :SampleAssociation, 'labimotion/libs/sample_association'
96
96
  autoload :PropertiesHandler, 'labimotion/libs/properties_handler'
97
97
  autoload :KlassVersion, 'labimotion/libs/klass_version'
98
+ autoload :ReleaseMetadata, 'labimotion/libs/release_metadata'
98
99
  autoload :AttachmentHandler, 'labimotion/libs/attachment_handler'
99
100
  autoload :VocabularyHandler, 'labimotion/libs/vocabulary_handler'
100
101
  autoload :XlsxExporter, 'labimotion/libs/xlsx_exporter'
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.3.2
4
+ version: 2.3.3
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-10-02 00:00:00.000000000 Z
12
+ date: 2026-10-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: caxlsx
@@ -140,6 +140,7 @@ files:
140
140
  - lib/labimotion/libs/klass_version.rb
141
141
  - lib/labimotion/libs/nmr_mapper.rb
142
142
  - lib/labimotion/libs/properties_handler.rb
143
+ - lib/labimotion/libs/release_metadata.rb
143
144
  - lib/labimotion/libs/sample_association.rb
144
145
  - lib/labimotion/libs/template_hub.rb
145
146
  - lib/labimotion/libs/template_matcher.rb