labimotion 2.1.0.rc13 → 2.1.0.rc15

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/labimotion/apis/generic_element_api.rb +3 -2
  3. data/lib/labimotion/apis/labimotion_api.rb +3 -1
  4. data/lib/labimotion/apis/labimotion_hub_api.rb +83 -10
  5. data/lib/labimotion/collection/import.rb +3 -3
  6. data/lib/labimotion/constants.rb +7 -0
  7. data/lib/labimotion/entities/dataset_entity.rb +1 -0
  8. data/lib/labimotion/entities/element_entity.rb +1 -0
  9. data/lib/labimotion/entities/element_revision_entity.rb +1 -1
  10. data/lib/labimotion/entities/generic_klass_entity.rb +1 -0
  11. data/lib/labimotion/entities/generic_public_entity.rb +1 -0
  12. data/lib/labimotion/entities/klass_revision_entity.rb +5 -8
  13. data/lib/labimotion/entities/segment_entity.rb +1 -0
  14. data/lib/labimotion/entities/segment_revision_entity.rb +1 -1
  15. data/lib/labimotion/helpers/dataset_helpers.rb +1 -1
  16. data/lib/labimotion/helpers/element_helpers.rb +7 -3
  17. data/lib/labimotion/helpers/generic_helpers.rb +1 -0
  18. data/lib/labimotion/helpers/param_helpers.rb +6 -0
  19. data/lib/labimotion/helpers/segment_helpers.rb +1 -1
  20. data/lib/labimotion/libs/converter.rb +2 -1
  21. data/lib/labimotion/libs/dataset_builder.rb +2 -1
  22. data/lib/labimotion/libs/template_hub.rb +28 -10
  23. data/lib/labimotion/models/concerns/datasetable.rb +10 -6
  24. data/lib/labimotion/models/concerns/generic_klass_revisions.rb +3 -3
  25. data/lib/labimotion/models/concerns/generic_revisions.rb +2 -1
  26. data/lib/labimotion/models/concerns/klass_revision.rb +23 -0
  27. data/lib/labimotion/models/concerns/metadata_validation.rb +34 -0
  28. data/lib/labimotion/models/concerns/segmentable.rb +2 -2
  29. data/lib/labimotion/models/dataset_klass.rb +2 -0
  30. data/lib/labimotion/models/dataset_klasses_revision.rb +6 -1
  31. data/lib/labimotion/models/element_klass.rb +2 -0
  32. data/lib/labimotion/models/element_klasses_revision.rb +5 -2
  33. data/lib/labimotion/models/segment_klass.rb +2 -0
  34. data/lib/labimotion/models/segment_klasses_revision.rb +6 -1
  35. data/lib/labimotion/models/template_submission.rb +52 -0
  36. data/lib/labimotion/version.rb +1 -1
  37. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2d91570fdadd94114fe2f9d718500c5b63952aedc8d4b3c70284f50f0332aa7
4
- data.tar.gz: 2aa299d45b1136966622d165fbb4f09416d9f13b9d7e11c8a857c14c6499dfe4
3
+ metadata.gz: 00f664a1f81cf5face58aaffb1855e0056643ae6e624ec01a41299e2097d4f78
4
+ data.tar.gz: f2591aee3db2b0de0019f7a85191b1bf0b84f1ee5ebf9ada9861782115fd415c
5
5
  SHA512:
6
- metadata.gz: 52896a77e3e27514c0216b081e1899878097f313dab59051bd6a2916752f50bdf4b55d11dbf87c08484c8c4a8e298415dcffe2257b6fd8585ec09de7f064931c
7
- data.tar.gz: 5e870746f14902a8e23dd42be15c18f0eb798cc313428f917e8dd8d06ccc0b601e04bae967ffd5a008274e2f1542f7c292e28e228846c5a1b70fed027b68a62c
6
+ metadata.gz: fcf9e9ee81ce54790ad7005f5b9bb6143052f1c5c0b5fcfe3fb4fd1d88c49603d8b91783dd997f0aa5a2242a22aaa34e77bcd9d7680006d4aab4d945e2b82785
7
+ data.tar.gz: deb0ed0dbf6537d1af22cb89fabae0d1f2c82565c21e5cf4a588be70c2dcc79e1e83c8695aa3188105d7fed2b08e794b03d0511c9819625d064ea7544d76999c
@@ -378,6 +378,7 @@ module Labimotion
378
378
  requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass]
379
379
  requires :id, type: Integer, desc: 'Klass ID'
380
380
  requires :properties_template, type: Hash
381
+ optional :metadata, type: Hash, default: {}
381
382
  optional :release, type: String, default: 'draft', desc: 'release status', values: %w[draft major minor patch]
382
383
  end
383
384
  after_validation do
@@ -473,7 +474,7 @@ module Labimotion
473
474
  detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels,
474
475
  policy: @element_policy
475
476
  ),
476
- attachments: attach_thumbnail(element&.attachments)
477
+ attachments: Entities::AttachmentEntity.represent(element&.attachments)
477
478
  }
478
479
  rescue StandardError => e
479
480
  Labimotion.log_exception(e, current_user)
@@ -516,7 +517,7 @@ module Labimotion
516
517
  element,
517
518
  detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels,
518
519
  ),
519
- attachments: attach_thumbnail(element&.attachments),
520
+ attachments: Entities::AttachmentEntity.represent(element&.attachments),
520
521
  }
521
522
  rescue StandardError => e
