labimotion 2.0.0 → 2.1.0.rc11

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/lib/labimotion/apis/exporter_api.rb +271 -0
  3. data/lib/labimotion/apis/generic_dataset_api.rb +37 -3
  4. data/lib/labimotion/apis/generic_element_api.rb +106 -4
  5. data/lib/labimotion/apis/generic_klass_api.rb +42 -2
  6. data/lib/labimotion/apis/labimotion_api.rb +1 -0
  7. data/lib/labimotion/apis/segment_api.rb +5 -3
  8. data/lib/labimotion/entities/application_entity.rb +7 -80
  9. data/lib/labimotion/entities/dataset_entity.rb +8 -3
  10. data/lib/labimotion/entities/dataset_klass_entity.rb +3 -2
  11. data/lib/labimotion/entities/element_entity.rb +1 -1
  12. data/lib/labimotion/entities/element_klass_entity.rb +2 -2
  13. data/lib/labimotion/entities/element_revision_entity.rb +3 -1
  14. data/lib/labimotion/entities/eln_element_entity.rb +4 -2
  15. data/lib/labimotion/entities/generic_klass_entity.rb +8 -9
  16. data/lib/labimotion/entities/generic_public_entity.rb +5 -3
  17. data/lib/labimotion/entities/klass_revision_entity.rb +2 -1
  18. data/lib/labimotion/entities/properties_entity.rb +5 -0
  19. data/lib/labimotion/entities/segment_entity.rb +5 -2
  20. data/lib/labimotion/entities/segment_revision_entity.rb +4 -2
  21. data/lib/labimotion/entities/vocabulary_entity.rb +2 -2
  22. data/lib/labimotion/helpers/converter_helpers.rb +16 -3
  23. data/lib/labimotion/helpers/dataset_helpers.rb +31 -6
  24. data/lib/labimotion/helpers/element_helpers.rb +7 -4
  25. data/lib/labimotion/helpers/exporter_helpers.rb +139 -0
  26. data/lib/labimotion/helpers/param_helpers.rb +6 -0
  27. data/lib/labimotion/helpers/segment_helpers.rb +2 -2
  28. data/lib/labimotion/libs/converter.rb +14 -0
  29. data/lib/labimotion/libs/data/layer/StdDataset.json +212 -0
  30. data/lib/labimotion/libs/data/mapper/Chemwiki.json +2 -2
  31. data/lib/labimotion/libs/dataset_builder.rb +2 -1
  32. data/lib/labimotion/libs/export_element.rb +11 -2
  33. data/lib/labimotion/libs/nmr_mapper.rb +13 -0
  34. data/lib/labimotion/libs/properties_handler.rb +12 -2
  35. data/lib/labimotion/libs/sample_association.rb +7 -4
  36. data/lib/labimotion/libs/template_matcher.rb +39 -0
  37. data/lib/labimotion/libs/vocabulary_handler.rb +8 -6
  38. data/lib/labimotion/libs/xlsx_exporter.rb +285 -0
  39. data/lib/labimotion/models/concerns/datasetable.rb +3 -3
  40. data/lib/labimotion/models/concerns/element_fetchable.rb +53 -0
  41. data/lib/labimotion/models/concerns/generic_klass.rb +16 -0
  42. data/lib/labimotion/models/concerns/segmentable.rb +44 -7
  43. data/lib/labimotion/models/dataset_klass.rb +30 -2
  44. data/lib/labimotion/models/device_description.rb +36 -0
  45. data/lib/labimotion/models/element.rb +37 -1
  46. data/lib/labimotion/models/element_klass.rb +12 -1
  47. data/lib/labimotion/models/reaction.rb +36 -0
  48. data/lib/labimotion/models/research_plan.rb +40 -0
  49. data/lib/labimotion/models/sample.rb +36 -0
  50. data/lib/labimotion/models/screen.rb +36 -0
  51. data/lib/labimotion/models/segment_klass.rb +12 -4
  52. data/lib/labimotion/models/wellplate.rb +40 -0
  53. data/lib/labimotion/utils/serializer.rb +2 -0
  54. data/lib/labimotion/utils/units.rb +609 -468
  55. data/lib/labimotion/utils/utils.rb +42 -0
  56. data/lib/labimotion/version.rb +1 -1
  57. data/lib/labimotion.rb +12 -0
  58. metadata +45 -8
