labimotion 2.1.0.rc13 → 2.2.0.rc1

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/labimotion/apis/generic_element_api.rb +1 -0
  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/constants.rb +7 -0
  6. data/lib/labimotion/entities/dataset_entity.rb +1 -0
  7. data/lib/labimotion/entities/element_entity.rb +1 -0
  8. data/lib/labimotion/entities/element_revision_entity.rb +1 -1
  9. data/lib/labimotion/entities/generic_klass_entity.rb +1 -0
  10. data/lib/labimotion/entities/generic_public_entity.rb +1 -0
  11. data/lib/labimotion/entities/klass_revision_entity.rb +5 -8
  12. data/lib/labimotion/entities/segment_entity.rb +1 -0
  13. data/lib/labimotion/entities/segment_revision_entity.rb +1 -1
  14. data/lib/labimotion/helpers/dataset_helpers.rb +1 -1
  15. data/lib/labimotion/helpers/element_helpers.rb +2 -1
  16. data/lib/labimotion/helpers/generic_helpers.rb +1 -0
  17. data/lib/labimotion/helpers/param_helpers.rb +2 -0
  18. data/lib/labimotion/helpers/segment_helpers.rb +1 -1
  19. data/lib/labimotion/libs/template_hub.rb +28 -10
  20. data/lib/labimotion/models/concerns/datasetable.rb +8 -5
  21. data/lib/labimotion/models/concerns/generic_klass_revisions.rb +2 -1
  22. data/lib/labimotion/models/concerns/generic_revisions.rb +2 -1
  23. data/lib/labimotion/models/concerns/klass_revision.rb +23 -0
  24. data/lib/labimotion/models/concerns/metadata_validation.rb +34 -0
  25. data/lib/labimotion/models/concerns/segmentable.rb +2 -2
  26. data/lib/labimotion/models/dataset_klass.rb +2 -0
  27. data/lib/labimotion/models/dataset_klasses_revision.rb +6 -1
  28. data/lib/labimotion/models/element_klass.rb +2 -0
  29. data/lib/labimotion/models/element_klasses_revision.rb +5 -2
  30. data/lib/labimotion/models/segment_klass.rb +2 -0
  31. data/lib/labimotion/models/segment_klasses_revision.rb +6 -1
  32. data/lib/labimotion/models/template_submission.rb +52 -0
  33. data/lib/labimotion/version.rb +1 -1
  34. 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: 4397eed28e8f2c6f0e21e022191d842dbb7a42000a2efe72731a89ab8bf9e77b
4
+ data.tar.gz: 3ae429541da0f787d6d0f544307b5acfbf3e922d83c7fc7cd27e47738eb5880e
5
5
  SHA512:
6
- metadata.gz: 52896a77e3e27514c0216b081e1899878097f313dab59051bd6a2916752f50bdf4b55d11dbf87c08484c8c4a8e298415dcffe2257b6fd8585ec09de7f064931c
7
- data.tar.gz: 5e870746f14902a8e23dd42be15c18f0eb798cc313428f917e8dd8d06ccc0b601e04bae967ffd5a008274e2f1542f7c292e28e228846c5a1b70fed027b68a62c
6
+ metadata.gz: 12a5d77d600daaf1a45640ab5ff7d253a608cd683fac28a5286019a7990e85cb7c1f0c957240fab625d3dbb703196dcb918b652bcc86457c6ac88cc085990ea0
7
+ data.tar.gz: 28453642570e3e177c0c2684590cdb715016c4043645d9f2e097eca0a4f41e9e3ff30564df8a0b7d5800bfb36a2fd224aa40cb91a107a2bc53adddc1db133070
@@ -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
@@ -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
@@ -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
@@ -128,6 +128,7 @@ module Labimotion
128
128
  attributes['properties']['uuid'] = uuid
129
129
  attributes['uuid'] = uuid
130
130
  attributes['klass_uuid'] = properties['klass_uuid']
131
+ attributes['updated_at'] = Time.current
131
132
  element.update_columns(attributes)
132
133
  end
133
134
  # element.save_segments(segments: params[:segments], current_user_id: current_user.id)
@@ -333,7 +334,7 @@ module Labimotion
333
334
 
334
335
  def create_repo_klass(params, current_user, origin)
335
336
  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')
337
+ attributes = response.slice('name', 'label', 'desc', 'icon_name', 'uuid', 'klass_prefix', 'is_generic', 'identifier', 'properties_release', 'version', 'metadata')
337
338
  attributes['properties_release']['identifier'] = attributes['identifier']
338
339
  attributes['properties_template'] = attributes['properties_release']
339
340
  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'
@@ -47,6 +47,7 @@ module Labimotion
47
47
  requires :name, type: String
48
48
  optional :properties, type: Hash
49
49
  optional :properties_release, type: Hash
50
+ optional :metadata, type: Hash
50
51
  optional :collection_id, type: Integer
51
52
  requires :container, type: Hash
52
53
  optional :user_labels, type: Array
@@ -58,6 +59,7 @@ module Labimotion
58
59
  optional :name, type: String
59
60
  requires :properties, type: Hash
60
61
  optional :properties_release, type: Hash
62
+ optional :metadata, type: Hash
61
63
  requires :container, type: Hash
62
64
  optional :user_labels, type: Array
63
65
  optional :segments, type: Array, desc: 'Segments'
@@ -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
@@ -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
@@ -32,26 +32,29 @@ module Labimotion
32
32
  )
33
33
  end
34
34
 
35
- def save_dataset(**args)
35
+ def save_dataset(**dataset_args)
36
36
  return if not_dataset?
37
37
 
38
- klass = Labimotion::DatasetKlass.find_by(id: args[:dataset_klass_id])
38
+ args = dataset_args[:dataset]
39
+ dataset_klass_id = args[:dataset_klass_id]
40
+ klass = Labimotion::DatasetKlass.find_by(id: dataset_klass_id)
39
41
  uuid = SecureRandom.uuid
42
+ metadata = args[:metadata] || {}
40
43
  props = args[:properties]
41
44
  props['pkg'] = Labimotion::Utils.pkg(props['pkg'])
42
45
  props['identifier'] = klass.identifier if klass.identifier.present?
43
46
  props['uuid'] = uuid
44
47
  props['klass'] = 'Dataset'
45
48
  props['klass_uuid'] = klass.uuid
46
- props = Labimotion::VocabularyHandler.update_vocabularies(props, args[:current_user], args[:element])
49
+ props = Labimotion::VocabularyHandler.update_vocabularies(props, dataset_args[:current_user], dataset_args[:element])
47
50
 
48
51
  ds = Labimotion::Dataset.find_by(element_type: self.class.name, element_id: id)
49
52
  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
+ ds.update!(properties_release: klass.properties_release, uuid: uuid, dataset_klass_id: dataset_klass_id, properties: props, klass_uuid: klass.uuid)
51
54
  end
52
55
  return if ds.present?
53
56
 
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)
57
+ 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
58
  end
56
59
 
57
60
  def destroy_datasetable
@@ -45,7 +45,8 @@ module Labimotion
45
45
  version: version,
46
46
  created_by: updated_by,
47
47
  properties_release: properties_release,
48
- released_at: released_at
48
+ released_at: released_at,
49
+ metadata: metadata
49
50
  }
50
51
  attributes["#{self.class.name.underscore.split('/').last}_id"] = id
51
52
  "#{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.2.0.rc1'
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.2.0.rc1
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: 2025-12-25 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