522
523
  Labimotion.log_exception(e, current_user)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # app/api/labimotion/central_api.rb
2
4
  module Labimotion
3
5
  class LabimotionAPI < Grape::API
@@ -11,4 +13,4 @@ module Labimotion
11
13
  mount Labimotion::StandardLayerAPI
12
14
  mount Labimotion::VocabularyAPI
13
15
  end
14
- end
16
+ end
@@ -11,24 +11,24 @@ module Labimotion
11
11
 
12
12
  namespace :labimotion_hub do
13
13
  namespace :list do
14
- desc "get active generic templates"
14
+ desc 'get active generic templates'
15
15
  params do
16
- requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass]
16
+ requires :klass, type: String, desc: 'Klass', values: Labimotion::Constants::Klass::ALL
17
17
  optional :with_props, type: Boolean, desc: 'With Properties', default: false
18
18
  end
19
19
  get do
20
20
  list = "Labimotion::#{params[:klass]}".constantize.where(is_active: true).where.not(released_at: nil)
21
- list = list.where(is_generic: true) if params[:klass] == 'ElementKlass'
22
- entities = Labimotion::GenericPublicEntity.represent(list, displayed: params[:with_props], root: 'list')
21
+ list = list.where(is_generic: true) if params[:klass] == Labimotion::Constants::Klass::ELEMENT
22
+ Labimotion::GenericPublicEntity.represent(list, displayed: params[:with_props], root: 'list')
23
23
  rescue StandardError => e
24
24
  Labimotion.log_exception(e, current_user)
25
25
  []
26
26
  end
27
27
  end
28
28
  namespace :fetch do
29
- desc "get active generic templates"
29
+ desc 'get active generic templates'
30
30
  params do
31
- requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass]
31
+ requires :klass, type: String, desc: 'Klass', values: Labimotion::Constants::Klass::ALL
32
32
  requires :origin, type: String, desc: 'origin'
33
33
  requires :identifier, type: String, desc: 'Identifier'
34
34
  end
@@ -43,13 +43,16 @@ module Labimotion
43
43
  end
44
44
 
45
45
  namespace :element_klasses_name do
46
- desc "get klasses"
46
+ desc 'get klasses'
47
47
  params do
48
- optional :generic_only, type: Boolean, desc: "list generic element only"
48
+ optional :generic_only, type: Boolean, desc: 'list generic element only'
49
49
  end
50
50
  get do
51
- list = Labimotion::ElementKlass.where(is_active: true) if params[:generic_only].present? && params[:generic_only] == true
52
- list = Labimotion::ElementKlass.where(is_active: true) unless params[:generic_only].present? && params[:generic_only] == true
51
+ if params[:generic_only].present? && params[:generic_only] == true
52
+ list = Labimotion::ElementKlass.where(is_active: true, is_generic: true)
53
+ else
54
+ list = Labimotion::ElementKlass.where(is_active: true)
55
+ end
53
56
  list.pluck(:name)
54
57
  rescue StandardError => e
55
58
  Labimotion.log_exception(e, current_user)
@@ -57,6 +60,76 @@ module Labimotion
57
60
  end
58
61
  end
59
62
 
63
+ namespace :submit do
64
+ desc 'submit a template'
65
+ params do
66
+ requires :klass, type: String, desc: 'klass of the template',
67
+ values: Labimotion::Constants::Klass::ALL
68
+ requires :id, type: Integer, desc: 'template revision id'
69
+ requires :contact_email, type: String, desc: 'email of the submitter', regexp: URI::MailTo::EMAIL_REGEXP
70
+ requires :application, type: String, desc: 'application for the template'
71
+ requires :message, type: String, desc: 'message of the submission'
72
+ end
73
+ post do
74
+ klass = params[:klass]
75
+ template = "Labimotion::#{klass}esRevision".constantize.find(params[:id])
76
+ klass_data = "Labimotion::#{klass}Entity".constantize.represent(template.klass, displayed_in_list: true)
77
+ metadata = {
78
+ klass: {
79
+ klass: klass,
80
+ data: klass_data
81
+ },
82
+ submission: {
83
+ last_name: current_user.last_name,
84
+ first_name: current_user.first_name,
85
+ email: current_user.email,
86
+ id: current_user.id,
87
+ contact_email: params[:contact_email],
88
+ application: params[:application],
89
+ message: params[:message]
90
+ }
91
+ }
92
+
93
+ # Submit the main template
94
+ result = Labimotion::TemplateHub.send_to_central_hub(klass, template.properties_release, metadata,
95
+ request.headers['Origin'])
96
+
97
+ # Increment submitted counter if submission was successful
98
+ template.increment_submitted! if result[:mc] == 'ss00'
99
+
100
+ # For SegmentKlass, also submit the associated ElementKlass
101
+ if klass == Labimotion::Constants::Klass::SEGMENT && result[:mc] == 'ss00'
102
+ element_klass = template.klass.element_klass
103
+ element_klass_data = Labimotion::ElementKlassEntity.represent(element_klass, displayed_in_list: true)
104
+ element_metadata = {
105
+ klass: {
106
+ klass: Labimotion::Constants::Klass::ELEMENT,
107
+ data: element_klass_data
108
+ },
109
+ submission: metadata[:submission].merge(
110
+ associated_submission_id: result[:data][:id]
111
+ )
112
+ }
113
+
114
+ element_result = Labimotion::TemplateHub.send_to_central_hub(Labimotion::Constants::Klass::ELEMENT,
115
+ element_klass.properties_release,
116
+ element_metadata, request.headers['Origin'])
117
+
118
+ # Increment the ElementKlass revision counter if submission was successful
119
+ if element_result[:mc] == 'ss00'
120
+ # Find the revision that matches the ElementKlass UUID
121
+ element_revision = Labimotion::ElementKlassesRevision.find_by(uuid: element_klass.uuid)
122
+ element_revision&.increment_submitted!
123
+ end
124
+ end
125
+
126
+ # Only return the main result; TODO: handle element_result as well
127
+ result
128
+ rescue StandardError => e
129
+ Labimotion.log_exception(e, current_user)
130
+ { mc: 'se00', msg: e.message, data: [] }
131
+ end
132
+ end
60
133
  end