@@ -1,88 +1,15 @@
1
1
  # frozen_string_literal: true
2
- #
2
+
3
3
  module Labimotion
4
4
  ## ApplicationEntity
5
- class ApplicationEntity < Grape::Entity
6
- CUSTOM_ENTITY_OPTIONS = %i[anonymize_below anonymize_with].freeze
5
+ # This class serves as the base entity for the Labimotion module,
6
+ # inheriting from the core ApplicationEntity and providing custom formatting.
7
+ class ApplicationEntity < ::Entities::ApplicationEntity
8
+ # Common condition for fields that should not be displayed in list views
9
+ DISPLAYED_IN_LIST_CONDITION = { unless: :displayed_in_list }.freeze
7
10
 
8
11
  format_with(:eln_timestamp) do |datetime|
9
- datetime.present? ? datetime&.strftime('%Y-%m-%d %H:%M:%S %Z') : nil
10
- end
11
-
12
- def self.expose!(*args)
13
- fields = args.first
14
- options = args.last.is_a?(Hash) ? args.pop : {}
15
- options = merge_options(options) # merges additional params set in #with_options
16
- expose_fields_with_anonymization!(fields, options)
17
- end
18
-
19
- # rubocop:disable Metrics/MethodLength
20
- def self.expose_fields_with_anonymization!(fields, options)
21
- anonymize_below = options[:anonymize_below] || 0
22
- anonymize_with = options.key?(:anonymize_with) ? options[:anonymize_with] : '***'
23
-
24
- Array(fields).each do |field|
25
- expose(field, options) do |represented_object, _options|
26
- if detail_levels[represented_object.class] < anonymize_below
27
- anonymize_with
28
- elsif respond_to?(field, true) # Entity has a method with the same name
29
- send(field)
30
- elsif represented_object.respond_to?(field)
31
- represented_object.public_send(field)
32
- else
33
- represented_object[field] # works both for AR and Hash objects
34
- end
35
- end
36
- end
37
- end
38
- private_class_method :expose_fields_with_anonymization!
39
- # rubocop:enable Metrics/MethodLength
40
-
41
- def self.expose_timestamps(timestamp_fields: %i[created_at updated_at], **additional_args)
42
- timestamp_fields.each do |field|
43
- expose field, format_with: :eln_timestamp, **additional_args
44
- end
45
- end
46
-
47
- # overridden method from Grape::Entity to support our custom anonymization options
48
- # https://github.com/ruby-grape/grape-entity/blob/v0.7.1/lib/grape_entity/entity.rb#L565
49
- def self.valid_options(options)
50
- options.each_key do |key|
51
- next if OPTIONS.include?(key) || CUSTOM_ENTITY_OPTIONS.include?(key)
52
-
53
- raise ArgumentError, "#{key.inspect} is not a valid option."
54
- end
55
-
56
- options[:using] = options.delete(:with) if options.key?(:with)
57
- options
58
- end
59
- private_class_method :valid_options
60
-
61
- private
62
-
63
- def displayed_in_list?
64
- options[:displayed_in_list] == true
65
- end
66
-
67
- def current_user
68
- unless options[:current_user]
69
- raise MissingCurrentUserError, "#{self.class} requires a current user to work properly"
70
- end
71
-
72
- options[:current_user]
73
- end
74
-
75
- def detail_levels
76
- maximal_default_levels = Hash.new(10) # every requested detail level will be returned as 10
77
- minimal_default_levels = Hash.new(0) # every requested detail level will be returned as 0
78
- return maximal_default_levels if !options.key?(:detail_levels) || options[:detail_levels].empty?
79
-
80
- # When explicitly configured detail levels are available, we want to return only those and all other
81
- # requests (by using `detail_levels[SomeUnconfiguredModel]`) should return the minimum detail level
82
- minimal_default_levels.merge(options[:detail_levels])
83
- end
84
-
85
- class MissingCurrentUserError < StandardError
12
+ datetime.present? ? datetime.strftime('%Y-%m-%d %H:%M:%S %Z') : nil
86
13
  end