61
134
  end
62
135
  end
@@ -69,7 +69,7 @@ module Labimotion
69
69
 
70
70
  dataset = Labimotion::Dataset.create!(
71
71
  fields.slice(
72
- 'properties', 'properties_release'
72
+ 'properties', 'properties_release', 'metadata'
73
73
  ).merge(
74
74
  ## created_by: current_user_id,
75
75
  element: element,
@@ -112,7 +112,7 @@ module Labimotion
112
112
 
113
113
  segment = Labimotion::Segment.create!(
114
114
  fields.slice(
115
- 'properties', 'properties_release'
115
+ 'properties', 'properties_release', 'metadata'
116
116
  ).merge(
117
117
  created_by: current_user_id,
118
118
  element: element,
@@ -149,7 +149,7 @@ module Labimotion
149
149
 
150
150
  element = Labimotion::Element.create!(
151
151
  fields.slice(
152
- 'name', 'properties', 'properties_release'
152
+ 'name', 'properties', 'properties_release', 'metadata'
153
153
  ).merge(
154
154
  created_by: current_user_id,
155
155
  element_klass: element_klass,
@@ -21,5 +21,12 @@ module Labimotion
21
21
  NMR_CONFIG = ::File.join(__dir__, 'libs', 'data', 'mapper', 'Source.json').freeze
22
22
  WIKI_CONFIG = ::File.join(__dir__, 'libs', 'data', 'mapper', 'Chemwiki.json').freeze
23
23
  end
24
+
25
+ module Klass
26
+ ELEMENT = 'ElementKlass'
27
+ SEGMENT = 'SegmentKlass'
28
+ DATASET = 'DatasetKlass'
29
+ ALL = [ELEMENT, SEGMENT, DATASET].freeze
30
+ end
24
31
  end
25
32
  end
@@ -9,6 +9,7 @@ module Labimotion
9
9
  expose :klass_ols, :klass_label, :klass_uuid
10
10
  expose :properties, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
11
11
  expose :properties_release, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
12
+ expose :metadata, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
12
13
 
13
14
  def klass_ols
14
15
  object&.dataset_klass&.ols_term_id
@@ -16,6 +16,7 @@ module Labimotion
16
16
  expose! :name
17
17
  expose! :properties
18
18
  expose! :properties_release
19
+ expose! :metadata
19
20
  expose! :short_label
20
21
  expose! :thumb_svg
21
22
  expose! :type
@@ -4,7 +4,7 @@ require 'labimotion/entities/application_entity'
4
4
  module Labimotion
5
5
  # ElementRevisionEntity
6
6
  class ElementRevisionEntity < Labimotion::ApplicationEntity
7
- expose :id, :element_id, :uuid, :name, :klass_uuid, :properties, :created_at
7
+ expose :id, :element_id, :uuid, :name, :klass_uuid, :properties, :metadata, :created_at
8
8
  def created_at
9
9
  object.created_at.strftime('%d.%m.%Y, %H:%M')
10
10
  end
@@ -8,6 +8,7 @@ module Labimotion
8
8
 
9
9
  expose :properties_template, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
10
10
  expose :properties_release, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
11
+ expose :metadata, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
11
12
  expose_timestamps(timestamp_fields: %i[released_at created_at updated_at sync_time])
12
13
  end
13
14
  end
@@ -18,6 +18,7 @@ module Labimotion
18
18
  expose! :version
19
19
  expose! :released_at
20
20
  expose :properties_release, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
21
+ expose :metadata, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
21
22
  expose :element_klass do |obj|
22
23
  if obj[:element_klass_id]
23
24
  { label: obj.element_klass.label, icon_name: obj.element_klass.icon_name, id: obj.element_klass_id }
@@ -2,15 +2,13 @@
2
2
 
3
3
  require 'labimotion/entities/application_entity'
4
4
  module Labimotion
5
- # KlassRevisionEntity
6
5
  class KlassRevisionEntity < Labimotion::ApplicationEntity
7
- expose :id, :uuid, :properties_release, :version, :released_at
6
+ expose :id, :uuid, :version, :released_at, :klass_id, :submitted
7
+ expose :properties_release, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
8
+ expose :metadata, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
8
9
 
9
- expose :klass_id do |object|
10
- klass_id = object.element_klass_id if object.respond_to? :element_klass_id
11
- klass_id = object.segment_klass_id if object.respond_to? :segment_klass_id
12
- klass_id = object.dataset_klass_id if object.respond_to? :dataset_klass_id
13
- klass_id
10
+ def klass_id
11
+ object.klass&.id
14
12
  end
15
13
 
16
14
  def released_at
@@ -18,4 +16,3 @@ module Labimotion
18
16
  end
19
17
  end
20
18
  end
21
-
@@ -8,6 +8,7 @@ module Labimotion
8
8
  expose :id, :segment_klass_id, :element_type, :element_id, :uuid, :klass_uuid, :klass_label
9
9
  expose :properties, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
10
10
  expose :properties_release, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
11
+ expose :metadata, **DISPLAYED_IN_LIST_CONDITION, anonymize_with: {}
11
12
 
12
13
  def klass_label
13
14
  object.segment_klass.label
@@ -3,7 +3,7 @@
3
3
  require 'labimotion/entities/application_entity'
4
4
  module Labimotion
5
5
  class SegmentRevisionEntity < Labimotion::ApplicationEntity
6
- expose :id, :segment_id, :uuid, :klass_uuid, :properties, :created_at
6
+ expose :id, :segment_id, :uuid, :klass_uuid, :properties, :metadata, :created_at
7
7
  def created_at
8
8
  object.created_at.strftime('%d.%m.%Y, %H:%M')
9
9
  end
@@ -17,7 +17,7 @@ module Labimotion
17
17
 
18
18
  def create_repo_klass(params, current_user, origin)
19
19
  response = Labimotion::TemplateHub.fetch_identifier('DatasetKlass', params[:identifier], origin)
20
- attributes = response.slice('ols_term_id', 'label', 'desc', 'uuid', 'identifier', 'properties_release', 'version') # .except(:id, :is_active, :place, :created_by, :created_at, :updated_at)
20
+ attributes = response.slice('ols_term_id', 'label', 'desc', 'uuid', 'identifier', 'properties_release', 'version', 'metadata') # .except(:id, :is_active, :place, :created_by, :created_at, :updated_at)
21
21
  attributes['properties_release']['identifier'] = attributes['identifier']
22
22
  attributes['properties_template'] = attributes['properties_release']
23
23
  attributes['place'] = ((Labimotion::DatasetKlass.all.length * 10) || 0) + 10
@@ -82,7 +82,8 @@ module Labimotion
82
82
  klass_uuid: klass[:uuid],
83
83
  properties: properties,
84
84
  properties_release: params[:properties_release],
85
- created_by: current_user.id,
85
+ metadata: params[:metadata] || {},
86
+ created_by: current_user.id
86
87
  }
87
88
  element = Labimotion::Element.new(attributes)
88
89
 
@@ -116,7 +117,8 @@ module Labimotion
116
117
  params.delete(:user_labels)
117
118
  attributes = declared(params.except(:segments), include_missing: false)
118
119
  properties['pkg'] = Labimotion::Utils.pkg(properties['pkg'])
119
- if element.klass_uuid != properties['klass_uuid'] || element.properties != properties || element.name != params[:name]
120
+ metadata = params[:metadata] || element.metadata || {}
121
+ if element.klass_uuid != properties['klass_uuid'] || element.properties != properties || element.name != params[:name] || element.metadata != metadata
120
122
  properties['klass'] = 'Element'
121
123
  uuid = SecureRandom.uuid
122
124
  properties['uuid'] = uuid
@@ -128,6 +130,8 @@ module Labimotion
128
130
  attributes['properties']['uuid'] = uuid
129
131
  attributes['uuid'] = uuid
130
132
  attributes['klass_uuid'] = properties['klass_uuid']
133
+ attributes['metadata'] = metadata
134
+ attributes['updated_at'] = Time.current
131
135
  element.update_columns(attributes)
132
136
  end
133
137
  # element.save_segments(segments: params[:segments], current_user_id: current_user.id)
@@ -333,7 +337,7 @@ module Labimotion
333
337
 
334
338
  def create_repo_klass(params, current_user, origin)
335
339
  response = Labimotion::TemplateHub.fetch_identifier('ElementKlass', params[:identifier], origin)
336
- attributes = response.slice('name', 'label', 'desc', 'icon_name', 'uuid', 'klass_prefix', 'is_generic', 'identifier', 'properties_release', 'version')
340
+ attributes = response.slice('name', 'label', 'desc', 'icon_name', 'uuid', 'klass_prefix', 'is_generic', 'identifier', 'properties_release', 'version', 'metadata')
337
341
  attributes['properties_release']['identifier'] = attributes['identifier']
338
342
  attributes['properties_template'] = attributes['properties_release']
339
343
  attributes['place'] = ((Labimotion::ElementKlass.all.length * 10) || 0) + 10
@@ -60,6 +60,7 @@ module Labimotion
60
60
  properties.delete('eln') if properties['eln'].present?
61
61
  klz.updated_by = current_user.id
62
62
  klz.properties_template = properties
63
+ klz.metadata = params[:metadata] || {}
63
64
  klz.save!
64
65
  klz.reload
65
66
  klz.create_klasses_revision(current_user) if params[:release] != 'draft'
@@ -13,6 +13,7 @@ module Labimotion
13
13
  optional :desc, type: String, desc: 'Klass desc'
14
14
  optional :klass_prefix, type: String, desc: 'Klass klass_prefix'
15
15
  optional :icon_name, type: String, desc: 'Klass icon_name'
16
+ optional :metadata, type: Hash, desc: 'Klass metadata'
16
17
  requires :properties_template, type: Hash, desc: 'Klass template'
17
18
  optional :properties_release, type: Hash, desc: 'Klass release'
18
19
  optional :released_at, type: DateTime, desc: 'Klass released_at'
@@ -29,6 +30,7 @@ module Labimotion
29
30
  requires :klass_prefix, type: String, desc: 'Element Klass Short Label Prefix'
30
31
  optional :icon_name, type: String, desc: 'Element Klass Icon Name'
31
32
  optional :desc, type: String, desc: 'Element Klass Desc'
33
+ optional :metadata, type: Hash, desc: 'Element Klass metadata'
32
34
  optional :properties_template, type: Hash, desc: 'Element Klass properties template'
33
35
  end
34
36
 
@@ -47,6 +49,7 @@ module Labimotion
47
49
  requires :name, type: String
48
50
  optional :properties, type: Hash
49
51
  optional :properties_release, type: Hash
52
+ optional :metadata, type: Hash
50
53
  optional :collection_id, type: Integer
51
54
  requires :container, type: Hash
52
55
  optional :user_labels, type: Array
@@ -58,6 +61,7 @@ module Labimotion
58
61
  optional :name, type: String
59
62
  requires :properties, type: Hash
60
63
  optional :properties_release, type: Hash
64
+ optional :metadata, type: Hash
61
65
  requires :container, type: Hash
62
66
  optional :user_labels, type: Array
63
67
  optional :segments, type: Array, desc: 'Segments'
@@ -67,6 +71,7 @@ module Labimotion
67
71
  params :upload_segment_klass_params do
68
72
  requires :label, type: String, desc: 'Klass label'
69
73
  optional :desc, type: String, desc: 'Klass desc'
74
+ optional :metadata, type: Hash, desc: 'Klass metadata'
70
75
  requires :properties_template, type: Hash, desc: 'Klass template'
71
76
  optional :properties_release, type: Hash, desc: 'Klass release'
72
77
  optional :released_at, type: DateTime, desc: 'Klass released_at'
@@ -93,6 +98,7 @@ module Labimotion
93
98
  requires :element_klass, type: Integer, desc: 'Element Klass Id'
94
99
  optional :desc, type: String, desc: 'Segment Klass Desc'
95
100
  optional :place, type: String, desc: 'Segment Klass Place', default: '100'
101
+ optional :metadata, type: Hash, desc: 'Klass metadata'
96
102
  optional :properties_template, type: Hash, desc: 'Element Klass properties template'
97
103
  end
98
104
 
@@ -99,7 +99,7 @@ module Labimotion
99
99
 
100
100
  def create_repo_klass(params, current_user, origin)
101
101
  response = Labimotion::TemplateHub.fetch_identifier('SegmentKlass', params[:identifier], origin)
102
- attributes = response.slice('label', 'desc', 'uuid', 'identifier', 'released_at', 'properties_release', 'version')
102
+ attributes = response.slice('label', 'desc', 'uuid', 'identifier', 'released_at', 'properties_release', 'version', 'metadata')
103
103
  attributes['properties_release']['identifier'] = attributes['identifier']
104
104
  attributes['properties_template'] = attributes['properties_release']
105
105
  attributes['place'] = ((Labimotion::SegmentKlass.all.length * 10) || 0) + 10
@@ -222,7 +222,8 @@ module Labimotion
222
222
  element_id: cds.id,
223
223
  properties: props,
224
224
  properties_release: klass.properties_release,
225
- klass_uuid: klass.uuid
225
+ klass_uuid: klass.uuid,
226
+ metadata: klass.metadata
226
227
  )
227
228
  end
228
229
 
@@ -44,7 +44,8 @@ module Labimotion
44
44
  element_id: container.id,
45
45
  properties: props,
46
46
  properties_release: klass.properties_release,
47
- klass_uuid: klass.uuid
47
+ klass_uuid: klass.uuid,
48
+ metadata: klass.metadata
48
49
  )
49
50
  end
50
51
 
@@ -5,9 +5,6 @@ require 'uri'
5
5
  require 'json'
6
6
  require 'date'
7
7
 
8
- # rubocop: disable Metrics/AbcSize
9
- # rubocop: disable Metrics/MethodLength
10
-
11
8
  module Labimotion
12
9
  ## TemplateHub
13
10
  class TemplateHub
@@ -18,12 +15,11 @@ module Labimotion
18
15
  "#{url}api/v1/labimotion_hub/#{api_name}"
19
16
  end
20
17
 
21
-
22
18
  def self.header(opt = {})
23
19
  opt || { timeout: 10, headers: { 'Content-Type' => 'text/json' } }
24
20
  end
25
21
 
26
- def self.handle_response(oat, response) # rubocop: disable Metrics/PerceivedComplexity
22
+ def self.handle_response(oat, response)
27
23
  begin
28
24
  response&.success? ? 'OK' : 'ERROR'
29
25
  rescue StandardError => e
@@ -56,10 +52,32 @@ module Labimotion
56
52
  Labimotion.log_exception(e)
57
53
  error!('Cannot connect to Chemotion Repository', 401)
58
54
  end
55
+
56
+ def self.send_to_central_hub(klass, template, metadata, origin)
57
+ body = {
58
+ template_klass: klass,
59
+ template: template,
60
+ metadata: metadata,
61
+ origin: origin
62
+ }
63
+ response = HTTParty.post(
64
+ Labimotion::TemplateHub.uri('template_submissions'),
65
+ headers: {
66
+ 'Content-Type' => 'application/json',
67
+ 'X-Origin-URL' => origin
68
+ },
69
+ body: body.to_json,
70
+ timeout: 10
71
+ )
72
+
73
+ if [200, 201].include?(response.code)
74
+ parsed_response = JSON.parse(response.body)
75
+ return { mc: 'ss00', data: { id: parsed_response['id'] } }
76
+ end
77
+ { mc: 'se00', msg: "HTTP #{response.code}: #{response.message}", data: {} }
78
+ rescue StandardError => e
79
+ Labimotion.log_exception(e)
80
+ { mc: 'se00', msg: "Connection failure: #{e.message}", data: {} }
81
+ end
59
82
  end
60
83
  end
61
-
62
- # rubocop: enable Metrics/AbcSize
63
- # rubocop: enable Metrics/MethodLength
64
- # rubocop: enable Metrics/ClassLength
65
- # rubocop: enable Metrics/CyclomaticComplexity
@@ -29,29 +29,33 @@ module Labimotion
29
29
  properties: ods.properties,
30
30
  properties_release: ods.properties_release,
31
31
  klass_uuid: ods.klass_uuid,
32
+ metadata: ods.metadata || {}
32
33
  )
33
34
  end
34
35
 
35
- def save_dataset(**args)
36
+ def save_dataset(**dataset_args)
36
37
  return if not_dataset?
37
38
 
38
- klass = Labimotion::DatasetKlass.find_by(id: args[:dataset_klass_id])
39
+ args = dataset_args[:dataset]
40
+ dataset_klass_id = args[:dataset_klass_id]
41
+ klass = Labimotion::DatasetKlass.find_by(id: dataset_klass_id)
39
42
  uuid = SecureRandom.uuid
43
+ metadata = args[:metadata] || {}
40
44
  props = args[:properties]
41
45
  props['pkg'] = Labimotion::Utils.pkg(props['pkg'])
42
46
  props['identifier'] = klass.identifier if klass.identifier.present?
43
47
  props['uuid'] = uuid
44
48
  props['klass'] = 'Dataset'
45
49
  props['klass_uuid'] = klass.uuid
46
- props = Labimotion::VocabularyHandler.update_vocabularies(props, args[:current_user], args[:element])
50
+ props = Labimotion::VocabularyHandler.update_vocabularies(props, dataset_args[:current_user], dataset_args[:element])
47
51
 
48
52
  ds = Labimotion::Dataset.find_by(element_type: self.class.name, element_id: id)
49
- if ds.present? && (ds.klass_uuid != klass.uuid || ds.properties != props)
50
- ds.update!(properties_release: klass.properties_release, uuid: uuid, dataset_klass_id: args[:dataset_klass_id], properties: props, klass_uuid: klass.uuid)
53
+ if ds.present? && (ds.klass_uuid != klass.uuid || ds.properties != props || ds.metadata != metadata)
54
+ ds.update!(properties_release: klass.properties_release, uuid: uuid, dataset_klass_id: dataset_klass_id, properties: props, klass_uuid: klass.uuid, metadata: metadata)
51
55
  end
52
56
  return if ds.present?
53
57
 
54
- Labimotion::Dataset.create!(properties_release: klass.properties_release, uuid: uuid, dataset_klass_id: args[:dataset_klass_id], element_type: self.class.name, element_id: id, properties: props, klass_uuid: klass.uuid)
58
+ Labimotion::Dataset.create!(properties_release: klass.properties_release, uuid: uuid, dataset_klass_id: dataset_klass_id, element_type: self.class.name, element_id: id, properties: props, klass_uuid: klass.uuid, metadata: metadata)
55
59
  end
56
60
 
57
61
  def destroy_datasetable
@@ -9,7 +9,6 @@ module Labimotion
9
9
  before_save :check_identifier
10
10
  end
11
11
 
12
-
13
12
  def check_identifier
14
13
  self.identifier = identifier || SecureRandom.uuid if self.has_attribute?(:identifier)
15
14
  end
@@ -34,7 +33,7 @@ module Labimotion
34
33
  properties_release: properties_release,
35
34
  released_at: DateTime.now,
36
35
  updated_by: current_user&.id,
37
- released_by: current_user&.id,
36
+ released_by: current_user&.id
38
37
  }
39
38
 
40
39
  self.update!(klass_attributes)
@@ -45,7 +44,8 @@ module Labimotion
45
44
  version: version,
46
45
  created_by: updated_by,
47
46
  properties_release: properties_release,
48
- released_at: released_at
47
+ released_at: released_at,
48
+ metadata: metadata || {}
49
49
  }
50
50
  attributes["#{self.class.name.underscore.split('/').last}_id"] = id
51
51
  "#{self.class.name}esRevision".constantize.create(attributes)
@@ -22,7 +22,8 @@ module Labimotion
22
22
  klass_uuid: klass_uuid,
23
23
  properties: properties,
24
24
  ## created_by: user_for_revision&.id,
25
- properties_release: properties_release
25
+ properties_release: properties_release,
26
+ metadata: metadata
26
27
  }
27
28
  attributes["#{Labimotion::Utils.element_name_dc(self.class.name)}_id"] = id
28
29
  attributes['name'] = name if self.class.name == 'Labimotion::Element'
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ # Shared concern for all Klass Revision models
5
+ # Provides a unified interface to access the parent klass object
6
+ module KlassRevision
7
+ extend ActiveSupport::Concern
8
+
9
+ # Returns the associated klass object (ElementKlass, SegmentKlass, or DatasetKlass)
10
+ def klass
11
+ return element_klass if respond_to?(:element_klass)
12
+ return segment_klass if respond_to?(:segment_klass)
13
+ return dataset_klass if respond_to?(:dataset_klass)
14
+
15
+ nil
16
+ end
17
+
18
+ # Increments the submitted counter and saves the record
19
+ def increment_submitted!
20
+ increment!(:submitted)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ ## Metadata Validation Concern
5
+ module MetadataValidation
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ validate :metadata_must_be_hash
10
+
11
+ # Provide default value for metadata to support migrations
12
+ # when the column might not exist yet
13
+ after_initialize :set_metadata_default
14
+ end
15
+
16
+ private
17
+
18
+ def set_metadata_default
19
+ self.metadata ||= {} if has_attribute?(:metadata)
20
+ end
21
+
22
+ def metadata_must_be_hash
23
+ # Skip validation if the metadata column doesn't exist yet (during migrations)
24
+ return unless has_attribute?(:metadata)
25
+
26
+ # Set default if nil
27
+ self.metadata ||= {}
28
+
29
+ return if metadata.is_a?(Hash)
30
+
31
+ errors.add(:metadata, 'must be a hash/object, not an array or other type')
32
+ end
33
+ end
34
+ end
@@ -68,14 +68,14 @@ module Labimotion
68
68
  # props = Labimotion::VocabularyHandler.update_vocabularies(props, current_user, self)