87
14
  end
88
15
  end
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
- #
2
+
3
3
  require 'labimotion/entities/application_entity'
4
+ require 'labimotion/entities/properties_entity'
4
5
  module Labimotion
5
6
  # Dataset entity
6
- class DatasetEntity < PropertiesEntity
7
- expose :id, :dataset_klass_id, :properties, :properties_release, :element_id, :element_type, :klass_ols, :klass_label, :klass_uuid
7
+ class DatasetEntity < Labimotion::PropertiesEntity
8
+ expose :id, :dataset_klass_id, :element_id, :element_type
9
+ expose :klass_ols, :klass_label, :klass_uuid
10
+ expose :properties, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
11
+ expose :properties_release, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
12
+
8
13
  def klass_ols
9
14
  object&.dataset_klass&.ols_term_id
10
15
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'labimotion/entities/generic_klass_entity'
3
4
  module Labimotion
4
- class DatasetKlassEntity < GenericKlassEntity
5
+ class DatasetKlassEntity < Labimotion::GenericKlassEntity
5
6
  expose(
6
- :ols_term_id,
7
+ :ols_term_id
7
8
  )
8
9
  end
9
10
  end
@@ -3,7 +3,7 @@
3
3
  require 'labimotion/entities/properties_entity'
4
4
  ## TODO: Refactor labimotion to use the same entities as chemotion
5
5
  module Labimotion
6
- class ElementEntity < PropertiesEntity
6
+ class ElementEntity < Labimotion::PropertiesEntity
7
7
  with_options(anonymize_below: 0) do
8
8
  expose! :can_copy, unless: :displayed_in_list
9
9
  expose! :can_publish, unless: :displayed_in_list
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
- #
2
+
3
3
  require 'labimotion/entities/generic_klass_entity'
4
4
 
5
5
  module Labimotion
6
6
  # ElementKlassEntity
7
- class ElementKlassEntity < GenericKlassEntity
7
+ class ElementKlassEntity < Labimotion::GenericKlassEntity
8
8
  expose :name, :icon_name, :klass_prefix, :is_generic
9
9
  end
10
10
  end
@@ -3,7 +3,7 @@
3
3
  require 'labimotion/entities/application_entity'
4
4
  module Labimotion
5
5
  # ElementRevisionEntity
6
- class ElementRevisionEntity < ApplicationEntity
6
+ class ElementRevisionEntity < Labimotion::ApplicationEntity
7
7
  expose :id, :element_id, :uuid, :name, :klass_uuid, :properties, :created_at
8
8
  def created_at
9
9
  object.created_at.strftime('%d.%m.%Y, %H:%M')
@@ -13,6 +13,8 @@ module Labimotion
13
13
  object.properties[Labimotion::Prop::LAYERS]&.keys.each do |key|
14
14
  field_sample_molecules = object.properties[Labimotion::Prop::LAYERS][key][Labimotion::Prop::FIELDS].select { |ss| ss['type'] == Labimotion::FieldType::DRAG_SAMPLE || ss['type'] == Labimotion::FieldType::DRAG_MOLECULE }
15
15
  field_sample_molecules.each do |field|
16
+ next unless field['value'].is_a?(Hash)
17
+
16
18
  idx = object.properties[Labimotion::Prop::LAYERS][key][Labimotion::Prop::FIELDS].index(field)
17
19
  sid = field.dig('value', 'el_id')
18
20
  next unless sid.present?
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
- #
2
+
3
3
  require 'labimotion/entities/application_entity'
4
4
  module Labimotion
5
5
  ## ElementEntity
6
- class ElnElementEntity < ApplicationEntity
6
+ class ElnElementEntity < Labimotion::ApplicationEntity
7
7
  with_options(anonymize_below: 0) do
8
8
  expose! :created_by
9
9
  expose! :id
@@ -30,6 +30,8 @@ module Labimotion
30
30
  ss['type'] == Labimotion::FieldType::DRAG_SAMPLE || ss['type'] == Labimotion::FieldType::DRAG_MOLECULE