69
69
  segment = Labimotion::Segment.where(element_type: self.class.name, element_id: self.id, segment_klass_id: seg['segment_klass_id']).order(id: :desc).first
70
70
  if segment.present? && (segment.klass_uuid != props['klass_uuid'] || segment.properties != props)
71
- segment.update!(properties_release: klass.properties_release, properties: props, uuid: uuid, klass_uuid: props['klass_uuid'])
71
+ segment.update!(properties_release: klass.properties_release, properties: props, uuid: uuid, klass_uuid: props['klass_uuid'], metadata: seg['metadata'] || {})
72
72
  # segments.push(segment)
73
73
  Labimotion::Segment.where(element_type: self.class.name, element_id: self.id, segment_klass_id: seg['segment_klass_id']).where.not(id: segment.id).destroy_all
74
74
  end
75
75
  next if segment.present?
76
76
 
77
77
  props['klass_uuid'] = klass.uuid
78
- segment = Labimotion::Segment.create!(properties_release: klass.properties_release, segment_klass_id: seg['segment_klass_id'], element_type: self.class.name, element_id: self.id, properties: props, created_by: args[:current_user_id], uuid: uuid, klass_uuid: klass.uuid)
78
+ segment = Labimotion::Segment.create!(properties_release: klass.properties_release, segment_klass_id: seg['segment_klass_id'], element_type: self.class.name, element_id: self.id, properties: props, created_by: args[:current_user_id], uuid: uuid, klass_uuid: klass.uuid, metadata: seg['metadata'] || {})
79
79
  # segments.push(segment)