31
31
  end
32
32
  field_sample_molecules.each do |field|
33
+ next unless field['value'].is_a?(Hash)
34
+
33
35
  idx = object.properties[Labimotion::Prop::LAYERS][key][Labimotion::Prop::FIELDS].index(field)
34
36
  sid = field.dig('value', 'el_id')
35
37
  next unless sid.present?
@@ -1,14 +1,13 @@
1
1
  # frozen_string_literal: true
2
- #
2
+
3
3
  require 'labimotion/entities/application_entity'
4
4
  module Labimotion
5
- # GenericKlassEntity
6
- class GenericKlassEntity < ApplicationEntity
7
- expose :id, :uuid, :label, :desc, :properties_template, :properties_release, :is_active, :version,
8
- :place, :released_at, :identifier, :sync_time, :created_by, :updated_by, :created_at, :updated_at
9
- expose_timestamps(timestamp_fields: [:released_at])
10
- expose_timestamps(timestamp_fields: [:created_at])
11
- expose_timestamps(timestamp_fields: [:updated_at])
12
- expose_timestamps(timestamp_fields: [:sync_time])
5
+ class GenericKlassEntity < Labimotion::ApplicationEntity
6
+ expose :id, :uuid, :label, :desc, :is_active, :version, :place
7
+ expose :released_at, :identifier, :sync_time
8
+
9
+ expose :properties_template, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
10
+ expose :properties_release, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
11
+ expose_timestamps(timestamp_fields: %i[released_at created_at updated_at sync_time])
13
12
  end
14
13
  end
@@ -10,15 +10,17 @@ module Labimotion
10
10
  expose! :desc
11
11
  expose! :icon_name
12
12
  expose! :klass_prefix
13
- expose! :klass_name
13
+ expose :klass_name do |obj|
14
+ obj[:name] || ''
15
+ end
14
16
  expose! :label
15
17
  expose! :identifier
16
18
  expose! :version
17
19
  expose! :released_at
18
- expose! :properties_release, if: :displayed
20
+ expose :properties_release, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
19
21
  expose :element_klass do |obj|
20
22
  if obj[:element_klass_id]
21
- { :label => obj.element_klass.label, :icon_name => obj.element_klass.icon_name }
23
+ { label: obj.element_klass.label, icon_name: obj.element_klass.icon_name, id: obj.element_klass_id }
22
24
  else
23
25
  {}
24
26
  end
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'labimotion/entities/application_entity'
3
4
  module Labimotion
4
5
  # KlassRevisionEntity
5
- class KlassRevisionEntity < ApplicationEntity
6
+ class KlassRevisionEntity < Labimotion::ApplicationEntity
6
7
  expose :id, :uuid, :properties_release, :version, :released_at
7
8
 
8
9
  expose :klass_id do |object|
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'labimotion/entities/application_entity'
2
3
 
3
4
  module Labimotion
@@ -57,6 +58,8 @@ module Labimotion
57
58
  end
58
59
 
59
60
  def update_sample_or_molecule_field(key, field, idx)
61
+ return unless field['value'].is_a?(Hash)
62
+
60
63
  sid = field.dig('value', 'el_id')
61
64
  return unless sid.present?
62
65
 
@@ -78,6 +81,8 @@ module Labimotion
78
81
  end
79
82
 
80
83
  def update_reaction_field(key, field, idx)
84
+ return unless field['value'].is_a?(Hash)
85
+
81
86
  sid = field.dig('value', 'el_id')
82
87
  return unless sid.present?
83
88
 
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'labimotion/entities/application_entity'
4
+ require 'labimotion/entities/properties_entity'
4
5
  module Labimotion
5
6
  ## Segment entity
6
- class SegmentEntity < PropertiesEntity
7
- expose :id, :segment_klass_id, :element_type, :element_id, :properties, :properties_release, :uuid, :klass_uuid, :klass_label
7
+ class SegmentEntity < Labimotion::PropertiesEntity
8
+ expose :id, :segment_klass_id, :element_type, :element_id, :uuid, :klass_uuid, :klass_label
9
+ expose :properties, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
10
+ expose :properties_release, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
8
11
 