80
80
  end
81
81
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'labimotion/models/concerns/generic_klass_revisions'
4
4
  require 'labimotion/models/concerns/generic_klass'
5
+ require 'labimotion/models/concerns/metadata_validation'
5
6
 
6
7
  module Labimotion
7
8
  class DatasetKlass < ApplicationRecord
@@ -9,6 +10,7 @@ module Labimotion
9
10
  self.table_name = :dataset_klasses
10
11
  include GenericKlassRevisions
11
12
  include GenericKlass
13
+ include MetadataValidation
12
14
 
13
15
  has_many :datasets, dependent: :destroy, class_name: 'Labimotion::Dataset'
14
16
  has_many :dataset_klasses_revisions, dependent: :destroy, class_name: 'Labimotion::DatasetKlassesRevision'
@@ -1,9 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'labimotion/models/concerns/klass_revision'
4
+ require 'labimotion/models/concerns/metadata_validation'
5
+
3
6
  module Labimotion
4
7
  class DatasetKlassesRevision < ApplicationRecord
5
8
  self.table_name = :dataset_klasses_revisions
6
9
  acts_as_paranoid
7
- has_one :dataset_klass, class_name: 'Labimotion::DatasetKlass'
10
+ include KlassRevision
11
+ include MetadataValidation
12
+ belongs_to :dataset_klass, class_name: 'Labimotion::DatasetKlass'
8
13
  end