9
12
  def klass_label
10
13
  object.segment_klass.label
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
- #
2
+
3
3
  require 'labimotion/entities/application_entity'
4
4
  module Labimotion
5
- class SegmentRevisionEntity < ApplicationEntity
5
+ class SegmentRevisionEntity < Labimotion::ApplicationEntity
6
6
  expose :id, :segment_id, :uuid, :klass_uuid, :properties, :created_at
7
7
  def created_at
8
8
  object.created_at.strftime('%d.%m.%Y, %H:%M')
@@ -12,6 +12,8 @@ module Labimotion
12
12
  object.properties[Labimotion::Prop::LAYERS]&.keys.each do |key|
13
13
  field_sample_molecules = object.properties[Labimotion::Prop::LAYERS][key][Labimotion::Prop::FIELDS].select { |ss| ss['type'] == Labimotion::FieldType::DRAG_SAMPLE || ss['type'] == Labimotion::FieldType::DRAG_MOLECULE }
14
14
  field_sample_molecules.each do |field|
15
+ next unless field['value'].is_a?(Hash)
16
+
15
17
  idx = object.properties[Labimotion::Prop::LAYERS][key][Labimotion::Prop::FIELDS].index(field)
16
18
  sid = field.dig('value', 'el_id')
17
19
  next unless sid.present?
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
- #
2
+
3
3
  require 'labimotion/entities/application_entity'
4
4
  module Labimotion
5
5
  # Dataset entity
6
- class VocabularyEntity < ApplicationEntity
6
+ class VocabularyEntity < Labimotion::ApplicationEntity
7
7
  expose :id, :identifier, :name, :label, :field_type, :opid, :term_id,
8
8
  :field_id, :properties, :source, :source_id, :layer_id
9
9
  expose :voc do |obj|
@@ -2,12 +2,25 @@
2
2
  require 'grape'
3
3
 
4
4
  module Labimotion
5
- ## ElementHelpers
5
+ ## ConverterHelpers
6
6
  module ConverterHelpers
7
7
  extend Grape::API::Helpers
8
8
 
9
- def demo(params)
10
- ### TODO: implement demo
9
+ # Update the general_description field in the container's extended_metadata
10
+ def self.update_general_description(container, current_user, date: nil, time: nil)
11
+ return unless container.present?
12
+
13
+ desc = container.extended_metadata['general_description']
14
+
15
+ general_desc = desc if desc.present? && desc.is_a?(Hash)
16
+ general_desc = JSON.parse(container.extended_metadata['general_description']) if desc.present? && desc.is_a?(String)
17
+ general_desc = {} unless general_desc.is_a?(Hash)
18
+ general_desc['creator'] = current_user.name if current_user.present?
19
+ general_desc['date'] = date if date.present?
20
+ general_desc['time'] = time if time.present?
21
+
22
+ container.extended_metadata['general_description'] = general_desc.to_json
23
+ container.save!
11
24
  end
12
25
  end
13
26
  end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'grape'
3
4
  module Labimotion
4
- ## DatasetHelpers
5
+ ## DatasetHelpers
5
6
  module DatasetHelpers
6
7
  extend Grape::API::Helpers
7
8
 
8
- def klass_list(is_active)
9
+ def klass_list(is_active, displayed_in_list = false)
10
+ scope = displayed_in_list ? Labimotion::DatasetKlass.for_list_display : Labimotion::DatasetKlass.all
9
11
  if is_active == true
10
- Labimotion::DatasetKlass.where(is_active: true).order('place') || []
12
+ scope.where(is_active: true).order('place') || []
11
13
  else
12
- Labimotion::DatasetKlass.all.order('place') || []
14
+ scope.order('place') || []
13
15
  end
14
16
  end
15
17
 
@@ -31,18 +33,41 @@ module Labimotion
31
33
  ds = Labimotion::DatasetKlass.find_by(ols_term_id: attributes['ols_term_id'])
32
34
  ds.update!(attributes)
33
35
  ds.create_klasses_revision(current_user)