9
14
  end
@@ -4,6 +4,7 @@ require 'labimotion/conf'
4
4
  require 'labimotion/models/concerns/generic_klass_revisions'
5
5
  require 'labimotion/models/concerns/generic_klass'
6
6
  require 'labimotion/models/concerns/workflow'
7
+ require 'labimotion/models/concerns/metadata_validation'
7
8
 
8
9
  module Labimotion
9
10
  class ElementKlass < ApplicationRecord
@@ -12,6 +13,7 @@ module Labimotion
12
13
  include GenericKlassRevisions
13
14
  include GenericKlass
14
15
  include Workflow
16
+ include MetadataValidation
15
17
  has_many :elements, dependent: :destroy, class_name: 'Labimotion::Element'
16
18
  has_many :segment_klasses, dependent: :destroy, class_name: 'Labimotion::SegmentKlass'
17
19
  has_many :element_klasses_revisions, dependent: :destroy, class_name: 'Labimotion::ElementKlassesRevision'
@@ -1,14 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'labimotion/models/concerns/workflow'
4
+ require 'labimotion/models/concerns/klass_revision'
5
+ require 'labimotion/models/concerns/metadata_validation'
4
6
 
5
7
  module Labimotion
6
8
  class ElementKlassesRevision < ApplicationRecord