34
- { status: 'success', message: "This dataset: [#{attributes['label']}] has been upgraded to the version: #{attributes['version']}!" }
36
+ { status: 'success',
37
+ message: "This dataset: [#{attributes['label']}] has been upgraded to the version: #{attributes['version']}!" }
35
38
  end
36
39
  else
37
40
  attributes['created_by'] = current_user.id
38
41
  ds = Labimotion::DatasetKlass.create!(attributes)
39
42
  ds.create_klasses_revision(current_user)
40
- { status: 'success', message: "The dataset: #{attributes['label']} has been created using version: #{attributes['version']}!" }
43
+ { status: 'success',
44
+ message: "The dataset: #{attributes['label']} has been created using version: #{attributes['version']}!" }
41
45
  end
42
46
  rescue StandardError => e
43
47
  Labimotion.log_exception(e, current_user)
44
48
  # { error: e.message }
45
49
  raise e
46
50
  end
51
+
52
+ def find_best_match_template(ols_term_id)
53
+ result = Labimotion::TemplateMatcher.find_best_match(ols_term_id)
54
+ if result[:template]
55
+ build_template_response(result[:template], result[:match_type], result[:info_messages])
56
+ else
57
+ { error: '', data: {}, info: "No matching template found for term id [#{ols_term_id}]" }
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def build_template_response(template, match_type, info_messages)
64
+ response = {
65
+ error: '',
66
+ data: Labimotion::DatasetKlassEntity.represent(template, displayed_in_list: false),
67
+ match_type: match_type
68
+ }
69
+ response[:info] = info_messages.join('; ') if info_messages.any?
70
+ response
71
+ end
47
72
  end
48
73
  end
@@ -93,12 +93,13 @@ module Labimotion
93
93
  all_coll = Collection.get_all_collection_for_user(current_user.id)
94
94
  element.collections << all_coll
95
95
  element.save!
96
- _properties = update_sample_association(params[:properties], current_user, element)
97
- element.properties = update_vocabularies(_properties, current_user, element)
96
+ element.properties = update_sample_association(params[:properties], current_user, element)
97
+ # element.properties = update_vocabularies(_properties, current_user, element)
98
98
  element.container = update_datamodel(params[:container], current_user)
99
99
  element.save!
100
100
  update_element_labels(element, params[:user_labels], current_user.id)
101
101
  element.save_segments(segments: params[:segments], current_user_id: current_user.id)
102
+ element.save!
102
103
  element
103
104
  rescue StandardError => e
104
105
  Labimotion.log_exception(e, current_user)
@@ -129,9 +130,11 @@ module Labimotion
129
130
  attributes['klass_uuid'] = properties['klass_uuid']
130
131
  element.update_columns(attributes)
131
132
  end
133
+ # element.save_segments(segments: params[:segments], current_user_id: current_user.id)
134
+ element.reload
132
135
  element.save_segments(segments: params[:segments], current_user_id: current_user.id)
133
136
  element.reload
134
- element.properties = update_vocabularies(element.properties, current_user, element)
137
+ # element.properties = update_vocabularies(element.properties, current_user, element)
135
138
  ## element.user_for_revision = current_user
136
139
  element.save!
137
140
  element
@@ -192,7 +195,7 @@ module Labimotion
192
195
  def element_revisions(params)
193
196
  klass = Labimotion::Element.find(params[:id])
194
197
  list = klass.elements_revisions unless klass.nil?
195
- list&.sort_by(&:created_at)&.reverse&.first(10)
198
+ list&.order(created_at: :desc)&.limit(10)
196
199
  rescue StandardError => e
197
200
  Labimotion.log_exception(e, current_user)
198
201
  raise e
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ # ExporterHelpers provides utility methods for formatting and processing export data
5
+ module ExporterHelpers
6
+ # Constants
7
+ CONST_UNNAMED = '(Unnamed)'
8
+
9
+ # Normalize column name, returns CONST_UNNAMED for blank values
10
+ def normalize_column_name(col_name)
11
+ return CONST_UNNAMED if col_name.nil? || col_name.to_s.strip.empty?
12
+
13
+ col_name
14
+ end
15
+
16
+ # Check if field type should be expanded (DRAG_SAMPLE or DRAG_MOLECULE)
17
+ def expandable_field?(field_type)
18
+ [Labimotion::FieldType::DRAG_SAMPLE, Labimotion::FieldType::DRAG_MOLECULE].include?(field_type)
19
+ end
20
+
21
+ # Generate sanitized filename for export
22
+ def generate_filename(element_name, segment_name, layer_name, field_name)
23
+ timestamp = Time.zone.now.strftime('%Y%m%d_%H%M%S')
24
+ parts = [element_name]
25
+ parts << segment_name unless segment_name.empty?
26
+ parts.push(layer_name, field_name, timestamp)
27
+ parts.join('_').gsub(/\s+/, '_').gsub(/[()]/, '(' => '[', ')' => ']')
28
+ end
29
+
30
+ # Format table cell value based on field type
31
+ def format_table_cell(sub_val, sub_field)
32
+ return '' if sub_field.fetch('id', nil).nil? || sub_val[sub_field['id']].nil?
33
+
34
+ case sub_field['type']
35
+ when Labimotion::FieldType::DRAG_SAMPLE, Labimotion::FieldType::DRAG_MOLECULE
36
+ format_drag_field_cell(sub_val, sub_field)
37
+ when Labimotion::FieldType::SELECT
38
+ format_select_cell(sub_val, sub_field)
39
+ when Labimotion::FieldType::SYSTEM_DEFINED
40
+ format_system_defined_cell(sub_val, sub_field)
41
+ else
42
+ format_default_cell(sub_val, sub_field)
43
+ end
44
+ rescue StandardError => e
45
+ Labimotion.log_exception(e)
46
+ ''
47
+ end
48
+
49
+ # Format expanded cell for DRAG_SAMPLE or DRAG_MOLECULE with sub-headers
50
+ def format_expanded_cell(sub_val, exp_field)
51
+ field_id = exp_field['id']
52
+ sub_header = exp_field['sub_header']
53
+
54
+ return '' if field_id.nil? || sub_val[field_id].nil?
55
+
56
+ val = sub_val[field_id]['value'] || {}
57
+ return '' if val.blank? || !val.is_a?(Hash)
58
+
59
+ property_key = map_sub_header_to_property(sub_header)
60
+ val[property_key].to_s
61
+ rescue StandardError => e
62
+ Labimotion.log_exception(e)
63
+ ''
64
+ end
65
+
66
+ # Map sub-header to property key
67
+ def map_sub_header_to_property(sub_header)
68
+ property_map = {
69
+ 'name' => 'el_name',
70
+ 'label' => 'el_label',
71
+ 'short_label' => 'el_short_label',
72
+ 'external_label' => 'el_external_label',
73
+ 'molecular_weight' => 'el_molecular_weight',
74
+ 'smiles' => 'el_smiles',
75
+ 'inchikey' => 'el_inchikey',
76
+ 'iupac' => 'el_iupac',
77
+ 'sum_formula' => 'el_sum_formula',
78
+ 'decoupled' => 'el_decoupled'
79
+ }
80
+
81
+ property_map[sub_header] || "el_#{sub_header}"
82
+ end
83
+
84
+ # Format drag field cell (DRAG_SAMPLE or DRAG_MOLECULE), returns hyperlink or label
85
+ def format_drag_field_cell(sub_val, sub_field)
86
+ val = sub_val[sub_field['id']]['value'] || {}
87
+ return '' if val.blank?
88
+
89
+ svg_url = val['el_svg'].to_s
90
+ if svg_url.present?
91
+ full_url = URI.join(Rails.application.config.root_url, svg_url).to_s
92
+ { hyperlink: full_url, text: '[image link]' }
93
+ else
94
+ val['el_label'].to_s
95
+ end
96
+ end
97
+
98
+ # Format select cell
99
+ def format_select_cell(sub_val, sub_field)
100
+ sub_val[sub_field['id']]['value'].to_s
101
+ end
102
+
103
+ # Format system-defined cell with unit
104
+ def format_system_defined_cell(sub_val, sub_field)
105
+ value = sub_val[sub_field['id']]['value'].to_s
106
+ unit = find_unit_label(sub_val, sub_field)
107
+
108
+ unit.present? ? "#{value} #{unit}" : value
109
+ end
110
+
111
+ # Find unit label for system-defined field
112
+ def find_unit_label(sub_val, sub_field)
113
+ value_system = extract_value_system(sub_val, sub_field)
114
+ find_unit_by_key(sub_field['option_layers'], value_system)
115
+ end
116
+
117
+ # Extract value_system from sub_val or sub_field
118
+ def extract_value_system(sub_val, sub_field)
119
+ field_data = sub_val[sub_field['id']]
120
+ field_data.is_a?(Hash) ? field_data['value_system'] : sub_field['value_system']
121
+ end
122
+
123
+ # Find unit label by field and key
124
+ def find_unit_by_key(option_layers, value_system)
125
+ field_config = Labimotion::Units::FIELDS.find { |o| o[:field] == option_layers }
126
+ return '' unless field_config
127
+
128
+ units = field_config.fetch(:units, [])
129
+ unit = units.find { |u| u[:key] == value_system }
130
+ unit ? unit.fetch(:label, '') : ''
131
+ end
132
+
133
+ # Format default cell
134
+ def format_default_cell(sub_val, sub_field)
135
+ cell_data = sub_val[sub_field['id']]
136
+ cell_data.is_a?(Hash) ? cell_data['value'].to_s : cell_data.to_s
137
+ end
138
+ end
139
+ end
@@ -134,5 +134,11 @@ module Labimotion
134
134
  optional :select_options, type: Hash, desc: 'selections'