7
9
  acts_as_paranoid
8
10
  self.table_name = :element_klasses_revisions
9
11
  include Workflow
10
- has_one :element_klass, class_name: 'Labimotion::ElementKlass'
11
-
12
+ include KlassRevision
13
+ include MetadataValidation
14
+ belongs_to :element_klass, class_name: 'Labimotion::ElementKlass'
12
15
 
13
16
  def migrate_workflow
14
17
  return if properties_release.nil? || properties_release['flow'].nil?
@@ -3,6 +3,7 @@
3
3
  require 'labimotion/models/concerns/generic_klass_revisions'
4
4
  require 'labimotion/models/concerns/generic_klass'
5
5
  require 'labimotion/models/concerns/workflow'
6
+ require 'labimotion/models/concerns/metadata_validation'
6
7
 
7
8
  module Labimotion
8
9
  class SegmentKlass < ApplicationRecord
@@ -11,6 +12,7 @@ module Labimotion
11
12
  include GenericKlassRevisions
12
13
  include GenericKlass
13
14
  include Workflow
15
+ include MetadataValidation
14
16
  belongs_to :element_klass, class_name: 'Labimotion::ElementKlass'
15
17
  has_many :segments, dependent: :destroy, class_name: 'Labimotion::Segment'
16
18
  has_many :segment_klasses_revisions, dependent: :destroy, class_name: 'Labimotion::SegmentKlassesRevision'
@@ -1,9 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'labimotion/models/concerns/klass_revision'
4
+ require 'labimotion/models/concerns/metadata_validation'
5
+
3
6
  module Labimotion
4
7
  class SegmentKlassesRevision < ApplicationRecord
5
8
  acts_as_paranoid
6
9
  self.table_name = :segment_klasses_revisions
7
- has_one :segment_klass, class_name: 'Labimotion::SegmentKlass'
10
+ include KlassRevision
11
+ include MetadataValidation
12
+ belongs_to :segment_klass, class_name: 'Labimotion::SegmentKlass'
8
13
  end
9
14
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: template_submissions
6
+ #
7
+ # id :bigint not null, primary key
8
+ # template_klass :string not null
9
+ # template :jsonb not null, default: {}
10
+ # metadata :jsonb not null, default: {}
11
+ # origin :string not null
12
+ # state :integer not null, default: 0
13
+ # created_at :datetime not null
14
+ # updated_at :datetime
15
+ # deleted_at :datetime
16
+ #
17
+ # Indexes
18
+ #
19
+ # idx_template_submissions_template (template) USING gin
20
+ # idx_template_submissions_metadata (metadata) USING gin
21
+ #
22
+
23
+ module Labimotion
24
+ class TemplateSubmission < ApplicationRecord
25
+ acts_as_paranoid
26
+
27
+ # State enum
28
+ enum state: {
29
+ pending: 0,
30
+ approved: 1,
31
+ rejected: 2,
32
+ released: 3
33
+ }
34
+
35
+ # Validations
36
+ validates :template_klass, presence: true
37
+ validates :template, presence: true
38
+ validates :metadata, presence: true
39
+ validates :origin, presence: true
40
+
41
+ # Scopes
42
+ scope :by_template_klass, ->(klass) { where(template_klass: klass) }
43
+ scope :by_state, ->(state) { where(state: state) }
44
+ scope :by_origin, ->(origin) { where(origin: origin) }
45
+ scope :by_email, ->(email) { where("metadata->'submission'->>'email' = ?", email) }
46
+ scope :by_contact_email, ->(email) { where("metadata->'submission'->>'contact_email' = ?", email) }
47
+ scope :by_any_email, lambda { |email|
48
+ where("metadata->'submission'->>'email' = ? OR metadata->'submission'->>'contact_email' = ?", email, email)
49
+ }
50
+ scope :recent, -> { order(created_at: :desc) }
51
+ end
52
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  ## Labimotion Version
4
4
  module Labimotion
5
- VERSION = '2.1.0.rc13'
5
+ VERSION = '2.1.0.rc15'
6
6
  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.1.0.rc13
4
+ version: 2.1.0.rc15
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: 2025-11-28 00:00:00.000000000 Z
12
+ date: 2026-01-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: caxlsx
@@ -120,7 +120,9 @@ files:
120
120
  - lib/labimotion/models/concerns/generic_klass.rb
121
121
  - lib/labimotion/models/concerns/generic_klass_revisions.rb
122
122
  - lib/labimotion/models/concerns/generic_revisions.rb
123
+ - lib/labimotion/models/concerns/klass_revision.rb
123
124
  - lib/labimotion/models/concerns/linked_properties.rb
125
+ - lib/labimotion/models/concerns/metadata_validation.rb
124
126
  - lib/labimotion/models/concerns/segmentable.rb
125
127
  - lib/labimotion/models/concerns/workflow.rb
126
128
  - lib/labimotion/models/dataset.rb
@@ -145,6 +147,7 @@ files:
145
147
  - lib/labimotion/models/segments_revision.rb
146
148
  - lib/labimotion/models/std_layer.rb
147
149
  - lib/labimotion/models/std_layers_revision.rb
150
+ - lib/labimotion/models/template_submission.rb
148
151
  - lib/labimotion/models/vocabulary.rb
149
152
  - lib/labimotion/models/wellplate.rb
150
153
  - lib/labimotion/utils/con_state.rb