135
135
  optional :option_layers, type: String, desc: 'option'
136
136
  end
137
+
138
+ params :table_xlsx_params do
139
+ requires :klass, type: String, desc: 'Generic Type', values: %w[Element Segment]
140
+ requires :layer_id, type: String, desc: 'layer identifier'
141
+ requires :field_id, type: String, desc: 'field identifier'
142
+ end
137
143
  end
138
144
  end
@@ -8,8 +8,8 @@ module Labimotion
8
8
  module SegmentHelpers
9
9
  extend Grape::API::Helpers
10
10
 
11
- def klass_list(el_klass, is_active=false)
12
- scope = Labimotion::SegmentKlass.all
11
+ def klass_list(el_klass, is_active = false, displayed_in_list = false)
12
+ scope = displayed_in_list ? Labimotion::SegmentKlass.for_list_display : Labimotion::SegmentKlass.all
13
13
  scope = scope.where(is_active: is_active) if is_active.present? && is_active == true
14
14
  scope = scope.joins(:element_klass).where(klass_element: params[:element], is_active: true).preload(:element_klass) if el_klass.present?
15
15
  scope.order('place') || []
@@ -6,6 +6,7 @@ require 'json'
6
6
  require 'date'
7
7
  require 'labimotion/version'
8
8
  require 'labimotion/utils/utils'
9
+ require 'labimotion/helpers/converter_helpers'
9
10
 
10
11
  # rubocop: disable Metrics/AbcSize
11
12
  # rubocop: disable Metrics/MethodLength
@@ -228,6 +229,19 @@ module Labimotion
228
229
  def self.update_ds(dataset, dsr, current_user = nil) # rubocop: disable Metrics/PerceivedComplexity
229
230
  layers = dataset.properties[Labimotion::Prop::LAYERS] || {}
230
231
  new_prop = dataset.properties
232
+
233
+ container = dataset.element
234
+
235
+ # Extract and update general description fields
236
+ general_dsr = dsr.select { |ds| ds[:layer] == 'general' }
237
+ if general_dsr.present?
238
+ date = general_dsr.find { |ds| ds[:field] == 'date' }&.dig(:value)
239
+ time = general_dsr.find { |ds| ds[:field] == 'time' }&.dig(:value)
240
+ Labimotion::ConverterHelpers.update_general_description(container, current_user, date: date, time: time)
241
+ else
242
+ Labimotion::ConverterHelpers.update_general_description(container, current_user)
243
+ end
244
+
231
245
  dsr.each do |ds|
232
246
  layer = layers[ds[:layer]]
233
247
  next if layer.blank? || layer[Labimotion::Prop::FIELDS].blank?